【Rematch 源码系列】三、Plugin factory 和 core plugins

·  阅读 255
【Rematch 源码系列】三、Plugin factory 和 core plugins

如无特殊说明,本专栏文章的代码版本均为 @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
复制代码

rematch 组成部分

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 钩子,还需要依次执行 onModelonStoreCreated 钩子:

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

前面提到,onModelonStoreCreated 之前执行,因此先看看 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 创建完成以前,是不可以访问 storeDispatchstoreGetStatedispatch 的。而创建完以后,首先是需要覆盖掉 storeDispatchstoreGetState,然后返回一个增强的 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 的处理差不多,但也有几点不同:

  1. model 配置里的 effect 参数支持函数形式,调用时,参数传入的是增强型的 dispatch 函数对象,返回值则是真正的 effects 对象。这样一来在 effect 中可以通过 dispatch 调用所有 model 的 effect 和 reducer。
  2. model 中单个 effect 函数上下文的 this 绑定到了 this.dispatch[model.name],也就是当前 model 的 dispatch。因此在内部可以使用 this 来调用当前 model 下的 reducer 和 effect。
  3. 对于 effect 增加了一个标识 isEffecttrue,用于和常规的 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 巧妙的设计拍手称快了吧。在接下来的一篇文章中,我会继续介绍几个第三方的插件,我们可以选择使用,为开发提效。敬请期待!

分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改