Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →
Middleware
Ya has visto el middleware en acción en el tutorial "Fundamentos de Redux". Si has usado bibliotecas del lado del servidor como Express o Koa, probablemente ya estés familiarizado con el concepto de middleware. En estos frameworks, el middleware es código que puedes colocar entre el momento en que el framework recibe una solicitud y cuando genera una respuesta. Por ejemplo, el middleware de Express o Koa puede añadir encabezados CORS, registro de eventos, compresión y más. La mejor característica del middleware es que es componible en cadena. Puedes usar múltiples middlewares de terceros independientes en un solo proyecto.
El middleware de Redux resuelve problemas diferentes al de Express o Koa, pero de manera conceptualmente similar. Proporciona un punto de extensión de terceros entre el despacho de una acción y el momento en que llega al reducer. La gente usa middleware de Redux para registro de eventos, informes de fallos, comunicación con APIs asíncronas, enrutamiento y más.
Este artículo se divide en una introducción detallada para ayudarte a comprender el concepto, y algunos ejemplos prácticos que muestran el poder del middleware al final. Puede resultarte útil alternar entre ambas partes según osciles entre el aburrimiento y la inspiración.
Entendiendo el Middleware
Aunque el middleware puede usarse para diversas tareas, incluyendo llamadas API asíncronas, es muy importante que entiendas su origen. Te guiaremos a través del proceso mental que lleva al middleware, usando como ejemplos el registro de eventos y los informes de fallos.
Problema: Registro de eventos
Uno de los beneficios de Redux es que hace los cambios de estado predecibles y transparentes. Cada vez que se despacha una acción, se calcula y guarda el nuevo estado. El estado no puede cambiar por sí solo, solo puede cambiar como consecuencia de una acción específica.
¿No sería útil registrar cada acción que ocurre en la aplicación, junto con el estado calculado después de ella? Cuando algo falle, podríamos revisar nuestro registro e identificar qué acción corrompió el estado.
¿Cómo abordamos esto con Redux?
Intento #1: Registro manual
La solución más ingenua es registrar manualmente la acción y el siguiente estado cada vez que llamas a store.dispatch(action). No es realmente una solución, sino un primer paso para entender el problema.
Nota
Si estás usando react-redux o bindings similares, probablemente no tengas acceso directo a la instancia del store en tus componentes. En los próximos párrafos, asume que pasas el store explícitamente.
Por ejemplo, llamas esto al crear una tarea:
store.dispatch(addTodo('Use Redux'))
Para registrar la acción y el estado, puedes cambiarlo a algo así:
const action = addTodo('Use Redux')
console.log('dispatching', action)
store.dispatch(action)
console.log('next state', store.getState())
Esto produce el efecto deseado, pero no querrás hacerlo cada vez.
Intento #2: Encapsular dispatch
Puedes extraer el registro en una función:
function dispatchAndLog(store, action) {
console.log('dispatching', action)
store.dispatch(action)
console.log('next state', store.getState())
}
Luego puedes usarla en todas partes en lugar de store.dispatch():
dispatchAndLog(store, addTodo('Use Redux'))
Podríamos terminar aquí, pero no es muy conveniente importar una función especial cada vez.
Intento #3: Monkeypatching de dispatch
¿Y si simplemente reemplazamos la función dispatch en la instancia del store? El store de Redux es un objeto simple con algunos métodos, y como estamos escribiendo JavaScript, podemos hacer monkeypatching de la implementación de dispatch:
const next = store.dispatch
store.dispatch = function dispatchAndLog(action) {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
¡Esto ya se acerca más a lo que queremos! No importa dónde despachemos una acción, se registrará garantizadamente. El monkeypatching nunca se siente bien, pero por ahora podemos tolerarlo.
Problema: Informes de fallos
¿Y si quisiéramos aplicar más de una transformación de este tipo a dispatch?
Otra transformación útil que me viene a la mente es reportar errores de JavaScript en producción. El evento global window.onerror no es fiable porque en algunos navegadores antiguos no proporciona información de la pila de llamadas, algo crucial para entender por qué ocurre un error.
¿No sería útil que, cada vez que se lance un error al despachar una acción, lo enviáramos a un servicio de reporte de fallos como Sentry con el stack trace, la acción que causó el error y el estado actual? Así sería mucho más fácil reproducir el error en desarrollo.
Sin embargo, es importante mantener el logging y el crash reporting separados. Idealmente, deberían ser módulos diferentes, potencialmente en paquetes distintos. De lo contrario, no podemos tener un ecosistema de estas utilidades. (Pista: ¡poco a poco llegamos al concepto de middleware!)
Si el logging y el crash reporting son utilidades separadas, podrían verse así:
function patchStoreToAddLogging(store) {
const 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) {
const 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
}
}
}
Si estas funciones se publican como módulos separados, podemos usarlas más tarde para parchear nuestro store:
patchStoreToAddLogging(store)
patchStoreToAddCrashReporting(store)
Aún así, esto no es elegante.
Intento #4: Ocultando el Monkeypatching
El monkeypatching es un truco. "Reemplaza cualquier método que quieras", ¿qué clase de API es esa? Mejor comprendamos su esencia. Antes, nuestras funciones reemplazaban store.dispatch. ¿Y si en su lugar devolvieran la nueva función dispatch?
function logger(store) {
const 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
}
}
Podríamos crear un helper dentro de Redux que aplique el monkeypatching como detalle de implementación:
function applyMiddlewareByMonkeypatching(store, middlewares) {
middlewares = middlewares.slice()
middlewares.reverse()
// Transform dispatch function with each middleware.
middlewares.forEach(middleware => (store.dispatch = middleware(store)))
}
Podríamos usarlo para aplicar múltiples middleware así:
applyMiddlewareByMonkeypatching(store, [logger, crashReporter])
Sin embargo, sigue siendo monkeypatching. El hecho de que lo ocultemos dentro de la biblioteca no altera este hecho.
Intento #5: Eliminando el Monkeypatching
¿Por qué sobrescribimos dispatch? Claro, para poder llamarlo después, pero también hay otra razón: para que cada middleware pueda acceder (y llamar) al store.dispatch previamente envuelto:
function logger(store) {
// Must point to the function returned by the previous middleware:
const next = store.dispatch
return function dispatchAndLog(action) {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
}
¡Esto es esencial para encadenar middleware!
Si applyMiddlewareByMonkeypatching no asigna store.dispatch inmediatamente después de procesar el primer middleware, store.dispatch seguirá apuntando a la función dispatch original. Entonces el segundo middleware también estará vinculado a la función dispatch original.
Pero hay otra forma de habilitar el encadenamiento. El middleware podría aceptar la función next() de dispatch como parámetro, en lugar de leerla de la instancia 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
}
}
}
Es un momento de “necesitamos profundizar más”, así que puede llevar un tiempo asimilarlo. La cascada de funciones parece intimidante. Las funciones flecha hacen que este currying sea más legible:
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
}
}
Esto es exactamente cómo luce el middleware de Redux.
Ahora el middleware toma la función next() de dispatch y devuelve una función dispatch, que a su vez sirve como next() para el middleware de la izquierda, y así sucesivamente. Sigue siendo útil tener acceso a métodos del store como getState(), por lo que store permanece disponible como argumento principal.
Intento #6: Aplicando el Middleware de Forma Ingenua
En lugar de applyMiddlewareByMonkeypatching(), podríamos escribir applyMiddleware() que primero obtenga la función dispatch() final completamente envuelta, y devuelva una copia del store usándola:
// 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 })
}
La implementación de applyMiddleware() que incluye Redux es similar, pero difiere en tres aspectos importantes:
-
Solo expone un subconjunto de la API del store al middleware:
dispatch(action)ygetState(). -
Hace un poco de magia para asegurar que si llamas a
store.dispatch(action)desde tu middleware en lugar denext(action), la acción recorrerá nuevamente toda la cadena de middlewares, incluyendo el actual. Esto es útil para middleware asíncrono. Hay una advertencia cuando se llama adispatchdurante la configuración, descrita a continuación. -
Para garantizar que solo puedas aplicar middleware una vez, opera sobre
createStore()en lugar de sobre elstoremismo. En lugar de(store, middlewares) => store, su firma es(...middlewares) => (createStore) => createStore.
Dado que es engorroso aplicar funciones a createStore() antes de usarlo, createStore() acepta un último argumento opcional para especificar dichas funciones.
Advertencia: Despacho durante la configuración
Mientras applyMiddleware se ejecuta y configura tu middleware, la función store.dispatch apuntará a la versión básica proporcionada por createStore. Despachar resultaría en que no se aplique ningún otro middleware. Si esperas una interacción con otro middleware durante la configuración, probablemente te decepcionarás. Debido a este comportamiento inesperado, applyMiddleware lanzará un error si intentas despachar una acción antes de completar la configuración. En su lugar, deberías comunicarte directamente con ese otro middleware mediante un objeto común (para un middleware de llamadas API, podría ser tu objeto cliente API) o esperar hasta después de construir el middleware con una devolución de llamada.
El enfoque final
Dado este middleware que acabamos de escribir:
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
}
}
Así es como aplicarlo a un store de Redux:
import { createStore, combineReducers, applyMiddleware } from 'redux'
const todoApp = combineReducers(reducers)
const store = createStore(
todoApp,
// applyMiddleware() tells createStore() how to handle middleware
applyMiddleware(logger, crashReporter)
)
¡Eso es todo! Ahora cualquier acción despachada a la instancia del store fluirá a través de logger y crashReporter:
// Will flow through both logger and crashReporter middleware!
store.dispatch(addTodo('Use Redux'))
Siete ejemplos
Si tu cabeza hirvió al leer la sección anterior, imagina lo que fue escribirla. Esta sección pretende ser un momento de relajación para ambos, y ayudará a poner tus engranajes en movimiento.
Cada función a continuación es un middleware válido de Redux. No son igualmente útiles, pero al menos son igual de divertidas.
/**
* 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()
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)
}
const 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 => {
const 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) {
const 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.)
const todoApp = combineReducers(reducers)
const store = createStore(
todoApp,
applyMiddleware(
rafScheduler,
timeoutScheduler,
thunk,
vanillaPromise,
readyStatePromise,
logger,
crashReporter
)
)