起因
在使用現(xiàn)成的小程序狀態(tài)管理方案時踩了個小坑箫锤,遂考慮能否在小程序引入前端生態(tài)中較為成熟的狀態(tài)管理方案搁痛。出于易用性和可拓展性的考量,這里選擇使用 mobx逼侦。
MobX 是一個簡單匿辩、可擴展的狀態(tài)管理方案腰耙。關(guān)于 MobX 的介紹與使用請移步 MobX
如果只是想在小程序中使用 mobx,可以直接參考 文檔 食用铲球。
過程
本次設(shè)計與實現(xiàn)除了完成基本的 mobx 與小程序之間的綁定功能挺庞,還希望能遵循幾個要點(痛點):
- 簡潔、靈活睬辐、完善的 API
- 函數(shù)式的實現(xiàn)與調(diào)用
- 完善的類型支持
?? API 設(shè)計
mobx 起源于 react 生態(tài)挠阁,但由于 react 和小程序本身的差異,照搬 mobx-react 中的調(diào)用方式是不可行的溯饵。這里希望能夠盡可能地保留 react 中的調(diào)用風(fēng)格侵俗,同時兼顧小程序本身的特性,其中的取舍也可以自行定奪丰刊。最終確定的 API 格式如下隘谣。
// App.ts
const App = provider<Stores>(stores)
App<AppOptions>({
...
})
// index.ts
inject<Stores>('storeA', 'storeB')(({ storeA, storeB }) =>
observer.page<Data, Custom>({
onLoad() {
this.data.storeA.count
storeA.add()
},
})
)
這樣我們主要暴露三個函數(shù):provider、 observer 和 inject啄巧。provider 用于向全局綁定 stores寻歧,observer 創(chuàng)建頁面或組件,并通過 inject 向 observer 中注入需被監(jiān)聽的 store秩仆。如果對這種調(diào)用方式尚有疑問码泛,可以先看下文。
?? 內(nèi)部實現(xiàn)
整體的實現(xiàn)原理很簡單澄耍。使用 mobx 提供的 autorun 函數(shù)噪珊,autorun 會在其內(nèi)部引用的 store 中 observable 屬性發(fā)生變化時觸發(fā)回調(diào),那么在其回調(diào)中去主動觸發(fā)小程序的狀態(tài)更新即可齐莲。
由于小程序本身沒有暴露 render 相關(guān)的接口痢站,加上 wxml 中只能引用 data 中的數(shù)據(jù),唯一的選擇就是將被監(jiān)聽的數(shù)據(jù)映射至頁面或組件的 data 中选酗,并在 autorun 觸發(fā)時主動調(diào)用 setData 觸發(fā)狀態(tài)更新阵难。
需要做的工作就是在頁面(或組件)聲明時將初始 store 數(shù)據(jù)與 data 一并作為參數(shù)傳入聲明函數(shù)中,頁面掛載時調(diào)用 autorun 函數(shù)以監(jiān)聽數(shù)據(jù)變化并觸發(fā)更新芒填,最后在卸載時銷毀 autorun 釋放監(jiān)聽呜叫。
實現(xiàn)細(xì)節(jié)如下。
provider.ts
provider 用于接收一個 stores 對象并將其綁定到 App 中殿衰,stores 對象即為項目中引用的 stores 的集合怀偷,格式為 { storeA, storeB }
,同時 App() 的調(diào)用可以在 provider 的內(nèi)部進(jìn)行播玖。實現(xiàn)很簡單,將 stores 添加為 App 的參數(shù)屬性即可饭于。
type AppOptions<T> = WechatMiniprogram.App.Options<T>
const provider = <TStores extends AnyObject>(stores: TStores) => <TAppOptions extends AnyObject>(
options: AppOptions<TAppOptions>
) => App({ ...options, stores })
export default provider
observer.ts
observer 是實現(xiàn)監(jiān)聽函數(shù)的核心蜀踏。小程序中的聲明函數(shù)有兩個维蒙,分別為 Page() 和 Component(),對應(yīng)頁面和組件果覆。這里以 Page() 為例颅痊。
observer 的職責(zé)為接收需要被監(jiān)聽的 stores 對象,并將其映射至頁面 data 中局待。同時觀測 stores 中屬性的變化斑响,當(dāng)觀測屬性發(fā)生變化時調(diào)用 setData() 更新頁面的狀態(tài),觸發(fā)視圖更新钳榨。
借助 mobx 提供的 autorun 函數(shù)舰罚,我們可以在 autorun 回調(diào)觸發(fā)時進(jìn)行 setData 操作。
那么思路大致為薛耻,首先將 stores 與 data 合并一同作為 options 傳入 Page 中营罢,并在頁面 onLoad 時調(diào)用 autorun,當(dāng)其回調(diào)函數(shù)觸發(fā)時立即調(diào)用 setData 將狀態(tài)更新至頁面中饼齿。同時需要在頁面 onUnload 時銷毀 autorun饲漾。
observer 調(diào)用后需要返回一個 observe 函數(shù)以接收 observedStores 對象,即需要在當(dāng)前頁面注入并監(jiān)聽的 stores缕溉。observedStores 一般需要由 inject 內(nèi)部根據(jù)外界指定 storeNames 計算并傳入考传,但亦可以在外界直接傳入 store 對象的引用。在 observe 中傳入 observedStores 并調(diào)用會進(jìn)行頁面聲明证鸥。
最終的調(diào)用格式為 observer.page(options)({ storeA, storeB })
實現(xiàn)如下僚楞。
import { autorun, IReactionDisposer } from 'mobx'
import { is, toData } from './utils'
import diff from './diff'
const observer = {
page: <TData extends DataOption, TCustom extends CustomOption>(
options: PageOptions<TData, TCustom>
) => {
let dispose: IReactionDisposer
const { data = {}, onLoad, onUnload } = options
return (observedStores: AnyObject = {}) =>
Page({
...options,
data: { ...data, ...toData(observedStores) },
onLoad(query) {
dispose = autorun(() => {
if (this.data) {
const diffs: AnyObject = diff({ ...this.data, ...toData(observedStores) }, this.data)
this.setData(diffs)
}
})
if (is.fun(onLoad)) onLoad.call(this, query)
},
onUnload() {
if (dispose) dispose()
if (is.fun(onUnload)) onUnload.call(this)
},
})
}
}
export default observer
注:每一次觸發(fā)更新時都將 stores 中全部屬性都更新至 data 中顯然是不可取的,這里使用 diff 對兩次狀態(tài)比對并進(jìn)行最小狀態(tài)更新以優(yōu)化性能敌土。diff 函數(shù)參考 westore 中的實現(xiàn)镜硕,diff 后的結(jié)果可以直接作為 setData 的參數(shù)傳入。
toData 的作用為將 mobx 中的 observable 對象返干,深度拷貝為一個符合小程序 data 格式的 JS 對象兴枯,具體實現(xiàn)可參照源碼中 utils.ts 文件。
Inject.ts
最后確定 inject 的實現(xiàn)思路矩欠。inject 負(fù)責(zé)兩個工作财剖,一是接收被調(diào)用時傳入的 storeNames,即需被監(jiān)聽的 store 的名稱列表癌淮,計算出對應(yīng)的 observedStores 對象躺坟,并將其傳遞給 observer 以供監(jiān)聽。二是將 provider 綁定的 stores 的直接引用通過其返回函數(shù)的調(diào)用傳遞到外界以供使用乳蓄。
結(jié)合上文的調(diào)用來看咪橙,首先 inject('storeA', 'storeB')
接收兩個 storeName,返回一個函數(shù),這個函數(shù)接收另一個函數(shù) createObserver 作為參數(shù)美侦,createObserver 的參數(shù)即是 stores 對象产舞,在這里便得到了 stores 的引用。同時 createObserver 的返回值就是 observer 調(diào)用產(chǎn)生的返回值菠剩。inject 內(nèi)部拿到 observer 返回的函數(shù)后即可在內(nèi)部調(diào)用并將 observedStores 作為參數(shù)傳入以供監(jiān)聽易猫。
最終的調(diào)用格式為 inject('storeA', 'storeB')(({ storeA, storeB }) => observer.page(options)
實現(xiàn)如下。
const mapStores = <TStores extends AnyObject>(names: (keyof TStores)[]) => (source: TStores) => {
const target: TStores = {} as TStores
names.forEach(key => {
if (source && source[key]) {
target[key] = source[key]
}
})
return target
}
const inject = <TStores extends AnyObject>(...storeNames: (keyof TStores)[]) => (
createObserver: (stores: TStores) => (observedStores: AnyObject) => void | string
) => {
const stores = getApp().stores ?? {}
const observedStores = mapStores(storeNames)(stores)
return createObserver(stores)(observedStores)
}
export default inject
這里通過 getApp() 拿到 stores 的引用具壮,將其作為參數(shù)傳遞到 createObserver 中准颓。并將 storeNames 和 stores 通過 mapStores 計算得到監(jiān)聽目標(biāo)對象 observedStores。傳入 observer 產(chǎn)生的 observe 函數(shù)并調(diào)用即完成狀態(tài)注入與頁面聲明棺妓。
結(jié)束
感謝您的閱讀攘已。