利用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 相关文章推荐
js window.event对象详尽解析
Feb 17 Javascript
JavaScript isArray()函数判断对象类型的种种方法
Oct 11 Javascript
Knockout数组(observable)使用详解示例
Nov 15 Javascript
使用 JavaScript 进行函数式编程 (一) 翻译
Oct 02 Javascript
Jquery和angularjs获取check框选中的值的方法汇总
Jan 17 Javascript
BootstrapTable与KnockoutJS相结合实现增删改查功能【一】
May 10 Javascript
Winform客户端向web地址传参接收参数的方法
May 17 Javascript
JavaScript知识点总结(十六)之Javascript闭包(Closure)代码详解
May 31 Javascript
jQuery实现表格行和列的动态添加与删除方法【测试可用】
Aug 01 Javascript
vue watch监听对象及对应值的变化详解
Feb 24 Javascript
Node.js开发之套接字(socket)编程入门示例
Nov 05 Javascript
vuex 实现getter值赋值给vue组件里的data示例
Nov 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
后宫无数却洁身自好的男主,唐三只爱小舞
2020/03/02 国漫
基于PHP+Ajax实现表单验证的详解
2013/06/25 PHP
解决CodeIgniter伪静态失效
2014/06/09 PHP
thinkPHP简单实现多个子查询语句的方法
2016/12/05 PHP
PHP new static 和 new self详解
2017/02/19 PHP
PHP支付宝当面付2.0代码
2018/12/21 PHP
Laravel5.5 视图 - 创建视图和数据传递示例
2019/10/21 PHP
云网广告中的代码,提示出错,大家找找
2006/11/21 Javascript
IE与Firefox在JavaScript上的7个不同写法小结
2009/09/14 Javascript
js实现拉伸拖动iframe的具体代码
2013/08/03 Javascript
使用JavaScript实现Java的List功能(实例讲解)
2013/11/07 Javascript
深入理解JS正则表达式---分组
2016/07/18 Javascript
如何使用bootstrap框架 bootstrap入门必看!
2017/04/13 Javascript
微信小程序云开发(数据库)详解
2019/05/17 Javascript
Vue程序调试的方法
2019/06/17 Javascript
浅谈layui 表单元素的选中问题
2019/10/25 Javascript
mpvue实现微信小程序快递单号查询代码
2020/04/03 Javascript
JS数据类型判断的几种常用方法
2020/07/07 Javascript
Python实例一个类背后发生了什么
2016/02/09 Python
Python实现处理逆波兰表达式示例
2018/07/30 Python
PyQt5根据控件Id获取控件对象的方法
2019/06/25 Python
Python搭建HTTP服务过程图解
2019/12/14 Python
Pytorch之contiguous的用法
2019/12/31 Python
python装饰器实现对异常代码出现进行自动监控的实现方法
2020/09/15 Python
表单button的outline在firefox浏览器下的问题
2012/12/24 HTML / CSS
加拿大高尔夫超市:Golf Town
2018/01/12 全球购物
澳大利亚运动鞋零售商:The Athlete’s Foot
2018/11/04 全球购物
乌克兰电子产品和家用电器购物网站:TOUCH
2019/08/09 全球购物
德国苹果商店:MacTrade
2020/05/18 全球购物
MYSQL基础面试题
2012/05/13 面试题
厨师岗位职责
2013/11/12 职场文书
护理专业学生的求职信范文
2013/12/11 职场文书
微博营销计划书
2014/01/10 职场文书
领导工作表现评语
2015/01/04 职场文书
2015小学教师年度考核工作总结
2015/05/12 职场文书
golang json数组拼接的实例
2021/04/28 Golang