前端中的庫不少,開發這些庫的做者會盡量的覆蓋到你們在業務中千奇百怪的需求,可是總有沒法預料到的,因此優秀的庫就須要提供一種機制,讓開發者能夠干預插件中間的一些環節,從而完成本身的一些需求。前端
本文將從axios
、vuex
和redux
的實現來教你怎麼編寫屬於本身的插件機制。vue
對於新手來講:
本文能讓你搞明白神祕的插件和攔截器究竟是什麼東西。ios
對於老手來講:
在你寫的開源框架中也加入攔截器或者插件機制,讓它變得更增強大吧!git
首先咱們模擬一個簡單的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傳入的每一對函數做爲一個intercetpor
api
// 把
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也構造爲一個攔截器的結構,接下來
以這樣一段調用代碼爲例:
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提供了一個api用來在action被調用先後插入一些邏輯:
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中的中間件機制,須要先理解一個方法: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
複製代碼
axios
把用戶註冊的每一個攔截器構形成一個promise.then所接受的參數,在運行時把全部的攔截器按照一個promise鏈的形式以此執行。但願看了這篇文章的你,能對於前端庫中的插件機制有進一步的瞭解,進而更優秀的使用或者寫出更好的開源框架。
本文所寫的代碼都整理在這個倉庫裏了:
github.com/sl1673495/t…
代碼是使用ts編寫的,js版本的代碼在js文件夾內,各位能夠按本身的需求來看。