当前阅读的 Express 版本:4.17.1。基本源码目录:
Express
├── benchmarks 基准相关
├── examples 案例代码
├── lib express 核心源码目录
│ ├── middleware 中间件相关
│ │ ├── init.js 将新增加在 req 和 res 的功能挂载到原始请求的 req 和 res 的原型上
│ │ └── query.js 将请求 url 中的 query 部分添加到 req.query
│ ├── router 路由相关(核心)
│ │ ├── index.js
│ │ ├── layer.js
│ │ └── route.js
│ ├── application.js 创建 express 应用后可直接调用的 api 均在此处(核心)
│ ├── express.js 创建 express 应用
│ ├── request.js 丰富了 http 中 request 实例的功能
│ ├── response.js 丰富了 http 中 response 实例的功能
│ ├── utils.js 一些辅助工具函数
│ ├── view.js 封装了模板渲染引擎,通过 res.render() 调用引擎渲染网页
├── test 单元测试
├── index.js require('express') 的入口
整体上,express 的源码目录是相对简单的,核心源码在 lib 目录下
先从入口开始,当执行:const express = require('express')
的时候,会取到 express 源码的根目录,找到 index.js 文件
express\index.js
module.exports = require('./lib/express');
这个文件就只有简单的一行有意义的代码,就是将 ./lib/express
引入并导出
再看看,./lib/express.js
中导出了什么
express\lib\express.js
// ...
exports = module.exports = createApplication;
function createApplication() {/.../}
./lib/express.js
里面实际上就是导出了 createApplication 函数
也就是说,我们通过 require('express')
引进来的实际上就是 createApplication
函数对象
到此,入口结束,下面来看看 createApplication
这个函数对象
先来看一段使用例子:
const express = require('express')
const app = express()
上一节入口分析得到,require('express')
得到的就是 createApplication
函数对象
express\lib\express.js
var EventEmitter = require('events').EventEmitter;
var mixin = require('merge-descriptors');
var proto = require('./application');
var req = require('./request');
var res = require('./response');
function createApplication() {
// 定义了一个 app 函数
var app = function(req, res, next) {
app.handle(req, res, next);
};
// 通过对象合并的方式,往 app 上挂载一系列方法,mixin 实用了 merge-descriptors 这个库的能力
// 往 app 上混入 EventEmitter.prototype 上的属性和方法
mixin(app, EventEmitter.prototype, false);
// 往 app 上混入 application.js 里面定义的方法
mixin(app, proto, false);
// 将 Node 的 http.IncomingMessage 类放到 app.request 上
// req 实际上是 req = Object.create(http.IncomingMessage.prototype)
// 也就是说 app.request 的核心还是 Node 的 http 的 request 实例
// 但是 express 往 req 上拓展了不少功能,例如: res.param 等,具体可以看 request.js 文件
app.request = Object.create(req, {
app: { configurable: true, enumerable: true, writable: true, value: app }
})
// 将 Node 的 http.ServerResponse 类放到 app.response 上
// res 实际上是 res = Object.create(http.ServerResponse.prototype)
// 也就是说 app.response 的核心还是 Node 的 http 的 response 实例
// 但是 express 往 res 上拓展了不少功能,例如: res.send 等,具体可以看 response.js 文件
app.response = Object.create(res, {
app: { configurable: true, enumerable: true, writable: true, value: app }
})
// 调用 app.init 进行初始化
// 这个 init 函数是什么时候被挂载上去的?就是在 mixin(app, proto, false) 的时候
// proto 其实就是通过 require('./application') 引进来的
// application 这个里面定义了一系列往 app 上挂载的方法
app.init();
// 最后将定义的 app 函数返回
return app;
}
createApplication 做的事:
-
定义了一个 app 函数
-
通过 mixin 往 app 函数对象上混入一系列方法
-
将 Node 的 http 模块的 request、response 实例分别挂在到 app.request 和 app.response 上
-
调用在 mixin 阶段混入的 app.init 进行初始化
app.init = function init() { this.cache = {}; this.engines = {}; this.settings = {}; this.defaultConfiguration(); };
-
将 app 函数对象返回
所以,const app = express()
得到的这个 app 对象实际上就是 createApplication 里面定义的 app 函数对象
使用 express 开启服务器最主要的三步:
const express = require('express')
const app = express()
app.listen(9000, () => {
console.log('服务器开启: 0.0.0.0:9000');
})
第三步就是: app.listen
通过上一节知道,app 实际上就是 createApplication 里面定义的 app 函数对象,而在 app 的 listen 方法其实是在 mixin 往 app 混入方法的时候挂载到 app 上的,这些混入的方法基本都定义在 application.js 里面:
express\lib\express.js
var mixin = require('merge-descriptors');
var proto = require('./application');
function createApplication() {
var app = function(req, res, next) {
app.handle(req, res, next);
};
mixin(app, proto, false);
// ...
}
接下来看看 app.listen 被定义的地方:
express\lib\application.js
var http = require('http');
// 定义 app 函数对象上的 listen 方法
// 实际上还是通过 Node 的 http 模块开启服务
app.listen = function listen() {
// 通过 Node 的 http.createServer 创建一个服务
// 这里的 this 就是 app 函数对象本身
var server = http.createServer(this);
// 开启这个服务
return server.listen.apply(server, arguments);
};
app.listen 其实很简单,就是依赖于 Node 原生的 http 模块开启一个服务,比如 Node 开启服务的代码:
const http = require('http')
// 创建一个服务器
const server = http.createServer((req, res) => {
res.end('server success')
})
// 启动服务器,指定端口号和主机地址
server.listen(9000, () => {
console.log(`服务器已启动:0.0.0.0:9000`)
})
这一看,其实就完全对上了,app.listen 主要作用:
-
首先,通过 http.createServer 创建一个服务 server,创建这个服务需要一个回调函数作为参数,这里传的是 this,实际上就是 app 函数本身
function createApplication() { var app = function(req, res, next) { app.handle(req, res, next); } }
也就是说后续需要执行这个回调的时候,实际上执行的就是 app 函数里面的 app.handle 函数
-
通过 server.listen 开启服务,这里会通过 apply 的方式将参数传进来
先来看一段实例代码:
const express = require('express')
const app = express()
const myMiddleFun = (req, res, next) => {
console.log('中间件')
next()
}
app.use(myMiddleFun)
app.listen(9000, () => {
console.log('服务器启动: 0.0.0.0:9000')
})
接下来看看 app.use 函数:
express\lib\application.js
// app.use 的使用形式
// 1、app.use((req, res, next) => {}) // 只传了中间件函数
// 2、app.use('/', (req, res, next) => {}) // 传了路径和中间件函数
// 3、app.use('/',(req, res, next) => {},(req, res, next) => {},...) // 连续注册多个中间件
app.use = function use(fn) {
var offset = 0; // 定义偏移量
var path = '/'; // 定义路径,默认为 '/'
// fn 代表使用 app.use 传进来的第一位参数
// 如果 fn 不是函数形式,那么就是 app.use('/', (req, res, next) => {})
if (typeof fn !== 'function') {
// 将第一位参数给 arg
var arg = fn;
// 如果是数组,取出第一位
while (Array.isArray(arg) && arg.length !== 0) {
arg = arg[0];
}
// first arg is the path
if (typeof arg !== 'function') {
offset = 1; // offset 偏移量赋值为 1
path = fn; // 将路径赋值给 path
}
}
// 将参数 arguments 从 offset 偏移量后开始切割得到中间件函数数组
// offset:如果第一位参数传的是路径,那么 offset=1,代表 arguments 从 1 之后的才是中间件函数
// 否则,offset=0
// flatten 的作用是扁平化数组
// app.use('/', (req, res, next) => {}, (req, res, next) => {}, ...)
var fns = flatten(slice.call(arguments, offset));
// fns 长度为 0,说明没有传入中间件函数,报错
if (fns.length === 0) {
throw new TypeError('app.use() requires a middleware function')
}
// 主要就是通过 new Router 创建了 Router 实例挂载到 app._router 上
this.lazyrouter();
// 从 app._router 上取出 Router 实例
var router = this._router;
// 遍历中间件函数数组
fns.forEach(function (fn) {
// non-express app
// 从这里可以看出,实际上 app.use 是调用 router.use 进行中间件注册
// 第一次的时候,中间件的回调函数一般都没有 handle 和 set,即 fn.handle 和 fn.set 为 undefined
if (!fn || !fn.handle || !fn.set) {
return router.use(path, fn);
}
//...
}, this);
// this 就是 app 函数对象,将 this 返回使得支持链式调用
return this;
};
**总结:**app.use 里面最核心的就是调用了 router.use,也就是说,app.use 只是 router.use 的一层包装,它的本质还是 router.use
在说 router 之前,得先了解 router 是怎么的来的,回到之前的 app.use = function use(fn) {}
,创建 router 的是:
express\lib\application.js
app.use = function use(fn) {
// ...
// 主要就是通过 new Router 创建了 Router 实例挂载到 app._router 上
this.lazyrouter();
// 从 app._router 上取出 Router 实例
var router = this._router;
// 遍历中间件函数数组
fns.forEach(function (fn) {
// non-express app
// 从这里可以看出,实际上 app.use 是调用 router.use 进行中间件注册
// 第一次的时候,中间件的回调函数一般都没有 handle 和 set,即 fn.handle 和 fn.set 为 undefined
if (!fn || !fn.handle || !fn.set) {
return router.use(path, fn);
}
//...
}, this);
};
首先,可以知道 router 来自于 app._router
,那么 app._router
又是什么时候被赋值的呢?答案在 this.lazyrouter()
中
来看看 app.lazyrouter()
所做的事:
express\lib\application.js
var Router = require('./router');
app.lazyrouter = function lazyrouter() {
// 如果 app 上没有 _router
if (!this._router) {
// 创建一个 Router 实例挂载到 app 上
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));
}
}
可以发现,app._router 是通过 new Router 得到的实例,而 Router 是通过 var Router = require('./router/index.js')
得到
接下来看看 router\index.js
,这个模块里面导出的是:
express\lib\router\index.js
var proto = module.exports = function(options) {
var opts = options || {};
// 定义 router 函数对象
function router(req, res, next) {
router.handle(req, res, next);
}
// mixin Router class functions
// 往 router 实例上混入一系列方法,这些方法定义在 proto 上
setPrototypeOf(router, proto)
// 继续往 router 函数对象上挂载一些其他属性
router.params = {};
router._params = [];
router.caseSensitive = opts.caseSensitive;
router.mergeParams = opts.mergeParams;
router.strict = opts.strict;
// 定义 stack 用于存储中间件
router.stack = [];
// 返回 router,在 new Router 后得到的就是这个 router 函数
return router;
}
- 定义了 router 函数
- 通过
setPrototypeOf(router, proto)
往 router 上混入定义在 proto 上的一些方法 - 继续混入其他属性,例如 router.params 、router.stack 等
- 最后将 router 函数返回
也就是说,new Router 的到的是返回的 router 函数,而 router.use 毫无疑问,就是在
setPrototypeOf(router, proto) 时混入到 router 中的
express\lib\router\index.js
// router.use 是在这里定义的
// 这里需要注意的是 this 指向的问题,如果是执行 router.use,那么此时 this 指向的是 router
// router.use 的使用形式
// 1、router.use((req, res, next) => {}) // 只传了中间件函数
// 2、router.use('/', (req, res, next) => {}) // 传了路径和中间件函数
// 3、router.use('/', (req, res, next) => {}, (req, res, next) => {}, ...) // 连续注册多个中间件
proto.use = function use(fn) {
var offset = 0; // 偏移量
var path = '/'; // 路径
// 如果 fn 不是函数形式,那么就是 router.use('/', (req, res, next) => {})
if (typeof fn !== 'function') {
// 将第一位参数给 arg
var arg = fn;
// 如果是数组,取出第一位
while (Array.isArray(arg) && arg.length !== 0) {
arg = arg[0];
}
if (typeof arg !== 'function') {
offset = 1; // offset 偏移量赋值为 1
path = fn; // 将路径赋值给 path
}
}
// 将参数 arguments 从 offset 偏移量后开始切割得到中间件函数数组
// offset:如果第一位参数传的是路径,那么 offset=1,代表 arguments 从 1 之后的才是中间件函数
// 否则,offset=0
// flatten 的作用是扁平化数组
// router.use('/', (req, res, next) => {}, (req, res, next) => {}, ...)
var callbacks = flatten(slice.call(arguments, offset));
// callbacks 长度为 0,说明没有传入中间件函数,报错
if (callbacks.length === 0) {
throw new TypeError('Router.use() requires a middleware function')
}
// 遍历中间件函数数组
for (var i = 0; i < callbacks.length; i++) {
var fn = callbacks[i];
// 中间件不是函数形式,报错
if (typeof fn !== 'function') {
throw new TypeError('Router.use() requires a middleware function but got a ' + gettype(fn))
}
// add the middleware
debug('use %o %s', path, fn.name || '<anonymous>')
// 实例化 Layer,并将 路径、中间件函数fn 以及一些其他参数传进去
// new Layer 时会将 中间件函数fn 挂载到实例 layer.handle 上
var layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: false,
end: false
}, fn);
// 设置
layer.route = undefined;
// 这里的 this 实际上是 router
// 就是将 layer 实例存到 router.stack 数组
// 当后面需要使用到中间件函数时,同 router.stack 中逐一取出 layer 实例,执行实例的 handle
this.stack.push(layer);
}
// 将 router 返回,用于链式调用
return this;
};
- 前面的参数处理过程与 app.use 一样
- 得到中间件数组后,遍历数组
- 将 path 路径,中间件函数fn 当做 new Layer 的参数,得到实例 layer
- 将实例 layer 添加到 router.stack 数组(后面需要执行中间件函数的时候,再把这些实例从 stack 中逐一取出来)
接下来看看 new Layer
做的事
express\lib\router\layer.js
// Layer 构造函数
function Layer(path, options, fn) {
if (!(this instanceof Layer)) {
return new Layer(path, options, fn);
}
debug('new %o', path)
var opts = options || {};
// 将 new Layer 传进来的中间件函数 fn 挂载到 Layer.handle
this.handle = fn;
this.name = fn.name || '<anonymous>';
this.params = undefined;
this.path = undefined;
this.regexp = pathRegexp(path, this.keys = [], opts);
// set fast path flags
this.regexp.fast_star = path === '*'
this.regexp.fast_slash = path === '/' && opts.end === false
}
可以看出,new Layer 得到的实例 layer 会有一些属性,其中比较重要的就是 layer.handle,这上面是挂载了当前的中间件函数
router.use 最主要的就是:
- 通过 new Layer 实例化的形式,往将
中间件函数
挂到 layer 实例的 handle 属性身上,每一个中间件函数
都会有一个 layer 实例去保存 - 将保存有
中间件函数
的 layer 实例放进 stack 数组
中间件调用的时机,也就是请求被处理的阶段,基本流程如下:
在 Node 中:
const http = require('http')
// 创建一个服务器
const server = http.createServer((req, res) => {
res.end('server success')
})
// 启动服务器,指定端口号和主机地址
server.listen(9000, () => {
console.log(`服务器已启动:0.0.0.0:9000`)
})
当外部访问 0.0.0.0:9000
的时候,会触发 http.createServer
的回调函数 (req, res) => {}
而 express 中,调用 http.createServer
的地方上面已经说过,就是在 app.listen 中
express\lib\application.js
// 定义 app 函数对象上的 listen 方法
// 实际上还是通过 Node 的 http 模块开启服务
app.listen = function listen() {
// 通过 Node 的 http.createServer 创建一个服务
// 这里的 this 就是 app 函数对象本身
var server = http.createServer(this);
// 开启这个服务
return server.listen.apply(server, arguments);
};
而这里 http.createServer
的回调函数是 this,也就是 app 函数本身,再看回 app 函数:
express\lib\express.js
function createApplication() {
// 定义了一个 app 函数
var app = function(req, res, next) {
app.handle(req, res, next);
};
//...
return app
}
也就是说,外部访问触发的回调是 app 函数,执行 app 函数实际上是执行 app.handle 函数
express\lib\application.js
// 外部访问服务,触发 app 函数调用,app 函数实际是调用 app.handle
app.handle = function handle(req, res, callback) {
// 取到 router 实例
var router = this._router;
//...
// app.handle 实际上是调用的 router.handle 方法
router.handle(req, res, done);
}
app.handle 的核心是执行 router.handle 函数
express\lib\router\index.js
// 定义 router.handle
proto.handle = function handle(req, res, out) {
// 将 router 实例保存在 self 上
var self = this;
debug('dispatching %s %s', req.method, req.url);
var idx = 0; // 索引
var protohost = getProtohost(req.url) || ''
var removed = '';
var slashAdded = false;
var paramcalled = {};
// store options for OPTIONS request
// only used if OPTIONS request
var options = [];
// middleware and routes
// 拿到 router 中存放 layer 实例的数组
// layer.handle 上挂载了中间件函数
var stack = self.stack;
// manage inter-router variables
var parentParams = req.params;
var parentUrl = req.baseUrl || '';
var done = restore(out, req, 'baseUrl', 'next', 'params');
// setup next layer
req.next = next;
// for options requests, respond with a default if nothing else responds
if (req.method === 'OPTIONS') {
done = wrap(done, function(old, err) {
if (err || options.length === 0) return old(err);
sendOptionsResponse(res, options, old);
});
}
// setup basic req values
req.baseUrl = parentUrl;
req.originalUrl = req.originalUrl || req.url;
// 执行 next 函数,也就是说,执行 router.handle 的时候,会自动调用 next
next();
function next() {/.../}
function trim_prefix() {/.../}
}
router.handle 中主要的逻辑就是调用了一次 next 函数,也就是说,当外部访问的时候,会触发:
app --> app.handle --> router.handle --> next
下面再来看看 next 函数:
express\lib\router\index.js
// 定义 router.handle
proto.handle = function handle(req, res, out) {
// 将 router 实例保存在 self 上
var self = this;
// ...
var idx = 0; // 记录当前查找的中间件layer 在 stack 中索引
// ...
// 拿到 router 中存放 layer 实例的数组
// layer.handle 上挂载了中间件函数
var stack = self.stack;
// ...
// 执行 next 函数,也就是说,执行 router.handle 的时候,会自动调用 next
next();
function next() {
// ...
var layer;
var match;
var route;
// 找到匹配的中间件 layer,当 match=true 代表找到匹配的
// 如果找到,match=true,就跳出 while 循环(match !== true 才继续循环),所以需要调用 next 再次进入
// 会判断请求路径、请求类型[post、get、...]
while (match !== true && idx < stack.length) {
// 取出 stack 数组中 idx 下标对应的 layer 实例
// idx++:当前是 0,那么 会取到 stack[0],执行完 stack[0],idx 加 1
layer = stack[idx++];
// 判断请求路径是否一致
match = matchLayer(layer, path);
route = layer.route;
if (typeof match !== 'boolean') {
// hold on to layerError
layerError = layerError || match;
}
if (match !== true) {
continue;
}
if (!route) {
// process non-route handlers normally
continue;
}
if (layerError) {
// routes do not match with a pending error
match = false;
continue;
}
// 获取请求类型,判断请求类型是否一致
var method = req.method;
var has_method = route._handles_method(method);
// build up automatic options response
if (!has_method && method === 'OPTIONS') {
appendMethods(options, route._options());
}
// 请求类型不一致,match 置为 false
if (!has_method && method !== 'HEAD') {
match = false;
continue;
}
}
// 没找到匹配的中间件,结束
if (match !== true) {
return done(layerError);
}
// this should be done for the layer
self.process_params(layer, paramcalled, req, res, function (err) {
if (err) {
return next(layerError || err);
}
if (route) {
// 执行 layer.handle_request
return layer.handle_request(req, res, next);
}
// 这里面的主要逻辑其实也是执行 layer.handle_request
trim_prefix(layer, layerError, layerPath, path);
});
}
function trim_prefix() {
// ...
if (layerError) {
layer.handle_error(layerError, req, res, next);
} else {
layer.handle_request(req, res, next);
}
}
}
next 的主要逻辑:
-
通过 while 循环从 stack 数组中找到第一个匹配的
中间件layer
-
中间件需要
路径
和请求类型
都匹配上app.use('/info', middleWare) app.get('/info', middleWare)
-
-
执行这个
中间件layer
的 handle_request
express\lib\router\layer.js
function Layer(path, options, fn) {
// ...
// 将 new Layer 传进来的中间件函数 fn 挂载到 Layer.handle
this.handle = fn;
// ...
}
Layer.prototype.handle_request = function handle(req, res, next) {
var fn = this.handle;
// ...
try {
fn(req, res, next);
} catch (err) {
next(err);
}
};
可以发现,Layer.handle_request 的主要逻辑就是将之前挂载在 layer.handle 上的中间件函数拿来执行 fn(req, res, next)
,并且将参数 req, res, next 传进去
那么当我们在使用中间件的时候:
- 如果调用了 next,那么会继续从 stack 数组中寻找下一个符合条件的
中间件layer
。(因为之前的 idx 已经改变,所以会从下一个索引位置开始查找) - 如果没有调用 next,那么会停止,不会继续查找下一个中间件
这也是为什么,写了一堆中间件,条件都符合,如果没有执行 next,那么永远只会执行第一个中间件
中间件的触发,主要就是外部访问,从存储所有中间件layer
的 stack 数组中找到匹配的 中间件layer
,执行 layer.handle_request
,实际上就是执行 layer.handle,而以前就是把中间件函数挂载在 layer.handle 上的,所以实际就是执行中间件函数,并且会把 next 当做参数传进去,当使用的时候调用了 next,继续去 stack 数组中查找下一个匹配的中间件layer
基本流程就是:
express\lib\express.js
var bodyParser = require('body-parser')
exports = module.exports = createApplication;
// 上面有 exports = module.exports = createApplication
// 所以往 exports 上加东西实际上就是往 createApplication 中加东西
// 暴露一些内置的 express 中间件
// 使用 body-parser 库的 json 处理 body 是 application/json 格式的
exports.json = bodyParser.json
// 处理 url 的 query 参数,例如: http://127.0.0.1:9000/info?name=jack
exports.query = require('./middleware/query');
// 使用 body-parser 库的 raw 处理 body 是 raw 格式
exports.raw = bodyParser.raw
// 使用 serve-static 库作为 express 开启静态资源服务器的能力
exports.static = require('serve-static');
// 使用 body-parser 库的 text 处理 body 是 tetx 格式
exports.text = bodyParser.text
// 使用 body-parser 库的 urlencoded 处理 body 是 x-www-form-urlencoded 格式
exports.urlencoded = bodyParser.urlencoded
Express 内置了一些中间件的功能,例如解析 body 参数为 application/json、x-www-form-urlencoded
可以直接使用:
const express = require('express')
const app = express()
app.use(express.json())
app.use(express.urlencoded())
其实,上面那两个,express 内部还是依赖于 body-parse 的
参考文章: