Это – Дэн Абрамов. У него 27К подписчиков (это круто). И в прошлом году он сделал редакс.
Редакс – это библиотека для организации архитектуры приложения.
Главные отличия редакса от остальных подходов — это то, что 1) всё состояние приложения хранится в единственном месте…
и 2) приложение строится по однонаправленному потоку данных (http://redux.js.org/docs/basics/DataFlow.html ).
Однонаправленный поток кардинально отличается от MVC, где C—V и C—M обычно двунаправленные. Однонаправленный поток упрощает понимание приложения.
Как я уже сказал, всё состояние приложения хранится в единственном месте. Это место называется «стор». Например, у такого приложения, которое показывает цифру,..
...стор может быть таким.
Второй принцип — состояние приложения нельзя изменять напрямую.
Состояние нельзя изменять напрямую. То есть вот так: store.number += 1
— делать нельзя. Чтобы изменить стор, нужно сгенерировать событие с описанием изменения. Это делается с помощью store.dispatch()
.
Объект, который передаётся в dispatch()
, называется «экшен» (action). Это обычный JavaScript-объект. Он может содержать любые поля, которые будут описывать изменение, единственное обязательное поле — type
.
Редакс запрещает напрямую изменять состояние, потому что нужно следить за изменениями, чтобы при изменениях стора обновлять представление.
Третий принцип — все изменения обрабатываются функцией, которая называется «редьюсер» и которая должна быть чистой.
Ещё раз: экшен — это событие, которое генерируется функцией dispatch()
.
Чистая функция — это функция, которая всегда для одного и того же входного значения возвращает одинаковый результат + не изменяет ничего снаружи себя.
Синус — чистая функция, потому что для одного и того же угла она всегда возвращает одно и то же значение.
parseInt()
— чистая.
$.ajax()
— не чистая, потому что в разные моменты времени она может вернуть разные значения.
Первая функция — чистая, потому что она ничего не изменяет снаружи.
Вторая — нет, потому что она изменяет свой параметр, и внешний код после вызова этой функции работает уже с другим параметром.
Третья — нет, потому что она тоже изменяет состояние системы снаружи себя (выводит что-то в консоль).
Эта функция, которая обрабатывает экшены, называется редьюсером.
Она обрабатывает каждый экшен, который происходит в приложении. Функция принимает на вход есть текущее состояние приложения и экшен, который только что произошёл, и для каждого типа экшена изменяет состояние таким образом, как считает нужным. Обратите внимание, что состояние здесь не изменяется напрямую. Вместо этого возвращается новое состояние, которым Редакс уже сам заменяет старое.
Итак, это главные принципы редакса...
(Список принципов: http://redux.js.org/docs/introduction/ThreePrinciples.html )
...и согласно ним редакс-приложение строится по такой схеме.
Вопрос: что делать с сайд-эффектами?
Сайд-эффекты — это когда вы изменяете глобальную переменную, делаете запрос на сервер, пишете что-то в лог.
Проблема в том, что в этой схеме сайд-эффектам нет места. Единственное место, где происходят изменения — это редьюсер, а он обязан быть чистой функцией.
На помощь приходит middleware. Мидлвейр находится между кодом, который диспатчит экшены (то есть генерирует события) и редьюсером (то есть функцией, которая их преобразует в новое состояние), пропускает через себя все возникающие экшены, меняет их (если считает нужным) и отправляет дальше — или не отправляет. Особенность мидлвейра в том, что он быть чистой функцией не обязан — и поэтому он может делать сайд-эффекты вроде запросов на сервер или чего угодно.
На самом деле мидлвейров может быть много, и тогда они организуются по цепочке.
Фактически, мидлвейр — это просто функция, которая принимает текущий стор, следующий в цепочке мидлвейр и текущий экшен и что-то делает с ним.
Мидлвейры подключаются при создании стора. createStore
и applyMiddleware
— это методы редакса.
Я не буду рассказывать, как писать свой мидлвейр, потому что создавать свой миддлвейр для работы с сайд-эффектами — слишком сложно. Вместо этого мы посмотрим на те решения, которые уже есть. К слову, мидлвейры создаются сообществом и не входят в поставку редакса.
Есть куча методов организации сайд-эффектов. Я знаю два. Первый — это redux-thunk, с которым вы, создавая приложение с редаксом, столкнётесь точно.
Зачем он нужен? Классический редакс умеет работать только с экшенами, которые представлены простыми объектами.
Применяя мидлвейр redux-thunk вот таким вот образом,..
...вы получаете возможность вместо простых обьектов...
…диспатчить функции. Когда вы задиспатчите функцию, redux-thunk вызовет её и передаст в неё метод dispatch стора, который вы потом сможете вызвать тогда, когда вам нужно. Такие функции называются thunk-ами.
Или вот так, если вынести создание экшена в отдельную функцию. Такое вынесение, кстати, — стандартный шаблон в Редаксе, и функция, создающая экшен, называется экшен-криэйтор.
Что ещё можно делать с redux-thunk? Можно диспатчить несколько экшенов подряд, чтобы выполнить логаут и затем очистить стор.
Можно диспатчить экшены по таймеру, чтобы побесить пользователя всплывающими модальными окнами. Можно делать что угодно, потому что у нас есть dispatch
— и даже есть текущее состояние стора, которое передаётся вторым параметром.
Redux-thunk удобен для простых сайд-эффектов. Когда нужно организовать сложный поток сайд-эффектов, он становится неудобен. В этом случае пригождается redux-saga.
Что такое redux-saga? Вообще, сага — это подход, пришедший с бекенда.
Redux-saga — это альтернативный подход к организации сайд-эффектов. Вместо того, чтобы диспатчить функции, которые обрабатываются redux-thunk-ом, вы создаёте сагу, которая собирает всю логику обработки внутрь себя. В отличие от thunk-ов, которые выполняются, когда вы их диспатчите, саги запускаются при старте приложения и как бы «работают в фоне». Саги слушают все экшены, которые диспатчит стор, и решают, что делать с ними.
И у саг в редаксе два преимущества по сравнению с thunk-ами:
— Они позволяют организовывать сложные последовательности сайд-эффектов
— И они очень легко тестируются
Давайте посмотрим, как выглядит сага.
Помните старый пример логина с помощью thunk-а? Давайте поменяем его на сагу.
Что меняется?
— Функция становится генератором
— Прямые вызовы заменяются на непрямые, через call()
; dispatch()
заменяется на put()
— Стиль вызовов меняется на синхронный (без колбеков) — и именно вот это позволяет писать сложные асинхронные запросы
На экшены можно подписываться двумя способами. Первый — простой: мы оборачиваем сагу и просим redux-saga вызывать её каждый раз, когда произойдёт экшен LOGIN_REQUESTED
.
Второй способ более сложный. Мы помещаем код саги в бесконечный цикл и размещаем в начале yield take('LOGIN_REQUESTED')
. Когда сага начнёт выполняться, выполнение дойдёт до этой строки, остановится и не продолжится до тех пор, пока не сгенерируется экшен LOGIN_REQUESTED
.
Второй способ позволяет ловить несколько экшенов. Здесь код на месте многоточия выполнится только тогда, когда произойдёт ANOTHER_ACTION
, которому предшествовал LOGIN_REQUESTED
.
Так саги устанавливаются. Они запускаются сразу после вызова createStore()
.
Примеры кода с сагами
Бизнес-логика
С бизнес-логикой всё просто.
Бизнес-логику — мидлвейрам!
Почему?
— Сайд-эффекты, которым принадлежит большая часть бизнес-логики (thunk-ах или сагах), и так будут в мидлвейрах. Есть смысл перенести оставшуюся бизнеслогику туда
— Мидлвейры имеют доступ ко всему стору, а редьюсеры — только к части
— Можно диспатчить несколько экшенов
Ещё ссылки:
Question: Where to put business logic / validations?
Recommendations for best practices regarding action-creators, reducers, and selectors
То есть на этой схеме бизнес-логика должна находиться в этом чёрном блоке.
Пример кода из одного из лучших open-source проектов на редаксе. Редьюсеры в нём максимально простые, …
...а экшен-криэйторы сложные и содержат всю логику.
Тестирование
Что вообще можно тестировать в Редаксе? Обычные экшен-крийэторы тестировать просто. Асинхронные — сложно, потому что они возвращают функции, а проверить, что вернулась правильная функция, сложно. Редьюсеры — просто.
(Как тестировать: экшен-криэйторы , асинхронные экшен-криэйторы , редьюсеры .)
Я расскажу про то, как тестировать саги. Саги тестировать очень просто.
Помните этот слайд?
Самый кайф — вот в этих выделенных функциях.
call($.ajax)
не вызывает $.ajax
напрямую, а генерирует декларативное описание вызова в виде простого JS-объекта. Благодаря этому для тестирования достаточно сравнить два объекта через deepEqual
.
Вот такую вот сагу…
…протестировать очень просто.
Обратите внимание, что для того, чтобы сэмулировать ответ сервера, мы просто передаём его в генератор.
И проверить то, что генератор вызывает $.ajax
и делает этим запрос на сервер, очень просто, потому что генератор возвращает объект с описанием вызова.
То есть ещё раз: мы декларируем вызов AJAX-запроса…
…и просто сравниваем декларацию.
Точно так же и с генерацией экшенов. Если мы хотим проверить, что сага сгенерирует экшен LOGIN_SUCCESS
, мы просто сравниваем ту декларацию, которую вернул генератор, с ожидаемой.
Что ещё не поместилось в доклад
Ducks
redux-devtools
redux-devtools-extension
Reselect
Документация
andrewngu/sound-redux
Hashnode/mern-starter
Будьте умничками.
Спасибо!
twitter.com/iamakulov