express源码解析(转)
express是nodejs平台上一个非常流行的框架,4.2.0是最新的版本,相比3.x版本优化了代码和api,去除了connect模块,自己实现了一个router组件,实现http请求的顺序流程处理,去除了很多绑定的中间件,使代码更清晰。
1.使用express
如何使用express在官网有很好的讲解,只用experssjs实例app的几个函数,就可以构建构建web程序。
1 | var express = require('express'); |
上面是一个简单的web程序,返回浏览器hello world,就几个步骤,获取express实例对象,加入需要的中间件,加入路由响应,启动服务器,很简单吧,相比java,.net的框架轻量了很多,而且不需要单独架设web服务器,利用nodejs的异步非阻塞机制,可以大大提高网站的并发量。
1.1中间件
app.use 加入中间件,所谓中间件其实就是java,.net平台MVC框架都会有的filter。
app.use(["path"],function(req,res,next){}) 有两个参数,path代表route路径,可选,为空表示匹配所有路径,后面是回调函数,需要添加一个next参数,执行时,框架将传入一个next函数,调用它启动下一个中间件,下面是一个中间件的示例
1 | app.use('/public',express.static(__dirname + '/public')); |
我们添加了一个自定义的中间件,打印出 hello middleware 从上面我们可以看出app.use把中间件加入一个栈中,http request将触发整个中间件链条,并依次执行(通过 next() 函数实现),功能类似于filter,但其作用却大于filter,它可以动态地给req,res添加内容。margon是一个日志记录的包,记录每个request的信息。express.static()是express保留的唯一个内置中间件,对/pulic路径下的route导向静态资源文件,不调用next。这样中间件就可以实现对指定或所有路径request和response的处理。
1.2 app.get()/app.VERB()
app.get有两个功能,第一次看express文档时都会很疑惑,app.get可以获取app.set设置的全局变量,也可以设置路由的处理函数,下面是get实现的源码,对js不是很熟悉的人会很纠结,代码里找不到get函数啊,app.get和app[‘get’]的方式都可以定义对象的函数,下面是其实现的源码。
1 | >application.js |
methods是一个数组,存储了http所有请求的类型,在method模块里定义,除了基本的get、port请求外,还有多达十几种请求,可能是为了兼容新的http标准吧。app[method]中,method==’get’且只有一个参数,则执行set,执行的是获取变量的功能,否则,执行app.get('path',function(req,res){})中path对应的回调函数,执行route组件的get方法(实现方式和这里一样),将route和回调存储进一个栈中。http请求触发执行,app.get也将产生一条路由中间件,执行后返回浏览器html页面。
module.exports = [
'get',
'post',
'put',
'head',
'delete',
'options',
'trace',
'copy',
'lock',
'mkcol',
'move',
'purge',
'propfind',
'proppatch',
'unlock',
'report',
'mkactivity',
'checkout',
'merge',
'm-search',
'notify',
'subscribe',
'unsubscribe',
'patch',
'search'
];
2.了解express4.2的结构
下面是express4.2的文件结构图:
- express.js和application.js是主要的框架文件,暴露了express的api。
- router文件夹下为router组件,负责中间件的插入和链式执行,具体讲解在下一章节。
- middleware下的init.js和query.js为两个中间件,init.js的作用是初始化request,response,看一下代码就能明白:
1 | exports.init = function(app){ |
query.js中间件的作用是格式化url,将url中的rquest参数剥离,储存到req.query中:
1 | module.exports = function query(options){ |
- request.js和response.js, 提供了一些方法丰富request和response实例的功能,在init.js中初始化了http的req和res实例。
req.\__proto__ = app.request;res.\__proto__ = app.response; - view.js提供模板渲染引擎的封装,通过
res.render()调用引擎渲染网页,具体请看第五章
3.Router组件
Router组件由三个文件组成,index.js为主文件,route.js主要功能是路由处理,layer保存中间件的数据结构,Router组件实例化后的对象如下图所示,stack为中间件栈:这是第一章里代码执行时的对象结构图,我们可以看到route存储了五个中间件,包含两个默认的query和expressInit组件:
1 | { [Function: router] |
下面是Route的实例,stack为其http.verb的method和响应函数对, 如下图所示,”/“为一条路由的路径,接受method为get的http请求。
1 | { path: '/', |
- index.js主要处理中间件的执行,包括中间的插入,错误处理,执行(handle)等
- route.js主要处理路由信息,每条路由都会生成一个Route实例,通过index.js里的
proto.route(path)方法可以创建一个path对应的Route实例,并封装在layer中,加入中间件栈。另外Route.get (Route[‘get’]) 方法也是在这里动态生成的。
- layer.js是中间件的存储结构。
看
router.stack的最后一条,发现它的handle是一个无名的function,看了源码你就会知道,这个无名funtion就是路由’/‘对应的处理函数,每条路由都会作为一个中间件加入栈中。
我们每次调用app.get()就新建了一个Route实例(见1.2节代码),调用链条为app['get']=>router.Route['get']。
如下代码,调用Route['get'],Route中将会将get标示加入self.methods中,防止重复定义,然后生成一个数据项加入self.stack,数据项{ method: 'get', handle: [Function] }含method标示和路由处理函数fn。
在1.2节代码中,app.get()函数将Route实例封装在layer中,作为一个中间件加入栈中,当触发执行时,会将处理函数fn取出执行。
1 | >route.js |
4.中间件触发流程
4.1主要过程
中间件触发通过以下代码:
1 | >express.js |
express模块返回一个app作为http.createServer()的回调函数,这样一个http请求将触发执行app.handle()执行中间件,下面我们看看app.handle()的代码:
1 | app.handle = function(req, res, done) { |
app.handle()调用了router组件的handle(req,res,fn)函数执行中间件,链式执行完所有中间件后,done函数是定义的错误处理函数,在htpp.createServer(function(res,req,done)中传入,下面将讲述express的核心route组件。
4.2 router组件
router组件主要有三个文件组成,index.js和route.js是其主要逻辑部分,layer.js作为中间件封装的数据结构。
下面的代码是route生成http.verb的函数:
1 | methods.forEach(function(method){ |
1.2节中,application.js里的methods.each调用的就是这里生成http.verb处理函数,Reoute实例化的时候就生成了对应http.verb的处理函数(Route[‘method’])。
代码里可以看出,http.verb可以一次添加多个处理函数,形式为function(req,res,next)或者function(req,res).
下面是router组件构建的函数:
1 | var proto = module.exports = function(options) { |
router是一个对象构造函数,router.__proto__ = proto引入了整个proto的所有函数,包括use,handle等待相关中间件操作函数,定义了中间件储存的数组,配置等。
router完全可以作为一个对象定义为 var router ={},源码里,发现router没有进行实例化,所以这个构造函数式没有必要的。
下面我们看看router是如何触发链式执行的:
1 | proto.handle = function(req, res, done) { |
这里主要展示了中间件执行的过程,每调用一次next()就会有一个中间件触发,并再一次调用next(),看next里的代码:
1 | if (route) { |
layer是一个保存中间件路径,处理函数的数据结构,想详细了解请看源码,上面的代码表示如果路径和这个中间件配置的路径匹配,则执行其回调,next就是我们第一章开头讲的,下一个中间件触发的函数,现在应该知道这个函数从哪来了吧?
递归执行next,直到执行完为止,执行done,但是我们会发现done一直就是null,不知道这是不是老的nodejs遗留下来的问题,createServer的回调现在不会传入第三个参数了。
1 | var layer = stack[idx++]; |
下面看看route.js 是如何运行的吧
1 | #router/index.js |
这个函数把一条路由和它的route.dispatch作为一个中间件加入了栈中,并返回一个Route实例,Route实例包含了路由处理的各种方法和信息,其中route.dipatch也是其原型函数,用来处理相同路由的不同http.verb,下面我们看看这个函数。
1 | Route.prototype.dispatch = function(req, res, done){ |
这个函数很长,主要过程可以简单叙述一下,也是通过next_layer函数,链式访问一条路由的post,get等方法的回调函数,根据req.method来判断请求类型,执行相应处理函数,不同http.verb可以执行不同回调,也就是说,express一条路由可以响应多种类型的请求。但是注意到,这样回调函数应该写成function(req, res, next){ ..... next()}。
讲了很多,估计大家都昏头了,下面的流程图会很清晰的让大家知道整个过程。
5.View的实现
4.x版本的render和3.x版本不一样,这里以回调的方式进行render,而不在内部调用res.send()示例:
1 | res.render('index', function(err, html){ |
res.render()的实现比中间件简单很多,总体来说,经过三次封装,进行了一些配置,调用链条为res.render() => app.render() =>view.render()=> require("jade")/reqiure("ejs").render(),首先看app.engine,将jade或ejs模板引擎的render函数存入了engines数组中
1 | app.engine = function(ext, fn){ |
app.defaultConfiguration()(app初始化的一个函数),把View的构造函数保存。
1 | // default configuration |
app.render()将其取出并调用,初始化一个View实例,并执行‘view.render()’渲染模板,注意初始化函数将engines传入了View实例,里面保存了模板引擎的render函数。
1 | view = new (this.get('view'))(name, { |
view.render()执行的便是模板引擎的render函数,fn为渲染完成后的回调函数。
1 | View.prototype.render = function(options, fn){ |




