引言
在 koa 里使用路由一般都是这样写1
2
3
4const router = requier('koa-router')
router.get('/users', async (ctx, next) => {
ctx.body = 'users'
})
这样的路由设置显得不是那样的直观,而反观 Java 中 Spring 的写法,就感觉更简洁以及更优雅1
2
3
4
5
6
7
public class UserController {
'/users') (
String Users() {
return 'users';
}
}
而在 Nest 中其实已经实现了这样的写法, 因为 nest 是由 Typescript 编写的,TS 本身就支持 Decorator 的写法,再结合 reflect-metadata 来实现内部路由装饰器。
1 | @Controller('users') |
在这里,我们使用原生的 javascript 来实现在 koa 中使用装饰器路由
什么是 Decorator
首先,我们先认识一下 Decorator,它是 es6 新增的函数。
1 | @testable |
上面的代码中,@testable
就是一个修饰器,它为 MyTestableClass
这个类加上了静态属性 isTestable
这是一个最简单的例子,一般情况下,我们是需要给类的实例添加方法,所以需要在 prototype 上添加属性,对 js 的原型链不太熟悉的同学可以先看下这篇文章 详解 Javascript 的原型链与继承(从 ES5 到 ES6) , 同时可以在修饰器上传入参数
类的修饰
1 | function testable(isTestable) { |
方法的修饰
在修饰方法的时候,修饰器函数一共接收三个参数,分别是 target
(类的原型对象),name
(所要修饰的属性名),descriptor
(该属性的描述对象)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29class Boy {
@speak('中文')
show () {
console.log('I can speak ' + this.language)
}
}
function speak (language) {
return function (target, name, descriptor) {
/**
* 修饰器内参数的值如下
* function (
* target: Boy {}
* name: show
* descriptor: {
* value: [Function: show],
* enumerable: true,
* configurable: false,
* writable: true
* }
* )
*/
target.language = language
return descriptor
}
}
const luke = new Boy()
luke.show() // I can speak 中文
这里我们就简要介绍一下 Decorator,如果想要深入了解,可以看下阮一峰老师的 ES6入门
babel 转码
Babel 默认只转换新的 JavaScript 句法(syntax),而不转换新的 API ,比如 Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise 等全局对象,以及一些定义在全局对象上的方法(比如 Object.assign)都不会转码。Babel 默认不转码的 API 非常多,详细清单可以查看 definitions.js 文件
而这里的 Decorator 也同样需要安装 babel 的 plugin。
如果你使用的是 babel6,那么需要安装 transform-decorators-legacy
1 | npm install --save-dev babel-preset-env transform-decorators-legacy |
.babelrc 可以这样配置1
2
3
4{
"presets": ["env"],
"plugins": ["transform-decorators-legacy"]
}
如果你使用的是 babel7,那么需要安装 @babel/plugin-proposal-decorators
1 | npm install --save-dev @babel/preset-env @babel/plugin-proposal-decorators |
1 | { |
之后在项目的启动文件头部,还需要引入 babel-register 和 polyfill1
2
3
4
5
6
7// babel6
require('babel-register')()
require('babel-polyfill')
// babel7
require('@babel/register')()
require('@babel/polyfill')
Koa 中使用装饰器路由
先看看传统的 koa 写法(不带装饰器)1
2
3
4
5
6
7
8
9
10
11const koa = require('koa')
const Router = require('koa-router')
const app = new Koa()
const router = new Router()
router.get('/users', async (ctx, next) => {
ctx.body = 'users'
})
app.use(router.routes()).use(router.allowedMethods())
首先,我们先实现 controller 类的装饰器1
2const symbolPrefix = Symbol('prefix')
const controller = path => target => (target.prototype[symbolPrefix] = path)
这里用到了 ES6 新增的数据类型 Symbol,它表示独一无二的值。由于每一个 Symbol 值都是不相等的,这意味着 Symbol 值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。
接着定义两个工具函数
formatPath
能够格式化路由,使不带 ‘/‘ 的路由带上它isArray
输出数组
1 | const formatPath = path => path.startsWith('/') ? path : `/${path}` |
设置路由的映射关系1
2
3
4
5
6
7
8
9
10
11
12
13
14
15const routerMap = new Map()
/**
* Map({
* target: any // 类的实例
* method: 'GET' // http method
* path: '/list' // 接口路径
* }, routerFunction)
*/
const router = conf => (target, name, descriptor) => {
conf.path = formatPath(conf.path)
routerMap.set({
target,
...conf
}, target[name])
}
设置完后,新建一个 Route 的类,能够接收 Koa 实例和 api 文件夹路径。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25const Router = require('koa-router')
const glob = require('glob')
class Route {
constructor (app, apiPath) {
this.app = app
this.apiPath = apiPath
this.router = new Router()
}
init () {
// 将 api 文件接口全部同步载入
glob.sync(path.resolve(this.apiPath, './*.js')).forEach(require)
for (let [conf, controller] of routerMap) {
const controllers = isArray(controller)
const prefixPath = conf.target[symbolPrefix] ? formatPath(conf.target[symbolPrefix]) : ''
const routerPath = prefixPath + conf.path
this.router[conf.method](routerPath, ...controllers)
}
this.app.use(this.router.routes())
this.app.use(this.router.allowedMethods())
}
}
再分别使用之前的 router 函数给各种请求方法配好映射1
2
3
4
5
6
7
8
9const methods = {}
;['head', 'options', 'get', 'post', 'put', 'del', 'patch', 'all'].forEach((key) => {
methods[key] = function (path) {
return router({
methods: key,
path
})
}
})
除了路由方法,我们还需要对中间件也进行一次装饰器修饰1
2
3
4
5
6
7
8const middleware = (...mids) => {
return (...args) => {
const [target, name, descriptor] = args
target[name] = isArray(target[name])
target[name].unshift(...mids)
return descriptor
}
}
因为可能一个路由会调用多个中间件,所以使用 ...mids
表示传进来的中间件,再将其一个个加到对应 routerMap 的前面。这样,每次发起请求时,都会先按传入的中间件顺序进行调用,之后才执行路由的方法
所以的步骤都完成之后,我们来看看最后使用装饰器的代码长什么样1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16const { controller, get, post, middleware } = require('../lib/decorator')
const { log, validate } = require('../middlewares/user')
@controller('api/user')
class userController {
@get('/info')
@middleware(log, validate)
async getUserInfo (ctx, next) {
ctx.body = {
username: 'Kerminate',
job: 'programmer'
}
}
}
export default userController
controller
, get
, post
, middleware
是我们之前对路由作了一层包装后暴露出来的接口,log
, validate
则是两个中间件
完整项目可参见源码