不少面對象語言中都有裝飾器(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
關於Node.js中的路由,你們應該都很熟悉了,不管是在本身寫的http/https
服務中,仍是在Express
、Koa
等框架中。咱們要爲路由提供請求的URL
和其餘須要的GET
及POST
等參數,隨後路由須要根據這些數據來執行相應的代碼。
關於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
方法添加路徑和請求方法。
第四行,經過裝飾器限制發起請求須要用戶登陸。
第五行,經過裝飾器限制發起請求的用戶必須擁有開發者或者網站管理員權限。
第六行,經過裝飾器檢查請求參數必須包含phone
和password
字段。
第七行,經過裝飾器爲請求打印log。
第八行,路由真正執行的方法。
這樣不只簡化、規範化了路由的寫法,減小了代碼的冗餘和錯誤,還使代碼含義一目瞭然,無需註釋也能通俗易懂,便於維護、交接等事宜。shell
下面就着手寫一個關於movies
的路由具體實例,示例採用koa2
+ koa-router
爲基礎組織代碼。數組
文件路徑:/server/routers/movies.js
babel
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.js
less
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
類的構造函數接收兩個參數app
和routesPath
,app
即爲koa2
實例,routesPath
爲路由文件路徑,如上面movies.js
的routesPath
爲/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
函數也很簡單把接受到的路徑格式化處理,把路由處理函數包裝成數組,以後與target
、method
一塊兒構造城對象塞入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, );
接下來是get
、post
、put
、delete
方法的具體實現,其實就是調用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}`); })