Express 源碼分析1-(服務啓動和請求服務過程)

前言

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

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

不管那個框架的中間件, 路由等的處理,都是從這裏是一個入口,對服務器的資源的任何訪問都會先進入http.createServer方法的回調函數,也就是下面的方法github

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

以前咱們已經分析過了Eggjs 框架,可是沒有分析Nodejs實現後端框架實現的底層原理,由於Eggjs 是基於Koa 實現的一個上層框架, 咱們此次來經過Express來分析下Nodejs 實現後端框架的底層原理。express

源碼結構

咱們先從express clone 一份源碼,其對應的lib 文件夾就是express框架的整個源碼json

其最重要的幾個文件是:後端

  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方法以下:bash

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

咱們先不具體分析代碼邏輯, 咱們能夠根據上面的圖片,分析下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)

相關文章
相關標籤/搜索