QCon 全球软件开发大会(北京站)门票 9 折倒计时 4 天,点击立减 ¥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:222909

评论

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

区块链供应链金融落地解决方案,数据上链存储

WX13823153201

区块链供应链金融落地

不要再满世界找linux命令了,常用的我都给你整理了

北游学Java

Linux 程序人生 C/C++ 后端开发 linux命令

牛批!阿里P8Java架构师总结整理了一份Spring MVC详细教程。真香系列!

Java成神之路

Java 程序员 架构 面试 编程语言

蘑菇街Java大牛熬夜纯手写肛出的Spring AOP/IOC思维导图及源码笔记,赶紧收藏学习!

Java成神之路

Java 编程 程序员 架构 面试

微博和B站屏蔽马保国相关信息:自媒体蹭热度要适可而止

石头IT视角

架构师训练营第一期-第十周学习总结

卖猪肉的大叔

极客大学架构师训练营

shell脚本的使用该熟练起来了,你说呢?(篇二)

良知犹存

Shell

阿里内部“高并发通关秘籍”曝光,看完带给你独一无二的认知!

比伯

Java 编程 架构 面试 计算机

架构师训练营第一期-第十周课后作业

卖猪肉的大叔

极客大学架构师训练营

anyRTC 11月SDK更新

anyRTC开发者

flutter uni-app WebRTC RTC sdk

优秀商业可视化大屏(BI)设计演示

Marilyn

UI 商业智能

阿里技术专家熬夜一个月肛出内部“微服务学习笔记”,太完美了

小Q

Java 学习 编程 面试 微服务

Spring 源码学习 02:关于 Spring IoC 和 Bean 的概念

程序员小航

spring 源码 源码分析 ioc

现在Php、Java、Python横行霸道的市场,C++程序员们都在干什么呢?

ShenDu_Linux

c++ 程序员 编程语言 C语言 软件工程师

前嗅教你大数据:常见几种编码介绍

前嗅大数据

大数据 编码 编码指南 前嗅大数据

面试大厂被MyBatis源码问到“哑口无言”?这份《MyBatis源码学习笔记+面试真题》助你吊打面试官!

Java成神之路

Java 程序员 架构 面试 编程语言

今年最火的 Golang 云原生开源项目,可能就是它了!

孙健波

Kubernetes k8s OAM KubeVela CloudNative

5分钟学会6个阿里内部编程的方法

Java架构师迁哥

40 张图带你搞懂 TCP 和 UDP

云流

编程 程序员 前端 后端 网络

阿里P8Java大牛总结的MySQL最全整理(面试题+笔记+导图),面试大厂不再被MySql难倒!

Java成神之路

Java 程序员 架构 面试 编程语言

监控之美——监控系统选型分析及误区探讨

华章IT

运维 云原生 监控 Prometheus

《华为数据之道》读书笔记:第 8 章 打造“清洁数据”的质量综合管理能力

方志

数字化转型 数据质量管理

软件架构指南

信码由缰

软件架构

【经验分享】打破CMDB认知误区,掌握建设关键!

嘉为蓝鲸

运维 运维自动化 数据可视化 CMDB 配置信息

SpringBoot有多重要?面试用SpringBoot把面试官唬住了要30k都行!

Java成神之路

Java 程序员 架构 面试 编程语言

根治可扩展、高可用、高性能“神器”:SpringCloud+Nginx高并发编程手册

Java架构追梦

Java nginx 架构 面试 微服务

阿里云在应用扩缩容下遇到的挑战与选型思考

阿里巴巴云原生

阿里云 Kubernetes 容器 云原生

完了!这57道面试题(美团、BAT、携程),我咋一个都不会?

比伯

Java 程序员 架构 面试 计算机

二分发代码模板

小兵

苹果开始告别英特尔

罗燕珊

macOS Big Sur 芯片 苹果 MacBook 英特尔

1. 揭秘Spring类型转换 - 框架设计的基石

YourBatman

Spring Framework 类型转换 Converter

边缘计算隔离技术的挑战与实践

边缘计算隔离技术的挑战与实践

让React组件变得可响应-InfoQ