React 组件复用和组合 (一)

组件的复用和组合,可以帮助我们在现有的轮子上扩展新的功能,提高工作效率,避免重复造轮子。React 组件化的开发方式可以很好地实现复用和组合的功能,本章主要围绕高阶组件来讨论下这一问题。

高阶组件(HOC)

高阶组件(HOC)是 React 中用于重用组件逻辑的高级技术。 HOC 本身不是 React API 的一部分。 它们是从 React 构思本质中浮现出来的一种模式。

通俗点高阶组件就是一个函数,其接受一个组件并返回对着个组件功能上的扩展复用的新的组件。

const EnhancedComponent = higherOrderComponent(WrappedComponent);

高阶组件常见有两种实现方式,一种是 Props Proxy(属性代理),一种是 Inheritance Inversion(继承反转)

Props Proxy

Props Proxy 模式可以对 WrappedComponent 的 props 进行操作扩展,抽离 state,并可以使用其他元素来包裹 WrappedComponend 来实现扩展组件的功能。

操作 props

1
2
3
4
5
6
7
8
function propsHOC(WrappedComponent) {
return class extends Component {
render() {
let newProps = {/.../};
return <WrappedComponent {...props} {...newProps}/>
}
}
}

可以看到,传递给 WrappedComponent 的属性首先传递给了高阶组件返回的组件,这样我们就获得了props的控制权。

抽离 state

我们可以将 WrappedComponent 中的状态提到包裹组件中,一种很常见的操作就是将不受控组件转换成受控组件。通常,我们在设计 UI 组件的时候,组件应该简单只负责展示 (不受控组件)。对组件的修改逻辑不应该放在组件中,而是由调用者来提供。这样,我们就可以用HOC来将无状态组件变成受控组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function stateHoc(WrappedComponent) {
return class extends Component {
constructor(props) {
super(props);
this.state = {
value: ''
};
}
onChange(event) {
this.setState({
value: event.target.value
})
}
render() {
const props = {
...this.props,
value: this.state.value,
onChange: this.onChange
}
return <WrappedComponent {...props}/>
}
}
}
@stateHoc
class InputStateLess extends Component {
render() {
return <input name="name" value={this.props.value} />;
}
}

Inheritance Inversion

反向继承,我们采用直接继承 WrappedComponent 的方案,而不是采用包裹 WrappedComonent 的代理方案。这意味着,我们可以调用 WrappedComponent 的属性,声明周期等任何内容。

1
2
3
4
5
6
7
function iiOC(WrappedComponent) {
return class extends WrappedComponent {
render() {
return super.render();
}
}
}

通过我们可以完全操作 WrappedComponent 上的内容,我们可以实现渲染劫持等操作,改变 WrappedComponent 的任何行为。
Inheritance Inversion 是继承的思想,对于 WrappedComponent 也有较强的侵入性,因此并不常见。

高阶组件的应用

高阶组件的本质是统一抽象功能,强调逻辑和UI的分离

view 层分离

在设计组件的时候,我们会尽可能考虑组件的复用性,对于组件的 view 层,我们期望是组件与组件之间没有重叠的部分,重叠的部分应该被抽出来形成更细粒度的组件,这样方便我们各种各样的组件组合。每个最小的基础组件我们都期望他是一个木偶组件(Dumb Component)
木偶组件,指只会接受 props 并且渲染结果完全依赖 props 的组件。Dumb 组件不应该依赖除了 React.js 和 Dumb 组合以外的内容(比如不应该依赖 redux,mobx 等)。这样的组件可复用性是最好的,其他人可以放心使用。
当然仅有 Dumb 组件,是不能工作的,因为他们没有逻辑。为此还应该有这么一类组件,他们只负责应用逻辑,和各种数据打交道,然后把数据以 props 的形式传递给 Dumb 组件。
注意,Dumb 绝对不能依赖 Smart 组件,这相当增加了 Dumb 输出的不确定性。如果一个组件是 Dumb 的,那么它的子组件们都应该是 Dumb 的才对。

a.png

逻辑层的分离

组件中的交互逻辑和业务逻辑有很大部分也是重复的,我们可以将这写公共部分进行抽象封装起来,来为其他组件增加新的能力,这也就是高阶组建的思想。每个独立可重用的逻辑都是一个 Decorator 装饰器。
适用于高阶组件的逻辑层应该时那些完全不与 DOM 相关的内容。比如数据校验,权限控制,或者通过数据变化间接控制 DOM 的。

举例 Form 表单的抽离

Form 中,会包含不同的组件,input, selector, checkbox 等等,也可能会是多种常见组件的组合。

b.png

如图,一个下拉搜索框,由 input,select, list 三个纯粹的细粒度的 Dumb 组件组成。对于每个 UI 都有自己的数据 validator 验证规则,和数据变化回调规则。我们可以将这部分逻辑的对应关系和UI的绑定做成一个 HOC 组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 函数柯里化,为 HOC 提供参数
// validator 用户定义的变淡验证规则
// changeFunc 相应数据变化的函数
function formFactory(validator, changeFunc = 'onChange') {
return function(WrappedComponent) {
return class extends React.Component {
getBind(changeFunc, validtor) {
//...
}
render() {
const props = {
...this.props,
[trigger]: this.getBind(trigger, validtor)
}
return <WrappedComponent {...props} />
}
}
}
}
// 为 Input 组件绑定验证规则
formFactory((value) => {
return value > 0
})(<Input name="age" placeholder="请输入年龄"/>)

高阶组件的问题

不确定性 & 命名冲突

高阶组件的变更是由 props 引起的,且高阶组件之间是互相独立的。因此当组件的 props 发生变化后,我们很难确定是哪个高级组件引起的 props 变动或者是组件本身引起的。同样多个高阶组件的引入的 props 也有可能因为同名原因导致互相覆盖,产生许多无用的组件嵌套加深组件层级,这些问题高阶组件都无法解决。
官方文档上推荐我们使用高阶组件的时候提出了如下约定:

约定: 给包裹组件传递不相关的属性(Props)

这个问题只能靠约定而没有办法约束,因此可维护性变低。

静态方法必须复制 & Refs 不会被传递

由于高阶组件包裹的特性,原有组件上的静态方法并不会得到传递。同样的道理 ref 在容器组件上应用 ref 也不会直接传递给原有组件。我们都需要在容器组件上做静态方法的复制和 ref 的传递。

高阶组件的带来的副作用有没有解决办法呢?render-props 可以在一定程度上来解决,这个之后我们再谈吧。

参考资料