Express原始碼學習-路由篇

Brolly發表於2018-03-23

Express 簡介

Express 是一個簡潔而靈活的 node.js Web應用框架, 提供了一系列強大特性幫助你建立各種 Web 應用,和豐富的 HTTP 工具。 使用 Express 可以快速地搭建一個完整功能的網站。

Express官網 也是一個非常友好的文件。

Express 框架核心特性:

  • 路由: 定義了路由表用於執行不同的 HTTP 請求動作。
  • 中介軟體: 可以設定中介軟體來響應 HTTP 請求。
  • 模板引擎: 可以通過向模板傳遞引數來動態渲染 HTML 頁面。

這裡先從Express路由開始學習解析,再繼續它的其他核心特性進行學習與探索。

安裝 Express (V4.16.2)

npm i express -S
複製程式碼

Express簡單使用

1.建立一個接收get請求的伺服器

// 1.get.js
const express = require('express');
const app = express();

const port = 8080;

// path 是伺服器上的路徑,callback是當路由匹配時要執行的函式。
app.get('/', function(req,res){ // 註冊一個get請求路由
    res.end('hello express!'); // 結束響應
});

app.listen(port,function(){ // 開啟監聽埠
    console.log(`server started on port ${port}`);
});

複製程式碼

2.執行上面程式碼

nodemon是一種實用工具,將為您的源的任何變化並自動重啟伺服器監控。


$ nodemon 1.get.js

瀏覽器訪問 localhost:8080 結果如下:
hello express!

若訪問一個未處理的路由路徑 如localhost:8080/user 結果如下:
Cannot GET /user  也就是我們常見的404 Not Found

複製程式碼

根據上面測試案例自己實現一個簡單express

express簡單實現

// express-1.0.js
const http = require('http');
const url = require('url');

function createApplication() {

    function app(req, res) {
        app.handle(req, res);
    }

    app.routes = []; // 路由容器
    app.get = function(path, handle) { // 註冊get方法的路由
        app.routes.push({ // 路由表
            path,
            handle,
            method: 'get'
        });
    }

    app.listen = function() { // 建立http伺服器
        const server = http.createServer(app);
        server.listen.apply(server, arguments);
    }

    app.handle = function(req, res) { // 路由處理
        let { pathname } = url.parse(req.url); // 獲取請求路徑
        let ids = 0;
        let routes = this.routes; // 獲取路由表
        function next() { // next控制路由進入下一匹配
            if (ids >= routes.length) {
                return res.end(`Cannot ${req.method} ${pathname}`);
            }

            let {path, method, handle} = routes[ids++];
            // 進行路由匹配
            if ((pathname === path || pathname === '*') && method === req.method.toLowerCase()) {
                return handle(req, res)
            } else { // 如果不匹配 則去下一個路由匹配
                next();
            }
        }
        next();
    }

    return app;
}

module.exports = createApplication;
複製程式碼

匯入我們自己的express.js


const express = require('./express-1.0.js');
const app = express();

app.get('/get', function(req, res) {
    res.send('hello express');
});
複製程式碼

下面我們開始進行原始碼分析

原始碼分析

express原始碼目錄結構

Express原始碼學習-路由篇

路由系統

對於路由中介軟體,在整個個Router路由系統中stack 存放著一個個layer, 通過layer.route 指向route路由物件, route的stack的裡存放的也是一個個layer,每個layer中包含(method/handler)。

Express原始碼學習-路由篇

在原始碼裡面主要涉及到幾個類和方法

createApplicaton
Application(proto)
Router
Route
Layer
複製程式碼
  1. express()返回一個app 實際上express指向內部createApplication函式 函式執行返回app
var mixin = require('merge-descriptors'); // merge-descriptors是第三方模組,合併物件的描述符
var proto = require('./application'); // application.js中 匯出 proto

function createApplicaton() { // express()
	var app = function(req, res, next) { // app是一個函式
        app.handle(req, res, next); // 處理路由
    };

    mixin(app, EventEmitter.prototype, false); // 將EventEmitter.prototype的物件屬性合併到app上一份
    mixin(app, proto, false); // 將proto中的掛載的一些屬性方法 如 app.get app.post 合併到app上
    app.init(); // 初始化 宣告一些私有屬性
    return app;
}
複製程式碼

app上的很多屬性方法都來自application.js匯出的proto物件, 在router/index.js中 也有一個命名為proto的函式物件 掛載了一些靜態屬性方法 proto.handle proto.param proto.use ...

// router/index.js
var proto = module.exports = function(options) {}

複製程式碼

在application.js 匯出的proto中 掛載這合併到app上的請求方法,原始碼如下:

// application.js

// methods是一個陣列集合[get, post, put ...],裡面存放了一系列http請求方法 通過遍歷給app上掛載一系列請求方法,即app.get()、app.post() 等
methods.forEach(function(method){
  app[method] = function(path){
    if (method === 'get' && arguments.length === 1) {
      // app.get(setting)
      return this.set(path);
    }

    this.lazyrouter(); // 建立一個Router例項 this._router = new Router();

    var route = this._router.route(path); // 對應router/index 中的 proto.route
    route[method].apply(route, slice.call(arguments, 1)); // 新增路由中介軟體 下面詳細講解
    return this;
  };
});
複製程式碼

this.lazyrouter 用來建立一個Router例項 並掛載到 app._router

// application.js
methods.forEach(function(method){
  app[method] = function(path){

    this.lazyrouter(); // 建立一個Router例項 this._router = new Router();
  }
});

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));
  }
};
複製程式碼

註冊中介軟體

可以通過兩種方式新增中介軟體:app.use用來新增非路由中介軟體,app[method]新增路由中介軟體,這兩種新增方式都在內部呼叫了Router的相關方法來實現:

註冊非路由中介軟體

// application.js

app.use = function(fn) { // fn 中介軟體函式 有可能是[fn]
    var offset = 0;
    var path = '/';

    var fns = flatten(slice.call(arguments, offset)); // 展平陣列
    // setup router
    this.lazyrouter(); // 建立路由物件
    var router = this._router;

    fns.forEach(function(fn) {
      router.use(path, function mounted_app(req, res, next) { // app.use 底層呼叫了router.use
      var orig = req.app;
      fn.handle(req, res, function (err) {
        setPrototypeOf(req, orig.request)
        setPrototypeOf(res, orig.response)
        next(err);
      });
    });
}

複製程式碼

通過上面知道了 app.use底層呼叫了router.use 接下來我們再來看看router.use

// router/index.js
var Layer = require('./layer');

proto.use = function(fn) {
    var offset = 0; // 引數偏移值
    var path = '/'; // 預設路徑 /  因為 use

    // 第一個引數有可能是path 所以要從第二個引數開始擷取
    if (typeof arg !== 'function') {
         offset = 1;
         path = fn;
    }

    // 展平中介軟體函式集合 如中介軟體函式是以陣列形式註冊的如[fn, fn] 當slice擷取時會變成[[fn, fn]] 所以需要展平為一維陣列
    var callbacks = flatten(slice.call(arguments, offset)); // 擷取中介軟體函式

    for(var i = 0; i < callbacks.length; i++) { // 遍歷每一箇中介軟體函式
        var fn = callbacks[i];
        if (typeof fn !== 'function') { // 錯誤提示
          throw new TypeError('Router.use() requires a middleware function but got a ' + gettype(fn))
        }

        // 新增中介軟體
        // 例項化一個layer 物件
        var layer = new Layer(path, {
            sensitive: this.caseSensitive,
            strict: false,
            end: false
        }, fn);

        // 非路由中介軟體,該欄位賦值為undefined
        layer.route = undefined;
        this.stack.push(layer); // 將layer新增到 router.stack
    }
}
複製程式碼

註冊路由中介軟體

在上面application.js 中的app物件 新增了很多http關於請求 app[method]就是用來註冊路由中介軟體

// application.js

var app = exports = module.exports = {};

var Router = require('./router');
var methods = require('methods');

methods.forEach(function(method){
  app[method] = function(path){ // app上新增 app.get app.post app.put 等請求方法

    if (method === 'get' && arguments.length === 1) {
      // app.get(setting)
      return this.set(path);
    }

    this.lazyrouter(); // 建立Router例項物件 並賦給this._router

    // 呼叫this._router.route => router/index.js中的proto.route
    var route = this._router.route(path);
    route[method].apply(route, slice.call(arguments, 1)); // 呼叫router中對應的method方法 註冊路由
    return this;
  };
});

複製程式碼

在上面 app[method]中底層實際呼叫了router[method] 也就是 this._router.route[method]

我們看看this._router.route(path); 這段程式碼發生了什麼

var route = this._router.route(path); // 裡面建立了一個route例項 並返回
route[method].apply(route, slice.call(arguments, 1)); // 呼叫route中的物件的route[method]
複製程式碼

router 中的 this._router.route 建立一個route物件 生成一個layer 將path 和 route.dispatch 傳入layer, layer的route指向 route物件 將Router和Route關聯來起來, 最後把route物件作為返回值

// router/index.js

var Route = require('./route');
var Layer = require('./layer');

proto.route = function (path) {
    var route = new Route(path); // app[method]註冊一個路由 就會建立一個route物件

    var layer = new Layer(path, { // 生成一個route layer
      sensitive: this.caseSensitive,
      strict: this.strict,
      end: true
    }, route.dispatch.bind(route)); // 裡將生成的route物件的dispatch作為引數傳給layer裡面

    // 指向剛例項化的路由物件(非常重要),通過該欄位將Router和Route關聯來起來
    layer.route = route;

    this.stack.push(layer); // 將layer新增到Router的stack中
    return route; // 將生成的route物件返回
}

複製程式碼

對於路由中介軟體,路由容器中的stack(Router.stack)裡面的layer通過route欄位指向了路由物件,那麼這樣一來,Router.stack就和Route.stack發生了關聯,關聯後的示意模型如下圖所示:

Express原始碼學習-路由篇

app[method]中 呼叫 route[method].apply(route, slice.call(arguments, 1));

// router/index.js

// 實際上 application.js 中 app[method] 呼叫的是 router物件中對應的http請求方法 額外新增了一個all方法
methods.concat('all').forEach(function(method){
  proto[method] = function(path){
    var route = this.route(path) // 返回建立的route物件
    route[method].apply(route, slice.call(arguments, 1)); // router[method] 又呼叫了 route[method]
    return this;
  };
});

複製程式碼

最後我們來看下 router/route.js 中的Route

// router/route.js

function Route(path) { // Route類
  this.path = path;
  this.stack = []; // route的stack

  debug('new %o', path)
  this.methods = {}; // 用於各種HTTP方法的路由處理程式
}

var Layer = require('./layer');
var methods = require('methods');

// 又是相同的一段程式碼 在給Route的原型上新增http方法
methods.forEach(function(method){
  Route.prototype[method] = function(){
    var handles = flatten(slice.call(arguments)); // 傳遞進來的處理函式

    for (var i = 0; i < handles.length; i++) {
      var handle = handles[i];

      if (typeof handle !== 'function') {
        var type = toString.call(handle);
        var msg = 'Route.' + method + '() requires a callback function but got a ' + type
        throw new Error(msg);
      }

      debug('%s %o', method, this.path)

      var layer = Layer('/', {}, handle); // 在route中也有layer 裡面儲存著 method和handle
      layer.method = method;

      this.methods[method] = true; // 標識 存在這個method的路由處理函式
      this.stack.push(layer); // 將layer 新增到route的stack中
    }

    return this; // 將route物件返回
  };
});
複製程式碼

Route 中的all方法

Route.prototype.all = function all() {
  var handles = flatten(slice.call(arguments));

  for (var i = 0; i < handles.length; i++) {
    var handle = handles[i];

    if (typeof handle !== 'function') {
      var type = toString.call(handle);
      var msg = 'Route.all() requires a callback function but got a ' + type
      throw new TypeError(msg);
    }

    var layer = Layer('/', {}, handle);
    layer.method = undefined; // all 匹配所以方法

    this.methods._all = true; // all方法標識
    this.stack.push(layer); // 新增到route的stack中
  }

  return this;
};
複製程式碼

最終路由註冊關係鏈 app[method] => router[method] => route[method] 最終在route[method]裡完成路由註冊

接下來我們看看Layer

// route/layer.js
var pathRegexp = require('path-to-regexp');

module.exports = Layer;

function Layer(path, options, fn) {
    if (!(this instanceof Layer)) {
        return new Layer(path, options, fn);
    }

    this.handle = fn; // 儲存處理函式
    this.regexp = pathRegexp(path, this.keys = [], opts); // 根據路由路徑 生成路由規則正則 用來路由匹配
}

Layer.prototype.match = function(path) { // 請求路徑 是否匹配 該層路由路徑規則this.regexp

}
複製程式碼

啟動server

// application.js
var http = require('http');

app.listen = function listen() {
  var server = http.createServer(this); // this => express.js 中的 app
  return server.listen.apply(server, arguments);
};

// express.js 中的 app
var app = function(req, res, next) {
  app.handle(req, res, next);
};

複製程式碼

路由呼叫

app.handle 呼叫this._router.handle 進行路由處理

express.js
function createApplicaton() {
    let app = function(req, res, next) { // 持續監聽請求
        app.handle(req, res, next); // 路由處理函式
    }
}

複製程式碼

express.js中的app.handle 實際來自application.js的app.handle 底層呼叫router.handle

// application.js

app.handle = function handle(req, res, callback) {
  var router = this._router;

  // final handler
  var done = callback || finalhandler(req, res, {
    env: this.get('env'),
    onerror: logerror.bind(this)
  });

  router.handle(req, res, done); // 底層呼叫router.handle
};
複製程式碼

router.handle 呼叫 內部next函式 在router的stack中尋找匹配的layer

// router/index.js


function matchLayer(layer, path) {
  try {
    return layer.match(path);
  } catch (err) {
    return err;
  }
}


proto.handle = function(req, res, done) {
    // middleware and routes
    var stack = self.stack; // router的stack 裡面存放著中介軟體和路由
    var idx = 0;

    next();
    function next(err) {

        if (idx >= stack.length) { // 邊界判斷
             setImmediate(done, layerError);
             return;
        }

        // 獲取請求路徑
        var path = getPathname(req);

        var layer;
        var match;
        var route;
        while (match !== true && idx < stack.length) { // 一層層進行路由匹配
          layer = stack[idx++]; // 從router的stack取出沒一個layer

          match = matchLayer(layer, path); // matchLayer呼叫 該layer的match方法進行路由路徑進行匹配

          route = layer.route; // 得到關聯的route物件
          var method = req.method; // 獲取請求方法

          var has_method = route._handles_method(method); // 呼叫route的_handles_method返回Boolean值

          if (match !== true) { // 如果沒匹配上處理
            return done(layerError);
          }

          if (route) { // 呼叫layer的handle_request 處理請求 執行handle
             return layer.handle_request(req, res, next);
          }
        }
    }
}
複製程式碼

路由處理 app.handle => router.handle => layer.handle_request

layer的handle_request 呼叫next一次獲取route stack中的處理方法


Layer.prototype.handle_request = function handle(req, res, next) {
  var fn = this.handle;  // 獲取new Layer時儲存的handle
    // function Layer() {
        // this.handle = fn;
    // }

  if (fn.length > 3) {
    // not a standard request handler
    return next();
  }

  try {
    fn(req, res, next);
  } catch (err) {
    next(err);
  }
};
複製程式碼

小結

app.use用來新增非路由中介軟體,app[method]新增路由中介軟體,中介軟體的新增需要藉助Router和Route來完成,app相當於是facade,對新增細節進行了包裝。

Router可以看做是一個存放了中介軟體的容器。對於裡面存放的路由中介軟體,Router.stack中的layer有個route屬性指向了對應的路由物件,從而將Router.stack與Route.stack關聯起來,可以通過Router遍歷到路由物件的各個處理程式。

相關文章