溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

如何為MobX開啟Time-Travelling引擎

發(fā)布時(shí)間:2022-03-25 09:50:56 來源:億速云 閱讀:279 作者:iii 欄目:web開發(fā)

本篇內(nèi)容主要講解“如何為MobX開啟Time-Travelling引擎”,感興趣的朋友不妨來看看。本文介紹的方法操作簡(jiǎn)單快捷,實(shí)用性強(qiáng)。下面就讓小編來帶大家學(xué)習(xí)“如何為MobX開啟Time-Travelling引擎”吧!

前言

了解 mobx-state-tree 的同學(xué)應(yīng)該知道,作為 MobX 官方提供的狀態(tài)模型構(gòu)建庫(kù),MST 提供了很多諸如 time travel、hot reload 及 redux-devtools支持 等很有用的特性。但 MST 的問題在于過于 opinioned,使用它們之前必須接受它們的一整套的價(jià)值觀(就跟 redux 一樣)。

我們先來簡(jiǎn)單看一下 MST 中如何定義 Model 的:

import { types } from "mobx-state-tree"

const Todo = types.model("Todo", {

    title: types.string,

    done: false

}).actions(self => ({

    toggle() {

        self.done = !self.done

    }

}))

const Store = types.model("Store", {

    todos: types.array(Todo)

})

老實(shí)講我第一次看到這段代碼時(shí)內(nèi)心是拒絕的,主觀實(shí)在是太強(qiáng)了,最重要的是,這一頓操作太反直覺了。直覺上我們使用 MobX 定義模型應(yīng)該是這樣一個(gè)姿勢(shì):

import { observable, action } from 'mobx'

class Todo {

    title: string;

    @observable done = false;

    @action

    toggle() {

        this.done = !this.done;

    }

}

class Store {

    todos: Todo[]

}

用 class-based 的方式定義 Model 對(duì)開發(fā)者而言顯然更直觀更純粹,而 MST 這種“主觀”的方式則有些反直覺,這對(duì)于項(xiàng)目的可維護(hù)性并不友好(class-based 方式只要了解最基本的 OOP 的人就能看懂)。但是相應(yīng)的,MST 提供的諸如 time travel 等能力確實(shí)又很吸引人,那有沒有一種方式可以實(shí)現(xiàn)既能舒服的用常規(guī)方式寫 MobX 又能享受 MST 同等的特性呢?

相對(duì)于 MobX 的多 store 和 class-method-based action 這種序列化不友好的范式而言,Redux 對(duì) time travel/action replay 這類特性支持起來顯然要容易的多(但相應(yīng)的應(yīng)用代碼也要繁瑣的多)。但是只要我們解決了兩個(gè)問題,MobX 的 time travel/action replay 支持問題就會(huì)迎刃而解:

收集到應(yīng)用的所有 store 并對(duì)其做 reactive 激活,在變化時(shí)手動(dòng)序列化(snapshot)。完成 store -> reactive store collection -> snapshot(json) 過程。

將收集到的 store 實(shí)例及各類 mutation(action) 做標(biāo)識(shí)并做好關(guān)系映射。完成 snapshot(json) -> class-based store 的逆向過程。

針對(duì)這兩個(gè)問題,mmlpx 給出了相應(yīng)的解決方案:

DI + reactive container + snapshot (收集 store 并響應(yīng) store 變化,生成序列化 snapshot)

ts-plugin-mmlpx + hydrate (給 store 及 aciton 做標(biāo)識(shí),將序列化數(shù)據(jù)注水成帶狀態(tài)的 store 實(shí)例)

下面我們具體介紹一下 mmlpx 是如何基于 snapshot 給出了這兩個(gè)解決方案。

Snapshot 需要的基本能力

上文提到,要想為 MobX 治下的應(yīng)用狀態(tài)提供 snapshot 能力,我們需要解決以下幾個(gè)問題:

收集應(yīng)用的所有 store

MobX 本身在應(yīng)用組織上是弱主張的,并不限制應(yīng)用如何組織狀態(tài) store、遵循單一 store(如redux) 還是多 store 范式,但由于 MobX 本身是 OOP 向,在實(shí)踐中我們通常是采用 MVVM 模式 中的行為準(zhǔn)則定義我們的 Domain Model 和 UI-Related Model(如何區(qū)別這兩類的模型可以看 MVVM 相關(guān)的文章或 MobX 官方最佳實(shí)踐,這里不再贅述)。這就導(dǎo)致在使用 MobX 的過程中,我們默認(rèn)是遵循多 store 范式的。那么如果我們想把應(yīng)用的所有的 store 管理起來應(yīng)該這么做呢?

在 OOP 世界觀里,想管理所有 class 的實(shí)例,我們自然需要一個(gè)集中存儲(chǔ)容器,而這個(gè)容器通常很容易就會(huì)聯(lián)想到 IOC Container (控制反轉(zhuǎn)容器)。DI(依賴注入) 作為最常見的一種 IOC 實(shí)現(xiàn),能很好的替代之前手動(dòng)實(shí)例化 MobX Store 的方式。有了 DI 之后我們引用一個(gè) store 的方式就變成這樣了:

import { inject } from 'mmlpx'

import UserStore from './UserStore'

class AppViewModel {

    @inject() userStore: UserStore

    loadUsers() {

        this.userStore.loadUser()

    }

}

之后,我們能很容易地從 IOC 容器中獲取通過依賴注入方式實(shí)例化的所有 store 實(shí)例。這樣收集應(yīng)用所有 store 的問題就解決了。

更多 DI 用法看這里 mmlpx di system

響應(yīng)所有 store 的狀態(tài)變化

獲取到所有 store 實(shí)例后,下一步就是如何監(jiān)聽這些 store 中定義的狀態(tài)的變化。

如果在應(yīng)用初始化完成后,應(yīng)用內(nèi)的所有 store 都已實(shí)例完成,那么我們監(jiān)聽整個(gè)應(yīng)用的變化就會(huì)相對(duì)容易。但通常在一個(gè) DI 系統(tǒng)中,這種實(shí)例化動(dòng)作是 lazy 的,即只有當(dāng)某一 Store 被真正使用時(shí)才會(huì)被實(shí)例化,其狀態(tài)才會(huì)被初始化。這就意味著,在我們開啟快照功能的那一刻起,IOC 容器就應(yīng)該被轉(zhuǎn)換成 reactive 的,從而能對(duì)新加入管理的 store 及 store 里定義的狀態(tài)實(shí)行自動(dòng)綁定監(jiān)聽行為。

這時(shí)我們可以通過在 onSnapshot 時(shí)獲取到當(dāng)前 IOC Container,將當(dāng)前收集的 stores 全部 dump 出來,然后基于 MobX ObservableMap 構(gòu)建一個(gè)新的 Container,同時(shí) load 進(jìn)之前的所有的 store,最后對(duì) store 里定義的數(shù)據(jù)做遞歸遍歷同時(shí)使用 reaction 做 track dependencies,這樣我們就能對(duì)容器本身(Store 加入/銷毀)及 store 的狀態(tài)變化做出響應(yīng)了。如果當(dāng)變化觸發(fā) reaction 時(shí),我們對(duì)當(dāng)前應(yīng)用狀態(tài)做手動(dòng)序列化即可得到當(dāng)前應(yīng)用快照。

具體實(shí)現(xiàn)可以看這里:mmlpx onSnapshot

從 Snapshot 中喚醒應(yīng)用

通常我們拿到應(yīng)用的快照數(shù)據(jù)后會(huì)做持久化,以確保應(yīng)用在下次進(jìn)入時(shí)能直接恢復(fù)到退出時(shí)的狀態(tài) ── 或者我們要實(shí)現(xiàn)一個(gè)常見的 redo/undo 功能。

在 Redux 體系下這個(gè)事情做起來相對(duì)容易,因?yàn)楸旧頎顟B(tài)在定義階段就是 plain object 且序列化友好的。但這并不意味著在序列化不友好的 MobX 體系里不能實(shí)現(xiàn)從 Snapshot 中喚醒應(yīng)用。

想要順利地 resume from snapshot,我們得先達(dá)成這兩個(gè)條件:

給每個(gè) Store 加上唯一標(biāo)識(shí)

如果我們想讓序列化之后的快照數(shù)據(jù)順利恢復(fù)到各自的 Store 上,我們必須給每一個(gè) Store 一個(gè)唯一標(biāo)識(shí),這樣 IOC 容器才能通過這個(gè) id 將每一層數(shù)據(jù)與其原始 Store 關(guān)聯(lián)起來。

在 mmlpx 方案下,我們可以通過 @Store 和 @ViewModel 裝飾器將應(yīng)用的 global state 和 local state 標(biāo)記起來,同時(shí)給對(duì)應(yīng)的模型 class 一個(gè) id:

@Store('UserStore')

class UserStore {}

但是很顯然,手動(dòng)給 Store 命名的做法很愚蠢且易出錯(cuò),你必須確保各自的命名空間不重疊(沒錯(cuò) redux 就是這么做的[攤手])。

好在這個(gè)事情有 ts-plugin-mmlpx 來幫你自動(dòng)完成。我們?cè)诙x Store 的時(shí)候只需要這么寫:

@Store

class UserStore {}

經(jīng)過插件轉(zhuǎn)換后就變成:

@Store('UserStore.ts/UserStore')

class UserStore {}

通過 fileName + className 的組合通常就可以確保 Store 命名空間的唯一性。更多插件使用信息請(qǐng)關(guān)注 ts-plugin-mmlpx 項(xiàng)目主頁(yè) .

Hyration

從序列化的快照狀態(tài)中激活應(yīng)用的 reactive 系統(tǒng),從靜態(tài)恢復(fù)到動(dòng)態(tài)這個(gè)逆向過程,跟 SSR 中的 hydration 非常相似。實(shí)際上這也是在 MobX 中實(shí)現(xiàn) Time Travelling 最難處理的一步。不同于 redux 和 vuex 這類 Flux-inspired 庫(kù),MobX 中狀態(tài)通常是基于 class 這種充血模型定義的,我們?cè)诮o模型脫水再重新注水之后,還必須確保無(wú)法被序列化的那些行為定義(action method)依然能正確的與模型上下文綁定起來。單單重新綁定行為還沒完,我們還得確保反序列化之后數(shù)據(jù)的 mobx 修飾也是跟原來保持一致的。比如我之前用 observable.ref 、observable.shallow 及 ObservableMap 這類有特殊行為的數(shù)據(jù)裝飾,在重注水之后必須還能保持原始的特性不變,尤其是 ObservableMap 這類非 object Array 的不可直接序列化的數(shù)據(jù),我們都得想辦法能讓他們重新激活回復(fù)原狀。

好在我們整個(gè)方案的基石是 DI 系統(tǒng),這就給我們?cè)谡{(diào)用方請(qǐng)求獲取依賴時(shí)提供了“做手腳”的可能。我們只需要在依賴被 get 時(shí)判斷其是否由從序列化數(shù)據(jù)填充而來的,即 IOC 容器中保存的 Store 實(shí)例并非原始類型的實(shí)例,這時(shí)候便開啟 hydrate 動(dòng)作,然后給調(diào)用方返回注水之后的 hydration 對(duì)象。激活的過程也很簡(jiǎn)單,由于我們 inject 時(shí)上下文中是有 store 的類型(Constructor)的,所以我們只要重新初始化一個(gè)新的空白 store 實(shí)例之后,使用序列化數(shù)據(jù)對(duì)其進(jìn)行填充即可。好在 MobX 只有三種數(shù)據(jù)類型,object、array 和 map,我們只需要簡(jiǎn)單的對(duì)不同類型做一下處理就能完成 hydrate:

if (!(instance instanceof Host)) {

    const real: any = new Host(...args);

    // awake the reactive system of the model

    Object.keys(instance).forEach((key: string) => {

        if (real[key] instanceof ObservableMap) {

            const { name, enhancer } = real[key];

            runInAction(() => real[key] = new ObservableMap((instance as any)[key], enhancer, name));

        } else {

            runInAction(() => real[key] = (instance as any)[key]);

        }

    });

    return real as T;

}

hydrate 完整代碼可以看這里:hyrate

應(yīng)用場(chǎng)景

相較于 MST 的快照能力(MST 只能對(duì)某一 Store 做快照,而不能對(duì)整個(gè)應(yīng)用快照),基于 mmlpx 方案在實(shí)現(xiàn)基于 Snapshot 衍生的功能時(shí)變得更加簡(jiǎn)單:

Time Travelling

Time Travelling 功能在實(shí)際開發(fā)中有兩種應(yīng)用場(chǎng)景,一種是 redo/undo,一種是 redux-devtools 之類提供的應(yīng)用 replay 功能。

在搭載 mmlpx 之后 MobX 實(shí)現(xiàn) redo/undo 就變得很簡(jiǎn)單,這里不再貼代碼(其實(shí)就是 onSnapshot 和 applySnapshot 兩個(gè) api),有興趣的同學(xué)可以查看 mmlpx todomvc demo (就是文章開頭貼的 gif 效果) 和 mmlpx 項(xiàng)目主頁(yè)。

類似 redux-devtools 的功能實(shí)現(xiàn)起來相對(duì)麻煩一點(diǎn)(其實(shí)也很簡(jiǎn)單),因?yàn)槲覀円雽?shí)現(xiàn)對(duì)每一個(gè) action 做 replay,前提條件是每個(gè) action 都有一個(gè)唯一標(biāo)識(shí)。redux 里的做法是通過手動(dòng)編寫具備不同命名空間的 action_types 來實(shí)現(xiàn),這太繁瑣了(參考Redux數(shù)據(jù)流管理架構(gòu)有什么致命缺陷,未來會(huì)如何改進(jìn)?)。好在我們有 ts-plugin-mmlpx 可以幫我們自動(dòng)的幫我們給 action 起名(原理同自動(dòng)給 store 起名)。解決掉這個(gè)麻煩之后,我們只需要在 onSnapshot 的同時(shí)記錄每個(gè) action,就能在 mobx 里面輕松的使用 redux-devtool 的功能了。

SSR

我們知道,React 或 Vue 在做 SSR 時(shí),都是通過在 window 上掛載全局變量的方式將預(yù)取數(shù)據(jù)傳遞到客戶端的,但通常官方示例都是基于 Redux 或 Vuex 來做的,MobX 在此之前想實(shí)現(xiàn)客戶端激活還是有些事情要解決的。現(xiàn)在有了 mmlpx 的幫助,我們只需要在應(yīng)用啟動(dòng)之前,使用傳遞過來的預(yù)取數(shù)據(jù)在客戶端應(yīng)用快照即可基于 MobX 實(shí)現(xiàn)客戶端狀態(tài)激活:

import { applySnapshot } from 'mmlpx'

if (window.__PRELOADED_STATE__) {

    applySnapshot(window.__PRELOADED_STATE__)

}

應(yīng)用 crash 監(jiān)控

這個(gè)只要使用的狀態(tài)管理庫(kù)具備對(duì)任一時(shí)間做完整的應(yīng)用快照,同時(shí)能從快照數(shù)據(jù)激活狀態(tài)關(guān)系的能力就能實(shí)現(xiàn)。即檢查到應(yīng)用 crash 時(shí)按下快門,將快照數(shù)據(jù)上傳云端,最后在云端平臺(tái)通過快照數(shù)據(jù)還原現(xiàn)場(chǎng)即可。如果我們上傳的快照數(shù)據(jù)還包括用戶前幾次的操作棧,那么在監(jiān)控平臺(tái)對(duì)用戶操作做 replay 也不成問題。

到此,相信大家對(duì)“如何為MobX開啟Time-Travelling引擎”有了更深的了解,不妨來實(shí)際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI