「ReadingKoa」Day 1 — Koa中間件的運行原理


Koa中的中間件與Express的不同,Koa使用洋蔥模型。 Koa的中間件僅包含四個文件,今天我們只看主要文件-application.js。 它已經包含了中間件如何工作的核心邏輯。
洋蔥模型

準備工作

git clone [email protected]:koajs/koa.git
npm install

我們在項目的根目錄添加index.js進行測試:

// index.js
// Include the entry file of koa
const Koa = require('./lib/application.js');
const app = new Koa();
const debug = require('debug')('koa');
app.use(async (ctx, next) => {
  console.log(1);
  await next();
  console.log(6);
  const rt = ctx.response.get('X-Response-Time');
  console.log(`${ctx.method} ${ctx.url} - ${rt}`);
});
// time logger here
app.use(async (ctx, next) => {
  console.log(2);
  const start = Date.now();
  await next();
  console.log(5);
  const ms = Date.now() - start;
  ctx.set('X-Response-Time', `${ms}ms`);
});
app.use(async (ctx, next) => {
  console.log(3);
  ctx.body = 'Hello World';
  await next();
  console.log(4);
});

app.listen(3000);

你可以運行以下命令來啓動服務器:

node index.js

打開瀏覽器並訪問http:// localhost:3000,您將看到1,2,3,4,5,6輸出。 我們把這稱爲洋蔥模型(中間件)

洋蔥模型如何工作

讓我們閱讀koa的核心,看看中間件是如何工作的。 在index.js中,我們使用如下中間件:

const app = new Koa();
app.use(// middleware);
app.use(// middleware);
app.listen(3000);

看一下application.js,這是與中間件相關的編碼,我在代碼中添加了一些註釋。

const compose = require('koa-compose');

module.exports = class Application extends Emitter {
  
  constructor() {
    super();
    this.proxy = false;
    // Step 0: 初始化中間件列表
    this.middleware = [];
  }

  use(fn) {
    // Step 1: 將中間件添加到列表中
    this.middleware.push(fn);
    return this;
  }

  listen(...args) {
    debug('listen');
    // Step 2: 使用 this.callback() 組成所有中間件
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }

  callback() {
    // Step 3: 這是最重要的部分-compose,將所有內容分組
    // 中間件到一個大 function 並返回 promise ,我們將繼續看
    // 關於這個函數
    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);
    // Step 4: Resolve 這個 promise
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }
}

關於 Compose 函數

有關compose函數的更多信息,我們可以看一下koa-compose包:

module.exports = compose
function compose (middleware) {
  // skipped type checking code here
  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)
      }
    }
  }
}

所有中間件都傳遞到compose函數中,並返回dispatch(0),它立即執行dispatch函數並返回promise。 在我們瞭解dispatch函數的內容之前,我們必須瞭解promise的語法。

關於 Promise

通常我們這樣使用 promise :

const promise = new Promise(function(resolve, reject) {
  if (success){
    resolve(value);
  } else {
    reject(error);
  }
});

在Koa中,我們可以這樣用:

let testPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('test success');
  }, 1000);
});
Promise.resolve(testPromise).then(function (value) {
  console.log(value); // "test success"
});

因此,我們知道在compose函數中,它會返回一個promise

回到Koa——compose middleware

module.exports = compose
function compose (middleware) {
  // skipped type checking code here
  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)
      }
    }
  }
}

dispatch是一個遞歸函數,它將循環所有中間件。 在我們的index.js中,我們有3箇中間件,所有3箇中間件將在await next()之前執行那些編碼。

app.use(async (ctx, next) => {
  console.log(2);
  const start = Date.now();
  await next(); // <- stop here and wait for the next middleware complete
  console.log(5);
  const ms = Date.now() - start;
  ctx.set('X-Response-Time', `${ms}ms`);
});

我們可以看看index.js中這三個中間件的執行順序:

  • 執行dispatch(0)時,執行Promise.resolve(fn(context, dispatch.bind(null, 0 + 1)))
  • 第一個中間件內容將一直運行到await next()
  • next() = dispatch.bind(null, 0 + 1), 這是第二個中間件。
  • 第二個中間件將一直運行到await next()
  • next() = dispatch.bind(null, 1 + 1),這是第三種中間件,它一直運行到await next()
  • next() = dispatch.bind(null, 2 + 1),沒有第四種中間件,它將立即返回if (!fn) return Promise.resolve(),解析第三中間件中的await next(),執行第三中間件中的其餘代碼。
  • 解析第二個中間件中的await next(),執行第二個中間件中的剩餘代碼。
  • 他等待第一個中間件中的next()被解析,第一個中間件中的剩餘代碼被執行。

爲什麼選擇洋蔥模型?

如果我們在中間件中有 async / await,則編碼將更加簡單。 當我們想爲api請求編寫時間記錄器時,通過添加以下中間件可以非常簡單:

app.use(async (ctx, next) => {
  const start = Date.now();
  await next(); // your API logic
  const ms = Date.now() - start;
  console.log('API response time:' + ms);
});
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章