如何組織Mobx/Redux中的Store

讀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ù)的緩存趣竣。意思是遥缕,不太明白:

  1. 是store為頁面服務(wù)(將頁面中需要的可能會(huì)變更的數(shù)據(jù)另外存儲(chǔ)起來)
  2. 還是數(shù)據(jù)就是數(shù)據(jù)单匣,頁面只是取得數(shù)據(jù)(類似于數(shù)據(jù)庫)

最后搜到了作者的一句話户秤,大概就是,隨便你怎么用泡徙,用出花來都行膜蠢,鼓勵(lì)大家自主創(chuàng)新。所以在前期寫頁面時(shí)我也主要使用的一種比較流行的方式糖荒,就是上面的方式1模捂,store為頁面服務(wù)狂男。

0x00 store為頁面服務(wù)岖食,以及遇到的坑

在前期使用這種方案真心不要太爽:

  1. 看著設(shè)計(jì)圖想想所需要的數(shù)據(jù)
  2. 按照這些數(shù)據(jù)構(gòu)建每個(gè)頁面的state樹節(jié)點(diǎn)
  3. 寫可能的action處理每一個(gè)用戶行為
  4. 構(gòu)建好用戶請求用到的異步action
  5. 寫reducer處理請求到的數(shù)據(jù)
  6. 接入現(xiàn)實(shí)數(shù)據(jù)
  7. 測試

完美泡垃,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樹上其监,就如下圖所示抖苦。

page based state.png

0x02 遇到了什么問題

  1. 重復(fù)頁面倒退
  2. 數(shù)據(jù)同步

重復(fù)頁面倒退是指如下這種情況:

wenti1.png

假設(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/

flow.png

如此装畅,我們就不必在意頁面取什么數(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)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末秃励,一起剝皮案震驚了整個(gè)濱河市氏仗,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌夺鲜,老刑警劉巖皆尔,帶你破解...
    沈念sama閱讀 216,919評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件呐舔,死亡現(xiàn)場離奇詭異,居然都是意外死亡床佳,警方通過查閱死者的電腦和手機(jī)滋早,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,567評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來砌们,“玉大人杆麸,你說我怎么就攤上這事±烁校” “怎么了昔头?”我有些...
    開封第一講書人閱讀 163,316評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長影兽。 經(jīng)常有香客問我揭斧,道長,這世上最難降的妖魔是什么峻堰? 我笑而不...
    開封第一講書人閱讀 58,294評(píng)論 1 292
  • 正文 為了忘掉前任讹开,我火速辦了婚禮,結(jié)果婚禮上捐名,老公的妹妹穿的比我還像新娘旦万。我一直安慰自己,他們只是感情好镶蹋,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,318評(píng)論 6 390
  • 文/花漫 我一把揭開白布成艘。 她就那樣靜靜地躺著,像睡著了一般贺归。 火紅的嫁衣襯著肌膚如雪淆两。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,245評(píng)論 1 299
  • 那天拂酣,我揣著相機(jī)與錄音秋冰,去河邊找鬼。 笑死踱葛,一個(gè)胖子當(dāng)著我的面吹牛丹莲,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播尸诽,決...
    沈念sama閱讀 40,120評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼甥材,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了性含?” 一聲冷哼從身側(cè)響起洲赵,我...
    開封第一講書人閱讀 38,964評(píng)論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后叠萍,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體芝发,經(jīng)...
    沈念sama閱讀 45,376評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,592評(píng)論 2 333
  • 正文 我和宋清朗相戀三年苛谷,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了辅鲸。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,764評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡腹殿,死狀恐怖独悴,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情锣尉,我是刑警寧澤刻炒,帶...
    沈念sama閱讀 35,460評(píng)論 5 344
  • 正文 年R本政府宣布,位于F島的核電站自沧,受9級(jí)特大地震影響坟奥,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜拇厢,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,070評(píng)論 3 327
  • 文/蒙蒙 一爱谁、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧孝偎,春花似錦管行、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,697評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽荡陷。三九已至雨效,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間废赞,已是汗流浹背徽龟。 一陣腳步聲響...
    開封第一講書人閱讀 32,846評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留唉地,地道東北人据悔。 一個(gè)月前我還...
    沈念sama閱讀 47,819評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像耘沼,于是被迫代替她去往敵國和親极颓。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,665評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容