黃金搭檔 -- JS 裝飾器(Decorator)與Node.js路由

不少面對象語言中都有裝飾器(Decorator)函數的概念,Javascript語言的ES7標準中也說起了Decorator,我的認爲裝飾器是和async/await同樣讓人興奮的的變化。正如其「裝飾器」的叫法所表達的,他能夠對一些對象進行裝飾包裝而後返回一個被包裝過的對象,能夠裝飾的對象包括:類,屬性,方法等。
Node.js目前已經支持了async/await語法,但decorator還須要babel插件支持,具體的配置不在敘述。(截至發稿時間2018-12-29)
下面是引用的關於decorator語法的一個示例:git

@testable
class Person {
  @readonly
  @nonenumerable
  name() { return `${this.first} ${this.last}` }
}

從上面代碼中,咱們一眼就能看出,Person類是可測試的,而name方法是隻讀和不可枚舉的。es6

關於 Decorator 的詳細介紹參見下面兩篇文章:github

  1. 阮一峯《ECMAScript 6 入門》 -- Decorator
  2. 知乎 -- 《Decorators in ES7》

指望效果

關於Node.js中的路由,你們應該都很熟悉了,不管是在本身寫的http/https服務中,仍是在ExpressKoa等框架中。咱們要爲路由提供請求的URL和其餘須要的GETPOST等參數,隨後路由須要根據這些數據來執行相應的代碼。
關於Decorator和路由的結合咱們此次但願寫出相似下面的代碼:mongodb

@Controller('/tags')
export default class TagRouter {
  @Get(':/id')
  @Login
  @admin(['developer', 'adminWebsite'])
  @require(['phone', 'password'])
  @Log
  async getTagDetail(ctx, next) {
    //...
  }
}
關於這段代碼的解釋:
第一行,經過 Controller裝飾 TagRouter類,爲類下的路由函數添加統一路徑前綴 /tags
第二行,建立並導出 TagRouter類。
第三行,經過裝飾器爲 getTagDetail方法添加路徑和請求方法。
第四行,經過裝飾器限制發起請求須要用戶登陸。
第五行,經過裝飾器限制發起請求的用戶必須擁有開發者或者網站管理員權限。
第六行,經過裝飾器檢查請求參數必須包含 phonepassword字段。
第七行,經過裝飾器爲請求打印log。
第八行,路由真正執行的方法。

這樣不只簡化、規範化了路由的寫法,減小了代碼的冗餘和錯誤,還使代碼含義一目瞭然,無需註釋也能通俗易懂,便於維護、交接等事宜。shell

具體實現

下面就着手寫一個關於movies的路由具體實例,示例採用koa2 + koa-router爲基礎組織代碼。數組

文件路徑:/server/routers/movies.jsbabel

import mongoose from 'mongoose';

import { Controller, Get, Log } from '../decorator/router';
import { getAllMovies, getSingleMovie, getRelativeMovies } from '../service/movie';

@Controller('/movies')
export default class MovieRouter {
  @Get('/all')
  @Log
  async getMovieList(ctx, next) {
    const type = ctx.query.type;
    const year = ctx.query.year;

    const movies = await getAllMovies(type, year);

    ctx.body = {
      data: movies,
      success: true,
    };
  }

  @Get('/detail/:id')
  @Log
  async getMovieDetail(ctx, next) {
    const id = ctx.params.id;
    const movie = await getSingleMovie(id);
    const relativeMovies = await getRelativeMovies(movie);

    ctx.body = {
      data: {
        movie,
        relativeMovies,
      },
      success: true,
    }
  }
}

代碼中Controller爲路由添加統一前綴,Get指定請求方法和路徑,Log打印日誌,參考上面的預期示例。app

關於 mongodb以及獲取數據的代碼這裏就不貼出了,畢竟只是示例而已,你們能夠根據本身的資源,自行修改成本身的邏輯。

重點咱們看一下,GET /movies/all以及GET /movies//detail/:id這兩個路由的裝飾器實現。框架

文件路徑:/server/decorator/router.jsless

import KoaRouter from 'koa-router';
import { resolve } from 'path';
import glob from 'glob'; // 使用shell模式匹配文件

export class Route {
  constructor(app, routesPath) {
    this.app = app;
    this.router = new KoaRouter();
    this.routesPath = routesPath;
  }

  init = () => {
    const {app, router, routesPath} = this;
    glob.sync(resolve(routesPath, './*.js')).forEach(require);
    // 具體處理邏輯
    app.use(router.routes());
    app.use(router.allowedMethods());
  }
};
  • 首先,導出一個Route類,提供給外部使用,Route類的構造函數接收兩個參數approutesPathapp即爲koa2實例,routesPath爲路由文件路徑,如上面movies.jsroutesPath/server/routers/
  • 而後,提供一個初始化函數init,初始化邏輯中。引用全部routesPath下的路由,並use路由實例。

這樣的話咱們就能夠在外部這樣調用Route類:

import {Route} from '../decorator/router';
import {resolve} from 'path';

export const router = (app) => {
  const routesPath = resolve(__dirname, '../routes');
  const instance = new Route(app, routesPath);

  instance.init();
}

好了,基本框架搭好了,來看具體邏輯的實現。

先補充完init方法:

文件路徑:/server/decorator/router.js

const pathPrefix = Symbol('pathPrefix');

  init = () => {
    const {app, router, routesPath} = this;
    glob.sync(resolve(routesPath, './*.js')).forEach(require);

    R.forEach( // R爲'ramda'方法庫,相似'lodash'
      ({target, method, path, callback}) => {
        const prefix = resolvePath(target[pathPrefix]);
        router[method](prefix + path, ...callback);
      }
    )(routeMap)
    
    app.use(router.routes());
    app.use(router.allowedMethods());
  }

爲了加載路由,須要一個路由列表routeMap,而後遍歷routeMap,掛載路由,init工做就完成了。
下邊的重點就是向routeMap中塞入數據,這裏每一個路由對象採用object的形式有四個key,分別爲target, method, path, callback

target即爲裝飾器函數的 target(這裏主要爲了獲取路由路徑的前綴), method爲請求方法, path爲請求路徑, callback爲請求執行的函數。

下邊是設置路由路徑前綴和塞入routeMap內容的裝飾器函數:

export const Controller = path => (target, key, descriptor) => {
  target.prototype[pathPrefix] = path;
  return descriptor;
}

export const setRouter = method => path => (target, key, descriptor) => {
  routeMap.push({
    target,
    method,
    path: resolvePath(path),
    callback: changeToArr(target[key]),
  });
  return descriptor;
}
  • Controller就很少說了,就是掛載前綴路徑到類的原型對象上,這裏須要注意的是Controller做用於類,因此target是被修飾的類自己。
  • setRouter函數也很簡單把接受到的路徑格式化處理,把路由處理函數包裝成數組,以後與targetmethod一塊兒構造城對象塞入routeMap

這裏有兩個輔助函數,簡單貼下代碼看下:

import R from 'ramda'; // 相似'lodash'的方法庫

// 若是路徑是以/開頭直接返回,不然補充/後返回
const resolvePath = R.unless(
  R.startsWith('/'),
  R.curryN(2, R.concat)('/'),
);

// 若是參數是函數直接返回,不然包裝成數組返回
const changeToArr = R.unless(
  R.is(Array),
  R.of,
);

接下來是getpostputdelete方法的具體實現,其實就是調用setRouter就好了:

export const Get = setRouter('get');

export const Post = setRouter('post');

export const Put = setRouter('put');

export const Delete = setRouter('delete');

至此,主要的功能就所有實現了,接下來是一些輔助Decorator,你們能夠參考和使用core-decorators.js,它是一個第三方模塊,提供了幾個常見的修飾器,經過它也能夠更好地理解修飾器。

下面以Log爲示例,實現一個輔助Decorator,其餘Decorator你們本身發揮:

let logTimes = 0;

export const convert = middleware => (target, key, descriptor) => {
  target[key] = R.compose(
    R.concat(
      changeToArr(middleware)
    ),
    changeToArr,
  )(target[key]);

  return descriptor;
}

export const Log = convert(async (ctx, next) => {
  logTimes++;
  console.time(`${logTimes}: ${ctx.method} - ${ctx.url}`);
  await next();
  console.timeEnd(`${logTimes}: ${ctx.method} - ${ctx.url}`);
})
convert是一個輔助函數,首先把普通函數轉換成數組,而後跟其餘中間件函數合併。此輔助函數也可用於其餘輔助Decorator。

好了,到此文章就結束了,你們多交流,本人github
下一篇:分享koa2源碼解讀

最後貼出關鍵的/server/decorator/router.js的完整代碼

import R from 'ramda';
import KoaRouter from 'koa-router';
import glob from 'glob';
import {resolve} from 'path';

const pathPrefix = Symbol('pathPrefix')
const routeMap = [];
let logTimes = 0;

const resolvePath = R.unless(
  R.startsWith('/'),
  R.curryN(2, R.concat)('/'),
);

const changeToArr = R.unless(
  R.is(Array),
  R.of,
);

export class Route {
  constructor(app, routesPath) {
    this.app = app;
    this.router = new KoaRouter();
    this.routesPath = routesPath;
  }

  init = () => {
    const {app, router, routesPath} = this;
    glob.sync(resolve(routesPath, './*.js')).forEach(require);

    R.forEach(
      ({target, method, path, callback}) => {
        const prefix = resolvePath(target[pathPrefix]);
        router[method](prefix + path, ...callback);
      }
    )(routeMap)
    app.use(router.routes());
    app.use(router.allowedMethods());
  }
};

export const Controller = path => (target, key, descriptor) => {
  console.log(target);
  target.prototype[pathPrefix] = path;
  return descriptor;
}

export const setRouter = method => path => (target, key, descriptor) => {
  console.log('setRouter');
  routeMap.push({
    target,
    method,
    path: resolvePath(path),
    callback: changeToArr(target[key]),
  });
  return descriptor;
}

export const Get = setRouter('get');

export const Post = setRouter('post');

export const Put = setRouter('put');

export const Delete = setRouter('delete');

export const convert = middleware => (target, key, descriptor) => {
  target[key] = R.compose(
    R.concat(
      changeToArr(middleware)
    ),
    changeToArr,
  )(target[key]);

  return descriptor;
}

export const Log = convert(async (ctx, next) => {
  logTimes++;
  console.time(`${logTimes}: ${ctx.method} - ${ctx.url}`);
  await next();
  console.timeEnd(`${logTimes}: ${ctx.method} - ${ctx.url}`);
})
相關文章
相關標籤/搜索