React 数据管理之 Rematch

2,384 阅读5分钟

TL;DR

通过本文将了解:

  1. Rematch 的定位
  2. Rematch 创新点
  3. 对比 Rematch 和 dva
  4. Rematch 源码分析,了解其插件系统实现方式

Rematch 的定位

Rematch 的定位是基于 redux 的数据管理方案,并为开发者提供了更好的开发体验。更好的开发体验包括:

  • 支持 TS
  • 简化 redux 配置
  • 减少模板代码
  • 支持异步副作用处理机制

等等...

Rematch 创新点

1. 返回的 Store 支持 TS

使用 Rematch 后,我们可以通过 store.dispatch.model.xxx() 来执行一个 Action 操作,并且 store.dispatch 的类型是 TS 自动推算出来的,无需开发者手动定义。这不仅增强了编码的效率(类型自动推导)和正确性(类型检测),而且还解决了在纯 redux 方案中维护和使用 ActionType 的痛点。

由于每个 Model 是一个单独的文件,那么在真实的业务场景中,我们如何在一个 Model 中使用其他 Model 的数据或行为呢?要实现这样的功能,我们需要通过 Rematch 的最佳实践,分以下三步创建 Store。

  1. 将每个 Model 通过 createModel 方法创建出来
  2. 通过 Models<RootModel> 组装出最终的 models
  3. 创建 Store
type CountState = number

// 每个 model 通过 createModel 方法创建出来
// increment1 方法中使用了 count2 中的 Action,其中 dispatch 是由 TS 自动推导出来的
const count1 = createModel<RootModel>()({
  state: 0,
  reducers: {},
  effects: dispatch => {
    return {
      increment1: (payload: number) => {
        dispatch.count2.increment2(payload)
      },
    }
  },
})

// 每个 model 通过 createModel 方法创建出来
const count2 = createModel<RootModel>()({
  state: 0,
  reducers: {
    increment2: (state: CountState, payload: number): CountState =>
      state + payload,
  },
})

// 通过 `Models<RootModel>` 组装出最终的 Model 类型
interface RootModel extends Models<RootModel> {
  count1: typeof count1
  count2: typeof count2
}
const models: RootModel = { count1, count2 }
const store = init({
  models,
})

// store.getState() 和  store.dispatch 已经被自动推算出类型了

2. 基于 model 组织状态

在 Rematch 中,基于 Model 组织状态的好处有:

  1. 减少了模板代码。Model 组织状态后,可自动生成 ActionType。开发者便无需声明 ActionType,也不用在 reducer 函数中对 ActionType 进行判断了。
  2. 将状态和对状态的操作放在同一个文件中,简化了代码组织。无需纠结 reducer、ActionCreator 和 ActionType 分别应该放在哪里。

3. 副作用处理实现

粗略一看以为 Rematch 的副作用是基于 redux-thunk 实现的,但实际上并不是。Rematch 的副作用处理实现方式非常直观简单,可以通过以下两点讲清楚:

  1. effects 方法调用时,实际上会 dispatch 一个 {model}/{effect} 的 Action。
  2. Rematch 实现了一个 redux middleware,在该 middleware 中判断 Action 是否是一个 effect,如果是则调用 effect 函数。

既然副作用实现和 redux-thunk 没有任何关系,所以当 effects 是一个函数时,它并没有第二个参数 getState,不要和 redux-thunk 搞混了。

// redux-thunk
function (dispatch, getState) {
  // ...
}

// Rematch 的 effects
createModel()({
  state: 0,
  reducers: {},
  // 这里没有第二个参数 getState
  effects: dispatch => {
    return {
      increment1: (payload: number) => {
        dispatch.count2.increment2(payload)
      },
    }
  },
})

4. 提供更易用的 redux 使用方案

Rematch 的核心定位就是简化 redux 的使用,所以它内部支持了一些实用的功能。

  1. 提供 redux.rootReducers 选项,选项,rootReducers 优先于 model.reducers 执行,rootReducers 的返回值会被作为新的 state 传入 model.reducers 中,参考源码。可用于修改整个 Store 的数据,例如:重置状态。也可用于将纯 Redux 项目的 reducers 复制过来,便于平滑接入 Rematch。
  2. model 提供 baseReducer 选项,baseReducer 的优先级也比 model.reducers 高,会将 baseReducer 的返回值作为新的状态传给 model.reducers,参考源码。和 redux.rootReducers 一样,也可用于重置状态。
  3. 提供 select 插件包,通过 reselect 实现的计算属性。但个人觉得学习成本偏高,写起来有点看不懂。
  4. 提供 loading 插件,插件按照 effect/model/global 进行划分。
  5. 提供 persist 插件,可将 store 中数据持久化。
  6. 提供 immer 插件,可以在 reducer 中直接使用修改 state。
  7. 提供 updated 插件,可以记录每个 effect 的最后结束时间。
  8. 提供 typed-state 插件,用于每次 Action 执行完后,检查 State 是否还符合定义的类型。开发者通过 model.typings 字段传入期望的类型。

对比 Rematch 和 dva

先谈下 Rematch 和 dva 的相同点:

  1. 通过 Model 来组织 State
  2. 简化 redux 的配置和使用
  3. 都提供了 immer 和 loading 插件

它们的不同点如下:

  1. dva 定位更偏向于基于 redux 的应用框架解决方案,它集成了 react-router、同构(服务端和浏览器端)请求方式,实现了懒加载 Model 和页面。而 Rematch 更倾向于提供更易用的 API,如:支持 TS、简化配置和多种插件,提升开发体验。
  2. dva 的副作用使用 redux-saga 实现,并且副作用是一个生成器函数,学习成本偏高。而 Rematch 的副作用实现机制更简单,更符合直觉。
  3. dva 支持 model 的动态新增和删除。但是 Rematch 只实现了新增 model,不支持删除 model,而且动态新增 model 后也不会自动修改 Store 的类型定义。可以认为 Rematch 并不推荐动态增删 model,因为动态修改后 Store 的类型就和运行时类型不一致了,Rematch 最大的优势(类型定义)就失效了。

插件系统

Rematch 的插件系统都是通过回调函数实现,Rematch 封装了 forEachPlugin 方法来调用插件的回调函数。

function forEachPlugin(method, fn): void {
  config.plugins.forEach(plugin => {
    if (plugin[method]) {
      fn(plugin[method]!)
    }
  })
}

当看到 forEachPlugin 这种实现时,心中就产生了一个疑惑。这种方式如何使用插件函数的返回值呢?

参考下面的代码,通过修改闭包外部变量的方式,就可以将插件函数的返回值利用上了。

let rootReducer = {}

// 这种方式也可以利用插件的返回值
// 例如通过插件的 onRootReducer 钩子修改 rootReducer
bag.forEachPlugin("onRootReducer", onRootReducer => {
  rootReducer = onRootReducer(rootReducer, bag) || rootReducer
})

看完 Rematch 源码后,感觉 Rematch 的源码还是比较整洁易读的,其插件系统实现得非常简单。

总结

通过本文我们知道 Rematch 是基于 Redux 实现的数据管理方案,其主要优势在于创建出的 Store 支持 TypeScript 类型定义。除了支持 TS 外,它还提供易用的副作用处理机制、分 Model 组织状态、多种插件支持等,极大地提升了开发体验。