為了賬號安全,請及時綁定郵箱和手機立即綁定

多維度分析 Express、Koa 之間的區別

2020.03.22 21:36 406瀏覽

圖片描述
Express 歷史悠久相比 Koa 學習資料多一些,其自帶 Router、路由規則、View 等功能,更接近于 Web FrameWork 的概念。Koa 則相對輕量級,更像是對 HTTP 的封裝,自由度更多一些,官方 koajs/koa/wiki 提供了一些 Koa 的中間件,可以自行組合。

本文重點從 Handler 處理方式中間件執行機制響應機制多個維度來看待 Express、Koa 的區別。

Handler 處理方式

這個是 Express、Koa(koa1、koa2)的重點區別:

Express

Express 使用普通的回調函數,一種線性的邏輯,在同一個線程上完成所有的 HTTP 請求,Express 中一種不能容忍的是 Callback,特別是對錯捕獲處理起來很不友好,每一個回調都擁有一個新的調用棧,因此你沒法對一個 callback 做 try catch 捕獲,你需要在 Callback 里做錯誤捕獲,然后一層一層向外傳遞。

Koa1

目前我們使用的是 Koa2,Koa1 是一個過度版,因此也有必要了解下,它是利用 generator 函數生成器 + co 來實現的 “協程響應”

先說下 Generator 和協程,協程是處于線程的環境下,同一時刻一個線程只能執行一個協程,相比線程它更加輕量級,沒有了線程的創建、銷毀,上下文切換等消耗,它不受操作系統管理,由具體的應用程序所控制,Generator 也是在 ES6 中所實現,它由函數的調用者給予授權執行,因此也稱為 “半協程/像協程”,完全的協程是所有的函數都可控制。

在說下 co,Generator 加上 co 這個必殺器,完全干掉了回調函數這種寫法,co 是什么呢?它是一種基于 Promise 對象的 Generator 函數流程自動管理,可以像寫同步代碼一樣來管理我們的異步代碼。

Koa2(現在 Koa 默認的)

Koa2 這個現在是 Koa 的默認版本,與 Koa1 最大的區別是使用 ES7 的 Async/Await 替換了原來的 Generator + co 的模式,也無需引入第三方庫,底層原生支持,Async/Await 現在也稱為 JS 異步的終極解決方案

Koa 使用的是一個洋蔥模型,它的一個特點是級聯,通過 await next() 控制調用 “下游” 中間件,直到 “下游” 沒有中間件且堆棧執行完畢,最終在流回 “上游” 中間件。這種方式有個優點特別是對于日志記錄(請求->響應耗時統計)、錯誤處理支持都很完美。

因為其背靠 Promise,Async/Await 只是一個語法糖,因為 Promise 是一種鏈式調用,當多個 then 鏈式調用中你無法提前中斷,要么繼續像下傳遞,要么 catch 拋出一個錯誤。對應到 Koa 這個框架也是你只能通過 await next() 來控制是否像下流轉,或者拋出一個錯誤,無法提前終止。

上面說到無法提前終止,后來有看過 Teambiton 嚴清老師自己實現的一個框架 Toa,基于 Koa 進行開發,它的其中一個特點是可以通過 context.end() 提前終止,感興趣的可以去看看 toajs/toa

中間件實現機制

Koa 中間件機制

Koa (>=v7.6)默認支持 Async/Await,在 Koa 中多個異步中間件進行組合,其中一個最核心的實現是 koa-compse 這個組件,下面一步一步的進行實現。

從三個函數開始做為例子開始封裝一個類似于 koa-compse 的組合函數:

async function f1(ctx, next) {
  console.log('f1 start ->');
  await next();
  console.log('f1 end <-');
}

async function f2(ctx, next) {
  console.log('f2 start ->');
  await next();
  console.log('f2 end <-');
}

async function f3(ctx) {
  console.log('f3 service...');
}

如果是按照 Koa 的執行順序,就是先讓 f1 先執行、f1 的 next 參數是 f2、f2 的 next 參數是 f3,可以看到 f3 是最后一個函數,處理完邏輯就結束,模擬實現:

  • 行 {1} 定義一個中間件的集合
  • 行 {2} 定義 use 方法,像中間件集合里 push 中間件,可以看成類似于 app.use()
  • 行 {3} 依次掛載我們需要的執行的函數 f1、f2、f3
  • 行 {5} 執行 next1(),也即先從 f1 函數開始執行
  • 行 {4.3} 定義 next1 執行函數,middlewares[0] 即 f1 函數,其函數內部調用 f2,我們在行 {4.2} 定義 next2 執行函數
  • 行 {4.2} 定義 next2 執行函數,middlewares[1] 即 f2 函數,其函數內部要調用 f3,我們再次定義 next3 執行函數
  • 行 {4.1} 定義 next1 執行函數,middlewares[2] 即 f3 函數,因為其是最后一步,到這里也就結束了
const ctx = {}
const middlewares = []; // {1} 定義一個中間件的集合
const use = fn => middlewares.push(fn); // {2} 定義 use 方法

// {3}
use(f1);
use(f2);
use(f3);

// {4}
const next3 = () => middlewares[2](ctx); // {4.1}
const next2 = () => middlewares[1](ctx, next3); // {4.2}
const next1 = () => middlewares[0](ctx, next2); // {4.3}

// {5}
next1()

// 輸出結果
// f1 start ->
// f2 start ->
// f3 service...
// f2 end <-
// f1 end <-

上面輸出結果是我們所期望的,但是如果我們在新增一個 f4 呢,是不是還得定義呢?顯然這樣不是很合理,我們需要一個更通用的方法來組合我們這些函數,通過上面例子,可以看出是由規律性的,可以通過遞歸遍歷來實現,實現如下:

  • 行 {1} {2} 為邊界處理,首先 middlewares 是一個數組,其次數組中的每個元素必須為函數
  • 行 {4} 定義 dispatch 函數這里是我們實現的關鍵
  • 行 {5} i 為當前執行到中間件集合 middlewares 的哪個位置了,如果等于 middlewares 的長度,也就執行完畢直接返回;
  • 行 {6} 取出當前遍歷到的函數定義為 fn
  • 行 {7} 執行函數 fn,傳入 dispatch 函數且 i+1,但是注意一定要 bind 下,因為 bind 會返回一個函數,并不會立即執行,什么時候執行呢?也就是當前 fn 函數里的 await next() 執行時,此時這個 next 也就是現在 fn 函數傳入的 dispatch.bind(null, (i + 1))
  • 行 {8} 中間的任一個中間件出現錯誤,就直接返回
/**
 * 中間件組合函數,可以參考 https://github.com/koajs/compose/blob/master/index.js
 * @param { Array } middlewares 
 */
function compose(ctx, middlewares) {
  // {1}
  if (!Array.isArray(middlewares)) throw new TypeError('Middlewares stack must be an array!')
  
  // {2}
  for (const fn of middlewares) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }
  
  return function() {
    const len = middlewares.length; // {3} 獲取數組長度
    const dispatch = function(i) { // {4} 這里是我們實現的關鍵
      if (len === i) { // {5} 中間件執行完畢
        return Promise.resolve();
      } else {
        const fn = middlewares[i]; // {6}
        
        try {
          // {7} 這里一定要 bind 下,不要立即執行
          return Promise.resolve(fn(ctx, dispatch.bind(null, (i + 1))));
        } catch (err) {
          // {8} 返回錯誤
          return Promise.reject(err);
        }
      }
    }

    return dispatch(0);
  }
}

const fn = compose(ctx, middlewares);

fn();

進行測試,是我們期望的結果,它的執流程為 f1 -> f2 -> f3 -> f2 -> f1,剛開始從 f1 往下游執行,直到 f3 最后一個中間件執行完畢,在流回到 f1,這種模式另外一個名字就是最著名的 “洋蔥模型”;

f1 start ->
f2 start ->
f3 service...
f2 end <-
f1 end <-

以上就是 Koa 中間件 Compose 的核心實現,關于 Koa 的更多內容可參見 Github 源碼。

Express 中間件機制

筆者這里看到是 Express 4.x 版本,其中一個重大改變是移除了內置中間件 Connect,詳情參考 遷移到 Express 4.x

我們通常說 Express 是線性的,那么請看下面代碼:

const Express = require('express')
const app = new Express();
const sleep = () => new Promise(resolve => setTimeout(function(){resolve(1)}, 2000))
const port = 3000

function f1(req, res, next) {
  console.log('f1 start ->');
  next();
  console.log('f1 end <-');
}

function f2(req, res, next) {
  console.log('f2 start ->');
  next();
  console.log('f2 end <-');
}

async function f3(req, res) {
  //await sleep();
  console.log('f3 service...');
  res.send('Hello World!')
}

app.use(f1);
app.use(f2);
app.use(f3);
app.get('/', f3)
app.listen(port, () => console.log(`Example app listening on port ${port}!`))

控制臺執行 curl localhost:3000 輸出如下,有點迷惑了,不是線性嗎?為什么和我們上面講 Koa 的輸出順序是一致呢?不也是洋蔥模型嗎?

f1 start ->
f2 start ->
f3 service...
f2 end <-
f1 end <-

少年,先莫及,再看一段代碼。
上面我們的 f3 函數其中注釋了一條代碼 await sleep() 延遲執行,現在讓我們打開這個注釋。

async function f3(req, res) {
  await sleep(); // 改變之處
  console.log('f3 service...');
  res.send('Hello World!')
}

控制臺再次執行 curl localhost:3000,發現順序發生了改變,上游中間件并沒有等待 f3 函數執行完畢,就直接執行了。

f1 start ->
f2 start ->
f2 end <-
f1 end <-
f3 service...

下面試圖復現其執行過程,可以看到 f1、f2 為同步代碼,而 f3 為異步,說了這么多,答案終于出來了。
Express 中間件實現是基于 Callback 回調函數同步的,它不會去等待異步(Promise)完成,這也解釋了為什么上面的 Demo 我加上異步操作,順序就被改變了。
在 Koa 的中間件機制中使用 Async/Await(背后全是 Promise)以同步的方式來管理異步代碼,它則可以等待異步操作。

f1 (req, res) {
  console.log('f1 start ->');
  f2 (req, res) { // 第一個 next() 地方
    console.log('f2 start ->');
    async f3 (req, res) { // 第二個 next() 地方
      await sleep(); // 改變之處
      console.log('f3 service...');
      res.send('Hello World!')
    }
    console.log('f2 end <-');
  }
  console.log('f1 end <-');
}

Express 中間件源碼解析

看過 Express 的源碼,再去看 Koa 的源碼,你會發現 Koa 是真的簡潔精煉,Express 的源碼看起來還是有點繞,需要時間去梳理,下面貼兩個重點實現的地方,詳情可參考 Express 4.x 源碼,感興趣的可以看下。

  1. 中間件掛載

初始化時主要通過 proto.use 方法將中間件掛載到自身的 stack 數組中

// https://github.com/expressjs/express/blob/4.x/lib/router/index.js#L428
proto.use = function use(fn) {
  var offset = 0;
  var path = '/';

  ...

  var callbacks = flatten(slice.call(arguments, offset));

  if (callbacks.length === 0) {
    throw new TypeError('Router.use() requires a middleware function')
  }

  for (var i = 0; i < callbacks.length; i++) {
    var fn = callbacks[i];

    if (typeof fn !== 'function') {
      throw new TypeError('Router.use() requires a middleware function but got a ' + gettype(fn))
    }

    // add the middleware
    debug('use %o %s', path, fn.name || '<anonymous>')

    var layer = new Layer(path, {
      sensitive: this.caseSensitive,
      strict: false,
      end: false
    }, fn);

    layer.route = undefined;

    this.stack.push(layer); // 中間件 route 的 layer 對象的 route 為 undefined,區別于路由的 router 對象
  }

  return this;
};
  1. 中間件的執行

Express 中間件的執行其中一個核心的方法為 proto.handle 下面省略了很多代碼。詳情參見源碼 Express 4.x,如何進行多個中間件的調用呢?proto.handle 方法的核心實現定義了 next 函數遞歸調用取出需要執行的中間件。

// https://github.com/expressjs/express/blob/dc538f6e810bd462c98ee7e6aae24c64d4b1da93/lib/router/index.js#L136
proto.handle = function handle(req, res, out) {
  var self = this;
  ...
  next();

  function next(err) {
    ...
    // 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;

      if (typeof match !== 'boolean') {
        // hold on to layerError
        layerError = layerError || match;
      }

      if (match !== true) {
        continue;
      }

      if (!route) {
        // process non-route handlers normally
        continue;
      }

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

響應機制

Koa 響應機制

在 Koa 中數據的響應是通過 ctx.body 進行設置,注意這里僅是設置并沒有立即響應,而是在所有的中間件結束之后做了響應,源碼中是如下方式寫的:

const handleResponse = () => respond(ctx);
fnMiddleware(ctx).then(handleResponse)

function respond(ctx) {
  ...
  res.end(body);
}

這樣做一個好處是我們在響應之前是有一些預留操作空間的,例如:

async function f1(ctx, next) {
  console.log('f1 start ->');
  await next();
  ctx.body += 'f1';
  console.log('f1 end <-');
}
async function f2(ctx, next) {
  console.log('f2 start ->');
  await next();
  ctx.body += 'f2 ';
  console.log('f2 end <-');
}
async function f3(ctx) {
  ctx.body = 'f3 '
  console.log('f3 service...');
}
fn().then(() => {
  console.log(ctx); // { body: 'f3 f2 f1' }
});

Express 響應機制

在 Express 中我們直接操作的是 res 對象,在 Koa 中是 ctx,直接 res.send() 之后就立即響應了,這樣如果還想在上層中間件做一些操作是有點難的。

function f2(req, res, next) {
  console.log('f2 start ->');
  next();
  res.send('f2 Hello World!') // 第二次執行
  console.log('f2 end <-');
}

async function f3(req, res) {
  console.log('f3 service...');
  res.send('f3 Hello World!') // 第一次執行
}

app.use(f2);
app.use(f3);
app.get('/', f3)

注意:向上面這樣如果執行多次 send 是會報 ERR_HTTP_HEADERS_SENT 錯誤的。

總結

本文從 Handler 處理方式、中間件執行機制的實現、響應機制三個維度來對 Express、Koa 做了比較,通常都會說 Koa 是洋蔥模型,這重點在于中間件的設計。但是按照上面的分析,會發現 Express 也是類似的,不同的是Express 中間件機制使用了 Callback 實現,這樣如果出現異步則可能會使你在執行順序上感到困惑,因此如果我們想做接口耗時統計、錯誤處理 Koa 的這種中間件模式處理起來更方便些。最后一點響應機制也很重要,Koa 不是立即響應,是整個中間件處理完成在最外層進行了響應,而 Express 則是立即響應。

點擊查看更多內容

本文首次發布于慕課網 ,轉載請注明出處,謝謝合作

2人點贊

若覺得本文不錯,就分享一下吧!

評論

相關文章推薦

正在加載中
意見反饋 幫助中心 APP下載
官方微信

舉報

0/150
提交
取消
TLC官网 <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <文本链> <文本链> <文本链> <文本链> <文本链> <文本链>