利用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 相关文章推荐
jquery三个关闭弹出层的小示例
Nov 05 Javascript
原生js和jquery实现图片轮播淡入淡出效果
Apr 23 Javascript
关于JS 预解释的相关理解
Jun 28 Javascript
jQuery动态生成Bootstrap表格
Nov 01 Javascript
js 去掉字符串前后空格实现代码集合
Mar 25 Javascript
JavaScript数据类型和变量_动力节点Java学院整理
Jun 26 Javascript
JS与CSS3实现图片响应鼠标移动放大效果示例
May 04 Javascript
微信小程序仿RadioGroup改变样式的处理方案
Jul 13 Javascript
纯js+css实现仿移动端淘宝网站的弹出详情框功能
Dec 29 Javascript
vue中实现动态生成二维码的方法
Feb 21 Javascript
jQuery实现倒计时功能完整示例
Jun 01 jQuery
vue 需求 data中的数据之间的调用操作
Aug 05 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
3种平台下安装php4经验点滴
2006/10/09 PHP
7个超级实用的PHP代码片段
2011/07/11 PHP
php动态生成版权所有信息的方法
2015/03/24 PHP
PHP实现创建一个RPC服务操作示例
2020/02/23 PHP
php自动加载代码实例详解
2021/02/26 PHP
兼容FireFox 的 js 日历 支持时间的获取
2009/03/04 Javascript
js类的静态属性和实例属性的理解
2009/10/01 Javascript
js调用AJAX时Get和post的乱码解决方法
2013/06/04 Javascript
每天一篇javascript学习小结(属性定义方法)
2015/11/19 Javascript
jquery显示隐藏元素的实现代码
2016/05/19 Javascript
基于Vuejs实现购物车功能
2016/08/02 Javascript
模板视图和AngularJS之间冲突的解决方法
2016/11/22 Javascript
用headjs来管理和加载js 提高网站加载速度
2016/11/29 Javascript
jQuery实现按比例缩放图片的方法
2017/04/29 jQuery
js实现多张图片延迟加载效果
2017/07/17 Javascript
利用jQuery+localStorage实现一个简易的计时器示例代码
2017/12/25 jQuery
浅谈Webpack 是如何加载模块的
2018/05/24 Javascript
vue+element-ui集成随机验证码+用户名+密码的form表单验证功能
2018/08/05 Javascript
layer.close()关闭进度条和Iframe窗的方法
2018/08/17 Javascript
[06:16]第十四期-国士无双绝地翻盘之撼地神牛
2014/06/24 DOTA
Python中urllib2模块的8个使用细节分享
2015/01/01 Python
python使用wmi模块获取windows下硬盘信息的方法
2015/05/15 Python
让Django支持Sql Server作后端数据库的方法
2018/05/29 Python
Python Flask框架扩展操作示例
2019/05/03 Python
Python Django 前后端分离 API的方法
2019/08/28 Python
使用Python将字符串转换为格式化的日期时间字符串
2019/09/01 Python
python使用OpenCV模块实现图像的融合示例代码
2020/04/10 Python
python中xlrd模块的使用详解
2021/02/01 Python
JAKO-O德国野酷台湾站:德国首屈一指的婴幼童用品品牌
2019/01/14 全球购物
2014年学习雷锋活动总结
2014/03/01 职场文书
财务部副经理岗位职责范本
2014/06/17 职场文书
党的群众路线教育实践活动个人整改措施落实情况
2014/11/04 职场文书
党员进社区活动总结
2015/05/07 职场文书
php 获取音视频时长,PHP 利用getid3 获取音频文件时长等数据
2021/04/01 PHP
iPhone13再次曝光
2021/04/15 数码科技
总结三种用 Python 作为小程序后端的方式
2022/05/02 Python