利用Dectorator分模組儲存Vuex狀態(上)

coffee-ai發表於2019-02-04

1、引言

在H5的Vue專案中,最為常見的當為單頁應用(SPA),利用Vue-Router控制元件的掛載與複用,這時使用Vuex可以方便的維護資料狀態而不必關心元件間的資料通訊。但在Weex中,不同的頁面之間使用不同的執行環境,無法共享資料,此時多為通過BroadcastChannelstorage模組來實現資料通訊,本文主要使用修飾器(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本身可以用subscribesubscribeAction訂閱相應的mutationaction,但只支援同步執行,而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,將setItemgetItem封裝為promise物件,後續使用時可以避免過多的回撥結構。

現在我們把storagesetItem方法寫入到修飾器:

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() {/** 業務程式碼 **/ },
    },
};
複製程式碼

3.2、讀取module資料

完成了儲存資料到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儲存是非同步的,為防止元件掛載以後傳送請求返回的資料被本地資料覆蓋,需要在本地資料讀取並mergestate以後再呼叫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分模組讀取資料再mergestate,提高資料儲存效率的同時實現與業務邏輯程式碼的解耦。但還存在一些可優化的點:

1、觸發action將會儲存所有module中的state,只需儲存必要狀態,避免無用資料。

2、對於通過registerModule註冊的module,需支援自動讀取本地資料。

在此不再展開,將在後續版本中實現。

相關文章