概述
Node.JS 官方提供的最简单的服务器例子如下:
- const server = http.createServer((req, res) => {
- res.statusCode = 200;
- res.setHeader('Content-Type', 'text/plain');
- res.end('Hello World!\n');
- });
Express 框架没有那么神奇, 只是代理了 http.createServer(requestHandler) 中的 requestHandler. 并使用已经注册了的中间件和路由匹配响应传来的用户请求.
整体思路
通过阅读源码, 我觉得可以把 Express 逻辑分成两段: 启动服务和响应请求.
启动服务阶段指的是 http.createServer(requestHandler) 和 server.listener() 两个 API 被调用前执行的一系列初始化工作.
响应请求阶段指的是服务器接收来自客户端请求时触发的 request 事件的 handler.
启动服务阶段
启动服务最重要的部分就是注册中间件和路由了.
中间件和路由可以说是几乎所有服务器都会提供的功能. 在 Express 框架里, 中间件和路由都会抽象成 layer 对象, 在这篇文章里, 存储中间件 layer 对象的容器叫做中间件 router 对象, 存储路由 layer 对象的容器叫做路由 router 对象.
在 Express 框架里, 中间件就是匹配路径就会执行的回调, 而路由不仅要匹配路径还要匹配 http method(如 get,post 之类). 所以对于中间件 router 对象, 匹配路径之后会直接执行回调, 但是路由 router 对象的匹配路径之后执行的回调统一为 router.handle(req, res, next), 里面的逻辑会继续匹配 http method.
1. App.use 方法
不论是注册中间件 router 对象还是路由 router 对象, 我们都会使用 App.use.
App.use 方法实质上是调用它自身的 router 对象的 use 方法:
- var router = this._router;
- fns.forEach(function (fn) {
- // non-express App
- if (!fn || !fn.handle || !fn.set) {
- return router.use(path, fn);
- }
- debug('.use App under %s', path);
- fn.mountpath = path;
- fn.parent = this;
- // restore .App property on req and res
- 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);
- });
- });
- // mounted an App
- fn.emit('mount', this);
- }, this);
2. 中间件 router 对象
当我们调用类似 App.use('/', fn) 这样的语句, 其实就是注册中间件.
这里必须说明一下, 每一个 express App 初始化的时候会使用 App.lazyrouter() 来实例化一个 router 对象, 在这篇文章里, 我们姑且叫它中间件 router 对象, 因为它主要是负责储存中间件 layer 对象的, 但是它还可以注册 router 对象, 例如开发中我们会调用形如 App.use('/test', testRouter) 的语句.
中间件 router 对象维护这一个 stack 数组, 用来装载 Layer 对象.
当 router 对象的 use 方法被调用的时候, 就会把路径和回调封装成一个 Layer 对象, 并放入 stack 数组中.
请注意: 中间件 router 对象的 layer 对象的 route 是 undefined, 跟路由 router 对象的 layer 对象的 route 是不一样的.
- var layer = new Layer(path, {
- sensitive: this.caseSensitive,
- strict: false,
- end: false
- }, fn);
- layer.route = undefined;
- this.stack.push(layer);
3. 路由 router 对象
当我们调用形如 App.use('/test', testRouter) 的语句, 可以表述为注册了一个路由中间件, 而这个中间件就是下面的 router 函数:
- function router(req, res, next) {
- router.handle(req, res, next);
- }
为了区别与中间件 router 对象, 在这篇文章里, 把注册在中间件 router 对象上的路由中间件定义为路由 router 对象.
到这里, 我最想告诉大家的是, 在 express 里, router 对象是可以通过这种方式嵌套的.
就和前面提到的一样, 路由也会被抽象成 layer 对象, 并把 router 函数作为 Layer 构造函数的第三个参数传入.
4. HTTP Method 方法和 Route 实例
HTTP Method 指的是 get,post,put,delete,header 之类的 http 请求方法.
路由 router 对象不仅需要匹配路径还需要匹配 HTTP Method. 而负责匹配 HTTP Method 的功能是由 Route 实例来完成.
当我们在调用 App[method] 或者 router[method] 时, 就是在调用 router.route 方法 (就是下面的 this.route(path)), 如下:
- // create Router#VERB functions
- methods.concat('all').forEach(function(method){
- proto[method] = function(path){
- var route = this.route(path)
- route[method].apply(route, slice.call(arguments, 1));
- return this;
- };
- });
router.route 方法里面会生成一个新的 layer 对象, 并把回调设置为 route.dispatch.bind(route), 这一点与前面提到的中间件 router 对象不同, 而且 layer 的 route 不再是 undefined, 最后返回新的 Route 实例. 代码如下:
- 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 实例的作用是什么呢? 先看看它的构造函数:
- function Route(path) {
- this.path = path;
- this.stack = [];
- debug('new %o', path)
- // route handlers for various http methods
- this.methods = {};
- }
Route 实例维护着一个 stack 数组, 作用是收集 Layer 对象; 还维护这一个 methods 对象, 作用是指示该 route 对象可以匹配的 http methods.
route 收集的 Layer 对象维护着路由真正的回调, 就是下面的 handle:
- var layer = Layer('/', {
- }, handle);
- layer.method = method;
- this.methods[method] = true;
- this.stack.push(layer);
5. Layer 对象
一个 Layer 对象维护这一个路径和回调, 它会把路径正则表达式化, 用以在响应请求阶段匹配路径, 先看看它的构造函数:
- 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);
- // set fast path flags
- this.regexp.fast_star = path === '*'
- this.regexp.fast_slash = path === '/' && opts.end === false
- }
有三种 layer 对象:
Layer 类别 | route | method |
---|---|---|
中间件 Layer | undefined | undefined |
路由 Layer | 非 undefined | undefined |
route Layer | undefined | 非 undefined |
中间件 Layer 实例的回调是 fn, 也就是注册的中间件函数; 路由 Layer 实例的回调都是 function router(req, res, next);route Layer 实例的回调都是 route.dispatch.bind(route).
响应请求阶段
通过启动服务阶段, 我们已经把服务器的准备工作完成 -- 注册了中间件和路由.
当应用执行到 server.listener() 时, 就可以开始接受并处理客户端的请求, 最后返回服务器响应.
1. 增强 req 对象和 res 对象
当一个请求到来的时候, Node.JS 会把请求抽象成 req(http.IncomingMessage 的实例), 把响应抽象成 res(http.ServerResponse 的实例), 传入 server 的 request 事件的 handler, 但是在 Express 框架里, req 对象和 res 对象被增强了.
增强内容可以参考 express.JS 同目录下的 request.JS 和 response.JS.
那么是怎么增强的呢?
在 App.lazyrouter 方法里, 已经添加了一个中间件, 就是下面的 middleware.init(this)
- 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));
- }
- };
而在 middleware.init(this) 里, 可以看到重新设置了 req 和 res 的原型:
- 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();
- };
- };
2. 正则表达式匹配中间件和路由
由于在启动服务阶段, 我们已经注册好了中间件和路由, 并把它们都抽象成 layer 对象, 所以在处理请求阶段的时候, 就清晰明了了.
基本逻辑是: 遍历 router 维护的 stack 容器; 对于中间件 layer(就是 layer.route 为 undefined 的), 路径匹配成功后就可以执行中间件函数了; 对于路由 layer(就是 layer.route 不是 undefined 的), 路径匹配成功后还需要匹配 http method 才能执行路由函数.
这一过程, 有如下的重要方法:
App.handle,express App 处理请求的入口, 实质上是调用了自身 router 的 handle router.handle, 遍历 router 维护的 stack 数组, 找到匹配路径的 layer 对象 Route.prototype._handles_method, 对于路由 layer 对象, 还需要这个方法验证是否可以匹配 http method Route.prototype.dispatch, 遍历 route 维护的 stack 数组, 找到匹配路径和 http method 的 layer 对象 Layer.prototype.match, 路径匹配的关键 Layer.prototype.handle_request, 匹配成功后执行回调
3. 模板引擎
模板引擎并不是 express 作者原创的, 而是引入了别的第三方库, 然后使用第三方库提供的 API 渲染出响应页面, 并返回给客户端.
目前支持较多的是 ejs 和 pug 这两个模板引擎.
Express 镶嵌
一个 Express App 是可以挂载到另一个 Express App 上的, 因为本质上一个 Express App 就是为了维护起自身的 router 对象, 所以挂载的方式其实就是在 parent express App 的上注册一个中间件, 该中间件负责把 req 和 res 传递给 child express App, 并让它们建立起父子关系, 源码如下:
- // restore .App property on req and res
- 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);
- });
- });
来源: https://juejin.im/post/5bb8405cf265da0ab719c259