这是我参与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)
最后执行 Redux 的 createStore 并将上文处理后的参数依次传入。
讲完 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 的基础上扩展了两个字段,分别是 name、addModel。
- 关于 name 这个 name 就是钱文忠的自增 store count。
const storeName = initConfig.name ?? `Rematch Store ${count}`
- 关于 addModel 在 addModel 内部:
首先验证 model 是否存在 name、baseReducer 字段。
然后执行 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,并通过 createDispatcher 将 actionDispatcher 作为 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。并添加了 name、addModel 两个属性。
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 的实现部分已经全部结束。