前言
在項目中,如何管理loading
是一個很頭疼的方式,有的是在請求封裝裏去做了一個全屏loading
,有的是在單頁面中管理一個loading
,比如如下:
data(){
return{
loading:false
}
},
methods:{
async change(){
this.loading = true
// 執行異步操作
await asyncAction()
this.loading = false
}
}
如果管理的是一個還好,但是如果頁面上存在着多個異步操作,維護起來簡直要命。
之前有使用過dva
這個基於react-saga
的數據流框架,他有一個插件dva-loading
非常的好用,但由於是React
框架上的。一直想着如何借鑑其思路實現一個基於vuex
的loading
插件,是非常想做的一件事。
好在,[email protected]
中新增了一個subscribeAction
這個方法。
store.subscribeAction({
before: (action, state) => {
console.log(`before action ${action.type}`)
},
after: (action, state) => {
console.log(`after action ${action.type}`)
}
})
簡單來說就是我們在發起的每一個action
都可以在這個方法中訂閱到,before
和after
兩個過程。心裏一想,這豈不是跟dva-loading
有一樣的一個動作嗎。
如何實現
先看一下dva-loading
的源碼,分析一下:
爲了方便描述,直接貼源碼補充註釋,具體dva
的使用與vuex
的異同,可以看我之前寫的那篇如何使用dva與服務端進行數據交互
const SHOW = '@@DVA_LOADING/SHOW'; // 定義reducer 類似於mutation
const HIDE = '@@DVA_LOADING/HIDE';
const NAMESPACE = 'loading'; // 定義命名空間
function createLoading(opts = {}) {
// 獲取參數中的命名空間或者本地的名稱
const namespace = opts.namespace || NAMESPACE;
const { only = [], except = [] } = opts;
if (only.length > 0 && except.length > 0) {
throw Error('It is ambiguous to configurate `only` and `except` items at the same time.');
}
// 初始化state
const initialState = {
global: false, // 定義全局 loading
models: {}, // 定義模塊
effects: {}, // 定義effect 異步操作
};
const extraReducers = {
[namespace](state = initialState, { type, payload }) {
const { namespace, actionType } = payload || {};
let ret;
switch (type) {
// 如果是顯示的話 將全局loading置爲true
// 並且將本model中所有的loading都置爲 true
case SHOW:
ret = {
...state,
global: true,
models: { ...state.models, [namespace]: true },
effects: { ...state.effects, [actionType]: true },
};
break;
// 如果是顯示的話 將本模塊的都置爲false
case HIDE: {
const effects = { ...state.effects, [actionType]: false };
// 遍歷操作將所有的都置爲false
const models = {
...state.models,
[namespace]: Object.keys(effects).some(actionType => {
const _namespace = actionType.split('/')[0];
if (_namespace !== namespace) return false;
return effects[actionType];
}),
};
// 遍歷所有的model 將所有命名空間中的都置爲false
const global = Object.keys(models).some(namespace => {
return models[namespace];
});
ret = {
...state,
global,
models,
effects,
};
break;
}
default:
ret = state;
break;
}
// 最後返回到state中 成功修改了store state中的數據
return ret;
},
};
// 發起異步操作
function onEffect(effect, { put }, model, actionType) {
const { namespace } = model;
if (
(only.length === 0 && except.length === 0) ||
(only.length > 0 && only.indexOf(actionType) !== -1) ||
(except.length > 0 && except.indexOf(actionType) === -1)
) {
return function*(...args) {
// 用了generator做異步處理
// 發起一個同步操作 傳入命名空間與異步操作type
yield put({ type: SHOW, payload: { namespace, actionType } });
// 具體執行的異步操作
yield effect(...args);
yield put({ type: HIDE, payload: { namespace, actionType } });
};
} else {
return effect;
}
}
return {
extraReducers,
onEffect,
};
}
export default createLoading;
模仿dva-loading
開始動手實現
const NAMESPACE = "loading"; // 定義模塊名
const SHOW = "@@LOADING/SHOW" // 顯示mutation 同步type
const HIDE = "@@LOADING/HIDE"
const createLoadingPlugin = ({
namespace = NAMESPACE,
includes = [],
excludes = []
} = {}) => {
return store => {
if (store.state[namespace]) {
throw new Error(
`createLoadingPlugin: ${namespace} exited in current store`
);
}
// new vuex的時候註冊一個模塊進去
store.registerModule(namespace, {
namespaced: true,
state: {
global: false, // 定義全局loading
effects: {}
},
// 同步方法
mutations: {
SHOW(state, { payload }) {
state.global = true;
state.effects = {
...state.effects,
[payload]: true // 將當前的action 置爲true
};
},
HIDE(state, { payload }) {
state.global = false;
state.effects = {
...state.effects,
[payload]: false // 將當前的action 置爲false
};
}
}
});
store.subscribeAction({
// 發起一個action 之前會走這裏
before: action => {
console.log(`before action ${action.type}`);
if (onEffect(action, includes, excludes)) {
store.commit({ type: SHOW, payload: action.type });
}
},
// 發起一個action 之後會走這裏
after: action => {
console.log(`after action ${action.type}`);
if (onEffect(action, includes, excludes)) {
store.commit({ type: HIDE, payload: action.type });
}
}
});
};
};
// 判斷是否要執行
function onEffect({ type }, includes, excludes) {
if (includes.length === 0 && excludes.length === 0) {
return true;
}
if (includes.length > 0) {
return includes.indexOf(type) > -1;
}
return excludes.length > 0 && excludes.indexOf(type) === -1;
}
export default createLoadingPlugin;
使用
- 在dva中是這樣使用的
@connect(({loading})=>({
loading:loading.effects['user/addAction']
}))
- vuex中是這樣使用的
import { mapState } from "vuex";
computed: {
...mapState({
loading: state => state["@@LOADING"].effects["user/addAction"]
})
},
兩者相同的都是在effects[]
這個數組中取key
爲我們要執行的那個異步操作的type
這樣就能獲取到他的loading
狀態
因爲在兩者的全局loading
模塊中,有一個effects
對象數組,結構類似於這樣
state = {
effects:[
{'user/addAction':true},
{'user/updateAction':false},
]
}
好了這下可以愉快的在頁面中使用loading
了。對了差點忘記,還要去註冊插件
import createLoadingPlugin from "@/utils/vuex-loading";
export default new Vuex.Store({
plugins: [createLoadingPlugin()],
state: {},
mutations: {},
actions: {},
modules
});
特別注意的是,因爲是基於subscribeAction
這個方法實現的,所以需要將vuex
版本升級至3.1.0+
關於
- 借鑑dva-loading
- 本文首發於實現一個vuex-loading插件