利用Dectorator分模組儲存Vuex狀態
在H5的Vue專案中,最為常見的當為單頁應用(SPA),利用Vue-Router
控制組件的掛載與複用,這時使用Vuex可以方便的維護資料狀態而不必關心元件間的資料通訊。但在Weex中,不同的頁面之間使用不同的執行環境,無法共享資料,此時多為通過BroadcastChannel
或storage
模組來實現資料通訊,本文主要使用修飾器(Decorator)來擴充套件Vuex的功能,實現分模組儲存資料,並降低與業務程式碼的耦合度。
2、Decorator
設計模式中有一種裝飾器模式,可以在執行時擴充套件物件的功能,而無需建立多個繼承物件。類似的,Decorator可以在編譯時擴充套件一個物件的功能,降低程式碼耦合度的同時實現多繼承一樣的效果。
2.1、Decorator安裝
目前Decorator還只是一個提案,在生產環境中無法直接使用,可以用babel-plugin-transform-decorators-legacy
來實現。使用npm管理依賴包的可以執行以下命令:
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 ] ] } 複製程式碼
這時可以在程式碼裡編寫Decorator函數了。
2.2、Decorator的編寫
在本文中,Decorator主要是對方法進行修飾,主要程式碼如下:
decorator.js
const actionDecorator = (target, name, descriptor) => { const fn = descriptor.value; descriptor.value = function(...args) { console.log('呼叫了修飾器的方法'); return fn.apply(this, args); }; return descriptor; }; 複製程式碼
store.js
const module = { state: () => ({}), actions: { @actionDecorator someAction() {/** 業務程式碼 **/ }, }, }; 複製程式碼
可以看到,actionDecorator
修飾器的三個入參和Object.defineProperty
一樣,通過對module.actions.someAction
函式的修飾,實現在編譯時重寫someAction
方法,在呼叫方法時,會先執行console.log('呼叫了修飾器的方法');
,而後再呼叫方法裡的業務程式碼。對於多個功能的實現,比如儲存資料,傳送廣播,列印日誌和資料埋點,增加多個Decorator即可。
3、Vuex
Vuex本身可以用subscribe
和subscribeAction
訂閱相應的mutation
和action
,但只支援同步執行,而Weex的storage
儲存是非同步操作,因此需要對Vuex的現有方法進行擴充套件,以滿足相應的需求。
3.1、修飾action
在Vuex裡,可以通過commit mutation
或者dispatch action
來更改state
,而action
本質是呼叫commit mutation
。因為storage
包含非同步操作,在不破壞Vuex程式碼規範的前提下,我們選擇修飾action來擴充套件功能。
storage
使用回撥函式來讀寫item
,首先我們將其封裝成Promise
結構:
storage.js
const storage = weex.requireModule('storage'); const handler = { get: function(target, prop) { const fn = target[prop]; // 這裡只需要用到這兩個方法 if ([ 'getItem', 'setItem' ].some(method => method === prop)) { return function(...args) { // 去掉回撥函式,返回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無儲存state而出現報錯 return resolve(result); }) }) } } return fn; }, }; export default new Proxy(storage, handler); 複製程式碼
通過Proxy
,將setItem
和getItem
封裝為promise
物件,後續使用時可以避免過多的回撥結構。
現在我們把storage
的setItem
方法寫入到修飾器:
decorator.js
import storage from './storage'; // 加個rootKey,防止rootState的namespace為''而導致報錯 // 可自行替換為其他字串 import {rootKey} from './constant'; const setState = (target, name, descriptor) => { const fn = descriptor.value; descriptor.value = function(...args) { const [{state, commit}] = args; // action為非同步操作,返回promise, // 且需在狀態修改為fulfilled時再將state儲存到storage return fn.apply(this, args).then(async data => { // 獲取store的moduleMap const rawModule = Object.entries(this._modulesNamespaceMap); // 根據當前的commit,查詢此action所在的module const moduleMap = rawModule.find(([, module]) => { return module.context.commit === commit; }); if (moduleMap) { const [key, {_children}] = moduleMap; const childrenKeys = Object.keys(_children); // 只獲取當前module的state,childModule的state交由其儲存,按module儲存資料,避免儲存資料過大 // Object.fromEntries可使用object.fromentries來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
狀態修改為fulfilled
後呼叫storage
的儲存功能,及時儲存資料狀態以便在新開Weex頁面載入最新資料。
store.js
import setState from './decorator'; const module = { state: () => ({}), actions: { @setState someAction() {/** 業務程式碼 **/ }, }, }; 複製程式碼
3.2、讀取module資料
完成了儲存資料到storage
以後,我們還需要在新開的Weex頁面例項能自動讀取資料並初始化Vuex的狀態。在這裡,我們使用Vuex的plugins
設定來完成這個功能。
首先我們先編寫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; // 根據path讀取當前module下儲存在storage裡的資料 const data = parseJSON(await storage.getItem(`${path.join('/')}/`)) || {}; const children = Object.entries(_children); if (!children.length) { return data; } // 剔除childModule的資料,遞迴讀取 const childModules = await Promise.all( children.map(async ([childKey, child]) => { return [childKey, await getModuleState(child, path.concat(childKey))]; }) ); return { ...data, ...Object.fromEntries(childModules), } }; // 讀取本地資料,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的資料按照module
讀取,但Weex的IOS/Andriod中的storage
儲存是非同步的,為防止元件掛載以後傳送請求返回的資料被本地資料覆蓋,需要在本地資料讀取並merge
到state
以後再呼叫new Vue
,這裡我們使用一個簡易的interceptor
來攔截:
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() {/** 業務程式碼 **/ }, }, plugins: [getState], modules: { /** children module**/ } }; 複製程式碼
app.js
import {runInterceptor} from './interceptor'; // 待攔截器內所有promise返回resolved後再例項化Vue根元件 // 也可以用Vue-Router的全域性守衛來完成 runInterceptor('start').then(() => { new Vue({/** other code **/}); }); 複製程式碼
這樣就實現了Weex頁面例項化後,先讀取storage
資料到Vuex的state
,再例項化各個Vue的元件,更新各自的module
狀態。
4、TODO
通過Decorator實現了Vuex的資料分模組儲存到storage
,並在Store
例項化時通過plugin
分模組讀取資料再merge
到state
,提高資料儲存效率的同時實現與業務邏輯程式碼的解耦。但還存在一些可優化的點:
1、觸發action會將所有module
中的所有state
全部,只需儲存所需狀態,避免儲存無用資料。
2、對於通過registerModule
註冊的module
,需支援自動讀取本地資料。
3、無法通過_modulesNamespaceMap
獲取namespaced
為false
的module
,需改為遍歷_children
。
在此不再展開,將在後續版本中實現。