Mидлвар¶
Вы видели мидлвары (Middleware) в действии в примере асинхронных экшенов. Если вы когда-либо использовали такие серверные библиотеки, как Express и Koa, то, вероятно, вы уже хорошо знакомы с концепцией мидлвар. В этих фреймворках мидлвары — это части кода, которые вы можете поместить между фреймворком, принимающим запрос и фреймворком, генерирующим ответ. Например, мидлвары из Express или Koa могут добавлять CORS-заголовки, логирование, сжатие и т. д. Лучшая особенность мидлваров заключается в том, что их можно соединять в цепочки/последовательности. Вы можете использовать множество независимых сторонних мидлваров в одном проекте.
Redux-мидлвары, в отличие от мидлваров Express или Koa, решают немного другие проблемы, но концептуально схожим способом. Они предоставляют стороннюю точку расширения, между отправкой экшена и моментом, когда этот экшен достигает редьюсера. Люди используют Redux-мидлвары для логирования, сообщения об ошибках, общения с асинхронным API, роутинга и т.д.
Эта статья разделена на углубленное введение, которое поможет вам хорошо разобраться в концепции, и пару практических примеров в самом конце, которые покажут вам всю силу мидлваров. Вам может показаться полезным периодическое переключение между этими частями, так же, как между скукой и вдохновением.
Понимание мидлваров¶
Т. к. мидлвары могут использоваться для различных задач, в том числе и для асинхронных обращений к API, то очень важно, чтобы вы понимали, откуда они пришли. Мы покажем вам ход мыслей, шаг за шагом ведущий к мидлварам, используя логирование и сообщения об ошибках в качестве примера.
Проблема: логирование¶
Одно из достоинств Redux — он делает изменения состояния приложения предсказуемыми и прозрачными. Каждый раз, когда посылается экшен, новое состояние вычисляется и сохраняется. Состояние не может измениться самостоятельно, оно может меняться только, как последовательность определенных экшенов.
Разве не было бы хорошо, если бы мы записывали каждое действие, которое происходило в приложении, вместе с состоянием, которое было вычислено после этого действия? Когда что-то идет не так, мы можем просмотреть наш лог и понять, какой именно экшен испортил наше состояние.
Как мы подходим к этому с Redux?
Попытка #1: Логируем вручную¶
Простейшее решение — самостоятельно записывать экшен и состояние каждый раз, когда вы вызываете store.dispatch(action)
. На самом деле это не слишком хорошее решение, это просто первый шаг на пути к пониманию проблемы.
Обратите внимание
Если вы используете react-redux или похожий биндинг, у вас, скорее всего, не будет прямого доступа к экземпляру стора в ваших компонентах. Для следующих нескольких параграфов представьте, что вы передаете состояние явно.
Например, вы вызываете такой код, когда создаете todo-элемент:
1 |
|
Для того чтобы логировать экшен и состояние, вы можете изменить код примерно так:
1 2 3 4 5 |
|
Это даст желаемый эффект, но вы бы не хотели делать так каждый раз.
Попытка #2: Оборачиваем Dispatch¶
Вы можете вынести логирование в функцию:
1 2 3 4 5 |
|
Вы можете использовать ее везде вместо обычного store.dispatch()
:
1 |
|
Мы бы могли закончить на этом, но не очень удобно импортировать специальную функцию каждый раз.
Попытка #3: Monkeypatching для Dispatch¶
Что, если мы просто заменим функцию dispatch
в экземпляре стора? Redux стор — это простой объект с парой методов, а мы пишем на JavaScript, следовательно, мы можем применить технику monkeypatch для реализации dispatch
:
1 2 3 4 5 6 7 |
|
Это уже ближе к тому, что нам нужно! Не важно, откуда мы посылаем экшен, он гарантированно будет залогирован. Monkeypatching никогда не покажется правильным ходом, но пока мы можем с этим жить.
Проблема: Сообщения об ошибках.¶
Что, если мы захотим применить больше одного такого преобразования к dispatch
?
Другое такое изменение, которое приходит мне в голову, это сообщения о JavaScript-ошибках в продакшене. Глобальное событие window.onerror
не надежно потому, что оно в некоторых старых браузерах не предоставляет информацию о стеке вызовов, которая важна для понимания того, почему же произошла ошибка.
Разве не было бы полезно, если бы каждый раз, когда ошибка выбрасывалась, как результат отправки какого-либо экшена, мы могли бы отправить ее (ошибку), вместе со стеком вызовов, экшеном, который вызвал ошибку и актуальным состоянием в сервис сообщения об ошибках, такой как Sentry. В таком случае гораздо легче воспроизвести ошибку в разработке.
Однако важно, чтобы мы держали логирование и сообщения об ошибках раздельно. В идеальном случае, мы хотим получить их, как разные модули из разных пакетов. В противном случае, мы не сможем иметь экосистему из такого рода утилит. (Подсказка: мы медленно подходим к тому, что такое мидлвары!)
Если логирование и сообщения об ошибках являются отдельными утилитами, то они могут выглядеть так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
Если эти функции опубликованы, как отдельные модули, то позже мы можем использовать их для изменения нашего стора:
1 2 |
|
Но это все еще не очень хорошо.
Попытка #4: Прячем Monkeypatching¶
Monkeypatching — это хак. "Замените любой метод, который хотите" — что это за вид API? Давайте разберемся в его сути. Ранее наши функции заменяли store.dispatch
. Что если бы они вместо этого возвращали новую функцию dispatch
?
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Мы могли бы предоставить функцию-помощник внутри Redux, которая могла бы применять актуальный monkeypatching, как часть имплементации:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Мы можем использовать такой подход для применения нескольких мидлваров:
1 2 3 4 |
|
Тем не менее - это все еще monkeypatching. Факт того, что мы прячем его внутри библиотеки, не отменяет использования monkeypatching.
Попытка #5: Убираем Monkeypatching¶
Зачем мы перезаписываем dispatch
? Конечно же для того, чтобы иметь возможность потом его вызвать. Но есть еще и другая причина: каждый мидлвар имеет доступ (и возможность вызвать) ранее обернутый store.dispatch
:
1 2 3 4 5 6 7 8 9 10 11 |
|
Это важно для возможности объединять мидлвары в цепочки!
Если applyMiddlewareByMonkeypatching
не сохранит store.dispatch
сразу после обработки первого мидлвара, store.dispatch
будет продолжать ссылаться на оригинальную функцию dispatch
. Следовательно второй мидлвар тоже будет связан с оригинальной функцией dispatch
.
Но есть еще другой метод реализации объединения мидлваров в цепочки (chaining). Мидлвар мог бы принимать функцию отправки экшена next()
в параметрах вместо того, чтобы читать ее из экземпляра стора.
1 2 3 4 5 6 7 8 9 10 |
|
Это тот момент, когда “we need to go deeper”, так что имеет смысл потратить некоторе время на это. Каскад функций выглядит пугающим. Стрелочные функции из ES6 делают это каррирование чуть более простым для глаз:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
Именно так выглядят мидлвары в Redux.
Теперь мидлвар принимает функцию отправки экшена next()
и возвращает другую функцию отправки экшена, которая, в свою очередь, является функцией отправки экшена next()
для мидлвара слева. Все еще полезно иметь доступ к некоторым методам стора, например к getState()
, следовательно, store
остается доступен, как аргумент самого верхнего уровня.
Попытка #6: Простейшее применение мидлваров¶
Вместо applyMiddlewareByMonkeypatching()
мы могли бы написать функцию applyMiddleware()
, которая сначала получает финальную, полностью обернутую функцию dispatch()
и возвращает копию стора, которая использует эту функцию:
1 2 3 4 5 6 7 8 9 10 11 |
|
Реализация applyMiddleware()
, которая поставляется с Redux, похожа на эту, но отличается тремя важными аспектами:
-
Она предоставляет мидлвару только подмножество API стора: методы
dispatch(action)
иgetState()
. -
Она использует некоторые хитрости для того, чтобы убедиться, что, экшен снова пройдет через всю цепочку мидлваров, включая текущий, если вы вызываете
store.dispatch(action)
из вашего мидлвара вместоnext(action)
. Это полезно для асинхронных мидлваров, как мы ранее видели. -
Для того чтобы гарантировать, что вы можете применить мидлвар только один раз, она работает с
createStore()
, а не с самимstore
. Вместо(store, middlewares) => store
, ее сигнатурой является(...middlewares) => (createStore) => createStore
.
Предостережение: отправка во время установки
Пока applyMiddleware
выполняет и настраивает ваши мидлвари, функцияstore.dispatch
будет указывать на оригинальную версию, предоставляемую createStore
. Отправка приведет к тому, что никакой другой мидлвар не будет применен. Если вы ожидаете взаимодействия с другим мидлваром во время настройки, вы, вероятно, будете разочарованы. Из-за этого неожиданного поведения applyMiddleware
выдаст ошибку, если вы попытаетесь отправить экшен до завершения установки. Вместо этого вам следует, либо напрямую связываться с этим другим мидлваром через общий объект (для мидлвара, вызывающего API, это может быть ваш клиентский объект API), либо ждать, пока мидлвар не будет создан с колбэком.
Финальный подход¶
Дан мидлвар который мы только что написали:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
Вот так можно его применить к Redux стору:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Вот и все! Теперь любые экшены, отправленные в экземпляр стора, будут проходить через logger
и crashReporter
:
1 2 |
|
Семь примеров¶
Если ваша голова вскипела от прочтения предыдущего раздела, представьте, каково было написать это. Этот раздел предназначен для расслабления меня и вас и поможет запустить ваши шестеренки.
Каждая из функций, приведенных ниже, является валидным Redux-мидлваром. Они не являются в равной степени полезными, но, по крайней мере, они в равной степени забавны.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
|