侯策《前端开发核心知识进阶》读书笔记——React组件设计
组件的单一职责
原则上讲,组件只应该做一件事情。但是对于应用来说,全部组件都拆散,只有单一职责并没有必要,反而增加了编写的繁琐程度。那什么时候需要拆分组件,保证单一职责呢?如果一个功能集合有可能发生变化,那么就需要最大程度地保证单一职责。
单一职责带来的最大好处就是在修改组件时,能够做到全在掌控下,不必担心对其他组件造成影响。举个例子:我们的组件需要通过网络请求获取数据并展示数据内容,这样一来潜在的功能集合改变就有:
- 请求 API 地址发生变化
- 请求返回数据格式变化
- 开发者想更换网络请求第三方库,比如 jQuery.ajax 改成 axios
- 更改请求数据逻辑
再看一个例子:我们需要一个 table 组件,渲染一个 list,那么潜在更改的可能有:
- 限制一次性渲染的 item 个数(只渲染前 10 个,剩下的懒加载)
- 当数据列表为空时显示 “This list is empty”
- 任何渲染逻辑的更改
实际看一个场景
import axios from 'axios' class Weather extends Component { constructor(props) { super(props) this.state = { temperature: 'N/A', windSpeed: 'N/A' } } componentDidMount() { axios.get('http://weather.com/api').then(response => { const { current } = response.data this.setState({ temperature: current.temperature, windSpeed: current.windSpeed }) }) } render() { const { temperature, windSpeed } = this.state return ( <div className="weather"> <div>Temperature: {temperature} °C</div> <div>Wind: {windSpeed} km/h</div> </div> ) } }
这个组件很容易理解,并且看上去没什么大问题,但是并不符合单一职责。比如这个 Weather 组件将数据获取与渲染逻辑耦合在一起,如果数据请求有变化,就需要在 componentDidMount 生命周期中进行改动;如果展示天气的逻辑有变化,render 方法又需要变动。
如果我们将这个组件拆分成:WeatherFetch 和 WeatherInfo 两个组件,这两个组件各自只做一件事情,保持单一职责:
import axios from 'axios' import WeatherInfo from './weatherInfo' class WeatherFetch extends Component { constructor(props) { super(props) this.state = { temperature: 'N/A', windSpeed: 'N/A' } } componentDidMount() { axios.get('http://weather.com/api').then(response => { const { current } = response.data this.setState({ temperature: current.temperature, windSpeed: current.windSpeed }) }) } render() { const { temperature, windSpeed } = this.state return ( <WeatherInfo temperature={temperature} windSpeed={windSpeed} /> ) } }
在另外一个文件中:
const WeatherInfo = ({ temperature, windSpeed }) =>
(
<div className="weather">
<div>Temperature: {temperature} °C</div>
<div>Wind: {windSpeed} km/h</div>
</div>
)
如果我们想进行重构,使用 async/await 代替 Promise,只需要直接更改 WeatherFetch 组件:
class WeatherFetch extends Component { // ... async componentDidMount() { const response = await axios.get('http://weather.com/api') const { current } = response.data this.setState({ temperature: current.temperature, windSpeed: current.windSpeed }) }) } // ... }
这只是一个简单的例子,在真实项目中,保持组件的单一职责将会非常重要,甚至我们可以使用 HoC 强制组件的单一职责性。
组件通信和封装
组件关联有紧耦合和松耦合之分,众所周知,松耦合带来的好处是很直接的:
- 一处组件的改动完全独立,不影响其他组件
- 更好的复用设计
- 更好的可测试性
场景:简单计数器
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> ) } } class Controls extends Component { updateNumber(toAdd) { this.props.parent.setState(prevState => ({ number: prevState.number + toAdd })) } render() { return ( <div className="controls"> <button onClick={() => this.updateNumber(+1)}> Increase </button> <button onClick={() => this.updateNumber(-1)}> Decrease </button> </div> ) } }
这样的组件实现问题很明显:App 组件不具有封装性,它将实例传给 Controls 组件,Controls 组件可以直接更改 App state 的内容。
优化后的代码
class App extends Component { constructor(props) { super(props) this.state = { number: 0 } } updateNumber(toAdd) { this.setState(prevState => ({ number: prevState.number + toAdd })) } render() { return ( <div className="app"> <span className="number">{this.state.number}</span> <Controls onIncrease={() => this.updateNumber(+1)} onDecrease={() => this.updateNumber(-1)} /> </div> ) } } const Controls = ({ onIncrease, onDecrease }) => { return ( <div className="controls"> <button onClick={onIncrease}>Increase</button> <button onClick={onDecrease}>Decrease</button> </div> ) }
这样一来,Controls 组件就不需要再知道 App 组件的内部情况,实现了更好的复用性和可测试性,App 组件因此也具有了更好的封装性。
组合性
如果两个组件 Composed1 和 Composed2 具有相同的逻辑,我们可以使用组合性进行拆分重组:
const instance1 = ( <Composed1> // Composed1 逻辑 // 重复逻辑 </Composed1> ) const instance2 = ( <Composed2> // 重复逻辑 // Composed2 逻辑 </Composed2> )
重复逻辑提取为common组件
const instance1 = ( <Composed1> <Logic1 /> <Common /> </Composed1> ) const instance2 = ( <Composed2> <Common /> <Logic2 /> </Composed2> )
副作用和(准)纯组件
组件可测试性
组件命名
(待完善)