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會報錯,但是可用
報錯表明,這個引數啊,他不能是個陣列啊,他要是一個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中是四個元素
4. 小結
只能說非常奇怪。可能大佬實現上有其他考慮,如有了解,還請告知。但是我感覺是非常的弔詭了。