構建大型 Mobx 應用的幾個建議

Mobx 與 Redux 類似,都是適用於狀態管理的出色工具。它一樣遵循單向數據流,一樣能與 React 搭檔配合。與 Redux 不一樣的是,它的學習成本更低,框架體系更加完善(好比它自帶異步操做的解決方案,而 Redux 只提供了中間件體系,必須藉助第三方類庫實現)。若是說 Redux 只是繼承了 Flux 的衣鉢的話,那麼 Mobx 則是基於 Flux 的再一次進化javascript

Mobx 是一枚優秀的工具,可是它的學習資料很是有限,即便是官網的文檔也仍是拘泥於 API 的講解而缺少實戰的例子(我的在這裏推薦 MobX Quick Start Guide 這本薄薄的冊子是比官網更好的入門)。前端

從去年下半年開始工做項目中的狀態管理框架逐漸從 Redux 替換爲 Mobx,這些項目中一樣須要處理大量的數據和複雜的交互,因而逐漸在這方面積攢了一些經驗。這些經驗大可能是我我的踩過的坑,這些坑倒不是技術上的難點痛點,而是如何讓程序更易於維護的技巧和模式。但願對那些正在使用 Mobx 作開發的同窗有所幫助。這其中會使用 Redux 的開發模式進行對比,更易於理解。java

這三個建議分別是:react

  • 將一切狀態存儲在 Mobx 中(儘量 schema 定義數據結構)
  • 賦予狀態生命週期(別忘了緩存)
  • 雖然 Mobx 很快,仍是請遵照 Mobx 的性能守則

將一切狀態存儲在 Mobx 中

回憶一下在完整的 Redux 應用中,單個頁面中會存在哪些狀態:git

  • 業務狀態:好比購物車已添加的商品
  • UI狀態:好比提交按鈕是否能夠點擊,是否須要展現錯誤提示
  • 應用級別狀態:應用是處於離線仍是上線狀態,用戶是否登陸

React 組件的狀態屬性來源也同時有好幾個渠道:github

  • 父組件自身的狀態
  • store 的注入
  • store 通過 selector 計算以後注入的

那麼當咱們嘗試追溯或者 debug 某個屬性的狀態來源時,面臨的是 (3 × 3 = )9 種的可能,尤爲是當你在把不一樣的狀態拆分爲不一樣的 reducer 時,這會帶來很是大的困惱redux

因此我我的的第一條件建議是:儘量的將狀態都存儲在同一處 Mobx 中。注意這裏有幾個關鍵詞:api

  • 儘量的:用萬能的二分法進行劃分,咱們老是能把狀態劃分爲「共享的」(好比上面說的「應用級別狀態」)和「非共享的」(好比對應於某個特定業務)。鑑於共享狀態須要被全應用所引用,因此沒法把它分配到某個特定業務的狀態下,而須要獨立出來。
  • 同一處:在設計狀態的時候應該遵循兩條基本的原則:1) 每一個頁面應該有本身的狀態; 2) 每一個被複用的組件應該有本身的狀態;根據以上兩點能夠推斷,React 組件應該是 stateless 的

頁面級別的狀態像是容器,將上面描述的三類的狀態都集中在一塊兒。好比在新用戶註冊的頁面中,用戶填寫的用戶名、密碼和郵箱信息屬於業務狀態,密碼長度不夠給出的錯誤提示屬於應用狀態,註冊按鈕是否能夠被點擊屬於UI狀態。這樣作的緣由不只僅是由於便於狀態的追溯和管理,還由於它們自然就是相關的。例如當註冊信息填寫不完整時,提交按鈕天然是不可被點擊的狀態,而密碼填寫格式不正確時,錯誤信息應該天然出現。也就是說大部分的 UI 狀態和應用狀態是基於業務狀態「計算」或者說「衍生」出來的,在 Mobx 中,咱們能夠藉助autorunreactioncomputed很容易的實現這些須要被計算的狀態,而且及時的更新它們。緩存

Schema 很重要

不知道你會不會有這樣的一個疑問,若是把不一樣性質的狀態集中在一塊兒,當你須要在提交頁面時如何正確的挑選出業務信息而且組裝它們,而且保證提交的業務信息是正確的?這裏我推薦使用 schema 來確保來提早定義業務信息的數據格式。不只僅在 Mobx 應用中,使用 schema 應該是在構建全部大型前端應用貫穿的最佳實踐。隨着應用變得龐大,程序的不一樣組成之間,內部和外部之間的通訊也變得愈來愈複雜。對每一類須要使用的消息或者對象提早定義 schema,有利於確保通訊的正確性,防止傳入不存在的字段,或者傳入字段的類型不正確;同時也具備自解釋的文檔的做用,有利於從此的維護。例如使用 hapijs/joi 類庫定義用戶信息的 schema :數據結構

const Joi = require('joi');

const schema = Joi.object().keys({
    username: Joi.string().alphanum().min(3).max(30).required(),
    password: Joi.string().regex(/^[a-zA-Z0-9]{3,30}$/),
    access_token: [Joi.string(), Joi.number()],
    birthyear: Joi.number().integer().min(1900).max(2013),
    email: Joi.string().email({ minDomainAtoms: 2 })
})
複製代碼

使相似的 schema 類庫很是多,最簡單的使用 ImmutableJS 裏的 Record 類型也能起到約束的做用,這裏就不展開了

另外一方面,一個組件的理想狀態應該是對於它的使用者來講是無感知的,這裏的「無感知」有幾層意思:最基礎的,它的 API 設計應該和原生控件保持一致,感覺不到特殊性;額外的他也不關心你的內部實現,你的內部狀態管理與他無關。因此爲了保證應用架構的一致性,我仍是建議把組件內的全部狀態交由 Mobx 管理,這麼作一樣可以利用到上面所說的各類優點。

最後把狀態都交由 Mobx 管理的結果就是 React 組件都是無狀態的了,便於測試和維護。

值得一提的是,在沒有 Mobx 框架的狀況下,在 Redux 應用裏,我仍是傾向於將狀態都集中在組件的 state 中進行管理,但美中不足的是,state 機制沒有明確的給出基於狀態的衍生機制。然而爲何不放在 reducer 中進行管理呢,這就要聊到我給出的第二條建議了

賦予狀態生命週期

在開發 Redux 的應用中,不知道你有沒有考慮過這樣一個問題,什麼樣的狀態應該放在組件的 state 中?什麼樣的狀態應該放在 store 中? 這個問題實際上是有明確答案的,引用官方文檔裏的話回答這個問題:

Some common rules of thumb for determining what kind of data should be put into Redux:

  • Do other parts of the application care about this data?
  • Do you need to be able to create further derived data based on this original data?
  • Is the same data being used to drive multiple components?
  • Is there value to you in being able to restore this state to a given point in time (ie, time travel debugging)?
  • Do you want to cache the data (ie, use what's in state if it's already there instead of re-requesting it)?
  • Do you want to keep this data consistent while hot-reloading UI components (which may lose their internal state when swapped)?

簡單來講就是,若是這份數據須要被共享,那麼就把它放在 store 中。這也同時暗示了另一件事:放在 store 中的數據是沒有生命週期的,又或者更嚴謹的說,它們的生命週期等同於整個應用的生命週期。

而頁面和組件都是有生命週期的。舉個例子,你須要在某個後臺頁面中持續的錄入商品信息而後點擊提交,而後繼續錄入。雖然是同一路由,同一組件,同一頁面,但每一次的錄入你面對的都是新的頁面實例,這是頁面生命週期的開始,直到你填寫信息而且提交成功,這是頁面生命週期的結束,也意味着這個實例被銷燬。組件更是要面臨這個問題,它甚至可能在同一個頁面上被屢次使用,每一次被使用組件和狀態都須要是全新的實例

可是不管是在 Mobx 中仍是 Redux 中都不提供這樣的機制。或許你經常使用mobx-reactinject函數,它的做用是將經過<Provider />綁定在上下文的 store 注入到組件中,和 redux-react 中的 connect 無異,與聲明週期無關

這裏咱們使用高階組件(High Order Component)解決這個問題。

高階組件解決問題的原理很是簡單,它把 store 實例和組件實例鏈接在一塊兒(封裝在另外一個組件裏),而後讓他們同生同滅

假設咱們的高階組件函數名稱爲withMobx,咱們將withMobx的用法設計以下,與 Redux 的 connect 用法類似:

@withMobx(props => {
  return {
    userStore: new UserStore()
  }
})
@observer
export default class UserForm extends Component {

}
複製代碼

withMobx 的實現也很是簡單,鑑於咱們這裏並非高階組件節目,這裏就不鑑賞高階組件的各類開發模式,你也能夠藉助 acdlite/recompose 建立。這裏直接亮出手寫答案:

export default function withMobx(injectFunction) {
  return function(ComposedComponent) {
    return class extends React.Component {
      constructor(props) {
        super(props);
        this.injectedProps = injectFunction(props);
      }
      render() {
        // const injectedProps = injectFunction(this.props);
        return <ComposedComponent {...this.props} {...this.injectedProps} />; } }; }; } 複製代碼

值得玩味的事情是 this.injectedProps 的計算位置,是放在即將返回的匿名類的構造函數中。但事實上它能夠存在於其餘的位置,好比上述代碼的註釋中,即 render 函數裏。可是這樣可能會致使一些問題的發生,這裏能夠發揮大家的想象力。

本質上這和組件本身的 state 相似,只不過咱們把 state 交給 Mobx 實現,而後經過高階組件把它們粘合在一塊兒

緩存很是重要

在頻繁的建立 store 實例的過程當中會形成數據的浪費。

假設有組件用於選擇某個國家的城市<CitySelector country="" />。你只須要傳遞進這個國家的名稱,它就可以動態的請求該國家下全部的城市名稱以供選擇。可是由於被銷燬的緣故,以前請求的結果並無保存下來,致使下次傳入相同國家時會再次請求相同的數據。因此:

  1. 在每次請求以前,先檢查緩存中是否有須要的數據,若是存在則直接使用
  2. 在請求數據以後(意味着以前沒有緩存),將數據先保存在緩存中

而至於 cache 模塊藉助Map數據類型就可以實現,key 則按照具體的需求設置便可

準守性能規則

雖然從某些方面來講 Mobx 比 Redux 性能更好,可是 Mobx 仍然有罩門的。舉個例子:

class Person extends React.Component {
  constructor(props) {
    super(props)
  }
  render() {
    console.log('Person Render')
    const {name} = this.props
    return <li>{name}</li>
  }
}

@inject("store")
@observer
class App extends React.Component {
  constructor(props) {
    super(props);
    setInterval(this.props.store.updateSomeonesName, 1000 * 1)
  }
  render() {
    console.log('App Render')
    const list = this.props.store.list
    return <ul> {list.map((person) => { return <Person key={person.name} name={person.name} ></Person> })} </ul>
  }
}
複製代碼

updateSomeonesName方法會隨機的更改列表中某個對象的name字段。從實際運行的狀態看,即便只有一個對象的字段發生了更改,整個<App />和其餘未修改的對象的<Person />組件都會從新進行渲染。

改善這個問題的方法之一,就是給Person組件一樣以mobx-react類庫的observer進行「裝飾」:

@observer
class Person extends React.Component {
複製代碼

observer的做用用原文的話說就是:

Function (and decorator) that converts a React component definition, React component class or stand-alone render function into a reactive component, which tracks which observables are used by render and automatically re-renders the component when one of these values changes.

通過「裝飾」以後的組件Person,只有在name發生更改以後纔會進行從新渲染

然而咱們能夠作的更好,用官方的話說就是Dereference values late當須要的時候再對值進行引用

在上面的代碼的例子中,假設咱們不會在Person組件中使用(渲染)name字段,而是經過另外一個名爲PersonName的組件進行渲染,那麼在App裏,Person裏,都不該該訪問name字段。不然會形成無心義的渲染。簡單來講,就是下面 1 和 2 的區別:

@observer
class Person extends React.Component {
  constructor(props) {
    super(props)
  }
  render() {
    // 1:
    return <PersonName person={this.props.person}></PersonName>
    // 2:
    return <PersonName name={this.props.person.name}></PersonName>
  }
}
複製代碼
  • 若是你使用了 1 的寫法,那麼當person.name發生更改時,<Person />組件不會從新渲染,只有 <PersonName /> 會從新渲染
  • 若是你使用了 2 的寫法,那麼當person.name發生更改時,<Person />組件會從新渲染,<PersonName />組件也會從新渲染

固然直接傳遞整個對象到組件中也存在問題,會形成數據的冗餘,給從此的維護者形成困難

結尾

固然還有不少關於開發 Mobx 應用的經驗能夠分享,但以上三條是我我的感觸最深的,也是我我的認爲最重要的。 但願對你們有所幫助


本文也同時發佈在個人知乎前端專欄,歡迎你們關注

相關文章
相關標籤/搜索