React 学习之 Redux 中间件

704 阅读9分钟

Middleware

中间件这个词已经不算陌生了,因为在使用 Express 搭建后台服务时,就写过相关功能的中间件:用于后台系统相关操作的权限验证,无权限则将直接返回 403 及相关提示信息

它的一个优秀特性就是可以 链式组合 (如 Express 的 next() 方法可以将本中间件处理的结果交给下个中间件进行后续处理)

Redux Middleware

Redux 的 middleware 就是用于解决不同的问题的,与上面 Express 中间件的概念是类似的,但它提供的是 action 发起之后,到达 reducer 之前的功能扩展。因而我们可以利用 Redux middleware 来进行日志记录、创建崩溃报告、调用异步接口或者路由等等

它类似于插件,可以在不影响原本功能,并且不改动原本代码的基础上,对其功能进行增强。在 Redux 中,中间件主要用于增强 dispatch 函数。实现 Redux 中间件的 基本原理 就是更改 store 的 dispatch 函数

Redux 中间件书写

中间件本身是一个函数,该函数接受一个 store 参数,表示创建的仓库,该仓库并非一个完整的仓库对象,仅仅包含 getStatedispatch (这是由于 applyMiddleware 函数只会将此对象的这两个属性传递进来)。该函数的运行时间是:在创建仓库之后

// redux: createStore.js
export default function createStore(
    reducer,
    preloadedState,
    enhancer
) {
    /* source code */
}

如上,createStore 方法可以接受三个参数:reducer[preloadedState]enhancer

  • reducer (Function):接受两个参数 (state, action),返回新的 state
  • [preloadedState] (any):作为创建 store 时的初始状态值 (可不传递)
  • enhancer (Function):它是一个组合 store creator 的函数,返回一个新的经过强化的 store creator,它与 middleware 相似,允许通过复合函数来改变 store 接口

第二个参数传递函数,不传递第三个参数时,会将此函数作为第三个参数使用 (内部会校验传递的函数是否符合它的要求),并将仓库的默认状态值置为 undefined

// 普通用法
import {createStore} from 'redux'
const initialState = {}
// 第二个参数传递非函数,则作为 store 的初始化状态
const store = createStore(reducer, initialState)

console.log(store.getState()) // 打印:{}

由于创建仓库后需要自动运行设置的中间件函数,因而我们需要在创建仓库时就告诉它该运行的中间件,即通过调用 Redux 提供的 applyMiddleware 函数,并将此函数的返回值传递给 createStore 函数的 第二个参数 (只传两个参数时)第三个参数 (第二个参数为状态初始值)

import {createStore, applyMiddleware} from 'redux'
import reducer from './reducer'

/* applyMiddleware 需要的中间件函数写法 */
function logger(store) {
    /* 分发 action 的日志记录 */
    // next() 是去触发下一个中间件的 dispatch 函数 (洋葱模型)
    return function(next) {
        // 返回最终要应用的 dispatch 函数
        return function(action) {
            console.log('之前的状态:', store.getState())
            console.log('分发的 Action:', action)
            // ⚠️注意:如果在这里调用 store.dispatch 分发 action
            // 由于链式调用的关系,会无限递归导致浏览器卡死
            next(action)
            console.log('之后的状态:', store.getState())
        }
    }
}

// 因而,logger 可以使用 箭头函数进行简写:
const logger = store => next => action => {
    console.log('之前的状态:', store.getState())
    console.log('分发的 Action:', action)
    next(action)
    console.log('之后的状态:', store.getState())
}

function catchError(store) {
    /* 错误捕获 */
}

// 应用中间件,方式 1:
// 多个中间件依次传入此函数即可
const store = createStore(reducer, applyMiddleware(logger, catchError))

applyMiddleware 函数:用于记录需要应用哪些中间件,它返回一个 函数 a函数 a 用于记录创建仓库的方法,给 函数 a 传入创建仓库的函数 createStore 会返回 函数 b函数 b 接受 reducer 创建返回仓库对象 store

import {createStore, applyMiddleware} from 'redux'
import reducer from './reducer'

// 应用中间件,方式 2 (一般不会这么写):
const storeCreator = applyMiddleware(logger, catchError)

const store = storeCreator(createStore)(reducer)

栗子:日志记录与错误捕获

当我们想要在每次分发 action 改变数据时,均希望打印日志记录 之前的状态分发的 action之后的状态。当开发阶段出现数据异常,我们就可以通过日志记录查看是哪个 action 导致的状态异常

1. 手动添加

从需求点出发,我们最直接的解决方案就是在 store.dispatch(action) 前后手动添加打印日志的代码 (为了这个点去实现,我们还需要去每个 store.dispatch(action) 前后均去加相同的重复代码 <然而它还算不上是一个解决方案>)

2. 封装 dispatch 函数

基于上面一点的思考,我们可以考虑去封装 dispatch 函数,使它能够做到打印前后状态的功能,并能分发 action (它已经可以实现需求点的功能分离,但我们还需要在每次使用时进行导入)

function loggerDispatch(store, action) {
    console.log('之前的状态:', store.getState())
    console.log('分发的 Action:', action)
    store.dispatch(action)
    console.log('之后的状态:', store.getState())
}
// 后续在分发 action 时,替换 store.dispatch(action) 方法,如:
// store.dispatch(createAddUserAction({id: 1008, name: 'K.'}))
loggerDispatch(store, createAddUserAction({id: 1008, name: 'K.'}))

3. 覆盖 store.dispatch 函数

通过上一篇文章:《手写实现 createStore ...》,我们知道通过调用 createStore 返回的仅是一个普通对象,而且能在其 dispatch 函数中能够拿到之前的状态,分发的 action 以及更新后的状态,而 dispatch 仅仅是一个分发 action 的函数,那么我们就可以通过重写 store.dispatch 函数来达到打印日志的功能:

// 存储分发 action 功能的 dispatch 函数
let next = store.dispatch
store.dispatch = function loggerDispatch(store, action) {
    console.log('之前的状态:', store.getState())
    console.log('分发的 Action:', action)
    const result = next(action)
    console.log('之后的状态:', store.getState())
    return result
}

next = store.dispatch
store.dispatch = function catchErrorDispatch(store, action) {
    try {
        return next(action)
    } catch(error) {
        console.log('catch an error: ', error)
        window.alert(error.message)
        throw error
    }
}
// 那么我们后续再调用 store.dispatch 方法时
// 在任何地方都会打印日志且能捕获错误了

至此,我们已经得到了想要的结果

4. 抽离覆盖逻辑

当我们想要在分发 action 时附加不同的功能,那么我们就得根据需要去封装 覆盖 store.dispatch 方法去扩展 dispatch 函数的功能 (比如:日志打印错误捕获),我们将它们分为不同的模块导出,在需要使用某个功能时对 store 的 dispatch 方法进行覆盖 (函数用法,那自然也可以将它们顺序调用去重复覆盖从而组合所有的附加功能)

// addDispatchLogger.js
export default function addDispatchLogger(store) {
    const next = store.dispatch
    store.dispatch = function loggerDispatch(action) {
        console.log('之前的状态:', store.getState())
        console.log('分发的 Action:', action)
        const result = next(action)
        console.log('之后的状态:', store.getState())
        return result
    }
}

// catchDispatchError.js
export default function addCatchDispatchError(store) {
    const next = store.dispatch
    store.dispatch = function catchErrorDispatch(action) {
        try {
            return next(action)
        } catch(error) {
            console.log('catch an error: ', error)
            window.alert(error.message)
            throw error
        }
    }
}

// 依次调用则可以将多个功能进行组合:
addDispatchLogger(store)
addCatchDispatchError(store)

5. 改动 store 对象覆盖 ➡️ 返回新函数覆盖

上面的思想是去 覆盖 store.dispatch 函数,但我们考虑不去直接改动它,而是返回一个新的函数,再通过一个组合功能的辅助函数去组合附加的功能:

// addDispatchLogger.js
export default function addDispatchLogger(store) {
    const next = store.dispatch
    // 之前的写法:
    // store.dispatch = function loggerDispatch(action) {
    return function loggerDispatch(action) {
        console.log('之前的状态:', store.getState())
        console.log('分发的 Action:', action)
        const result = next(action)
        console.log('之后的状态:', store.getState())
        return result
    }
}

// catchDispatchError.js
export default function addCatchDispatchError(store) {
    const next = store.dispatch
    return function catchErrorDispatch(action) {
        try {
            return next(action)
        } catch(error) {
            console.log('catch an error: ', error)
            window.alert(error.message)
            throw error
        }
    }
}

// 辅助方法 applyFeatures.js
export default function applyFeatures(store, features) {
    features = features.slice()
    features.forEach(feature => {
        store.dispatch = feature(store)
    })
}

// 组合使用:
applyFeatures(store, [addDispatchLogger, addCatchDispatchError])

6. 柯里化

上面的内容均是将中间件串联起来,依次去覆盖 store.dispatch 方法,这样做的目的是我们可以在后面直接通过 store.dispatch 调用每个中间件,而且每个中间件都可以操作前一个中间件封装后的 store.dispatch 方法 (那么,若初始应用中间件或某一环覆盖 store.dispatch 方法失败,那么就可能导致之后的中间件获取到的 dispatch 方法 不是预期的结果)

本文开头描述 Express 中间件时,每个中间件函数都会接受三个参数:requestreponsenext,Express 中间件函数的 next 方法是决定是否交给下一个中间件处理

这里我们也引入 next 方法,但它是新的 dispatch 函数,用于去触发下一个中间件的 dispatch 函数,为使 redux 的中间件实现链式调用的效果 (并不会覆盖 store 本身的 dispatch 方法):

// addDispatchLogger.js
export default function addDispatchLogger(store) {
    return function withLoggerDispatch(next) {
        return function loggerDispatch(action) {
            console.log('之前的状态:', store.getState())
            console.log('分发的 Action:', action)
            const result = next(action)
            console.log('之后的状态:', store.getState())
            return result
        }
    }
}

// 使用 ES6 箭头函数 (柯里化) (看起来就有点吓人了):
const addDispatchLogger = store => next => action => {
    console.log('之前的状态:', store.getState())
    console.log('分发的 Action:', action)
    const result = next(action)
    console.log('之后的状态:', store.getState())
    return result
}

这就是 Redux middleware 的样子,middleware 接受一个 next() 作为 dispatch 函数,并返回一个新的 dispatch 函数,返回的这个函数又会作为下一个 middleware 的 next(),以此类推......

7. 实现 applyMiddleware

由上,我们可以写一个 applyMiddleware 方法替换掉之前的 applyFeatures 方法,在这个方法中,我们取得新的 dispatch 函数并进行链式传递,且返回 store 的副本:

function applyMiddleware(store, middlewares) {
    // 洋葱模型链式调用需要逆序
    middlewares = [...middlewares].reverse()
    let dispatch = store.dispatch
    middlewares.forEach(middlware => {
        // middleware 函数柯里化可见第 6 点
        dispatch = middleware(store)(dispatch)
    })
    // return Object.assign({}, store, {dispatch})
    return {
        ...store,
        dispatch
    }
}

当然,上述方法只是简单地实现中间件的应用,与 Redux 本身的实现方案是不同的,部分取自 【Redux 自述 —— Middleware】

更进一步实现 applyMiddleware

由于上文 Redux 中间件书写 中写到 applyMiddleware 函数的第二种使用方式:

const store = applyMiddleware(...middlewares)(createStore)(reducer)

接下来,让我们把它完善一下:

/**
* @param {any} middlewares 需要应用的所有中间件
*/
export default function applyMiddleware(...middlewares) {
    /**
    * @param {Fcuntion} createStore 仓库创建函数
    */
    return function (createStore) {
        /**
        * @param {Fcuntion} reducer
        * @param {any | undefined} preloadedState 仓库初始值
        */
        return function (reducer, preloadedState) {
            const store = createStore(reducer, preloadedState)
            const {getState, dispatch: storeDispatch} = store
            const simpleStore = {
                getState,
                /* 更新 dispatch */
                dispatch: (...args) => dispatch(...args)
            }
            // 初次调用,则报错处理
            let dispatch = () => {
                throw new Error('现在还不能使用 dispatch 函数')
            }
            const dispatchCreators = middlewares.map(middleware => middleware(simpleStore))
            // 给 dispatch 赋值
            dispatch = compose(...dispatchCreators)(storeDispatch)
            return {
                ...store,
                dispatch
            }
        }
    }
}

/**
* 组合多个函数,返回一个新的函数
* @param {Function[]} funcs
*/
function compose(...funcs) {
    const len = funcs.length
    if (len === 0) {
        return args => args
    } else if (len === 1) {
        return funcs[0]
    }

    return funcs.reduce((a, b) => (...args) => a(b(...args)))
    
    /*
    组合多个函数,将后一个函数返回的结果传给上一个函数进行调用
    return function (...args) {
        let result = null
        for (let i = len - 1; i >= 0; i --) {
            const func = funcs[i]
            if (i === len - 1) {
                result = func(...args)
            } else {
                result = func(result)
            }
        }
        return result
    }
    */
}

至此,中间件部分的基本内容就结束了 (那么上篇手写 createStore 方法要兼容 applyMiddleware 方法则需要进行修改)