讀Mobx官方文檔中的最佳實(shí)踐有感冀泻,并結(jié)合一些自己項(xiàng)目經(jīng)驗(yàn)弹渔±谈剑總結(jié)一下遇到的坑您没,和預(yù)備的解決方案氨鹏。
用了半年的redux,依稀記得剛開始用Redux的時(shí)候到處查找文檔想知道組織state結(jié)構(gòu)的最佳實(shí)踐跟继,因?yàn)樵趖odo list的demo中用list代表todos舔糖,并不知道這個(gè)狀態(tài)下的todos是純數(shù)據(jù)金吗,還是頁面中展示數(shù)據(jù)的緩存趣竣。意思是遥缕,不太明白:
- 是store為頁面服務(wù)(將頁面中需要的可能會(huì)變更的數(shù)據(jù)另外存儲(chǔ)起來)
- 還是數(shù)據(jù)就是數(shù)據(jù)单匣,頁面只是取得數(shù)據(jù)(類似于數(shù)據(jù)庫)
最后搜到了作者的一句話户秤,大概就是,隨便你怎么用泡徙,用出花來都行膜蠢,鼓勵(lì)大家自主創(chuàng)新。所以在前期寫頁面時(shí)我也主要使用的一種比較流行的方式糖荒,就是上面的方式1模捂,store為頁面服務(wù)狂男。
0x00 store為頁面服務(wù)岖食,以及遇到的坑
在前期使用這種方案真心不要太爽:
- 看著設(shè)計(jì)圖想想所需要的數(shù)據(jù)
- 按照這些數(shù)據(jù)構(gòu)建每個(gè)頁面的state樹節(jié)點(diǎn)
- 寫可能的action處理每一個(gè)用戶行為
- 構(gòu)建好用戶請求用到的異步action
- 寫reducer處理請求到的數(shù)據(jù)
- 接入現(xiàn)實(shí)數(shù)據(jù)
- 測試
完美泡垃,action寫法統(tǒng)一蔑穴,管理方便存和,reducer寫法統(tǒng)一,管理方便祭饭,數(shù)據(jù)流下來叙量,刷新不用自己處理(setState)九串。但是绞佩,最終發(fā)現(xiàn),自己的項(xiàng)目猪钮,單頁面這樣寫寫很不錯(cuò),但是并不適合寫一個(gè)完整的app或者前后端同構(gòu)的web app烤低。(遇到啥問題下面說扑馁,先看看這種store的組織方式怎么做)
0x01 什么是store為頁面服務(wù)
即根節(jié)點(diǎn)往下是各個(gè)頁面,頁面中的組件對應(yīng)著state子樹中的節(jié)點(diǎn)涝登。
在React中,一個(gè)頁面是由若干個(gè)嵌套的組件構(gòu)成咽笼,每個(gè)組件都有相應(yīng)的數(shù)據(jù)輸入剑刑,最終這些數(shù)據(jù)輸入可以反映成一個(gè)樹狀結(jié)構(gòu),最后我們直觀的使用這個(gè)樹狀結(jié)構(gòu)到state樹上其监,就如下圖所示抖苦。
0x02 遇到了什么問題
- 重復(fù)頁面倒退
- 數(shù)據(jù)同步
重復(fù)頁面倒退是指如下這種情況:
假設(shè)這種情況:在React Native和Redux構(gòu)建的一個(gè)App中,我們從首頁feed流進(jìn)入某個(gè)id為1的文章的詳情頁,從詳情頁進(jìn)入了某個(gè)推薦列表卤材,然后又從這個(gè)推薦列表進(jìn)入了另一個(gè)id為2的文章的詳情頁。
現(xiàn)在問題來了帆精,無論是id為1的詳情頁卓练,還是id為2的詳情頁末贾,它們用的都是同一個(gè)state樹的分支(可能叫state.detailPage
)拱撵,所以當(dāng)頁面瀏覽到id為2的詳情頁的時(shí)候,數(shù)據(jù)請求完畢之后設(shè)置到state.detailPage
分支集索,id為1的數(shù)據(jù)就被替換掉了务荆。如果這個(gè)時(shí)候要回退到id為1的詳情頁,就必須得重新獲取數(shù)據(jù)盅惜。否則就還是id為2的數(shù)據(jù)。
在web應(yīng)用里屈芜,用戶比較習(xí)慣在白頁中重新加載數(shù)據(jù)(當(dāng)回退的時(shí)候),可是在app中,這是不符合習(xí)慣的,而且在app中頁面發(fā)生了變化坟乾,只是向一個(gè)頁面的堆棧中壓入一個(gè)新的頁面甚侣,之前的頁面并不會(huì)釋放調(diào)印荔,我們習(xí)慣性將獲取頁面數(shù)據(jù)放入componentWillMount或者constructor或者任一個(gè)生命周期函數(shù)中,都不會(huì)執(zhí)行(也就是說不會(huì)重新獲取數(shù)據(jù))水泉。
第二個(gè)問題是數(shù)據(jù)同步。
還記得flux的引入是為了解決什么問題嗎炕横?
facebook右上角的消息提醒總是莫名其妙的出現(xiàn),因?yàn)橥窍⑻嵝训臄?shù)據(jù)在不同的數(shù)據(jù)源中可能重復(fù)存有多份。使用flux可以讓數(shù)據(jù)的源頭只有一個(gè)腔寡,所有的展示都是通過一個(gè)源頭流下來的數(shù)據(jù)產(chǎn)生的,某個(gè)頁面中我讀取了當(dāng)前的所有消息,發(fā)送一個(gè)action告訴數(shù)據(jù)源似扔,現(xiàn)在未讀消息變?yōu)?了豪墅,然后所有地方的未讀消息,受這個(gè)數(shù)據(jù)的改變都變成0了。
然而如果采用頁面即數(shù)據(jù)的這種方案亭枷,即使數(shù)據(jù)源只有一個(gè)瘤睹,但是同一種數(shù)據(jù)也有可能在多個(gè)地方存儲(chǔ)過驴党。例如上面的例子:列表頁中可能有某個(gè)文章的標(biāo)題(在列表頁樹分支的某個(gè)節(jié)點(diǎn)上),這個(gè)文章的詳情頁也有這個(gè)文章的標(biāo)題鹏氧,假設(shè)我在某個(gè)地方(假設(shè)是詳情頁)更改了這個(gè)樹分支上的標(biāo)題茸俭,其他樹分支上標(biāo)題并不會(huì)改變(例如列表頁艇炎,因?yàn)槭遣煌臄?shù)據(jù))腺晾,依然沒有解決flux根本想解決的問題托慨。
0x10 尋求解決方案
兩個(gè)問題都有其各自單獨(dú)的解決方案。
要解決相同頁面會(huì)退的問題,就必須區(qū)分id為1和id為2的數(shù)據(jù),例如,我們可以在state.detailPage[1]
和state.detailPage[2]
中分別存放id為1的詳情頁的數(shù)據(jù)和id為2的詳情頁的數(shù)據(jù)宋列,然而redux的combine并不能動(dòng)態(tài)的增加分支盗迟,分之節(jié)點(diǎn)都是事先預(yù)置好的,要實(shí)現(xiàn)這種喂饥,我們只能自己寫中間件导饲,或者自己實(shí)現(xiàn)插入分支(我是這樣做的)硝岗。
如果要解決數(shù)據(jù)同步的問題,有兩種方案:第一種,使用事件機(jī)制,所有要跟著變動(dòng)的地方,建一個(gè)變更的事件,當(dāng)變更的時(shí)候觸發(fā)這個(gè)事件,讓所有相關(guān)的地方發(fā)生改變;第二種,不管是列表的標(biāo)題還是詳情的標(biāo)題,都只存一次,存在一個(gè)地方,那么不同地方取的都是同一個(gè)數(shù)據(jù),就可以自然同步了应闯。
方案一讓人感覺redux并沒有幫上什么忙惜辑,第二種方法不太好實(shí)現(xiàn),在實(shí)際中捧搞,我們混合使用了兩種方案抵卫。
這兩個(gè)問題看下來,讓人第一感受是:數(shù)據(jù)(按照ID區(qū)分胎撇,會(huì)同時(shí)出現(xiàn)在多處的那種)和頁面需要分離介粘,數(shù)據(jù)以表的形式存在,并且只存一次晚树。
如果解決這個(gè)問題呢姻采?還是以上面可能的app為例:
我們建立一個(gè)叫文章的state下的子樹,其是一個(gè)id, value的map爵憎,用id區(qū)分(當(dāng)然慨亲,得自己實(shí)現(xiàn)),當(dāng)然也會(huì)有一個(gè)叫首頁的子樹宝鼓,但是首頁只有一個(gè)刑棵,所以它可以正常來,但是首頁的feed流list只是一個(gè)id的list愚铡,其并不包含具體數(shù)據(jù)蛉签,具體數(shù)據(jù)都在叫文章的子樹里。
在reducer獲取的時(shí)候沥寥,先將列表接口獲取的已有的數(shù)據(jù)賦給文章map對應(yīng)id的各個(gè)文章碍舍,然后向列表頁(首頁feed)返回一個(gè)id的列表。列表頁要取詳情营曼,就去文章的map中自己取乒验。到了詳情頁,向后端接口獲取詳情數(shù)據(jù)蒂阱,再將文章map中锻全,讓正在訪問的這條的信息更新的更完備狂塘。
0x11 另一個(gè)構(gòu)建store的方法 - Mobx
其實(shí)總得來說flux應(yīng)該是一套從后端到前端一路向下的數(shù)據(jù)解決方案,而不應(yīng)該僅僅只是用在react的前端這塊的數(shù)據(jù)處理鳄厌,要是這樣的話荞胡,可能它方便之處并不在于單一數(shù)據(jù)源。而應(yīng)該在于前端開發(fā)和調(diào)試的時(shí)候了嚎,能規(guī)整代碼結(jié)構(gòu)泪漂,讓數(shù)據(jù)可追溯,并且可以很方便的緩存數(shù)據(jù)歪泳。而如果用上面提交的方案來處理前端數(shù)據(jù)萝勤,首先id動(dòng)態(tài)生成數(shù)據(jù)redux是天然支持的,我們得用其提供的方法自行實(shí)現(xiàn)呐伞。
另外敌卓,有很多reducers其實(shí)并沒有做數(shù)據(jù)處理,只是簡單的把數(shù)據(jù)做了轉(zhuǎn)發(fā)伶氢,而獲取數(shù)據(jù)由往往是通過異步的action來實(shí)現(xiàn)的趟径,那這樣的reducer是否有必要存在?
Mobx實(shí)際上是為了解決這樣麻煩的reducer而產(chǎn)生的癣防,直接讓action改動(dòng)數(shù)據(jù)蜗巧,然后用雙向綁定的方式將數(shù)據(jù)直接映射到界面中。用來簡化前端的數(shù)據(jù)流程蕾盯。他和MVVM的不同處在于數(shù)據(jù)是單獨(dú)出來的作為store的存在幕屹。在react組件中綁定store中的數(shù)據(jù),類似于以前打模板的方式刑枝,當(dāng)store變化時(shí)自動(dòng)就會(huì)映射到界面中香嗓,所有的數(shù)據(jù)操作都在action中進(jìn)行。
https://mobxjs.github.io/mobx/
如此装畅,我們就不必在意頁面取什么數(shù)據(jù)了靠娱,store就看成數(shù)據(jù)庫,使用mobx提供的observable.map生成按照id -> value的鍵值對來處理不同id的同種數(shù)據(jù)掠兄。
0x12 最佳實(shí)踐給的靈感
官方文檔給的給出了建議的構(gòu)建store的方式:https://mobxjs.github.io/mobx/best/store.html
Most applications benefit from having at least two stores. One for the UI state and one or more for the domain state.
建議我們至少新建兩個(gè)store(實(shí)際上應(yīng)該是兩種)像云,一個(gè)UI state一個(gè)domain state:
- UI state是指當(dāng)前UI的狀態(tài),比如:窗口尺寸蚂夕、當(dāng)前展示的頁面迅诬、渲染狀態(tài)、網(wǎng)絡(luò)狀態(tài)等等
- Domain state則主要包含頁面所需的各種數(shù)據(jù)(一般是需要從后端獲取的)婿牍。例如:
- 文章詳情(id為索引的數(shù)據(jù)表)
- 首頁feed(只有一個(gè)侈贷,不需要列表)
- 推薦列表(推薦id索引的數(shù)據(jù)表,每一項(xiàng)的內(nèi)容又是一個(gè)文章id的列表)
其新建store的方式也并不和redux一樣等脂,在mobx中俏蛮,一個(gè)store是一個(gè)類撑蚌,而具體的state則是它的實(shí)例。
另外搏屑,所有需要按照id區(qū)分争涌,多處會(huì)用到或者修改的數(shù)據(jù),應(yīng)單獨(dú)抽象成一個(gè)domain state辣恋。某store內(nèi)部自己需要的亮垫,按照id區(qū)分的數(shù)據(jù),可單獨(dú)以map的形式存在某store內(nèi)部伟骨。
在它官方給的例子中饮潦,只有一個(gè)domain state,就是TodoStore携狭,用來存儲(chǔ)todo list和相應(yīng)的操作(這些操作可以聲明成action)如果整個(gè)app中只有一個(gè)todo list的話害晦,那整個(gè)state就是一個(gè)TodoStore的實(shí)例了。
官方代碼略
class TodoStore
這樣抽象下來的話暑中,todo也可以抽象成一個(gè)類,而每個(gè)todo item都是todo的實(shí)例鲫剿,多個(gè)todo存儲(chǔ)在todoStore中鳄逾,也滿足我們對整個(gè)數(shù)據(jù)的抽象。
官方代碼略
class Todo
假設(shè)我們以后要新增查看todo item的詳情(例如:里面有具體計(jì)劃之類的)灵莲。我們也都是對同一個(gè)todo的對象進(jìn)行操作雕凹。而具體我們展現(xiàn)的是那個(gè)todo item的頁面,我們可以放到ui state中政冻。
不知道有沒有比較好理解枚抵,可以留言反饋下,或者實(shí)際操作下Mobx
0x20 效果
還是以上面的例子來說明明场,頁面有:首頁feed汽摹、詳情頁、推薦列表
0x21 創(chuàng)建store
import {
observable, action, extendObservable
} from 'mobx'
// ui state
export const ui = observable({
pendingRequests: 0,
})
// 首頁feed流數(shù)據(jù)
class HomeStore {
@observable feed: string[] = []
@action('獲取feed流') async fetchFeed() {
const data = await requestFromServer()
// 請求接口并且獲得了數(shù)據(jù) data
this.feed = data.list.map(item => {
const id = item.id
if(!detail.has(id))
detail.set(id, new Detail(item))
return id
})
}
}
// 需要是一個(gè)map的store苦锨,比如文章詳情逼泣,推薦列表等等
class mapStore<T> {
@observable data = observable.map<T>()
get(id: string) { return this.data.get(id) }
set(id: string, value: Detail) { this.data.set(id, value) }
has(id: string) { return this.data.has(id) }
}
// 文章詳情
class Detail {
id: string
// ...其他屬性
constructor(item: any) {
extendObservable(this, item)
}
@action('獲取詳情') async fetch() {
const data = await requestFromServer(this.id)
extendObservable(this, data)
}
@action('保存編輯') async save(data) {
extendObservable(this, data)
await submitToServer(data)
await this.fetch()
}
}
// 推薦列表
class Recommend {
@observable id: string = null
@observable list: any[] = []
constructor (id: string) {
this.id = id
this.fetch()
}
@action('獲取推薦列表') async fetch() {
this.list = await requestFromServer()
}
}
export const detail = new mapStore<Detail>()
export const home = new HomeStore
export const recommend = new mapStore<Recommend>()
0x22 頁面取數(shù)據(jù)
就單獨(dú)以一個(gè)首頁為例子吧,現(xiàn)在首頁的feed流中只有id舟舒,而具體數(shù)據(jù)都充detailStore中取
import * as React from 'react'
import { View, Text, ListView } from 'react-native'
import { observer } from 'mobx-react/native'
import { detail, home } from './stores'
const ds = new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 !== r2
})
export const Home = observer((props: any) => {
const list = home.feed.map(id => detail.get(id))
return <ListView
dataSource={ds.cloneWithRows(list)}
renderRow={(item) => <Text>{item.content}</Text>}
/>
})
假設(shè)我們現(xiàn)在進(jìn)入詳情頁拉庶,修改了某個(gè)文章
import { detail } from './stores'
// ...
detail.get(id).save(changedData)
列表頁會(huì)實(shí)時(shí)變動(dòng)。