如何組織Mobx/Redux中的Store

讀Mobx官方文檔中的最佳實踐有感,并結合一些自己項目經驗。總結一下遇到的坑,和預備的解決方案。

用了半年的redux,依稀記得剛開始用Redux的時候到處查找文檔想知道組織state結構的最佳實踐,因為在todo list的demo中用list代表todos,并不知道這個狀態下的todos是純數據,還是頁面中展示數據的緩存。意思是,不太明白:

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

最后搜到了作者的一句話,大概就是,隨便你怎么用,用出花來都行,鼓勵大家自主創新。所以在前期寫頁面時我也主要使用的一種比較流行的方式,就是上面的方式1,store為頁面服務。

0x00 store為頁面服務,以及遇到的坑

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

  1. 看著設計圖想想所需要的數據
  2. 按照這些數據構建每個頁面的state樹節點
  3. 寫可能的action處理每一個用戶行為
  4. 構建好用戶請求用到的異步action
  5. 寫reducer處理請求到的數據
  6. 接入現實數據
  7. 測試

完美,action寫法統一,管理方便,reducer寫法統一,管理方便,數據流下來,刷新不用自己處理(setState)。但是,最終發現,自己的項目,單頁面這樣寫寫很不錯,但是并不適合寫一個完整的app或者前后端同構的web app。(遇到啥問題下面說,先看看這種store的組織方式怎么做)

0x01 什么是store為頁面服務

即根節點往下是各個頁面,頁面中的組件對應著state子樹中的節點。

在React中,一個頁面是由若干個嵌套的組件構成,每個組件都有相應的數據輸入,最終這些數據輸入可以反映成一個樹狀結構,最后我們直觀的使用這個樹狀結構到state樹上,就如下圖所示。

page based state.png

0x02 遇到了什么問題

  1. 重復頁面倒退
  2. 數據同步

重復頁面倒退是指如下這種情況:

wenti1.png

假設這種情況:在React Native和Redux構建的一個App中,我們從首頁feed流進入某個id為1的文章的詳情頁,從詳情頁進入了某個推薦列表,然后又從這個推薦列表進入了另一個id為2的文章的詳情頁。

現在問題來了,無論是id為1的詳情頁,還是id為2的詳情頁,它們用的都是同一個state樹的分支(可能叫state.detailPage),所以當頁面瀏覽到id為2的詳情頁的時候,數據請求完畢之后設置到state.detailPage分支,id為1的數據就被替換掉了。如果這個時候要回退到id為1的詳情頁,就必須得重新獲取數據。否則就還是id為2的數據。

在web應用里,用戶比較習慣在白頁中重新加載數據(當回退的時候),可是在app中,這是不符合習慣的,而且在app中頁面發生了變化,只是向一個頁面的堆棧中壓入一個新的頁面,之前的頁面并不會釋放調,我們習慣性將獲取頁面數據放入componentWillMount或者constructor或者任一個生命周期函數中,都不會執行(也就是說不會重新獲取數據)。

第二個問題是數據同步

還記得flux的引入是為了解決什么問題嗎?

facebook右上角的消息提醒總是莫名其妙的出現,因為同是消息提醒的數據在不同的數據源中可能重復存有多份。使用flux可以讓數據的源頭只有一個,所有的展示都是通過一個源頭流下來的數據產生的,某個頁面中我讀取了當前的所有消息,發送一個action告訴數據源,現在未讀消息變為0了,然后所有地方的未讀消息,受這個數據的改變都變成0了。

然而如果采用頁面即數據的這種方案,即使數據源只有一個,但是同一種數據也有可能在多個地方存儲過。例如上面的例子:列表頁中可能有某個文章的標題(在列表頁樹分支的某個節點上),這個文章的詳情頁也有這個文章的標題,假設我在某個地方(假設是詳情頁)更改了這個樹分支上的標題,其他樹分支上標題并不會改變(例如列表頁,因為是不同的數據),依然沒有解決flux根本想解決的問題。

0x10 尋求解決方案

兩個問題都有其各自單獨的解決方案。

要解決相同頁面會退的問題,就必須區分id為1和id為2的數據,例如,我們可以在state.detailPage[1]state.detailPage[2]中分別存放id為1的詳情頁的數據和id為2的詳情頁的數據,然而redux的combine并不能動態的增加分支,分之節點都是事先預置好的,要實現這種,我們只能自己寫中間件,或者自己實現插入分支(我是這樣做的)。

如果要解決數據同步的問題,有兩種方案:第一種,使用事件機制,所有要跟著變動的地方,建一個變更的事件,當變更的時候觸發這個事件,讓所有相關的地方發生改變;第二種,不管是列表的標題還是詳情的標題,都只存一次,存在一個地方,那么不同地方取的都是同一個數據,就可以自然同步了。

方案一讓人感覺redux并沒有幫上什么忙,第二種方法不太好實現,在實際中,我們混合使用了兩種方案。

這兩個問題看下來,讓人第一感受是:數據(按照ID區分,會同時出現在多處的那種)和頁面需要分離,數據以表的形式存在,并且只存一次。

如果解決這個問題呢?還是以上面可能的app為例:

我們建立一個叫文章的state下的子樹,其是一個id, value的map,用id區分(當然,得自己實現),當然也會有一個叫首頁的子樹,但是首頁只有一個,所以它可以正常來,但是首頁的feed流list只是一個id的list,其并不包含具體數據,具體數據都在叫文章的子樹里。

在reducer獲取的時候,先將列表接口獲取的已有的數據賦給文章map對應id的各個文章,然后向列表頁(首頁feed)返回一個id的列表。列表頁要取詳情,就去文章的map中自己取。到了詳情頁,向后端接口獲取詳情數據,再將文章map中,讓正在訪問的這條的信息更新的更完備。

0x11 另一個構建store的方法 - Mobx

其實總得來說flux應該是一套從后端到前端一路向下的數據解決方案,而不應該僅僅只是用在react的前端這塊的數據處理,要是這樣的話,可能它方便之處并不在于單一數據源。而應該在于前端開發和調試的時候,能規整代碼結構,讓數據可追溯,并且可以很方便的緩存數據。而如果用上面提交的方案來處理前端數據,首先id動態生成數據redux是天然支持的,我們得用其提供的方法自行實現。

另外,有很多reducers其實并沒有做數據處理,只是簡單的把數據做了轉發,而獲取數據由往往是通過異步的action來實現的,那這樣的reducer是否有必要存在?

Mobx實際上是為了解決這樣麻煩的reducer而產生的,直接讓action改動數據,然后用雙向綁定的方式將數據直接映射到界面中。用來簡化前端的數據流程。他和MVVM的不同處在于數據是單獨出來的作為store的存在。在react組件中綁定store中的數據,類似于以前打模板的方式,當store變化時自動就會映射到界面中,所有的數據操作都在action中進行。

https://mobxjs.github.io/mobx/

flow.png

如此,我們就不必在意頁面取什么數據了,store就看成數據庫,使用mobx提供的observable.map生成按照id -> value的鍵值對來處理不同id的同種數據。

0x12 最佳實踐給的靈感

官方文檔給的給出了建議的構建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.

建議我們至少新建兩個store(實際上應該是兩種),一個UI state一個domain state:

  • UI state是指當前UI的狀態,比如:窗口尺寸、當前展示的頁面、渲染狀態、網絡狀態等等
  • Domain state則主要包含頁面所需的各種數據(一般是需要從后端獲取的)。例如:
    • 文章詳情(id為索引的數據表)
    • 首頁feed(只有一個,不需要列表)
    • 推薦列表(推薦id索引的數據表,每一項的內容又是一個文章id的列表)

其新建store的方式也并不和redux一樣,在mobx中,一個store是一個類,而具體的state則是它的實例。

另外,所有需要按照id區分,多處會用到或者修改的數據,應單獨抽象成一個domain state。某store內部自己需要的,按照id區分的數據,可單獨以map的形式存在某store內部。

在它官方給的例子中,只有一個domain state,就是TodoStore,用來存儲todo list和相應的操作(這些操作可以聲明成action)如果整個app中只有一個todo list的話,那整個state就是一個TodoStore的實例了。

官方代碼略class TodoStore

這樣抽象下來的話,todo也可以抽象成一個類,而每個todo item都是todo的實例,多個todo存儲在todoStore中,也滿足我們對整個數據的抽象。

官方代碼略class Todo

假設我們以后要新增查看todo item的詳情(例如:里面有具體計劃之類的)。我們也都是對同一個todo的對象進行操作。而具體我們展現的是那個todo item的頁面,我們可以放到ui state中。

不知道有沒有比較好理解,可以留言反饋下,或者實際操作下Mobx

0x20 效果

還是以上面的例子來說明,頁面有:首頁feed、詳情頁、推薦列表

0x21 創建store

import {
    observable, action, extendObservable
} from 'mobx'

// ui state
export const ui = observable({
    pendingRequests: 0,
})

// 首頁feed流數據
class HomeStore {
    @observable feed: string[] = []
    @action('獲取feed流') async fetchFeed() {
        const data = await requestFromServer()
        // 請求接口并且獲得了數據 data
        this.feed = data.list.map(item => {
            const id = item.id
            if(!detail.has(id))
                detail.set(id, new Detail(item))
            return id
        })
    }
}

// 需要是一個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 頁面取數據

就單獨以一個首頁為例子吧,現在首頁的feed流中只有id,而具體數據都充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>}
    />
})

假設我們現在進入詳情頁,修改了某個文章

import { detail } from './stores'

// ...

detail.get(id).save(changedData)

列表頁會實時變動。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,606評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,582評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,540評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,028評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,801評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,223評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,294評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,442評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,976評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,800評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,996評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,543評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,233評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,926評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,702評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容