前言
- 很多东西看起来复杂,实际弄一遍感觉简单,主要还是自己懒。
- 接上篇
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的所有功能都完成了。