koa中間執行機制

start


基於 koa 2.11 按如下流程分析:git

const Koa = require('koa');
const app = new Koa();

const one = (ctx, next) => {
  console.log('1-Start');
  next();
  ctx.body = { text: 'one' };
  console.log('1-End');
}
const two = (ctx, next) => {
  console.log('2-Start');
  next();
  ctx.body = { text: 'two' };
  console.log('2-End');
}

const three = (ctx, next) => {
  console.log('3-Start');
  ctx.body = { text: 'three' };
  next();
  console.log('3-End');
}

app.use(one);
app.use(two);
app.use(three);

app.listen(3000);

app.use()


use 方法定義在 koa/lib/application.js 中:github

use(fn) {
  // check middleware type, must be a function
  if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
  // 兼容 generator
  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;
}

this.middleware數組

這就是一個數組,用來存放全部中間件,而後按順序執行。promise

this.middleware = [];

app.listen()


這個方法定義在 koa/lib/application.js 中:閉包

listen(...args) {
  debug('listen');
  
  // 建立 http 服務並監聽
  const server = http.createServer(this.callback());
  return server.listen(...args);
}

this.callback()app

callback() {
  // 處理中間件
  const fn = compose(this.middleware);

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

  const handleRequest = (req, res) => {
    // 建立 Context
    const ctx = this.createContext(req, res);
    // 執行中間件處理請求和響應
    return this.handleRequest(ctx, fn);
  };

  return handleRequest;
}

this.handleRequestkoa

handleRequest(ctx, fnMiddleware) {
  const res = ctx.res;
  res.statusCode = 404;
  const onerror = err => ctx.onerror(err);
  // 將響應發出的函數
  const handleResponse = () => respond(ctx);
  onFinished(res, onerror);
  // 這裏會將 ctx 傳給中間件進行處理,
  // 當中間件流程走完後,
  // 會執行 then 函數將響應發出
  return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}

respond(ctx)函數

function respond(ctx) {
  // 省略其餘代碼
  // ...
  // 發出響應
  res.end(body);
}

捋一捋流程,由上面的代碼能夠知道,存放中間的數組是經過 compose 方法進行處理,而後返回一個fnMiddleware函數,接着將 Context 傳遞給這個函數來進行處理,當fnMiddleware執行完畢後就用respond方法將響應發出。ui

compose(this.middleware)


compose 函數經過koa-compose引入:this

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

compose 定義在koajs/compose/index.js

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) {
    // 這個 index 是標識上一次執行的中間件是第幾個
    let index = -1
    
    // 執行第一個中間件
    return dispatch(0)
    function dispatch (i) {
      // 檢查中間件是否已經執行過,
      // 舉個例子,當執行第一個中間件時 dispatch(0),
      // i = 0, index = -1, 說明沒有執行過,
      // 而後 index = i, 而 index 經過閉包保存,
      // 若是執行了屢次,就會報錯
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      
      // 經過傳入的索引從數組中獲取中間件
      let fn = middleware[i]
      
      // 若是當前索引等於中間件數組的長度,
      // 說明已經中間件執行完畢,
      // fn 爲 fnMiddleware(ctx) 時沒有傳入的第二個參數,
      // 即 fn = undefined
      if (i === middleware.length) fn = next
      // fn 爲 undefined, 返回一個已經 reolved 的 promise
      if (!fn) return Promise.resolve()
      
      try {
        // 執行中間件函數並將 dispatch 做爲 next 函數傳入
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

結束執行流程

如今來捋一下 fnMiddleware的執行流程:

// fnMiddleware 接收兩個參數
function (context, next) {
  // ....
}

// 將 context 傳入,並無傳入 next,
// 因此第一次執行時是沒有傳入 next 的
fnMiddleware(ctx).then(handleResponse).catch(onerror);

next == undefined時會結束中間件執行,流程以下:

function dispatch (i) {
  //...
  
  // 經過傳入的索引從數組中獲取中間件,
  // 可是由於已經執行完了全部中間件,
  // 因此當前 i 已經等於數組長度,
  // 即 fn = undefined
  let fn = middleware[i]

  // 若是當前索引等於中間件數組的長度,
  // 說明已經中間件執行完畢,
  // 又由於 fnMiddleware(ctx) 時沒有傳入的第二個參數 next,
  // 因此 fn = undefined
  if (i === middleware.length) fn = next
  
  // fn 爲 undefined, 返回一個已經 reolved 的 promise
  // 中間件執行流程結束
  if (!fn) return Promise.resolve()
  
  // ...
}

中間件執行流程

上面先說告終束流程,如今說一下如何順序執行,造成洋蔥模型:

function dispatch (i) {
  // ...省略其餘代碼
  
    try {
    // 分步驟說明
    // 首先經過 bind 將 dispatch 構建爲 next 函數
    const next = dispatch.bind(null, i + 1);
    // 將 ctx, next 傳入執行當前中間件,
    // 當在中間件中調用 next() 時,
    // 本質上是調用 diapatch(i + 1),
    // 也就是從數組中獲取下一個中間件進行執行,
    // 在這時,會中斷當前中間件的執行流程轉去執行下一個中間件,
    // 只有當下一箇中間件執行完畢,纔會恢復當前中間件的執行
    const result = fn(context, next);
    // 中間件執行完畢,返回已經 resolve 的 promise,
    // 那麼上一個中間件接着執行剩下的流程,
    // 這樣就造成了洋蔥模型
    return Promise.resolve(result);
  } catch (err) {
    return Promise.reject(err)
  }
}

開頭的例子執行結果以下:

const one = (ctx, next) => {
  console.log('1-Start');
  next();
  ctx.body = { text: 'one' };
  console.log('1-End');
}
const two = (ctx, next) => {
  console.log('2-Start');
  next();
  ctx.body = { text: 'two' };
  console.log('2-End');
}

const three = (ctx, next) => {
  console.log('3-Start');
  ctx.body = { text: 'three' };
  next();
  console.log('3-End');
}

// 1-Start
// 2-Start
// 3-Start
// 3-End
// 2-End
// 1-End
// 而 ctx.body 最終爲 { text: 'one' }

next()


沒有調用 next()

// 沒有調用 next() 函數
app.use((ctx, next) => {
  console.log('Start');
  ctx.body = { text: 'test' };
  console.log('End');
});

由於 next 函數本質上就是經過dispatch(i + 1)來調用下一個中間件,若是沒有調用 next 函數,就沒法執行下一個中間件,那麼就表明當前中間件流程執行結束。

屢次調用 next()

app.use((ctx, next) => {
  console.log('Start');
  ctx.body = { text: 'test' };
  // 屢次調用 next 函數
  next(); // 本質上是 dispatch(i + 1)
  next(); // 本質上是 dispatch(i + 1)
  console.log('End');
});

這裏假設 nextdispatch(3),那麼 index 就爲 2,第一次執行 next 函數時,會發生以下邏輯:

// index == 2
// i == 3
// 不會報錯
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
// 賦值後 index 爲 3 了 
index = i

假設第三個中間件是最後一箇中間件,那麼執行完第一次 next 函數會當即執行第二個 next 函數,依然執行這個邏輯,可是 index 已經爲 3 了,因此會致使報錯:

// index == 3
// i == 3
// 報錯
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
相關文章
相關標籤/搜索