Redux的中間件,Axios的攔截器、Vuex的插件讓你迷惑嗎?實現一個精簡版的就完全搞懂了。

前言

前端中的庫不少,開發這些庫的做者會盡量的覆蓋到你們在業務中千奇百怪的需求,可是總有沒法預料到的,因此優秀的庫就須要提供一種機制,讓開發者能夠干預插件中間的一些環節,從而完成本身的一些需求。前端

本文將從axiosvuexredux的實現來教你怎麼編寫屬於本身的插件機制。vue

  • 對於新手來講:
    本文能讓你搞明白神祕的插件和攔截器究竟是什麼東西。ios

  • 對於老手來講:
    在你寫的開源框架中也加入攔截器或者插件機制,讓它變得更增強大吧!git

axios

首先咱們模擬一個簡單的axios,github

const axios = config => {
  if (config.error) {
    return Promise.reject({
      error: 'error in axios',
    });
  } else {
    return Promise.resolve({
      ...config,
      result: config.result,
    });
  }
};
複製代碼

若是傳入的config中有error參數,就返回一個rejected的promise,反之則返回resolved的promise。vuex

先簡單看一下axios官方提供的攔截器示例:編程

axios.interceptors.request.use(function (config) {
    // 在發送請求以前作些什麼
    return config;
  }, function (error) {
    // 對請求錯誤作些什麼
    return Promise.reject(error);
  });

// 添加響應攔截器
axios.interceptors.response.use(function (response) {
    // 對響應數據作點什麼
    return response;
  }, function (error) {
    // 對響應錯誤作點什麼
    return Promise.reject(error);
  });
複製代碼

能夠看出,不論是request仍是response的攔截求,都會接受兩個函數做爲參數,一個是用來處理正常流程,一個是處理失敗流程,這讓人想到了什麼?redux

沒錯,promise.then接受的一樣也是這兩個參數。axios

axios內部正是利用了promise的這個機制,把use傳入的每一對函數做爲一個intercetporapi

// 把
axios.interceptors.response.use(func1, func2)

// 註冊爲
const interceptor = {
    resolved: func1,
    rejected: func2
}
複製代碼

接下來簡單實現一下:

// 先構造一個對象 存放攔截器
axios.interceptors = {
  request: [],
  response: [],
};

// 註冊請求攔截器
axios.useRequestInterceptor = (resolved, rejected) => {
  axios.interceptors.request.push({ resolved, rejected });
};

// 註冊響應攔截器
axios.useResponseInterceptor = (resolved, rejected) => {
  axios.interceptors.response.push({ resolved, rejected });
};

// 運行攔截器
axios.run = config => {
  const chain = [
    {
      resolved: axios,
      rejected: undefined,
    },
  ];

  // 把請求攔截器往數組頭部推
  axios.interceptors.request.forEach(interceptor => {
    chain.unshift(interceptor);
  });

  // 把響應攔截器往數組尾部推
  axios.interceptors.response.forEach(interceptor => {
    chain.push(interceptor);
  });

  // 把config也包裝成一個promise
  let promise = Promise.resolve(config);

  // 暴力while循環解憂愁 
  // 利用promise.then的能力遞歸執行全部的攔截器
  while (chain.length) {
    const { resolved, rejected } = chain.shift();
    promise = promise.then(resolved, rejected);
  }

  // 最後暴露給用戶的就是響應攔截器處理事後的promise
  return promise;
};
複製代碼

axios.run這個函數看運行時的機制,首先構造一個chain做爲promise鏈,而且把正常的請求也就是咱們的請求參數axios也構造爲一個攔截器的結構,接下來

  • 把request的interceptor給unshift到函數頂部
  • 把response的interceptor給push到函數尾部

以這樣一段調用代碼爲例:

axios.useRequestInterceptor(resolved1, rejected1); // requestInterceptor1 

axios.useRequestInterceptor(resolved2, rejected2); // requestInterceptor2 

axios.useResponseInterceptor(resolved1, rejected1); // responseInterceptor1 

axios.useResponseInterceptor(resolved2, rejected2); // responseInterceptor2
複製代碼

這樣子構造出來的promise鏈就是這樣的chain結構:

[
    requestInterceptor2,
    requestInterceptor1,
    axios,
    responseInterceptor1,
    responseInterceptor2
]
複製代碼

至於爲何requestInterceptor的順序是反過來的,仔細看看代碼就知道 XD。

有了這個chain以後,只須要一句簡短的代碼:

let promise = Promise.resolve(config);

  while (chain.length) {
    const { resolved, rejected } = chain.shift();
    promise = promise.then(resolved, rejected);
  }

  return promise;
複製代碼

promise就會把這個鏈從上而下的執行了。

以這樣的一段測試代碼爲例:

axios.useRequestInterceptor(config => {
  return {
    ...config,
    extraParams1: 'extraParams1',
  };
});

axios.useRequestInterceptor(config => {
  return {
    ...config,
    extraParams2: 'extraParams2',
  };
});

axios.useResponseInterceptor(
  resp => {
    const {
      extraParams1,
      extraParams2,
      result: { code, message },
    } = resp;
    return `${extraParams1} ${extraParams2} ${message}`;
  },
  error => {
    console.log('error', error)
  },
);
複製代碼

成功的調用

在成功的調用下輸出 result1: extraParams1 extraParams2 message1

(async function() {
  const result = await axios.run({
    message: 'message1',
  });
  console.log('result1: ', result);
})();
複製代碼

失敗的調用

(async function() {
  const result = await axios.run({
    error: true,
  });
  console.log('result3: ', result);
})();
複製代碼

在失敗的調用下,則進入響應攔截器的rejected分支:

首先打印出攔截器定義的錯誤日誌:
error { error: 'error in axios' }

而後因爲失敗的攔截器

error => {
  console.log('error', error)
},
複製代碼

沒有返回任何東西,打印出result3: undefined

能夠看出,axios的攔截器是很是靈活的,能夠在請求階段任意的修改config,也能夠在響應階段對response作各類處理,這也是由於用戶對於請求數據的需求就是很是靈活的,沒有必要干涉用戶的自由度。

vuex

vuex提供了一個api用來在action被調用先後插入一些邏輯:

vuex.vuejs.org/zh/api/#sub…

store.subscribeAction({
  before: (action, state) => {
    console.log(`before action ${action.type}`)
  },
  after: (action, state) => {
    console.log(`after action ${action.type}`)
  }
})
複製代碼

其實這有點像AOP(面向切面編程)的編程思想。

在調用store.dispatch({ type: 'add' })的時候,會在執行先後打印出日誌

before action add
add
after action add
複製代碼

來簡單實現一下:

import { Actions, ActionSubscribers, ActionSubscriber, ActionArguments } from './vuex.type';

class Vuex {
  state = {};

  action = {};

  _actionSubscribers = [];

  constructor({ state, action }) {
    this.state = state;
    this.action = action;
    this._actionSubscribers = [];
  }

  dispatch(action) {
    // action前置監聽器
    this._actionSubscribers
      .forEach(sub => sub.before(action, this.state));

    const { type, payload } = action;
    
    // 執行action
    this.action[type](this.state, payload).then(() => {
       // action後置監聽器
      this._actionSubscribers
        .forEach(sub => sub.after(action, this.state));
    });
  }

  subscribeAction(subscriber) {
    // 把監聽者推動數組
    this._actionSubscribers.push(subscriber);
  }
}

const store = new Vuex({
  state: {
    count: 0,
  },
  action: {
    async add(state, payload) {
      state.count += payload;
    },
  },
});

store.subscribeAction({
  before: (action, state) => {
    console.log(`before action ${action.type}, before count is ${state.count}`);
  },
  after: (action, state) => {
    console.log(`after action ${action.type}, after count is ${state.count}`);
  },
});

store.dispatch({
  type: 'add',
  payload: 2,
});
複製代碼

此時控制檯會打印以下內容:

before action add, before count is 0
after action add, after count is 2
複製代碼

輕鬆實現了日誌功能。

固然Vuex在實現插件功能的時候,選擇性的將 type payload 和 state暴露給外部,而再也不提供進一步的修改能力,這也是框架內部的一種權衡,固然咱們能夠對state進行直接修改,可是不可避免的會獲得Vuex內部的警告,由於在Vuex中,全部state的修改都應該經過mutations來進行,可是Vuex沒有選擇把commit也暴露出來,這也約束了插件的能力。

redux

想要理解redux中的中間件機制,須要先理解一個方法:compose

function compose(...funcs: Function[]) {
  return funcs.reduce((a, b) => (...args: any) => a(b(...args)))
}
複製代碼

簡單理解的話,就是compose(fn1, fn2, fn3) (...args) = > fn1(fn2(fn3(...args)))
它是一種高階聚合函數,至關於把fn3先執行,而後把結果傳給fn2再執行,再把結果交給fn1去執行。

有了這個前置知識,就能夠很輕易的實現redux的中間件機制了。

雖然redux源碼裏寫的不多,各類高階函數各類柯里化,可是抽絲剝繭之後,redux中間件的機制能夠用一句話來解釋:

把dispatch這個方法不端用高階函數包裝,最後返回一個強化事後的dispatch

以logMiddleware爲例,這個middleware接受原始的redux dispatch,返回的是

const typeLogMiddleware = (dispatch) => {
    // 返回的其實仍是一個結構相同的dispatch,接受的參數也相同
    // 只是把原始的dispatch包在裏面了而已。
    return ({type, ...args}) => {
        console.log(`type is ${type}`)
        return dispatch({type, ...args})
    }
}
複製代碼

有了這個思路,就來實現這個mini-redux吧:

function compose(...funcs) {
    return funcs.reduce((a, b) => (...args) => a(b(...args)));
}

function createStore(reducer, middlewares) {
    let currentState;
    
    function dispatch(action) {
        currentState = reducer(currentState, action);
    }
    
    function getState() {
        return currentState;
    }
    // 初始化一個隨意的dispatch,要求外部在type匹配不到的時候返回初始狀態
    // 在這個dispatch後 currentState就有值了。
    dispatch({ type: 'INIT' });  
    
    let enhancedDispatch = dispatch;
    // 若是第二個參數傳入了middlewares
    if (middlewares) {
        // 用compose把middlewares包裝成一個函數
        // 讓dis
        enhancedDispatch = compose(...middlewares)(dispatch);
    }  
    
    return {
        dispatch: enhancedDispatch,
        getState,
    };
}

複製代碼

接着寫兩個中間件

// 使用

const otherDummyMiddleware = (dispatch) => {
    // 返回一個新的dispatch
    return (action) => {
        console.log(`type in dummy is ${type}`)
        return dispatch(action)
    }
}

// 這個dispatch實際上是otherDummyMiddleware執行後返回otherDummyDispatch
const typeLogMiddleware = (dispatch) => {
    // 返回一個新的dispatch
    return ({type, ...args}) => {
        console.log(`type is ${type}`)
        return dispatch({type, ...args})
    }
}

// 中間件從右往左執行。
const counterStore = createStore(counterReducer, [typeLogMiddleware, otherDummyMiddleware])

console.log(counterStore.getState().count)
counterStore.dispatch({type: 'add', payload: 2})
console.log(counterStore.getState().count)

// 輸出:
// 0
// type is add
// type in dummy is add
// 2
複製代碼

總結

  1. axios 把用戶註冊的每一個攔截器構形成一個promise.then所接受的參數,在運行時把全部的攔截器按照一個promise鏈的形式以此執行。
  • 在發送到服務端以前,config已是請求攔截器處理事後的結果
  • 服務器響應結果後,response會通過響應攔截器,最後用戶拿到的就是處理事後的結果了。
  1. vuex的實現最爲簡單,就是提供了兩個回調函數,vuex內部在合適的時機去調用(我我的感受大部分的庫提供這樣的機制也足夠了)。
  2. redux的源碼裏寫的最複雜最繞,它的中間件機制本質上就是用高階函數不斷的把dispatch包裝再包裝,造成套娃。本文實現的已是精簡了n倍之後的結果了,不過複雜的實現也是爲了不少權衡和考量,Dan對於閉包和高階函數的運用已經爐火純青了,只是外人去看源碼有點頭禿...

但願看了這篇文章的你,能對於前端庫中的插件機制有進一步的瞭解,進而更優秀的使用或者寫出更好的開源框架。

本文所寫的代碼都整理在這個倉庫裏了:
github.com/sl1673495/t…

代碼是使用ts編寫的,js版本的代碼在js文件夾內,各位能夠按本身的需求來看。

相關文章
相關標籤/搜索