Koa以特殊的中间件实现形式,关键代码只用4个文件,就构建了效率极高的NodeJs框架。虽然应用的人数远不及前辈Express,但也有一众拥趸。
Koa的组件都以中间件的形式实现,也使得构建十分简单。koa-router的实现,也就是仅仅layer.js和router.js两个文件。由此,想要自己实现一个基于ES6修饰器(@decorator)的写法,类似Nest。
// test.controller.js
const { Controller, Get } = require('./decorator')
@Controller("/test")
export default class TestController {
@Get('/getone')
async sectOne(ctx) {
console.log('getone start...')
ctx.body = 'get one : ' + ctx.request.query.one
console.log('getone end...')
}
}
// controller use
const Koa = require('koa')
const testController = require('./test.controller')
const koa = new Koa()
app.use(testController.routes()).use(testController.allowedMethods())
实现中发现一些令人迷惑的部分。
先吐个槽: NPM包的官方文档中router加入中间件的写法如下
// session middleware will run before authorize
router
.use(session())
.use(authorize());
// use middleware only with given path
router.use('/users', userAuth());
// or with an array of paths
router.use(['/users', '/admin'], userAuth());
app.use(router.routes());
只能说太迷惑人了
// middleware
function one(ctx, next) {
console.log('middleware one')
next()
}
router.use(one) // 这样才能行
router.use(one()) // 这样不行,这样写都已经执行了
上面写法中只能是方法返回一个方法才可行,而这个官方文档也太迷惑了。
1. koa-router的实现
以下是router.js中的构造函数
// constructor
function Router(opts) {
if (!(this instanceof Router)) {
return new Router(opts);
}
this.opts = opts || {};
this.methods = this.opts.methods || [
'HEAD',
'OPTIONS',
'GET',
'PUT',
'PATCH',
'POST',
'DELETE'
];
this.params = {}; // router接受的参数配置
this.stack = []; // router中执行的方法,包括中间件和router内的逻辑
};
// listen
Router.prototype.routes = Router.prototype.middleware = function () {
var router = this;
var dispatch = function dispatch(ctx, next) {
...... // 调用router.routes()时返回dispatch函数,触发后执行
...... // 而这里的装配,其实就是将this.stack中对应的路径方法放入数组,触发后依次执行(stack中元素是layer.js的构建)
};
dispatch.router = this;
return dispatch;
};
2. router.use到底调用了什么
那么问题来了
function one(ctx, next) {
console.log('middleware one')
next()
}
function two(ctx, next) {
console.log('middleware two')
next()
}
router.use('/test/one', one)
router.use('/test/one', two)
router.get('/test/one', (ctx, next) => {
console.log('router test')
ctx.body = 'finish'
})
router.use('/test/two', two)
router.get('/test/two', (ctx, next) => {
console.log('router test')
ctx.body = 'finish'
})
app.use(router.routes()).use(router.allowedMethods()).listen(port, () => {
console.log('Server started on port ' + port, ', NODE_ENV is:', env)
})
简单的测试代码如上,然后我们再Router.prototype.routes内的dispatch中加入断点观察调用时发生的情况。
明明我们再代码中只是使用get,认为我们监听了两个路径,为什么this.stack中会存在5个Layer?
这就说到了router.use方法
Router.prototype.use = function () {
...... // 前面代码主要是支持路径数组的传入
...... // 把参数中的方法拿出来
// 以下的实现明显中间件是可以传多个的
middleware.forEach(function (m) {
if (m.router) {
...... // 可以传入带router对象
} else { // 传入普通方法中间件调用register
router.register(path || '(.*)', [], m, { end: false, ignoreCaptures: !hasPath });
}
});
return this;
};
如果啊按官方文档所说,构建我们需要的装饰器,如下
// decorator.js
const KoaRouter = require('koa-router')
let router = new KoaRouter()
// 实现controller修饰器
function Controller(prefix, middleware) {
if (prefix) router.prefix(prefix)
return function (target) {
let reqList = Object.getOwnPropertyDescriptors(target.prototype)
// 排除构造函数,取出其他方法
for (let v in reqList) {
if (v !== 'constructor') {
let fn = reqList[v].value
fn(router, middleware)
}
}
return router
}
}
// 实现router方法修饰器
function KoaRequest({ url, methods, routerMiddlewares}) {
return function (target, name, descriptor) {
let fn = descriptor.value
descriptor.value = function(router:KoaRouter, controllerMiddlewares:Array<Function> = []) {
let allMiddleware = controllerMiddlewares.concat(routerMiddlewares)
// 可以这样写
for(let item of allMiddleware) {
router.use(item)
}
// 或者如下
// router.use(url, [...controllerMiddlewares.concat(routerMiddlewares)])
// 然后调用router方法
router[method](url, async (ctx, next) => {
fn(ctx, next)
})
}
}
}
function Get(url, middleware) {
return KoaRequest({ url, methods: ['GET'], routerMiddlewares: middleware })
}
...... // 如Get一样实现Post, Put, Delete, All
module.exports = { Controller, Get, Post, Put, Delete, All }
然后,router.stack中就会存在很多路径一样的Layer,在触发后循环去查找相关路径下的中间件和router监听的方法。实在是没理解这样的用意,有谁了解过麻烦告知。
但就目前看来,有点怪。Layer中,中间件和router监听内的逻辑其实都是在Layer对象的stack中,也就是说,其实可以直接放入同一个路径下的Layer stack中就好了,避免了调用时循环去查询。
3. router.register
记得router.use中,如果传入的不是带router的对象,那么会走入else逻辑。
middleware.forEach(function (m) {
if (m.router) {
...... // 可以传入带router对象
} else { // 传入普通方法中间件调用register
router.register(path || '(.*)', [], m, { end: false, ignoreCaptures: !hasPath });
}
});
return this;
也就是调用了router.register去注册相关路径的监听和中间件。router.register如下
// register 就是创建了一个Layer然后放入stack中
Router.prototype.register = function (path, methods, middleware, opts) {
opts = opts || {};
var router = this;
var stack = this.stack;
// support array of paths
if (Array.isArray(path)) {
......
}
// create route
var route = new Layer(path, methods, middleware, {
......
});
if (this.opts.prefix) {
route.setPrefix(this.opts.prefix);
}
// add parameter middleware
......
stack.push(route);
return route;
};
// 其实router.get等方法,最后也是调用的register方法
Router.prototype.all = function (name, path, middleware) {
var middleware;
if (typeof path === 'string') {
middleware = Array.prototype.slice.call(arguments, 2);
} else {
middleware = Array.prototype.slice.call(arguments, 1);
path = name;
name = null;
}
this.register(path, methods, middleware, {
name: name
});
return this;
};
可以看出,register接受path, methods, middleware, opts参数,是最终的实现。这里明显是一个对外能够调用的方法,但是官方文档并没有提及这个方法的使用。
根据register对修饰器进行修改,如下,可以得到一个路径对应一个stack的结构
// 实现router方法修饰器 decorator.js
function KoaRequest({ url, methods, routerMiddlewares}) {
return function (target, name, descriptor) {
let fn = descriptor.value
descriptor.value = function(router:KoaRouter, controllerMiddlewares:Array<Function> = []) {
let allMiddleware = controllerMiddlewares.concat(routerMiddlewares)
allMiddleware.push(fn)
router.register(url, methods, allMiddleware) // 传入middleware数组
}
}
}
由于项目是用typescript写的,ts如下
// 实现router方法修饰器 decorator.ts
function KoaRequest({ url, methods, routerMiddlewares = []}:KoaMethodParams):Function {
return function (target, name, descriptor) {
let fn = descriptor.value
descriptor.value = function(router:KoaRouter, controllerMiddlewares:Array<Function> = []) {
let allMiddleware = controllerMiddlewares.concat(routerMiddlewares)
allMiddleware.push(fn)
router.register(url, methods, allMiddleware) // ts编译中allMiddleware会报错,但是可用
}
}
}
ts编译中allMiddleware会报错,但是可用
/**
* Create and register a route.
*/
register(path: string | RegExp, methods: string[], middleware: Router.IMiddleware, opts?: Object): Router.Layer;
确实d.ts文件中参数传入必须是一个Router.IMiddleware。然而,根据上面router.js内部源码和layer.js中stack的存储,我们知道这个middleware参数传个数组是可以使用的。
// 项目内测试修饰器controller,四个路径监听
@Controller("/test", [controllerMiddleOne, controllerMiddleTwo])
export default class TestController {
@Get('/getone', [routerMiddleOne])
async testOne(ctx) {
console.log('testOne start...')
ctx.body = 'get one : ' + ctx.request.query.one
console.log('testOne end...')
}
@Get('/gettwo', [routerMiddleTwo])
async testTwo(ctx) {
console.log('testTwo start...')
ctx.body = 'get two : ' + ctx.request.query.one
console.log('testTwo end...')
}
@Get('/retry')
async retry(ctx) {
console.log('retry start...')
ctx.status = 404
console.log('retry end...')
}
@Post('/nothing')
async nothing(ctx) {
ctx.body = { test: ctx.request.one.nothing }
}
}
可见确实,stack中是四个元素
4. 小结
只能说非常奇怪。可能大佬实现上有其他考虑,如有了解,还请告知。但是我感觉是非常的吊诡了。