Koa2.0源碼解析-中間件的設計

剖析connect.js的中間件設計,利於咱們更好的理解Koa2.0中間件設計原理。git

1、前言

    首先對於這兩個框架最起碼要看過文檔或者是敲過入門的小demo,由於咱們讀源碼並不僅是爲了裝X,更重要的是幫助咱們理解爲何要這樣用?爲何這裏會有坑?github

    回憶一下如何用Node建立http服務:數組

const http = require('http')
    const app = http.createServer((req, res) => {
        // 處理一個個http請求
        res.end('hello world')
    })
    
    app.listen(3000)
複製代碼

    而這裏對於如何優雅的處理每一次請求就成了一個值得思考的問題,而在connect.js中你能夠這樣處理:bash

const http = require('http')
const connect = require('connect')
const app = connect()

app.use((req, res, next) => {
  // 中間件1
  next()
})

app.use((req, res, next) => {
  // 中間件2
  next()
})

app.use((re, res, next) => {
  // 中間件3
  // 響應結束
  res.end('hello world')
})

http.createServer(app).listen(3000)
複製代碼

2、connect中間件原理

    理解connect中間件實現原理,咱們須要從這四個方法入手:app

  • createServer: 如何定義處理請求方法?
  • use: 怎樣註冊咱們的中間件?
  • handle: 中間件的執行流程是怎樣的?
  • call: 執行中間件方法須要注意什麼?
一、createServer
function createServer() {
      function app(req, res, next){ app.handle(req, res, next); } 
      merge(app, proto);
      merge(app, EventEmitter.prototype); 
      app.route = '/'; 
      app.stack = []; 
      return app;
    }
複製代碼

    createServer是connect的入口方法,它返回一個處理請求的方法,內部再調用handle來處理這些註冊的中間件,也就是中間件的處理流程。框架

    connect並無採用構造函數的方式,而將須要用到的屬性方法拷貝到app對象上使用,而對於Koa2.x中則是採用ES6的class實現。koa

    這裏的route是中間件的默認路由(這裏的路由與咱們理解的路由有所差異,後面會提到),stack主要用來存放中間件。async

二、use
function use(route, fn) {
      var handle = fn;
      var path = route;
    
      // 不傳入route則默認爲'/',這種基本是框架處理參數的一種套路
      if (typeof route !== 'string') {
        handle = route;
        path = '/';
      }
    
      ...
      // 存儲中間件
      this.stack.push({ route: path, handle: handle });
      
      // 以便鏈式調用
      return this;
    }
複製代碼

    use方法中的核心就是將用戶傳入的參數整合成咱們後續要用的layer對象包含路由和執行方法,而且將一個個layer對象存儲在stack中,從這裏咱們能夠猜想出中間件註冊的順序十分重要。函數

三、handle與call

    這裏咱們須要將handle與call結合起來理解,它們能夠說是connect的靈魂。ui

function handle(req, res, out) {
      var index = 0;
      var stack = this.stack;
      ...
      function next(err) {
        ...
        // 依次取出中間件
        var layer = stack[index++]
    
        // 終止條件
        if (!layer) {
          defer(done, err);
          return;
        }
    
        var path = parseUrl(req).pathname || '/';
        var route = layer.route;
    
        // 路由匹配規則
        if (path.toLowerCase().substr(0, route.length) !== route.toLowerCase()) {
          return next(err);
        }
        ...
        call(layer.handle, route, err, req, res, next);
      }
    
      next();
    }
複製代碼

    handle方法的關鍵點在於經過next方法依次檢測當前中間件是否應該執行。而next方法中的路由匹配規則可讓咱們清楚的明白這裏並非徹底相等的匹配而是一種包含的關係:

app.use('/foo', (req, res, next) => next())
    app.use('/foo/bar', (req, res, next) => next())
複製代碼

    因此當你訪問/foo/bar路由時,這兩個中間件都會執行。

    若是不匹配當前中間件,那麼會自動調用next方法將進行下一個中間件的檢測。

    當路由匹配無誤,那麼就會調用call方法來執行當前中間件的處理函數:

function call(handle, route, err, req, res, next) {
      var arity = handle.length;
      var error = err;
      var hasError = Boolean(err);
    
      try {
        if (hasError && arity === 4) {
          // 錯誤處理中間件
          handle(err, req, res, next);
          return;
        } else if (!hasError && arity < 4) {
          // 請求處理中間件
          handle(req, res, next);
          return;
        }
      } catch (e) {
        // 記錄錯誤
        error = e;
      }
    
      // 將錯誤傳遞下去
      next(error);
    }
複製代碼

    這裏能夠看到call內部經過調用try/catch捕獲中間件錯誤,而且經過參數個數和有無錯誤來決定執行錯誤處理中間件仍是請求處理中間件,其它的狀況則是自動調用next方法去檢查下一個中間件。若是try/catch捕獲到錯誤以後,會一直將這個錯誤傳遞下去,直到遇到錯誤處理中間件。

    因此這裏咱們能夠發現這個handle是有點樸實的,它會一直去檢查中間件數組直到數組遍歷完或者是next調用鏈斷掉(也就是你在中間件中沒有手動調用next),這裏咱們能夠經過流程圖看一下hanle與call的處理過程:

四、小結

    這時咱們能夠發現connect的幾個特色

  • 當中間件發生錯誤時,handle函數並非當即進入錯誤處理狀態,而是將錯誤逐層傳遞,直到找到錯誤處理中間件,而且你的錯誤中間件必須是四個參數;
  • 中間件的執行流程是經過next連接的;
  • 咱們須要手動調用res.end結束響應;
  • 當咱們使用ES8的async方法時,沒法捕獲到錯誤。

3、Koa2.0中間件

    Koa2.0中間件的實現與connect中間件原理基本類似,主要區別就在於中間件執行流程上的細節處理。

    首先咱們要知道async函數返回的是一個Promise對象,因此當async內部發生錯誤,這個Promise對象就會將狀態轉換爲reject。這也是爲何try/catch沒法捕獲它的狀態,因此捕獲async函數的內部錯誤,實際上就是Promise對象的錯誤處理,接下來咱們看Koa2.0中next方法的實現:

function compose (middleware) {
      if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
      for (const fn of middleware) {
        if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
      }
    
      return function (context, next) {
        let index = -1
        return dispatch(0)
        function dispatch (i) {
          if (i <= index) return Promise.reject(new Error('next() called multiple times'))
          index = i
          let fn = middleware[i]
          if (i === middleware.length) fn = next
          if (!fn) return Promise.resolve()
          try {
            return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
          } catch (err) {
            return Promise.reject(err)
          }
        }
      }
    }
複製代碼

    從上述代碼中能夠看出Koa2.0中間件處理的流程比connect更加簡單,首先Koa2.0中沒有路由,無需在傳遞的過程當中匹配路由。

    而咱們經過層層傳遞Promise對象,造成了一條Promise鏈,一旦出現reject狀態,那麼會當即進入catch方法,這也正好解決了connect中須要將錯誤層層傳遞到錯誤中間件的缺點。

    而當咱們調用next方法時,就是調用dispatch.bind(null, i + 1),直白一點,就是:

function next () {
        return dispatch(i + 1)
    }
複製代碼

    而對於這條Promise鏈,Koa2.0中最後這樣處理:

fnMiddleware(ctx).then(handleResponse).catch(onerror)
複製代碼

    經過handleResponse方法幫助咱們自動調用res.end(),這就是爲何在Koa中咱們這樣設置返回值:

app.use(ctx => {
      ctx.body = 'Hello Koa'
    })
複製代碼

    而且這裏經過系統自帶的onerror方法幫助咱們處理錯誤,而且在onerror內部使用:

this.app.emit('error', err, this);
複製代碼

    從而爲用戶提供監聽error來集中處理錯誤的功能。

    從connect到koa2.0,但願能夠幫助你徹底理解中間件的實現原理。

4、寫在最後

    這裏可能有人不解,難道講Koa都不提一下洋蔥模型嗎?其實看到這裏,我相信你已經明白next的執行流程實際上就是一個函數遞歸執行的過程,這也就是爲何咱們會用洋蔥模型來形容它。


    喜歡本文的小夥伴,能夠gay一下或者關注個人訂閱號,ε=ε=ε=(~ ̄▽ ̄)~

相關文章
相關標籤/搜索