вторник, 15 октября 2019 г.

Redux: прослойки (middleWares)

В настоящей статье хочу осветить схему с прослойками в Redux: Что это такой? Как реализовано? Как использовать?
Здесь не будет рассказано про «основы» redux: actions, reducers, action creators и т.д. С этим нужно знакомиться отдельно. Возможно, я напишу про это, а возможно уже написал.


Что такое middleWare?

Простым языком, middleWare – это обертка (wrapper) для функции dispatch хранилища Redux. Получилось как-то не очень просто. Если еще проще, то это возможность выполнить какие-то действия до того, как будет выполнен dispatch (либо можно заблокировать вызов последнего). А dispatch, как вы знаете, запускает reducer.
Для такой обертки можно использовать:
1) ручной способ;
2) особый вид функции + функцию applyMiddleware библиотеки Redux.


Ручной способ.

Сначала посмотрим на ручной способ.
Допустим у нас есть некоторый код, который вызывает dispatch для передачи action в наш reducer:
//…
store.dispatch({type: “CREATE”, dataType: “REQUESTS”, payload: this.state.request});
//…
Ни что не мешает добавить в код дополнительные проверки или actions, перед тем как запустить dispatch. Это и будет наша простейшая прослойка.
Например, запустить несколько dispatch, где первый добавляет данные в хранилище, а второй управляет состоянием формы:
store.dispatch({type: “CREATE”, dataType: “REQUESTS”, payload: this.state.request});
store.dispatch({type: “CLOSE_FORM”});
Другой пример прослойки – работа с веб-сервисами:
//…
let result = this.webservice.createRequest(this.state.request);
result.then(data => store.dispatch({type: “CREATE”, dataType: “REQUESTS”, payload: data}))
//…
В примерах я не стал использовать action creators, чтобы упростить код. Хотя, конечно, правильно реализовать отдельный уровень абстракции в виде action creators.
Вот и вся суть middleWare. Хотя, конечно, с такой трактовкой, можно все что угодно считать middleWare.
Приведенный выше способ является наиболее простым, но с точки зрения управления кодом менее предпочтительным: обертка может быть разбросана по всему приложению, динамически подключать и отключать сложно, переписывать и изменять тоже придется в разных местах, дублирование кода и другие «прелести». В общем, способ подходит либо как временное решение, либо в очень маленьких проектах.
А теперь перейдем ко второму способу – использование функции applyMiddleware.


applyMiddleware

applyMiddleware – это функция из библиотеки redux. Функция реализует store enhancer (расширитель хранилища… не знаю, как по-русски это обозвать, пусть будет так).
Store Enhancer является отдельной темой для обсуждения, нас сейчас интересует только то, как applyMiddleware делает прослойку, а не то как реализовываются enhancers.
applyMiddleware принимает в качестве параметров одну или более функций, составляет из этих функций цепочку вызовов (в цепочку входит оригинальный dispatch) и заменяет в store оригинальный dispatch на результирующую цепочку.
applyMiddleware, как я уже говорил является enhancer и применяется при создании хранилища.
//…
const middleware = applyMiddleware(func1, func2);
const store = createStore(reducerFunc, middleware);
//…
В упрощенном варианте цепочка вызовов будет выглядеть так: func1 -> func2 -> dispatch(action). То есть цепочка выполняется слева на право. Как правило функции друг о друге ничего не знают, они просто вызывают следующую функцию (или не вызывают, блокируя вызовы).
Функции, передаваемые в applyMiddleware, должны удовлетворять определенной сигнатуре:
({ getState, dispatch }) => next => action
Далее, для лучшего понимания я буду использовать полный синтаксис, вместо стрелочного. Но поскольку синтаксис большинства middleware (по крайней мере на этапе формирования цепочки) будет одинаковым, то лучше использовать стрелочную нотацию (пример также будет далее).
В функцию первым делом передаются функции getState и dispatch. Их использование – опционально, необходимо исходить из задачи. Например, getState можно использовать для получения текущего состояния store и далее, после вызова dispatch, для сравнения изменений или, к примеру, для логирования.
function func1({getState, dispatch}) {             
   console.log(getState());             
   /// …
}
func1 должна вернуть функцию, которая принимает параметр следующей функции вызова.
function func1({getState, dispatch}) {             
   console.log(getState());             
    return function receiveNext(next){
         //…             
     } 
}
applyMiddleware после запуска func1 вызовет результирующую функцию, а в параметр передаст следующую функцию в цепочке (не совсем точно, чуть далее объясню). После выполнения у нас опять должна вернуться функция, но уже принимающая параметр action. Эта функция и реализовывает прослойку, она же является dispatch’ером (она и передается как next).
function func1({getState, dispatch}) {
      return function receiveNext(next){
               return processAction(action){
                       //…                           
                       next(action);                       
                }
      }
}
Здесь я добавил инструкцию next(action), которая запускает следующий dsipatch’ер. Хотя использование необязательное, в этом случае цепочка запуска прервется и в reducer ничего не передастся.
Это и есть полная сигнатура middleWare-функции. В упрощенном виде она выглядит вот так:
const func1 = ({getState, dispatch}) => next => action => {
     //..   
    next(action)
}
С сигнатурой разобрались. Запоминать ее не нужно, она всегда одинаковая, ее всегда можно найти в документации к redux.


Как applyMiddleware строит цепочку из функций?

Как я и сказал выше, первым делом каждая функция запускается по отдельности и ей передается 2 параметра – getState и dispatch (в оригинале реализовано это через Array.map, но для наглядности здесь и далее я буду приводить примеры с ручным перечислением/запуском):
const receiveNextF1 = func1({getState, dispatch});
const receiveNextF2 = func2({getState, dispatch});
На выходе мы имеем две функции, которые принимают функцию в параметре next ( return function receiveNext(next){ } ).
Далее выполнение начинается с правой крайней функции – func2 (а точнее с результатом ее выполнения).
applyMiddleware запускает результирующую функцию (receiveNextF2) и в параметр next передает оригинальный dispatch:
const dispatcherF2 = receiveNextF2(dispatch);
в dispatcherF2 будет вот такой код (next я заменил на dispatch):
processAction(action){ 
    dispatch(action);
}
Далее тоже самое делается для следующей функции, но в качестве параметра next передается результат предыдущего запуска:
const dispatcherF1 = receiveNextF1(dispatcherF2);
в dispatcherF1 будет вот такой код:
processAction(action){
  dispatcherF2(action);
}
После того, как обработана вся цепочка, dispatherF1 сохраняется в объекте store как свойство dispatch. Теперь при вызове store.dispatch() запускается сначала dispatcherF1, он запускает dispatcherF2 и последний запускает оригинальный dispatch, который в свою очередь запускает reducer.
Вот так строится цепочка middleWare на базе функции applyMiddleware.


Пример

В завершение приведу пример прослойки для вызова сразу нескольких actions (взял у Freeman’а). Запускаются они, конечно, не одновременно, а последовательно. Является хорошей альтернативой запуска нескольких dispatch.
const multiActions = ({dispatch, getState}) => next => action => {
    if (Array.isArray(action)) {
        action.forEach(a => next(a));
    }
    else { next(action); }
}
export default createStore(reducer, applyMiddleware(multiActions));
Теперь мы можем в dispatch передать несколько actions:
dispatch([{type: “STORE”, payload: this.state.request }, {type: “CLOSE_FORM”} ])
Всю эту жуть вида [{},{}] конечно же нужно заменить на actionCreators, тогда получится более лаконичный код:
dispatch([createRequest(request), closeForm()])
На этом все.

Ссылки:

Комментариев нет: