Rematch 的内部实现(上)| 8月更文挑战

346 阅读3分钟

这是我参与8月更文挑战的第16天,活动详情查看:8月更文挑战

前言

对于熟悉 React 的同学应该不会对 Redux 陌生,只要不是很小的需求,基本都会需要用到状态管理,而 Rematch 就是一个 Redux 的替代品。

它在 React 的基础上解决了很多问题,比如模板代码过多,同一个操作对应的变量、处理方法分散在多个页面等。

首先来看看 Rematch 是如何使用的。

举个例子

import { init } from '@rematch/core'
import * as models from './models'

const count = {
    state: 0,
    reducers: { 
        increment(state, payload) {
            return state + payload
        }
    },
    effects: {
        async incrementAsync(payload, rootState) {
            await new Promise(resolve => setTimeout(resolve, 1000))
            this.increment(payload)
        }
    }
}

const store = init({
    models: {
        count,
    },
})

export default store

可以看到,在 Redux 中需要创建 action.jsstate.jsreducer.js,但是 Rematch 允许你把这三者结合到一起,这组单元被称为 model

值得一提的是,Rematch 也需要使用 Redux,也可以理解为 Rematch 是在 Redux 上做的扩展。

另外,由于 Rematch 是用 Typescript 开发的,为了方便解释,下面把 Typescript 类型全部去掉了再说明。

下面来看看 Rematch 都做了什么工作。

init

上面的例子中只使用了 init 这个 API。

export const init = (initConfig) => {
    const config = createConfig(initConfig || {})
    return createRematchStore(config)
}

传入 init 的配置,会首先传入 createConfig,然后会调用 createRematchStore。

function createConfig(initConfig) {
    const storeName = initConfig.name ?? `Rematch Store ${count}`
    count += 1
    const config = {
        name: storeName,
        models: initConfig.models || {},
        plugins: initConfig.plugins || [],
        redux: {
            reducers: {},
            rootReducers: {},
            enhancers: [],
            middlewares: [],
            ...initConfig.redux,
            devtoolOptions: {
                name: storeName,
                ...(initConfig.redux?.devtoolOptions ?? {}),
            },
        },
    }
    validateConfig(config)
    
    config.plugins.forEach((plugin) => {
        if (plugin.config) {
            config.models = merge(config.models, plugin.config.models)
            if (plugin.config.redux) {
                config.redux.initialState = merge(
                    config.redux.initialState,
                    plugin.config.redux.initialState
                )
                // ...
            }
            validatePlugin(plugin)
        }
    })
    
    return config
}

这里首先创建了一个自增的 storeName,然后将传入的 initConfig 对象全部拆分到 config 中,后续所有的操作都会在这个对象上处理。

config.redux 后续会传入给 Redux 作为 Redux 的初始化参数,毕竟 Rematch 是基于 Redux 实现的。

然后分别执行 validateConfigvalidatePlugin。 这两个 validate 函数都是相同的套路。

function validatePlugin() {
    validate(() => [
        [
            !ifDefinedIsFunction(plugin.onStoreCreated),
            'Plugin onStoreCreated must be a function',
        ],
        [
            !ifDefinedIsFunction(plugin.onModel), 
            'Plugin onModel must be a function',
        ],
        // ...

    ])
}

function validate(runValidations) {
    if (process.env.NODE_ENV !== 'production') {
        const validations = runValidations()
        const errors: string[] = []

        validations.forEach((validation) => {
            const isInvalid = validation[0]
            const errorMessage = validation[1]
            if (isInvalid) {
                errors.push(errorMessage)
            }
        })

        if (errors.length > 0) {
            throw new Error(errors.join(', '))
        }
    }
}

可以看到,这里提取出 validate 函数,可以对传入的需验证数组,依次遍历并检查数组的第一项是否为 true,如果不为 true 则抛出数据第二项的错误提示。

然后 validateConfigvalidatePlugin 都可以使用这个 validate 方法来实现:

  • 对多种异常参数情况进行校验
  • 没有冗余的判断,更简洁、优雅

createRematchStore

通过上文对 initConfig 的格式化以及异常传入格式校验,下面进入正文。

function createRematchStore(config) {
    const bag = createRematchBag(config)
    bag.reduxConfig.middlewares.push(createEffectsMiddleware(bag))

    bag.forEachPlugin('createMiddleware', (createMiddleware) => {
        bag.reduxConfig.middlewares.push(createMiddleware(bag))
    })
    const reduxStore = createReduxStore(bag)
    // ...
}

首先将格式化后的 config 传入 createRematchBag

function createRematchBag(config) {
    return {
        models: createNamesModels(config.models),
        reduxConfig: config.redux,
        forEachPlugin(method, fn) {
            config.plugins.forEach(plugin => {
                if (plugin[method]) {
                    fn(plugin[method])
                }
            })
        }
    }
}

createRematchBag 返回了一个包含 modelsreduxConfigforEachPlugin 的对象。

  • models 对传入的 models 进行校验
  • reduxConfig 后续传入 redux 的配置。这里只改了个命名
  • forEachPlugin 相当于 Array.prototype.find,可以检索出 method plugin 并且传入第二个参数表示的回调函数。

然后执行了 createEffectsMiddleware。在 Rematcheffects 类似使用了 Redux + redux-thunk 中的 会返回函数的 action

function createEffectsMiddleware(bag) {
    return store => next => action => {
        if (action.type in bag.effects) {
            next(action)
            
            return bag.effects[action.type](
                action.payload,
                store.getState(),
                action.meta,
            )
        }
        
        return next(action)
    }
}

可以看到,这里的 createEffectsMiddleware 其实就是一个 effects 的 Redux 中间件。 开始的三个函数参数,store、next、action 分别代表了:

  • Redux 的 store
  • Redux 的 dispatch
  • Redux 的 action

先判断 action.type 是否命中 Rematch 中的任意一个 effects。

如果命中,首先先执行 next(action),即使命中了 Rematch 的 effects ,也会继续执行后续可能存在的 reducer。

然后会执行对应的 Rematch effect,并传入三个参数,分别是 action.payloadstore state、以及 action.meta

关于action.meta

action.meta 是为了扩展 action 而添加的参数。

详见:redux-toolkit.js.org/api/createA…

小结

本篇介绍了 Rematch 对传入 initConfig 进行格式化并通过 createEffectsMiddleware 实现了一个 effect 中间件。