编写合格的React组件

众知React应用是一种基于组件的架构模式, 复杂的UI能够经过一些小的组件组合起来, 站在软件工程的角度这样的开发方式会提升开发效率, 程序的健壮性和可维护性。javascript

但在实际组件的编写中咱们一般会遇到一个问题: 复杂的组件每每具备多种职责, 而且组件之间的耦合性很高, 咱们越写越复杂的组件会产生技术负债, 恐惧每一次需求的变化, 在后期维护上花费很高的时间和精力成本。java

那么为了解决这个问题, 咱们须要思考如下2个问题:react

  • 复杂组件如何拆分?
  • 组件之间如何通讯会下降他们的耦合性或者说依赖?

Single responsibility 原则

A component has a single responsibility when it has one reason to change.ios

Single responsibility principle (SRP) 要求一个组件只作一件事情, 单一任务, 良好的可测试性, 是编写复杂组件的基础。这样当咱们需求变化时候, 咱们也只须要修改单一的组件, 不会出现连锁反应形成的"开发信心缺失"。git

举个实际的例子: 获取远程数据组件, 先分析出该组件中可能变化的点github

  • 请求地址
  • 响应的数据格式
  • 使用不一样的HTTP库
  • 等等

再举个例子: 表格组件, 拿到设计图看到设计图上有4行3列的数据, 直接写死4行3列是没有智慧的, 咱们仍是先要考虑可能变化的点:编程

  • 增长行列或者减小行列
  • 空的表格如何显示
  • 请求到的表格数据格式发生变化

有些人会以为是否是想太多? 不少时候人们一般会忽视SRP, 起初看来确实写在一块儿也没有糟更重要的缘由是由于写的快, 由于不须要去思考组件结构和通讯之类的事情, 可是在产品需求变化频繁的今天, 惟有良好的组件化设计才能保障产品迭代的速度与质量。redux

实践: 拆分一个Weather组件

import axios from 'axios';
class Weather extends Component {
   constructor(props) {
     super(props);
     this.state = { temperature: 'N/A', windSpeed: 'N/A' };
   }
 
   render() {
     const { temperature, windSpeed } = this.state;
     return (
       <div className="weather"> <div>Temperature: {temperature}°C</div> <div>Wind: {windSpeed}km/h</div> </div>
     );
   }
   
   componentDidMount() {
     axios.get('http://weather.com/api').then(function(response) {
       const { current } = response.data; 
       this.setState({
         temperature: current.temperature,
         windSpeed: current.windSpeed
       })
     });
   }
}
复制代码

明显这个组件的设计违反了SRP, 先让咱们分析一下Weather组件中有哪些会变化的点:axios

  • 网络请求部分可能会变, 好比服务器地址, 响应的数据格式
  • UI展现的逻辑可能会变, 有可能之后要增长其余天气信息

为了拥抱以上的变化咱们能够将Weather拆分红2个组件: WeatherFetchWeatherInfo, 分别用来处理网络请求和UI信息的展现。api

拆分为的组件应该是这样的

// Weather
class Weather extends Component {
   render() {
     return <WeatherFetch />;
   }
}

// WeatherFetch
class WeatherFetch extends Component {
   constructor(props) {
     super(props);
     this.state = { temperature: 'N/A', windSpeed: 'N/A' };
   }
 
    render() {
         const { temperature, windSpeed } = this.state;
         return (
           <WeatherInfo temperature={temperature} windSpeed={windSpeed} />
         );
    }
   
   componentDidMount() {
     axios.get('http://weather.com/api').then(function(response) {
       const { current } = response.data; 
       this.setState({
         temperature: current.temperature,
         windSpeed: current.windSpeed
       });
     });
   }
}

// WeatherInfo
function WeatherInfo({ temperature, windSpeed }) {
   return (
     <div className="weather">
       <div>Temperature: {temperature}°C</div>
       <div>Wind: {windSpeed} km/h</div>
     </div>
   );
}
复制代码

HOC的应用

Higher order component is a function that takes one component and returns a new component.

有些时候拆分组件也不必定是万能的, 好比想给一个组件上额外添加一些参数。 这时咱们可以使用高阶组件(HOC)

HOC最经典的使用场景是 props proxy , 即包裹一个组, 为其添加props或者修改已经存在的props, 并返回一个新组件。

function withNewFunctionality(WrappedComponent) {
  return class NewFunctionality extends Component {
    render() {
      const newProp = 'Value';
      const propsProxy = {
         ...this.props,
         // Alter existing prop:
         ownProp: this.props.ownProp + ' was modified',
         // Add new prop:
         newProp
      };
      return <WrappedComponent {...propsProxy} />; } } } const MyNewComponent = withNewFunctionality(MyComponent); 复制代码

Props proxy

写一个最基础的表单, 一个input, 一个button

class PersistentForm extends Component {  
  constructor(props) {
    super(props);
    this.state = { inputValue: localStorage.getItem('inputValue') };
    this.handleChange = this.handleChange.bind(this);
    this.handleClick = this.handleClick.bind(this);
  }

  render() {
    const { inputValue } = this.state;
    return (
      <div className="persistent-form"> <input type="text" value={inputValue} onChange={this.handleChange}/> <button onClick={this.handleClick}>Save to storage</button> </div> ); } handleChange(event) { this.setState({ inputValue: event.target.value }); } handleClick() { localStorage.setItem('inputValue', this.state.inputValue); } } 复制代码

咱们如今应该能本能的感受出上面的代码哪里有问题, 这个组件作了2件事情违反了SRP: input的点击事件将用户输入的内容存储到state, button的点击事件将state存储到localStorage, 如今让咱们拆分这两件事情。

class PersistentForm extends Component {  
  constructor(props) {
    super(props);
    this.state = { inputValue: props.initialValue };
    this.handleChange = this.handleChange.bind(this);
    this.handleClick = this.handleClick.bind(this);
  }

  render() {
    const { inputValue } = this.state;
    return (
      <div className="persistent-form"> <input type="text" value={inputValue} onChange={this.handleChange}/> <button onClick={this.handleClick}>Save to storage</button> </div> ); } handleChange(event) { this.setState({ inputValue: event.target.value }); } handleClick() { this.props.saveValue(this.state.inputValue); } } 复制代码

改为这样的话咱们须要一个父组件来提供存储到localStorage的功能, 这时候HOC就派上用场了, 咱们经过HOC为刚才的组件添加存储到localStorage的功能。

function withPersistence(storageKey, storage) {
  return function(WrappedComponent) {
    return class PersistentComponent extends Component {
      constructor(props) {
        super(props);
        this.state = { initialValue: storage.getItem(storageKey) };
      }

      render() {
         return (
           <WrappedComponent initialValue={this.state.initialValue} saveValue={this.saveValue} {...this.props} /> ); } saveValue(value) { storage.setItem(storageKey, value); } } } } 复制代码

最后把他们变为一个组件, 搞定!

const LocalStoragePersistentForm 
  = withPersistence('key', localStorage)(PersistentForm);

const instance = <LocalStoragePersistentForm />;
复制代码

经过HOC添加的localStora存储功能复用起来无比的方便, 好比如今有另外一个表单须要使用localStorage存储功能, 咱们只须要修改传递参数便可

const LocalStorageMyOtherForm
  = withPersistence('key', localStorage)(MyOtherForm);

const instance = <LocalStorageMyOtherForm />; 复制代码

Render highjacking

除了 props proxy 以外, HOC还有一个经典应用场景是 render highjacking

function withModifiedChildren(WrappedComponent) {
  return class ModifiedChildren extends WrappedComponent {
    render() {
      const rootElement = super.render();
      const newChildren = [
        ...rootElement.props.children, 
        // Insert a new child:
        <div>New child</div>
      ];
      return cloneElement(
        rootElement, 
        rootElement.props, 
        newChildren
      );
    }
  }
}
const MyNewComponent = withModifiedChildren(MyComponent);
复制代码

props proxy 不一样的是, render highjacking 能够在不 入侵 原组件的状况下, 修改其UI渲染。

Encapsulated 封装

An encapsulated component provides props to control its behavior while not exposing its internal structure.

Coupling (耦合) 是软件工程中不得不考虑的问题之一, 如何解耦或者下降耦合也是软件开发工程师遇到的难题。

低耦合如上图, 当你须要修改系统的一个部分时可能只会影响一小部分其余系统, 而下面这种高耦合是让开发人员对软件质量失去信心的原罪, 改一处可能瞬间爆炸。

隐藏信息

一个组件可能要操做refs, 可能有state, 可能使用了生命周期方法, 这些具体的实现细节其余组件是不该该知道的, 即: 组件之间须要隐藏实现细节, 这也是组件拆分的标准之一。

通讯

组件拆分后, 原来直接获取的数据, 如今就要依靠通讯来获取, 虽然更加繁琐, 可是在可读性和维护性上带来的好处远远大于它的复杂性的。React组件之间通讯的主要手段是:props

// 使用props通讯
<Message text="Hello world!" modal={false} />;

// 固然也能够传递复杂数据
<MoviesList items={['Batman Begins', 'Blade Runner']} />
<input type="text" onChange={handleChange} />

// 固然也能够直接传递组件(ReactNode)
function If({ component: Component, condition }) {
  return condition ? <Component /> : null;
}
<If condition={false} component={LazyComponent} />  

复制代码

Composable 组合

A composable component is created from the composition of smaller specialized components.

Medium上有一篇文章叫作 组合是React的心脏 (Composition is the heart of React), 由于它发挥了如下3个优势:

  • 单一责任
  • 复用性
  • 灵活性

接下来举🌰说明

单一责任

const app = (
  <Application> <Header /> <Sidebar> <Menu /> </Sidebar> <Content> <Article /> </Content> <Footer /> </Application>
);
复制代码

app这个组件中的每一个组件都只负责它该负责的部分, 好比Application只是一个应用的容器, <Footer />负责渲染页面底部的信息, 页面结构一目了然。

复用性

提取出不一样组件中的相同代码是提高维护性的最佳实践, 好比

const instance1 = (
  <Composed1> <Piece1 /> <Common /> </Composed1>
);
const instance2 = (
  <Composed2> <Common /> <Piece2 /> </Composed2>
);
复制代码

灵活性

组合的特性可让编写React代码时候很是灵活, 当组件组合时须要经过props进行通讯, 好比 父组件能够经过children prop 来接收子组件。

当咱们想为移动和PC展现不一样的UI时咱们一般会写成如下这样:

render(){
    return (<div> {Utils.isMobile() ? <div>Mobile detected!</div> : <div>Not a mobile device</div>} </div>) 
}
复制代码

At first glance, it harmeless, 可是它明显将判断是否时移动端的逻辑与组件耦合了。这不是在拼积木, 这是在"入侵"积木!

让咱们拆分判断逻辑与UI试图, 而且看看React如何使用 children prop 灵活的进行数据通讯。

function ByDevice({ children: { mobile, other } }) {
  return Utils.isMobile() ? mobile : other;
}

<ByDevice>{{
  mobile: <div>Mobile detected!</div>,
  other:  <div>Not a mobile device</div>
}}</ByDevice>
复制代码

Reusable 复用

A reusable component is written once but used multiple times.

软件世界常常犯的错误就是 reinventing the wheel (造轮子), 好比在项目中编写了已经存在的工具或者库, React组件也是同样的, 咱们要考虑代码的复用性, 尽量的下降重复的代码和造轮子的事情发生, 是咱们代码"写一次, 可使用不少次"。

Reuse of a component actually means the reuse of its responsibility implementation.

在这里能够找到不少高质量的React组件, 避免咱们造轮子: Absolutely Awesome React Components & Libraries

经过阅读上面这些可复用的高质量React组件的源码咱们会收获到更多复用的思想以及一些API的使用技巧好比:React.cloneElement等等。

Pure Component

Pure Component是从函数式编程延伸出来的概念, pure function always returns the same output for given the same input. 好比

const sum= (a, b) => a + b // sum(1, 1) // => 2

给相同的参数永远会获得相同的结果, 当一个函数内部使用全局变量的话那么那个函数可能会变得不那么"纯"(impure)。

let said = false;

function sayOnce(message) {
  if (said) {
    return null;
  }
  said = true;
  return message;
}

sayOnce('Hello World!'); // => 'Hello World!'
sayOnce('Hello World!'); // => null
复制代码

impure函数就是给定相同的参数确有可能获得不一样的结果, 那么组件也是一个道理, pure component组件会让咱们对本身的组件质量充满信心, 可是不可能全部的组件咱们均可以写成 pure component. 好比咱们的组件里面有一个<Input />, 那么咱们的组件不接受任何参数, 可是每次均可能产生不同的结果。

真实世界中太多impure的事情, 好比全局状态, 可改变的全局状态贻害不浅, 数据被意外改变致使意外的行为, 若是实在要使用全局状态, 那么考虑使用Redux吧。除了全局状态致使impure的东西还有不少好比网络请求, local storage等等, 那如何让咱们的组件尽量的变成pure component呢?

答案: purification

下面让咱们实践一下如何将impure中pure的部分过滤出来, 成为一个almost pure组件, 用前面获取天气的那个例子, 咱们把网络请求这种impure的东西使用redux-saga过滤出来

这是以前的代码:

class WeatherFetch extends Component {  
   constructor(props) {
     super(props);
     this.state = { temperature: 'N/A', windSpeed: 'N/A' };
   }

   render() {
     const { temperature, windSpeed } = this.state;
     return (
       <WeatherInfo temperature={temperature} windSpeed={windSpeed} /> ); } componentDidMount() { axios.get('http://weather.com/api').then(function(response) { const { current } = response.data; this.setState({ temperature: current.temperature, windSpeed: current.windSpeed }) }); } } 复制代码

改造后

// 定义action
export function fetch() {
  return {
    type: 'FETCH'
  };
}

// 定义dispatch handler
import { call, put, takeEvery } from 'redux-saga/effects';

export default function* () {
  yield takeEvery('FETCH', function* () {
    const response = yield call(axios.get, 'http://weather.com/api');
    const { temperature, windSpeed } = response.data.current;
    yield put({
      type: 'FETCH_SUCCESS',
      temperature,
      windSpeed
    });
  });
}

// 定义reducer 
const initialState = { temperature: 'N/A', windSpeed: 'N/A' };

export default function(state = initialState, action) {
  switch (action.type) {
    case 'FETCH_SUCCESS': 
      return {
        ...state,
        temperature: action.temperature,
        windSpeed: action.windSpeed
      };
    default:
      return state;
  }
}

// 使用redux链接起来
import { connect } from 'react-redux';
import { fetch } from './action';

export class WeatherFetch extends Component {
   render() {
     const { temperature, windSpeed } = this.props;
     return (
       <WeatherInfo temperature={temperature} windSpeed={windSpeed} /> ); } componentDidMount() { this.props.fetch(); } } function mapStateToProps(state) { return { temperature: state.temperate, windSpeed: state.windSpeed }; } export default connect(mapStateToProps, { fetch }); 复制代码

将impure的组件改为almost pure的组件可让咱们更了解程序的行为, 也将变得更易于测试

import assert from 'assert';
import { shallow, mount } from 'enzyme';
import { spy } from 'sinon';
// Import the almost-pure version WeatherFetch
import { WeatherFetch } from './WeatherFetch';
import WeatherInfo from './WeatherInfo';

describe('<WeatherFetch />', function() {
  it('should render the weather info', function() {
    function noop() {}
    const wrapper = shallow(
      <WeatherFetch temperature="30" windSpeed="10" fetch={noop} />
    );
    assert(wrapper.contains(
      <WeatherInfo temperature="30" windSpeed="10" />
    ));
  });

  it('should fetch weather when mounted', function() {
    const fetchSpy = spy();
    const wrapper = mount(
     <WeatherFetch temperature="30" windSpeed="10" fetch={fetchSpy}/>
    );
    assert(fetchSpy.calledOnce);
  });
});
复制代码

其实上面的almost pure组件仍然有优化的空间, 咱们能够借助一些工具库让它成为pure component

import { connect } from 'react-redux';  
import { compose, lifecycle } from 'recompose';
import { fetch } from './action';

export function WeatherFetch({ temperature, windSpeed }) {  
   return (
     <WeatherInfo temperature={temperature} windSpeed={windSpeed} /> ); } function mapStateToProps(state) { return { temperature: state.temperate, windSpeed: state.windSpeed }; } export default compose( connect(mapStateToProps, { fetch }), lifecycle({ componentDidMount() { this.props.fetch(); } }) )(WeatherFetch); 复制代码

可测试性

A tested component is verified whether it renders the expected output for a given input. A testable component is easy to test.

如何确保组件按照咱们的指望工做, 一般咱们会改下数据或者条件之类的而后在浏览器中看结果, 称之为手动验证。 这样手动验证有一些缺点:

  1. 临时修改代码为了验证容易出错
  2. 每次修改代码 每次验证很低效

所以, 咱们须要须要编写一些unit tests来帮助咱们测试组件, 可是编写unit tests的前提是, 咱们的组件是可测试的, 一个不可测试的组件绝对是设计不良的。

A component that is untestable or hard to test is most likely badly designed.

组件变得难以测试有不少因素, 好比太多的props, 高度耦合, 全局变量等等, 下面经过一个例子让咱们理解如何编写可测试组件。

编写一个Controls组件, 目的是实现一个计数器, 点击Increase则加1, 点击Decrease则减1, 先来一个错误的设计

<Control parent={ConponentName}
复制代码

假设咱们是这样使用的, 意图是咱们传入一个父组件, 点击Control的加减操做会修改父组件的state值

import assert from 'assert';
import { shallow } from 'enzyme';

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       
    }));
  }
}

class Temp extends Component {
  constructor(props) {
    super(props);
    this.state = { number: 0 };
  }
  render() {
    return null;
  }
}

describe('<Controls />', function() {
  it('should update parent state', function() {
    const parent = shallow(<Temp/>);
    const wrapper = shallow(<Controls parent={parent} />); assert(parent.state('number') === 0); wrapper.find('button').at(0).simulate('click'); assert(parent.state('number') === 1); wrapper.find('button').at(1).simulate('click'); assert(parent.state('number') === 0); }); }); 复制代码

因为咱们设计的Controls组件与父组件依赖很强, 致使咱们编写单元测试很复杂, 这时咱们就应该思考重构这个Controls提升它的可测试性了。

import assert from 'assert';
import { shallow } from 'enzyme';
import { spy } from 'sinon';

function Controls({ onIncrease, onDecrease }) {
  return (
    <div className="controls"> <button onClick={onIncrease}>Increase</button> <button onClick={onDecrease}>Decrease</button> </div>
  );
}

describe('<Controls />', function() {
  it('should execute callback on buttons click', function() {
    const increase = sinon.spy();
    const descrease = sinon.spy();
    const wrapper = shallow(
      <Controls onIncrease={increase} onDecrease={descrease} /> ); wrapper.find('button').at(0).simulate('click'); assert(increase.calledOnce); wrapper.find('button').at(1).simulate('click'); assert(descrease.calledOnce); }); }); 复制代码

重构后咱们的组件使用方法变为 <Controls onIncrease={increase} onDecrease={descrease} />, 这样的使用方式完全解耦了Controls和父组件之间的关系, 即: Controls只负责按钮UI的渲染。

可读性

A meaningful component is easy to understand what it does.

代码的可读性对于产品迭代的重要性是不可忽视的, obscured code不只会让维护者头疼, 甚至咱们本身也没法理解代码的意图。曾经有一个有趣的统计, 编程工做是由: 75%的读代码(理解) + 20%的修改现有代码 + 5%新代码组成的。

self-explanatory code无疑是提升代码可读性最直接最好的方法

举一个例子:

// <Games> renders a list of games
// "data" prop contains a list of game data
function Games({ data }) {
  // display up to 10 first games
  const data1 = data.slice(0, 10);
  // Map data1 to <Game> component
  // "list" has an array of <Game> components
  const list = data1.map(function(v) {
    // "v" has game data
    return <Game key={v.id} name={v.name} />;
  });
  return <ul>{list}</ul>;
}

<Games 
   data=[{ id: 1, name: 'Mario' }, { id: 2, name: 'Doom' }] 
/>
复制代码

下面让咱们重构这段代码, 使它能够 self-explanatoryself-documenting .

const GAMES_LIMIT = 10;

function GamesList({ items }) {
  const itemsSlice = items.slice(0, GAMES_LIMIT);
  const games = itemsSlice.map(function(gameItem) {
    return <Game key={gameItem.id} name={gameItem.name} />;
  });
  return <ul>{games}</ul>;
}

<GamesList 
  items=[{ id: 1, name: 'Mario' }, { id: 2, name: 'Doom' }]
/>
复制代码

一个可读性良好的React组件应该作到: 经过读nameprops就能够看出这段代码的意图。

写在最后的

即便编写出了自我感受良好的组件, 咱们也该在一次一次迭代中去 Do continuous improvement, 正如做家William Zinsse说过一句话

rewriting is the essence of writing. I pointed out that professional writers rewrite their sentences over and over and then rewrite what they have rewritten.

重构, 编写高质量, 可扩展, 可维护的应用是每一个开发人员的追求。

本文参考: 7 Architectural Attributes of a Reliable React Component

既然都看到这里了, 点个赞吧 💗

相关文章
相关标签/搜索