【译】Express.js 工作原理
February 14, 2020
web 开发者想必都一定听说过 express.js。Express 是 node 中比较受欢迎的轻量的 web 服务器框架。
本文我们将通过分析 express 源码来了解在底层它是如何工作的。学习一个流行框架的原理,可以帮助我们更好地使用它们来构建应用程序,减少引入它们带来的“魔法”问题。
本文目录:
为了更好地理解源码,将它 clone 到本地是比较好地方式,本文使用的版本。 评论中的
// ...
是部分源码为了简洁做了省略。
“Hello World” 例子
我们从官网的“Hello world”例子开始深入源码:
const express = require('express');
const app = express();
app.get('/', (req, res) => res.send('Hello World!'));
app.listen(3000, () => console.log('Example app listening on port 3000!'));
上述代码启动了端口号 3000 的 HTTP 服务器,并在命中 GET /
路由后发送“Hello World!”文本响应。笼统地来讲,我们可以从以下四个阶段来分析:
- 创建一个新的 express 应用
- 创建一个新的路由
- 基于给定的端口号启动 HTTP 服务器
- 当有请求进来时做出对应处理
创建新的 express 应用
const app = express()
这个表达式可以创建一个新的 express 应用。在 lib/express.js 中的 createApplication
函数是默认导出的对象,就是我们看到的 express()
函数调用。
比较关键的代码是:
// ...
var mixin = require('merge-descriptors');
var proto = require('./application');
// ...
function createApplication() {
// 这个是该函数返回的变量,具体我们会在后面讲到,重要的是要记住它的签名:function(req, res, next)
var app = function(req, res, next) {
app.handle(req, res, next);
};
// ...
// `mixin` 函数将所有 `proto` 的方法合并至 `app`,我们例子中用到的 `get` 方法就是合并进来的
mixin(app, proto, false);
// ...
return app;
}
从这个函数中返回的app
对象就是我们程序中用到的。app.get
方法通过 merge-descriptors 库中的 mixin
函数从 proto
中获取并添加的。
proto
它是从 lib/application 中导入的。
创建新的路由
我们下面来简单看一下 app.get
方法在源码中的实现:
var slice = Array.prototype.slice;
// ...
/**
* Delegate `.VERB(...)` calls to `router.VERB(...)`.
*/
// `methods` 是一个包含 ['get', 'post', ...] 等 HTTP 方法的数组
methods.forEach(function(method) {
// 这应该是 app.get 方法的签名
app[method] = function(path) {
// 一些初始化的代码
// 在应用的 router 内部根据 path 创建一个 route
var route = this._router.route(path);
// 根据从第二个参数开始后续的参数来调用 handler
route[method].apply(route, slice.call(arguments, 1));
// 返回 `app` 实例,来使方法可以链式调用
return this;
};
});
这里实现比较巧妙的是,所有的 HTTP 动词方法,如 app.get
,app.post
,app.put
本质上功能都是类似的。如果我们只关注 get
方法,可以简化为:
app.get = function(path, handler) {
// ...
var route = this._router.route(path);
route.get(handler);
return this;
};
尽管上面的函数有两个参数,它实际和 app[method] = function(path){...}
定义是相似的。第二个参数 handler
是在 slice.call(arguments, 1) 中获取的。
长话短说,
app.<method>
只是在应用 router 中使用route
方法保存 route,然后传递handler
到route.<method>
router 的 route()
方法在 lib/router/index.js 中定义:
// proto 是 `_router` 对象的原型定义
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.get
方法的定义与 app.get
类似,在 lib/router/route.js 中:
methods.forEach(function(method) {
Route.prototype[method] = function() {
// `flatten` 将嵌套的数组转换成一维的数组
var handlers = flatten(slice.call(arguments));
for (var i = 0; i < handles.length; i++) {
var handle = handles[i];
// ...
// 传给 route 的每个 handler 都会创建一个 layer,并添加到 route 栈中
var layer = Layer('/', {}, handle);
// ...
this.stack.push(layer);
}
return this;
};
});
每个 route 可以有多个 handler,并基于每个 handler 构造 Layer
并压入栈中。
分层
_router
和 route
都使用了同一类型的对象 Layer
。我们可以从构造函数的定义来观察 layer 是用来做什么的:
function Layer(path, options, fn) {
// ...
this.handle = fn;
this.regexp = pathRegexp(path, (this.keys = []), opts);
// ...
}
每个 layer 都有一个 path,一些 option 和一个待处理的函数。在我们的 router 中,这个函数是 route.dispatch
(我们会在下面的章节介绍用途,它类似于将请求传递到单个路由)。在 route 中,这个函数就是我们定义的 handler 函数。
每个 layer 也有一个处理请求的函数:handle_request
,在 Layer 初始化阶段真正执行传入的函数。
我们来回顾一下通过 app.get
方法创建一个 route:
- 在应用的 router(
this._router
) 上创建一个 route - route
dispatch
方法作为 handler 方法传递给 layer,这个 layer 压入 router 栈中 - 请求的 handler 本身作为 handler 方法传递给 layer,这个 layer 压入 route 栈
在最后,所有的 handler 以在 route 栈内部的 layer 的形式存储到 app
中,dispatch
方法传递给 layer 并存储在 router 栈上。
处理 HTTP 请求也与此类似,我们随后来了解。
启动 HTTP 服务
在配置好路由之后,服务器开始启动。在我们的例子中调用了 app.listen
方法,传入了端口号和回调函数。可以通过 lib/application.js 来了解:
app.listen = function listen() {
var server = http.createServer(this);
return server.listen.apply(server, arguments);
};
看起来 app.listen
只是包装了一下 http.createServer
。这是说的通的,回想一下第一章,app
实际上是 function(req, res, next){…} 签名的函数,与 http.createServer
所需要的函数签名 function(req, res) {…} 是兼容的。
当意识到这些时你会发现这样非常简洁,express.js 所提供的所有能力可以总结为是一个非常高明的处理函数。
处理 HTTP 请求
现在我们知道 app
实际是一个普通的请求处理对象,我们下面通过定位一个 HTTP 请求来最终定位到内部 handler 的位置。
从 lib/express.js 中的 createApplication
函数:
var app = function(req, res, next) {
app.handle(req, res, next);
};
请求传递到 app.handle
的方法在 lib/application.js 中定义:
app.handle = function handle(req, res, callback) {
// `this._router` 是我们从 `app.get` 处定义的路由
var router = this._router;
// ...
// 请求进一步传递到 `handle` 方法
router.handler(req, res, done);
};
router.handler
方法在 lib/router/index.js 中定义:
proto.handle = function handle(req, res, out) {
var self = this;
// ...
// self.stack 是在调用时压入的所有layer
var stack = self.stack;
// ...
next();
function next(err) {
// ...
// 根据请求获取path name
var path = getPathname(req);
// ...
var layer;
var match;
var route;
while (match !== true && idx < stack.length) {
layer = stack[idx++];
match = matchLayer(layer, path);
route = layer.route;
// ...
if (match !== true) {
continue;
}
// ... 一些验证 HTTP 方法,headers 的校验代码
}
// ... 一些验证
// 为 layer 处理所有参数
self.process_params(layer, paramcalled, req, res, function(err) {
if (route) {
// 一旦参数处理完成, 就会调用 `layer.handler_request` 方法
return layer.handle_request(req, res, next);
}
});
}
};
简单来讲,router.handle
函数在它的栈上遍历所有的 layer,直到找到匹配 path 的请求。最终会调用 layer 的 handle_request
方法,执行先前定义过的 handler 函数。这个 handler 函数就是在 [lib/router/route.js] 中定义的 dispatch
方法:
Route.prototype.dispatch = function dispatch(req, res, done) {
var stack = this.stack;
// ...
next();
function next(err) {
// ...
var layer = stack[idx++];
// ... 一些校验和错误检查
layer.handle_request(req, res, next);
// ...
}
};
与 router 类似,每个 route 遍历所有的 layer,调用它们的 handle_request
方法,即执行传递给 layer 的 handler 方法。最后,HTTP 请求到达应用的底层。
其他
尽管我们看到了 express 来使 web 服务器工作的的核心代码,它还提供了很多其他的功能。我们跳过了许多必要的验证和所有的帮助函数。最后,一个非常关键的中间件功能,它能够帮助我们劫持所有请求并做出对应的处理。
参考
https://www.sohamkamani.com/blog/2018/05/30/understanding-how-expressjs-works/