# 【Rematch 源码系列】五、Rematch v2, a big step


Rematch 源码解读系列的第 5️⃣ 篇，关于 Rematch V2。

<!--more-->

从这篇文章开始，我们来了解一下 Rematch v2。

## 我加入 Rematch 的原因

在说 v2 之前，先给大家说说我加入 Rematch 的经历。

现在回想起来，也没想到自己会成为 Rematch 的贡献者，这也是我第一个深度参与的知名开源项目。最开始是因为公司新项目使用了 Rematch，在使用的过程中我就发现它的 TS 兼容很差，特别是 `dispatch`，TS 类型根本没兼容，我为此提过 [issue](https://github.com/rematch/rematch/issues/787) 和对应的 PR，但是当时 Rematch 没有及时处理。我便只在内部脚手架中提了一个 PR，创建了一个文件 `rematch.d.ts` 用于覆盖 rematch 自身的类型定义，对大部分类型进行了修正。

大概过了一周，Rematch 的维护者邮件联系到我，想让我帮忙处理 Rematch 的类型问题，我很爽快地答应了，由此便参与到 v2 的迭代中。

Rematch v1 的最后一个版本，也是这个专栏前面文章的示例代码版本 v1.4.0 发布于 2020-02-23。在这之后，Rematch 团队便开始了 v2 的迭代（此时 Rematch 的创始人已不再接手 Rematch），并于 [2020-07-30](https://github.com/rematch/rematch/releases?after=%40rematch%2Fimmer%402.0.0-next.1) 发布了第一个预发布版本 @rematch/core@2.0.0-next.1。而我是在 2020-08-17 开始参与 Rematch（感慨一下，这篇博客写于 2021-08-17，一晃一年就过去了），并推动了 @rematch/core@2.0.0-next.2 的预发布版本在 2020-08-19 进行发布。随后经历了 10 个预发布版本，我们终于在 2021-01-31 发布了正式的 v2，历时近一年。

以上便是 Rematch v2 历史的简要回顾和我加入 Rematch 的原因。在这篇文章中，我会主要介绍 Rematch v2 升级带来的一些变化。由于我参与的大部分是 TS 的类型兼容，因此这些变化，一部分是我自己有涉及的，其他部分则是我询问了团队的一些成员，了解到了他们的一些想法。

## 目录结构的变化

Rematch v2 对代码和目录结构做了很大改动，并使用 [lerna](https://github.com/lerna/lerna) 进行项目管理（由于 rematch 含有一些 plugins，因此天生适合 lerna 这种 monorepo 包管理方案）。让我们先看看目录结构的变化：

### Rematch v1 的目录结构

```
...
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 v2 的目录结构

```
...
blog
docs
website
packages
|—— core
|  |—— test
|  |—— src
|  |  |—— bag.ts
|  |  |—— config.ts
|  |  |—— dispatcher.ts
|  |  |—— reduxStore.ts
|  |  |—— rematchStore.ts
|  |  |—— types.ts
|  |  |—— validate.ts
|  |  |—— index.ts
|  |—— node_modules
|  |—— ...
|—— immer
|  |—— test
|  |—— src
|  |  |—— index.ts
|  |—— node_modules
|  |—— ...
|—— loading
|—— select
|—— ...
```

可以看到，之前的 plugins 都作为 package 进行管理，而 rematch 核心代码也作为了一个 package 叫做 core，两个核心 plugins 和 pluginFactory 被删除，相关代码被整合到 core 中，且新增 `bag.ts` 和 `dispatcher.ts`。后面我会每个单独讲讲。

## 去除 Rematch 类，改为函数形式

在我看来，Rematch v2 中最大的变化便是去除代码中的”类“，都改为了函数。

先看看 v1 中的 rematch class（这里我会省略大部分代码，如果想学习全部代码，可以查看专栏前面的相关文章）：

```ts
/**
 * Rematch class
 *
 * an instance of Rematch generated by "init"
 */
export default class Rematch {
  protected config: R.Config;
  // ... other private configs

  constructor(config: R.Config) {}

  public init() {
    // ...

    const rematchStore = {
      name: this.config.name,
      ...redux.store,
      // ...
    };

    return rematchStore;
  }
}
```

调用时：

```ts
export const init = (initConfig: R.InitConfig = {}): R.RematchStore => {
  const name = initConfig.name || count.toString();
  count += 1;
  const config: R.Config = mergeConfig({ ...initConfig, name });
  return new Rematch(config).init();
};
```

看看代码的调用方式，其实这里使用”类“并没有凸显出哪些优势。我们再来看看 v2：

```ts
export default function createRematchStore<
  TModels extends Models<TModels>,
  TExtraModels extends Models<TModels>
>(config: Config<TModels, TExtraModels>): RematchStore<TModels, TExtraModels> {
  // ...

  const reduxStore = createReduxStore(bag);

  let rematchStore = {
    ...reduxStore,
    name: config.name,
    // ...
  } as RematchStore<TModels, TExtraModels>;

  return rematchStore;
}
```

v2 中定义了一个函数，并取了一个描述性的函数名 `createRematchStore`，使用时也更简单：

```ts
/**
 * Prepares a complete configuration and creates a Rematch store.
 */
export const init = <
  TModels extends Models<TModels>,
  TExtraModels extends Models<TModels> = Record<string, never>
>(
  initConfig?: InitConfig<TModels, TExtraModels>
): RematchStore<TModels, TExtraModels> => {
  const config = createConfig(initConfig || {});
  return createRematchStore(config);
};
```

关于这样的改动，现 Rematch 维护者 [semoal](https://github.com/semoal) 的观点和我一致，他认为 JavaScript 更适合函数式的编程，他也更喜欢这种方式。其实我之前有写过一篇文章：[JavaScript 的面向对象，你真的懂了吗？是否一定需要使用”类“来编程？](https://juejin.cn/post/6844904137688121352)，在这篇文章中，我也分析过使用纯对象和”类“进行编程的差异，很多时候，我们其实无需使用”类“，而且 JavaScript 中，类的本质还是函数，还是对象。而 JavaScript 中类的继承，本质还是原型链的链式关联，感兴趣的同学可以看看。

## 删除核心 plugins 以及 pluginFactory

专栏[前面的文章](https://juejin.cn/post/6992935103286640671)提到过 Rematch 的插件系统，其中有 pluginFactory 以及 dispatch 和 effects 两个核心插件。然而这些代码在 v2 中都被移除了，相关逻辑被整合到了 core 中。

> 强烈建议读完[前面的专栏文章](https://juejin.cn/post/6992935103286640671)以后再来阅读这个部分。

### pluginFactory

[pluginFactory](https://juejin.cn/post/6992935103286640671#heading-0)的作用我认为有两点：

1. 支持 `exposed`。插件可以使用 `exposed` 属性，暴露的方法或对象会被绑定到 pluguinFactory 上

2. 将所有 hook 上下文的 `this` 也绑定到 pluginFactory 上

rematch v1 中的两个核心 plugin，dispatch 和 effects 分别暴露了 `dispatch` 和 `effects` 两个对象以供其他插件在 hooks 函数中进行访问。但是在 v2 中，一切都变成了函数以简化流程，`effects` 放到了下面将提到的 rematch bag 配置中，而 `dispatch` 则是直接操作 `redux.dispatch`。

#### v2 中对 exposed 的误用

其实说实话，关于 pluginFactory 这部分的优化，我觉得早期团队做得也不是特别好。例如之前所有 hooks 中的 `this` 绑定到 pluginFactory，是可以便捷访问 `dispatch` 和 `effects` 的，但是在 v2 中，`dispatch` 直接等同于 `redux.dispatch`，需要通过 `rematchStore.dispatch` 访问，而 `effects` 则放入了 rematch bag 中。调用不同 hooks 的时候，传参也不一致，但是我猜测可能其他插件也没有在各自的 hooks 中高频访问 `dispatch` 和 `effects`，所以也没人关注或提出相关缺陷。

除了上面的，还有一个很大的问题就是 v2 对 `exposed` 参数的误用。在 v1 中，各插件是可以通过使用 `exposed` 参数，来暴露一些方法或属性的，比如 `dispatch` 和 `effects`，但是这个暴露范围仅限插件之间（因为是挂在 pluginFactory 对象上），而 v2 则专门创建了一个 `addExposed` 函数用于将插件的这些方法或属性直接暴露给 `rematchStore`，个人认为，这是对最初设计理念的误解。正常来说，达到这样的目的应该使用 `onStoreCreated` 这个 hook 来实现。如果是单纯增加这个功能，也显得冗余。

下面是 `addExposed` 代码：

```ts
/**
 * Adds properties exposed by plugins into the Rematch instance. If a exposed
 * property is a function, it passes rematch as the first argument.
 *
 * If you're implementing a plugin in TypeScript, extend Rematch namespace by
 * adding the properties that you exposed from your plugin.
 */
function addExposed<
  TModels extends Models<TModels>,
  TExtraModels extends Models<TModels>
>(
  store: RematchStore<TModels, TExtraModels>,
  plugins: Plugin<TModels, TExtraModels>[]
): void {
  plugins.forEach((plugin) => {
    if (!plugin.exposed) return;
    const pluginKeys = Object.keys(plugin.exposed);
    pluginKeys.forEach((key) => {
      if (!plugin.exposed) return;
      const exposedItem = plugin.exposed[key] as
        | ExposedFunction<TModels, TExtraModels>
        | ObjectNotAFunction;
      const isExposedFunction = typeof exposedItem === "function";

      store[key] = isExposedFunction
        ? (...params: any[]): any =>
            (exposedItem as ExposedFunction<TModels, TExtraModels>)(
              store,
              ...params
            )
        : Object.create(plugin.exposed[key]);
    });
  });
}
```

### dispatch plugin

dispatch 在 v1 中主要是用于增强 `redux.dispatch`，支持链式派发 action，例如 `dispatch.modelName.reducerName`。而这部分逻辑都被放到了 v2 的 [`dispatcher.ts`](https://github.com/rematch/rematch/blob/main/packages/core/src/dispatcher.ts) 文件中。

### effects plugin

effects plugin 做了两点：

1. 暴露了全局 `effects`
2. 使用 `middleware` hook 处理副作用
3. 支持链式派发副作用 action，例如 `dispatch.modelName.effectName`。

在 v2 中，第一点的 effects 放到了 rematch bag 中，第二点则专门创建了一个函数 `createEffectsMiddleware` 来处理。第三点逻辑也移到了[`dispatcher.ts`](https://github.com/rematch/rematch/blob/main/packages/core/src/dispatcher.ts) 文件中。

## 新增 rematch bag

v2 中增加了一个对象叫做 rematchBag，用于存储一些全局可访问的信息，例如之前 v1 rematch class 中的 `forEachPlugin` 方法，以及完善的 models，reduxConfig 配置，以及上面提到的 effects 插件暴露的全局 `effects`：

```ts
/**
 * Object for storing information needed for the Rematch store to run.
 * Purposefully hidden from the end user.
 */
export interface RematchBag<
  TModels extends Models<TModels>,
  TExtraModels extends Models<TModels>
> {
  models: NamedModel<TModels>[];
  reduxConfig: ConfigRedux;
  forEachPlugin: <Hook extends keyof PluginHooks<TModels, TExtraModels>>(
    method: Hook,
    fn: (content: NonNullable<PluginHooks<TModels, TExtraModels>[Hook]>) => void
  ) => void;
  effects: ModelEffects<TModels>;
}
```

## 新增 plugin hooks

v1 的插件一共可以使用四个 hook，分别是：`onInit`，`middleware`，`onModel` 和 `onStoreCreated`。v2 删除了 `onInit`（因为 `onInit` 只在 pluginFactory 中使用，前面提到 pluginFactory 已经被移除），新增了 `onReducer` 和 `onRootReducer`，同时将 `middleware` 更名为 `createMiddleware`。

### middleware 更名为 createMiddleware

之前的 `middleware` 参数直接接收一个 redux middleware，而改为 createMiddleware 后，接收的则是一个函数，函数返回的还是 redux middleware，不过可以传递上面提到的 rematch bag 作为参数，以此方便用户对 middleware 进行定制。

v1 中使用 middleware 的是 effects, typed-state 和 subscriptions（v2 中被移除）三个插件，v2 中则仅剩 typed-state，且暂时也没有利用 rematch bag 参数。

例如 v1 和 v2 中使用对比：

**v1：**

```ts
const typedStatePlugin = (): Plugin => ({
  // ...
  middleware: (store) => (next) => (action) => {
    // ...
  },
});
```

**v2：**

```ts
const typedStatePlugin = <
  TModels extends Models<TModels>,
  TExtraModels extends Models<TModels> = Record<string, any>
>(
  config = DEFAULT_SETTINGS
): Plugin<TModels, TExtraModels> => {
  // ...

  return {
    // ...
    createMiddleware: () => (store) => (next) => (action) => {
      // ...
    },
  };
};
```

### 移除 onInit hook

onInit hook 在 pluginFactory 的 create 方法中被调用，我在 v1 代码中搜索了一下，也没看到有 plugin 使用这个 hook，因此不清楚它的具体作用。

v2 中由于 pluginFactory 的移除，因此 onInit 也没有了。

### 新增 onReducer hook

v2 在 onModel 的基础上新增了 onReducer，用于更细粒度的控制 reducer 行为。该 hook 会在生成单个 model reducer 的时候被调用，如果有返回值，则最终的 reducer 被返回值替代，否则继续使用原 reducer。

[之前讲 immer 插件](https://juejin.cn/post/6995151476389576734#heading-0)的时候提到过，v1 中的 immer 主要通过 `redux.combineReducers` 配置实现，且这个配置存在覆盖的问题。因此在 v2 中，通过更细粒度的 onReducer 来重构了 immer plugin，同理，也重构了 persist 插件。这样一来，就避免了同时使用 immer 和 persist 时 `combineReducers` 被覆盖的问题。

### 新增 onRootReducer hook

这个 hook 和 onReducer 类似，只不过是在最后生成 rootReducer 时调用。目前只有 persist 插件使用了这个 hook。

## 总结

以上便是本次升级带来的主要变化，看着有点多，实际对代码运行无太大影响，而且由于采用函数式的编程方式，代码流程变得更加清晰易懂。但仍然存在一些问题，例如这次升级的人员和 Rematch v1 的开发人员不是一批人，而且作者也已经不再维护该框架，难免会出现对之前设计理念的一些误解。

除此之外，v1 代码中使用了大量的 `this`，而且并不遵循纯函数的编程方式，代码里各种 mutation。v2 中减少了 `this` 的使用，但是还是存在不少对参数的 mutation。这些都让源码变得更加复杂，流程难以梳理和理解。

---

接下来的最后一篇文章，我会给大家讲讲我最擅长的部分，也就是 Rematch 的类型系统。这是一个很复杂的系统，我在 v2 中几乎将它重写了一遍，极大地提高了 Rematch 的可用性。不过，也还仍然残留许多问题，有些可能是我的知识所限无法解决，还有一些也因为 TypeScript 的设计限制，我都会在接下来的文章中和大家讨论，也希望有更厉害的类型体操队员能够加入我们，持续完善 Rematch，打造最佳体验。敬请期待！

