如何管理 vue 項目的數據?這個問題彷佛早已經有答案了,無非就是使用 vuex ,全局 store,整個應用維護一個超大的 Object,界面的顯示狀況隨着超大 Object 的變化而變化。javascript
看起來很簡單,不就維護一個 Object 嘛,實際上,要想組織好數據這塊代碼,必須事先對項目的數據結構理解得很是透徹,而後像設計數據庫表同樣把各個 module 的樣子設計出來。實際上,我的以爲設計 vuex 的 module 比設計數據庫表複雜得多:html
一、像數據庫同樣設計各個業務實體的外貌,這部分設計難度應該和數據庫表設計差很少;前端
二、維護一堆 ajax 請求狀態;vue
三、如何優雅地複用 module。好比有一個 PersonListModule,在一個頁面上有兩處要用到 PersonListModule 中的列表數據:一個是要在表格控件裏面展現,一個是要在下拉控件裏面展現,每一個控件中展現的列表數據篩選條件不同;java
四、如何同步 vuex 中的數據和服務器端數據。vuex 的超大 Object 能夠看作服務器端數據在客戶端內存中的一個緩存,怎麼設計這個緩存的同步策略?git
對於三、4兩個問題,結合起來更恐怖:同步服務器端數據到 PersonListModule 的同時,還要考慮如何從 PersonListModule 中篩選出分頁數據到頁面展現,還要篩選出多個列表,還要考慮在什麼時機從新更新「緩存」,想一想就頭大。github
假設咱們能力很強大,設計出了能完美應對上述問題的 store 方案,還有一個大問題攔着咱們呢:如何保證這套設計的可擴展性?由於業務系統變化無窮,不知道何時產品經理又有新想法了,咱們得設計能很好地應對變化無窮的需求嗎?ajax
vuex 的思惟模式主要是從數據着手,由數據推導出界面的樣子,這就須要先設計好 store 結構了。要設計好 store 結構,目測必須具有以下特質的工程師才能作好:vuex
一、對項目業務瞭解很是深刻;數據庫
二、具有超強的抽象思惟能力;
三、經驗豐富,能儘可能想到設計出的 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 組件結合。好比,要讓微博首頁和分組頁面中的微博列表能複用 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>
嗯,看起來挺好的!
能夠在上一個問題解決的基礎上,加上緩存功能,目測有大把現成的緩存策略能夠參考(服務器端都玩兒爛了),因爲絕大部分系統並不須要這層緩存功能,因此此處不贅述。
上述方案,思惟方向的確是致使最後執行起來輕鬆了不少,從具體到抽象的過程,很天然,符合思考習慣。可是最終的代碼仍是會很容易搞得很亂的:
一、mergeModules()
要照顧各類合併策略;
二、createXXXModule()
方法會抽出不少層。好比能夠從 createWeiboListModule()
抽出來 createContinuousListModule()
,用於構造通用的具有「向前向後」加載能力的列表 Module,最終可能會造成一條經常的「繼承鏈」,須要本身去定義維護這套繼承邏輯,心累。
其實上面兩條一看,就知道有現成的解決方案了: class。
參考此處實現:https://github.com/yibuyishen...(代碼還在完善中)。
具體業務代碼寫起來就像是這樣了:
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>
其實就是換一種思路:從界面推導數據,從具體到抽象。