Прокачайте свое взаимодействие с MobX

в 10:22, , рубрики: architecture, di, javascript, mobx, mvvm, Observer, React, ReactJS, TypeScript

В этой статье я опишу структурированный подход к использованию MobX, который может помочь упростить процесс разработки. Здесь не будет описываться код, только описание подхода к использованию. На код будут даваться ссылки. И я очень прошу вас посмотреть на примеры, которые я прикладываю. В них можно будет наглядно увидеть все плюсы описываемой архитектуры.

Также важно будет упомянуть, что для полного понимания описанного в статье, нужно быть знакомым с паттернами Observable/Observer, MVVM и DI.

О самой технологии MobX уже знает немало человек. Судя по данным npmjs.com, этот пакет в среднем скачивается около 850 тысяч раз в неделю. Его главным соперником можно считать библиотеку Redux, и судя по тем же данным, её в среднем скачивают в 8 раз чаще. Такую популярность Redux на фоне гораздо более удобного MobX мне сложно принять, так как я являюсь ярым сторонником этой библиотеки. Поэтому в этой статье я бы хотел описать, почему MobX настолько хорош и как можно сделать его ещё удобнее.

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

  • В MobX не нужно писать шаблонный код;

  • В MobX сторов может быть множество, а значит их можно логически разделять;

  • MobX гораздо проще для восприятия и изучения;

  • Типизация в MobX гораздо проще описывается и используется.

Но! Есть довольно большое «Но!». Мне не по душе подход самого MobX, который описывается для взаимодействия этой библиотеки с React. В примерах, которые описывают разработчики MobX предлагается создавать некоторый объект – стор, – в котором должна храниться обновляемая информация. Проблема в том, что это слишком размытое представление, не описывающее возможности более сложного взаимодействия, например, при вложенности компонент или при использовании одних сторов другими.

Уровень первый: Better MVVM

Начнем с терминологии. Гораздо логичнее использовать MobX, применяя паттерн MVVM. Формально, использование паттерна MVVM с MobX ничего нового не добавляет, только даёт названия сущностям системы. Моделью (Model) в таком подходе является любой JavaScript объект, а его описанием – его типизация. Представление (View) – это React-компонент. А модель представления (ViewModel) – некоторый класс, хранящий в себе observable поля, по факту являющийся стором.

На этом моменте заканчиваются все вводные. Далее я буду рассказывать о доработках, способных улучшить взаимодействие.

Взаимодействие View и ViewModel можно немного прокачать:

  • Сам объект ViewModel’и можно создавать в момент первой отрисовки View. Так, в конструкторе ViewModel можно писать код, который сработает перед монтированием View. К тому же в памяти компьютера не придется хранить неиспользуемую информацию.

  • Пропы, передаваемые во View можно передавать ViewModel’и. При этом удобно сделать поле, в котором хранятся пропы у ViewModel, observable, что позволит автоматически отслеживать их изменение.

  • При вложенности одной View в другой, можно передавать дочерней ViewModel ссылку на родительскую ViewModel.

  • При необходимости у ViewModel можно описать метод, запускаемый при монтировании и размонтировании View.

Для наглядности, давайте я опишу это на картинке

Прокачайте свое взаимодействие с MobX - 1

View1 инициализирует ViewModel1, передавая ей свои пропы. View2 является дочерним элементом View1 (необязательно напрямую) в виртуальном DOM’е. View2 инициализирует ViewModel2, передает ей ссылку на ViewModel1 и свои пропы.

View не обязательно должен быть observer-компонентом. Его главная задача проинициализировать объект его ViewModel’и, передать ей свои пропы и, если требуется, вызывать колбэки своего жизненного цикла.

ChildView

Ещё одна доработка по MVVM – введение понятия ChildView – некоторого компонента внутри View, который также имеет ссылку на ViewModel. ChildView не инициализирует новую ViewModel и не передаёт ей свои пропы. ChildView также не обязательно должен быть observer-компонентом.

Прокачайте свое взаимодействие с MobX - 2

В указанном примере у View есть 3 ChildView и каждая имеет ссылку на ViewModel. Из представленных ChildView observer-компонентом является только нижняя левая. Остальные используют статические поля и/или методы ViewModel.

Описание сущностей архитектуры в вакууме не имеет особого смысла. Поэтому я вам крайне рекомендую посмотреть на то, как эти сущности выглядят в коде: CodeSandbox / GitHub.

Уровень второй: Services

У описанного подхода все ещё могут быть свои минусы. Например, зачем хранить некоторую общую информацию для всего приложения в какой-то ViewModel’и?  В таком случае логично сделать некоторый отдельный класс. Предлагаю называть такой класс сервисом.

В сервисе должна храниться общая информация, например, информация о пользователе – статус его авторизации и список разрешенных действий; или методы: создание всплывающего уведомления, переключение между страницами, и т.п. Сервис также может являться стором, то есть хранить observable поля.

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

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

Прокачайте свое взаимодействие с MobX - 3

Ограничений, накладываемых на сервисы, не очень много. Если в общем случае у ViewModel может быть ссылка на 1 другую ViewModel – родительскую, – то сервисов она может использовать сколько необходимо. Причем одни сервисы могут свободно использовать другие сервисы.

В реализациях DI часто есть деление на Singleton и Transient классы. И в общем случае я рекомендую делать ViewModel Transient классом, а сервис Singleton.

Если вам интересно посмотреть на работу View, ChildView и ViewModel совместно с сервисами и паттерном DI, можете взглянуть на этот пример: CodeSandbox / GitHub.

Уровень третий: Better Model

Как я писал выше любой JavaScript объект уже является моделью. В TypeScript по-хорошему у каждой такой модели должен быть определен тип. И этого уже достаточно.

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

В таких моделях помимо информации о полях – их тип и, возможно, значения по умолчанию – можно хранить метаинформацию о полях. В дополнительных декораторах можно указывать, как нужно и нужно ли в принципе валидировать определенные поля; а также указывать изменение каких полей нужно отслеживать.

Поигравшись немного с этой концепцией, я описал сущность такой модели. У "прокаченных" моделей могут быть следующие плюсы:

  • Прокаченная модель автоматически отслеживает изменения своих полей. Причем, если при инициализации о модели поле field было равно пустой строке, затем стало равно любой другой строке, а затем снова пустой, модель будет говорить, что данные не менялись.

  • Модель способна отслеживать, какие поля в настоящее время не являются валидными. Причем в процессе валидации могут участвовать не только значения полей, но и весь объект в целом.

  • Прокаченная модель способна наследоваться от другой. При этом в дочерней модели можно перезаписывать метаинформацию родительской модели и/или создавать новые поля.

  • Полем модели может быть другая модель, массив моделей или словарь со значениями моделями. В таком случае у родительской модели есть механизм контролирования изменения и проверки валидации вложенных моделей.

Пример на работу с Model можно посмотреть по этим ссылкам: CodeSandbox / GitHub.

Резюмируя

В конце хотелось бы перечислить преимущества описанной архитектуры:

  • Используя данный подход к разработке можно структурировать подход к обработке состояния приложения. При этом сам подход использует исключительно существующие паттерны.

  • У этого подхода нет необходимости в написании шаблонного кода и нет никаких проблем с типизацией.

  • Данные можно логично разделить по нескольким классам-сторам, причем так, что общие (находящиеся в сервисах) можно будет использовать во всем проекте, а частные (находящиеся во ViewModel) можно будет использовать только в необходимых частях.

  • View могут состоять только из JSX кода, не используя ни одного хука.

  • Работа с формами сокращается до качественного описания моделей.

В дополнение могу ещё сказать, что разработчикам, знакомым с Vue, будет гораздо проще перейти на React с такой архитектурой, нежели с Redux, ведь MobX в формате MVVM имеет очень много схожих концепций.

Ссылки

Послесловие

В статье я занимался только описанием сущностей архитектуры, и того, как они должны между собой взаимодействовать. И хоть я приложил весьма, как мне кажется, полезные ссылки с кодом, своих комментариев я по нему не оставил. Поэтому чуть позже я напишу ещё 1-2 статьи, в которых я опишу полезные use-case’ы описанной архитектуры. А если в комментариях я увижу заинтересованность, я займусь этим как можно скорее.

Автор: Александрович Дмитрий

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js