本文基于Express 4.15.2
我之前的Express学习笔记还在这里
Express 常被用来进行Node.js后台的快速搭建。它仅仅对Node.js进行了简单的封装,结合中间件等很自然和好用的概念,很容易上手和学习。Express的API主要包括Application,Request,Response,Router几个部分,这是Express代码主要实现的部分。在我看来,Express贵在它的中间件,它提供了足够自由的空间但也做出规范,提供req
, res
, next
, err
给中间件操作。它的生态系统也是围绕这个展开的。
创建服务器 让我们从头回忆,用Express创建一个简单的服务器。像下面这样。
1 2 3 4 5 6 7 8 9 10 var express = require ('express' )var app = express ()app.get ('/' , function (req, res ) { res.send ('Hello World!' ) }) app.listen (3000 , function ( ) { console .log ('Hello world!' ) })
先通过构造函数创建一个Express应用(Application)。接着为它指定路由规则。最后通过app.listen()
的方式启动服务器。对比下Node.js原生的写法:
1 2 3 4 5 6 var http = require ('http' );var server = http.createServer (function (request, response ) { res.write ('Hello world' ); res.end (); }).listen (3000 );
那么Express的app究竟是什么,路由中间件又是如何绑定上去的。这些问题需要通过源码来解答。
代码结构 Express代码整体设计并不复杂(相对于Vue这样的复杂设计 ),比较容易看懂,一些常见的功能已经事先写成依赖包抽取出来,如debug (打印debug信息)和deprecate (显示API已废弃)等。Express的源码部分位于lib/
路径下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ... - lib/ - middleware/ - init.js - query.js - router/ - index.js - layer.js - route.js - application.js - express.js - request.js - response.js - utils.js - view.js ... - index.js ...
1 2 ... module .exports = require ('./lib/express' );
根目录下的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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 exports = module .exports = createApplication;function createApplication ( ) { var app = function (req, res, next ) { app.handle (req, res, next); }; mixin (app, EventEmitter .prototype , false ); mixin (app, proto, false ); ... app.init (); return app; }
上面的mixin
引入自merge-description ,功能非常简单——通过描述符融合两个对象并返回,源码也很简单,主要由getOwnPropertyDescriptor
及defineProperty
方法实现,感兴趣的可以一看。
app通过mixin
继承了两个预定义对象,其中EventEmitter 来自Node.js的API,继承后app将获得事件发布订阅功能。proto
由Application.js
导出定义。其中定义了app.listen()
方法。
1 2 3 4 app.listen = function listen ( ) { var server = http.createServer (this ); return server.listen .apply (server, arguments ); };
综上就很明白了
首先,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 2 3 4 var myMiddleware = function (req, res, next ){ next (); };
如果是错误处理的中间件,需要将err传入为第一个参数
1 2 3 4 var myErrorMiddleware = function (req, res, next ){ next (); };
所以,一个请求进入Express后,处理的流程大致是下面这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 ↓ --------------- | middleware1 | --------------- ↓ --------------- | ... ... ... | --------------- ↓ --------------- | middlewareN | --------------- ↓
其中每个中间件能拿到req
和res
抑或err
,在完成自身工作后调用next()
执行下一个中间件。那么这些middleware是怎么放置在这条链上,请求又是如何通过next()
一步步向下传递的呢?
中间件分两种:普通中间件和路由中间件。它们都通过handle
方法处理;前者通过use
方法注册,后者通过VERB
(或all
)方法注册。其中
前者匹配所有以path
开始的路径,而后者会精确匹配path
路径;
前者对于请求的方法不做要求,而后者只能处理指定的请求。
1 2 3 4 5 6 7 app.use ('/user' , function (req, res, next ) { }); app.get ('/user' , function (req, res, next ) { });
app层面 在app层面,即Application.js
中,是由app.use(fn)
, app.VERB
(或app.all
)和app.handle(req, res, cb)
完成的。而它们只是router的同名方法的简单封装。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 app.use = function use (fn ) { var offset = 0 ; var path = '/' ; if (typeof fn !== 'function' ) { var arg = fn; while (Array .isArray (arg) && arg.length !== 0 ) { arg = arg[0 ]; } if (typeof arg !== 'function' ) { offset = 1 ; path = fn; } } var fns = flatten (slice.call (arguments , offset)); ... ... fns.forEach (function (fn ) { if (!fn || !fn.handle || !fn.set ) { return router.use (path, fn); } ... router.use (path, function mounted_app (req, res, next ) { var orig = req.app ; fn.handle (req, res, function (err ) { setPrototypeOf (req, orig.request ) setPrototypeOf (res, orig.response ) next (err); }); }); ... }, this ); return this ; };
其中flatten 用于将多层嵌套数组扁平化为1层。可见到,app的use
方法在做了预处理工作后,调用router的use
完成注册工作。
需要路由中间件时,我们需要使用动词对应的方法(或all
)去注册。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 methods.forEach (function (method ){ app[method] = function (path ){ if (method === 'get' && arguments .length === 1 ) { return this .set (path); } this .lazyrouter (); var route = this ._router .route (path); route[method].apply (route, slice.call (arguments , 1 )); return this ; }; });
app的handle
方法就比较简单了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 app.handle = function handle (req, res, callback ) { var router = this ._router ; var done = callback || finalhandler (req, res, { env : this .get ('env' ), onerror : logerror.bind (this ) }); if (!router) { debug ('no routes defined on app' ); done (); return ; } router.handle (req, res, done); };
Router层面 Express里中间件的具体实现在Router对象中。Router包含了Express中最为核心的概念。app中的许多API都是对Router API的简单封装。可以通过app._router
来访问app的Router对象。Router的源码位于lib/router/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 var proto = module .exports = function (options ) { var opts = options || {}; function router (req, res, next ) { router.handle (req, res, next); } setPrototypeOf (router, proto) ... router.stack = []; return router; };
Router对象有一个关键属性stack
,为一个数组,存放着所有的中间件。每一个中间件都是一个Layer对象,如果该中间件是一个路由中间件,则相应的Layer对象的route属性会指向一个Route对象,表示一条路由。
注册 每次调用app.use()
时,会执行router.use()
为stack
属性添加一个新的中间件,这个中间件是由Layer对象包装的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 proto.use = function use (fn ) { ... if (callbacks.length === 0 ) { throw new TypeError ('Router.use() requires middleware functions' ); } for (var i = 0 ; i < callbacks.length ; i++) { var fn = callbacks[i]; ... var layer = new Layer (path, { sensitive : this .caseSensitive , strict : false , end : false }, fn); layer.route = undefined ; this .stack .push (layer); } return this ; };
对于路由中间件要复杂些,路由中间件是通过router.route()
方法注册的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 proto.route = function route (path ) { var route = new Route (path); var layer = new Layer (path, { sensitive : this .caseSensitive , strict : this .strict , end : true }, route.dispatch .bind (route)); layer.route = route; this .stack .push (layer); return route; };
看来,还需要去route.js
中看看这个对象的定义。
1 2 3 4 5 6 7 8 9 10 function Route (path ) { this .path = path; this .stack = []; debug ('new %o' , path) this .methods = {}; }
可以看到,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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 methods.forEach (function (method ){ Route .prototype [method] = function ( ){ var handles = flatten (slice.call (arguments )); for (var i = 0 ; i < handles.length ; i++) { var handle = handles[i]; ... var layer = Layer ('/' , {}, handle); layer.method = method; this .methods [method] = true ; this .stack .push (layer); } return this ; }; });
即,当调用route.VERB()的时候,新建一个layer放在route.stack中。
通过上面的分析可以发现,Router其实是一个二维的结构。一个可能的router.stack结构如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ---------------- | layer1 | ---------------- ↓ ---------------- layer2.route.stack ------------ ------------ ------------ | layer2 | ------------------> | layer2-1 |-->| layer2-2 |-->| layer2-3 | ---------------- ------------ ------------ ------------ ↓ ---------------- layer3.route.stack ------------ ------------ | layer3 | ------------------> | layer3-1 |-->| layer3-2 | ---------------- ------------ ------------ ↓ ---------------- | ...... | ---------------- ↓ ---------------- | layerN | ----------------
Layer Layer对象的构造函数如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function Layer (path, options, fn ) { if (!(this instanceof Layer )) { return new Layer (path, options, fn); } debug ('new %o' , path) var opts = options || {}; this .handle = fn; this .name = fn.name || '<anonymous>' ; this .params = undefined ; this .path = undefined ; this .regexp = pathRegexp (path, this .keys = [], opts); this .regexp .fast_star = path === '*' this .regexp .fast_slash = path === '/' && opts.end === false }
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 proto.handle = function handle (req, res, out ) { ... var stack = self.stack ; ... next (); function next (err ) { ... while (match !== true && idx < stack.length ) { ... } if (match !== true ) { return done (layerError); } ... } function trim_prefix (layer, layerError, layerPath, path ) { ... } };
在初始化和预处理后,调用了next()
函数。在next()
函数中,主要的部分是while语句判断是否遍历完成整个stack
,完成后执行done()。这部分代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 while (match !== true && idx < stack.length ) { layer = stack[idx++]; match = matchLayer (layer, path); route = layer.route ; if (typeof match !== 'boolean' ) { layerError = layerError || match; } if (match !== true ) { continue ; } if (!route) { continue ; } if (layerError) { match = false ; continue ; } var method = req.method ; var has_method = route._handles_method (method); if (!has_method && method === 'OPTIONS' ) { appendMethods (options, route._options ()); } if (!has_method && method !== 'HEAD' ) { match = false ; continue ; } }
其中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 2 3 4 5 6 7 8 9 10 11 self.process_params (layer, paramcalled, req, res, function (err ) { if (err) { return next (layerError || err); } if (route) { return layer.handle_request (req, res, next); } trim_prefix (layer, layerError, layerPath, path); });
从回调函数中可以看到,如果是路由中间件可以直接调用layer.handle_request(req, res, next)
执行真正的中间件函数。如果是普通中间件,还需要在trim_prefix
中对路径处理后才会调用layer.handle_request(req, res, next)
。
1 2 3 4 5 6 7 8 9 function trim_prefix (layer, layerError, layerPath, path ) { ... if (layerError) { layer.handle_error (layerError, req, res, next); } else { layer.handle_request (req, res, next); } }
在路由中间件中,layer.handle_request(req, res, next)
调用的中间函数实际上是route.dispatch
方法,在Route对象内容分发请求,它的逻辑相对router.handle
要简单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 Route .prototype .dispatch = function dispatch (req, res, done ) { var idx = 0 ; var stack = this .stack ; if (stack.length === 0 ) { return done (); } var method = req.method .toLowerCase (); if (method === 'head' && !this .methods ['head' ]) { method = 'get' ; } req.route = this ; next (); function next (err ) { if (err && err === 'route' ) { return done (); } if (err && err === 'router' ) { return done (err) } var layer = stack[idx++]; if (!layer) { return done (err); } if (layer.method && layer.method !== method) { return next (err); } if (err) { layer.handle_error (err, req, res, next); } else { layer.handle_request (req, res, next); } } };
可以看到,next
函数逻辑像下面这样
如果有错,直接调用done()
传递错误
获取layer
,如果不存在,调用done()
layer
和当前方法不匹配时,跳过当前layer
,执行next()
,继续下一个route函数
layer
和当前方法匹配时,根据有无错误执行layer.handle_error(err, req, res, next)
或是layer.handle_request(req, res, next)
。
综上,一个请求到达时,流程顺序像下面这样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 ↓ ---------------- | layer1 | ---------------- ↓ ---------------- | layer2 | ---------------- ↓ ---------------- layer3.route.stack ------------ ------------ ------------ | layer3 | ------------------> | layer3-1 |-->| layer3-2 |-->| layer3-3 | --- ---------------- ------------ ------------ ------------ | | --------------------------------------------------------------------------- ↓ ---------------- layer4.route.stack ------------ ------------ | layer4 | ------------------> | layer4-1 |-->| layer4-2 | --- ---------------- ------------ ------------ | | ------------------------------------------------------------ ↓ ---------------- | ...... | ---------------- ↓ ---------------- | layerN | ---------------- ↓
每个中间件的处理过程则像下面这样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 ↓ No -------------------- ------| path match | | -------------------- | ↓ Yes | -------------------- Yes --------------------- No | | has route |-------| http method match |------ | -------------------- --------------------- | | ↓ No | Yes | | -------------------- | | | | process params |<----------------- | | -------------------- | | ↓ | | -------------------- | | | execute function | | | -------------------- | | ↓ | | -------------------- | ----->| next layer |<--------------------------------- -------------------- ↓
在参数处理的过程中,每个参数的处理函数只会执行一次,并将结果保存在缓存中。在处理同一个请求的过程中,如果需要处理某个参数,会首先检查缓存,如果缓存中不存在,才会执行其处理函数。
内置中间件 在middleware
目录下,放置了两个Express内置中间件,在app.lazyRouter
中自动加载。
1 2 3 4 5 6 7 8 9 10 11 app.lazyrouter = function lazyrouter ( ) { if (!this ._router ) { this ._router = new Router ({ caseSensitive : this .enabled ('case sensitive routing' ), strict : this .enabled ('strict routing' ) }); this ._router .use (query (this .get ('query parser fn' ))); this ._router .use (middleware.init (this )); } };
其中第一个的作用是解析URL query,query(this.get('query parser fn'))
用于设置URL query解析器。第二个的作用是将req和res分别暴露给对方,并让它们分别继承自express定义的app.request
和app.response
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 exports .init = function (app ){ return function expressInit (req, res, next ){ if (app.enabled ('x-powered-by' )) res.setHeader ('X-Powered-By' , 'Express' ); req.res = res; res.req = req; req.next = next; setPrototypeOf (req, app.request ) setPrototypeOf (res, app.response ) res.locals = res.locals || Object .create (null ); next (); }; };
在express.js
中,req
和res
分别继承自了request.js
和response.js
的导出对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function createApplication ( ) { ... app.request = Object .create (req, { app : { configurable : true , enumerable : true , writable : true , value : app } }) app.response = Object .create (res, { app : { configurable : true , enumerable : true , writable : true , value : app } }) ... }
其他 关于参数处理和视图渲染,我看得不是很仔细,就不再赘述了。有兴趣的可以自行去参考中的链接学习。
参考