三步法解析Express源碼

關注公衆號「執鳶者」,獲取大量教學視頻及私人總結麪筋並進入專業交流羣.
image
在抖音上有幸看到一個程序員講述如何閱讀源代碼,主要分爲三步:領悟思想、把握設計、體會細節。javascript

  1. 領悟思想:只需體會做者設計框架的初衷和目的
  2. 把握設計:只需體會代碼的接口和抽象類以及宏觀的設計
  3. 體會細節:是基於頂層的抽象接口設計,逐漸展開代碼的畫卷

基於上述三步法,火燒眉毛的拿Express開刀了。本次源碼解析有什麼不到位的地方各位讀者能夠在下面留言,咱們一塊兒交流。前端

1、領悟思想

在Express中文網上,介紹Express是基於Node.js平臺,快速、開放、極簡的Web開發框架。在這句話裏面能夠獲得解讀出如下幾點含義:java

  1. Express是基於Node.js平臺,而且具有快速、極簡的特色,說明其初衷就是爲了經過擴展Node的功能來提升開發效率。
  2. 開放的特色說明該框架不會對開發者過多的限制,能夠自由的發揮想象進行功能的擴展。
  3. Express是Web開發框架,說明做者的定位就是爲了更加方便的幫助咱們處理HTTP的請求和響應。

2、把握設計

理解了做者設計的思想,下面從源碼目錄、核心設計原理及抽象接口三個層面來對Express進行總體的把握。

2.1 源碼目錄

以下所示是Express的源碼目錄,相比較來講仍是比較簡單的。

├─application.js---建立Express應用後可直接調用的api均在此處(核心)<br/>
├─express.js---入口文件,建立一個Express應用<br/>
├─request.js---豐富了http中request實例上的功能<br/>
├─response.js---豐富了http中response實例上的功能<br/>
├─utils.js---工具函數<br/>
├─view.js---與模板渲染相關的內容<br/>
├─router---與路由相關的內容(核心)<br/>
| ├─index.js<br/>
| ├─layer.js<br/>
| └route.js<br/>
├─middleware---與中間件相關的內容<br/>
| ├─init.js---會將新增長在request和response新增長的功能掛載到原始請求的request和response的原型上<br/>
| └query.js---將請求url中的query部分添加到request的query屬性上<br/>程序員

2.2 抽象接口

對源碼的目錄結構有了必定了解,下面利用UML類圖對該系統各個模塊的依賴關係進一步瞭解,爲後續源碼分析打好基礎。

2.3 設計原理

這一部分是整個Express框架的核心,下圖是整個框架的運行流程,一看是否是很懵逼,爲了搞清楚這一部分,須要明確四個概念:Application、Router、Layer、Route。

爲了明確上述四個概念,先引入一段代碼
const express = require('./express');
const res = require('./response');
const app = express();
app.get('/test1', (req, res, next) => {
    console.log('one');
    next();
}, (req, res) => {
    console.log('two');
    res.end('two');
})
app.get('/test2', (req, res, next) => {
    console.log('three');
    next();
}, (req, res) => {
    console.log('four');
    res.end('four');
})
app.listen(3000);
  1. Application
    表示一個Express應用,經過express()便可進行建立。
  2. Router<
    路由系統,用於調度整個系統的運行,在上述代碼中該路由系統包含app.get('/test1',……)和app.get('/test2',……)兩大部分
  3. Layer
    表明一層,對於上述代碼中app.get('/test1',……)和app.get('/test2',……)均可以成爲一個Layer
  4. Route
    一個Layer中會有多個處理函數的狀況,這多個處理函數構成了Route,而Route中的每個函數又成爲Route中的Layer。對於上述代碼中,app.get('/test1',……)中的兩個函數構成一個Route,每一個函數又是Route中的Layer。

瞭解完上述概念後,結合該幅圖,就大概能對整個流程有了直觀感覺。首先啓動服務,而後客戶端發起了http://localhost:3000/test2的請求,該過程應該如何運行呢?express

  1. 啓動服務時會依次執行程序,將該路由系統中的路徑、請求方法、處理函數進行存儲(這些信息根據必定結構存儲在Router、Layer和Route中)
  2. 對相應的地址進行監聽,等待請求到達。
  3. 請求到達,首先根據請求的path去從上到下進行匹配,路徑匹配正確則進入該Layer,不然跳出該Layer。
  4. 若匹配到該Layer,則進行請求方式的匹配,若匹配方式匹配正確,則執行該對應Route中的函數。

上述解釋的比較簡單,後續會在細節部分進一步闡述。api

3、體會細節

經過上述對Express設計原理的分析,下面將從兩個方面作進一步的源碼解讀,下面流程圖是一個常見的Express項目的過程,首先會進行app實例初始化、而後調用一系列中間件,最後創建監聽。對於整個工程的運行來講,主要分爲兩個階段:初始化階段、請求處理階段,下面將以app.get()爲例來闡述一下該核心細節。

3.1 初始化階段

下面利用app.get()這個路由來了解一下工程的初始化階段。

  1. 首先來看一下app.get()的內容(源代碼中app.get()是經過遍歷methods的方式產生)數組

    app.get = function(path){
        // ……
        this.lazyrouter();
    
        var route = this._router.route(path);
        route.get.apply(route, slice.call(arguments, 1));
        return this;
    };
  2. 在app.lazyrouter()會完成router的實例化過程app

    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));
      }
    };

    注意:該過程當中實際上是利用了單例模式,保證整個過程當中獲取router實例的惟一性。框架

  3. 調用router.route()方法完成layer的實例化、處理及保存,並返回實例化後的route。(注意源碼中是proto.route)函數

    router.prototype.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;// 把route放到layer上
    
      this.stack.push(layer); // 把layer放到數組中
      return route;
    };
  4. 將該app.get()中的函數存儲到route的stack中。(注意源碼中也是經過遍歷method的方式將get掛載到route的prototype上)

    Route.prototype.get = function(){
        var handles = flatten(slice.call(arguments));
    
        for (var i = 0; i < handles.length; i++) {
          var handle = handles[i];
          // ……
          // 給route添加layer,這個層中須要存放方法名和handler
          var layer = Layer('/', {}, handle);
          layer.method = method;
    
          this.methods[method] = true;
          this.stack.push(layer);
        }
    
        return this;
      };

注意:上述代碼均刪除了源碼中一些異常判斷邏輯,方便讀者看清總體框架。

經過上述的分析,能夠看出初始化階段主要作了兩件事情:

  1. 將路由處理方式(app.get()、app.post()……)、app.use()等劃分爲路由系統中的一個Layer。
  2. 對於每個層中的處理函數所有存儲至Route對象中,一個Route對象與一個Layer相互映射。

3.2 請求處理階段

當服務啓動後即進入監聽狀態,等待請求到達後進行處理。

  1. app.listen()使服務進入監聽狀態(實質上是調用了http模塊)

    app.listen = function listen() {
      var server = http.createServer(this);
      return server.listen.apply(server, arguments);
    };
  2. 當鏈接創建會調用app實例,app實例中會當即執行app.handle()函數,app.handle()函數會當即調用路由系統的處理函數router.handle()

    app.handle = function handle(req, res, callback) {
      var router = this._router;
      // 若是路由系統中處理不了這個請求,就調用done方法
      var done = callback || finalhandler(req, res, {
        env: this.get('env'),
        onerror: logerror.bind(this)
      });
      //……
      router.handle(req, res, done);
    };
  3. router.handle()主要是根據路徑獲取是否有匹配的layer,當匹配到以後則調用layer.prototype.handle_request()去執行route中內容的處理

    router.prototype.handle = function handle(req, res, out) {
      // 這個地方參數out就是done,當全部都匹配不到,就從路由系統中出來,名字很形象
      var self = this;
      // ……
      var stack = self.stack;
      
      // ……
    
      next();
    
      function next(err) {
        // ……
        // get pathname of request
        var path = getPathname(req);
    
        // find next matching layer
        var layer;
        var match;
        var route;
    
        while (match !== true && idx < stack.length) {
          layer = stack[idx++];
          match = matchLayer(layer, path);
          route = layer.route;
          // ……
        }
    
        // no match
        if (match !== true) {
          return done(layerError);
        }
        // ……
    
        // Capture one-time layer values
        req.params = self.mergeParams
          ? mergeParams(layer.params, parentParams)
          : layer.params;
        var layerPath = layer.path;
    
        // this should be done for the layer
        self.process_params(layer, paramcalled, req, res, function (err) {
          if (err) {
            return next(layerError || err);
          }
    
          if (route) {
            return layer.handle_request(req, res, next);
          }
    
          trim_prefix(layer, layerError, layerPath, path);
        });
      }
    
      function trim_prefix(layer, layerError, layerPath, path) {
        // ……
    
        if (layerError) {
          layer.handle_error(layerError, req, res, next);
        } else {
          layer.handle_request(req, res, next);
        }
      }
    };
  4. layer.handle_request()會調用route.dispatch()觸發route中內容的執行

    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);
      }
    };
  5. route中的經過判斷請求的方法和route中layer的方法是否匹配,匹配的話則執行相應函數,若全部route中的layer都不匹配,則調到外層的layer中繼續執行。

    Route.prototype.dispatch = function dispatch(req, res, done) {
      var idx = 0;
      var stack = this.stack;
      if (stack.length === 0) {
        return done();
      }
    
      var method = req.method.toLowerCase();
      // ……
        
      next();
      // 此next方法是用戶調用的next,若是調用next會執行內層的next方法,若是沒有匹配到會調用外層的next方法
      function next(err) {
        // ……
    
        var layer = stack[idx++];
        if (!layer) {
          return done(err);
        }
    
        if (layer.method && layer.method !== method) {
          return next(err);
        }
    
        // 若是當前route中的layer的方法匹配到了,執行此layer上的handler
        if (err) {
          layer.handle_error(err, req, res, next);
        } else {
          layer.handle_request(req, res, next);
        }
      }
    };

經過上述的分析,能夠看出初始化階段主要作了兩件事情:

  1. 首先判斷layer中的path和請求的path是否一致,一致則會進入route進行處理,不然調到下一層layer
  2. 在route中會判斷route中的layer與請求方法是否一致,一致的話則函數執行,不然不執行,全部route中的layer執行完後跳到下層的layer進行執行。

1.若是以爲這篇文章還不錯,來個分享、點贊、吧,讓更多的人也看到

2.關注公衆號執鳶者,領取學習資料,按期爲你推送原創深度好文

歡迎你們關注公衆號(回覆「書籍」獲取大量前端學習資料,回覆「前端視頻」獲取大量前端教學視頻)
相關文章
相關標籤/搜索