如何优雅地处理loading
在业务代码中,为了判断异步操作是否在运行中,往往通过额外的标志量来区分;本文将从dva-loading
源码出发,分析在React项目中如何统一管理loading状态,然后给出一种在Vue项目中管理loading的解决方案。
参考
- vue-wait,在vue中管理页面上的多个加载状态
- dva-loading,dva内置的加载状态插件
- dva-loading 实践用法
业务场景
在业务代码中,我们往往通过一个额外的标志量来判断是否需要展示加载状态
function fetchList(){
if(this.isFetchList) return // 避免重复请求
this.isFetchList = true
fetchListApi().then((res)=>{
// do res
}).finally(()=>{
this.isFetchList = false // 重置状态
})
}
当需要判断的操作过多时,需要花费大量的精力来维护这些状态。
在dva中可以使用dva-loading来管理对应的action
,然后通过注入的loading.effects[actionType]
可以很方便地获取某个action是否在运行中,下面将分析dva-loading
的实现原理。
dav-loading源码分析
在package/dva-loading
目录下可以看见dva-loading
插件的源码。
在之前的文章:dva源码分析中介绍了dva中的插件系统,这里我们来研究一下dva-loading
插件的实现原理
const SHOW = '@@DVA_LOADING/SHOW';
const HIDE = '@@DVA_LOADING/HIDE';
const NAMESPACE = 'loading';
function createLoading(opts = {}) {
const namespace = opts.namespace || NAMESPACE;
const { only = [], except = [] } = opts;
//只能配置only或except中的一个
if (only.length > 0 && except.length > 0) {
throw Error('It is ambiguous to configurate `only` and `except` items at the same time.');
}
const initialState = {
global: false,
models: {},
effects: {},
};
const extraReducers = {
[namespace](state = initialState, { type, payload }) {
const { namespace, actionType } = payload || {};
let ret;
switch (type) {
// 将actionType对应的loading状态设置为true,在页面上通过loading.effects[actionType]获取对应的effect loading状态
case SHOW:
ret = {
...state,
global: true,
models: { ...state.models, [namespace]: true },
effects: { ...state.effects, [actionType]: true },
};
break;
// 将actionType对应的loading状态设置为false
case HIDE: {
const effects = { ...state.effects, [actionType]: false };
// 判断还存在effect loading为true的model
const models = {
...state.models,
[namespace]: Object.keys(effects).some(actionType => {
const _namespace = actionType.split('/')[0];
if (_namespace !== namespace) return false;
return effects[actionType];
}),
};
const global = Object.keys(models).some(namespace => {
return models[namespace];
});
ret = {
...state,
global,
models,
effects,
};
break;
}
default:
ret = state;
break;
}
return ret;
},
};
function onEffect(effect, { put }, model, actionType) {
const { namespace } = model;
// 获取配置项中的only和except,判断actionType是否需要添加loading
if (
(only.length === 0 && except.length === 0) ||
(only.length > 0 && only.indexOf(actionType) !== -1) ||
(except.length > 0 && except.indexOf(actionType) === -1)
) {
return function*(...args) {
// 在执行effect之前将actionType对应的loading设置为true
yield put({ type: SHOW, payload: { namespace, actionType } });
yield effect(...args);
// 在执行effect之后将actionType对应的loading设置为false
yield put({ type: HIDE, payload: { namespace, actionType } });
};
} else {
return effect;
}
}
// dva插件格式,返回包含特定key的一个对象
return {
extraReducers, // 在创建store时调用
onEffect,
};
}
export default createLoading;
可以看见dva-loading
主要实现了两个钩子
- 在
extraReducers
中注册了一个处理loading的reducer,根据action.type
区分是显示还是隐藏,根据action.payload.actionType
判断当前正在进行loading状态切换的action onEffect
注册的方法会在dva.start
时收集到saga中,最后在sagaMiddleware.run
时,首先执行put({type:SHOW})
将loading状态置为true,然后再执行原本的effect,最后执行put({type:HIDE})
将loading状态置为false
实现vue-loading
参考dva-loading
,我实现了一个vue-loading,用于管理vuex中异步action的执行状态。
大致实现过程为,通过vuex插件劫持store._actions
,并在action执行前后修改内部loading状态,整个过程分为两部分
内置loading模块
这个模块用来保存所有action的loading状态,在视图中可以通过store.state.loading[actionName]
的方式获取执行状态。
// loading模块
const createLoadingModule = () => {
return {
namespaced: true,
state: {},
mutations: {
[LOADING_START](state, key) {
Vue.set(state, key, true) // 动态添加state
},
[LOADING_END](state, key) {
Vue.set(state, key, false)
}
},
}
}
劫持所有action
在注册插件时劫持所有的action,在调用前后提交loading状态
const defaultConfig = {
// loading模块的名称,使用方式:this.$store.state[loadingModuleName]
loadingModuleName: LOADING_NAME,
// 当前触发的action名字,context与payload同原始的dispatch参数
// 在当前action执行前调用
before(key, context, payload) {},
// 在当前action执行后调用
after() {}
}
const createVuexLoading = (config) => (store) => {
config = Object.assign(defaultConfig, config)
// 注册loadingModule
const loadingModule = createLoadingModule()
const {loadingModuleName} = config
store.registerModule(loadingModuleName, loadingModule)
// 开始和结束的钩子
const hooks = {
before(key, context, payload) {
config.before(key, context, payload)
store.commit(`${loadingModuleName}/${LOADING_START}`, key)
},
after(key, context, payload) {
store.commit(`${loadingModuleName}/${LOADING_END}`, key)
config.after(key, context, payload)
}
}
// 劫持所有的action,需要注意只有返回promise的action会正常触发loading监听
const {_actions} = store
Object.keys(_actions).forEach(key => {
_actions[key] = _actions[key].map(action => {
return async (context, payload) => {
hooks.before(key, context, payload) // commit loading start
try {
await action(context, payload)
} catch (e) {
console.log(e)
} finally {
hooks.after(key, context, payload) // commit loading end
}
}
})
})
}
此外,我们也可以效仿dva-loading
,在config中增加对于only
和except
配置的支持,只监听或排除某些特定的actionType。完整代码已放在github上。
小结
本文从应用场景出发,分析了dva-loading
的实现原理,然后给出了Vue项目中一种自动更新action的loading状态的方案,两种方案的实现原理基本类似
- 通过插件实现一个内置的loading模块,用于获取每个action的loading状态
- 在action执行前将actionType对应的loading更新为true,在action执行后将actionType对应的loading更新为false
dva-loading
基于redux-saga
,通过获取的effect,重新实现一个迭代器函数,并在effect前后执行对应loading state逻辑vue-loading
通过store._actions
获取并劫持所有的action,在原始action执行前后执行对应loading state更新逻辑
当然,上述方案的使用依赖于使用全局状态管理的整个应用,对于局部组件的state,由于对应的状态更新并不会经过统一的store处理,因此无法统一监听并修改loading状态。这样可以算作是使用全局状态的一个优点吧...
你要请我喝一杯奶茶?
版权声明:自由转载-非商用-保持署名和原文链接。
本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。