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

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。

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

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

(0)

相关推荐

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

    1.引言 在H5的Vue项目中,最为常见的当为单页应用(SPA),利用Vue-Router控制组件的挂载与复用,这时使用Vuex可以方便的维护数据状态而不必关心组件间的数据通信.但在Weex中,不同的页面之间使用不同的执行环境,无法共享数据,此时多为通过BroadcastChannel或storage模块来实现数据通信,本文主要使用修饰器(Decorator)来扩展Vuex的功能,实现分模块存储数据,并降低与业务代码的耦合度. 2.Decorator 设计模式中有一种装饰器模式,可以在运行时扩展

  • 利用Angularjs中模块ui-route管理状态的方法

    ui-router 的工作原理非常类似于 Angular 的路由控制器,但它只关注状态. 在应用程序的整个用户界面和导航中,一个状态对应于一个页面位置 通过定义controller.template和view等属性,来定义指定位置的用户界面和界面行为 通过嵌套的方式来解决页面中的一些重复出现的部位 最简单的形式 模板可以通过下面这种最简单的方式来指定 <!-- in index.html --> <body ng-controller="MainCtrl"> &

  • nuxt踩坑之Vuex状态树的模块方式使用详解

    初次看到这个模块方式,感觉很是新奇,之前的vuex状态树使用方法用的也有些腻了,就想来实践一发新的东西 废话不多说,直接进入正题 Vuex状态树-模块方式官方文档解读 状态树还可以拆分成为模块,store 目录下的每个 .js 文件会被转换成为状态树指定命名的子模块 这句话啊,看了半天,我都没绕出来.之前一直用的是store目录下文件为:index.js.state.js.mutations.js.actions.js.后三个是index.js的子模块,你说这每个js文件都是一个模块?懵逼一分钟

  • uni-app微信小程序登录并使用vuex存储登录状态的思路详解

    微信小程序注册登录思路 (这是根据自身的项目的思路,不一定每个项目都适用) 1.制作授权登录框,引导用户点击按钮 2.uni.login获取code 3.把code传给后端接口,后端返回如下数据 openid: "ogtVM5RWdfadfasdfadfadV5s" status: 1 // 状态码:status==0(该用户未注册,需调用注册接口) status==1(该用户已注册) 4.判断用户是否注册,并调用用户信息接口 (1)若已注册则提示登录成功,并调用后台给的获取用户信息的

  • vuex分模块后,实现获取state的值

    问题:vuex分模块后,一个模块如何拿到其他模块的state值,调其他模块的方法? 思路: 1.通过命名空间取值--this.$store.state.car.list // OK 2.通过定义该属性的getter方法,因方法全局注册,不存在命名空间,可以通过this直接调用. this.$store.state.car.carGetter 我在car模块中自己的定义了state, getters, this.$store.state.car.list可以拿到值. 但是,this.$store.

  • vue使用Vuex状态管理模式

    目录 1.基于单向数据流问题而产生了Vuex 2.安装及使用 3.核心及使用方法 (1)State (2) Getters (3) Mutation (4) Action (5)Modules 4.Vuex和全局对象的不同 1.基于单向数据流问题而产生了Vuex 单向数据流是vue 中父子组件的核心概念,props 是单向绑定的.当父组件的属性值发生变化的时候,会传递给子组件发生相应的变化,从而形成一个单向下行的绑定,父组件的属性改变会流向下行子组件中,但是反之,为了防止子组件无意间修改了父组件

  • Vue详细讲解Vuex状态管理的实现

    目录 什么是状态管理 Vuex的使用 单一状态树和mapState辅助函数 1. 单一状态树 2. mapState辅助函数 getters的基本使用 1. getter的使用 2. getters第二个参数 3. getters的返回函数 (了解) 4. mapGetters的辅助函数 mutation基本使用 1. mutation携带数据 2. mutation重要原则 actions的基本使用 1. actions的分发操作 2. actions的辅助函数 3. actions的异步操作

  • 详解vuex状态管理模式

    一.前言 本次接受一个BI系统,要求是能够接入数据源-得到数据集-对数据集进行处理-展现为数据的可视化,这一个系统为了接入公司自身的产品,后端技术采用spring boot,前端采用vue+vuex+axios的项目架构方式,vuex作为vue的状态管理,是尤为重要的部分.这里,我将vuex如何运作和使用做一次总结,有错的地方,望多多提点. 二.vuex简单使用 安装vuex cnpm install vuex --save 在src目录下建立文件夹,命名为store,建立index.js 如图

  • 快速掌握Vue3.0中如何上手Vuex状态管理

    Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式.它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化.Vuex 也集成到 Vue 的官方调试工具 devtools,提供了诸如零配置的 time-travel 调试.状态快照导入导出等高级调试功能. 如果您不打算开发大型单页应用,使用 Vuex 可能是繁琐冗余的.确实是如此--如果您的应用够简单,您最好不要使用 Vuex.一个简单的 store 模式就足够您所需了.但是,如果您需要构建一个中大型

  • 如何利用Typescript封装本地存储

    目录 前言 本地存储使用场景 使用中存在的问题 解决方案 功能实现 加入过期时间 加入数据加密 加入命名规范 完整代码 总结 前言 本地存储是前端开发过程中经常会用到的技术,但是官方api在使用上多有不便,且有些功能并没有提供给我们相应的api,比如设置过期时间等.本文无意于介绍关于本地存储概念相关的知识,旨在使用typescript封装一个好用的本地存储类. 本地存储使用场景 用户登录后token的存储 用户信息的存储 不同页面之间的通信 项目状态管理的持久化,如redux的持久化.vuex的

随机推荐