nodejs之koa中間件源碼解析

前言

上一篇《nodejs之express中間件》已經對express中間件的實現作了詳細的講解,同時也對實現中間件的框架Connect的源碼作了簡單的分析。而且也提到了express的中間件是直線型,koa的中間件是洋蔥型。本篇就來講說koa的中間件。node

koa介紹

koa和express是同一個團隊開發的。與express很像,也是一個自身功能極簡的框架,因此在一個項目中所須要的東西大可能是以中間件的形式引入。git

目前koa有1.x和2.x版本,1.x版本基於generator,2.x版本基於
generator/async、await。因爲generator的語法相比async又很明顯的劣勢,因此後續的版本中會去掉generator的使用,而是所有采用async的方式。不過在2.x這個過渡版中依然兼容generator,須要注意的是在使用generator或者使用了依賴generator的第三方庫時,會報出一個警告,大體意思「generator在當前版本還能夠正常使用,可是會在後續的版本中移除」。
注:本文全部koa的寫法都是koa2版本的。而且兼容koa3。github

koa中間件

相比express的直線型中間件,koa的中間件就不是那麼直觀了。先看一張圖
在這裏插入圖片描述
把洋蔥的一圈看作是一箇中間件,直線型就是從第一個中間件走到最後一個,可是洋蔥型就很特殊了,最先use的中間件在洋蔥的最外層,開始的時候會按順序走到全部中間件,而後按照倒序再走一遍全部的中間件,至關於每一箇中間件都會進入兩次。這就給了咱們更多的操做空間。
看下面一段代碼express

const koa = require('koa');

let server = new koa();

server.use(async (ctx, next) => {
    console.log('a-1');
    next();
    console.log('a-2');
})

server.use(async (ctx, next) => {
    console.log('b-1');
    next();
    console.log('b-2');
})

server.use(async (ctx, next) => {
    console.log('c');
})

server.listen(3000);

代碼執行後命令行輸出順序爲
在這裏插入圖片描述segmentfault

  • koa官方文檔上把外層的中間件稱爲"上游",內層的中間件爲"下游"。

通常的中間件都會執行兩次,調用next以前爲第一次,調用next時把控制按順序傳遞給下游的中間件。當下遊再也不有中間件或者中間件沒有執行next函數時,就將依次恢復上游中間件的行爲,讓上游中間件執行next以後的代碼。數組

  • 若是某一箇中間件中有異步代碼,最好使用async/await處理異步。

使用方式咱們已經搞清了,下面就看看內部實現原理閉包

源碼解析

在github上找到koa的源碼,核心的函數都在application.js中。
通過上一篇connect源碼的學習,這部分源碼讀起來應該也是壓力不大。所有代碼也就200多行,這裏我只截取其中比較重要的部分。app

'use strict';
module.exports = class Application extends Emitter {

  constructor() {
    super();

    this.proxy = false;
    this.middleware = [];
    this.subdomainOffset = 2;
    this.env = process.env.NODE_ENV || 'development';
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
    if (util.inspect.custom) {
      this[util.inspect.custom] = this.inspect;
    }
  }

  listen(...args) {
    debug('listen');
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }

  use(fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    if (isGeneratorFunction(fn)) {
      deprecate('Support for generators will be removed in v3. ' +
                'See the documentation for examples of how to convert old middleware ' +
                'https://github.com/koajs/koa/blob/master/docs/migration.md');
      fn = convert(fn);
    }
    debug('use %s', fn._name || fn.name || '-');
    this.middleware.push(fn);
    return this;
  }

  callback() {
    const fn = compose(this.middleware);

    if (!this.listenerCount('error')) this.on('error', this.onerror);

    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn);
    };

    return handleRequest;
  }

  handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;
    const onerror = err => ctx.onerror(err);
    const handleResponse = () => respond(ctx);
    onFinished(res, onerror);
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }
};

1.先來重點看下use函數
第一個if是用來作校驗的,第二個if是用來兼容generator函數的。
use的最後執行了this.middleware.push(fn);就是將中間件放到middleware數組中,相似connect中的stack數組。框架

2.再來看下callback函數
callback函數的返回值就是listen函數中執行createServer函數的回調,也就是handleRequest函數,也能夠理解成是用來響應request事件的函數。dom

3.注意在callback中執行this.handleRequest(ctx, fn);的時候這個fn是中間件數組經過compose函數處理後返回的值。那在看handleRequest函數中最後就調用了這個函數,那麼在這以前就要知道compose函數對中間件數組作了什麼?

找到koa-compose這個包,截取其中的compose函數。

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) {
        // last called middleware #
        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)
            }
        }
    }
}

前面兩段很明顯是在驗證數組和函數的類型。而後就是返回一個函數,這個函數就是callback函數中的fn,也是在傳入handleRequest函數中最後調用的那個函數。
先總結下上面的內容:

  1. 在use函數中先把中間件添加到this.middleware數組中。
  2. callback函數中先經過compose函數對this.middleware數組進行操做,將數組轉換爲一個函數賦值給fn(注意這個函數比較特殊,下面會詳細講解),而後再經過handleRequest函數去調用這個fn函數,顯而易見這個函數必定返回Promise對象。

這個流程若是搞清楚後,再來看看這個compose到底作了什麼。
實際上compose就是返回一個函數,函數內利用閉包實現了對中間件數組的遍歷,返回一個Promise對象,而且resolve回調就是執行當前索引對應的中間件函數,而且將參數next賦值爲內部閉包函數的調用,調用的同時將索引值+1,這樣在中間件中執行next()就至關於執行了dispatch(i+1),天然就找到了下一個中間件。若是中間件沒有執行next或者遍歷了數組中所有的中間件後,天然就開始一級一級向上執行每一箇中間件next後的代碼。這就是洋蔥圈的實現。

這裏的邏輯稍微有點饒,若是仍是不懂也不要緊,我將代碼流程簡化,經過圖片再來講下整個實現過程。

在這裏插入圖片描述
整個核心仍是compose函數。若是將這個函數簡化到極致,就是以下的代碼

function a(){
    console.log('a-1');
    next(b);
    console.log('a-2');
}
function b(){
    console.log('b-1');
    next(c);
    console.log('b-2');
}
function c(){
    console.log('c');
}
function next(fn){
    return Promise.resolve(fn());
}

// 輸出結果爲
//  a-1
//  b-1
//  c
//  b-2
//  a-2

總結

到此咱們已經分析了express中間件和koa中間件的實現原理。兩者的源碼都是很是簡潔,而且設計巧妙、可讀性高。不少細節處理都值得借鑑。但願本身在源碼閱讀的道路上越走越遠。

相關文章
相關標籤/搜索