前端未来的主流技术方向有哪些?腾讯、京东、同城旅行等大厂都是怎么布局的?戳此了解 了解详情
写点什么

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:113627
用户头像

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

关注

评论

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

2020 全球 JS 调查报告新鲜出炉

蓝色的秋风

typescript Vue 前端 React React Native

谈谈SpringFramework与IoC依赖查找

Java架构师迁哥

Python 循环语句

赵开忠

28天写作

关于事务、redolog 写入的两个问题分析

程序员架构进阶

MySQL innodb 事务 28天写作

JVM故障诊断和处理工具

Silently9527

Java JVM jvm调优

面试官:你说一下对Java中的volatile的理解吧

云流

Java 编程 面试 多线程

CSS14 - 元素可见性

桃夭十一里

html/css

CSS15 - 界面样式&垂直对齐

桃夭十一里

html/css

大小公司产品经理岗位对比

Kr

大厂面试题:你知道JUC中的Semaphore、CyclicBarrier、CountDownLatch吗

Java鱼仔

Java 面试 并发 JUC

「大厂攻略」打开线程 | 进程 | 协程的大门

我是程序员小贱

面试 操作系统

京东RPA:以企业数字化转型为驱动的机器人流程自动化解决方案专家

京东科技开发者

RPA 企业信息化 数字化运维

MySQL查询——连接查询

程序员的时光

程序员 28天写作

甲方日常 85

句子

工作 随笔杂谈 日常

1月|日更挑战活动说明

InfoQ写作平台官方

活动专区 七日更

大厂面试官竟然这么爱问Kafka,一连八个Kafka问题把我问蒙了

五分钟学大数据

大数据 kafka

如何使用maven 轻松重构项目

田维常

maven

网络故障排除工具,你用过哪些?

精选算法面试-数组II

李孟

面试 算法 数组 28天写作

AES/CBC/PKCS5Padding到底是什么

kof11321

加密解密

我们设计的是微服务还是小单体应用

xcbeyond

微服务 方法论 DDD 28天写作

智能building之二 智能单体建筑

张老蔫

28天写作

Mysql报表下载为什么出现了重复的数据?

三石

MySQL性能优化 28天写作 mysql重复数据

夜莺二次开发指南系列

qinyening

开源 监控 滴滴夜莺 夜莺监控 运维平台

TCP波场拼系统开发|TCP波场拼软件APP开发

开發I852946OIIO

系统开发

从一场营地教育直播,看懂众盟“私域流量营销”的底层逻辑

脑极体

我给职场新人提个建议

石云升

创业 28天写作 职场新人

28 天带你玩转 Kubernetes-- 第七天(玩转 Dockerfile)

Java全栈封神

Docker Kubernetes k8s Dockerfile 28天写作

重学JS | 异步编程 async/await

梁龙先森

前端 编程语言 28天写作

创业失败启示录|校园微生活之快叫外卖

青城

28天写作 创业失败启示录 青城

最近很火的京东、天猫超市飞天茅台抢购是怎么回事,从原理流程给你们分析一波

谙忆

技术为帆,纵横四海- Lazada技术东南亚探索和成长之旅

技术为帆,纵横四海- Lazada技术东南亚探索和成长之旅

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