Express 原始碼分析1-(服務啟動和請求服務過程)

bluebrid發表於2018-12-22

前言

關於Node JS 的後端框架,不管是Express,Koa, 甚至Eggjs(Eggjs 是基於Koa 底層封裝的框架),都是基於NodeJS 的http模組進行處理的,其最重要的是方法是

const server = http.createServer((rep, res) => {
    res.end('hello world')
})
server.listen(prot, () => {
    console.log('Server Started. Port: '+ prot)
})
複製程式碼

無論那個框架的中介軟體, 路由等的處理,都是從這裡是一個入口,對伺服器的資源的任何訪問都會先進入http.createServer方法的回撥函式,也就是下面的方法

(rep, res) => {
    res.end('hello world')
}
複製程式碼

之前我們已經分析過了Eggjs 框架,但是沒有分析Nodejs實現後端框架實現的底層原理,因為Eggjs 是基於Koa 實現的一個上層框架, 我們這次來通過Express來分析下Nodejs 實現後端框架的底層原理。

原始碼結構

我們先從express clone 一份原始碼,其對應的lib 資料夾就是express框架的整個原始碼

Express 原始碼分析1-(服務啟動和請求服務過程)

其最重要的幾個檔案是:

  1. express.js (專案的入口檔案,暴露除了很多物件,其中最重要的是一個createApplication 方法)(重點)
  2. application.js (最核心的一個檔案,但是是對上面createApplication方法,返回的app 物件去掛載很多方法)(重點)
  3. request.js 和response.js 兩個檔案,主要是對http.createServer 方法中的rep 和 res 進行相應的封裝處理
  4. utils.js 只是封裝了一些幫助方法
  5. View.js 模版引擎的相關的方法
  6. router 資料夾,是express實現的關鍵,也就是路由的處理,我們的任何一個請求,其實對應的就是一個路由, 然後返回相應的資源(重點)
  7. middleware, 是定義中介軟體的資料夾,不過其中只有兩個很簡單的內建中介軟體, 因為Express的很多中介軟體都是第三方的庫

我們下面根據啟動服務* 和 ** 訪問服務 兩個流程來分析express, 會針對上面標註為(重點)的相應的檔案,進行詳細的分析.

啟動服務

express.js

我們先從怎麼使用開始,作為入口,下面是一個簡單的express的demo.

const express = require('./lib/express')
const app = module.exports = express()

app.get('/', (req, res) => {
  res.end('hello world')
})
if (!module.parent) {
  app.listen(3000);
  console.log('Express started on port 3000')
}
複製程式碼

上面一段簡單的程式碼,我們就已經搭建好了一個後端服務,當我們用瀏覽器開啟http://localhost:3000/時,就會顯示hello world.,下面我們就來看看是怎麼實現的.

const app = module.exports = express() 可以看出express()應該是express.js 檔案裡面暴露出來的一個方法, 其對應的指令碼是: exports = module.exports = createApplication; createApplication方法如下:

function createApplication() {
  var app = function(req, res, next) {
    app.handle(req, res, next);
  };

  mixin(app, EventEmitter.prototype, false);// 合併prototype
  mixin(app, proto, false);// 合併proto

  app.request = Object.create(req, {
    app: { configurable: true, enumerable: true, writable: true, value: app }
  })

  app.response = Object.create(res, {
    app: { configurable: true, enumerable: true, writable: true, value: app }
  })

  app.init();
  return app;
}
複製程式碼

這個方法,返回一個app 物件, 這個物件相當於繼承與EventEmitter.prototype 和一個proto物件原型, 然後執行了app.init()方法,這個方法主要是做一些初始化工作並清空cache, engines, settings, 並且去初始化一些配置,比如說:

  this.enable('x-powered-by');
  this.set('etag', 'weak');
  this.set('env', env);
  this.set('query parser', 'extended');
  this.set('subdomain offset', 2);
  this.set('trust proxy', false);
複製程式碼

this.set設定的值是儲存在settings中的, 比如我們我們可以this.settings['x-powered-by'] 可以在應用中任何的地方去呼叫.所以這裡有一個擴充套件出一個應用:

const express = require('./lib/express');

const app = module.exports = express();
app.set('config', {
  url: 'http://localhost:8080',
  userInfo: {
    name: 'ivan fan',
    age: 18
  }
})
app.get('/', (req, res) => {
  const config = app.get('config')
  console.log(config)
  res.end('hello world')
})
if (!module.parent) {
  app.listen(3000);
  console.log('Express started on port 3000');
}
複製程式碼

上面我們通過app.set 去設定一個config的值,我們在其他的地方可以通過app.get去獲取這個值,這樣看起來感覺沒有什麼用途,因為我們可以直接定義一個變數就可以,沒必要通過app.set, 但是如果我們的應用很大的時候,我們將專案拆分成了很多單獨的檔案,我們只是共享了app物件,但是在多個js檔案中可能需要公用一個全域性的配置,我們可以建立一個config.json檔案,在不同的頁面都去import 進來,但是如果如果我們在app.js中將這個配置注入到app中其他的地方,只要通過app.get就可以達到共享的作用。

總結:

  1. express.js 只是暴露除了一個createApplication方法, 並且返回了一個app物件
  2. 給app物件的原型做了相應的處理
  3. 給app 進行初始化設定

application.js

上面我們已經分析了express.js檔案,知道其返回了一個app物件,但是我們至今位置沒有看到哪裡定義了listenget方法。

我們在上面分析發現,執行了mixin(app, proto, false);方法,這個是在app 原型上去新增了另外一個原型,而proto指向的就是application.js檔案, 下面我們就來具體分析這個檔案。

listen

首先我們找到listen 方法,其程式碼如下:

app.listen = function listen() {
  var server = http.createServer(this);
  return server.listen.apply(server, arguments);
};
複製程式碼

這個就是我們在一開始說的,所有的Nodejs 後端框架都是基於http 這個模組的實現的,所以這個裡我們就已經實現了一個後端的服務。

get

在我們的demo 中,我們有呼叫一個app.get方法,其程式碼如下:

app.get('/', (req, res) => {
  const config = app.get('config')
  console.log(config)
  res.end('hello world')
})
複製程式碼

但是我們找遍了整個application.js檔案,都沒有找到這個方法在哪裡實現的, get只是http請求眾多方法的其中一個, http方法,還有'post','put','delete'等一些列方法,為了簡潔,express 引用了第三方庫methods, 這個庫幾乎涵蓋了http 請求的常見方法,所以通過迴圈去給app掛載不同的方法(Koa 也是這樣處理)

methods.forEach(function(method){
  app[method] = function(path){
    if (method === 'get' && arguments.length === 1) {
      // app.get(setting)
      return this.set(path);
    }
    this.lazyrouter();
    var route = this._router.route(path);
    route[method].apply(route, slice.call(arguments, 1));
    return this;
  };
});
複製程式碼

首先this.lazyrouter(); 方法是去給app物件掛載一個_router的路由(Router)屬性, 然後我們在看下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;
};
複製程式碼

從上面的程式碼可知,var route = this._router.route(path);, route也就是this.stack 中的layer中的reoute, 所以最後回撥函式是掛載在stack 的layer 上面的。

route[method].apply(route, slice.call(arguments, 1));app.get 的回撥函式掛載在route屬性上面,其程式碼如下(刪除異常處理程式碼):

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]; 
      var layer = Layer('/', {}, handle);
      layer.method = method;
      this.methods[method] = true;
      this.stack.push(layer);
    }

    return this;
  };
});
複製程式碼

Express 原始碼分析1-(服務啟動和請求服務過程)

我們先不具體分析程式碼邏輯, 我們可以根據上面的圖片,分析下app物件下的一個資料結構:

  1. 在app 上面掛載一個_router屬性, (router/index.js)
  2. _router下面有一個stack 的屬性,其是一個陣列
  3. stack陣列中,儲存的都是一個Layer型別的物件
  4. Layer 物件中又掛載了一個route(Route)的物件
  5. route物件儲存了path(path:/abc), methods, 同樣也有一個stack的屬性,也是一個陣列, 同樣裡面儲存的也是一個Layer 物件
  6. Layer裡面掛載了一個重要的屬性handle, 其實從現在的分析看,這個handle就是我們app.get方法的第二個回撥函式引數.

上面我們已經分析了express啟動的過程,下面我們來分析訪問服務express處理的過程,也就是我們訪問http://localhost:3000/時,express到底做了些什麼.

訪問服務

從一開始,我們就知道,對伺服器的方法,首先都會進入http.createServer的回撥函式,而且express是通過listen方法,執行這個方法的

app.listen = function listen() {
  var server = http.createServer(this);
  return server.listen.apply(server, arguments);
};
複製程式碼

其中this就是app例項,也就是在express.js檔案中定義的,如下:

 var app = function(req, res, next) {
    app.handle(req, res, next);
  };
複製程式碼

在這個方法中,會呼叫handle方法,下面我們來分析下這個方法

handle

handle的程式碼如下:

app.handle = function handle(req, res, callback) {
  var router = this._router;
  var done = callback || finalhandler(req, res, {
    env: this.get('env'),
    onerror: logerror.bind(this)
  });

  if (!router) {
    debug('no routes defined on app');
    done();
    return;
  }

  router.handle(req, res, done);
};
複製程式碼

然後會執行router.handle(req, res, done);, 在上面我們已經得知, this._router指向的是router/index.js這個資料夾的物件,下面我們進入到這個handle方法中, 這個方法很長,但是其實就是根據我們訪問的路徑來查詢對應的Layer, 其關鍵程式碼是:

    while (match !== true && idx < stack.length) {
      layer = stack[idx++];
      match = matchLayer(layer, path);
      route = layer.route;
    ...
    }
複製程式碼

通過matchLayer(layer, path);去匹配layer. 找到Layer後,然後去執行layer.handle_request(req, res, next);

Layer.prototype.handle_request = function handle(req, res, next) {
  var fn = this.handle;

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

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

var fn = this.handle; 這個fn 其實指向的就是app.get裡面的第二引數,也就是回撥函式,

app.get('/', (req, res) => {
  res.end('hello world')
})
複製程式碼

然後就相當於請求完成了。

總結

上面我們已經分析了,Express 在啟動的整個過程,主要是進行資料的一些載入處理和路由的處理,而且也分析了我們在請求Server時的整個過程。

後續我會繼續分析use 的用法,並且針對express.static 原始碼來分析Express 中介軟體的處理和總結中介軟體的使用方式,以及express.static對快取的處理(Etag, Last-Modified)

相關文章