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源码解析》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

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

用户故事地图

用户故事地图

Jeff Patton / 李涛、向振东 / 清华大学出版社 / 2016-4-1 / 59.00元

用户故事地图作为一种有效的需求工具,越来越广泛地应用于开发实践中。本书以用户故事地图为主题,强调以合作沟通的方式来全面理解用户需求,涉及的主题包括怎么以故事地图的方式来讲用户需求,如何分解和优化需求,如果通过团队协同工作的方式来积极吸取经验教训,从中洞察用户的需求,开发真正有价值的、小而美的产品和服务。本书适合产品经理、用户体验设计师、产品负责人、业务分析师、IT项目经理、敏捷教练和精益教练阅读和......一起来看看 《用户故事地图》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具