Koa以特殊的中間件實現形式,關鍵代碼只用4個文件,就構建了效率極高的NodeJs框架。雖然應用的人數遠不及前輩Express,但也有一衆擁躉。typescript
Koa的組件都以中間件的形式實現,也使得構建十分簡單。koa-router的實現,也就是僅僅layer.js和router.js兩個文件。由此,想要本身實現一個基於ES6修飾器(@decorator)的寫法,相似Nest。npm
// 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加入中間件的寫法以下bash
// 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());
複製代碼
只能說太迷惑人了session
// middleware
function one(ctx, next) {
console.log('middleware one')
next()
}
router.use(one) // 這樣才能行
router.use(one()) // 這樣不行,這樣寫都已經執行了
複製代碼
上面寫法中只能是方法返回一個方法纔可行,而這個官方文檔也太迷惑了。app
如下是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;
};
複製代碼
那麼問題來了koa
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中加入斷點觀察調用時發生的狀況。async
明明咱們再代碼中只是使用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中就行了,避免了調用時循環去查詢。
記得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會報錯,可是可用
報錯代表,這個參數啊,他不能是個數組啊,他要是一個IMiddleware。那咱們就查看一下這個d.ts文件。/**
* 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中是四個元素
只能說很是奇怪。可能大佬實現上有其餘考慮,若有了解,還請告知。可是我感受是很是的弔詭了。