在H5的Vue項目中,最爲常見的當爲單頁應用(SPA),利用Vue-Router
控制組件的掛載與複用,這時使用Vuex能夠方便的維護數據狀態而沒必要關心組件間的數據通訊。但在Weex中,不一樣的頁面之間使用不一樣的執行環境,沒法共享數據,此時多爲經過BroadcastChannel
或storage
模塊來實現數據通訊,本文主要使用修飾器(Decorator)來擴展Vuex的功能,實現分模塊存儲數據,並下降與業務代碼的耦合度。webpack
設計模式中有一種裝飾器模式,能夠在運行時擴展對象的功能,而無需建立多個繼承對象。相似的,Decorator能夠在編譯時擴展一個對象的功能,下降代碼耦合度的同時實現多繼承同樣的效果。web
目前Decorator還只是一個提案,在生產環境中沒法直接使用,能夠用babel-plugin-transform-decorators-legacy
來實現。使用npm管理依賴包的能夠執行如下命令:npm
npm install babel-plugin-transform-decorators-legacy -D
複製代碼
而後在 .babelrc 中配置設計模式
{
"plugins": [
"transform-decorators-legacy"
]
}
複製代碼
或者在webpack.config.js中配置promise
{
test: /\.js$/,
loader: "babel-loader",
options: [
plugins: [
require("babel-plugin-transform-decorators-legacy").default
]
]
}
複製代碼
這時能夠在代碼裏編寫Decorator函數了。bash
在本文中,Decorator主要是對方法進行修飾,主要代碼以下:babel
decorator.jsweex
const actionDecorator = (target, name, descriptor) => {
const fn = descriptor.value;
descriptor.value = function(...args) {
console.log('調用了修飾器的方法');
return fn.apply(this, args);
};
return descriptor;
};
複製代碼
store.jsapp
const module = {
state: () => ({}),
actions: {
@actionDecorator
someAction() {/** 業務代碼 **/ },
},
};
複製代碼
能夠看到,actionDecorator
修飾器的三個入參和Object.defineProperty
同樣,經過對module.actions.someAction
函數的修飾,實如今編譯時重寫someAction
方法,在調用方法時,會先執行console.log('調用了修飾器的方法');
,然後再調用方法裏的業務代碼。對於多個功能的實現,好比存儲數據,發送廣播,打印日誌和數據埋點,增長多個Decorator便可。異步
Vuex自己能夠用subscribe
和subscribeAction
訂閱相應的mutation
和action
,但只支持同步執行,而Weex的storage
存儲是異步操做,所以須要對Vuex的現有方法進行擴展,以知足相應的需求。
在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';
// 存放commit和module鍵值對的WeakMap對象
import {moduleWeakMap} from './plugin';
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 => {
const {module, moduleKey} = moduleWeakMap.get(commit) || {};
if (module) {
const {_children} = module;
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(moduleKey, 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() {/** 業務代碼 **/ },
},
};
複製代碼
完成了存儲數據到storage
之後,咱們還須要在新開的Weex頁面實例能自動讀取數據並初始化Vuex的狀態。在這裏,咱們使用Vuex的plugins
設置來完成這個功能。
首先咱們先編寫Vuex的plugin
:
plugin.js
import storage from './storage';
// 加個rootKey,防止rootState的namespace爲''而致使報錯
// 可自行替換爲其餘字符串
import {rootKey} from './constant';
const parseJSON = (str) => {
try {
return str ? JSON.parse(str) : undefined;
} catch(e) {}
return undefined;
};
export const moduleWeakMap = new WeakMap();
const getState = (store) => {
const getStateData = async function getModuleState(module, path = []) {
const moduleKey = `${path.join('/')}/`;
const {_children, context} = module;
const {commit} = context || {};
// 初始化store時將commit和module存入WeakMap,以便setState時快速查找對應module
moduleWeakMap.set(commit, {module, moduleKey});
// 根據path讀取當前module下存儲在storage裏的數據
const data = parseJSON(await storage.getItem(moduleKey)) || {};
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
狀態。
經過Decorator實現了Vuex的數據分模塊存儲到storage
,並在Store
實例化時經過plugin
分模塊讀取數據再merge
到state
,提升數據存儲效率的同時實現與業務邏輯代碼的解耦。但還存在一些可優化的點:
action
將會存儲全部module
中的state
,只需保存必要狀態,避免無用數據。registerModule
註冊的module
,需支持自動讀取本地數據。在此再也不展開,將在後續版本中實現。