在 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.
在此不再展开, 将在后续版本中实现.
来源: https://juejin.im/post/5c5400dbf265da2dc972d079