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

600 阅读4分钟

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

上篇说到了 createEffectsMiddleware 其实就是实现了一个支持 effects 的 Redux 中间件,这篇我们继续往下说。

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

通过 forEachPlugin 找到 createMiddleware 插件并传入后续给 redux 使用的中间件。

这里展开说明一下 createMiddleware

createMiddleware 来自 Rematch 提供的插件 plugins/typed-state,如没有配置则 forEachPlugin 不会执行第二个参数回调。

如果传入的话执行下面的代码。

function createMiddleware: () => (store) => (next) => (action) => {
    const called = next(action)
    const [modelName] = action.type.split('/')
    const typings = cachedTypings[modelName]
    if (typings) {
        validate(typings, store.getState()[modelName], modelName, config)
    } else if (config.strict && config.logSeverity) {
        logger(
            config.logSeverity,
            `[rematch]: Missing typings definitions for \`${modelName}\` model`
        )
    }
    return called
}

可以看到 createMiddleware 函数的返回值又是一个 Redux 的中间件。

它的作用是对 cachedTypings 中定义的 model 分别进行校验,具体代码如下。

function validate(typeSpecs, values, modelName, config) {
    if (process.env.NODE_ENV !== 'production') {
        const keys = Object.keys(typeSpecs)
        keys.forEach((typeSpecName) => {
        if (typeSpecs[typeSpecName]) {
            const error = typeSpecs[typeSpecName](
                values,
                typeSpecName,
                modelName,
                'property',
                null,
                PROP_TYPES_SECRET
            )
            if (error instanceof Error && config.logSeverity) {
                logger(config.logSeverity, `[rematch] ${error.message}`)
            }
        }
    }
}

然后执行 createReduxStore

function createReduxStore(bag) {
    bag.models.forEach((model) => createModelReducer(bag, model))

    const rootReducer = createRootReducer(bag)

    const middlewares = Redux.applyMiddleware(...bag.reduxConfig.middlewares)
    const enhancers = composeEnhancersWithDevtools(
        bag.reduxConfig.devtoolOptions
    )(...bag.reduxConfig.enhancers, middlewares)

    const createStore = bag.reduxConfig.createStore || Redux.createStore
    const bagInitialState = bag.reduxConfig.initialState
    const initialState = bagInitialState === undefined ? {} : bagInitialState

    return createStore(
        rootReducer,
        initialState,
        enhancers
    )
}

createReduxStore 就是 Rematch 最核心的部分了,经过前面对传入 initConfig 的各种格式化及处理之后,现在要创建真正的 Redux Store了。 首先看到遍历了 models 并执行了 createModelReducer

function createModelReducer(bag, model) {
    const modelReducerKeys = Object.keys(model.reducers)
    modelReducerKeys.forEach((reducerKey) => {
        const actionName = isAlreadyActionName(reducerKey)
            ? reducerKey
            : `${model.name}/${reducerKey}`

        modelReducers[actionName] = model.reducers[reducerKey]
    })

    const combinedReducer = (state, action) => {
        if (action.type in modelReducers) {
            return modelReducers[action.type](state, action.payload, action.meta)
        }
        return state
    }

    const modelBaseReducer = model.baseReducer

    let reducer = !modelBaseReducer
        ? combinedReducer
        : (state = model.state, action) =>
            combinedReducer(modelBaseReducer(state, action), action)

    bag.forEachPlugin('onReducer', (onReducer) => {
        reducer = onReducer(reducer, model.name, bag) || reducer
    })

    bag.reduxConfig.reducers[model.name] = reducer
}

由于已经在 model 上绑定了 reducers,createModelReducer 首先会遍历 reducer 并检查是否已经是 Rematch 中的 action 了。即通过是否存在 '/' 来判断。

function isAlreadyActionName(reducerKey) {
    return reducerKey.indexOf('/') > -1
}

然后如果不是 Rematch 中的 action,则添加上 model.name/ 作为前缀,拼成一个 Rematch 的 action。

然后新创建的 reducer 变量其实就是传入两个参数,分别是 state、action,并依次传入链式的 reducer 去处理。

处理的先后顺序是:开发者在 model 中定义的 baseReducer > combineReducer

这里的 combineReducer 做的处理是:如果当前的 action 存在于 model 的reducer 中,则直接执行这个 reducer,否则则返回 state。

可以看到,为什么在 model 中配置的 reducer 和 Redux 中的 reducer 效果是一样的,是因为最终要传入 redux 中的 reducer 已经在执行前判断了是否命中 model 中的 reducer。从这里可以得到的结论是,Rematch 可以和原有的 redux 的reducer 同时存在,且 Rematch.reducer > Redux.reducer

然后如果插件中配置了 onReducer 事件,则依次执行。

最后将 model 下的 reducer 绑定在 bag.reduxConfig.reducers 上。

接着往下看,

const rootReducer = createRootReducer(bag)
const middlewares = Redux.applyMiddleware(...bag.reduxConfig.middlewares)

这里的 rootReducer 其实就是经过 Redux combineReducer 的返回结果,并且将前文中处理后的中间件传入 Redux。

然后看到 composeEnhancersWithDevtools 的部分。

const enhancers = composeEnhancersWithDevtools(
    bag.reduxConfig.devtoolOptions
)(...bag.reduxConfig.enhancers, middlewares)
function composeEnhancersWithDevtools(
    devtoolOptions = {}
) {
    return !devtoolOptions.disabled &&
        typeof window === 'object' &&
        window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
        ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__(devtoolOptions)
        : Redux.compose
}

composeEnhancersWithDevtools 的处理就是如果:

  • 处于浏览器环境
  • 安装了 Redux Devtools
  • 未禁用 Redux Devtools

则初始时传入 Rematch init 的 devtoolOptions 传入并开启 Redux Devtools

然后将其他中间件依次传入 compose

最后一段:

const createStore = bag.reduxConfig.createStore || Redux.createStore
const bagInitialState = bag.reduxConfig.initialState
const initialState = bagInitialState === undefined 
    ? {} 
    : bagInitialState 
    
return createStore(rootReducer, initialState, enhancers)

最后执行 ReduxcreateStore 并将上文处理后的参数依次传入。

讲完 createReduxStore,接着往下看:

// ...
const reduxStore = createReduxStore(bag)
let rematchStore = {
    ...reduxStore,
    name: config.name,
    addModel(model) {
        validateModel(model)
        createModelReducer(bag, model)
        prepareModel(rematchStore, bag, model)
        reduxStore.replaceReducer(createRootReducer(bag))
        reduxStore.dispatch({ type: '@@redux/REPLACE' })
    },
}

现在已经知道 reduxStore 就是 redux.createStore 所返回的 store 了。

然后创建了一个变量 rematchStore,它在 store 的基础上扩展了两个字段,分别是 nameaddModel

  • 关于 name 这个 name 就是钱文忠的自增 store count。
const storeName = initConfig.name ?? `Rematch Store ${count}`
  • 关于 addModel 在 addModel 内部:

首先验证 model 是否存在 namebaseReducer 字段。

然后执行 createModelReducer,将 reducer 添加 model.name 并执行插件的 onReducer 钩子。

之后执行 prepareModel

function prepareModel(rematchStore, bag, model) {
    const modelDispatcher = {}
    rematchStore.dispatch[`${model.name}`] = modelDispatcher

    createDispatcher(rematchStore, bag, model)

    bag.forEachPlugin('onModel', (onModel) => {
        onModel(model, rematchStore)
    })
}

Rematch 中可以有两种方式调用 model 中的 reducer、effect。

dispatch.dialog.show()
dispatch({ type: 'dialog/show' })

prepareModel 的就是这个 feature 的具体实现。

首先根据 model.name 作为 key,在 dispatch 上绑定一个空对象。 然后遍历 model 上的所有 reducer,并通过 createDispatcheractionDispatcher 作为 value 绑定上去。

这里的 actionDispatcher 其实就是一个新的对象,包含:

  • action.payload
  • action.meta
  • isEffect (是否是 effect 或 reducer)
function createDispatcher(rematchStore, bag, model) {
    const modelDispatcher = rematch.dispatch[model.name]

    const modelReducersKeys = Object.keys(model.reducers)
    modelReducersKeys.forEach((reducerName) => {
        validateModelReducer(model.name, model.reducers, reducerName)

        modelDispatcher[reducerName] = createActionDispatcher(
            rematch,
            model.name,
            reducerName,
            false
        )
    })
    
    if (model.effects) {
        effects =
            typeof model.effects === 'function'
                ? (model.effects as ModelEffectsCreator<TModels>)(rematch.dispatch)
                : model.effects
    }

    const effectKeys = Object.keys(effects)
    effectKeys.forEach((effectName) => {
        validateModelEffect(model.name, effects, effectName)

        bag.effects[`${model.name}/${effectName}`] = effects[effectName].bind(
            modelDispatcher
        )

        modelDispatcher[effectName] = createActionDispatcher(
            rematch,
            model.name,
            effectName,
            true
        )
    })
}

effects 的处理和 reducers 的几乎一样,都是以 model.name 作为前缀

接着看 `addModel`` 的后续部分。

addModel(model) { 
    // ...
    reduxStore.replaceReducer(createRootReducer(bag)) 
    reduxStore.dispatch({ type: '@@redux/REPLACE' }) 
},

如果执行了 addModel,经过前面的处理后,这里执行 redux.store 的 replaceReducer 完成 reducer 的替换。 最后执行 @@redux/REPLACE 这个 action。

到此 addModel 的部分已经结束。

上文已经讲到了基于 redux.store 创建了 rematchStore。并添加了 nameaddModel 两个属性。

addExposed(rematchStore, config.plugins)

addExposed 的作用是先校验插件的状态(exposed),然后绑定到 rematchStore 上。

function createRematchStore(config) {
    // ...
    bag.models.forEach((model) => prepareModel(rematchStore, bag, model))
    bag.forEachPlugin('onStoreCreated', (onStoreCreated) => {
        rematchStore = onStoreCreated(rematchStore, bag) || rematchStore
    })

    return rematchStore
}

作为 createRematchStore 的最后一段,和 addModel 一样,将 model 中的 reducer、effects 都绑定到 dispatch 上。

然后调用插件的 onStoreCreated 钩子。

最后返回这个 rematchStore

由于在 init 函数中,最终返回的是执行 createRematchStore 函数的返回值,所以最终 init 返回的值也是这个 rematchStore

这里的 rematchStore 既包含了 redux.store 原本的方法,Rematch 还在它的基础上做了扩展:

  • model 中的 reducer、effects 绑定到 dispatch
  • 调用 dispatch 时会自动拼装 action.name
  • 对于传入 Rematch 的插件,会在 Rematch store 的初始化的各个阶段去调用生命周期钩子

到此,关于 Rematch 的实现部分已经全部结束。