前言
最近需要做Node
中间层的需求,优雅的名字是back-end for front-end(BEF)
,实现前后端彻底解耦。BFE
由来已久,服务端服务前端的一种逻辑分层模式,这种分层模式由来已久。在做中间层API
接口开发时,选用Koa
作为创建Node
服务的JavaScript
库,因此在各种文档中经常见到路由挂载的代码。
const Koa = require('koa');
const Router = require('koa-router');
const app = new Koa();
const router = new Router();
router
.get('/', async (ctx, next) => {
// do something
})
.post('/login', async (ctx, next) => {
// do something
});
app
.use(router.routes())
.use(router.allowedMethods());
app.listen(3000, () => {
console.log('server is runnung: http://localhost:3000');
});
在进行路由挂载的时候一直很好奇:
new Router()
到底做了什么呢?router
如何实现具体路径匹配到置顶函数的?router.routes()
到底做了什么呢?router.allowedMethods()
到底做了什么呢?- 能不能不要写这样的
.get/.put
的样板代码进行路由挂载,而通过一个router-config.js
文件生成server
端路由呢?
基础
再搞懂这些问题首先我们需要明确亮点:1、没有Koa
使用node
的http
模块如何使用路由;2、Koa
是中间件机制的(时刻记住,Koa
实现各种功能就是执行对应的moddleware
)。
// 使用 http 模块实现路由
const http = require('http');
const { parse } = require('url');
function callback(req, res) {
// 根据req.url执行对应的action
const { pathname } = parse(req.url);
switch (pathname) {
case '/':
// do something
break;
...
default:
break;
}
};
const server = http.createServer(callback);
// 使用事件监听执行callback
server.on('request', callback);
server.listen(3000);
// 使用 koa-router 实现路由
const Koa = require('koa');
const KoaRouter = require('koa-router');
const app = new Koa();
// 创建 router 实例对象
const router = new KoaRouter();
//注册路由
router.get('/', async (ctx, next) => {
ctx.body = {
code: 200,
msg: 'success',
data: 'index'
};
});
app.use(router.routes()); // 添加路由中间件
app.use(router.allowedMethods()); // 对请求进行一些限制处理
app.listen(3000);
而且我们知道Koa
实际上就是将中间件应用在callback
中,每次request
进来时执行一次所有的中间件函数,从而响应对应的请求。也就是说,其实koa-router
也是在对应的路由匹配到之后,执行相应的中间件函数,从而响应对应的请求的。因此我们可以大胆的猜测一下,.get/.put
等方法是实现路由和操作函数的绑定,router.routes()
是将路由中间件进行注册到Koa
的middleware
数组中等待执行,router.allowedMethods()
是执行一些异常兜底操作的。
源码架构
koa-router
的源码很简单主要是lib
目录下的两个文件layer.js
和router.js
。
router.js
文件做了什么。
// 依赖文件导入
var debug = require('debug')('koa-router');
var compose = require('koa-compose');
var HttpError = require('http-errors');
var methods = require('methods');
var Layer = require('./layer');
// 导出 Router
module.exports = Router;
// 声明 Router
function Router(opts) {
// do somethig
}
// 挂载对应操作方法
for (var i = 0; i < methods.length; i++) {
// do somethig
}
// 为 delete 方法声明一个别名 del
Router.prototype.del = Router.prototype['delete'];
// 为router应用middleware
Router.prototype.use = function () {
// do somethig
}
// 设置路由前缀
Router.prototype.prefix = function (prefix) {
// do somethig
}
// 路由中间件,这里也看到了 .routes 实际是 .middleware 的别名
Router.prototype.routes = Router.prototype.middleware = function () {
// do somethig
}
// 中间件的异常处理逻辑和兜底操作
Router.prototype.allowedMethods = function (options) {
// do somethig
}
// 提供便捷的方法一次注册所有的 methods 方法
Router.prototype.all = function (name, path, middleware) {
// do something
}
// 路由重定向
Router.prototype.redirect = function (source, destination, code) {
// do something
}
// 将路由路径与操作函数绑定
Router.prototype.register = function (path, methods, middleware, opts) {
// do something
}
// 通过name查询命名路由
Router.prototype.route = function (name) {
// do something
}
// 路由拼装
Router.prototype.url = function (name, params) {
// do something
}
// 根据path和method匹配到对应到路由中间件
Router.prototype.match = function (path, method) {
// do something
}
// 对路由添加校验中间件
Router.prototype.param = function(param, middleware) {
// do something
}
// 为类Router添加静态路由拼装方法
Router.url = function (path) {
// do something
}
由此koa-router
的源码架构如下:
源码解析
- 实例化
Router
&Layer
对象 进行路由操作之前首先需要实例化Router
对象:
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 = {};
this.stack = [];
}
可以看到Router
实际有用的属性包含三个methods
数组,params
对象,stack
数组。methods
数组存储http
方法可用方法名,在allowMethods
方法会使用到;params
对象,存储的是键为参数名与值为对应的参数校验函数,这样是为了通过在全局存储参数的校验函数,方便在注册路由的时候为路由的中间件函数数组添加校验函数,在param
方法会使用到;stack
数组,则是存储每一个路由,也就是Layer
的实例对象,每一个路由都相当于一个Layer
实例对象。
对于Layer
对象,使用一个实例化的Layer
管理每一个路由:
function Layer(path, methods, middleware, opts) {
this.opts = opts || {};
this.name = this.opts.name || null;
this.methods = [];
this.paramNames = [];
this.stack = Array.isArray(middleware) ? middleware : [middleware];
for(var i = 0; i < methods.length; i++) {
var l = this.methods.push(methods[i].toUpperCase());
if (this.methods[l-1] === 'GET') {
this.methods.unshift('HEAD');
}
}
// ensure middleware is a function
for (var i = 0; i < this.stack.length; i++) {
var fn = this.stack[i];
var type = (typeof fn);
if (type !== 'function') {
throw new Error(
methods.toString() + " `" + (this.opts.name || path) +"`: `middleware` "
+ "must be a function, not `" + type + "`"
);
}
}
this.path = path;
this.regexp = pathToRegexp(path, this.paramNames, this.opts);
debug('defined route %s %s', this.methods, this.opts.prefix + this.path);
}
Layer
的实例对象核心逻辑是在于将path
转化为正则表达式用于匹配请求的路由,然后将路由的处理中间件添加到Layer
的 stack
数组中。
method
相关函数method
函数将对应的路中处理中间件进行注册,其中核心逻辑还在是this.register
这个函数中。
for (var i = 0; i < methods.length; i++) {
function setMethodVerb(method) {
Router.prototype[method] = function(name, path, middleware) {
var middleware;
// 判断有没有传 name 参数
if (typeof path === "string" || path instanceof RegExp) {
middleware = Array.prototype.slice.call(arguments, 2);
} else {
middleware = Array.prototype.slice.call(arguments, 1);
path = name;
name = null;
}
this.register(path, [method], middleware, {
name: name
});
return this;
};
}
setMethodVerb(methods[i]);
}
register
函数对于每一个路由实例化一个Layer
对象存储在router.stack
数组理进行管理。其中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)) {
for (var i = 0; i < path.length; i++) {
var curPath = path[i];
router.register.call(router, curPath, methods, middleware, opts);
}
return this;
}
// create route
var route = new Layer(path, methods, middleware, {
end: opts.end === false ? opts.end : true,
name: opts.name,
sensitive: opts.sensitive || this.opts.sensitive || false,
strict: opts.strict || this.opts.strict || false,
prefix: opts.prefix || this.opts.prefix || "",
ignoreCaptures: opts.ignoreCaptures
});
if (this.opts.prefix) {
route.setPrefix(this.opts.prefix);
}
// add parameter middleware
for (var i = 0; i < Object.keys(this.params).length; i++) {
var param = Object.keys(this.params)[i];
route.param(param, this.params[param]);
}
stack.push(route);
return route;
}
- Router.prototype.match
通过上面的模块注册好路由之后,当请求打进来之后就是通过
match
函数找到匹配路由中间件函数。
Router.prototype.match = function (path, method) {
var layers = this.stack;
var layer;
var matched = {
path: [],
pathAndMethod: [],
route: false
};
for (var len = layers.length, i = 0; i < len; i++) {
layer = layers[i];
debug('test %s %s', layer.path, layer.regexp);
if (layer.match(path)) {
matched.path.push(layer);
if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) {
matched.pathAndMethod.push(layer);
if (layer.methods.length) matched.route = true;
}
}
}
return matched;
}
- Router.prototype.routes
使用
router.routes
将路由模块添加到koa
的中间件处理机制当中,因此routes
需要符合Koa
的中间件机制。
Router.prototype.routes = Router.prototype.middleware = function () {
var router = this;
var dispatch = function dispatch(ctx, next) {};
dispatch.router = this;
return dispatch;
}
routes
方法通过return dispatch
进入到Koa
的中间件执行机制中,并在dispatch
方法中return compose(layerChain)(ctx, next);
将每个路由注册的中间件处理函数进行执行。
5. Router.prototype.allowedMethod
allowedMethod
方法是处理错误请求的,需要进入到Koa
中间件机制中,因此其也是返回一个中间处理函数。
Router.prototype.allowedMethods = function (options) {
options = options || {};
var implemented = this.methods;
return function allowedMethods(ctx, next) {}
}
- Router.prototype.use
use
方法是为router
添加中间件处理函数的,且该函数是在所有路由执行之前执行。
Router.prototype.use = function () {
var router = this;
// 中间件函数数组
var middleware = Array.prototype.slice.call(arguments);
var path;
// 支持同时为多个路由绑定中间件函数: router.use(['/use', '/admin'], auth());
if (Array.isArray(middleware[0]) && typeof middleware[0][0] === 'string') {
middleware[0].forEach(function (p) {
// 递归调用
router.use.apply(router, [p].concat(middleware.slice(1)));
});
// 直接返回, 下面是非数组 path 的逻辑
return this;
}
// 如果第一个参数有传值为字符串, 说明有传路径
var hasPath = typeof middleware[0] === 'string';
if (hasPath) {
path = middleware.shift();
}
middleware.forEach(function (m) {
// 如果有 router 属性, 说明这个中间件函数是由 Router.prototype.routes 暴露出来的
// 属于嵌套路由
if (m.router) {
// 这里的逻辑很有意思, 如果是嵌套路由, 相当于将需要嵌套路由重新注册到现在的 Router 对象上
m.router.stack.forEach(function (nestedLayer) {
// 如果有 path, 那么为需要嵌套的路由加上路径前缀
if (path) nestedLayer.setPrefix(path);
// 如果本身的 router 有前缀配置, 也添加上
if (router.opts.prefix) nestedLayer.setPrefix(router.opts.prefix);
// 将需要嵌套的路由模块的 stack 中存储的 Layer 加入到本 router 对象上
router.stack.push(nestedLayer);
});
// 这里与 register 函数的逻辑类似, 注册的时候检查添加参数校验函数 params
if (router.params) {
Object.keys(router.params).forEach(function (key) {
m.router.param(key, router.params[key]);
});
}
} else {
// 没有 router 属性则是常规中间件函数, 如果有给定的 path 那么就生成一个 Layer 模块进行管理
// 如果没有 path, 那么就生成通配的路径 (.*) 来生成 Layer 来管理
router.register(path || '(.*)', [], m, { end: false, ignoreCaptures: !hasPath });
}
});
return this;
}
由此可以看出整个路由执行的过程如下:
解疑
在koa-router
源码Layer.js
中有一段代码如下:
if (this.methods[l-1] === 'GET') {
this.methods.unshift('HEAD');
}
可以看到在Layer
的构建函数里面,在对于methods
的处理中,会进行判断如果该请求为GET
请求,那么就需要在GET
请求前面添加一个HEAD
方法,其原因在于HEAD
方法与GET
方法基本是一致的,所以koa-router
在处理GET
请求的时候顺带将HEAD
请求一并处理,因为两者的区别在于HEAD
请求没有相应数据具体内容。