[译] 更可靠的 React 组件:合理的封装

2020-06-15 22:24:13 浏览数 (1)

原文摘自: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 对象中包含了一个可修改的数字属性,并负责渲染该数字:

代码语言:javascript复制
// 问题在于:破坏了封装
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。

代码语言:javascript复制
// 问题在于:使用了父组件的内部结构
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:

代码语言:javascript复制
// 解决方法:恢复封装
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> 被转换成了一个无状态组件:

代码语言:javascript复制
// 解决方法:使用回调函数升级符组件的 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> 变得易于重用。测试它同样方便:只需要修改点击按钮时的回调就可以了。

0 人点赞