koa,redux等主流框架的中介軟體設計思想原理分析

FE_xuer發表於2019-03-03

es6時代來了,相信會讓一批有java,C++等面嚮物件語言開發基礎的夥子們,感受到來自js世界滿滿的善意。es6可以讓開發者幾乎擺脫prototype的程式設計模式,讓開發更加如絲般順滑,雖然目前大部分瀏覽器並沒有支援es6,但是打雞血般突飛猛進的node和與時俱進的babel,還是已經讓大部分前端和node開發者享受到es6時代的酸爽。物件導向有很多精妙的設計思想,雖說思想
js框架相信大家都用過不少了,前端如redux,後臺框架如express,koa,等等等等,當然還有很多其他優秀的框架,不過與我們今天的主題無關就不多說了。如果大家使用過redux或者koa,應該對其中的中介軟體不會陌生。中介軟體雖然在不同的框架中用法各有不同,但是實現原理卻是大體一致的。我們發現作為一箇中介軟體,不管其具體實現的是什麼能力,其實它一個最主要的職能就是增強目標物件的能力。在研究各大中介軟體的過程中,隱隱約約看看一個及其熟悉的背影,那就是裝飾模式。在眾多設計模式中,裝飾模式應用最廣泛的就是增強目標物件能力,大部分中介軟體的實現,應該都是借鑑了裝飾模式這種靈活的設計思想。因此,這裡我們首先來介紹一下 裝飾模式,說到裝飾模式,就不得不先提一下es7提案中新增的註解功能(本人習慣叫註解,因為寫法類似於java中的註解),比如如下一個類,定義了加和減兩個方法:

class MyClass {
  add(a, b){
    return a + b;
  }
  sub(a, b){
    return a - b;
  }
}
複製程式碼

假如現在有個需求,需要實現每次呼叫add或者sub函式的時候,都分別列印出方法呼叫前後的log,比如呼叫前`before operate`,呼叫後列印`after operate`,我們是否需要在呼叫前後分別呼叫console.log(),es7裡面當然不必了,我們只需要定義好我們需要的列印函式,然後使用@註解,比如如下使用方式:

//註解的函式定義
let log = (type) => {
    const logger = console;
    return (target, name, descriptor) => {
      const method = descriptor.value;
      descriptor.value =  (...args) => {
            logger.info(`(${type}) before function execute: ${name}(${args}) = ?`);
            let ret = method.apply(target, args);
            logger.info(`(${type})after function execute: ${name}(${args}) => ${ret}`);
            return ret;
        }
    }
}
//註解呼叫
class MyClass {
  @log("add")
  add(a, b){
    return a + b;
  }
  @log("sub")
  sub(a, b){
    return a - b;
  }
}
複製程式碼

如上在我們呼叫MyClass例項化方法add和sub的時候,分別會列印呼叫前和呼叫後的日誌了,這就是在不改動MyClass原始碼的情況下,使用裝飾模式對於原方法add和sub的能力增強,這是es7的語法,定義註解的方式很簡單,一個函式返回另一個函式,返回函式的引數分別是target:類的上下文,name:目標方法名,descriptor就不用解釋了吧,不理解可以看看defineProperty的定義,簡單易用,需要增強其他能力,那就多定義幾個,多@幾下。這是es7的,編譯器支援的還是看著有點抽象,接下來我們來看看普通es5物件如何使用裝飾模式進行能力的增強。如下一個add函式

function add(a, b){
	return a + b;
}
複製程式碼

現在需要增強log和notify的能力,在呼叫前列印日誌併傳送訊息。程式碼如下:

function logDecorator(target){
	var old = target;
	return function(){
		console.log("log before operate");
		var ret = old.apply(null,arguments);
		console.log(target.name,"results:",ret,",log after operate");
		return ret;
	}
}

function notifyDecorator(target){
	var old = target;
	return function(){
		console.log("notify before operate");
		var ret = old.apply(null,arguments);
		console.log("finished, notify u");
		return ret;
	}
}
var add = logDecorator(notifyDecorator(add));
複製程式碼

稍微解釋一下,var old = target;先將原目標儲存,並返回一個函式,在該函式中var ret = old.apply(null,arguments);執行原目標函式的呼叫,這時候,或前或後,在需要的節點進行具體的能力增強即可,是不是很失望呢,咋就這麼簡單?不好意思,真就這麼簡單,這就是各大框架中高大上的中介軟體的基本原理了。以koa舉例,如果我們需要簡單實現一個log中介軟體,應該怎麼做呢?

module.exports = (opts = {}) => {
    var log = console.log;
    return async (ctx, next) => {
        log("before ",ctx.request.url, "...");
        await next();
        log("after ",ctx.request.url, "...");
    }
}
複製程式碼

如上程式碼就是了,當然,我們可以在中介軟體中做一些過濾條件,比如我們只希望對非靜態資源的請求進行自定義的log等等。koa以及express作為一個後臺框架,中介軟體比較不同的地方就在於路由的實現,聽起來似乎有點複雜哦。其實,以koa為例,想要實現路由,我們對ctx.request.url進行字串分析處理進入不同的處理函式,是否就可以有一個基本的路由功能了呢?所以中介軟體很強大,其實也很簡單,它並不矛盾。中介軟體定義完了,接下來看看怎麼用了。
我們的中介軟體可能需要十個八個,那這麼多箇中介軟體們是如何進行compose呢,不同框架實現方式可能不太一致,但是原理還是同一個原理。一批中介軟體加入之後,存於一個函式列表中,然後對列表中的函式進行順序執行,且每一個函式的返回值作為下一個函式的入參。我們以koa和redux的中介軟體為例來分析一下。首先來看koa的:

app.use(中介軟體);
複製程式碼

koa-compose原始碼:

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, function next () {
          return dispatch(i + 1)
        }))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}
複製程式碼
let fn = compose(middlewares);
fn(ctx)...;
複製程式碼

首先,使用app.use()加入中介軟體,使用如上compose函式對中介軟體middlewares列表進行遞迴呼叫。具體程式碼就不一一解釋了吧,對於熟悉koa以及express的同學,應該很熟悉next的用法,這其實就是我們前面的var old = target;這種方式的升級版本,並且通過next的方式可以更加優雅地解了中介軟體新增的問題,而不需要使用巢狀呼叫的方式。
遞迴遍歷是個思路,其實我們js原生提供了一種方式進行compose,可以更加優雅解決這個問題,redux就是採用了這種呼叫方式,就是使用reduce函式,我們來看看redux處理方式:

export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
複製程式碼

reduce再加上es6簡直賞心悅目有沒有,如果看的不太舒服可以轉成es5看看,給大家一個簡單的測試用例跑跑,可能會更加好理解:

function fun1(obj){
	console.log(1);
	obj.a=1;
	return obj;
}
function fun2(obj){
	console.log(2);
	obj.b=2;
	return obj;
}
let fn = compose(fun1,fun2);
fun({});
複製程式碼

看看呼叫的結果是啥,這只是一個幫助理解的小栗子,栗子雖小,但是已經小秀了一把肌肉了,重點就在於我們在各個中介軟體中透傳傳入的這個引數obj了,可以是個物件,也可以是個函式,總之是我們可以為所欲為地增強它的能力。
根據不同的目的,中介軟體的實現機制會有一些差異,koa跟redux其實就有比較明顯的一些區別,有興趣可以深入去看看,但是萬變不離其宗。
到此,中介軟體的定義和呼叫中的一些核心邏輯就講完了,都是個人一些淺見,水平有限,如有謬誤,敬請指出!!!

相關文章