Управление состоянием Angular — не бойтесь шаблонов
Многие люди рассуждают о шаблоне, который создается при внедрении библиотеки управления состоянием на основе redux в приложении Angular. Все, что я могу ответить на это:
Не бойся шаблона. Бойся сильного связывания!
Тем не менее, я могу понять таких людей, они в некотором смысле правы. Управление состоянием может очень быстро стать запутанным. Особенно, если вы работаете в большой команде с разным уровнем опыта.
Я думаю, что больше всего боли причиняют шаблоны. Иногда это просто отсутствие четкого разделения интересов. Отсутствие четко определенных умных и глупых компонентов. Состояние, которое не является хорошо структурированным или просто слишком большим, чтобы обрабатываться одним компонентом контейнера. Сложные подписки внутри компонентов, которые могут объединять несколько наблюдаемых потоков(observables).
Добавьте сверху некоторые побочные эффекты. Добавьте информацию о маршрутизации в состояние. И внезапно:
Код больше не поддерживаемый. Архитектура Redux обещала решить проблемы сокрытия источника состояния приложения, но теперь стало ещё сложнее следить за потоком данных внутри вашего приложения. Тебя бомбит. Ты винишь того, кто принял решение применить управление состоянием или того, кто его реализовал.
Пожалуйста, потерпи немного :)
В этом посте я покажу как вы можете «управлять» вашей библиотекой управления состоянием. Я буду использовать ngrx в моих примерах, но на самом деле не имеет значения, какую библиотеку на основе редукса вы используете. Концепция остается прежней. Даже простые старые сервисы могут быть использованы для удержания вашего состояния.
1. Используй Typescript
Это лёгкая задача для разработчиков Angular. Стоит к нему немного привыкнуть, и вы уже не сможете без него обходиться. Мы будем использовать его силу, чтобы определять наши типизированные действия и использовать их в редукторах (reducer) и эффектах. Таким образом, ошибки кодирования возникают во время компиляции по мере разработки, а не во время выполнения. Давайте прямо сейчас погрузимся в наши типы действий:
Это довольно прямое определение стандартного типа Action
, объединяющего типы Type
и Payload.
Таким образом, фактически каждый объект в форме {type, payload}
подтверждает этот тип.
Мы могли бы использовать это сейчас, чтобы создать функцию, которая возвращает определенное действие и затем отправляет его через ngrx store.dispatch:
Вопрос закрыт. Это хорошо работает для одной функции конструктора действия, но этого недостаточно. Мы не подумали про редукторы и эффекты, и, когда приложение подрастёт, нам придётся писать много шаблонного и дублирующего кода.
Давайте улучшим наш дизайн с помощью функций-фабрик. Они помогают нам немного урезать шаблон:
Теперь мы можем использовать createType
и createActionCreator
для определения наших CommitActions
:
Это альтернатива подходу определения действий на основе классов, который широко применяется пользователями ngrx.
Теперь мы готовы отправить действие из компонента контейнера:
Следующим шагом является настройка редуктора. Самое интересное в редукторах и их эффектах заключается в том, что они поддерживают проверку типов также во время компиляции.
Функция isAction
не только проверяет соответствие одного из ожидаемых действий, но и позволяет нам использовать типизированную полезную нагрузку внутри операторов is
благодаря Typescript Type Guards.
Тип TypedActionCreator
— это функция, которая также сохраняет тип действия как свойство. Это выглядит немного странно, но позволяет нам выполнять проверку isAction
без создания дополнительного шаблона.
Полный исходный код на github.
2. Подумайте о внедрении зависимостей (Dependency Injection)
Внедрение зависимостей является одной из основных особенностей Angular. Это очень упрощает многое для нас, разработчиков, а именно тестирование и вложенные зависимости. Но за всё надо платить.
На первый взгляд, внедрение чего угодно куда угодно кажется великим благом. Однако, я тут вижу проблему, которая заключается в том, что разработчики перестают думать о зависимостях. Вот что не даёт мне покоя:
Действительно ли я хочу иметь прямую зависимость от сторонней библиотеки в моем компоненте?
Это подводит нас к сильному связыванию. Связывание — это плохо.
Поэтому мы не хотим иметь прямую зависимость в библиотеке управления состоянием наших компонентов-контейнеров.
Нет:
— встряхните головой —
К сожалению, так делается в каждом онлайн примере ngrx
. Если вам сказали, что контейнерные компоненты являются умными, то это не обязательно означает, что они должны импортировать каждую стороннюю библиотеку в качестве зависимости. Есть способ получше.
Да:
— кивните одобрительно —
Теперь вы даже не можете сказать, что за ним стоит ngrx или что мы применяем шаблон redux
. Только имена переменных и имена типов позволяют догадаться, что мы используем redux
. Но я мог бы переименовать их:
Должен признать, что мы описали лишь крошечную деталь, и я все еще продолжаю это обдумывать. Мы могли бы переименовать переменные во что-то более знакомое для нашей предметной области (бизнеса).
Когда мы говорим на языке redux
, и находимся в его контексте, термин «действие» (action), безусловно, корректен. Но когда мы в наших контейнерных компонентах говорим на языке бизнеса, то действие больше похоже на команду (например, LOAD_COMMITS
). Когда команда завершена, происходит событие (например, LOAD_COMMITS_SUCCESS
, LOAD_COMMITS_FAILED
), основанное на ее результате.
Выделение зависимостей также помогает нам повысить стабильность тестов. Правило тестов № 1 (по крайней мере для меня) гласит: не макетируй (mock) то, чем не владеешь. Зачем? Потому что это вне вашего контроля. Если в сторонней библиотеке изменяется что-то, о чем мы не знаем, то это в конечном итоге ломает все наши тесты. Тесты ломаются по неправильной причине. Они должны сломаться, если кто-то изменит поведение в логике нашего приложения, а не если инструмент управления состоянием изменился.
У нас должны быть специальные тесты контрактов для проверки интеграции с вашей библиотекой управления состоянием. Тесты должны потерпеть неудачу, если библиотека изменяется неожиданным способом.
Что мы выиграли?
- Слабая связность
- Чистый код
- Поддерживаемость
- Хотите заменить свою реализацию
redux
или текущее решение по управлению состоянием, не сходя с ума? Теперь нет проблем :)
Но как это вообще работает? Где находится код отправки (dispatch)? Позвольте мне представить вам Связанные действия (Bound Actions). Это не новая концепция, просто она не получила широкого распространения в сообществе Angular. На это определенно стоит посмотреть.
CreateBoundActionCreator
связывает ActionCreator
с функцией отправки (dispatch). Он возвращает функцию, которая принимает полезную нагрузку, затем создает действие и напрямую отправляет его с помощью данной функции отправки. Таким образом, мы можем скрыть зависимость ngrx от вызывающего действия.
Давайте определим связанное действие в нашем уже существующем классе CommitActions.
Для этого класс CommitActions
должен стать сервисом Angular, который внедряет хранилище ngrx и затем связывает функцию отправки с функцией действия:
Теперь компонент контейнера больше не имеет прямой зависимости от ngrx. Это то, чего мы хотели достичь.
3. Используй селекторы (selectors)
До сих пор мы только посылали команду, чтобы запустить побочный эффект (отправка HTTP запроса) и, в конце концов, загрузить коммиты в хранилище. Но мы все равно должны показать их пользователю. Для этого мы должны выбрать данные из хранилища Redux
в нашем компоненте CommitsContainer
. Это можно сделать напрямую, если мы внедрим в него сервис хранилища из ngrx.
К сожалению, при таком подходе мы снова получаем нежелательную зависимость от нашей библиотеки управления состоянием. Для этого мы создаем сервис CommitSelector,
который содержит наши селекторы и может быть внедрен в компонент контейнера, чтобы эффективно скрыть от него зависимость ngrx.
В CommitsContainer
мы теперь можем использовать этот сервис, выбрать данные, и передать их нашему презентационному компоненту в шаблоне:
Еще одна действительно приятная особенность селекторов — возможность их комбинировать. Таким образом, несколько простых селекторов могут быть объединены в один сложный. Ngrx поставляется с модифицируемыми функциями createSelector
. Но вы также можете использовать библиотеку reselect.
Давайте посмотрим на пример. В приведенном выше коде мы захардкодили имя пользователя. Теперь мы хотели бы передать его динамически через параметр url-маршрута.
Я расширил пример использования ngrx/router-store
для синхронизации текущего состояния маршрутизатора (angular/router) с нашим хранилищем ngrx. Это позволяет нам использовать мощность комбинированных селекторов, где routeSelector
извлекает состояние маршрутизатора из хранилища, а routeParamSelector
считывает из него переданный параметр маршрута.
Теперь мы можем использовать селектор в нашем сервисе CommitSelectors
:
CommitsContainer
описывает наблюдаемое (observable) имя пользователя и при каждом изменении выполняет действие loadCommits
:
Мы разработали компонент, который не имеет прямой зависимости ни от маршрутизатора, ни от библиотеки управления состоянием. Это важный шаг для получения обслуживаемого, тестируемого и более чистого кода, который более чётко выражает сферу его деятельности.
В следующем посте я, вероятно, сделаю еще один шаг и представлю ConnectMixin
, который эффективно превратит наши контейнерные компоненты в глупые, которые просто используют @Input()
и @Output()
для подключения к хранилищу.
Посмотрите полный исходный код на github.
Подпишись на меня в Twitter :)
Примечание переводчика: примеры кода дополнены комментариями, исправлены незначительные ошибки, текст незначительно сокращён для более литературно красивого перевода.