利用Dectorator分模塊存儲(chǔ)Vuex狀態(tài)的實(shí)現(xiàn)
1、引言
在H5的Vue項(xiàng)目中,最為常見(jiàn)的當(dāng)為單頁(yè)應(yīng)用(SPA),利用Vue-Router控制組件的掛載與復(fù)用,這時(shí)使用Vuex可以方便的維護(hù)數(shù)據(jù)狀態(tài)而不必關(guān)心組件間的數(shù)據(jù)通信。但在Weex中,不同的頁(yè)面之間使用不同的執(zhí)行環(huán)境,無(wú)法共享數(shù)據(jù),此時(shí)多為通過(guò)BroadcastChannel或storage模塊來(lái)實(shí)現(xiàn)數(shù)據(jù)通信,本文主要使用修飾器(Decorator)來(lái)擴(kuò)展Vuex的功能,實(shí)現(xiàn)分模塊存儲(chǔ)數(shù)據(jù),并降低與業(yè)務(wù)代碼的耦合度。
2、Decorator
設(shè)計(jì)模式中有一種裝飾器模式,可以在運(yùn)行時(shí)擴(kuò)展對(duì)象的功能,而無(wú)需創(chuàng)建多個(gè)繼承對(duì)象。類(lèi)似的,Decorator可以在編譯時(shí)擴(kuò)展一個(gè)對(duì)象的功能,降低代碼耦合度的同時(shí)實(shí)現(xiàn)多繼承一樣的效果。
2.1、Decorator安裝
目前Decorator還只是一個(gè)提案,在生產(chǎn)環(huán)境中無(wú)法直接使用,可以用babel-plugin-transform-decorators-legacy來(lái)實(shí)現(xiàn)。使用npm管理依賴(lài)包的可以執(zhí)行以下命令:
npm install babel-plugin-transform-decorators-legacy -D
然后在 .babelrc 中配置
{
"plugins": [
"transform-decorators-legacy"
]
}
或者在webpack.config.js中配置
{
test: /\.js$/,
loader: "babel-loader",
options: [
plugins: [
require("babel-plugin-transform-decorators-legacy").default
]
]
}
這時(shí)可以在代碼里編寫(xiě)Decorator函數(shù)了。
2.2、Decorator的編寫(xiě)
在本文中,Decorator主要是對(duì)方法進(jìn)行修飾,主要代碼如下:
decorator.js
const actionDecorator = (target, name, descriptor) => {
const fn = descriptor.value;
descriptor.value = function(...args) {
console.log('調(diào)用了修飾器的方法');
return fn.apply(this, args);
};
return descriptor;
};
store.js
const module = {
state: () => ({}),
actions: {
@actionDecorator
someAction() {/** 業(yè)務(wù)代碼 **/ },
},
};
可以看到,actionDecorator修飾器的三個(gè)入?yún)⒑蚈bject.defineProperty一樣,通過(guò)對(duì)module.actions.someAction函數(shù)的修飾,實(shí)現(xiàn)在編譯時(shí)重寫(xiě)someAction方法,在調(diào)用方法時(shí),會(huì)先執(zhí)行console.log('調(diào)用了修飾器的方法');,而后再調(diào)用方法里的業(yè)務(wù)代碼。對(duì)于多個(gè)功能的實(shí)現(xiàn),比如存儲(chǔ)數(shù)據(jù),發(fā)送廣播,打印日志和數(shù)據(jù)埋點(diǎn),增加多個(gè)Decorator即可。
3、Vuex
Vuex本身可以用subscribe和subscribeAction訂閱相應(yīng)的mutation和action,但只支持同步執(zhí)行,而Weex的storage存儲(chǔ)是異步操作,因此需要對(duì)Vuex的現(xiàn)有方法進(jìn)行擴(kuò)展,以滿(mǎn)足相應(yīng)的需求。
3.1、修飾action
在Vuex里,可以通過(guò)commit mutation或者dispatch action來(lái)更改state,而action本質(zhì)是調(diào)用commit mutation。因?yàn)閟torage包含異步操作,在不破壞Vuex代碼規(guī)范的前提下,我們選擇修飾action來(lái)擴(kuò)展功能。
storage使用回調(diào)函數(shù)來(lái)讀寫(xiě)item,首先我們將其封裝成Promise結(jié)構(gòu):
storage.js
const storage = weex.requireModule('storage');
const handler = {
get: function(target, prop) {
const fn = target[prop];
// 這里只需要用到這兩個(gè)方法
if ([
'getItem',
'setItem'
].some(method => method === prop)) {
return function(...args) {
// 去掉回調(diào)函數(shù),返回promise
const [callback] = args.slice(-1);
const innerArgs = typeof callback === 'function' ? args.slice(0, -1) : args;
return new Promise((resolve, reject) => {
fn.call(target, ...innerArgs, ({result, data}) => {
if (result === 'success') {
return resolve(data);
}
// 防止module無(wú)保存state而出現(xiàn)報(bào)錯(cuò)
return resolve(result);
})
})
}
}
return fn;
},
};
export default new Proxy(storage, handler);
通過(guò)Proxy,將setItem和getItem封裝為promise對(duì)象,后續(xù)使用時(shí)可以避免過(guò)多的回調(diào)結(jié)構(gòu)。
現(xiàn)在我們把storage的setItem方法寫(xiě)入到修飾器:
decorator.js
import storage from './storage';
// 加個(gè)rootKey,防止rootState的namespace為''而導(dǎo)致報(bào)錯(cuò)
// 可自行替換為其他字符串
import {rootKey} from './constant';
const setState = (target, name, descriptor) => {
const fn = descriptor.value;
descriptor.value = function(...args) {
const [{state, commit}] = args;
// action為異步操作,返回promise,
// 且需在狀態(tài)修改為fulfilled時(shí)再將state存儲(chǔ)到storage
return fn.apply(this, args).then(async data => {
// 獲取store的moduleMap
const rawModule = Object.entries(this._modulesNamespaceMap);
// 根據(jù)當(dāng)前的commit,查找此action所在的module
const moduleMap = rawModule.find(([, module]) => {
return module.context.commit === commit;
});
if (moduleMap) {
const [key, {_children}] = moduleMap;
const childrenKeys = Object.keys(_children);
// 只獲取當(dāng)前module的state,childModule的state交由其存儲(chǔ),按module存儲(chǔ)數(shù)據(jù),避免存儲(chǔ)數(shù)據(jù)過(guò)大
// Object.fromEntries可使用object.fromentries來(lái)polyfill,或可用reduce替代
const pureState = Object.fromEntries(Object.entries(state).filter(([stateKey]) => {
return !childrenKeys.some(childKey => childKey === stateKey);
}));
await storage.setItem(rootKey + key, JSON.stringify(pureState));
}
// 將data沿著promise鏈向后傳遞
return data;
});
};
return descriptor;
};
export default setState;
完成了setState修飾器功能以后,就可以裝飾action方法了,這樣等action返回的promise狀態(tài)修改為fulfilled后調(diào)用storage的存儲(chǔ)功能,及時(shí)保存數(shù)據(jù)狀態(tài)以便在新開(kāi)Weex頁(yè)面加載最新數(shù)據(jù)。
store.js
import setState from './decorator';
const module = {
state: () => ({}),
actions: {
@setState
someAction() {/** 業(yè)務(wù)代碼 **/ },
},
};
3.2、讀取module數(shù)據(jù)
完成了存儲(chǔ)數(shù)據(jù)到storage以后,我們還需要在新開(kāi)的Weex頁(yè)面實(shí)例能自動(dòng)讀取數(shù)據(jù)并初始化Vuex的狀態(tài)。在這里,我們使用Vuex的plugins設(shè)置來(lái)完成這個(gè)功能。
首先我們先編寫(xiě)Vuex的plugin:
plugin.js
import storage from './storage';
import {rootKey} from './constant';
const parseJSON = (str) => {
try {
return str ? JSON.parse(str) : undefined;
} catch(e) {}
return undefined;
};
const getState = (store) => {
const getStateData = async function getModuleState(module, path = []) {
const {_children} = module;
// 根據(jù)path讀取當(dāng)前module下存儲(chǔ)在storage里的數(shù)據(jù)
const data = parseJSON(await storage.getItem(`${path.join('/')}/`)) || {};
const children = Object.entries(_children);
if (!children.length) {
return data;
}
// 剔除childModule的數(shù)據(jù),遞歸讀取
const childModules = await Promise.all(
children.map(async ([childKey, child]) => {
return [childKey, await getModuleState(child, path.concat(childKey))];
})
);
return {
...data,
...Object.fromEntries(childModules),
}
};
// 讀取本地?cái)?shù)據(jù),merge到Vuex的state
const init = getStateData(store._modules.root, [rootKey]).then(savedState => {
store.replaceState(merge(store.state, savedState, {
arrayMerge: function (store, saved) { return saved },
clone: false,
}));
});
};
export default getState;
以上就完成了Vuex的數(shù)據(jù)按照module讀取,但Weex的IOS/Andriod中的storage存儲(chǔ)是異步的,為防止組件掛載以后發(fā)送請(qǐng)求返回的數(shù)據(jù)被本地?cái)?shù)據(jù)覆蓋,需要在本地?cái)?shù)據(jù)讀取并merge到state以后再調(diào)用new Vue,這里我們使用一個(gè)簡(jiǎn)易的interceptor來(lái)攔截:
interceptor.js
const interceptors = {};
export const registerInterceptor = (type, fn) => {
const interceptor = interceptors[type] || (interceptors[type] = []);
interceptor.push(fn);
};
export const runInterceptor = async (type) => {
const task = interceptors[type] || [];
return Promise.all(task);
};
這樣plugin.js中的getState就修改為:
import {registerInterceptor} from './interceptor';
const getState = (store) => {
/** other code **/
const init = getStateData(store._modules.root, []).then(savedState => {
store.replaceState(merge(store.state, savedState, {
arrayMerge: function (store, saved) { return saved },
clone: false,
}));
});
// 將promise放入攔截器
registerInterceptor('start', init);
};
store.js
import getState from './plugin';
import setState from './decorator';
const rootModule = {
state: {},
actions: {
@setState
someAction() {/** 業(yè)務(wù)代碼 **/ },
},
plugins: [getState],
modules: {
/** children module**/
}
};
app.js
import {runInterceptor} from './interceptor';
// 待攔截器內(nèi)所有promise返回resolved后再實(shí)例化Vue根組件
// 也可以用Vue-Router的全局守衛(wèi)來(lái)完成
runInterceptor('start').then(() => {
new Vue({/** other code **/});
});
這樣就實(shí)現(xiàn)了Weex頁(yè)面實(shí)例化后,先讀取storage數(shù)據(jù)到Vuex的state,再實(shí)例化各個(gè)Vue的組件,更新各自的module狀態(tài)。
4、TODO
通過(guò)Decorator實(shí)現(xiàn)了Vuex的數(shù)據(jù)分模塊存儲(chǔ)到storage,并在Store實(shí)例化時(shí)通過(guò)plugin分模塊讀取數(shù)據(jù)再merge到state,提高數(shù)據(jù)存儲(chǔ)效率的同時(shí)實(shí)現(xiàn)與業(yè)務(wù)邏輯代碼的解耦。但還存在一些可優(yōu)化的點(diǎn):
1、觸發(fā)action會(huì)將所有module中的所有state全部,只需保存所需狀態(tài),避免存儲(chǔ)無(wú)用數(shù)據(jù)。
2、對(duì)于通過(guò)registerModule注冊(cè)的module,需支持自動(dòng)讀取本地?cái)?shù)據(jù)。
3、無(wú)法通過(guò)_modulesNamespaceMap獲取namespaced為false的module,需改為遍歷_children。
在此不再展開(kāi),將在后續(xù)版本中實(shí)現(xiàn)。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Vue中如何合并el-table第一列相同數(shù)據(jù)
這篇文章主要介紹了Vue中如何合并el-table第一列相同數(shù)據(jù)問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03
解決vue-router路由攔截造成死循環(huán)問(wèn)題
這篇文章主要介紹了解決vue-router路由攔截造成死循環(huán)問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-08-08
vue+element+springboot實(shí)現(xiàn)文件下載進(jìn)度條展現(xiàn)功能示例
本文主要介紹了vue + element-ui + springboot 實(shí)現(xiàn)文件下載進(jìn)度條展現(xiàn)功能,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-11-11
vue2.0/3.0中provide和inject的用法示例
provide和inject是成對(duì)出現(xiàn)的,主要用于父組件向子孫組件傳遞數(shù)據(jù),這篇文章主要給大家介紹了關(guān)于vue2.0/3.0中provide和inject用法的相關(guān)資料,需要的朋友可以參考下2021-09-09
vue?LogicFlow更多配置選項(xiàng)示例詳解
這篇文章主要為大家介紹了vue?LogicFlow更多配置選項(xiàng)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01

