99

写 React 组件的最佳实践

 6 years ago
source link: https://mp.weixin.qq.com/s?__biz=MzI2NzI4MTEwNA%3D%3D&mid=2247484591&idx=1&sn=aa75549c6861808be762b2a9d561c2fc&chksm=ea807310ddf7fa06a19310924877a387c2b0d1f529e0d5e8eb9014d93fa52b444b716eae5f36%23rd
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

写 React 组件的最佳实践

Original 蒋宏伟 58无线技术 2017-12-01 09:46 Posted on

本文为译文,已获得原作者允许,原文地址:http://scottdomes.com/blog/our-best-practices-for-writing-react-components/

当我第一次开始写 React 时,我发现多少个 React 教程,就有多少种写 React 组件方法。虽然如今,框架已经成熟,但是并没有一个 “正确” 写组件的方法。

一年以来,我们的团队写了大量的 React 组件。我们精益求精,不断完善写 React 组件的方法。

本文介绍了,我们团队写 React 组件的最佳实践。
我们希望,无论你是初学者,还是经验丰富的人,这篇文章都会对你有用的。

在开始介绍之前,先说几个点:

  • 我们团队使用 ES6 和 ES7 的语法。

  • 如果不清楚表现组件(presentational components)和容器组件(container components)之间的区别,我们建议先阅读 这篇文章。

  • 如果有任何建议,问题或反馈意见,请在评论中通知我们。

基于类的组件

基于类的组件(Class based components)是包含状态和方法的。
我们应该尽可能地使用基于函数的组件(Functional Components
)来代替它们。但是,现在让我们先来讲讲怎么写基于类的组件。

让我们逐行地构建我们的组件。

引入 CSS

import React, { Component } from 'react'
import { observer } from 'mobx-react'

import ExpandableForm from './ExpandableForm'
import './styles/ProfileContainer.css'

我认为最理想的 CSS 应该是 CSS in JavaScript。但是,这仍然是一个新的想法,还没有一个成熟的解决方案出现。
所以,现在我们还是使用将 CSS 文件引入到每个 React 组件中的方法。

我们团队会先引入依赖文件(node_modules 中的文件),然后空一行,再引入本地文件。

初始化状态

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 }

可以使用在 constructor 中初始化状态的老方法。
也可以使用 ES7 这种简单的初始化状态的新方法。
更多,请阅读 这里。

propTypes and defaultProps

import React, { Component } from 'react'
import { observer } from 'mobx-react'
import { string, object } from 'prop-types'

import ExpandableForm from './ExpandableForm'
import './styles/ProfileContainer.css'

export default class ProfileContainer extends Component {  state = { expanded: false }  static propTypes = {    model: object.isRequired,    title: string  }  static defaultProps = {    model: {      id: 0    },    title: 'Your Name'  }

propTypes 和 defaultProps 是静态属性(static properties),在组件代码中,最好把它们写在组件靠前的位置。当其他开发人员查看这个组件的代码时,应该立即看到 propTypes 和 defaultProps,因为它们就好像这个组件的文档一样。(译注:关于组件书写的顺序,参考 这篇文章)

如果使用 React 15.3.0 或更高版本,请使用 prop-types 代替 React.PropTypes。使用 prop-types 时,应当将其解构。

所有组件都应该有 propTypes

Methods

import React, { Component } from 'react'
import { observer } from 'mobx-react'
import { string, object } from 'prop-types'

import ExpandableForm from './ExpandableForm'
import './styles/ProfileContainer.css'

export default class ProfileContainer extends Component {  state = { expanded: false }  static propTypes = {    model: object.isRequired,    title: string  }  static defaultProps = {    model: {      id: 0    },    title: 'Your Name'  }  handleSubmit = (e) => {    e.preventDefault()
   this.props.model.save()  }  handleNameChange = (e) => {
   this.props.model.changeName(e.target.value)  }  handleExpand = (e) => {    e.preventDefault()
   this.setState({ expanded: !this.state.expanded })  }

使用基于类的组件时,当你将方法传递给组件时,你必须保证方法在调用时具有正确的上下文 this。常见的方法是,通过将 this.handleSubmit.bind(this) 传递给子组件来实现。

我们认为,上述方法更简单,更直接。通过 ES6 箭头功能自动 bind 正确的上下文。

给 setState 传递一个函数

在上面的例子中,我们这样做:

this.setState({ expanded: !this.state.expanded })

因为 setState 它实际上是异步的。
由于性能原因,所以 React 会批量的更新状态,因此调用 setState 后状态可能不会立即更改。

这意味着在调用 setState 时,不应该依赖当前状态,因为你不能确定该状态是什么!

解决方案是:给 setState 传递函数,而不是一个普通对象。函数的第一个参数是前一个状态。

this.setState(prevState => ({ expanded: !prevState.expanded }))

解构 Props

import React, { Component } from 'react'
import { observer } from 'mobx-react'
import { string, object } from 'prop-types'

import ExpandableForm from './ExpandableForm'
import './styles/ProfileContainer.css'

export default class ProfileContainer extends Component {  state = { expanded: false }  static propTypes = {    model: object.isRequired,    title: string  }  static defaultProps = {    model: {      id: 0    },    title: 'Your Name'  }
 handleSubmit = (e) => {    e.preventDefault()  
   this.props.model.save()  }  handleNameChange = (e) => {
   this.props.model.changeName(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>    )  } }

如上,当组件具有多个 props 值时,每个 prop 应当单独占据一行。

@observer
export default class ProfileContainer extends Component {

如果使用 mobx,那么应当是用装饰器(decorators)。其本质是将装饰器的组件传递到一个函数。

使用装饰器一种更加灵活和更加可读的方式。
我们团队在使用 mobx 和我们自己的 mobx-models 库时,使用了大量的装饰器。

如果您不想使用装饰器,也可以按照下面的方式做:

class ProfileContainer extends Component {
 // Component code
}
  export default observer(ProfileContainer)

避免传递一个新闭包(Closures)给子组件,像下面这样:

<input
    type="text"value={model.name}
   // onChange={(e) => { model.name = e.target.value }}
   // ^ 上面是错误的. 使用下面的方法:
   onChange={this.handleChange}    placeholder="Your Name"
/>

为什么呢?因为每次父组件 render 时,都会创建一个新的函数(译注:通过 (e) => { model.name = e.target.value } 创建的新的函数也叫 闭包)。

如果将这个新函数传给一个 React 组件,无论这个组件的其他 props 有没有真正的改变,都就会导致它重新渲染。

调和(Reconciliation)是 React 中最耗费性能的一部分。因此,要避免传递新闭包的写法,不要让调和更加消耗性能!另外,传递类的方法的之中形式更容易阅读,调试和更改。

下面是我们整个组件:

import React, { Component } from 'react'
import { observer } from 'mobx-react'
import { string, object } from 'prop-types'

// 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: object.isRequired,    title: 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>    )  } }

基于函数的组件

基于函数的组件(Functional Components)是没有状态和方法的。它们是纯粹的、易读的。尽可能的使用它们。

propTypes

import React from 'react'
import { observer } from 'mobx-react'
import { func, bool } from 'prop-types'

import './styles/Form.css'

ExpandableForm.propTypes = {  onSubmit: func.isRequired,  expanded: bool }

// Component declaration

在声明组件之前,给组件定义 propTypes,因为这样它们可以立即被看见。
我们可以这样做,因为 JavaScript 有函数提升(function hoisting)。

解构 Props 和 defaultProps

import React from 'react'
import { observer } from 'mobx-react'
import { func, bool } from 'prop-types'

import './styles/Form.css'

ExpandableForm.propTypes = {  onSubmit: func.isRequired,  expanded: bool,  onExpand: func.isRequired }

function ExpandableForm(props) {
 const formStyle = props.expanded ?
  {height: 'auto'} : {height: 0}
  return (
   <form style={formStyle} onSubmit={props.onSubmit}>      {props.children}
     <button onClick={props.onExpand}>Expand</button>    </form>  ) }

我们的组件是一个函数,函数的参数就是组件的 props。我们可以使用解构参数的方式:

import React from 'react'
import { observer } from 'mobx-react'

import { func, bool } from 'prop-types'
import './styles/Form.css'

ExpandableForm.propTypes = {
  onSubmit: func.isRequired,
  expanded: bool,
  onExpand: func.isRequired
}

function ExpandableForm({ onExpand, expanded = false, children, onSubmit }) {
  const formStyle = expanded ?
    {height: 'auto'} : {height: 0}
     return (    <form style={formStyle} onSubmit={onSubmit}>      {children}      <button onClick={onExpand}>Expand</button>    </form>  ) }

注意,我们还可以使用默认参数作为 defaultProps,这种方式可读性更强。
如果 expanded 未定义,则将其设置为false。(这样可以避免类似 ‘Cannot read of undefined’ 之类的错误)

避免使用函数表达式的方式来定义组件,如下:

const ExpandableForm = ({ onExpand, expanded, children }) => {

这看起来非常酷,但是在这里,通过函数表达式定义的函数却是匿名函数。

如果 Bable 没有做相关的命名配置,那么报错时,错误堆栈中不会告诉具体是哪个组件出错了,只会显示 <> 。这使得调试变得非常糟糕。

匿名函数也可能会导致 React 测试库 Jest 出问题。由于这些潜在的隐患,我们推荐使用函数声明,而不是函数表达式。

因为基于函数的组件不能使用修饰器,所以你应当将基于函数的组件当做参数,传给修饰器对应的函数:

import React from 'react'
import { observer } from 'mobx-react'
import { func, bool } from 'prop-types'

import './styles/Form.css'

ExpandableForm.propTypes = {
  onSubmit: func.isRequired,
  expanded: bool,
  onExpand: func.isRequired
}

function ExpandableForm({ onExpand, expanded = false, children, onSubmit }) {
  const formStyle = expanded ?
    {height: 'auto'} : {height: 0}
     return (    <form style={formStyle} onSubmit={onSubmit}>      {children}      <button onClick={onExpand}>Expand</button>    </form>  ) } export default observer(ExpandableForm)

全部的代码如下:

import React from 'react'
import { observer } from 'mobx-react'
import { func, bool } from 'prop-types'

// Separate local imports from dependencies
import './styles/Form.css'

// Declare propTypes here, before the component (taking advantage of JS function hoisting)
// You want these to be as visible as possible
ExpandableForm.propTypes = {  onSubmit: func.isRequired,  expanded: bool,  onExpand: func.isRequired }

// Destructure props like so, and use default arguments as a way of setting defaultProps
function ExpandableForm({ onExpand, expanded = false, children, onSubmit }) {
 const formStyle = expanded ?
     { height: 'auto' } : { height: 0 }
 return (    
   <form style={formStyle} onSubmit={onSubmit}>      {children}
     <button onClick={onExpand}>Expand</button>    </form>  ) } // Wrap the component instead of decorating it export default observer(ExpandableForm)

JSX 中的条件表达式

很可能你会做很多条件渲染。这是你想避免的:

不,三目嵌套不是一个好主意。

有一些库解决了这个问题(JSX-Control Statementments),但是为了引入另一个依赖库,我们使用复杂的条件表达式,解决了这个问题:

使用大括号包裹一个立即执行函数(IIFE),然后把你的 if 语句放在里面,返回你想要渲染的任何东西。
请注意,像这样的 IIFE 可能会导致一些性能消耗,但在大多数情况下,可读性更加重要。

更新:许多评论者建议将此逻辑提取到子组件,由这些子组件返回的不同 button。这是对的,尽可能地拆分组件。

另外,当你有布尔判断渲染元素时,不应该这样做:

{
  isTrue
   ? <p>True!</p>
   : <none/>
}

应该使用短路运算:

{
  isTrue && <p>True!</p>
}



部门招聘

高级Java开发工程师

工作职责:

1、负责58同城APP,58同镇等相关后端研发工作;

2、负责基础平台的架构设计,核心代码开发;

3、调研并掌握业内通用技术方案,引入项目迭代,提升研发效率;

职位要求:

1、3年以上Java互联网项目开发经验;

2、Java基础扎实,编码规范,程序具备较高的健壮性,熟悉常用设计模式;

3、对MVC框架、RPC框架、基础服务组件等有深入的研究;

4、掌握Linux环境下的网络编程、多线程编程,数据结构和算法能力良好;

5、对高并发高可用系统设计有深入的实践经验;

6、具有高度的责任心、勇于承担责任,能承受较强的工作压力;

7、积极主动,敢于接受挑战,有较强的团队合作精神;

高级前端研发工程师

工作职责:

1、负责58同城App前端产品研发;

2、负责58同城前端无线产品某一技术方向,人才培养;

3、前端研发所需类库、框架、脚手架搭建;

4、交互模式调研及创新(React,ReactNative);

职位要求:

1、计算机及相关专业本科以上学历;

2、3年以上前端开发经验,负责过复杂应用的前端设计和开发 ;

3、精通web前端技术(js/css/html),熟悉主流框架类库的设计实现、w3c标准,熟悉ES6/7优先;

4、熟悉前端模块化开发方式(commonjs/webpack …);

5、熟悉移动端开发、自适应布局和开发调试工具,熟悉hybrid app开发;

6、掌握一门后端语言(node/java/php...),对前后端合作模式有深入理解;

7、有良好的产品意识和团队合作意识,能够和产品、UI交互部门协作完成产品面向用户端的呈现;

8、有技术理想,致力于用技术去推动和改变前端研发;

9、熟悉Vue/React/ReactNative优先,有BAT等公司经验优先;

高级Android开发工程师

岗位描述:

1、负责58同城App的研发工作;

2、肩负平台化任务(插件框架,Walle,Hybrid,WubaRN) ;

3、维护和开发服务库,公共库的工作;

4、调研Android前端技术;

5、提升开发效率和应用性能;

职位要求:

1、2年以上的Android开发工作经验;

2、精通Java语言,精通Android Studio开发,了解Gradle编译;

3、精通常用算法、数据结构和架构设计;

4、了解Android性能限制及优化方案;

5、了解常用的开源工具:Volley,RxJava,Fresco等等;

6、了解git, maven等等工具;

7、有插件开发经验,Hybrid开发经验,ReactNative开发经验优先;

8、积极主动、喜欢挑战,有强烈的创业精神,能承受高强度的工作压力;

以上如有小伙伴感兴趣,请发送简历到:

[email protected]

Image

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK