利用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管理依赖包的可以执行以下命令:
npminstallbabel-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
constactionDecorator=(target,name,descriptor)=>{
constfn=descriptor.value;
descriptor.value=function(...args){
console.log('调用了修饰器的方法');
returnfn.apply(this,args);
};
returndescriptor;
};
store.js
constmodule={
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里,可以通过commitmutation或者dispatchaction来更改state,而action本质是调用commitmutation。因为storage包含异步操作,在不破坏Vuex代码规范的前提下,我们选择修饰action来扩展功能。
storage使用回调函数来读写item,首先我们将其封装成Promise结构:
storage.js
conststorage=weex.requireModule('storage');
consthandler={
get:function(target,prop){
constfn=target[prop];
//这里只需要用到这两个方法
if([
'getItem',
'setItem'
].some(method=>method===prop)){
returnfunction(...args){
//去掉回调函数,返回promise
const[callback]=args.slice(-1);
constinnerArgs=typeofcallback==='function'?args.slice(0,-1):args;
returnnewPromise((resolve,reject)=>{
fn.call(target,...innerArgs,({result,data})=>{
if(result==='success'){
returnresolve(data);
}
//防止module无保存state而出现报错
returnresolve(result);
})
})
}
}
returnfn;
},
};
exportdefaultnewProxy(storage,handler);
通过Proxy,将setItem和getItem封装为promise对象,后续使用时可以避免过多的回调结构。
现在我们把storage的setItem方法写入到修饰器:
decorator.js
importstoragefrom'./storage';
//加个rootKey,防止rootState的namespace为''而导致报错
//可自行替换为其他字符串
import{rootKey}from'./constant';
constsetState=(target,name,descriptor)=>{
constfn=descriptor.value;
descriptor.value=function(...args){
const[{state,commit}]=args;
//action为异步操作,返回promise,
//且需在状态修改为fulfilled时再将state存储到storage
returnfn.apply(this,args).then(asyncdata=>{
//获取store的moduleMap
constrawModule=Object.entries(this._modulesNamespaceMap);
//根据当前的commit,查找此action所在的module
constmoduleMap=rawModule.find(([,module])=>{
returnmodule.context.commit===commit;
});
if(moduleMap){
const[key,{_children}]=moduleMap;
constchildrenKeys=Object.keys(_children);
//只获取当前module的state,childModule的state交由其存储,按module存储数据,避免存储数据过大
//Object.fromEntries可使用object.fromentries来polyfill,或可用reduce替代
constpureState=Object.fromEntries(Object.entries(state).filter(([stateKey])=>{
return!childrenKeys.some(childKey=>childKey===stateKey);
}));
awaitstorage.setItem(rootKey+key,JSON.stringify(pureState));
}
//将data沿着promise链向后传递
returndata;
});
};
returndescriptor;
};
exportdefaultsetState;
完成了setState修饰器功能以后,就可以装饰action方法了,这样等action返回的promise状态修改为fulfilled后调用storage的存储功能,及时保存数据状态以便在新开Weex页面加载最新数据。
store.js
importsetStatefrom'./decorator';
constmodule={
state:()=>({}),
actions:{
@setState
someAction(){/**业务代码**/},
},
};
3.2、读取module数据
完成了存储数据到storage以后,我们还需要在新开的Weex页面实例能自动读取数据并初始化Vuex的状态。在这里,我们使用Vuex的plugins设置来完成这个功能。
首先我们先编写Vuex的plugin:
plugin.js
importstoragefrom'./storage';
import{rootKey}from'./constant';
constparseJSON=(str)=>{
try{
returnstr?JSON.parse(str):undefined;
}catch(e){}
returnundefined;
};
constgetState=(store)=>{
constgetStateData=asyncfunctiongetModuleState(module,path=[]){
const{_children}=module;
//根据path读取当前module下存储在storage里的数据
constdata=parseJSON(awaitstorage.getItem(`${path.join('/')}/`))||{};
constchildren=Object.entries(_children);
if(!children.length){
returndata;
}
//剔除childModule的数据,递归读取
constchildModules=awaitPromise.all(
children.map(async([childKey,child])=>{
return[childKey,awaitgetModuleState(child,path.concat(childKey))];
})
);
return{
...data,
...Object.fromEntries(childModules),
}
};
//读取本地数据,merge到Vuex的state
constinit=getStateData(store._modules.root,[rootKey]).then(savedState=>{
store.replaceState(merge(store.state,savedState,{
arrayMerge:function(store,saved){returnsaved},
clone:false,
}));
});
};
exportdefaultgetState;
以上就完成了Vuex的数据按照module读取,但Weex的IOS/Andriod中的storage存储是异步的,为防止组件挂载以后发送请求返回的数据被本地数据覆盖,需要在本地数据读取并merge到state以后再调用newVue,这里我们使用一个简易的interceptor来拦截:
interceptor.js
constinterceptors={};
exportconstregisterInterceptor=(type,fn)=>{
constinterceptor=interceptors[type]||(interceptors[type]=[]);
interceptor.push(fn);
};
exportconstrunInterceptor=async(type)=>{
consttask=interceptors[type]||[];
returnPromise.all(task);
};
这样plugin.js中的getState就修改为:
import{registerInterceptor}from'./interceptor';
constgetState=(store)=>{
/**othercode**/
constinit=getStateData(store._modules.root,[]).then(savedState=>{
store.replaceState(merge(store.state,savedState,{
arrayMerge:function(store,saved){returnsaved},
clone:false,
}));
});
//将promise放入拦截器
registerInterceptor('start',init);
};
store.js
importgetStatefrom'./plugin';
importsetStatefrom'./decorator';
constrootModule={
state:{},
actions:{
@setState
someAction(){/**业务代码**/},
},
plugins:[getState],
modules:{
/**childrenmodule**/
}
};
app.js
import{runInterceptor}from'./interceptor';
//待拦截器内所有promise返回resolved后再实例化Vue根组件
//也可以用Vue-Router的全局守卫来完成
runInterceptor('start').then(()=>{
newVue({/**othercode**/});
});
这样就实现了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。
在此不再展开,将在后续版本中实现。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。
