Express源码解析

栏目: Node.js · 发布时间: 6年前

内容简介:NodeJS官方提供的最简单的服务器例子如下:Express框架没有那么神奇,只是代理了通过阅读源码,我觉得可以把Express逻辑分成两段:启动服务和响应请求。

NodeJS官方提供的最简单的服务器例子如下:

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对象

当一个请求到来的时候,NodeJS会把请求抽象成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渲染出响应页面,并返回给客户端。

目前支持较多的是 ejspug 这两个模板引擎。

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);
    });
});
复制代码

以上所述就是小编给大家介绍的《Express源码解析》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

汇编语言(第3版)

汇编语言(第3版)

王爽 / 清华大学出版社 / 2013-9 / 36.00元

《汇编语言(第3版)》具有如下特点:采用了全新的结构对课程的内容进行组织,对知识进行最小化分割,为读者构造了循序渐进的学习线索;在深入本质的层面上对汇编语言进行讲解;对关键环节进行深入的剖析。《汇编语言(第3版)》可用作大学计算机专业本科生的汇编教材及希望深入学习计算机科学的读者的自学教材。一起来看看 《汇编语言(第3版)》 这本书的介绍吧!

html转js在线工具
html转js在线工具

html转js在线工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具