在之前第一篇[1]中,主要描述在使用Redux中碰到的兩個問題以及由Mobx最佳實踐中自己的對于組織Store的自己的看法,之后也有嘗試過幾種不同的Mobx的Store的組織方案,在這里和大家分享一下自己的經驗
前言
關于不按照頁面來區分store是這系列文章的前提。第一篇想首先就Store的構建和Action的拆分給出一些建議,后期還會介紹Mobx的后端渲染和持久化、序列化的一些方案。
關于常見的store和action的組織方案
在第一篇里,一個簡單的瀑布流+詳情的頁面中,我將獲取詳情的action直接放到了詳情的對象里(store)。
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()
}
}
并且在大多數的事例的Mobx的項目中,都是這樣組織action和store的,例如:
這種組織方式的好處是,在action和store一一對應的系統中,我們可以很容易找到操作該store的所有action,代碼結構比較直觀。
遇到的問題
首先說會遇到的問題:store實例與實例之間過于耦合了,在上面首頁feed流(HomeStore)的store對象中的action里,我們會需要操作一個詳情map的store的實例。同理,推薦列表的store也一樣。
如此,整個store和action就會被耦合成一個整體,當我們需要時,我們只能夠一次性生成整個store。當我們要改動任意一個,可能就會觸發這個整體的問題,不夠靈活。
所以,store和action應當是分離的,只要對外提供的接口相同,內部實現可以隨意變化。并且不同store之間的數據應當也是互相分離的,不會有實例間的互相引用,不同的action類別也可以拆分出來,對單個或者多個state(store的實例)進行操作。
重新組織action和store
首先明確store和action的概念,store是指在應用中唯一存儲數據的地方,而action則是所有觸發store數據變化的地方(在Mobx中沒有reducer和dispatch的概念,action直接觸發store改動并且同步的響應在頁面中)。
根據這個概念,將action拆分出來。
還是以上一篇文章的例子來說:
一個首頁(文章的feed流)、各個文章詳情頁、推薦列表頁。
Stores
Store中只存數據
export class HomeStore {
@observable feed: string[] = []
}
class MapStore<T> {
@observable data = asMap<T>({})
// 獲取,可以在該方法中做各種容錯
get(id: string) { return this.data.get(id) }
// 設置
set(id: string, value: T) { this.data.set(id, value) }
// 判斷
has(id: string) { return this.data.has(id) }
// 合并數據,可以在該方法中做各種容錯
merge(id: string, value: T) { /*...*/ }
}
export class Detail {
id: string = null
@observable title: string = null
// ...other properties
}
export class DetailStore extends MapStore<Detail> {}
export class Recommend {
id: string = null
@observable list: any[] = []
}
export class RecommendStore extends MapStore<Recommend> {}
Actions
只在Action中操作數據
export HomeActions {
private home: HomeStore
private details: DetailStore
constructor({ home, details } as any) {
this.home = home
this.details = details
}
@action async fetch(pn: number = 1) {
this.home.feed = await fetch(url).then(res => res.json()).map(item => {
this.details.merge(item.id, item)
return item.id
})
}
}
export DetailActions {
private details: DetailStore
constructor({ details } as any) {
this.details = details
}
@action async fetch(id: string) {
this.details.merge(id, await fetch(url))
}
@action async save(data: any) {
/* ... */
}
}
export RecommendActions {
private recommend: RecommendStore
private details: DetailStore
constructor({ home, details } as any) {
this.home = home
this.details = details
}
@action async fetch(pn: number = 1) {
this.recommend.list = await fetch(url).then(res => res.json()).map(item => {
this.details.merge(item.id, item)
return item.id
})
}
}
Components
組建或者頁面中的store都是由注入的方式提供的。
import * as React from 'react'
import { observer } from 'mobx-react/native'
@observer(['home', 'details'])
export class Home
extends React.Component<{ home: HomeStore, details: DetailStore}, {}> {
}
效果
實際上在我們的stores和actions中,我們只是聲明了他們,而并沒有生成實例,當我們的app啟動時,我們首先需要新建store和action。
import {
HomeStore,
DetailStore,
RecommendStore
} from './stores'
import {
HomeActions,
DetailActions,
RecommendActions
} from './store'
const mobxStates = {
home: new HomeStore,
details: new DetailStore,
recommends: new RecommendStore,
}
const actions = {
home: new HomeActions(mobxStates),
detail: new DetailActions(mobxStates),
recommend: new RecommendActions(mobxStates)
}
export function App(props: any) {
return <Provider
{...mobxStates}
actions={actions}
>{routes}</Provider>
}
還是以一個首頁為例子吧
import * as React from 'react'
import { View, Text, ListView } from 'react-native'
import { observer } from 'mobx-react/native'
const ds = new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 !== r2
})
interface HomeProps {
home: HomeStore
details: DetailStore
}
@observer['home', 'details']
export class Home extends React.Component<HomeProps, {}> {
render () {
const { home, details } = this.props
const list = home.feed.map(id => details.get(id))
return <ListView
dataSource={ds.cloneWithRows(list)}
renderRow={(item) => <Text>{item.content}</Text>}
/>
}
}