解析Koa-Router,迈入Web次时代第一步(上)
发布于 8 年前 作者 hyj1991 18707 次浏览 来自 分享

为什么选择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框架编写路由函数的稍不注意就会触发的暗坑~ 欢迎各位观看,如果有问题,可以留言大家一起讨论 :)

5 回复

这个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文档<直接阅读源码理解核心实现~

回到顶部