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();