Express源码阅读
本文基于Express 4.15.2
我之前的Express学习笔记还在这里
Express常被用来进行Node.js后台的快速搭建。它仅仅对Node.js进行了简单的封装,结合中间件等很自然和好用的概念,很容易上手和学习。Express的API主要包括Application,Request,Response,Router几个部分,这是Express代码主要实现的部分。在我看来,Express贵在它的中间件,它提供了足够自由的空间但也做出规范,提供req
, res
, next
, err
给中间件操作。它的生态系统也是围绕这个展开的。
创建服务器
让我们从头回忆,用Express创建一个简单的服务器。像下面这样。
1 | var express = require('express') |
先通过构造函数创建一个Express应用(Application)。接着为它指定路由规则。最后通过app.listen()
的方式启动服务器。对比下Node.js原生的写法:
1 | var http = require('http'); |
那么Express的app究竟是什么,路由中间件又是如何绑定上去的。这些问题需要通过源码来解答。
代码结构
Express代码整体设计并不复杂(相对于Vue这样的复杂设计),比较容易看懂,一些常见的功能已经事先写成依赖包抽取出来,如debug(打印debug信息)和deprecate(显示API已废弃)等。Express的源码部分位于lib/
路径下:
1 | ... |
1 | ... |
根目录下的index.js
是整个项目的入口,所做的工作只是引入lib/
中的express.js
。lib/
目录下middleware
目录下放置了内置的中间件,router
中放置中间件的功能实现。下面的几个文件中
- application.js 应用的定义,app对象的API
- express.js,对app,router等功能的封装
- request.js和response.js是对http中res以及req的封装和增强
- utils.js 常用工具函数的封装
- view.js 建立拓展名和渲染引擎的联系
Application
express.js
和Application.js
大致告诉我们了express
和app
究竟为何物。
1 | //express.js |
上面的mixin
引入自merge-description,功能非常简单——通过描述符融合两个对象并返回,源码也很简单,主要由getOwnPropertyDescriptor
及defineProperty
方法实现,感兴趣的可以一看。
app通过mixin
继承了两个预定义对象,其中EventEmitter来自Node.js的API,继承后app将获得事件发布订阅功能。proto
由Application.js
导出定义。其中定义了app.listen()
方法。
1 | app.listen = function listen() { |
综上就很明白了
- 首先,
express()
返回的app
其实是一个函数,可以接受req
,res
以及定义在Express中的next - 之后,
app.listen()
方法实际上执行了http.createServer(app).listen()
总结,**express()
是个工厂函数,负责生产作为http.createServer(app)
的参数**。之后,让我们继续深入看看Application.js
中的代码。
app的工作主要是在Application.js
中进行的。下面介绍一些它的属性和相关方法
cache
,Object
,缓存视图信息engine
,Object
,视图渲染引擎engine(ext, fn)
,绑定拓展名和渲染方法,返回app自身用于链式调用
settings
,app的设置信息,可以通过set
,get
方法设置和获取。get(setting)
(或set(setting)
)获取配置值,这里是完整的配置表set(setting, value)
设置配置值enabled(setting)
和disabled(setting)
语法糖,获取Boolean类型配置值enable(setting)
和disable(setting)
语法糖,设置Boolean类型配置值
locals
在app生命周期内存储用户信息mountpath
顶级app挂载路径path()
返回app的挂载路径use(fn)
挂载中间件时可以指定第一个参数为挂载路径
router
该路由中间件属性在4.x中已废弃,由route(path)
方法替代_router
在lazyrouter
内部方法中加载,Router对象,中间件的容器。详细介绍在下文的Router一节中。
除了这些属性外,还有一些常见或内部使用的方法
defaultConfiguration
初始化默认配置lazyrouter
自身路由的懒加载,原因写在了源码注释里handle(req, res, cb)
最关键的app方法,用于分发请求完成路由功能。但它实际上只是对router.handle
做了简单的封装。并在最后通过finalhandler来做兜底。finalhandler是个很轻量级的依赖包,用于生成404页面和记录错误。详细文档见github.use(fn)
最关键的app方法,用于添加中间件,主要逻辑是将arguments
中的path
和fn
列表拆开来,再调用router的use
方法注册route(path)
调用router的route
方法注册一个路由中间件param(name, fn)
在自身的router对象中注册参数匹配回调VERB(path, fn)
为HTTP动词绑定路径和处理函数,主要功能也是由router代劳。有意思的是,HTTP动词是通过methods这个依赖包来返回的,这个包原理也非常简单——http.METHODS
。all(path)
用来匹配任意一个HTTP动词,其余功能和上面相同,实现上即将该路由中间件的所有动词都绑定上处理函数。del
已废弃render(name, options, cb)
调用渲染引擎渲染模板listen()
即上文提到的方法,对http.listen
的封装
中间件
中间件是Express设计的最精妙的地方,也是工作量最大的地方。之前Express利用了Connect来做这项工作。在当前版本中放在了router目录下去实现。整体来看,一个Express应用就是一系列的中间件首尾相接组成的。那么中间件是什么呢?用官网的话说就是“Middleware functions are functions that have access to the request object (req), the response object (res), and the next function in the application’s request-response cycle.”从code的角度来看,就是下面这样
1 | var myMiddleware = function(req, res, next){ |
如果是错误处理的中间件,需要将err传入为第一个参数
1 | var myErrorMiddleware = function(req, res, next){ |
所以,一个请求进入Express后,处理的流程大致是下面这样的:
1 | ↓ |
其中每个中间件能拿到req
和res
抑或err
,在完成自身工作后调用next()
执行下一个中间件。那么这些middleware是怎么放置在这条链上,请求又是如何通过next()
一步步向下传递的呢?
中间件分两种:普通中间件和路由中间件。它们都通过handle
方法处理;前者通过use
方法注册,后者通过VERB
(或all
)方法注册。其中
- 前者匹配所有以
path
开始的路径,而后者会精确匹配path
路径; - 前者对于请求的方法不做要求,而后者只能处理指定的请求。
1 | app.use('/user', function(req, res, next) { |
app层面
在app层面,即Application.js
中,是由app.use(fn)
, app.VERB
(或app.all
)和app.handle(req, res, cb)
完成的。而它们只是router的同名方法的简单封装。
1 | app.use = function use(fn) { |
其中flatten用于将多层嵌套数组扁平化为1层。可见到,app的use
方法在做了预处理工作后,调用router的use
完成注册工作。
需要路由中间件时,我们需要使用动词对应的方法(或all
)去注册。
1 | methods.forEach(function(method){ |
app的handle
方法就比较简单了。
1 | app.handle = function handle(req, res, callback) { |
Router层面
Express里中间件的具体实现在Router对象中。Router包含了Express中最为核心的概念。app中的许多API都是对Router API的简单封装。可以通过app._router
来访问app的Router对象。Router的源码位于lib/router/index.js
1 | var proto = module.exports = function(options) { |
Router对象有一个关键属性stack
,为一个数组,存放着所有的中间件。每一个中间件都是一个Layer对象,如果该中间件是一个路由中间件,则相应的Layer对象的route属性会指向一个Route对象,表示一条路由。
注册
每次调用app.use()
时,会执行router.use()
为stack
属性添加一个新的中间件,这个中间件是由Layer对象包装的。
1 | proto.use = function use(fn) { |
对于路由中间件要复杂些,路由中间件是通过router.route()
方法注册的。
1 | proto.route = function route(path) { |
看来,还需要去route.js
中看看这个对象的定义。
1 | function Route(path) { |
可以看到,Route对象也有一个stack属性,为一个数组,其中的每一项也是一个Layer对象,是对路由处理函数的包装。我们可以把它理解成一个路由中间件对象。每次调用router.route()的时候,实际上是新建了一个layer放在router.stack中;并设置layer.route为新建的Route对象。
之后,通过route[method].apply(route, slice.call(arguments, 1))
为特定方法绑定handler,route[method]
定义在route.js
中
1 | methods.forEach(function(method){ |
即,当调用route.VERB()的时候,新建一个layer放在route.stack中。
通过上面的分析可以发现,Router其实是一个二维的结构。一个可能的router.stack结构如下所示:
1 | ---------------- |
Layer
Layer对象的构造函数如下
1 | function Layer(path, options, fn) { |
Layer的属性和一些方法介绍如下
handle
用户指定的中间件函数name
函数名params
参数名,在执行match
时赋值path
路径名,在执行match
时赋值regexp
路径的正则表达形式,由pathRegexp转换完成keys
路径匹配结果信息route
路由中间件的Route对象,或undefined
上面看到,普通中间件和路由中间件都通过Layer的形式插入在stack
中。尽管它们都有构造函数中声明的哪些属性,这两种Layer还是有所区别:
- Router中的Layer对象具有
route
属性,如果该属性不为undefined
,则表明为一个路由中间件;而Route中的Layer对象没有route
属性 - Route中的Layer对象具有
method
属性,表明该路由函数的HTTP方法;而Router中的Layer对象没有method
属性 - Route中的Layer对象的
keys
属性值均为[]
,regexp
属性值均为/^\/?$/i
,因为在Route模块中创建Layer对象时使用的是Layer('/', {}, fn)
请求处理
在中间件注册完成后,剩下的工作都是由app.handle()
或者说router.handle()
完成的。这部分代码比较复杂。大致结构如下
1 | proto.handle = function handle(req, res, out) { |
在初始化和预处理后,调用了next()
函数。在next()
函数中,主要的部分是while语句判断是否遍历完成整个stack
,完成后执行done()。这部分代码如下
1 | while (match !== true && idx < stack.length) { |
其中layer
表示当前中间件,调用matchLayer
方法即layer.match(path)
判断是否和当前路径匹配(这个过程会更新layer
中的path
和params
)。之后的逻辑如下:
- 如果
match
不为true
,即中间件和路径不匹配,则处理下一个中间件 - 如果
match
为true
,route
不存在,表示不是一个路由中间件,执行continue,之后跳出循环 - 如果
match
为true
,且route
存在。说明是需要的中间件。
3.1 如果has_method
为false
且HTTP方法为OPTIONS
,则执行appendMethods(options, route._options())
添加支持方法
3.2 如果has_method
为false
且HTTP方法不为HEAD
,则设置match
为false,即该路由无法处理该请求,此时由于match依然满足循环条件,因此会对下一个中间件进行判断
3.3 如果has_method
为true
,则由于match
不再满足循环条件,因此会跳出循环
整体来看,循环的主要作用就是从当前下标开始找出第一个能够处理该HTTP请求的中间件。如果是非路由中间件,则只要匹配路径即可;如果是路由中间件,则需要同时匹配路径和HTTP请求方法。
while语句后,如果match
为true
,说明遍历完成,直接执行done()
。否则将匹配中得到的path
和params
交给process_params方法作参数预处理。
1 | self.process_params(layer, paramcalled, req, res, function (err) { |
从回调函数中可以看到,如果是路由中间件可以直接调用layer.handle_request(req, res, next)
执行真正的中间件函数。如果是普通中间件,还需要在trim_prefix
中对路径处理后才会调用layer.handle_request(req, res, next)
。
1 | function trim_prefix(layer, layerError, layerPath, path) { |
在路由中间件中,layer.handle_request(req, res, next)
调用的中间函数实际上是route.dispatch
方法,在Route对象内容分发请求,它的逻辑相对router.handle
要简单
1 | Route.prototype.dispatch = function dispatch(req, res, done) { |
可以看到,next
函数逻辑像下面这样
- 如果有错,直接调用
done()
传递错误 - 获取
layer
,如果不存在,调用done()
layer
和当前方法不匹配时,跳过当前layer
,执行next()
,继续下一个route函数layer
和当前方法匹配时,根据有无错误执行layer.handle_error(err, req, res, next)
或是layer.handle_request(req, res, next)
。
综上,一个请求到达时,流程顺序像下面这样
1 | ↓ |
每个中间件的处理过程则像下面这样
1 | ↓ |
在参数处理的过程中,每个参数的处理函数只会执行一次,并将结果保存在缓存中。在处理同一个请求的过程中,如果需要处理某个参数,会首先检查缓存,如果缓存中不存在,才会执行其处理函数。
内置中间件
在middleware
目录下,放置了两个Express内置中间件,在app.lazyRouter
中自动加载。
1 | app.lazyrouter = function lazyrouter() { |
其中第一个的作用是解析URL query,query(this.get('query parser fn'))
用于设置URL query解析器。第二个的作用是将req和res分别暴露给对方,并让它们分别继承自express定义的app.request
和app.response
。
1 | exports.init = function(app){ |
在express.js
中,req
和res
分别继承自了request.js
和response.js
的导出对象。
1 | function createApplication() { |
其他
关于参数处理和视图渲染,我看得不是很仔细,就不再赘述了。有兴趣的可以自行去参考中的链接学习。