积梦前端采用的 React 状态管理方案: Rex

积梦(https://jimeng.io) 是一个为制造业制作的一个平台.
积梦的前端基于 React 做开发的. Rex 是我们在前端使用的状态管理方案, 类似 Redux.
从名字也可以看, Rex 是一个基于 Redux 做了大幅简化的方案.
另一方面, Rex 跟 Immer 有比较好的整合, 能够很轻松得使用不可变数据.

先前的技术方案

在开发 Rex 之前, 我们主要采用了 mobx-state-tree 的方案, 以及试验过 Redux.
最早的代码使用了 mobx 搭配 mobx-state-tree, 比较迎合 observe 的用法.
但是使用 mobx 全家桶遇到了一些比较困扰的问题,

  • mobx 对数据封装的话,数据量比较大的时候初始化非常慢, 对应图表.
  • observable 数据调试很不方便, 打印在 Console 是一个难以读取的对象.
  • mobx-state-tree 内置了 types, 加上偶尔有改版, 经常出现不可控, 比如报错, 字段修改.

由于我一直就是 immutable 数据的支持者, 就一直在试验能否用不可变数据解决这些问题.
但是早先主要是 immutablejs 方案, 按照以前的使用经验, 成本比较高.
后来出现了 immer, 在工业聚当有 Micheal 的介绍下我们开始局部尝试, 取得了不错的效果.
而且因为 immer 也是 mobx 全家桶作者 Micheal 发布的模块, 使用也比较顺畅.

最初我尝试过用 immer 搭配 Redux 来局部替换一些全局状态,
试验之后我觉得效果上没有达到预期,

  • Redux 的 action/dispatch 在 JavaScript 当中没有足够灵活,
    在函数式语言比如 Clojure 当中, 一切解释表达式, 默认不可变数据, 处理 action 非常顺畅,
    但是用 JavaScript, 加上 immer 之后好一些, 但还是需要显式地到处引入 immer.
  • TypeScript 类型跟 Redux 配合比较麻烦, 需要非常明确定义好各个 Action.
    在 ReasonML 当中用代数类型很容易定义不同的 Action, 而 TypeScript 相对繁琐.
    而且早先因为代码处理不干净, 类型推断并不是生效, 影响了实际体验.

所以 Redux 方案没有按预期地推进下去.

Rex 的特点

其实不管 Redux 还是 mobx-state-tree. 我想要的还是状态透传的功能.
通过 @connect(() => {}) 来封装组件, 让局部能获得访问全局状态的能力.
至于具体的数据操作, immer 已经做到我们可以接受的程度了.

后来在知乎看到过别人模仿 Redux 开发的类库, 我萌生了自己裁剪 Redux 代码的想法.
在同事的帮助下优化了 decorator 部分的代码, 我大致梳理出这样一个类库,

  • 基于 Context 实现 @connect() 的语法, 进行数据透传,
  • 用 immer 维护全局数据, 并且暴露出方便使用的方法.
  • 基于以前的代码, 大致处理好监听状态改变的的逻辑.
  • 生成 TypeScript 使用的类型文件.

Rex 的使用

目前 Rex 经过半年多的使用验证, 大致已经趋向稳定, 代码在 GitHub 上可以查看,
https://github.com/jimengio/rex
或者通过 npm 安装到本地,

npm install @jimengio/rex

使用 Rex 首先就是要定义全局状态的结构, 比如:

export interface IGlobalStore {
  obj: {
    a: number;
  };
  b: string;
}

export let initialStore: IGlobalStore = {
  obj: { a: 2 },
  b: "b"
};

然后初始化一个 globalStore, 包含该状态:

import { createStore } from "@jimengio/rex";

export let globalStore = createStore<IGlobalStore>(initialStore);

这里如果你想获取 store 的状态, 通过一个方法来读取,

globalStore.getState()

以及监听 store 的改变, 处理重绘:

globalStore.subscribe(() => {
  // rerender
});

当你要对数据进行操作时, 有两个方法可以使用, updateupdateAt.
这两个方法直接将 immer 封装在内, 虽然是赋值操作, 但实际上是不可变数据,
如果你对 immer 有疑问, 请仔细阅读它的文档并自行试验 https://github.com/mweststrat...
其实 updateAtupdate 的语法糖, 对于特定分支的数据的修改相对方便:

export function doIncData() {
  globalStore.update((store) => {
    store.b = "modified data";
  });

  globalStore.updateAt("obj", (obj) => {
    obj.a += 1;
  });
}

这个抽象大致从 Clojure 的 Atom 借鉴, 数据并不能直接修改.
如果你要修改状态, 就需要发送一个函数给 Rex, 然后 Rex 会在内部进行修改.

此外也提供了 RexProvider connectRex useRexContext 等函数, 跟 Redux 习惯尽量一致.
基于这些函数, 就能实现一个简单的状态管理, 以及数据更新的透传.

一些需要注意

Rex 实现较为简单, 目前没有做更深层的优化, 也在避免引入新概念.
实际使用除了一些模板代码, 主要是 immer 的不可变数据需要注意.
immer 使用的是可变数据的写法, 但是内部通过 proxy 机制进行了转化, 达到不可变数据的效果.
平时写代码的时候需要注意区分可变和不可变的数据, 比较写法上是类似的.

另外要注意上面比如 doIncData 函数, 如果内部包含异步操作, 需要注意,
Rex 并不能支持异步, 所以在异步事件前后, 都需要直接调用 .update(),
也就是说对应会有两个(甚至多个)更新事件, 界面会更新多次.

Rex 封装时, 考虑到性能问题, 做了一些基本的 shouldComponentUpdate 检测.
不过对于 useRexContext 这个 Hooks 的写法. 目前没有想明白怎么样处理, 需要手动处理.

其他

其他关于积梦前端的模块和工具可以查看我们的 GitHub 主页 https://github.com/jimengio .
招聘的计划和条件也在 GitHub 上有给出 https://github.com/jimengio/h... .

相关推荐