1. 程式人生 > 實用技巧 >koa-compose原始碼閱讀與學習

koa-compose原始碼閱讀與學習

原始碼倉庫:koa-compose

前言

文章開始之前來做一道題目。給一個函式陣列,封裝一個函式可以依次執行這個函式數組裡的函式

function func1() {
  console.log(1)
}
function func2() {
  console.log(2)
}
function func3() {
  console.log(3)
}
const arr=[func1,func2,func3]
寫一個compose函式,當我們呼叫compose的時候,依次執行func1、func2、func3,打印出1,2,3,4
function compose(){
  //your code goes here...
}

我們很快就能想到使用迴圈遍歷資料,依次執行

function compose() {
  for (let item of arr) {
    item()
  }
}
compose()
//列印輸出:
//1
//2
//3

當然這不是我們想要的答案,我們想要函式這樣執行func3(func2(func1()))。可以這樣寫程式碼

//法一:使用reduce,程式碼簡潔
function compose1() {
  return arr.reduce((prev, curr) => (...args) => curr(prev(...args)))
}

// 法二:也可以使用迴圈遍歷賦值
function compose2(){
  let prev
  for(let i=0;i<arr.length;i++){
    prev=arr[i](prev)
  }
  return prev || function(){}
}

我們變化一下,給函式傳入引數,題目變成下面這樣

function func1(next){
  console.log(1)
  next()
  console.log(2)
}
function func2(next){
  console.log(3)
  next()
  console.log(4)
}
function func3(next){
  console.log(5)
  next()
  console.log(6)
}
const arr=[func1,func2,func3]
寫一個compose函式,當我們呼叫composeSync的時候,打印出1,3,5,6,4,2
function composeSync(){
  //your code goes here...
}

我們先來分析一下題目,每個函式都帶有next引數,並且next是一個函式,又因為列印輸出的順序可知,next是陣列下一個項。也就是說compose函式需要把arr數組裡的每一項都串聯起來並把後一項當作引數傳入當前項執行,所以前半部分會輸出1,3,5.又因為都是同步的程式碼,所以next()都執行完之後才會執行後面的程式碼所以輸出6,4,2。分析完了之後我們可以開始寫程式碼了,最容易讓人想到的方式是遞迴

//方法一:使用遞迴
const composeSync1=function(){
  function dispatch(index){
    if(index===arr.length) return ;
    return arr[index](()=>dispatch(index+1))
  }
  return dispatch(0)
}
//方法二:使用迴圈(一般能用遞迴的都能使用迴圈)
const composeSync2=function(){
  let prev=()=>{ }
  for(let i=arr.length-1;i>=0;i--){
    prev=arr[i].bind(this,prev)
  }
  return prev()
} 
composeSync1()
composeSync2()

//列印輸出:
//1
//3
//5
//6
//4
//2

不知不覺我們已經把洋蔥模型基本實現了,只要稍加完善(容錯處理、非同步處理等等)即可使用。下一步我們加上錯誤處理和非同步處理,這個可以參考原始碼

//容錯:判斷一下arr是否是陣列,判斷arr每一項是否是函式,判斷陣列長度是否大於0,try...catch()...下銜接項
//非同步:async...await

koa-compose解析

我們先來字面上理解一下,compose是組合組成的意思,它的作用正是實現洋蔥模型,管理所有中介軟體的。koa-compose是koa的一箇中間件,它主要是實現洋蔥模型。通過上面的幾道題目,可以模糊認為compose就是洋蔥模型的雛型,數組裡面的每個函式就是一箇中間件,洋蔥模型的執行機制以及中介軟體的管理方式。那什麼是洋蔥模型呢?什麼是中介軟體呢?

洋蔥模型與中介軟體

  • 洋蔥模型:對資料進行序列處理的一種機制,類似於洋蔥,被一層層中介軟體(處理資料的)包裹。
  • 中介軟體:處理資料的函式、類、方法,分散在模型的各個部位。
    盜圖兩張:

原始碼解析

'use strict'

/**
 * Expose compositor.
 */

module.exports = compose

/**
 * Compose `middleware` returning
 * a fully valid middleware comprised
 * of all those which are passed.
 *
 * @param {Array} middleware
 * @return {Function}
 * @api public
 */

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!')
  }

  /**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */

  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)
      }
    }
  }
}

我們先忽略註釋,實際程式碼20行不到,非常精簡。首先判斷一下傳進來的middleware是不是陣列並迴圈一下判斷每一項是不是函式。然後在return一個函式傳進來引數context(上下文物件)、next(下一步要執行的函式,也就是中介軟體middleware相比當前項的下一項)。定義dispatch函式用於遞迴將func1,func2,func3封裝成func3(func2(func1))這種結構。首先要判斷邊界,通過index和middleware長度進行比較,還定義了一個index,用於判斷當前的中介軟體是否已經有過。最後return當前項,把下一項當作引數傳給當前項,這樣就能保證所有中介軟體都能巢狀完成。

手動實現一個洋蔥模型

實現步驟與思路就是我們剛開始做的那幾道題目,一步一步做過來即可實現一個簡易版本的洋蔥模型。最後貼一下程式碼

const app = { middlewares: [] };
app.use = (fn) => {
   app.middlewares.push(fn);
};

app.compose = function() {
  // Your code goes here
  function dispatch(index){
    if(index===app.middlewares.length) return ;
    const fn=app.middlewares[index];
    return fn(()=>dispatch(index+1))
  }
  dispatch(0);
}
app.use(next => {
   console.log(1);
   next();
   console.log(2);
});
app.use(next => {
   console.log(3);
   next();
   console.log(4);
});
app.use(next => {
   console.log(5);
   next();
   console.log(6);
});
app.compose();

參考