利用Dectorator分模块存储Vuex状态的实现


Posted in Javascript onFebruary 05, 2019

1、引言

在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。

在此不再展开,将在后续版本中实现。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
微博@符号的用户名提示效果。(想@到谁?)
Nov 05 Javascript
ajax页面无刷新 IE下遭遇Ajax缓存导致数据不更新的问题
Dec 11 Javascript
jquery+php随机生成红包金额数量代码分享
Aug 27 Javascript
javascript中call apply 与 bind方法详解
Mar 10 Javascript
AngularJs bootstrap搭载前台框架——准备工作
Sep 01 Javascript
微信小程序 ES6Promise.all批量上传文件实现代码
Apr 14 Javascript
vue自动化表单实例分析
May 06 Javascript
vue使用jsonp抓取qq音乐数据的方法
Jun 21 Javascript
vuejs 动态添加input框的实例讲解
Aug 24 Javascript
Vue项目中使用WebUploader实现文件上传的方法
Jul 21 Javascript
基于Element封装一个表格组件tableList的使用方法
Jun 29 Javascript
JS实现鼠标移动拖尾
Dec 27 Javascript
小程序页面动态配置实现方法
Feb 05 #Javascript
PHP实现基于Redis的MessageQueue队列封装操作示例
Feb 02 #Javascript
AngularJS实现的自定义过滤器简单示例
Feb 02 #Javascript
vue实现的树形结构加多选框示例
Feb 02 #Javascript
javascript中floor使用方法总结
Feb 02 #Javascript
JS对象和字符串之间互换操作实例分析
Feb 02 #Javascript
Vue+Element UI+Lumen实现通用表格分页功能
Feb 02 #Javascript
You might like
PHP4实际应用经验篇(2)
2006/10/09 PHP
PHP三层结构(上) 简单三层结构
2010/07/04 PHP
javascript操作JSON的要领总结
2012/12/09 Javascript
关于textarea提交的内容无法换行的解决办法
2013/04/09 Javascript
jQuery获取复选框被选中数量及判断选择值的方法详解
2016/05/25 Javascript
JS组件Bootstrap Table使用实例分享
2016/05/30 Javascript
全面解析JavaScript中apply和call以及bind(推荐)
2016/06/15 Javascript
移动端 一个简单易懂的弹出框
2016/07/06 Javascript
JavaScript中boolean类型之三种情景实例代码
2016/11/21 Javascript
Asp.Net之JS生成分页条的方法
2016/11/23 Javascript
vue使用localStorage保存登录信息 适用于移动端、PC端
2019/05/27 Javascript
微信小程序登录对接Django后端实现JWT方式验证登录详解
2019/07/29 Javascript
微信小程序获取当前时间及星期几的实例代码
2020/09/20 Javascript
[47:20]DAC2018 4.4 淘汰赛 Optic vs Mineski 第一场
2018/04/05 DOTA
Python获取某一天是星期几的方法示例
2017/01/17 Python
一篇文章弄懂Python中的可迭代对象、迭代器和生成器
2019/08/12 Python
NumPy统计函数的实现方法
2020/01/21 Python
python torch.utils.data.DataLoader使用方法
2020/04/02 Python
五款漂亮的纯CSS3动画按钮的实例教程
2014/11/21 HTML / CSS
css3实现简单的白云飘动背景特效
2020/10/28 HTML / CSS
html5教程画矩形代码分享
2013/12/04 HTML / CSS
Omio西班牙:全欧洲低价大巴、火车和航班搜索和比价
2017/02/11 全球购物
泰国的头号网上婴儿用品店:Motherhood.co.th
2019/04/09 全球购物
商务助理岗位职责
2013/11/13 职场文书
办公室秘书自我鉴定
2014/01/18 职场文书
基层党建工作宣传标语
2014/06/24 职场文书
数学教研活动总结
2014/07/02 职场文书
党员个人对照检查材料
2014/10/01 职场文书
监考失职检讨书
2015/01/26 职场文书
保管员岗位职责
2015/02/14 职场文书
岗位职责范本大全
2015/02/26 职场文书
2015年村计划生育工作总结
2015/04/28 职场文书
文明礼貌主题班会
2015/08/14 职场文书
导游词之无锡丝业博物馆
2019/11/12 职场文书
python实现简单倒计时功能
2021/04/21 Python
这样写python注释让代码更加的优雅
2021/06/02 Python