在上文中提到了一种基于 mobx 的 React 组件设计方案:它将状态与逻辑
剥离到 mobx
中,并通过工厂函数
将它们与组件进行关联、注入
,通过分层解决了前端组件基于UI进行测试的脆弱性
与组件生命周期被自动调用影响测试结果
的问题。但这种设计模式同样产生了一些新的问题,在本篇中,我们将逐一尝试解决。
优化
闭包导致的 api 不可测试
1 | function createCount(initialCount) { |
对于该工程函数,它将承担了状态与逻辑的聚合、组件的创建等任务,但由于闭包导致onClick
、didMount
等聚合后产生的 api 无法被测试。解决的办法其实很简单——OOP,让我们将工厂函数转化为工厂类:
1 | class CountFactory { |
- 我们将
countStore
与loadingStore
转化为私有属性实现依赖的解耦
(同时借助 ts 对 class 的优化,省去了手动赋值的过程) - 我们将
onClick
与didMount
转变为类的成员方法,使得其能对外暴露,方便测试 - 作为牺牲,我们暂时移除为
countStore
等被依赖的 store 提供初始化参数的功能(一般在 redux 中,初始值都是固定的,这点牺牲可以忍受)
接收全局 Store、Service
在我们将工厂函数转变为工厂类后,失去了通过函数参数像工厂提供全局Store和Service
的功能,该功能在组件间通信、发送接口请求时非常重要。为此,我们将创建一个工厂基类:
1 | import { ComponentType } from "react"; |
BaseFactory
类具有 2 个成员属性globalStores
和services
,前者将保存用户基本信息、loading 等全局状态,后者与发送接口请求相关。BaseFactory
类具有receive
公有方法,用于外部将需要的数据传递进来并赋值。- 后续的工厂类将继承
CountFactory
,并在实例化后调用receive
方法
使用 Reflect-Metadata 实现自动依赖注入
在完成第一步后,我们虽然实现了依赖解耦,但也面临一个新的问题:每次创建工厂实例都需要手动传递依赖类的实例,这个过程若人工完成则非常容易犯错。为此,我们将通过metadata提供的元编程能力,将依赖注入自动化
1 | import "reflect-metadata"; |
- 借助强大的 metadata 元编程,ts 会在经过
Injectable
装饰器描述的CountFactory
类上,将构造函数参数的类型CountStore
和LoadingStore
记录在CountFactory
上 Factory
将会读取传递给它的CountFactory
上的元数据,得到[CountStore, LoadingStore]
这一数组(数组的成员均为实体类)Factory
将这些实体类实例化并作为参数用于CountFactory
的实例化,这样自动依赖注入的过程就完成了
可以看到,到这一步为止,将工厂函数改造为工厂类带来的大多数问题都已经被解决,而两个组件之间的差异也仅在于传递给Factory
的工厂类名称而已。
类似 dva-loading 的内置 loading
在将工厂函数的原有问题解决完之后,我们会发现一个之前所忽略的一个小问题——需要人工设置 loading 状态——也在引入成员装饰器后有了解决的希望。这就开始着手处理:
1 | import { observable, action } from "mobx"; |
- 我们通过 mobx 定义了一个
LoadingStore
类和对应的loading
装饰器,前者保存了所有的被注册过的异步 api 的 loading 状态,后者通过 descriptor 对 loadingStore 进行操作。 globalStores
获取了loadingStore
,并通过BaseFactory
的receive
方法将 loadingStore 注入到工厂类中。CountFactory
工厂类对异步操作didMount
附加装饰器,并在 create 中通过类名/方法名
的形式取得这一 api 的 loading 状态。
这种方法相关便捷的实现了 loading 的内置效果,但是也存在一些问题:
- loadingStore 是一个独立的全局 Store,若某个工厂类被创建了 2 次,并且他们同时存在,那么 loading 便会互相干扰。
- 由于只有
MethodDecorator
能通过第三个参数取得原本属性的 descriptor,而PropertyDecorator
不能,且箭头函数定义方式会将函数以成员属性
而非原型方法
的形式创建,因此 loading 装饰器无法与箭头函数共存,需要在构造函数中人为绑定 this(是不是很熟悉?)
总结
至此,我们已经对原本的工厂函数进行大刀阔斧的改造,解决了依赖耦合、难以测试的问题,通过提供了自动依赖注入、内置 loading 的功能,大大提升了该模式的可用性。
到这里我们可以看到,在模式下的组件,整体上分为 4 层:
- 基于类的持久层:Store 与 Presenter
- 基于类的工厂:Factory
- 基于类的 Api 等:Service
- 函数式的组件:View
组件的函数化与逻辑的 oop 化是一个明显的特征。由于组件不再承担大量业务逻辑,函数式的写法可读性更高;而其他层为了使结构更清晰、易于测试,选择了 OOP 的模式