本文基于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 } }) ... }
其他 关于参数处理和视图渲染,我看得不是很仔细,就不再赘述了。有兴趣的可以自行去参考中的链接学习。
参考