Express原始碼解析

老臘肉學長發表於2019-03-04

概述

NodeJS官方提供的最簡單的伺服器例子如下:

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader(`Content-Type`, `text/plain`);
  res.end(`Hello World!
`);
});
複製程式碼

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原始碼@4.16.3
Express中文文件

相關文章