vuex
如何管理 vue 項目的數(shù)據(jù)炭菌?這個問題似乎早已經(jīng)有答案了黑低,無非就是使用 vuex ,全局 store蕾管,整個應(yīng)用維護(hù)一個超大的 Object掰曾,界面的顯示情況隨著超大 Object 的變化而變化停团。
看起來很簡單,不就維護(hù)一個 Object 嘛秒梅,實際上捆蜀,要想組織好數(shù)據(jù)這塊代碼辆琅,必須事先對項目的數(shù)據(jù)結(jié)構(gòu)理解得非常透徹婉烟,然后像設(shè)計數(shù)據(jù)庫表一樣把各個 module 的樣子設(shè)計出來似袁。實際上咐刨,個人覺得設(shè)計 vuex 的 module 比設(shè)計數(shù)據(jù)庫表復(fù)雜得多:
- 1定鸟、像數(shù)據(jù)庫一樣設(shè)計各個業(yè)務(wù)實體的外貌,這部分設(shè)計難度應(yīng)該和數(shù)據(jù)庫表設(shè)計差不多啼县;
- 2、維護(hù)一堆 ajax 請求狀態(tài)余蟹;
- 3子刮、如何優(yōu)雅地復(fù)用 module挺峡。比如有一個 PersonListModule,在一個頁面上有兩處要用到 PersonListModule 中的列表數(shù)據(jù):一個是要在表格控件里面展示佛呻,一個是要在下拉控件里面展示吓著,每個控件中展示的列表數(shù)據(jù)篩選條件不一樣送挑;
- 4、如何同步 vuex 中的數(shù)據(jù)和服務(wù)器端數(shù)據(jù)纺裁。vuex 的超大 Object 可以看做服務(wù)器端數(shù)據(jù)在客戶端內(nèi)存中的一個緩存欺缘,怎么設(shè)計這個緩存的同步策略挤安?
對于3蛤铜、4兩個問題,結(jié)合起來更恐怖:同步服務(wù)器端數(shù)據(jù)到 PersonListModule 的同時剿干,還要考慮如何從 PersonListModule 中篩選出分頁數(shù)據(jù)到頁面展示置尔,還要篩選出多個列表氢伟,還要考慮在什么時機(jī)重新更新“緩存”幽歼,想想就頭大试躏。
假設(shè)我們能力很強(qiáng)大颠蕴,設(shè)計出了能完美應(yīng)對上述問題的 store 方案助析,還有一個大問題攔著我們呢:如何保證這套設(shè)計的可擴(kuò)展性外冀?因為業(yè)務(wù)系統(tǒng)變化多端,不知道什么時候產(chǎn)品經(jīng)理又有新想法了西轩,我們得設(shè)計能很好地應(yīng)對變化多端的需求嗎藕畔?
為什么這么難庄拇?問題究竟出現(xiàn)在哪里?
vuex 的思維模式主要是從數(shù)據(jù)著手溶弟,由數(shù)據(jù)推導(dǎo)出界面的樣子辜御,這就需要先設(shè)計好 store 結(jié)構(gòu)了凰浮。要設(shè)計好 store 結(jié)構(gòu),目測必須具備如下特質(zhì)的工程師才能做好:
- 1、對項目業(yè)務(wù)了解非常深入笛厦;
- 2俺夕、具備超強(qiáng)的抽象思維能力;
- 3姨谷、經(jīng)驗豐富,能盡量想到設(shè)計出的 store 結(jié)構(gòu)能應(yīng)付哪些情況瞎颗、不能應(yīng)付哪些情況哼拔。
第2條的門檻實在是太高了瓣颅,能做到的前端工程師估計沒多少宫补。
怎么辦?
我們不應(yīng)該從數(shù)據(jù)推導(dǎo)出界面健民,而應(yīng)該從界面推導(dǎo)出數(shù)據(jù)荞雏,逐層抽象平酿。
比如現(xiàn)在要仿一個新浪微博首頁,頁面上主要包含的數(shù)據(jù)有:分組信息筑辨、微博列表棍辕、個人信息还绘、一些推薦信息等,那么就設(shè)計一個只針對該頁面的 module 抚太,大致結(jié)構(gòu)為:
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: 'QUITE', // 三種取值:QUITE -> 沒有加載电媳;UP -> 向上加載庆亡;DOWN -> 向下加載
// weiboList 的開始時間,可用這個時間戳做下一次的向上加載
startTime: '20170719234422',
// weiboList 的結(jié)束時間拼缝,可用這個時間戳做下一次的向下加載
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() {
...
}
}
};
針對這個頁面珍促,這個結(jié)構(gòu)猪叙,各個處理邏輯就具體化仁卷、特殊化了,代碼寫起來非常輕松芒帕。
代碼復(fù)用背蟆?
假設(shè)現(xiàn)在有個小組頁面哮幢,點進(jìn)去后可以看到該小組所有成員發(fā)的微博,因為是一個新的頁面垛叨,所以需要新起一個 module 嗽元,這也意味著要重復(fù)寫一遍 weiboList 相關(guān)的代碼喂击,豈不蛋疼翰绊!
此時可以考慮寫一個 createWeiboListModule()
函數(shù),用于創(chuàng)建這種通用 module 琳要,然后再寫一個 mergeModules()
函數(shù)稚补,把 createWeiboListModule()
函數(shù)創(chuàng)建出來的 module 對象和各頁面特殊的 module 合并起來框喳,樣子看起來大致是這樣:
mergeModules(createWeiboListModule(), {
state: {
...
},
mutations: {
...
},
actions: {
...
}
});
遇到需要復(fù)用的才抽取通用邏輯,很自然乍惊,很簡單润绎。
怎么結(jié)合 vue 組件诞挨?
上面的結(jié)構(gòu)有一個很大的問題,就是不能很好地和 vue 組件結(jié)合棍郎。比如涂佃,要讓微博首頁和分組頁面中的微博列表能復(fù)用 weiboList 相關(guān)代碼蜈敢,那么 weiboList 涉及到的 state扶认、action、mutation狱从、getter 的命名都要盡量保持一致叠纹,不然就要傳一個 nameMap(命名映射)給兩個頁面通用的 WeiboListComponent 組件誉察,看起來就像這樣:
<weibo-list-component :name-map="{weiboList: 'homePageWeiboList'}"></weibo-list-component>
簡直蛋疼!
好吧驼卖,那就嚴(yán)格約束這兩個頁面的 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 參數(shù)就行了:
<weibo-list-component namespace="page:home:weiboList"></weibo-list-component>
嗯蔽午,看起來挺好的及老!
如何處理“store 緩存”范抓?
可以在上一個問題解決的基礎(chǔ)上匕垫,加上緩存功能,目測有大把現(xiàn)成的緩存策略可以參考(服務(wù)器端都玩兒爛了)寞秃,由于絕大部分系統(tǒng)并不需要這層緩存功能偶惠,所以此處不贅述。
就這樣了嗎绑改?
上述方案,思維方向的確是導(dǎo)致最后執(zhí)行起來輕松了很多识腿,從具體到抽象的過程皆的,很自然,符合思考習(xí)慣。但是最終的代碼還是會很容易搞得很亂的:
- 1楞抡、
mergeModules()
要照顧各種合并策略析藕; - 2、
createXXXModule()
方法會抽出很多層竞慢。比如可以從createWeiboListModule()
抽出來createContinuousListModule()
筹煮,用于構(gòu)造通用的具備“向前向后”加載能力的列表 Module居夹,最終可能會形成一條常常的“繼承鏈”准脂,需要自己去定義維護(hù)這套繼承邏輯,心累沟饥。
其實上面兩條一看湾戳,就知道有現(xiàn)成的解決方案了: class。
參考此處實現(xiàn):https://github.com/yibuyisheng/vuex-model/blob/master/src/store/BaseModule.js (代碼還在完善中)遮晚。
具體業(yè)務(wù)代碼寫起來就像是這樣了:
class ContinuousList extends BaseModule {
state = {
list: [],
pageInfo: {
loadingStatus: 'QUITE',
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();
在對應(yīng)的 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>
總結(jié)
其實就是換一種思路:從界面推導(dǎo)數(shù)據(jù)其兴,從具體到抽象夸政。