侯策《前端开发核心知识进阶》读书笔记——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>
)

副作用和(准)纯组件

组件可测试性

组件命名

(待完善)

 

posted @ 2020-05-04 20:45  姚啊姚  阅读(202)  评论(0编辑  收藏  举报