【dva】dva使用與實現(二)

前言

  • 很多東西看起來複雜,實際弄一遍感覺簡單,主要還是自己懶。
  • 接上篇

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的所有功能都完成了。
發佈了153 篇原創文章 · 獲贊 8 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章