徐帅武,微医云服务团队前端工程师。一个爱折腾、爱作菜的前端程序猿前端
不少小伙伴使用 Koa
或 Egg
之类框架写接口时必定碰到过下面这种使人头大的写法,每次咱们定义一个路由写完 Controller 方法还要去 router 文件中再次定义一遍,很是的繁琐麻烦。webpack
这种写法当然很是诱人,可是为了一个写法去切换框架的代价是很是大的,那么咱们在 Koa
或 Egg
中可使用这种方法吗?或者说能够保持原有写法的同时渐进加强的使用装饰器来定义路由吗?答案是确定的,装饰器路由写法的实现其实很是简单,各位看官且往下看。git
装饰器实际上是一种语法糖,定义为一个普通的函数,调用时写成 @ + 函数名
。它能够放在类和类方法的定义前面。类与类方法的参数有所不一样。github
类的装饰器能够用来装饰整个类:web
@testable
class MyTestableClass { // ... } function testable(target) { target.isTestable = true; } MyTestableClass.isTestable // true // 此处代码来自阮一峰 ES6 入门教程 复制代码
传入的 target 参数就是类自己,若是装饰器函数有返回值就用返回值替换这个类。基本行为就是下面这样:正则表达式
@decorator
class A {} // 等同于 class A {} A = decorator(A) || A; 复制代码
类方法装饰器其实和 Object.defineProperty
方法很是像,三个参数分别是:npm
target
: 类对象其实就是 "类" 的 prototype 对象,它上面有个
constructor
属性指向类自己
name
:装饰属性的名称
descriptor
:属性的描述符,这个和
Object.defineProperty
方法是一致的
属性描述符的具体属性和描述能够看这里 descriptor。前端工程师
function foo(target, name, descriptor){
} 复制代码
装饰器须要传参时咱们能够建立一个高阶函数,这个函数返回一个装饰器函数就能够实现传参的目的了。app
function foo (url) {
return function (target, name, descriptor) { console.log(url) } } class Bar { @foo('/test') baz () { } } 复制代码
Reflect(反射)是 ES6 为了操做对象而提供的新 API,这里咱们要用到 MetaData 相关的 API 来为对象绑定一些路由数据,用来生成最终的路由文件。这个 API 目前尚未进入正式版本须要引入 reflect-metadata
这个库来支持相关 API,详情参见这里 reflect-metadata,咱们主要使用到下面两个 API:框架
// 设置元数据
Reflect.defineMetadata(metadataKey, metadataValue, target); // 获取设置的值 let result = Reflect.getMetadata(metadataKey, target); 复制代码
最终目标就是要生成 Koa
或 Egg
所须要的 router 配置,这里咱们拿 Koa 举例,须要的就是相似于下面这样一份配置,因此咱们的目标就是拿到建立下面这样一份文件,因此思路就是在 装饰器函数中能够拿到类方法 ,经过 reflect-metadata 能够在每一个方法上写入 路径、请求类型 等元数据,因此只须要统一对外提供一个注册的方法就能够把使用装饰器设置的路径和函数注册在 router 对象上,这样就完成了路由自动注册的过程。
const app = new Koa();
const router = new Router(); router.get('/user/info', UserInfoController); router.get('/user/list', UserListController); router.post('/user/create', UserCreateController); app.use(router.routes()) 复制代码
通常来讲某个 Class 对应的接口都会有一个统一的前缀,因此咱们定义一个 Controller 方法用来存储公共路径。
/** * Controller 装饰器 * 用来装饰 Controller 类 * * @param {string} [baseUrl=''] 类的公共前缀 * @returns * @memberof Decorator */ Controller (baseUrl = '') { return (target) => { Reflect.defineMetadata(BASE_URL, baseUrl, target) } } 复制代码
由于 koa-router 中注册 Get、Post 之类的方法参数都相同,因此装饰器能够注册一个通用的方法来生成各个方法的装饰器,代码以下:
/** * 用来生成各类方法装饰器的工具函数 * * @param {*} method * @memberof Decorator */ createMethodDecorator (method) { return (url) => { return (target, name, decorator) => { // target 为装饰方法所在的类 // 由于类方法的装饰器会比类的装饰器先执行, 在这个阶段拿不到 Controller 类的公共前缀 // 因此要存下 target 后面再根据所存的信息生成 router this.controllerList.add(target) // decorator.value 为装饰的函数自己 Reflect.defineMetadata(METHOD, method, decorator.value) // 没有指定请求的 url 就是用函数名做为 url Reflect.defineMetadata(METHOD_URL, url || name, decorator.value) } } } 复制代码
有了路由信息后就须要将全部的路由信息注册到 router 对象上来完成路由的注册,咱们遍历存储起来的全部 controller 类,而后获取到方法上面对应的路由信息来进行注册:
/** * 注册路由 * * @param {*} router Koa Router 对象 * @memberof Decorator */ registerRouter (router) { for (const controller of this.controllerList) { // 获取类构造函数,就是类装饰器中的 target 参数 const controllerCtor = controller.constructor const baseUrl = Reflect.getMetadata(BASE_URL, controllerCtor) || '' // 获取类对象上的全部属性 const allProps = Object.getOwnPropertyNames(controller) for (const props of allProps) { const handle = controller[props] // 遍历全部属性中是函数 且存在路由信息的函数 if (typeof handle !== 'function') { continue } const method = Reflect.getMetadata(METHOD, handle) const url = Reflect.getMetadata(METHOD_URL, handle) if (method && url && router[method]) { // 由于是 demo 暂时不校验和转换各个 url 的格式 // 实际使用中须要将三个路径拼接为合法的 url 格式 const completeUrl = this.prefix + baseUrl + url // 把接口路径和函数注册到 router 对象上 router[method](completeUrl, handle) } } } } 复制代码
最后咱们还要将全部的 Controller 文件加载进来,为了不手写,咱们建立一个 load 函数来自动加载全部的 Controller 文件。
这里咱们用到了 requireContext
函数,使用 webpack 打包的话这个方法为 require.context()
是默承认用的。若是没有使用 webpack 的话就须要手动引入 require-context
这个 npm 包来使用。
requireContext
这个方法有三个参数分别为:搜索的目录、是否搜索子文件夹、匹配文件的正则表达式。使用这个方法能够获取全部符合条件的模块。
import requireContext from 'require-context'
export const load = function (path) { const ctx = requireContext(path, true, /\.js$/) ctx.keys().forEach(key => ctx(key)) } // 使用:传入 controller 函数所在文件夹 load(path.resolve(__dirname, './controller')) 复制代码
装饰器的功能很是强大,除了上面的自动注册路由外还能够作更多的事情,好比路由的鉴权、中间件、依赖注入、参数校验、日志等等。
综上咱们实现了一个基于 Koa Router 装饰器路由,Express,Egg 之类的其余框架的实现也是一个道理,不一样框架根据路由注册方法的区别对 registerRouter
函数略加改造便可完成。对应已经存在已久的老项目也可以使用这种方式对新的路由进行装饰器的写法,本身定制 registerRouter
的实现达到渐进加强的效果。
本文实现的代码在这里 【Koa-Decorator-Demo】