vuex
如何管理 vue 项目的数据?这个问题似乎早已经有答案了,无非就是使用 vuex ,全局 store,整个应用维护一个超大的 Object,界面的显示情况随着超大 Object 的变化而变化。
看起来很简单,不就维护一个 Object 嘛,实际上,要想组织好数据这块代码,必须事先对项目的数据结构理解得非常透彻,然后像设计数据库表一样把各个 module 的样子设计出来。实际上,个人觉得设计 vuex 的 module 比设计数据库表复杂得多:
- 1、像数据库一样设计各个业务实体的外貌,这部分设计难度应该和数据库表设计差不多;
- 2、维护一堆 ajax 请求状态;
- 3、如何优雅地复用 module。比如有一个 PersonListModule,在一个页面上有两处要用到 PersonListModule 中的列表数据:一个是要在表格控件里面展示,一个是要在下拉控件里面展示,每个控件中展示的列表数据筛选条件不一样;
- 4、如何同步 vuex 中的数据和服务器端数据。vuex 的超大 Object 可以看做服务器端数据在客户端内存中的一个缓存,怎么设计这个缓存的同步策略?
对于3、4两个问题,结合起来更恐怖:同步服务器端数据到 PersonListModule 的同时,还要考虑如何从 PersonListModule 中筛选出分页数据到页面展示,还要筛选出多个列表,还要考虑在什么时机重新更新“缓存”,想想就头大。
假设我们能力很强大,设计出了能完美应对上述问题的 store 方案,还有一个大问题拦着我们呢:如何保证这套设计的可扩展性?因为业务系统变化多端,不知道什么时候产品经理又有新想法了,我们得设计能很好地应对变化多端的需求吗?
为什么这么难?问题究竟出现在哪里?
vuex 的思维模式主要是从数据着手,由数据推导出界面的样子,这就需要先设计好 store 结构了。要设计好 store 结构,目测必须具备如下特质的工程师才能做好:
- 1、对项目业务了解非常深入;
- 2、具备超强的抽象思维能力;
- 3、经验丰富,能尽量想到设计出的 store 结构能应付哪些情况、不能应付哪些情况。
第2条的门槛实在是太高了,能做到的前端工程师估计没多少。
怎么办?
我们不应该从数据推导出界面,而应该从界面推导出数据,逐层抽象。
比如现在要仿一个新浪微博首页,页面上主要包含的数据有:分组信息、微博列表、个人信息、一些推荐信息等,那么就设计一个只针对该页面的 module ,大致结构为:
const homePageModule = {
state: {
groupList: [{
id: 1,
name: '名人明星',
unread: 1
},
{
id: 2,
name: '同事',
unread: 0
}
],
groupListExtraInfo: {
// 初始显示多少个小组
initShowCount: 5,
loading: true
},
weiboList: [{
id: 1,
content: '<p>震惊部</p>',
author: 'yibuyisheng',
createTime: '20170719234422'
}],
weiboListPageInfo: {
loadingStatus: 'QUIET', // 三种取值:QUIET -> 没有加载;UP -> 向上加载;DOWN -> 向下加载
// weiboList 的开始时间,可用这个时间戳做下一次的向上加载
startTime: '20170719234422',
// weiboList 的结束时间,可用这个时间戳做下一次的向下加载
endTime: '20170719234422'
},
self: {
id: 1,
nickname: 'yibuyisheng',
email: 'yibuyisheng@163.com',
avatar: 'http://weibo.com/2674779523/profile?rightmod=1&wvr=6&mod=personinfo',
followedCount: 405,
followerCount: 235,
weiboCount: 1321
},
recommendMovies: [
...
],
recommendTopics: [
...
]
...
},
mutations: {
updateWeiboList(state, list) {
...
}
},
actions: {
appendWeiboList() {
...
},
prependWeiboList() {
...
}
}
};
针对这个页面,这个结构,各个处理逻辑就具体化、特殊化了,代码写起来非常轻松。
代码复用?
假设现在有个小组页面,点进去后可以看到该小组所有成员发的微博,因为是一个新的页面,所以需要新起一个 module ,这也意味着要重复写一遍 weiboList 相关的代码,岂不蛋疼!
此时可以考虑写一个 createWeiboListModule()
函数,用于创建这种通用 module ,然后再写一个 mergeModules()
函数,把 createWeiboListModule()
函数创建出来的 module 对象和各页面特殊的 module 合并起来,样子看起来大致是这样:
mergeModules(createWeiboListModule(), {
state: {
...
},
mutations: {
...
},
actions: {
...
}
});
遇到需要复用的才抽取通用逻辑,很自然,很简单。
怎么结合 vue 组件?
上面的结构有一个很大的问题,就是不能很好地和 vue 组件结合。比如,要让微博首页和分组页面中的微博列表能复用 weiboList 相关代码,那么 weiboList 涉及到的 state、action、mutation、getter 的命名都要尽量保持一致,不然就要传一个 nameMap(命名映射)给两个页面通用的 WeiboListComponent 组件,看起来就像这样:
<weibo-list-component :name-map="{weiboList: 'homePageWeiboList'}"></weibo-list-component>
简直蛋疼!
好吧,那就严格约束这两个页面的 state、action、mutation、getter 命名都保持一致吧!
简直超级蛋疼!
此时可以考虑用 namespace 来解决这个问题,比如上面的 homePageModule
可以把 weiboList
拆分出来:
const store = new vuex.Store({
...,
modules: {
'page:home': {
state: {
groupList: [{
id: 1,
name: '名人明星',
unread: 1
},
{
id: 2,
name: '同事',
unread: 0
}
],
groupListExtraInfo: {
// 初始显示多少个小组
initShowCount: 5,
loading: true
},
self: {
id: 1,
nickname: 'yibuyisheng',
email: 'yibuyisheng@163.com',
avatar: 'http://weibo.com/2674779523/profile?rightmod=1&wvr=6&mod=personinfo',
followedCount: 405,
followerCount: 235,
weiboCount: 1321
},
recommendMovies: [
...
],
recommendTopics: [
...
]
...
},
},
'page:home:weiboList': createWeiboListModule(...)
}
...
});
这样一来,只要给 vue 组件传一个 namespace 参数就行了:
<weibo-list-component namespace="page:home:weiboList"></weibo-list-component>
嗯,看起来挺好的!
如何处理“store 缓存”?
可以在上一个问题解决的基础上,加上缓存功能,目测有大把现成的缓存策略可以参考(服务器端都玩儿烂了),由于绝大部分系统并不需要这层缓存功能,所以此处不赘述。
就这样了吗?
上述方案,思维方向的确是导致最后执行起来轻松了很多,从具体到抽象的过程,很自然,符合思考习惯。但是最终的代码还是会很容易搞得很乱的:
- 1、
mergeModules()
要照顾各种合并策略; - 2、
createXXXModule()
方法会抽出很多层。比如可以从createWeiboListModule()
抽出来createContinuousListModule()
,用于构造通用的具备“向前向后”加载能力的列表 Module,最终可能会形成一条常常的“继承链”,需要自己去定义维护这套继承逻辑,心累。
其实上面两条一看,就知道有现成的解决方案了: class。
参考此处实现:https://github.com/yibuyisheng/vuex-model/blob/master/src/store/BaseModule.js(代码还在完善中)。
具体业务代码写起来就像是这样了:
class ContinuousList extends BaseModule {
state = {
list: [],
pageInfo: {
loadingStatus: 'QUIET',
startTime: '20170720003939',
endTime: '20170720003939'
}
}
@action
async appendList(...) {
...
const result = await request('some url', params);
this.updateList(result.list);
...
}
@action
prependList(...) { ... }
}
class WeiboList extends ContinuousList {
@action
async voteUp(...) {
...
await request('some url', params);
const weiboDetail = await updateWeibo('some url', params.weiboId);
const newList = this.state.list.map((wb) => {
return wb.id === weiboDetail.id ? weiboDetail : wb;
});
this.updateList(newList);
...
}
}
@composition(WeiboList)
class HomePage extends BaseModule {
$namespace = 'page:home:';
...
@action
requestRecommendInfo(...) {
...
}
...
}
HomePage.register();
在对应的 HomePage.vue 里面,大致是这样:
<template>
<div class="home-page-view">
...
<weibo-list-component namespace="page:home:weiboList"></weibo-list-component>
...
</div>
</template>
<script>
export default {
created() {
...
const constants = this.getConstants('page:home');
this.$store.dispatch(constants.REQUEST_RECOMMEND_INFO, params);
...
}
};
</script>
而 WeiboListComponent
组件大致是这样:
<template>
<div class="weibo-list-component">
...
</div>
</template>
<script>
export default {
props: {
namespace: {
type: String,
required: true
}
},
computed: {
weiboList() {
const constants = this.getConstants(this.namespace);
return this.$store.getters[constants.LIST];
}
},
created() {
...
const constants = this.getConstants(this.namespace);
this.$store.dispatch(constants.APPEND_LIST, params);
...
}
};
</script>
总结
其实就是换一种思路:从界面推导数据,从具体到抽象。