koa-router讓人迷惑的文件和原始碼實現

yichongwen發表於2018-12-11

Koa以特殊的中介軟體實現形式,關鍵程式碼只用4個檔案,就構建了效率極高的NodeJs框架。雖然應用的人數遠不及前輩Express,但也有一眾擁躉。

Koa的元件都以中介軟體的形式實現,也使得構建十分簡單。koa-router的實現,也就是僅僅layer.jsrouter.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中加入斷點觀察呼叫時發生的情況。

koa-router讓人迷惑的文件和原始碼實現

明明我們再程式碼中只是使用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會報錯,但是可用

koa-router讓人迷惑的文件和原始碼實現
報錯表明,這個引數啊,他不能是個陣列啊,他要是一個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中是四個元素

koa-router讓人迷惑的文件和原始碼實現

4. 小結

只能說非常奇怪。可能大佬實現上有其他考慮,如有了解,還請告知。但是我感覺是非常的弔詭了。

相關文章