QCon全球软件开发大会让你与150+位一线国内外技术专家深度交流,购票立减 ¥880 了解详情
写点什么

让 React 组件变得可响应

2016 年 8 月 02 日

编者按: InfoQ 开设栏目“品味书香”,精选技术书籍的精彩章节,以及分享看完书留下的思考和收获,欢迎大家关注。本文节选自由Artemij Fedosejev 著,奇舞团译的《React 精髓》一书,介绍了如何规划React 应用程序并创建可组合的React 组件。

第四章 让React 组件变得可响应

现在你已经知道如何创建有状态和无状态的React 组件,我们可以尝试将React 组件组合在一起构建更复杂的用户界面。事实上,我们现在可以开始创建第1 章讨论过的Web 应用程序Snapterest 了。在这个过程中,我们将学习如何规划React 应用程序并创建可组合的React 组件。让我们开始吧。

1. 使用 React 解决问题

编写 Web 应用程序之前,要考虑 Web 应用要解决什么问题。尽早且尽可能清晰地定义问题是走向成功解决方案(即有用的 Web 应用)的最重要一步。如果在开发过程之初没有定义清楚问题,或者定义得不准确,那么此后你将不得不停下来,并重新思考该怎么做,甚至要扔掉已经完成的一些代码并重新写。这是非常低效的,作为一个专业的软件开发者,你和你的团队的时间都是非常宝贵的,因此,花点时间提前想清楚问题是非常有益处的。在本书开始的时候,我强调过使用 React 的好处之一是代码重用,这意味着你将在更短的时间内做更多的事。因此,在看 React 代码之前,让我们先讨论要解决的问题是什么,而且要记得用 React 来思考。

我们将创建的 Snapterest 是一个 Web 应用程序,它会实时接收来自 Snapkite 引擎服务器的推文,并将推文逐条显示给用户。虽然我们不知道 Snapterest 会在什么时候收到一条新推文,但是当新推文到达时,它至少应该显示 1.5 秒钟以便用户有足够的时间看到并点击它。点击推文会将它添加到一个现有的推文集合或者创建一个新的集合。最终用户能够将集合导出为一段 HTML 代码。

对于我们正在构建的应用,上述描述非常笼统。让我们把它分解成更小的任务,如下图:

  1. 实时地从 Snapkite 引擎服务器接收推文。
  2. 每条推文至少显示 1.5 秒。
  3. 通过用户的点击事件将推文添加到一个集合中。
  4. 显示集合中的推文。
  5. 为集合创建 HTML 代码并导出。
  6. 通过用户点击事件从集合中移除推文。

你可以确定哪些任务能使用 React 解决吗?记住 React 是一个用户界面库,因此,任何与用户界面或用户界面交互相关的任务都可以使用 React 解决。在前面的列表中,除了第一个任务外,其他的任务 React 都能胜任,因为第一个任务描述的是数据获取,与用户界面毫无关系。任务 1 将会使用其他库来解决,我们将在下一章讨论。任务 2 和任务 4 描述的内容是需要被显示的,React 组件是最合适的选择。任务 3 和任务 6 描述的是用户事件,如我们在第 3 章所介绍的,用户事件处理可以被很好地封装在 React 组件中。任务 5 怎样使用 React 来解决呢?第 2 章我们讨论过ReactDOMServer.renderToStaticMarkup()方法能将 React 元素渲染成静态的 HTML 标记字符串,这正是我们解决任务 5 所需的方案。

现在我们已经为每一个任务确定了潜在的解决方案,让我们思考一下我们将要怎么把它们结合在一起来创建一个功能全面的 Web 应用程序。

有两种方法来创建可组合的 React 应用:

  • 先构建单个的 React 组件,然后将它们组合起来形成更高层级的 React 组件,自底向上来构建层级。
  • 从最顶级的 React 元素开始,然后实现它的子组件,自顶向下来构建层级。

从观察和理解应用架构的角度来看,第二种策略更有优势。我认为在考虑各个部分的功能如何实现之前,先了解所有组件如何组合在一起更重要。

2. 规划 React 应用程序

规划 React 应用时应遵循下面两条简单的原则:

  • 每个 React 组件应该代表一个用户界面元素。它应该封装最小的可复用元素。
  • 多个 React 组件应该组成一个独立的 React 组件。最终,整个用户页面应该封装成一个 React 组件。

参见下图,先从最上层的 React 组件 Application 开始。它将封装我们的整个 React 应用程序,它有两个子组件:Stream 和 Collection。Stream 组件将负责连接到一个消息流,接收和显示最新的消息。Stream 组件有两个子组件:StreamTweet 和 Header。StreamTweet 组件将负责显示最新的消息,它由 Header 和 Tweet 组合而成。Header 组件将会渲染头部,它没有子组件。Tweet 组件会渲染来自推文的一张图片。注意我们已经可以复用 Header 组件两次了。

我们的 React 组件的层次图

Collection 组件负责显示收集控件和推文列表。它有两个子组件:CollectionControls 和 tweetlist。前者又有两个子组件:Collection RenameForm 组件将渲染一个表单,用来重命名集合;CollectionExportForm 组件将渲染一个表单,用来将集合导出到一个叫作 CodePen 的服务,这是一个 HTML、CSS 和 JavaScript 的演示网站,可以在 http://codepen.io 上了解更多关于 Codepen 的信息。你可能已经注意到,我们将在 CollectionFenameForm 和 CollectionControls 组件中复用 Header 和 Button 组件。TweetList 组件将渲染一个推文列表。每一条推文将被渲染成一个 Tweet 组件。在 Collection 组件中,我们将再次复用 Header 组件。事实上,我们总共要复用 5 次 Header 组件。这对我们来说能省很大事。正如我们在前一章中讨论的,我们应该尽可能保持更多组件是无状态的。因此,总共 11 个组件中只有下面 5 个组件存储状态:

  • Application
  • CollectionControls
  • CollectionRenameForm
  • Stream
  • StreamTweet

有了规划之后,我们开始实现吧。

3. 创建一个 React 组件容器

让我们首先编辑应用程序的 JavaScript 主文件,使用下面的代码片段替换~/snapterest/source/app.js 文件内容:

复制代码
var React = require('react');
var ReactDOM = require('react-dom');
var Application = require('./components/Application.react');
ReactDOM.render(<Application />,
document.getElementById('react- application'));

这个文件仅有四行代码,实现的是:将 document.getElementById(‘react- application’) 作为组件的部署目标,并将 Application 组件渲染到 DOM 中。Web 应用程序的整个用户界面都将被封装在这个组件中。

接下来,切换到~/snapterest/source/components/ 目录并创建 Applica tion.react.js 文件。我们约定所有 React 组件的文件名都以 react.js 结尾,这样我们就可以很轻意地分辨 React 与非 React 文件。

让我们看一下 Application.react.js 文件的内容:

复制代码
var React = require('react');
var Stream = require('./Stream.react');
var Collection = require('./Collection.react');
var Application = React.createClass({
getInitialState: function() {
return {
collectionTweets: {}
};
},
addTweetToCollection: function(tweet) {
var collectionTweets = this.state.collectionTweets;
collectionTweets[tweet.id] = tweet;
this.setState({
collectionTweets: collectionTweets
});
},
removeTweetFromCollection: function(tweet) {
var collectionTweets = this.state.collectionTweets;
delete collectionTweets[tweet.id];
this.setState({
collectionTweets: collectionTweets
});
},
removeAllTweetsFromCollection: function() {
var collectionTweets = this.state.collectionTweets;
delete collectionTweets[tweet.id];
this.setState({
collectionTweets: {}
});
},
removeAllTweetsFromCollection: function () {
this.setState({
collectionTweets: {}
});
},
render: function() {
return (
<div className="container-fluid">
<div className="row">
<div className="col-md-4 text-center">
<Stream onAddTweetToCollection={this.addTweetToCollection} />
</div>
<div className="col-md-8">
<Collection
tweets={this.state.collectionTweets}
onRemoveTweetFromCollection={this.
removeTweetFromCollection}
onRemoveAllTweetsFromCollection={this.
removeAllTweetsFromCollection}/>
</div>
</div>
</div>
);
}
});
module.exports = Application;

这个组件的代码比 app.js 多了很多,但是这些代码可以很容易地分为三个逻辑部分:

  • 引入依赖模块
  • 定义 React 组件
  • 作为模块导出这个 React 组件

大多数 React 组件中都可以看到这样的逻辑分割,因为包装成 CommonJS 模块才能使用 Browserify 引入它们。事实上,这个源文件的第一部分和第三部分的写法都是 CommonJS 规定的,与 React 无关。使用这种模块规范的目的是将应用程序分解成模块以便复用。因为 React 组件和 CommonJS 模块都可以封装代码并使代码更灵活,所以它们在一起自然可以很好地工作。将最终的用户界面逻辑封装在一个 CommonJS 模块形式的 React 组件中,其他模块就可以复用这个被封装好的 React 组件了。

Application.react.js 文件的引入逻辑使用 require() 函数引入了依赖模块:

复制代码
var React = require('react');
var Stream = require('./Stream.react');
var Collection = require('./Collection.react');

这里 Application 组件引入了下面两个子组件:

  • Stream 组件将在用户界面中渲染信息流部分。
  • Collection 组件将在用户页面中渲染集合部分。

我们也需要引入 React 库,但这部分代码都是按照 CommonJS 模块规范编写的,与 React 本身无关。

Application.react.js 文件的第二部分逻辑创建带有以下方法的 ReactApp licaton 组件:

  • getInitialState()
  • addTweetToCollection()
  • removeTweetFromCollection()
  • removeAllTweetsFromCollection()
  • render()

只有 getInitialState() 和 render() 方法是 React API,其他方法都是这个组件封装的应用程序逻辑的一部分。讨论完这个组件的 render() 方法会渲染什么内容之后,我们再仔细分析每个逻辑方法:

复制代码
render: function () {
return (
<div className="container-fluid">
<div className="row">
<div className="col-md-4 text-center">
<Stream onAddTweetToCollection={this.addTweetToCollection} />
</div>
<div className="col-md-8">
<Collection
tweets={this.state.collectionTweets}
onRemoveTweetFromCollection={this.
removeTweetFromCollection}
onRemoveAllTweetsFromCollection={this.
removeAllTweetsFromCollection} />
</div>
</div>
</div>
);
}

这段代码使用 Bootstrap 框架定义了网页布局。如果你不熟悉 Bootstrap,我强烈推荐你访问 http://getbootstrap.com 上的文档。掌握了这个框架你就能用最快的速度和最简单的方法搭建用户界面原型。不过即使你不知道 Bootstrap,也不影响理解后面的内容。我们将网页划分为两列:一个小的和一个大的。小的包含 Stream 组件,大的包含 Collection 组件。可以想象我们的网页被划分成两个不等的部分,它们都包含 React 组件。

我们这样使用 Stream 组件:

<Stream onAddTweetToCollection={this.addTweetToCollection} />Stream 组件有一个 onAddTweetToCollection 属性,Application 组件将自己的 addTweetToCollection() 函数作为这个属性的值。addTweetTocollection() 函数会添加一条推文到集合中。这是 Applicaton 组件中的一个自定义方法,我们可以用 this 关键字来引用它。

让我们看一下 addTweetToCollection() 做了什么:

复制代码
addTweetToCollection: function (tweet) {
var collectionTweets = this.state.collectionTweets;
collectionTweets[tweet.id] = tweet;
this.setState({
collectionTweets: collectionTweets
});
}
<div class="md-section-divider"></div>

这个函数引用存储在当前 state 中的 CollectionTweets,添加一条新推文到 CollectonTweets 对象,并通过调用 setState() 函数来更新 state。在 Stream 组件中,当 addTweetToCollection() 函数被调用时,一条新推文会作为参数被传入。这是一个子组件更新其父组件 state 的例子。

这是 React 的一个重要机制,它的工作过程如下。

  1. 父组件传递一个回调函数作为子组件的属性。子组件可以通过 this.props 变量访问这个回调函数。
  2. 每当子组件想要更新父组件的 state 时,它就会调用这个回调函数并传递所有必要的数据到父组件的新状态中。
  3. 父组件更新它的 state,而且 state 更新会触发 render() 函数重新渲染所有必要的子组件。

这就是 React 中父组件与子组件的交互机制。这个机制允许子组件将应用程序状态管理委托到它的父组件,子组件只需要关心如何渲染自己就行了。了解了这个机制之后,我们还将多次使用它,因为大部分 React 组件要保持无状态。应该只有少量的父组件负责存储和管理应用程序的 state。这个最佳实践允许我们按照以下两个不同的关注点来有序地组织 React 组件:

管理应用程序的 state 和渲染。

只关注渲染并且将应用程序的 state 管理委托到父组件上。

Application 组件的第二个子组件 Collection 如下:

复制代码
<Collection
tweets={this.state.collectionTweets}
onRemoveTweetFromCollection={this.removeTweetFromCollection}
onRemoveAllTweetsFromCollection={this.removeAllTweetsFromCollection} />
<div class="md-section-divider"></div>

这个组件有如下一些属性。

  • tweets:引用当前推文集合。
  • onRemoveTweetFromCollection:这个函数从集合中删除特定的推文
  • onRemoveAllTweetFromCollection:这个函数从集合中删除所有的推文。

Collection 组件的属性仅仅关注下面两点:

  • 如何访问应用程序的 state。
  • 如何改变应用程序的 state。

显然,onRemoveTweetFromCollection 和 onRemoveAllTweetsFromCollection 函数允许 Collection 组件改变 Application 组件的 state。另一方面,tweets 属性把 Application 组件的 state 传递给 Collection 组件,使 Collection 组件获得访问 state 的只读权限。

你能觉察到在 Application 和 Collection 组件之间的数据的单向流动吗?以下是它的工作过程:

  1. 使用 Application 组件的 getInitalState() 方法初始化 collection Tweets 数据。
  2. collectionTweets 数据作为 tweets 属性传递给 Collection 组件。
  3. Collection 组件调用 removeTweetFromCollection 和 removeAllTweet FromCollection,更新 Application 中的 collectionTweets 数据,然后再次开始循环。

注意,Collection 组件不能直接改变 Application 组件的 state。Collection 组件有通过 this.props 对象访问 state 的只读权限,并仅可以通过调用父组件传递的回调函数来更新父组件的 state。在 Collection 组件中,这些回调函数是 this.props.onRemoveTweetFromCollection 和 this.props.onRemoveAllTweetFrom Collection。

在 React 组件层次中,这种数据流动的简单思维模型有助于增加组件的数量,而不增加用户页面的复杂性。比如,它可以有 10 个层级的 React 组件嵌套,如下图所示。

在这个层次结构中,如果组件 G 要改变根组件 A 的 state,其所用的方法与组件 B、组件 F 或者其他任何组件使用的方法完全相同。然而,在 React 中,你不应该将数据直接从组件 A 传递到组件 G。相反,你首先可以把它传递给组件 B,然后给组件 C,然后给组件 D,依此类推,直至组件 G。组件 B 到组件 F 必须携带一些 transit 属性,这些属性实际上只对组件 G 有用。这看起来可能是在浪费时间,但是这个设计使我们更容易调试应用程序,并可以推理出它是如何工作的。想优化应用程序的架构总是有办法的。Flux 就是一种优化方案,本书后面会讨论它。

最后,再看一下改变 Application 的 state 的两个方法:

复制代码
removeTweetFromCollection: function (tweet) {
var collectionTweets = this.state.collectionTweets;
delete collectionTweets[tweet.id];
this.setState({
collectionTweets: collectionTweets
});
}
<div class="md-section-divider"></div>

removeTweetFromCollection() 方法从存储在 Application 组件的推文集合中移除一条推文,它需要从组件的 state 中得到当前的 collectionTweet 对象,然后根据给定的 ID 从 state 对象中删除一条推文,并使用一个新的 collectionTweets 对象来更新组件的 state。

此外 removeAllTweetsFromCollection() 方法会从组件 state 中移除所有推文:

复制代码
removeAllTweetsFromCollection: function () {
this.setState({
collectionTweets: {}
});
}

这些方法都是在子组件 Collection 中调用的,因为该组件没有其他方法可以改变 Application 组件的 state。

4. 小结

在这一章中,我们学习了如何使用 React 解决问题。我们首先把问题分解成一些小问题,并讨论了如何使用 React 解决它们。然后,创建一些需要实现的 React 组件。最后,我们创建了第一个可组合的 React 组件,并学习了如何让父组件与它的子组件交互。在下一章,我们将实现我们的子组件并学习 React 生命周期相关的方法。

书籍介绍

《React 精髓》面向初中级前端开发者,从头到尾、由浅入深地介绍了使用 React 实现组件化 Web 应用的完整流程。作者从 React 元素、React 组件等基本的概念讲起,循序渐进地讨论了组件状态和生命周期,为开发完整的 React 应用打下了基础。与第三方 JavaScript 框架集成,以及对 React 组件进行单元测试,都是开发 React 应用的重要内容,《React 精髓》也有详细讲解。最后,为进一步提升 React 应用的灵活性,作者还以实例展示了如何引入 Flux 架构,让读者的开发技能更上一层楼。

2016 年 8 月 02 日 18:222903

评论

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

SumSwap在市场上的强大突破是否会成为DEX领域最大的黑马?

币圈资讯

MySQL 事务隔离

Sakura

四月日更

一种高效的统一前端团队代码规范的落地方案

benyasin

代码质量 eslint prettier 统一代码规范 代码格式化

NA(Nirvana)公链“为应用而生” NAC公链领跑公链新格局!

区块链第一资讯

【LeetCode】子集二Java题解

HQ数字卡

算法 LeetCode 4月日更

华为帐号服务学习笔记(三):10分钟完成Authorization Code模式客户端Demo开发

Coding狙击

android HMS

线上服务 CPU 100% ?一键定位 so easy!

Java小咖秀

性能 cpu 服务器 负载 紧急问题

Linux rmdir 命令

一个大红包

linux命令 4月日更

装双系统?不需要!教你在iMac上流畅使用Windows

懒得勤快

Mac 虚拟机 苹果 crossover

OKR实践中的痛点(5):战略缺失怎么玩OKR?

大叔杨

团队管理 OKR 敏捷 敏捷绩效

亿网嘉元是做什么的?

飞亚科技

systemctl的使用

高彪

linux运维 【4 月日更】

一文带你剖析LiteOS互斥锁Mutex源代码

华为云开发者社区

mutex LiteOS 互斥锁 互斥锁结构体

派出所重点人员管控系统开发,建设智慧警务

13828808769

智慧组工

你的数仓函数结果不稳定,可能是属性指定错了

华为云开发者社区

函数 GaussDB(DWS) 函数属性 函数下推 易失性级别

数据分析与数据增长核心逻辑杂谈

小飞象@木木自由

数据分析

大意!6行代码,“报废”5片单片机!

不脱发的程序猿

程序人生 嵌入式软件 单片机 四月日更 国产MCU

企业都在说敏捷开发!你真的了解敏捷开发嘛

攻城狮Chova

软件开发流程 4月日更 敏捷精益

Cloudreve 自建云盘实践,我说了没人能限得了我的容量和速度!

小傅哥

Java 小傅哥 Cloudreve 自建云盘

划重点丨详解Java流程控制语句知识点

华为云开发者社区

Java 流程控制语句

2D+1D | vivo官网Web 3D应用开发与实战

vivo互联网技术

WebGL web前端 3D数据可视化 Draco 3D

创建索引,这些知识应该了解

Simon

MySQL 索引

Dubbo 学习笔记(三) Spring Boot 整合 Dubbo(官方版)

U+2647

Spring Boot dubbo 四月日更

将本地maven仓库的数据恢复到Nexus仓库

白粥

工作笔记

云数据库时代的新思考,这位90后大咖想邀你聊聊

华为云开发者社区

数据库 开源 opengauss GaussDB 华为云数据库

公有云成本节省神器!京东云共享带宽包正式上线

京东科技开发者

公有云 带宽

智慧公安情报综合研判平台开发,助推公安信息化发展

13828808769

智慧城市

区块链电子合同技术方案,区块链电子合同存证

13828808769

区块链 区块链+

云小课 | 不了解EIP带宽计费规则?看这里!

华为云开发者社区

带宽 弹性公网IP 带宽变更 计费模式

css网页布局小结

Darren

CSS

进来看看是不是你想要的效果,Android吸顶效果,并有着ViewPager左右切换

第三女神程忆难

Java android kotlin 安卓 移动开发

让React组件变得可响应-InfoQ