Spec-Zone.ru › Redux 3

Среднезависимый слой

Вы видели среднезависимый слой в действии в примере Асинхронные действия. Если вы использовали библиотеки для серверной стороны, такие как Express и Koa, вы, вероятно, уже знакомы с концепцией среднезависимого слоя. В этих фреймворках среднезависимый слой — это код, который можно поместить между фреймворком, принимающим запрос, и фреймворком, генерирующим ответ. Например, среднезависимый слой Express или Koa может добавлять заголовки CORS, ведение логов, сжатие и многое другое. Лучшая особенность среднезависимого слоя заключается в том, что он компонуется в цепочке. В одном проекте можно использовать несколько независимых сторонних среднезависимых слоёв.

Среднезависимый слой Redux решает другие проблемы, чем среднезависимый слой Express или Koa, но концептуально аналогичным образом. Он предоставляет точку расширения сторонних разработчиков между отправкой действия и моментом, когда оно достигает редьюсера. Люди используют среднезависимый слой Redux для ведения логов, отслеживания ошибок, взаимодействия с асинхронным API, маршрутизации и других задач.

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

Понимание среднезависимого слоя

Хотя среднезависимый слой можно использовать для различных целей, включая асинхронные API-вызовы, очень важно понять, откуда он берётся. Мы расскажем вам о ходе мысли, ведущем к среднезависимому слою, используя ведение логов и отслеживание ошибок в качестве примеров.

Задача: Ведение логов

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

Разве не было бы здорово, если бы мы вели логи всех действий, происходящих в приложении, вместе с вычисленным после него состоянием? Когда что-то пойдёт не так, мы можем посмотреть в свой лог и понять, какое действие испортило состояние.

Как мы подходим к этой задаче с помощью Redux?

Попытка #1: Ручное ведение логов

Наиболее простое решение — просто самостоятельно записывать действие и следующее состояние каждый раз, когда вызываете store.dispatch(action). Это не совсем решение, но всего лишь первый шаг к пониманию проблемы.

Примечание

Если вы используете react-redux или аналогичные привязки, вы, вероятно, не будете иметь прямого доступа к экземпляру хранилища в своих компонентах. В следующих абзацах просто предположим, что вы явно передаёте хранилище.

Предположим, вызываете это при создании задачи:

store.dispatch(addTodo('Use Redux'))

Чтобы записать действие и состояние, вы можете изменить это на что-то вроде этого:

let action = addTodo('Use Redux')

console.log('dispatching', action)
store.dispatch(action)
console.log('next state', store.getState())

Это даёт нужный эффект, но вам не захочется делать это каждый раз.

Попытка #2: Обертка dispatch

Можно выделить ведение логов в функцию:

function dispatchAndLog(store, action) {
  console.log('dispatching', action)
  store.dispatch(action)
  console.log('next state', store.getState())
}

Затем вы можете использовать её везде вместо store.dispatch():

dispatchAndLog(store, addTodo('Use Redux'))

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

Попытка #3: Модификация dispatch

Что, если мы просто заменим функцию dispatch в экземпляре хранилища? Хранилище Redux — это просто обычный объект с несколькими методами, и мы работаем с JavaScript, поэтому мы можем просто модифицировать реализацию dispatch:

let next = store.dispatch
store.dispatch = function dispatchAndLog(action) {
  console.log('dispatching', action)
  let result = next(action)
  console.log('next state', store.getState())
  return result
}

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

Задача: Отслеживание сбоев

Что, если мы хотим применить более одной такой трансформации к dispatch?

Другая полезная трансформация, которая приходит в голову, — это отчёт об ошибках JavaScript в производстве. Глобальное событие window.onerror не надёжно, потому что в некоторых старых браузерах оно не предоставляет информацию о стеке, что важно для понимания причины возникновения ошибки.

Разве не было бы полезно, если бы каждый раз, когда при отправке действия была выброшена ошибка, мы отправляли её в службу отслеживания ошибок, такую как Sentry, вместе со стеком вызовов, действием, вызвавшим ошибку, и текущим состоянием? Это значительно упрощает воспроизведение ошибки в разработке.

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

Если ведение логов и отслеживание ошибок — это отдельные утилиты, они могут выглядеть так:

function patchStoreToAddLogging(store) {
  let next = store.dispatch
  store.dispatch = function dispatchAndLog(action) {
    console.log('dispatching', action)
    let result = next(action)
    console.log('next state', store.getState())
    return result
  }
}

function patchStoreToAddCrashReporting(store) {
  let next = store.dispatch
  store.dispatch = function dispatchAndReportErrors(action) {
    try {
      return next(action)
    } catch (err) {
      console.error('Caught an exception!', err)
      Raven.captureException(err, {
        extra: {
          action,
          state: store.getState()
        }
      })
      throw err
    }
  }
}

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

patchStoreToAddLogging(store)
patchStoreToAddCrashReporting(store)

Всё равно это не очень хорошо.

Попытка #4: Скрытие модификации

Модификация — это хак. «Заменить любой метод, который вам нравится», какой это API? Давайте определим его суть вместо этого. Ранее наши функции заменяли store.dispatch. Что если они возвращали вместо этого новую функцию dispatch?

function logger(store) {
  let next = store.dispatch

  // Previously:
  // store.dispatch = function dispatchAndLog(action) {

  return function dispatchAndLog(action) {
    console.log('dispatching', action)
    let result = next(action)
    console.log('next state', store.getState())
    return result
  }
}

Мы могли бы предоставить вспомогательную функцию в Redux, которая бы выполняла фактическую модификацию как деталь реализации:

function applyMiddlewareByMonkeypatching(store, middlewares) {
  middlewares = middlewares.slice()
  middlewares.reverse()

  // Transform dispatch function with each middleware.
  middlewares.forEach(middleware =>
    store.dispatch = middleware(store)
  )
}

Мы могли бы использовать её для применения нескольких среднезависимых слоёв так:

applyMiddlewareByMonkeypatching(store, [logger, crashReporter])

Однако это всё ещё модификация.
Факт, что мы скрыли её внутри библиотеки, ничего не меняет.

Попытка #5: Удаление модификации

Зачем нам вообще перезаписывать dispatch? Конечно, чтобы иметь возможность вызывать её позже, но есть и другая причина: чтобы каждый среднезависимый слой мог получить доступ (и вызвать) ранее обернутую функцию store.dispatch:

function logger(store) {
  // Must point to the function returned by the previous middleware:
  let next = store.dispatch

  return function dispatchAndLog(action) {
    console.log('dispatching', action)
    let result = next(action)
    console.log('next state', store.getState())
    return result
  }
}

Это важно для цепочки среднезависимых слоёв!

Если applyMiddlewareByMonkeypatching не присваивает store.dispatch сразу после обработки первого среднезависимого слоя, store.dispatch будет продолжать указывать на исходную функцию dispatch. Тогда второй среднезависимый слой также будет связан с исходной функцией dispatch.

Но есть и другой способ обеспечить цепочку. Среднезависимый слой может принимать функцию dispatch next() в качестве параметра вместо чтения её из экземпляра store.

function logger(store) {
  return function wrapDispatchToAddLogging(next) {
    return function dispatchAndLog(action) {
      console.log('dispatching', action)
      let result = next(action)
      console.log('next state', store.getState())
      return result
    }
  }
}

Это момент «нам нужно углубиться», поэтому это может занять некоторое время, чтобы это стало понятным. Функциональная цепочка кажется пугающей. Стрелочные функции ES6 упрощают это каррирование для глаз:

const logger = store => next => action => {
  console.log('dispatching', action)
  let result = next(action)
  console.log('next state', store.getState())
  return result
}

const crashReporter = store => next => action => {
  try {
    return next(action)
  } catch (err) {
    console.error('Caught an exception!', err)
    Raven.captureException(err, {
      extra: {
        action,
        state: store.getState()
      }
    })
    throw err
  }
}

Вот как выглядит среднезависимый слой Redux.

Теперь среднезависимый слой принимает функцию next() dispatch и возвращает функцию dispatch, которая в свою очередь служит next() для среднезависимого слоя слева и так далее. Всё ещё полезно иметь доступ к некоторым методам хранилища, таким как getState(), поэтому store остаётся доступным в качестве аргумента верхнего уровня.

Попытка #6: Применение среднезависимого слоя простым способом

Вместо applyMiddlewareByMonkeypatching(), мы могли бы написать applyMiddleware(), которое сначала получит окончательную, полностью обернутую функцию dispatch() и вернёт копию хранилища, используя её:

// Warning: Naïve implementation!
// That's *not* Redux API.
function applyMiddleware(store, middlewares) {
  middlewares = middlewares.slice()
  middlewares.reverse()
  let dispatch = store.dispatch
  middlewares.forEach(middleware =>
    dispatch = middleware(store)(dispatch)
  )
  return Object.assign({}, store, { dispatch })
}

Реализация applyMiddleware(), которая поставляется с Redux, похожа, но отличается в трёх важных аспектах:

  • Она предоставляет только подмножество API хранилища среднезависимому слою: dispatch(action) и getState().

  • Она немного хитрит, чтобы убедиться, что если вы вызываете store.dispatch(action) из своего среднезависимого слоя вместо next(action), действие фактически пройдёт через всю цепочку среднезависимых слоёв, включая текущий. Это полезно для асинхронных среднезависимых слоёв, как мы видели ранее.

  • Для обеспечения того, чтобы вы могли применять среднезависимый слой только один раз, она работает с createStore() вместо store. Вместо (store, middlewares) => store, её сигнатура — (...middlewares) => (createStore) => createStore.

Поскольку применение функций к createStore() до использования неудобно, createStore() принимает необязательный последний аргумент для указания таких функций.

Окончательный подход

Учитывая среднезависимый слой, который мы только что написали:

const logger = store => next => action => {
  console.log('dispatching', action)
  let result = next(action)
  console.log('next state', store.getState())
  return result
}

const crashReporter = store => next => action => {
  try {
    return next(action)
  } catch (err) {
    console.error('Caught an exception!', err)
    Raven.captureException(err, {
      extra: {
        action,
        state: store.getState()
      }
    })
    throw err
  }
}

Вот как применить его к хранилищу Redux:

import { createStore, combineReducers, applyMiddleware } from 'redux'

let todoApp = combineReducers(reducers)
let store = createStore(
  todoApp,
  // applyMiddleware() tells createStore() how to handle middleware
  applyMiddleware(logger, crashReporter)
)

Всё готово! Теперь любые действия, отправленные в экземпляр хранилища, будут проходить через logger и crashReporter:

// Will flow through both logger and crashReporter middleware!
store.dispatch(addTodo('Use Redux'))

Семь примеров

Если ваша голова вскипела от чтения предыдущего раздела, представьте, что было бы писать его. Этот раздел предназначен для отдыха и вам, и мне, и поможет вам заставить ваши шестерёнки работать.

Каждая функция ниже является допустимым среднезависимым слоем Redux. Они не одинаково полезны, но, по крайней мере, они одинаково забавны.

/**
 * Logs all actions and states after they are dispatched.
 */
const logger = store => next => action => {
  console.group(action.type)
  console.info('dispatching', action)
  let result = next(action)
  console.log('next state', store.getState())
  console.groupEnd(action.type)
  return result
}

/**
 * Sends crash reports as state is updated and listeners are notified.
 */
const crashReporter = store => next => action => {
  try {
    return next(action)
  } catch (err) {
    console.error('Caught an exception!', err)
    Raven.captureException(err, {
      extra: {
        action,
        state: store.getState()
      }
    })
    throw err
  }
}

/**
 * Schedules actions with { meta: { delay: N } } to be delayed by N milliseconds.
 * Makes `dispatch` return a function to cancel the timeout in this case.
 */
const timeoutScheduler = store => next => action => {
  if (!action.meta || !action.meta.delay) {
    return next(action)
  }

  let timeoutId = setTimeout(
    () => next(action),
    action.meta.delay
  )

  return function cancel() {
    clearTimeout(timeoutId)
  }
}

/**
 * Schedules actions with { meta: { raf: true } } to be dispatched inside a rAF loop
 * frame.  Makes `dispatch` return a function to remove the action from the queue in
 * this case.
 */
const rafScheduler = store => next => {
  let queuedActions = []
  let frame = null

  function loop() {
    frame = null
    try {
      if (queuedActions.length) {
        next(queuedActions.shift())
      }
    } finally {
      maybeRaf()
    }
  }

  function maybeRaf() {
    if (queuedActions.length && !frame) {
      frame = requestAnimationFrame(loop)
    }
  }

  return action => {
    if (!action.meta || !action.meta.raf) {
      return next(action)
    }

    queuedActions.push(action)
    maybeRaf()

    return function cancel() {
      queuedActions = queuedActions.filter(a => a !== action)
    }
  }
}

/**
 * Lets you dispatch promises in addition to actions.
 * If the promise is resolved, its result will be dispatched as an action.
 * The promise is returned from `dispatch` so the caller may handle rejection.
 */
const vanillaPromise = store => next => action => {
  if (typeof action.then !== 'function') {
    return next(action)
  }

  return Promise.resolve(action).then(store.dispatch)
}

/**
 * Lets you dispatch special actions with a { promise } field.
 *
 * This middleware will turn them into a single action at the beginning,
 * and a single success (or failure) action when the `promise` resolves.
 *
 * For convenience, `dispatch` will return the promise so the caller can wait.
 */
const readyStatePromise = store => next => action => {
  if (!action.promise) {
    return next(action)
  }

  function makeAction(ready, data) {
    let newAction = Object.assign({}, action, { ready }, data)
    delete newAction.promise
    return newAction
  }

  next(makeAction(false))
  return action.promise.then(
    result => next(makeAction(true, { result })),
    error => next(makeAction(true, { error }))
  )
}

/**
 * Lets you dispatch a function instead of an action.
 * This function will receive `dispatch` and `getState` as arguments.
 *
 * Useful for early exits (conditions over `getState()`), as well
 * as for async control flow (it can `dispatch()` something else).
 *
 * `dispatch` will return the return value of the dispatched function.
 */
const thunk = store => next => action =>
  typeof action === 'function'
    ? action(store.dispatch, store.getState)
    : next(action)

// You can use all of them! (It doesn't mean you should.)
let todoApp = combineReducers(reducers)
let store = createStore(
  todoApp,
  applyMiddleware(
    rafScheduler,
    timeoutScheduler,
    thunk,
    vanillaPromise,
    readyStatePromise,
    logger,
    crashReporter
  )
)

© 2015–2017 Dan Abramov
Licensed under the MIT License.
http://redux.js.org/docs/advanced/Middleware.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API