你在使用哪种编程语言?快来投票,亲手选出你心目中的编程语言之王 了解详情
写点什么

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

2017 年 3 月 01 日

React 起源于 Facebook 的内部项目,因为该公司对市场上所有 JavaScript MVC 框架,都不满意,就决定自己写一套,用来架设 Instagram 的网站。做出来以后,发现这套东西很好用,就在 2013 年 5 月开源了。

由于 React 的设计思想极其独特,属于革命性创新,性能出众,代码逻辑却非常简单。所以,越来越多的人开始关注和使用,认为它可能是将来 Web 开发的主流工具。

来自 MuseFind 的 Scott Domes 日前写了一篇文章,阐述了他们编写React 组件的最佳实践。Scott Domes 是MuseFind 的前端移动开发工程师。

经作者授权,InfoQ 翻译并分享本文。以下是正文:


当我第一次开始写React 代码时,我记得看到过许多不同的编写组件的方法,各教程之间有很大的不同。虽然自那时以来框架已经相当成熟,但似乎还没有一个确定“正确”编写的方式。

在过去一年里,在 MuseFind ,我们的团队编写了很多 React 组件。我们已经逐渐完善了方法,直到我们满意为止。

本指南代表我们建议的最佳做法。我们认为本文对新手和老手都有所帮助。

阅读本文之前,读者需要注意以下几点:

  • 我们使用 ES6 和 ES7 语法。
  • 如果你不确定展示型组件和容器组件之间的区别,我们建议你先阅读这篇文章
  • 如果您有任何建议问题或反馈,请在原文的评论区中告诉我们。

基于类的组件

基于类的组件是有状态的,或许还包含方法。我们尽可能少地使用它们,但它们也有自己的位置。

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

导入 CSS

复制代码
import React, {Component} from 'react'
import {observer} from 'mobx-react'
import ExpandableForm from './ExpandableForm'
import './styles/ProfileContainer.css'

理论上,我喜欢 CSS in JavaScript 。但它仍然是一个新的想法,还没有出现一个成熟的解决方案。在此之前,我们将一个 CSS 文件导入到每个组件。

我们还通过换行将依赖导入与本地导入分开。

初始化状态

复制代码
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 }

如果你使用 ES6(ES7 不适用),在构造函数中初始化状态。否则,使用专用于 ES7 的方法。更多信息在这篇文章

我们还要确保将我们的类导出为默认类。

propTypes 和 defaultProps

复制代码
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'
}

propTypes 和 defaultProps 是静态属性,在组件代码中声明的优先级尽可能高。由于它们作为文档,因此它们应该对其他读取文件的开发者可见。

所有的组件应该有 propTypes。

方法

复制代码
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 })
}

使用类组件,当将方法传递给子组件时,必须确保它们在调用时具有正确的 this。通常通过传递 this.handleSubmit.bind(this) 到子组件来实现。

我们认为这种方法更简洁也更容易,通过 ES6 的箭头函数自动保持正确的上下文。

解构 props

复制代码
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>
)
}
}

具有多个 props 的组件,每个 props 应该占据单独一个行,如上所示。

装饰器

复制代码
@observer
export default class ProfileContainer extends Component {

如果你使用像 mobx 这样的东西,你可以将类组件装饰成这样:这与将组件传递到函数相同。

装饰器通过灵活、可读的方式来修改组件功能。我们广泛地使用装饰器,配合mobx 和我们自己的 mobx-models 库。

如果您不想使用装饰器,请执行以下操作:

复制代码
class ProfileContainer extends Component {
// Component code
}
export default observer(ProfileContainer)

闭包

避免传递新的闭包到子组件,像这样:

复制代码
<input
type="text"
value={model.name}
// onChange={(e) => { model.name = e.target.value }}
// ^ Not this. Use the below:
onChange={this.handleChange}
placeholder="Your Name"/>

这就是为什么每次父组件渲染时,创建一个新的函数并传递给输入的原因。

如果输入是 React 组件,这将自动触发它重新渲染,而不管它的其他 props 是否实际改变。

调和算法(Reconciliation)是 React 最耗时的部分。不要让它比所需更难!此外,传递类的方法更容易阅读、调试和更改。

这是我们的完整组件:

复制代码
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>
)
}
}

函数组件

这些组件没有状态、方法。它们是纯粹的,简单的。因此要尽可能经常使用它们。

propTypes

复制代码
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

这里,我们在组件声明前分配 propTypes,因此它们立即可见。在组件声明下面,我们正确地分配它们。

解构 Props 和 defaultProps

复制代码
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>
)
}

我们的组件是一个函数,它的 Props 作为其参数。我们可以这样扩展:

复制代码
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>
)
}

注意,我们也可以使用默认参数作为 defaultProps 以高度可读的方式。如果展开没有定义的话,我们将其设置为 false。(这是一个有点强迫的例子,因为它是一个布尔,但非常有用,以避免“无法读取未定义”错误与对象)。

避免使用以下 ES6 语法:

复制代码
const ExpandableForm = ({ onExpand, expanded, children }) => {

看起来很现代,但此处的函数实际上是未命名的。

如果你的 Babel 设置正确,这个名字的缺失不会成为一个问题:但如果不是,任何错误将 < > 中显示,对调试而言,是一个非常严重的问题。

未命名的函数也可能导致 Jest(一个 React 测试库)的问题。由于潜在的难以理解的 bug,以及并没有什么真正的好处,我们建议使用 function 而不是 const。

包装

因为你不能使用装饰器和功能组件,你只需将函数作为参数传递给它:

复制代码
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)

这是我们的完整组件:

JSX 条件

很可能你要做很多条件渲染。这里是你想避免的地方:

(点击放大图像)

这是我在MuseFind 早期写的实际代码,饶恕我吧。

不,嵌套的三元运算并不是一个好主意。

有一些库解决了这个问题( JSX 控制语句),但是,而不是引入另一个依赖,我们解决了这种应对复杂条件的方法:

(点击放大图像)

以上所示是重构版本。

使用花括号括起一个 IIFE ,然后把你的 if 语句置于里面,返回任何你想要的渲染。请注意,这样的 IIFE 可能会导致性能下降,但在大多数情况下,它不会严重到以致失去可读性。

此外,当你只想渲染一个条件上的元素,而不是这样做:

复制代码
{
isTrue
? <p>True!</p>
: <none/>
}

使用 short-circuit 赋值:

复制代码
{
isTrue &&
<p>True!</p>
}

感谢王下邀月熊对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们。

2017 年 3 月 01 日 16:113623
用户头像

发布了 338 篇内容, 共 128.9 次阅读, 收获喜欢 852 次。

关注

评论

发布
暂无评论
发现更多内容

阿里又出“宝妈级”之作,这份SpringBoot应用到源码手册,全是精华

架构大师

Java spring

血汗全在这了!阿里P8珍藏笔记,SpringCloud精通日记

Java架构师之路

Java spring 微服务 Spring Cloud 阿里

直呼内行!阿里大佬离职带出内网专属“高并发系统设计”学习笔记

Java架构师之路

Java 学习笔记 高并发 阿里 笔记

什么?曾经和我一起打王者的兄弟,现在进了Alibaba,分享一波面经

Java架构师之路

Java 面试 阿里 面经 Alibaba

华为大佬万字长文总结,梳理的Java入门所有基础知识点,快收藏

java专业爱好者

Java

最新出炉!不得不看的208问!Java中高级面试题

Java架构师之路

Java 架构 面试题 面试总结 中高级

模块一作业

王小森

能让我工资腾讯云高工熬夜手写‘Java微服务学习笔记‘。涨3k?

马小轩

Java 架构 腾讯 面试 程序猿

同事闭关修炼1000小时!狂刷Alibaba面试指南(恒山版),最终直接斩获十七个offer

Java架构师之路

Java 架构 面试 高并发 阿里

一个成功的 Git 分支模型如何构建?

白亦杨

「Adobe国际认证」用视觉品牌吸引你的用户

Adobe国际认证

重磅!不容错过的阿里内部微服务速成手册也太赞了(2021版)

Java 白

Java

劲爆!97道大厂Java核心面试题出炉,来试试看你会几道题?

Java领路人

Java 编程 程序员 面试 架构师

Java小菜鸟如何从小厂逆袭,坐上美团L8技术专家,涨薪50%

喝酸奶不舔盖

TcaplusDB | 祖国富强统一,未来一路同行

tcaplus

数据库 TcaplusDB

一篇让我膜拜的文章,阿里大佬手写Docker学习笔记也就是让我五体投地的水平罢了

马小轩

Java 架构 面试 阿里 程序猿

行业痛点今何在?产业安全专家共话云安全

腾讯安全云鼎实验室

云计算 云安全

5W1H聊开源之Why——为什么要参与开源?

禅道项目管理

开源 项目

官宣!ElasticJob 3.0.0 版本正式发布

SphereEx

先到先得!“金九银十” 迎战大厂通过率达95%的Java核心面试知识

Java架构师之路

Java 面试 面经 一线大厂 核心面试

2021面试题最新出炉啦!2021金三银四Java中高级面试208问!

马小轩

Java 架构 面试 笔记 程序猿

先到先得!367W字!京东商城Java架构师设计的亿级高并发秒杀笔记

Java架构师之路

Java 高并发 架构师 京东 笔记

张某闭关修炼10000小时后,字节三面成功斩获offer

Java架构师之路

Java 架构 字节 面试真题

谁说专科不配进大厂,2年进入网易,5年跳槽腾讯,论我是怎么快速晋升的?

喝酸奶不舔盖

太硬核了!GitHub上堪称完美的神仙并发编程笔记,请收下我的下巴

程序员改bug

Java 架构 并发编程 编程语言

阿里大牛私藏MyBatis笔记遭泄露,入门到精通,直接学废了

Java架构师之路

Java mybatis 阿里 笔记 源码分分析

1小时点击量破千万!阿里巴巴首发:Java核心框架指导手册

Java架构

拒绝内卷,腾讯3面+阿里6面一次过,谈谈我的大厂面经

喝酸奶不舔盖

发布短短两小时!霸榜GitHub!Spring Boot实战文档

Java架构师之路

Java GitHub spring springboot 实战文档

数据备份 定期整理与备份手机中的重要资料

万里无云万里天

信息安全 数据备份 手机管理

“上班摸鱼”期间向阿里猛投简历,被刷两次后,终成“菜鸟”P6

Java架构师之路

Java 面试 面试题 阿里 一线大厂

云原生场景下企业API 网关选型及落地实践

云原生场景下企业API 网关选型及落地实践

MuseFind:编写React组件的最佳实践-InfoQ