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

收藏待读

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

很多面對象語言中都有裝飾器(Decorator)函數的概念,Javascript語言的ES7標準中也提及了Decorator,個人認為裝飾器是和 async/await 一樣讓人興奮的的變化。正如其「裝飾器」的叫法所表達的,他可以對一些對象進行裝飾包裝然後返回一個被包裝過的對象,可以裝飾的對象包括:類,屬性,方法等。

Node.js目前已經支持了 async/await 語法,但 decorator 還需要babel的插件支持,具體的配置不在敘述。(截至發稿時間2018-12-29)

下面是引用的關於 decorator 語法的一個示例:

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

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

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

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

期望效果

關於Node.js中的路由,大家應該都很熟悉了,無論是在自己寫的 http/https 服務中,還是在 ExpressKoa 等框架中。我們要為路由提供請求的 URL 和其他需要的 GETPOST 等參數,隨後路由需要根據這些數據來執行相應的代碼。

關於Decorator和路由的結合我們這次希望寫出類似下面的代碼:

@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。 第八行,路由真正執行的方法。

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

##具體實現

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

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

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 打印日誌,參考上面的預期示例。

關於 mongodb 以及獲取數據的代碼這裡就不貼出了,畢竟只是示例而已,大家可以根據自己的資源,自行修改為自己的邏輯。

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

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

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 函數也很簡單,把接受到的參數 path 做格式化處理,把 callback 函數包裝成數組,之後與 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}`);
})

複製代碼

原文 : 稀土掘金

相關閱讀

免责声明:本文内容来源于稀土掘金,已注明原文出处和链接,文章观点不代表立场,如若侵犯到您的权益,或涉不实谣言,敬请向我们提出检举。