koa-router讓人迷惑的文檔和源碼實現

Koa以特殊的中間件實現形式,關鍵代碼只用4個文件,就構建了效率極高的NodeJs框架。雖然應用的人數遠不及前輩Express,但也有一衆擁躉。typescript

Koa的組件都以中間件的形式實現,也使得構建十分簡單。koa-router的實現,也就是僅僅layer.jsrouter.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


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到底調用了什麼

那麼問題來了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中就行了,避免了調用時循環去查詢。

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. 小結

只能說很是奇怪。可能大佬實現上有其餘考慮,若有了解,還請告知。可是我感受是很是的弔詭了。

相關文章
相關標籤/搜索