前言
- 很多東西看起來複雜,實際弄一遍感覺簡單,主要還是自己懶。
- 接上篇
dva-loading
- 這個是一個全局性質的loading狀態變化。
- 一般我們寫個組件變化之類的都會在父組件上定義個狀態,然後滿足狀態條件就渲染特定的東西。這個loading把這些變成全局可用。不需要每次都寫了。
- 實際是我們在派發一個異步動作前後,這個插件會派發它自己的action,從而更改loading狀態。
- 工作模式是這樣:true是正在loading狀態,一開始,沒有組件派發action,所有的loading都是false,一旦有saga收到了action進行異步處理,那麼這個saga所在的namespace就會置爲true,全局也會置爲true,全局是只要有一個namespace是true,它就是true。等異步執行完,這個namespace就會變成false,全局會看是否所有的都是false,如果是,那就會改成false。基礎好的可能已經發現,這個操作簡直是爲some量身定做的。
- 通過拿到true和false可以得到加載狀態。全局可用可以省下大量代碼。
- 光說可能比較難理解,我特地截了圖:
- 可以發現在派發saga動作前後會派發個show和hide的action,有個global的true和false,每個組件的true和false,這個saga的true和false。
- 其實我最佩服的是能想出這模式的想象力。居然還能搞出全局loading,我以前從來沒這麼想過。
- 下面看使用方法:
cnpm i dva-loading --save
import createLoading from 'dva-loading'
app.use(createLoading())
- 三句話解決,完美。
dva-loading實現
- 但是這個是怎麼實現呢?需要藉助鉤子。
- 我們打印下createLoading的執行結果,發現就是個這樣的對象:
{
extraReducers:{loading:fn}
onEffect:fn
}
- 這個extraReducers和onEffect就是鉤子,dva裏做了一些鉤子函數,使用use或者給dva傳配置都是藉助鉤子來更改配置,這個和大多數框架差不多。
- 前面我們實現的dva是沒有鉤子函數的。所以需要做幾個鉤子出來。
- dva使用鉤子的方式有2種,一種就是use,一種是直接傳配置項,根據上面執行結果發現,這2種其實是一個方法。
- 所以我們需要提取配置項的鉤子,存到dva實例裏,並且把use的結果也存進去。
- 先模仿dva-loading把reducer和effects實現下,利用extraReducer鉤子可以製作reducer,我們只需要配置進入combineReducer的reducer即可。
- 而onEffect是就可以相當於我們自己寫effects的中間件,可以拿到要執行的effects。這樣我們在執行前後去put我們自己的action,用reducer處理成對應的狀態就可以了。
const SHOW = '@@DVA_LOADING/SHOW'
const HIDE = '@@DVA_LOADING/HIDE'
const NAMESPACE = 'loading'
export default function createLoading(options) {
let initalState = {
global: false,//全局
model: {},//用來確定每個namespace是true還是false
effects: {}//用來收集每個namespace下的effects是true還是false
}
const extraReducers = {//這裏直接把寫進combineReducer的reducer準備好,鍵名loading
[NAMESPACE](state = initalState, { type, payload }) {
let { namespace, actionType } = payload || {}
switch (type) {
case SHOW:
return {
...state,
global: true,
model: {
...state.model, [namespace]: true
},
effects: {
...state.effects, [actionType]: true
}
}
case HIDE: {
let effects = { ...state.effects, [actionType]: false }//這裏state被show都改成true了
let model = {//然後需要檢查model的effects是不是都是true
...state.model,
[namespace]: Object.keys(effects).some(actionType => {//查找修改完的effects
let _namespace = actionType.split('/')[0]//把前綴取出
if (_namespace != namespace) {//如果不是當前model的effects就繼續
return false
}//用some只要有一個true就會返回,是false就繼續
return effects[actionType]//否則就返回這個effects的true或者false
})
}
let global = Object.keys(model).some(namespace => {//只要有一個namespace是true那就返回
return model[namespace]
})
return {
effects,
model,
global
}
}
default: return state
}
}
}
function onEffect(effects, { put }, model, actionType) {//actiontype就是帶前綴的saga名
const { namespace } = model
return function* (...args) {
try {//這裏加上try,防止本身的effects執行掛了,然後就一直不會hide,導致整個功能失效。
yield put({ type: SHOW, payload: { namespace, actionType } })
yield effects(...args)
} finally {
yield put({ type: HIDE, payload: { namespace, actionType } })
}
}
}
return {
onEffect,
extraReducers
}
}
-
註釋全部寫上了,下面我們就需要把這個鉤子添加進我們的dva裏。
-
現在情況是我們這個配置是通過use或者options放進dva裏生成dva實例,因爲我們傳來的是個對象,裏面有可能有很多東西,所以還要對這個對象進行篩選,提取出我們想要的。於是就可以建一個類,來管理外部對象。
plugin.js
const hooks = [
"onEffect",//effect中間件
"extraReducers"//添加reducer
]
export function filterHooks(options) {//篩選符合鉤子名的配置項
return Object.keys(options).reduce((prev, next) => {
if (hooks.indexOf(next) > -1) {
prev[next] = options[next]
}
return prev
}, {})
}
export default class Plugin {//用來統一管理
constructor() {//初始化把鉤子都做成數組
this.hooks = hooks.reduce((prev, next) => {
prev[next] = []
return prev
}, {})//{hook:[],hook:[]}
}
use(plugin) {//因爲會多次use,所以就把函數或者對象push進對應的鉤子裏
const { hooks } = this
for (let key in plugin) {
hooks[key].push(plugin[key])
}//{hook:[fn|obj]}
}
get(key) {//不同的鉤子進行不同處理
if (key === 'extraReducers') {//處理reducer,就把所有對象併成總對象,這裏只能是對象形式才能滿足後面併入combine的操作。
return Object.assign({}, ...this.hooks[key])
} else {
return this.hooks[key]//其他鉤子就返回用戶配置的函數或對象
}
}
}
- 有了個這個鉤子管理機,我們還需要在對應的地方插入鉤子。
- 首先是解決use和配置項應該是同一個的問題:
let plugin = new Plugin()
plugin.use(filterHooks(opts))
app.use = plugin.use.bind(plugin)
- 這樣用use實際調用的就是plugin的use。要麼進配置項也是會走plugin.use的。
- 另外我們對配置項進行了過濾,把鉤子過濾出來傳進use裏。
- 再來就是配置reducer,extraReducer的添加很容易想到,它就是在combine那裏多加了一個我們配置好的reducer。
function getReducer(app) {
let reducers = {
router: connectRouter(app._history)
}
for (let m of app._models) {//m是每個model的配置
reducers[m.namespace] = function (state = m.state, action) {//組織每個模塊的reducer
let everyreducers = m.reducers//reducers的配置對象,裏面是函數
let reducer = everyreducers[action.type]//相當於以前寫的switch
if (reducer) {
return reducer(state, action)
}
return state
}
}
let extraReducers = plugin.get('extraReducers')
return combineReducers({
...reducers,
...extraReducers//這裏是傳來的中間件對象
})//reducer結構{reducer1:fn,reducer2:fn}
}
- 注意這個getReducer的函數必須放到dva裏面才能拿到plugin。
- 實際改動就2句話,get,然後放進去即可。
- 而effects中間件,肯定是對裏面執行saga的地方下手了:
function getSagas(app) {
let sagas = []
for (let m of app._models) {
sagas.push(function* () {
for (const key in m.effects) {//key就是每個函數名
const watcher = getWatcher(key, m.effects[key], m, plugin.get('onEffect'))
yield sagaEffects.fork(watcher) //用fork不會阻塞
}
})
}
return sagas
}
function getWatcher(key, effect, model, onEffect) {
function put(action) {
return sagaEffects.put({ ...action, type: prefixType(action.type, model) })
}
return function* () {
yield sagaEffects.takeEvery(key, function* (action) {//對action進行監控,調用下面這個saga
if (onEffect) {
for (const fn of onEffect) {//oneffect是數組
effect = fn(effect, { ...sagaEffects, put }, model, key)
}
}
yield effect(action, { ...sagaEffects, put })
})
}
}
- 監聽watcher的時候對每一個要執行的workerSaga包裹起來,傳遞過去。
- 這樣dva-loading的所有功能都完成了。