koa,redux等主流框架的中間件設計思想原理分析

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裏面固然沒必要了,咱們只須要定義好咱們須要的打印函數,而後使用@註解,好比以下使用方式:java

//註解的函數定義
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函數node

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

如今須要加強log和notify的能力,在調用前打印日誌併發送消息。代碼以下:es6

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中間件,應該怎麼作呢?express

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源碼:redux

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看看,給你們一個簡單的測試用例跑跑,可能會更加好理解:api

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其實就有比較明顯的一些區別,有興趣能夠深刻去看看,可是萬變不離其宗。
到此,中間件的定義和調用中的一些核心邏輯就講完了,都是我的一些淺見,水平有限,若有謬誤,敬請指出!!!瀏覽器

相關文章
相關標籤/搜索