利用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。
在此不再展开,将在后续版本中实现。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
上一篇: CSS3 弹性盒子