MuseFind:编写React组件的最佳实现

xypb1184 8年前
   <p>React 起源于 非死book 的内部项目,因为该公司对市场上所有 JavaScript MVC框架,都不满意,就决定自己写一套,用来架设 Instagram 的网站。做出来以后,发现这套东西很好用,就在2013年5月 开源 了。</p>    <p>由于React的设计思想极其独特,属于革命性创新,性能出众,代码逻辑却非常简单。所以,越来越多的人开始关注和使用,认为它可能是将来Web开发的主流工具。</p>    <p>当我第一次开始写React代码时,我记得看到过许多不同的编写组件的方法,各教程之间有很大的不同。虽然自那时以来框架已经相当成熟,但似乎还没有一个确定“正确”编写的方式。</p>    <p>在过去一年里,在 MuseFind ,我们的团队编写了很多React组件。我们已经逐渐完善了方法,直到我们满意为止。</p>    <p>本指南代表我们建议的最佳做法。我们认为本文对新手和老手都有所帮助。</p>    <p>阅读本文之前,读者需要注意以下几点:</p>    <ul>     <li>我们使用ES6和ES7语法。</li>     <li>如果你不确定展示型组件和容器组件之间的区别,我们建议你先阅读 <a href="/misc/goto?guid=4959739403984764524" rel="nofollow,noindex">这篇文章</a> 。</li>     <li>如果您有任何建议问题或反馈,请在原文的评论区中告诉我们。</li>    </ul>    <h2>基于类的组件</h2>    <p>基于类的组件是有状态的,或许还包含方法。我们尽可能少地使用它们,但它们也有自己的位置。</p>    <p>让我们逐行构建我们的组件。</p>    <h3>导入CSS</h3>    <pre>  <code class="language-javascript">import React, {Component} from 'react'  import {observer} from 'mobx-react'  import ExpandableForm from './ExpandableForm'  import './styles/ProfileContainer.css'</code></pre>    <p>理论上,我喜欢 CSS in JavaScript 。但它仍然是一个新的想法,还没有出现一个成熟的解决方案。在此之前,我们将一个CSS文件导入到每个组件。</p>    <p>我们还通过换行将依赖导入与本地导入分开。</p>    <h3>初始化状态</h3>    <pre>  <code class="language-javascript">import React, {Component} from 'react'  import {observer} from 'mobx-react'  import ExpandableForm from './ExpandableForm'  import './styles/ProfileContainer.css'  export default class ProfileContainer extends Component {    state = { expanded: false }</code></pre>    <p>如果你使用ES6(ES7不适用),在构造函数中初始化状态。否则,使用专用于ES7的方法。更多信息在 <a href="/misc/goto?guid=4959739581383979156" rel="nofollow,noindex">这篇文章</a> 。</p>    <p>我们还要确保将我们的类导出为默认类。</p>    <h3>propTypes和defaultProps</h3>    <pre>  <code class="language-javascript">propTypes和defaultProps    import React, {Component} from 'react'  import {observer} from 'mobx-react'  import ExpandableForm from './ExpandableForm'  import './styles/ProfileContainer.css'  export default class ProfileContainer extends Component {    state = { expanded: false }    static propTypes = {      model: React.PropTypes.object.isRequired,      title: React.PropTypes.string    }    static defaultProps = {      model: {        id: 0      },      title: 'Your Name'    }</code></pre>    <p>propTypes和defaultProps是静态属性,在组件代码中声明的优先级尽可能高。由于它们作为文档,因此它们应该对其他读取文件的开发者可见。</p>    <p>所有的组件应该有propTypes。</p>    <h3>方法</h3>    <pre>  <code class="language-javascript">import React, {Component} from 'react'  import {observer} from 'mobx-react'  import ExpandableForm from './ExpandableForm'  import './styles/ProfileContainer.css'  export default class ProfileContainer extends Component {    state = { expanded: false }    static propTypes = {      model: React.PropTypes.object.isRequired,      title: React.PropTypes.string    }    static defaultProps = {      model: {        id: 0      },      title: 'Your Name'    }    handleSubmit = (e) => {      e.preventDefault()      this.props.model.save()    }    handleNameChange = (e) => {      this.props.model.name = e.target.value    }    handleExpand = (e) => {      e.preventDefault()      this.setState({ expanded: !this.state.expanded })    }</code></pre>    <p>使用类组件,当将方法传递给子组件时,必须确保它们在调用时具有正确的this。通常通过传递this.handleSubmit.bind(this)到子组件来实现。</p>    <p>我们认为这种方法更简洁也更容易,通过ES6的箭头函数自动保持正确的上下文。</p>    <h3>解构props</h3>    <pre>  <code class="language-javascript">import React, {Component} from 'react'  import {observer} from 'mobx-react'  import ExpandableForm from './ExpandableForm'  import './styles/ProfileContainer.css'  export default class ProfileContainer extends Component {    state = { expanded: false }    static propTypes = {      model: React.PropTypes.object.isRequired,      title: React.PropTypes.string    }    static defaultProps = {      model: {        id: 0      },      title: 'Your Name'    }  handleSubmit = (e) => {      e.preventDefault()      this.props.model.save()    }    handleNameChange = (e) => {      this.props.model.name = e.target.value    }    handleExpand = (e) => {      e.preventDefault()      this.setState(prevState => ({ expanded: !prevState.expanded }))    }    render() {      const {        model,        title      } = this.props      return (         <ExpandableForm           onSubmit={this.handleSubmit}           expanded={this.state.expanded}           onExpand={this.handleExpand}>          <div>            <h1>{title}</h1>            <input              type="text"              value={model.name}              onChange={this.handleNameChange}              placeholder="Your Name"/>          </div>        </ExpandableForm>      )    }  }</code></pre>    <p>具有多个props的组件,每个props应该占据单独一个行,如上所示。</p>    <h3>装饰器</h3>    <pre>  <code class="language-javascript">@observer  export default class ProfileContainer extends Component {</code></pre>    <p>如果你使用像 mobx 这样的东西,你可以将类组件装饰成这样:这与将组件传递到函数相同。</p>    <p>装饰器 通过灵活、可读的方式来修改组件功能。我们广泛地使用装饰器,配合mobx和我们自己的 mobx-models 库。</p>    <p>如果您不想使用装饰器,请执行以下操作:</p>    <pre>  <code class="language-javascript">class ProfileContainer extends Component {    // Component code  }  export default observer(ProfileContainer)</code></pre>    <h3>闭包</h3>    <p>避免传递新的闭包到子组件,像这样:</p>    <pre>  <code class="language-javascript"><input   type="text"   value={model.name}   // onChange={(e) => { model.name = e.target.value }}   // ^ Not this. Use the below:   onChange={this.handleChange}   placeholder="Your Name"/></code></pre>    <p>这就是为什么每次父组件渲染时,创建一个新的函数并传递给输入的原因。</p>    <p>如果输入是React组件,这将自动触发它重新渲染,而不管它的其他props是否实际改变。</p>    <p>调和算法(Reconciliation)是React最耗时的部分。不要让它比所需更难!此外,传递类的方法更容易阅读、调试和更改。</p>    <p>这是我们的完整组件:</p>    <pre>  <code class="language-javascript">import React, {Component} from 'react'  import {observer} from 'mobx-react'  // Separate local imports from dependencies  import ExpandableForm from './ExpandableForm'  import './styles/ProfileContainer.css'  // Use decorators if needed  @observer  export default class ProfileContainer extends Component {    state = { expanded: false }    // Initialize state here (ES7) or in a constructor method (ES6)    // Declare propTypes as static properties as early as possible    static propTypes = {      model: React.PropTypes.object.isRequired,      title: React.PropTypes.string    }    // Default props below propTypes    static defaultProps = {      model: {        id: 0      },      title: 'Your Name'    }    // Use fat arrow functions for methods to preserve context (this will thus be the component instance)    handleSubmit = (e) => {      e.preventDefault()      this.props.model.save()    }    handleNameChange = (e) => {      this.props.model.name = e.target.value    }    handleExpand = (e) => {      e.preventDefault()      this.setState(prevState => ({ expanded: !prevState.expanded }))    }    render() {      // Destructure props for readability      const {        model,        title      } = this.props      return (         <ExpandableForm           onSubmit={this.handleSubmit}           expanded={this.state.expanded}           onExpand={this.handleExpand}>          // Newline props if there are more than two          <div>            <h1>{title}</h1>            <input              type="text"              value={model.name}              // onChange={(e) => { model.name = e.target.value }}              // Avoid creating new closures in the render method- use methods like below              onChange={this.handleNameChange}              placeholder="Your Name"/>          </div>        </ExpandableForm>      )    }  }</code></pre>    <h2>函数组件</h2>    <p>这些组件没有状态、方法。它们是纯粹的,简单的。因此要尽可能经常使用它们。</p>    <h3>propTypes</h3>    <pre>  <code class="language-javascript">propTypes    import React from 'react'  import {observer} from 'mobx-react'  import './styles/Form.css'  const expandableFormRequiredProps = {    onSubmit: React.PropTypes.func.isRequired,    expanded: React.PropTypes.bool  }  // Component declaration  ExpandableForm.propTypes = expandableFormRequiredProps</code></pre>    <p>这里,我们在组件声明前分配 propTypes,因此它们立即可见。在组件声明下面,我们正确地分配它们。</p>    <h3>解构Props和defaultProps</h3>    <pre>  <code class="language-javascript">import React from 'react'  import {observer} from 'mobx-react'  import './styles/Form.css'  const expandableFormRequiredProps = {    onSubmit: React.PropTypes.func.isRequired,    expanded: React.PropTypes.bool  }  function ExpandableForm(props) {    return (      <form style={props.expanded ? {height: 'auto'} : {height: 0}}>        {props.children}        <button onClick={props.onExpand}>Expand</button>      </form>    )  }</code></pre>    <p>我们的组件是一个函数,它的Props作为其参数。我们可以这样扩展:</p>    <pre>  <code class="language-javascript">import React from 'react'  import {observer} from 'mobx-react'  import './styles/Form.css'  const expandableFormRequiredProps = {    onExpand: React.PropTypes.func.isRequired,    expanded: React.PropTypes.bool  }  function ExpandableForm({ onExpand, expanded = false, children }) {    return (      <form style={ expanded ? { height: 'auto' } : { height: 0 } }>        {children}        <button onClick={onExpand}>Expand</button>      </form>    )  }</code></pre>    <p>注意,我们也可以使用默认参数作为defaultProps以高度可读的方式。如果展开没有定义的话,我们将其设置为false。(这是一个有点强迫的例子,因为它是一个布尔,但非常有用,以避免“无法读取未定义”错误与对象)。</p>    <p>避免使用以下ES6语法:</p>    <pre>  <code class="language-javascript">const ExpandableForm = ({ onExpand, expanded, children }) => {</code></pre>    <p>看起来很现代,但此处的函数实际上是未命名的。</p>    <p>如果你的Babel设置正确,这个名字的缺失不会成为一个问题:但如果不是,任何错误将<<anonymous>>中显示,对调试而言,是一个非常严重的问题。</p>    <p>未命名的函数也可能导致Jest(一个React测试库)的问题。由于潜在的难以理解的bug,以及并没有什么真正的好处,我们建议使用function而不是const。</p>    <h3>包装</h3>    <p>因为你不能使用装饰器和功能组件,你只需将函数作为参数传递给它:</p>    <pre>  <code class="language-javascript">import React from 'react'  import {observer} from 'mobx-react'  import './styles/Form.css'  const expandableFormRequiredProps = {    onExpand: React.PropTypes.func.isRequired,    expanded: React.PropTypes.bool  }  function ExpandableForm({ onExpand, expanded = false, children }) {    return (      <form style={ expanded ? { height: 'auto' } : { height: 0 } }>        {children}        <button onClick={onExpand}>Expand</button>      </form>    )  }  ExpandableForm.propTypes = expandableFormRequiredProps  export default observer(ExpandableForm)</code></pre>    <p>这是我们的完整组件:</p>    <h2>JSX条件</h2>    <p>很可能你要做很多条件渲染。这里是你想避免的地方:</p>    <p>(点击放大图像)</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/2fd9c6ba7cb278e57c175acedd6c6fea.png"></p>    <p>这是我在MuseFind早期写的实际代码,饶恕我吧。</p>    <p>不,嵌套的三元运算并不是一个好主意。</p>    <p>有一些库解决了这个问题( JSX控制语句 ),但是,而不是引入另一个依赖,我们解决了这种应对复杂条件的方法:</p>    <p>(点击放大图像)</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/e452437639b09d0c570acbcf8afa364d.png"></p>    <p>以上所示是重构版本。</p>    <p>使用花括号括起一个 IIFE ,然后把你的if语句置于里面,返回任何你想要的渲染。请注意,这样的IIFE可能会导致性能下降,但在大多数情况下,它不会严重到以致失去可读性。</p>    <p>此外,当你只想渲染一个条件上的元素,而不是这样做:</p>    <pre>  <code class="language-javascript">{    isTrue     ? <p>True!</p>     : <none/>  }</code></pre>    <p>使用short-circuit赋值:</p>    <pre>  <code class="language-javascript">{    isTrue &&       <p>True!</p>  }</code></pre>    <p> </p>    <p>来自:http://www.infoq.com/cn/articles/our-best-practices-for-writing-react-components</p>    <p> </p>