原文摘自:https://dmitripavlutin.com/7-architectural-attributes-of-a-reliable-react-component/
封装过的组件应提供 props 以控制其行为,而不是暴露内部的结构
耦合(coupling) 是一种表示组件之间依赖度的系统特征。根据依赖的程度,可以区分出两种耦合:
- 组件对其他组件了解的很少,甚至一无所知的情况,就是松耦合
- 组件掌握着其他组件的大量细节时,就是紧耦合
在设计系统结构和组件间关系的时候,应以松耦合为目标。
松耦合将带来如下的好处:
- 系统中的局部改变不影响他处
- 任何组件都可以被替代品取代
- 系统之间的组件可以复用,顺应了 DRY(Don't repeat yourself)原则
- 可以轻易测试独立的组件,提高了应用的测试代码覆盖率
反之,紧耦合的系统就没有上述便利。主要的缺点就在于无法轻易修改一个大量依赖其他组件的组件。甚至一个简单的改变都会导致连锁的修改。
封装,或者说是 信息隐藏,是设计组件时的基本原则,也是达成松耦合的关键。
1. 信息隐藏
一个封装良好的组件会隐藏其内部结构,并通过一组 props 提供控制其行为的途径。
隐藏内部结构是必须的。内部结构或实现细节不能被其他组件知道或关联。
React 组件可以是函数式的,也可以是基于类的,可以定义实例方法、设置 refs、维护 state 或是使用生命周期方法。这些实现细节被封装在组件自身中,其他组件不应该窥见其中的任何细节。
隐藏了内部结构的单元(units)-- 如此处谈论的组件,对其他单元的依赖是低的。低依赖度带来的是松耦合的好处。
2. 通信
细节隐藏是一种用来隔离组件的约束手段。虽然如此,还是需要组件之间的通信的。所以有请 props 吧~
作为组件的输入,prop 最好是 JS 基本类型 (如 string、number、boolean):
代码语言:javascript复制<Message text="Hello world!" modal={false} />;
必要的时候可以用对象或数组等复杂类型:
代码语言:javascript复制<MoviesList items={['Batman Begins', 'Blade Runner']} />
作为事件处理和异步操作时,可以指定为函数:
代码语言:javascript复制<input type="text" onChange={handleChange} />
prop 甚至可以是一个组件构造器。组件可被用来处理其他组件的实例化:
代码语言:javascript复制function If({ Component, condition }) {
return condition ? <Component /> : null;
}
<If condition={false} component={LazyComponent} />
为避免破坏封装,要谨慎对待 props 传递的细节。父组件对子组件设置 props 时,也不应该暴露自身的结构。比如,把整个组件实例或 refs 当成 props 传递就是个糟糕的决定。
访问全局变量是另一个对封装造成负面影响的问题。
3. 案例学习:封装的恢复
组件实例和 state 对象都是封装在组件内部的实现。当把父组件实例传递给子组件,想籍此来管理 state 时,就百分之百的破坏了封装。
来看一个这样的情况。
这是个显示一个数字,以及“加”、“减”两个按钮的简单应用:
代码语言:javascript复制<div id="root"></div>
代码语言:javascript复制class App extends React.Component {
...
}class Controls extends React.Component {
...
}ReactDOM.render(<App />, document.getElementById('root'));
该应用由两个组件组成:<App>
和 <Controls>
。
<App>
的 state 对象中包含了一个可修改的数字属性,并负责渲染该数字:
// 问题在于:破坏了封装
class App extends Component {
constructor(props) {
super(props);
this.state = { number: 0 };
}
render() {
return (
<div className="app">
<span className="number">{this.state.number}</span>
<Controls parent={this} />
</div>
);
}
}
<Controls>
渲染两个按钮,并在按钮上附加了点击事件处理函数。当用户点击时,父组件的 state 被更新,相应的数字显示也会加 1 或减 1。
// 问题在于:使用了父组件的内部结构
class Controls extends Component {
render() {
return (
<div className="controls">
<button onClick={() => this.updateNumber( 1)}>
Increase
</button>
<button onClick={() => this.updateNumber(-1)}>
Decrease
</button>
</div>
);
}
updateNumber(toAdd) {
this.props.parent.setState(prevState => ({
number: prevState.number toAdd
}));
}
}
当前的实现错在何处呢?
第一个问题是 <App>
被破坏的封装,其内部结构在应用里尽人皆知了。<App>
错误的允许 <Controls>
直接更新其内部 state 了。
随之发生的,第二个问题是 <Controls>
知道了太多 <App>
的细节。它可以访问父组件的实例、了解父组件的 state 对象结构,还知道如何更新父组件的 state。
被破坏的封装造成了两个组件的耦合。
一个麻烦的后果就是,<Controls>
难以被测试和重用了。对 <App>
的一个细小的结构改变,都将引起 <Controls>
或更多层子组件的连锁修改。
解决方法是设计一个方便的通信接口,同时满足松耦合和强封装。让我们对两个组件的结构和 props 都做出一些改进,以修复封装。
只有组件自身可以了解其 state 结构。<App>
的 state 管理要从 <Controls>
中挪回来。
然后,<App>
被修改为向 <Controls>
的 onIncrease 和 onDecrease 两个 props 中提供回调函数,用于升级 state:
// 解决方法:恢复封装
class App extends Component {
constructor(props) {
super(props);
this.state = { number: 0 };
}
render() {
return (
<div className="app">
<span className="number">{this.state.number}</span>
<Controls
onIncrease={() => this.updateNumber( 1)}
onDecrease={() => this.updateNumber(-1)}
/>
</div>
);
}
updateNumber(toAdd) {
this.setState(prevState => ({
number: prevState.number toAdd
}));
}
}
现在 <Controls>
接受到用于加减数字的两个回调函数。关键在于 <Controls>
不用再直接访问父组件 <App>
的 state 了。
此外 <Controls>
被转换成了一个无状态组件:
// 解决方法:使用回调函数升级符组件的 state
function Controls({ onIncrease, onDecrease }) {
return (
<div className="controls">
<button onClick={onIncrease}>Increase</button>
<button onClick={onDecrease}>Decrease</button>
</div>
);
}
<App>
的封装得到了恢复。由自身管理 state,这正是其本职工作。
此外 <Controls>
不再依赖于 <App>
的实现细节了。onIncrease 和 onDecrease 两个 prop 回调函数会在点击相应按钮时被调用,而这些回调函数中的实现细节, <Controls>
不再需要了解,也本不应该知道。
<Controls>
的可重用性和可测试性显著的提升了。
因为只需要回调函数,没有其他依赖,<Controls>
变得易于重用。测试它同样方便:只需要修改点击按钮时的回调就可以了。