среда, 16 октября 2019 г.

Redux: усилители (enhancers).

Введение


В данной статье хотел бы рассказать про усилители в Redux: Что это такое? Какие задачи решают? Как реализованы?

Enhancer можно назвать усилителем, но мне такое название не нравится (Что он там усиливает?!). Больше подходит название «расширитель» или «расширение». 
Собственно, enhancer это функция, позволяющая добавить к хранилищу дополнительный функционал. 
Исходя из назначения, название «расширитель» подходит больше. Но как бы оно там не называлось, наша задача понять, как работает enhancer, а не выбрать название.

А какие задачи у Enhancer? И тут повторюсь – добавить или изменить функциональность хранилища. Функциональность хранилища меняется путем изменения текущих свойств или добавлением новых.
Например, applyMiddleware (единственный enhancer в составе redux) делает обертку dispatch, заменяя оригинальное одноименное свойство. Как делается обертка можно посмотреть здесь.
Также можно добавить дополнительное свойство к хранилищу, например, dispatchAsync (пример в конце статьи).

Теперь перейдем от назначения к реализации.


Реализация.


Как вообще enhancer применяется к хранилищу? А применяется он во время создания хранилища:
createStore(reducer, initialState, enhancer())
или
createStore(reducer, enhancer())
То есть передается либо вторым, либо третьим аргументом в функцию createStore. И заметьте, передается именно результат выполнения, а не сама функция.

Какая сигнатура у enhancer? Функция enhancer должна вернуть функцию, которая принимает функцию createStore в качестве параметра):
function enhancer1(optionalParams){
return function receiveCreateStore(createStoreFunc){
        }

}
Далее функция (у нас это receiveCreateStore), принимающая createStore (через createStoreFunc), должна вернуть функцию (createEnhancedStore). Эта функция и должна вернуть объект-хранилище.
function enhancer1(){
return function receiveCreateStore(createStoreFunc){
return function createEnhancedStore(…args){
return {
                             // возвращаем хранилище
                             // по задумке - расширенное
                        }
               }

       }
}
Обратите внимание на аргументы функции – args. Сюда будут передаваться 2 параметра: reducer и initialState. Первый и второй аргумент функции createStore.
Это была сигнатура. 

Как правило из последней функции (у нас createEnhancedStore) вызывается опять createStore (скажем так, стандартный… далее объясню) и уже ее результат используется для «обертывания» или дополнения.
Поэтому в сигнатуру я еще добавлю вызов функции createStore и возвращение нового – измененного store. Итого получится следующее:
function enhancer1(){
return function receiveCreateStore(createStoreFunc){
return function createEnhancedStore(…args){
const store = createStoreFunc(…args);
return {
…store,
dispatch: _dispatch,  // замена оригинального dispatch
newDispatch: _newDispatch // добавление нового свойства

                        }
               }
       }
}
Это был готовый каркас функции-расширителя. В таком виде можно и использовать.
А вот упрощенная версия с использованием стрелочной нотации:
const enhancer = () => createStoreFunc => (…args) => {
const store = createStoreFunc(…args);
        return {
…store,
dispatch: _dispatch,  // замена оригинального dispatch
newDispatch: _newDispatch // добавление нового свойства/метода
        }
}
Теперь нам нужно обратиться к исходникам redux, чтобы понять: почему именно такая сигнатура, зачем передается createStore, зачем она вызывается повторно и другие детали реализации.

Оперировать будем исходниками createStore, enhancer’ом applyMiddleware и приведенным каркасом. Потом приведу пример для добавления нового метода к хранилищу (взял опять-таки у Freeman’а).

Сначала возьмем функцию createStore.
 
Нас интересует:
А) кусок 1, где описаны параметры, 
Б) кусок 2, где проверяется, был ли передан enhancer
В) и кусок 3, где вызывается enhancer.

Логика такая: если был передан enhancer, то запускается сначала он и функция createStore передает ему сама себя в качестве параметра.
Enhancer вернет функцию, которая принимает аргументы и возвращает объект store:
return function createEnhancedStore(…args){
    return {
        // возвращаем хранилище
    }
}
Далее createStore запустит возвращенную функцию и передаст ей два аргумента: reducer и initialState (через …args):
 
Это мы дошли до последней функции в каркасе, которая собственно и расширяет хранилище.
Как она это делает? Обычно используется функция createStore которая передавалась при первом запуске enhancer (подчеркну, обычно!). createStore запускается с переданными параметрами (reducer и preloadedState): 
const store = createStoreFunc(…args);
Управление передается «оригинальной» функции createStore, но теперь она запускается только с двумя параметрами, то есть без enhancer.
Поскольку enhancer пустой, то отмеченный на картинке вызов пропускается:
 
Во время выполнения «собирается» store и он же, как объект, возвращается enhancer’у:

 Теперь у нас есть созданный store и можем его расширить:
const enhancer = () => createStoreFunc => (…args) => {
const store = createStoreFunc(…args);
        return {
…store,
dispatch: _dispatch,  // замена оригинального dispatch
newDispatch: _newDispatch // добавление нового свойства/метода
        }
}
Давайте теперь на примере applyMiddleware посмотрим как он делает обертку для dispatch.
 

1) Это не особо относится к делу – здесь формируется массив из аргументов функции applyMiddleware.
2) Тут сразу возвращается функция, принимающая createStore.
3) Здесь функция createStore продолжает выполнение и запускает функцию из пункта 2, передает ей createStore, в ответ получает функцию. Здесь она указывается без параметров, а в «нашем» каркасе с параметрами args. Но на самом деле потом все равно используются параметры arguments.
4) И вот enhancer как раз вызывает «оригинальную» функцию createStore, полученную на шаге 2, передавая ей аргументы reducer и initialState. Как мы помним опять запускается createStore, но уже без вызова enhancer. В переменной store храним результат выполнения.
5) В этом куске делается обертка для dispatch.
6) Возвращается хранилище store (созданное на этапе 4) с усиленным dispatch.


Пример.


Как и обещал, пример добавления нового метода к хранилищу:

export const asyncEnhancer = delay => createStoreFunction => (...args) => {
    const store = createStoreFunction(...args);
    return {
        ...store,
        dispatchAsync: (action) => new Promise((resolve, reject) => {
            setTimeout(() => {
                store.dispatch(action);
                resolve();
            }, delay);
        })
    };
}
Здесь возвращается оригинальный store, но с новым методом - dispatchAsync, который запускает "обычный", но с задержкой.

На этом все.

Ссылки:

вторник, 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()])
На этом все.

Ссылки: