Примечание. Эта статья не является руководством по использованию дерева состояний mobx. На самом деле вся статья не имеет ничего общего с MST (mobx-state-tree).

Предисловие

Как официальная библиотека построения модели состояния, MST предоставляет множество замечательных функций, таких как путешествия во времени, горячая перезагрузка и поддержка redux-devtools. Но проблема с MST заключается в том, что он слишком самоуверен (о чем упоминалось на официальном сайте), и вы должны принять набор значений (точно так же, как redux) перед их использованием.

Давайте быстро посмотрим, как определить модель в MST:

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[]
}

Определение модели на основе классов, очевидно, более интуитивно понятно и чисто для разработчиков, а «субъективный» подход MST несколько противоречит интуиции, что не способствует ремонтопригодности проекта (подход на основе классов можно понимать до тех пор, пока как кто знает самое основное ООП). Но, соответственно, возможности, предоставляемые MST, такие как путешествия во времени, действительно привлекательны. Есть ли способ написать MobX обычным способом и пользоваться теми же функциями в MST?

По сравнению с действиями MobX с несколькими хранилищами и методами классов, недружественной парадигмой сериализации, поддержка Redux для путешествий во времени / воспроизведения действий, очевидно, намного проще (но соответствующий код приложения более громоздкий). Но до тех пор, пока мы решим две проблемы, проблема поддержки повтора путешествий во времени / действий в MobX будет решена:

1. Соберите все магазины приложения и реактивно активируйте их, вручную сериализуя их при изменении. Завершите хранилище - ›сбор реактивного хранилища -› процедура снимка (json).
2. Определите собранные экземпляры хранилища и различные мутации (действия) и сопоставьте отношения. Выполните обратный процесс создания снимка (json) - ›хранилище на основе классов.

Для этих двух проблем mmlpx дает соответствующее решение:

1. DI + реактивный контейнер + моментальный снимок (собирать хранилища и реагировать на изменения в хранилище, генерировать сериализованный моментальный снимок)
2. ts-plugin-mmlpx + гидрат (идентифицирует хранилище и действие, преобразовывает сериализованные данные в состояние с отслеживанием состояния). экземпляр магазина)

Давайте посмотрим, как mmlpx дает эти два решения на основе снимка.

Основные возможности, необходимые Snapshot

Как упоминалось выше, для обеспечения возможности создания снимков состояния приложения в MobX нам необходимо решить следующие проблемы:

Собери все магазины приложения

Сам MobX не имеет ни малейшего отношения к организации приложений. Он не ограничивает то, как приложение организует хранилище состояний, следует парадигме одного хранилища (например, redux) или нескольких хранилищ. Однако, поскольку MobX сам по себе является ООП, на практике мы обычно применяем режим MVVM. Кодекс поведения определяет нашу модель предметной области и модель, относящуюся к пользовательскому интерфейсу (как различать два типа моделей можно увидеть в статьях, связанных с MVVM, или в официальных передовых методах работы MobX, которые здесь не повторяются). Это заставляет нас подсознательно следовать парадигме нескольких магазинов при использовании MobX. Так что, если мы хотим управлять всеми магазинами в приложении?

В мировоззрении ООП для управления экземплярами всех классов нам, естественно, нужен централизованный контейнер для хранения, который часто легко называют контейнером IOC. Как наиболее распространенный тип реализации IOC, DI (внедрение зависимостей) является отличной альтернативой тому, как вы вручную создавали экземпляры хранилищ MobX. С помощью DI мы ссылаемся на магазин следующим образом:

import { inject } from 'mmlpx'
import UserStore from './UserStore'
class AppViewModel {
    @inject() userStore: UserStore
    
    loadUsers() {
        this.userStore.loadUser()
    }
}

После этого мы можем легко создать экземпляры всех экземпляров хранилища посредством внедрения зависимостей из контейнера IOC. Это решает проблему сбора всех магазинов в MobX.

Подробнее об использовании DI см. Здесь: mmlpx di system.

Реагировать на все изменения состояния магазина

Когда у вас есть все экземпляры хранилища, следующим шагом будет отслеживание изменений в состояниях.

Если все хранилища в приложении были созданы после инициализации приложения, относительно легко отслеживать изменения в приложении. Но обычно в системе DI создание экземпляра является ленивым, которое создается только тогда, когда хранилище действительно используется, а затем инициализируется его состояние. Это означает, что с того момента, как мы включаем функцию моментального снимка, контейнер IOC должен быть преобразован в реактивный, что позволяет автоматически связывать прослушиватели для состояния, определенного во вновь добавленном хранилище.

Теперь мы можем получить все хранилища, собранные в текущий момент текущим контейнером IOC с помощью метода onSnapshot, затем построить новый контейнер на основе MobX ObservableMap, загрузить все предыдущие хранилища, наконец, выполнить переопределение данных и рекурсивно отслеживать зависимости с помощью reaction, чтобы мы могли реагировать на контейнер (хранить добавление / удалять) и сохранять изменения состояния. Если изменение вызывает реакцию, мы можем вручную сериализовать текущее состояние приложения для моментального снимка приложения.

Конкретную реализацию можно увидеть здесь: mmlpx onSnapshot

Пробудить приложение из снимка

Обычно мы берем данные моментального снимка приложения и делаем их постоянными, чтобы гарантировать, что приложение может быть сразу же возвращено в состояние при выходе при следующем входе - или нам нужно реализовать общую функцию повтора / отмены.

Это относительно легко реализовать в системе Redux, потому что он определен с помощью простого объекта и удобен для сериализации. Но это не означает, что вы не можете разбудить приложение из моментального снимка в сериализованной недружественной системе MobX.

Чтобы успешно возобновить работу со снимка, мы должны выполнить эти два условия:

Добавьте уникальный идентификатор для каждого магазина

Если мы хотим, чтобы данные моментального снимка после сериализации были успешно восстановлены в собственном хранилище, мы должны дать каждому хранилищу уникальный идентификатор, чтобы контейнер IOC мог связать каждый уровень данных с исходным хранилищем через этот идентификатор.

В схеме mmlpx мы можем отметить глобальное и локальное состояние приложения с помощью декоратора @Store и @ViewModel и присвоить соответствующему классу модели идентификатор:

@Store('UserStore')
class UserStore {}

Но ручное присвоение имени Магазину глупо и подвержено ошибкам, вы должны убедиться, что ваши пространства имен не перекрываются (это именно то, что делает redux🙃).

К счастью, ts-plugin-mmlpx нас спас. Нам нужно только определить Store следующим образом:

@Store
class UserStore {}

После транспонирования плагина он становится:

@Store('UserStore.ts/UserStore')
class UserStore {}

Комбинация fileName + className обычно может гарантировать уникальность пространства имен Store. Для получения дополнительной информации об использовании плагина посетите домашнюю страницу проекта ts-plugin-mmlpx.

Hyration

Активируйте реактивную систему приложения из состояния сериализованного снимка, обратный процесс из статического в динамический очень похож на гидратацию в SSR. Фактически, это самый сложный шаг для реализации путешествия во времени в MobX. В отличие от библиотек, вдохновленных Flux, таких как redux и vuex, состояние в MobX обычно определяется на основе модели гиперемии класса. После обезвоживания и повторного заполнения модели мы также должны убедиться, что те поведения, которые не могут быть сериализованы, все еще правильно привязаны к контексту модели. Поведение повторного связывания еще не завершено, мы должны убедиться, что определение данных mobx после десериализации также соответствует оригиналу. Например, я использовал специальный модификатор, такой как observable.ref, observable.shallow и ObservableMap, мы должны сохранить исходную способность после пополнения. Специально для ObservableMap, которые не могут быть сериализованы, мы должны найти способ позволить им вернуться в исходное состояние.

К счастью, краеугольным камнем всего нашего решения является система DI, которая дает нам возможность «на практике», когда вызывающий абонент запрашивает зависимости. Все, что у нас есть, - это распознать, заполняется ли зависимость из сериализованных данных при получении зависимости, что означает, что экземпляр, хранящийся в контейнере IOC, не является экземпляром исходного типа класса. В это время начинается действие гидрата, а затем гидратация возвращается. Процесс активации также очень прост, поскольку у нас есть тип класса Store (Конструктор) в контексте inject, нам просто нужно повторно инициализировать новый пустой экземпляр хранилища и заполнить его сериализованными данными. К счастью, в MobX есть только три типа данных: объект, массив и карта. Нам нужно только провести простую обработку разных типов, чтобы полностью увлажнить:

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;
}

Вот исходный код гидрата.

Сценарии

По сравнению с возможностями моментальных снимков MST (MST может делать снимки только определенного хранилища, а не всего приложения), подход на основе mmlpx упрощает реализацию функций, производных от Snapshot:

Путешествие во времени

Функция «Путешествие во времени» имеет два сценария приложения, которые находятся в стадии разработки: один - это повтор / отмена, а другой - функция воспроизведения действия, предоставляемая redux-devtools.

После использования mmlpx реализация повтора / отмены в MobX очень проста. На самом деле вам просто нужны onSnapshot и applySnapshot два API. Начальное изображение в формате gif в статье было демонстрацией повтора / отмены, и вы можете проверить главную страницу проекта mmlpx за подробностями.

Такие функции, как redux-devtools, немного сложнее реализовать (на самом деле это просто), потому что мы хотим воспроизвести каждое действие, уникальный идентификатор для каждого действия, которое мы должны предоставить. Цель в redux достигается путем ручного написания action_types с разными пространствами имен. Это слишком громоздко. К счастью, у нас есть ts-plugins-mmlpx, который может помочь нам автоматически назвать действие (так же, как автоматическое присвоение имени магазину). После решения этой проблемы нам нужно только записывать каждое действие одновременно, когда onSnapshot работает, и тогда мы можем легко использовать функцию redux-devtool в MobX.

ССР

Мы знаем, что когда React или Vue выполняет SSR, предварительно выбранные данные передаются клиенту путем монтирования глобальных переменных в окне, но обычно официальный пример основан на Redux или Vuex, есть некоторые проблемы, которые необходимо решить, если мы хотим используйте вместо этого MobX. Теперь с помощью mmlpx все, что нам нужно, это использовать данные предварительной выборки для применения снимков на клиенте перед запуском приложения:

import { applySnapshot } from 'mmlpx'
if (window.__PRELOADED_STATE__) {
    applySnapshot(window.__PRELOADED_STATE__)
}

Мониторинг сбоев приложений

Эта функция может быть предоставлена ​​каждой библиотекой, которая может делать снимки состояний приложений и возобновлять их. То есть, когда обнаруживается сбой приложения, нажимается шторка, а затем данные снимка загружаются в облако, после чего облачная платформа может восстановить сцену с помощью данных снимка. Если загружаемые нами данные моментального снимка также включают стек предыдущих операций пользователя, можно воспроизвести операцию пользователя на платформе мониторинга.

В конце концов

Как сторонник парадигмы нескольких магазинов, MobX заменил в моем понимании позицию redux в области внешнего управления состоянием. Однако из-за отсутствия централизованного управления магазином в многоэтажной архитектуре MobX ему не хватало опыта разработки ряда функций, таких как путешествия во времени. Теперь с помощью mmlpx MobX также может включить Time Traveling, и последнее преимущество Redux, которое я думал, исчезло.