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

Первоначально я думал о ре-факторинге моего эмулятора 6502 для использования Redux, но понял, что это будет гораздо более сложная задача, поэтому решил вместо этого создать что-то с нуля. Заимствуя из приложения, которое я написал для книги, которую я опубликовал несколько лет назад, я создал redux-adventure, используя Angular 2 и TypeScript с Angular-CLI.

Концепции Redux

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

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

Redux отслеживает состояние за вас и предлагает три ключевых сервиса (есть и другие API, но я делаю это простым).

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

Игра

Игра redux-adventure довольно проста. Вы попадаете в случайную комнату в подземелье и должны исследовать подземелье, чтобы найти различные артефакты. Вы можете смотреть или путешествовать в четырех направлениях компаса, и если есть предмет, вы можете получить его и положить в свой инвентарь. Вы выиграете игру, получив все доступные предметы.

Состояние

Само состояние - это просто модель предметной области, представленная обычным старым объектом JavaScript (POJO). «Вещь» или артефакт имеет имя и описание. Тогда есть комнаты, которые выглядят так:

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

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

Также есть dungeonMaster, который генерирует мир из некоторой исходной информации и случайным образом генерирует стены. У любых классов или сервисов с поведением есть свои тесты. Теперь, когда мы определили мир, что мы можем сделать?

Действия

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

  • Переместить: обновляет текущую комнату до комнаты, в которую перешел пользователь, и обновляет консоль, чтобы указать движение и отобразить описание новой комнаты.
  • Получить: переносит инвентарь из текущей комнаты к пользователю.
  • Текст: добавляет строку текста в консоль.
  • Выиграл: передает последний элемент инвентаря пользователю, устанавливает флаг победы и обновляет консоль, чтобы указать, что пользователь выиграл.

За эту логику отвечает метод createAction. TypeScript позволяет мне писать интерфейсы, чтобы было понятнее, что проверяет действие. Вот интерфейс действия get:

А вот код, который берет исходное действие и преобразует его во внутреннее:

Обратите внимание, что одно «входящее» действие может быть преобразовано в три «внутренних» действия: текст с язвительным комментарием, когда нечего получить, действие по передаче инвентаря пользователю и действие, указывающее, что пользователь выиграл.

Перевод действий полностью тестируемый. Обратите внимание, что до сих пор мы работали на чистом TypeScript / JavaScript - ни один из этих кодов еще не зависит от какой-либо внешней инфраструктуры.

Редукторы

Для привыкания к редукторам может потребоваться некоторое время, но по сути они просто возвращают новое состояние, основанное на действии, и гарантируют, что существующее состояние не изменится. Самый простой способ справиться с редукторами - «снизу вверх», то есть взять свойства нижнего уровня или вложенные объекты и обработать их состояние, а затем объединить их в более высокие уровни.

Например, комната содержит набор предметов инвентаря. Действие «получить» передает инвентарь пользователю, поэтому things property комнаты обновляется новым массивом, который больше не содержит элемент. Вот код TypeScript:

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

Посмотреть соответствующие тесты, написанные на TypeScript, можно здесь. Обратите внимание, что в тестах я использую Object.freeze, чтобы гарантировать, что исходные экземпляры не мутируются. Я замораживаю и отдельные элементы, и список, а затем проверяю, что элемент успешно удален.

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

Редуктор для комнаты вызывает редуктор для свойства вещи и возвращает новую комнату с скопированными свойствами (и, в случае перехода в комнату, устанавливает visited flag).

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

В конце концов, тесты просто подтверждают, что состояние изменяется соответствующим образом на основе действия и не изменяет существующее состояние.

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

Это мощный способ создания программного обеспечения, потому что теперь, решите ли вы использовать Angular, React, простой JavaScript или любой другой фреймворк, основная бизнес-логика и домен остаются прежними. Код не меняется, все тесты действительны и не зависят от фреймворка, и единственное решение - это то, как вы его визуализируете.

Магазин Redux

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

Корневой компонент приложения Angular обрабатывает все это:

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

Шаблон также прост. Он отображает консоль, предоставляет синтаксический анализатор для получения пользовательского ввода, если игра еще не выиграна, и отображает карту комнат.

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

Компоненты

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

Если вы нервничаете из-за того, что элементы HTML смешиваются с компонентом, не волнуйтесь! Их можно полностью протестировать без браузера:

Компонент синтаксического анализатора существует исключительно для ввода и отправки действий. Главный компонент слушает синтаксический анализатор и использует эмиттер событий для отправки действий в хранилище Redux (этот код был указан ранее). Сам синтаксический анализатор имеет действие для выдачи ввода и другое действие, которое автоматически отправляется, когда пользователь нажимает ENTER из поля ввода:

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

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

Вывод

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

В общем, это позволяет мне создать домен с использованием ванильного TypeScript / JavaScript и согласованно объявлять любую логику, необходимую на клиенте, путем обращения к действиям и редукторам. Все они полностью тестируемы, поэтому я смог спроектировать и проверить логику игры, не полагаясь на какие-либо сторонние фреймворки.

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

Что вы думаете? Вы используете Redux в своих приложениях? Если да, поделитесь своими мыслями в комментариях ниже.

Первоначально опубликовано на сайте csharperimage.jeremylikness.com 30 июля 2016 г.