摘要:將收集到的實例及各類做標識并做好關系映射。但這并不意味著在序列化不友好的體系里不能實現從中喚醒應用。最后作為一個多范式的信徒,在一出現便取代了我心中在前端狀態管理領域的地位。
原文鏈接
注意:本文并非 mobx-state-tree 使用指南,事實上全篇都與 MST(mobx-state-tree) 無關。
前言了解 mobx-state-tree 的同學應該知道,作為 MobX 官方提供的狀態模型構建庫,MST 提供了很多諸如 time travel、hot reload 及 redux-devtools支持 等很有用的特性。但 MST 的問題在于過于 opinioned,使用它們之前必須接受它們的一整套的價值觀(就跟 redux 一樣)。
我們先來簡單看一下 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) })
老實講我第一次看到這段代碼時內心是拒絕的,主觀實在是太強了,最重要的是,這一頓操作太反直覺了。直覺上我們使用 MobX 定義模型應該是這樣一個姿勢:
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 對開發者而言顯然更直觀更純粹,而 MST 這種“主觀”的方式則有些反直覺,這對于項目的可維護性并不友好(class-based 方式只要了解最基本的 OOP 的人就能看懂)。但是相應的,MST 提供的諸如 time travel 等能力確實又很吸引人,那有沒有一種方式可以實現既能舒服的用常規方式寫 MobX 又能享受 MST 同等的特性呢?
相對于 MobX 的多 store 和 class-method-based action 這種序列化不友好的范式而言,Redux 對 time travel/action replay 這類特性支持起來顯然要容易的多(但相應的應用代碼也要繁瑣的多)。但是只要我們解決了兩個問題,MobX 的 time travel/action replay 支持問題就會迎刃而解:
收集到應用的所有 store 并對其做 reactive 激活,在變化時手動序列化(snapshot)。完成 store -> reactive store collection -> snapshot(json) 過程。
將收集到的 store 實例及各類 mutation(action) 做標識并做好關系映射。完成 snapshot(json) -> class-based store 的逆向過程。
針對這兩個問題,mmlpx 給出了相應的解決方案:
DI + reactive container + snapshot (收集 store 并響應 store 變化,生成序列化 snapshot)
ts-plugin-mmlpx + hydrate (給 store 及 aciton 做標識,將序列化數據注水成帶狀態的 store 實例)
下面我們具體介紹一下 mmlpx 是如何基于 snapshot 給出了這兩個解決方案。
Snapshot 需要的基本能力上文提到,要想為 MobX 治下的應用狀態提供 snapshot 能力,我們需要解決以下幾個問題:
收集應用的所有 storeMobX 本身在應用組織上是弱主張的,并不限制應用如何組織狀態 store、遵循單一 store(如redux) 還是多 store 范式,但由于 MobX 本身是 OOP 向,在實踐中我們通常是采用 MVVM 模式 中的行為準則定義我們的 Domain Model 和 UI-Related Model(如何區別這兩類的模型可以看 MVVM 相關的文章或 MobX 官方最佳實踐,這里不再贅述)。這就導致在使用 MobX 的過程中,我們默認是遵循多 store 范式的。那么如果我們想把應用的所有的 store 管理起來應該這么做呢?
在 OOP 世界觀里,想管理所有 class 的實例,我們自然需要一個集中存儲容器,而這個容器通常很容易就會聯想到 IOC Container (控制反轉容器)。DI(依賴注入) 作為最常見的一種 IOC 實現,能很好的替代之前手動實例化 MobX Store 的方式。有了 DI 之后我們引用一個 store 的方式就變成這樣了:
import { inject } from "mmlpx" import UserStore from "./UserStore" class AppViewModel { @inject() userStore: UserStore loadUsers() { this.userStore.loadUser() } }
之后,我們能很容易地從 IOC 容器中獲取通過依賴注入方式實例化的所有 store 實例。這樣收集應用所有 store 的問題就解決了。
更多 DI 用法看這里 mmlpx di system
響應所有 store 的狀態變化獲取到所有 store 實例后,下一步就是如何監聽這些 store 中定義的狀態的變化。
如果在應用初始化完成后,應用內的所有 store 都已實例完成,那么我們監聽整個應用的變化就會相對容易。但通常在一個 DI 系統中,這種實例化動作是 lazy 的,即只有當某一 Store 被真正使用時才會被實例化,其狀態才會被初始化。這就意味著,在我們開啟快照功能的那一刻起,IOC 容器就應該被轉換成 reactive 的,從而能對新加入管理的 store 及 store 里定義的狀態實行自動綁定監聽行為。
這時我們可以通過在 onSnapshot 時獲取到當前 IOC Container,將當前收集的 stores 全部 dump 出來,然后基于 MobX ObservableMap 構建一個新的 Container,同時 load 進之前的所有的 store,最后對 store 里定義的數據做遞歸遍歷同時使用 reaction 做 track dependencies,這樣我們就能對容器本身(Store 加入/銷毀)及 store 的狀態變化做出響應了。如果當變化觸發 reaction 時,我們對當前應用狀態做手動序列化即可得到當前應用快照。
具體實現可以看這里:mmlpx onSnapshot
從 Snapshot 中喚醒應用通常我們拿到應用的快照數據后會做持久化,以確保應用在下次進入時能直接恢復到退出時的狀態 ── 或者我們要實現一個常見的 redo/undo 功能。
在 Redux 體系下這個事情做起來相對容易,因為本身狀態在定義階段就是 plain object 且序列化友好的。但這并不意味著在序列化不友好的 MobX 體系里不能實現從 Snapshot 中喚醒應用。
想要順利地 resume from snapshot,我們得先達成這兩個條件:
給每個 Store 加上唯一標識如果我們想讓序列化之后的快照數據順利恢復到各自的 Store 上,我們必須給每一個 Store 一個唯一標識,這樣 IOC 容器才能通過這個 id 將每一層數據與其原始 Store 關聯起來。
在 mmlpx 方案下,我們可以通過 @Store 和 @ViewModel 裝飾器將應用的 global state 和 local state 標記起來,同時給對應的模型 class 一個 id:
@Store("UserStore") class UserStore {}
但是很顯然,手動給 Store 命名的做法很愚蠢且易出錯,你必須確保各自的命名空間不重疊(沒錯 redux 就是這么做的[攤手])。
好在這個事情有 ts-plugin-mmlpx 來幫你自動完成。我們在定義 Store 的時候只需要這么寫:
@Store class UserStore {}
經過插件轉換后就變成:
@Store("UserStore.ts/UserStore") class UserStore {}
通過 fileName + className 的組合通常就可以確保 Store 命名空間的唯一性。更多插件使用信息請關注 ts-plugin-mmlpx 項目主頁 .
Hyration從序列化的快照狀態中激活應用的 reactive 系統,從靜態恢復到動態這個逆向過程,跟 SSR 中的 hydration 非常相似。實際上這也是在 MobX 中實現 Time Travelling 最難處理的一步。不同于 redux 和 vuex 這類 Flux-inspired 庫,MobX 中狀態通常是基于 class 這種充血模型定義的,我們在給模型脫水再重新注水之后,還必須確保無法被序列化的那些行為定義(action method)依然能正確的與模型上下文綁定起來。單單重新綁定行為還沒完,我們還得確保反序列化之后數據的 mobx 定義也是跟原來保持一致的。比如我之前用 observable.ref 、observable.shallow 及 ObservableMap 這類有特殊行為的數據在重注水之后能保持原始的能力不變,尤其是 ObservableMap 這類非 object Array 的不可直接序列化的數據,我們都得想辦法能讓他們重新激活回復原狀。
好在我們整個方案的基石是 DI 系統,這就給我們在調用方請求獲取依賴時提供了“做手腳”的可能。我們只需要在依賴被 get 時判斷其是否由從序列化數據填充而來的,即 IOC 容器中保存的 Store 實例并非原始類型的實例,這時候便開啟 hydrate 動作,然后給調用方返回注水之后的 hydration 對象。激活的過程也很簡單,由于我們 inject 時上下文中是有 store 的類型(Constructor)的,所以我們只要重新初始化一個新的空白 store 實例之后,使用序列化數據對其進行填充即可。好在 MobX 只有三種數據類型,object、array 和 map,我們只需要簡單的對不同類型做一下處理就能完成 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
應用場景相較于 MST 的快照能力(MST 只能對某一 Store 做快照,而不能對整個應用快照),基于 mmlpx 方案在實現基于 Snapshot 衍生的功能時變得更加簡單:
Time TravellingTime Travelling 功能在實際開發中有兩種應用場景,一種是 redo/undo,一種是 redux-devtools 之類提供的應用 replay 功能。
在搭載 mmlpx 之后 MobX 實現 redo/undo 就變得很簡單,這里不再貼代碼(其實就是 onSnapshot 和 applySnapshot 兩個 api),有興趣的同學可以查看 mmlpx todomvc demo (就是文章開頭貼的 gif 效果) 和 mmlpx 項目主頁。
類似 redux-devtools 的功能實現起來相對麻煩一點(其實也很簡單),因為我們要想實現對每一個 action 做 replay,前提條件是每個 action 都有一個唯一標識。redux 里的做法是通過手動編寫具備不同命名空間的 action_types 來實現,這太繁瑣了(參考Redux數據流管理架構有什么致命缺陷,未來會如何改進?)。好在我們有 ts-plugin-mmlpx 可以幫我們自動的幫我們給 action 起名(原理同自動給 store 起名)。解決掉這個麻煩之后,我們只需要在 onSnapshot 的同時記錄每個 action,就能在 mobx 里面輕松的使用 redux-devtool 的功能了。
SSR我們知道,React 或 Vue 在做 SSR 時,都是通過在 window 上掛載全局變量的方式將預取數據傳遞到客戶端的,但通常官方示例都是基于 Redux 或 Vuex 來做的,MobX 在此之前想實現客戶端激活還是有些事情要解決的。現在有了 mmlpx 的幫助,我們只需要在應用啟動之前,使用傳遞過來的預取數據在客戶端應用快照即可基于 MobX 實現客戶端狀態激活:
import { applySnapshot } from "mmlpx" if (window.__PRELOADED_STATE__) { applySnapshot(window.__PRELOADED_STATE__) }應用 crash 監控
這個只要使用的狀態管理庫具備對任一時間做完整的應用快照,同時能從快照數據激活狀態關系的能力就能實現。即檢查到應用 crash 時按下快門,將快照數據上傳云端,最后在云端平臺通過快照數據還原現場即可。如果我們上傳的快照數據還包括用戶前幾次的操作棧,那么在監控平臺對用戶操作做 replay 也不成問題。
最后作為一個“多 store”范式的信徒,MobX 在一出現便取代了我心中 Redux 在前端狀態管理領域的地位。但苦于之前 MobX 多 store 架構下缺乏集中管理 store 的手段,其在 time travelling 等系列功能的開發體驗上一直有所欠缺。現在在 mmlpx 的幫助下,MobX 也能開啟 Time Travelling 功能了,Redux 在我心中最后的一點優勢也就蕩然無存了。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/96031.html
摘要:異步剪貼板操作過去的數年中,各瀏覽器基本上都在使用來進行剪貼板交互。而提供了新的,則為我們提供了另一種異步式的剪貼板操作方式,本文即是對該機制與接口規范的詳細介紹。 showImg(https://segmentfault.com/img/remote/1460000013854167); 前端每周清單第 55 期: MobX 4 特性概覽,iOS Hacks 分享, 分布式事務詳解 ...
摘要:在該版本發布之后,開發團隊并不會繼續發布新的特性,而會著眼于進行重大的錯誤修復。發布每六個星期,團隊就會創建新的分支作為發布通道,本文即是對新近發布的版本進行簡要介紹。 showImg(https://segmentfault.com/img/remote/1460000013229009); 前端每周清單專注前端領域內容,以對外文資料的搜集為主,幫助開發者了解一周前端熱點;分為新聞熱...
摘要:結合編輯器可以推導變量對應的類型以及內部的結構,提高代碼的健壯性和可維護性。通過充分利用時間回溯的特征,可以增強業務的可預測性與錯誤定位能力。對于對象的哪部分需要成為可觀察的,提供了細粒度的控制。 showImg(https://segmentfault.com/img/bVba6Ts?w=1218&h=525); 為什么要使用TypeScript 偵測錯誤 通過靜態類型檢測可以盡早檢...
摘要:所以這是一篇插隊的文章,用于去理解中的裝飾器和概念。因此,該的作用就是根據入參返回具體的描述符。其次局部來看,裝飾器具體應用表達式是,其函數簽名和是一模一樣。等裝飾器語法,是和直接使用是等效等價的。 ================前言=================== 初衷:以系列故事的方式展現 MobX 源碼邏輯,盡可能以易懂的方式講解源碼; 本系列文章: 《【用故事解...
閱讀 2805·2023-04-26 01:00
閱讀 745·2021-10-11 10:59
閱讀 2973·2019-08-30 11:18
閱讀 2666·2019-08-29 11:18
閱讀 1017·2019-08-28 18:28
閱讀 3010·2019-08-26 18:36
閱讀 2132·2019-08-23 18:16
閱讀 1065·2019-08-23 15:56