不少面对象语言中都有装饰器(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}`); })