编写React组件的最佳实践

DarWhitt 8年前
   <p>在我第一次编写 React 代码的时候,我见发现许多不同的方法可以用来编写组件,不同教程教授的内容也大不相同。尽管从那时候起框架已经相当成熟,但并没有一种固定的“正确”方式指导。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/f6c9c627c9366c3f6ad729882856cc74.png"></p>    <p>在  MuseFind  工作的一年里,我们的团队编写了许多 React 组件,后期我们对方法进行了优化直到满意为止。</p>    <p>本指南描述了我们推荐的最佳实践,不管你是一名初学者还是有经验的老手,希望它能对你有所帮助。</p>    <p>在我们开始之前,有几个地方要注意一下:</p>    <ul>     <li> <p>我们使用的是 ES6 和 ES7 的语法。</p> </li>     <li> <p>如果你对于现实和容器组件两者之间的区别不甚明了,建议首先阅读一下 <a href="/misc/goto?guid=4959739403984764524" rel="nofollow,noindex">这个</a> 。</p> </li>     <li> <p>如果有任何建议、疑问或者感想,请通过评论来让我们知晓。</p> </li>    </ul>    <h3><strong>基于类的组件</strong></h3>    <p>基于类的组件具有丰富状态而且可以含有方法。我们要尽可能有节制地去使用,因为它们也有特定的适用场合。</p>    <p>让我们使用一行一行的代码逐步地将我们的组件构建起来吧。</p>    <p><strong>引入 CSS</strong></p>    <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>我喜欢 在 JavaScript 中操作 CSS ,这在理论上这样做是可行的。不过它仍然是一种新的创意,还没出现切实可行的解决方案。不过在此之前,我们可以先为每一个组件引入一个 CSS 文件。</p>    <p>我们也通过另写一行将依赖引入从本地引入独立了出来。</p>    <p><strong>状态初始化</strong></p>    <p> </p>    <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>propTypes 和 defaultProps</p>    <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'    }</code></pre>    <p>propTypes 和 defaultProps 是静态属性,要在组件代码中尽可能高的位置进行声明。它们作为文档放在醒目的位置,其他开发者阅读此文件时能立即看到。</p>    <p>所有的组件都应该有 propTypes。</p>    <p><strong>方法</strong></p>    <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>    <p><strong>属性析构</strong></p>    <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>拥有许多属性的组件要让每个属性都另起一行,如上所示。</p>    <p><strong>装饰器</strong></p>    <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>    <p><strong>闭包</strong></p>    <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 组件,不管它的其它属性实际是否已经发生了变化,都会自动地触发让它重新渲染。</p>    <p>调和是 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>    <h3><strong>函数式组件</strong></h3>    <p>这些组件没有状态和方法。它们就是单纯的组件,容易理解。要尽可能常去使用它们。</p>    <p>propTypes</p>    <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  }  // Component declaration  ExpandableForm.propTypes = expandableFormRequiredProps</code></pre>    <p>这里,我们将 propTypes 分配给了顶部一行的变量。在组件声明的下面,我们对它们进行了正常的分配。</p>    <p>对 Props 和 defaultProps 进行析构</p>    <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>我的组件是一个函数,因此可以将它的属性看做是参数。我们可以像下面这样对它们进行扩展:</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 的角色。如果 expanded 是 undefined, 我们就会将其设置为 false。 (这个例子有点勉强,因为是一个布尔值,不过本身对于避免对象的“Cannot read <property> of undefined“这样的错误是很有用的)。</p>    <p>要避免如下这种 ES6 语法:</p>    <pre>  <code class="language-javascript">const ExpandableForm = ({ onExpand, expanded, children }) => {</code></pre>    <p>看着非常现代,不过这里的函数实际上没有被命令。</p>    <p>这样子的名称在 Bable 进行了正确的设置的情况下是可行的 — 但如果没有正确设置,任何错误都会以在<<anonymous>>中出现的方式显示,调试起来相当麻烦。</p>    <p>无名的函数也会在 Jest 这个 React 测试库中引发问题。为了避免潜在的复杂问题出现,我们建议使用 function 而不是 const。</p>    <p><strong>封装</strong></p>    <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>    <pre>  <code class="language-javascript">import React from 'react'  import {observer} from 'mobx-react'  // Separate local imports from dependencies  import './styles/Form.css'    // Declare propTypes here as a variable, then assign below function declaration   // You want these to be as visible as possible  const expandableFormRequiredProps = {    onSubmit: React.PropTypes.func.isRequired,    expanded: React.PropTypes.bool  }    // Destructure props like so, and use default arguments as a way of setting defaultProps  function ExpandableForm({ onExpand, expanded = false, children }) {    return (      <form style={ expanded ? { height: 'auto' } : { height: 0 } }>       {children}       <button onClick={onExpand}>Expand</button>     </form>    )  }    // Set propTypes down here to those declared above  ExpandableForm.propTypes = expandableFormRequiredProps    // Wrap the component instead of decorating it  export default observer(ExpandableForm)</code></pre>    <h3><strong>JSX 中的条件分支</strong></h3>    <p>你会有不少机会去做许多条件分支渲染。如下是你想要去避免的情况:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/f1823318d05ab4f4422d59518a8e756b.png"></p>    <p>嵌套的三元组并非不是好主意。</p>    <p>有一些库可以解决这个问题 ( JSX-Control Statements ),不过相比引入额外的依赖,通过如下这种方式解决复杂条件分支问题要更好:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/93b8f77c49b3679bca749a916de0975d.png"></p>    <p>使用花括弧封装一个 IIFE , 然后在里面放入 if 语句,可以返回任何你想要渲染的东西。注意像这样的 IIFE 对性能会有影响,不过在大多数情况中还不足以让我们为此选择丢掉可读性。</p>    <p>还有就是当你只想要在一个条件分支中渲染一个元素时,比起这样做…</p>    <pre>  <code class="language-javascript">{    isTrue     ? <p>True!</p>     : <none/>  }</code></pre>    <p>… 使用短路写法更划算:</p>    <pre>  <code class="language-javascript">{    isTrue &&       <p>True!</p>  }</code></pre>    <p> </p>    <p>来自:http://developer.51cto.com/art/201702/533215.htm</p>    <p> </p>