内容简介:很多面对象语言中都有装饰器(Decorator)函数的概念,Javascript语言的ES7标准中也提及了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 的详细介绍参见下面两篇文章:
期望效果
关于Node.js中的路由,大家应该都很熟悉了,无论是在自己写的 http/https
服务中,还是在 Express
、 Koa
等框架中。我们要为路由提供请求的 URL
和其他需要的 GET
及 POST
等参数,随后路由需要根据这些数据来执行相应的代码。
关于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
方法添加路径和请求方法。
第四行,通过装饰器限制发起请求需要用户登录。
第五行,通过装饰器限制发起请求的用户必须拥有开发者或者网站管理员权限。
第六行,通过装饰器检查请求参数必须包含 phone
和 password
字段。
第七行,通过装饰器为请求打印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
类的构造函数接收两个参数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
函数也很简单,把接受到的参数path
做格式化处理,把callback
函数包装成数组,之后与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}`); }) 复制代码
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- AI与云原生,技术圈最火热的搭档
- vue路由篇(动态路由、路由嵌套)
- 小程序封装路由文件和路由方法,5种路由方法全解析
- Vue的路由及路由钩子函数
- gin 源码阅读(二)-- 路由和路由组
- vue router 路由鉴权(非动态路由)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Programming Ruby
Dave Thomas、Chad Fowler、Andy Hunt / Pragmatic Bookshelf / 2004-10-8 / USD 44.95
Ruby is an increasingly popular, fully object-oriented dynamic programming language, hailed by many practitioners as the finest and most useful language available today. When Ruby first burst onto the......一起来看看 《Programming Ruby》 这本书的介绍吧!