为什么选择Koa-Router
之前的文章分析了Koa2.0框架下的核心发动机——compose模块。这次选择对Koa-Router模块来做一些深入源码实现的探讨,主要还是因为,无论使用nodejs作为服务器端语言实现什么样的web应用(注意是web应用),Koa-Router几乎都是必须的一个中间件模块。 那么对于纯api接口类的SAAS或者RPC远程接口调用来说,View层是冗余的;而对于广义上的前后端分离来说,以Node服务器为分界线,往后的都属于后端,往前的都属于前段范畴,这种系统模式下,现在比较常见的是Node前+Java后,所以呢,对于这样的Node服务器来说,model层又是可以抛弃的(因为不直接操作数据库)。 但是,无论Web应用如何生长,属于Controller层的路由中间件,地位无可替代。想想也是,一个连路由都不做区分的Web应用,那么就是所有的请求都走一个处理函数,这样的应用,还算是一个Web应用吗。 所以呢,面对地位如此特殊的路由中间件,面对身份如此高贵的路由中间件,不拿来细细探究一番,简直都不好意思和别人说自己研究过Koa框架。 言归正传,本文主要从Koa-Router模块的基本使用开始,和大家一起深入探讨下这些基本使用背后的奥秘,以及koa-router存在的一个暗坑的思考。对于核心模块的实现的理解程度,其实也决定了开发者对于整个项目细节的掌控力度。 本文讨论的Koa框架基于Koa2.0,所以讨论的Koa-Router的版本为koa-router@7.0.1。
如何使用Koa-Router
Koa-Router中间件的使用方法,一如Koa框架本身的简洁,但是需要注意的是:如果当前的框架使用的是Koa2.0,请使用:
npm install koa-router[@next](/user/next)
来安装7.x版本的koa-router模块,以适配Koa2.0框架;如果当前的框架使用的是Koa1.x,正常的安装方式即可。 下面给出的快速使用koa-router,搭建一个简单路由并且返回信息给相应页面的样例:
'use strict';
const Koa = require('koa');
const koaRouter = require('koa-router');
const app = new Koa();
const router = koaRouter();
app.use(router['routes']());
router.get('/index', function (ctx, next) {
ctx.body = 'Hello Koa2.0!';
});
app.listen(3000, ()=>console.log('Koa start at 3000...'));
使用node启动该js文件,这样本地访问http://127.0.0.1:3000/index,即可以看到页面响应数据:
Hello Koa2.0!
可以看到样例的搭建非常简单。由于中间件是在web服务器每一次请求才会触发中间件的逻辑操作,所以我们对于整个路由中间件原理的剖析从router.get开始。
router.get/post的背后
nodejs其实本身支持数十个http的动词操作,然而,大家最熟悉也是用的最多的还是get/post,这两个动词。
1.寻找get/post对应的原型方法:
查看核心文件router.js(位于lib/router.js),最上方即可以看到如下代码:
module.exports = Router;
很容易明白,整个koa-router模块引入后返回的其实是一个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 = [];
}
所以,第二节样例中的
const router = koaRouter();
一句,其实router得到的是Router类的一个实例,由于我们调用构造函数的时候,没有传入任何参数,所以Router构造函数里面的所有内部属性均为默认值,这里面比较重要的一个参数是:
this.stack = [];
然而在这里是完全看不出this.stack作用,我们接着往下分析。 由于上面router的值其实是Router的一个实例,那么很多朋友就开始寻找:
Router.prototype.get = function(){
//…
}
然后会发现,在这个文件里找不到,是不是很奇怪,别急,再仔细看源码,我们会发现如下一段代码:
methods.forEach(function (method) {
Router.prototype[method] = function (name, path, middleware) {
var middleware;
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;
};
});
这段代码明显是用来自动化生成Router的原型方法的,那么这里的methods,是一个第三方模块methods模块,作用就是返回http支持的动词请求,当然也包含我们要找的get/post了,所以第一步寻找router.get以及router.post对应的原型方法就比较容易了,就是上图中的Router.prototype[method]后面的函数。
2.分析自动化生成Router类的原型方法的函数。
以我们最常使用的
router.get(‘/index’, function(ctx, next){
//逻辑处理
})
为例,很显然,第一个if分支走的是else,重要的是这一句:
middleware = Array.prototype.slice.call(arguments, 1);
此时middleware的值为一个数组,数组里面的每一个元素就是传入get/post方法中的第二个参数,第三个参数…一直到最后一个参数,而这些,恰恰就是我们写的路由处理函数! 从这一句,我们也可以比较明显的发现,其实在koa-router下, 对于同一个路由,我们是可以写成如下的流处理形式:
async function indexStep1(ctx, next) {
//逻辑处理第一部分
await next();
}
async function indexStep2(ctx, next) {
//逻辑处理第二部分
await next();
}
async function indexStep3(ctx, next) {
//逻辑处理第三部分
await ctx.render('index');
}
router.get('/index', indexStep1, indexStep2, indexStep3);
回到正题,这一步获取middlewares完成后,就进入了下一段代码:
this.register(path, [method], middleware, {
name: name
});
return this;
这个函数的主要作用就是将获取到的路由路径、该路由路径对应的http动词,以及处理中间件函数数组,这三个作为入参调用register方法注册到Router的路由列表中。 而最后一个return出去的this对象,是为了可以书写链式调用路由注册方法,形如:
router.get(‘/index’,func).post(‘/post’,func)….
js开发者对于链式调用确实有一种难以言喻的亲近感~
3.register方法的作用
Router.prototype.register = function (path, methods, middleware, opts) {
opts = opts || {};
var router = this;
var stack = this.stack;
if (Array.isArray(path)) {
path.forEach(function (p) {
router.register.call(router, p, methods, middleware, opts);
});
return this;
}
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 || "",
});
if (this.opts.prefix) {
route.setPrefix(this.opts.prefix);
}
Object.keys(this.params).forEach(function (param) {
route.param(param, this.params[param]);
}, this);
stack.push(route);
return route;
};
老规矩,先上register函数的源码,我们按照重点逐段分析。
第一处重点:
if (Array.isArray(path)) {
path.forEach(function (p) {
router.register.call(router, p, methods, middleware, opts);
});
return this;
}
很明显,此处的path即为我们使用router.get(‘/index’)传入的路由路径,如果此参数为数组,则会使用array.forEach方法进行遍历调用register方法,所以此处我们可以发现,如果多个路由对应同一个处理函数,我们可以写成如下的形式:
router.get([‘index1’, ‘index2’, ‘index3’], function(ctx, next){});
第二处重点:
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 || "",
});
这段调用Layer类构造函数,传入路由路径,路由方法,存储中间件函数的数组,得到一个经过包装的router实例。 经过查看Layer的实现,可以明白,这个包装的过程主要是1.将path转换成正则符号,挂载入route.regexp属性上;2.将请求的method以数组的形式挂载到route.methods属性上;3.将请求的处理中间件存储数组,挂载到route.stack上去,这样一个经过包装的route实例就生成了。
第三处重点:
var stack = this.stack;
stack.push(route);
这一段,很明显,将得到的包装过的router实例,存入this.stack对应的数组里面。
一些结论
通过上面的分析,很容易明白,每次http请求到达node服务器时,Router实例router,即可通过router.stack,来获得所有开发者编写的路由以及路由处理函数(所谓的中间件)信息。再通过router.routes()方法进行匹配路由等处理。由于精力有限,此函数的解析不能在本文详细给出。 下一篇文章,将主要来分析router.routes()方法如何实现路由匹配,以及路由处理函数整合到Koa框架下的中间件体系中去的,还有由此引发的一个使用Koa2.0框架编写路由函数的稍不注意就会触发的暗坑~ 欢迎各位观看,如果有问题,可以留言大家一起讨论 :)
这个next跟struts2的filter很象啊
@yakczh 我搞java的朋友也是这么说的…不过我没有对java的几个框架仔细研究过,不敢随意发言哇…
@hyj1991 但是Filter很影响性能 ,不管有没有自己的操作,都得挨个过一遍, 请求少的时候没问题,如果api请求多了,中间件数量多了,相对不用filter要影响性能 了
@hyj1991 web框架原理都一样,不用框架,自己可以随心所欲处理请求,只要返回字符串 web框架把请求的整个流程都分成N个步骤,把每个步骤用过到的操作放到处理队列,或者硬编码加hook 这样大多数操作就不用写了,就自动有相应的功能了,但是随之而来的代价是每个操作不管有事没事,都得过一遍流程,就象你包里没有放炸药包,那也排队过地铁安检一样,而且如果每个流程之间有依赖关系或者数据引用就更麻烦了
@yakczh 是这样的,您说的确实都是对的,所以我在第一段也分析了,针对node在不同类型的web应用中使用,有一些可以不需要view层,有一些可以不需要model层,再细化的话,有一些web应用可能都不需要Session、Cookie处理这些(单纯的rpc远程调用服务);但是我觉得所有的web应用都会需要一个路由模块,就算是自己实现也是需要的哇; 所以这里的koa-router,正好将路由模块做成了一个中间件的形式,我觉得是符合未来node开发组件化和sdk化的大方向的,即项目由一个个高内聚低耦合的npm包,在极简的框架基础上进行自由的require构成; 所以我觉得对这个几乎都要使用的路由中间件,进行下源码层面的解析,会有助于开发者对项目掌控力度的增加。其实node服务端写多了就会发现,对于第三方包的引入,使用时总是可能会存在一些暗坑,那么我的感觉而言,搜谷歌搜stackoverflow<看官方最新的API文档<直接阅读源码理解核心实现~