如无特殊说明,本专栏文章的代码版本均为 @rematch/core: 1.4.0
上篇介绍了 rematch core 的相关代码,且其中忽视了 plugin 这一部分。这部分也是 rematch 的亮点所在,这一篇文章中我将详细介绍。
除此之外,我还会介绍 rematch 的两个 core plugins,为什么叫 core plugins,因为它们必须使用,才能让 rematch 发挥完整的功能。在下一篇文章中,我还会介绍几个第三方的 plugins,由开发者自由选择用还是不用。
在讲解之前,还是先回顾一下 rematch 的代码结构和组成部分:
...
plugins
|—— ...
|—— loading
|—— immer
|—— select
src
|—— plugins
| |—— dispatch.ts
| |—— effects.ts
|—— typings
| |—— index.ts
|—— utils
| |—— deprecate.ts
| |—— isListener.ts
| |—— mergeConfig.ts
| |—— validate.ts
|—— index.ts
|—— pluginFactory.ts
|—— redux.ts
|—— rematch.ts
Plugin Factory
首先是 Plugin 工厂函数,参数为 Rematch Store 初始化的参数,返回值为工厂对象,其中的关键方法属性为create
:
export default (config: R.Config) => ({
// ...
create(plugin: R.Plugin): R.Plugin {
// ... do some validations
if (plugin.onInit) {
plugin.onInit.call(this);
}
const result: R.Plugin | any = {};
if (plugin.exposed) {
for (const key of Object.keys(plugin.exposed)) {
this[key] =
typeof plugin.exposed[key] === "function"
? plugin.exposed[key].bind(this) // bind functions to plugin class
: Object.create(plugin.exposed[key]); // add exposed to plugin class
}
}
for (const method of ["onModel", "middleware", "onStoreCreated"]) {
if (plugin[method]) {
result[method] = plugin[method].bind(this);
}
}
return result;
},
});
主要是将 plugin 一些函数属性执行上下文的 this
绑定到 pluginFactory 上。如果 plugin 包含 exposed
属性,则将它们添加到 pluginFactory 上,用于 plugins 的共享。(在 Rematch v2 源码的升级改造中,误解了 exposed
属性的作用,造成了一些模棱两可的行为,后面的文章中我会提到)。最后返回一个包含了 plugin 钩子的对象。
pluginFactory 会在前面文章提到的 Rematch 类中被调用:
export default class Rematch {
protected config: R.Config;
protected models: R.Model[];
private plugins: R.Plugin[] = [];
private pluginFactory: R.PluginFactory;
constructor(config: R.Config) {
this.config = config;
this.pluginFactory = pluginFactory(config);
for (const plugin of corePlugins.concat(this.config.plugins)) {
this.plugins.push(this.pluginFactory.create(plugin));
}
// preStore: middleware, model hooks
this.forEachPlugin("middleware", (middleware) => {
this.config.redux.middlewares.push(middleware);
});
}
public forEachPlugin(method: string, fn: (content: any) => void) {
for (const plugin of this.plugins) {
if (plugin[method]) {
fn(plugin[method]);
}
}
}
// ...
}
在构造函数中,创建 pluginFactory 作为 Rematch 类的私有属性,然后依次将创建后的 plugin 放入一个数组。最后将 plugin 的 middleware 钩子配置到 redux 的 middleware 中。
除了 middleware 钩子,还需要依次执行 onModel
和 onStoreCreated
钩子:
export default class Rematch {
// ...
public addModel(model: R.Model) {
// ...
// run plugin model subscriptions
this.forEachPlugin("onModel", (onModel) => onModel(model));
}
public init() {
// collect all models
this.models = this.getModels(this.config.models);
for (const model of this.models) {
this.addModel(model);
}
// ...
this.forEachPlugin("onStoreCreated", (onStoreCreated) => {
const returned = onStoreCreated(rematchStore);
// if onStoreCreated returns an object value
// merge its returned value onto the store
if (returned) {
Object.keys(returned || {}).forEach((key) => {
rematchStore[key] = returned[key];
});
}
});
return rematchStore;
}
// ...
}
onModel
执行于遍历并添加 model 时,onStoreCreated
则当 store 创建完成并返回之前执行。前者通常用于读取、添加或修改 model 的配置,而后者则用于在 store 上添加新属性,如果有返回值且返回值是对象,则会将其上的属性都添加到 store 中。下面我们来看看两个具体的 plugin 以及这些钩子的应用。
Core Plugins
Rematch v1 的设计中,有两个核心 plugin,它们在 Rematch 类的构造函数中必被引用,可以看到下面的代码片段:
export default class Rematch {
// ...
constructor(config: R.Config) {
// ...
for (const plugin of corePlugins.concat(this.config.plugins)) {
this.plugins.push(this.pluginFactory.create(plugin));
}
// ...
}
// ...
}
这两个核心 plugin 分别是 dispatch 和 effects。dispatch plugin 用于增强 redux store 的 dispatch,使其支持链式调用,例如 dispatch.modelName.reducerName
,这是 rematch 的特色之一。而 effects plugin 则用于支持异步操作等副作用,并实现通过 dispatch.modelName.effectName
调用。
Dispatch plugin
先来看看 dispatch 的全部代码,然后我再将其拆解为两部分讲解:
const dispatchPlugin: R.Plugin = {
exposed: {
// required as a placeholder for store.dispatch
storeDispatch(action: R.Action, state: any) {
console.warn("Warning: store not yet loaded");
},
storeGetState() {
console.warn("Warning: store not yet loaded");
},
/**
* dispatch
*
* both a function (dispatch) and an object (dispatch[modelName][actionName])
* @param action R.Action
*/
dispatch(action: R.Action) {
return this.storeDispatch(action);
},
/**
* createDispatcher
*
* genereates an action creator for a given model & reducer
* @param modelName string
* @param reducerName string
*/
createDispatcher(modelName: string, reducerName: string) {
return async (payload?: any, meta?: any): Promise<any> => {
const action: R.Action = { type: `${modelName}/${reducerName}` };
if (typeof payload !== "undefined") {
action.payload = payload;
}
if (typeof meta !== "undefined") {
action.meta = meta;
}
return this.dispatch(action);
};
},
},
// access store.dispatch after store is created
onStoreCreated(store: any) {
this.storeDispatch = store.dispatch;
this.storeGetState = store.getState;
return { dispatch: this.dispatch };
},
// generate action creators for all model.reducers
onModel(model: R.Model) {
this.dispatch[model.name] = {};
if (!model.reducers) {
return;
}
for (const reducerName of Object.keys(model.reducers)) {
this.validate([
[
!!reducerName.match(/\/.+\//),
`Invalid reducer name (${model.name}/${reducerName})`,
],
[
typeof model.reducers[reducerName] !== "function",
`Invalid reducer (${model.name}/${reducerName}). Must be a function`,
],
]);
this.dispatch[model.name][reducerName] = this.createDispatcher.apply(
this,
[model.name, reducerName]
);
}
},
};
onModel hook
前面提到,onModel
在 onStoreCreated
之前执行,因此先看看 onModel
:
const dispatchPlugin: R.Plugin = {
exposed: {
// ...
storeDispatch(action: R.Action, state: any) {
console.warn("Warning: store not yet loaded");
},
dispatch(action: R.Action) {
return this.storeDispatch(action);
},
createDispatcher(modelName: string, reducerName: string) {
return async (payload?: any, meta?: any): Promise<any> => {
const action: R.Action = { type: `${modelName}/${reducerName}` };
if (typeof payload !== "undefined") {
action.payload = payload;
}
if (typeof meta !== "undefined") {
action.meta = meta;
}
return this.dispatch(action);
};
},
},
// ...
// generate action creators for all model.reducers
onModel(model: R.Model) {
this.dispatch[model.name] = {};
if (!model.reducers) {
return;
}
for (const reducerName of Object.keys(model.reducers)) {
// ... some validations
this.dispatch[model.name][reducerName] = this.createDispatcher.apply(
this,
[model.name, reducerName]
);
}
},
};
在 onModel
钩子中,会为每个 model 在 this.dispatch
(上面提到,exposed
里的属性都会被添加到 pluginFacotry,且钩子函数上下文中的 this
会被绑定到这个 pluginFactory,因此 this.dispatch
即为上面 exposed
中的 dispatch
函数)上创建一个空对象,属性名为 model 名,然后遍历 reducer,对于每一个 reducer,以 reducer 名字作为属性名,往先前的空对象上添加一个 actionCreator。这样一来,便支持了 reducer 的链式调用。
createDispatcher
函数用于生成 actionCreator,里面会使用 ${model 名}/${reducer 名}
作为 action type,最后拼装调用时传入的 payload 和 meta,使用 this.dispatch
调用(this.dispatch
同时也是一个函数)。
action 派发以后,会进入到正确的 reducer 执行,关于 reducer 构造的代码在前面的创建 Model reducers已经讲过,这里再回顾下相关代码:
this.createModelReducer = (model: R.Model) => {
const modelBaseReducer = model.baseReducer;
const modelReducers = {};
for (const modelReducer of Object.keys(model.reducers || {})) {
const action = isListener(modelReducer)
? modelReducer
: `${model.name}/${modelReducer}`;
modelReducers[action] = model.reducers[modelReducer];
}
const combinedReducer = (state: any = model.state, action: R.Action) => {
// handle effects
if (typeof modelReducers[action.type] === "function") {
return modelReducers[action.type](state, action.payload, action.meta);
}
return state;
};
this.reducers[model.name] = !modelBaseReducer
? combinedReducer
: (state: any, action: R.Action) =>
combinedReducer(modelBaseReducer(state, action), action);
};
可以看到,在 combinedReducer
中,会通过 action.type
为 action 分配正确的 reducer,而这里的 action.type
也由 ${model 名}/${reducer 名}
组合而成,从而完成匹配。
onStoreCreated hook
再来看看最后的 onStoreCreated
钩子:
const dispatchPlugin: R.Plugin = {
exposed: {
// required as a placeholder for store.dispatch
storeDispatch(action: R.Action, state: any) {
console.warn("Warning: store not yet loaded");
},
storeGetState() {
console.warn("Warning: store not yet loaded");
},
/**
* dispatch
*
* both a function (dispatch) and an object (dispatch[modelName][actionName])
* @param action R.Action
*/
dispatch(action: R.Action) {
return this.storeDispatch(action);
},
},
// access store.dispatch after store is created
onStoreCreated(store: any) {
this.storeDispatch = store.dispatch;
this.storeGetState = store.getState;
return { dispatch: this.dispatch };
},
};
由于 rematch store 是对 redux store 的增强,依赖于 redux store。因此,在 redux store 创建完成以前,是不可以访问 storeDispatch
,storeGetState
和 dispatch
的。而创建完以后,首先是需要覆盖掉 storeDispatch
和 storeGetState
,然后返回一个增强的 dispatch
,覆盖掉 redux store 原先的 dispatch
。
Effect plugin
effect plugin 用于支持副作用:
const effectsPlugin: R.Plugin = {
exposed: {
// expose effects for access from dispatch plugin
effects: {},
},
// add effects to dispatch so that dispatch[modelName][effectName] calls an effect
onModel(model: R.Model): void {
if (!model.effects) {
return;
}
const effects =
typeof model.effects === "function"
? model.effects(this.dispatch)
: model.effects;
for (const effectName of Object.keys(effects)) {
// ... some validations
this.effects[`${model.name}/${effectName}`] = effects[effectName].bind(
this.dispatch[model.name]
);
// add effect to dispatch
// is assuming dispatch is available already... that the dispatch plugin is in there
this.dispatch[model.name][effectName] = this.createDispatcher.apply(
this,
[model.name, effectName]
);
// tag effects so they can be differentiated from normal actions
this.dispatch[model.name][effectName].isEffect = true;
}
},
// process async/await actions
middleware(store) {
return (next) => async (action: R.Action) => {
// async/await acts as promise middleware
if (action.type in this.effects) {
await next(action);
return this.effects[action.type](
action.payload,
store.getState(),
action.meta
);
}
return next(action);
};
},
};
onModel
钩子中做的事和 dispatch plugin 中对 reducer 的处理差不多,但也有几点不同:
- model 配置里的 effect 参数支持函数形式,调用时,参数传入的是增强型的
dispatch
函数对象,返回值则是真正的 effects 对象。这样一来在 effect 中可以通过dispatch
调用所有 model 的 effect 和 reducer。 - model 中单个 effect 函数上下文的
this
绑定到了this.dispatch[model.name]
,也就是当前 model 的dispatch
。因此在内部可以使用this
来调用当前 model 下的 reducer 和 effect。 - 对于 effect 增加了一个标识
isEffect
为true
,用于和常规的 reducer 区分开来。
最后是 effect plugin 的核心部分,其相当于是自己实现了一个 redux 异步中间件。前面提到,effect action 也可以通过 rematchStore.dispatch
派发,当经过该异步中间件后,首先会判断其是否在 this.effects
中,如果是,首先将这个 action 往下处理,接着执行对应的 effect,传入的三个参数分别是 payload,global state 以及 meta(由于 meta 几乎很少用,而最常见的是 payload,因此采用了这样的顺序);如果不是,则直接往下处理。
注意:往下处理代表着被后面的 middleware 处理(如果有),最后会来到 reducer。这里有一个容易造成困惑的地方,因为按这样的设计,一旦 model 和 effect 同名,会导致 reducer 的执行顺序在 effect 之前。两者都会执行,这也引发了 Rematch v2 中 TS 设计部分的难处(后面文章中我会详细说明)。
总结
详细说了 Rematch 的插件机制,以及其核心的两个插件后,大家应该对 Rematch 巧妙的设计拍手称快了吧。在接下来的一篇文章中,我会继续介绍几个第三方的插件,我们可以选择使用,为开发提效。敬请期待!