React 入门指南(全)
原文:Introduction to React
协议:CC BY-NC-SA 4.0
一、什么是 React?
Electronic supplementary material The online version of this chapter (doi:10.1007/978-1-4842-1245-5_1) contains supplementary material, which is available to authorized users.
看到一个不可救药的不墨守成规者的固执受到热烈欢迎,我确实感到非常高兴。—阿尔伯特·爱因斯坦
您可能对这本书有一定程度的 JavaScript 知识。您也很有可能知道 React 是什么。本章强调了 React 作为一个框架的关键方面,解释了它所解决的问题,并描述了如何利用本书中包含的特性和其他信息来改进您的 web 开发实践,并使用 React 创建复杂但可维护的用户界面。
定义 React
React 是一个 JavaScript 框架。React 最初是由脸书的工程师创建的,旨在解决开发复杂的用户界面时遇到的挑战,这些界面的数据集会随着时间的推移而变化。这不是一个微不足道的任务,不仅要可维护,而且要可扩展到脸书的规模。React 实际上诞生于脸书的广告公司,他们一直在利用传统的客户端模型-视图-控制器方法。诸如此类的应用通常由双向数据绑定和呈现模板组成。React 通过在 web 开发方面取得一些大胆的进步,改变了创建这些应用的方式。当 React 在 2013 年发布时,web 开发社区对 React 所做的事情既感兴趣,又似乎感到厌恶。
正如您将在本书中发现的,React 挑战了已经成为 JavaScript 框架最佳实践事实上的标准的惯例。React 通过引入许多新的范例和改变创建可伸缩和可维护的 JavaScript 应用和用户界面的现状来做到这一点。随着前端开发心态的转变,React 提供了一组丰富的功能,使得许多技能水平的开发人员都可以编写单页应用或用户界面——从刚接触 JavaScript 的人到经验丰富的 web 老手。当你阅读这本书时,你会看到这些特性——比如虚拟 DOM、JSX 和 Flux 概念——并发现它们如何被用来创建复杂的用户界面。
简而言之,您还将看到脸书如何用 React Native 不断挑战开发领域。React Native 是一个新的开源库,利用与 React 的 JavaScript 库相同的原理来创建原生用户界面。通过创建原生 UI 库,React 推动了其“一次学习,随处编写”的价值主张。这种范式转变适用于能够利用 React 的核心概念来制作可维护的接口。到目前为止,您可能认为在开发方面没有 React 做不到的事情。事实并非如此,为了进一步理解 React 是什么,您需要理解 React 不是什么,这将在本章的后面学习。首先,您将了解导致 React 创建的潜在问题,以及 React 如何解决这些问题。
为什么要 React?
如前所述,对于一般的 web 开发来说,React 是一个不同的概念。这是对普遍接受的工作流程和最佳实践的转变。为什么脸书回避了这些趋势,转而支持创建一个全新的 web 开发过程的愿景?挑战公认的最佳实践是不是非常漫不经心,或者创建 React 是否有一个通用的商业案例?
如果你看看 React 背后的推理,你会发现它是为了满足脸书面临的一系列特定技术挑战的特定需求而创建的。这些挑战过去和现在都不是脸书所独有的,但脸书所做的是用自己解决问题的方法直接应对这些挑战。您可以把这看作是 Eric Raymond 在他的书《Unix 编程的艺术》中总结的 Unix 哲学的类比。在这本书里,Raymond 写了模块化规则,它是这样写的:
The only way to write complex software is to reduce its global complexity-build it with simple parts with well-defined interfaces-so that most of the problems are local, and you can hope to upgrade a part without destroying the whole.
这正是 React 在解决复杂用户界面问题时采用的方法。脸书在开发 React 时,并没有创建一个完整的模型-视图-控制器架构来取代现有的框架。没有必要重新发明那个特殊的轮子,增加创建大规模用户界面问题的复杂性。React 的创建是为了解决一个特殊的问题。
React 是为了处理在用户界面中显示数据而构建的。您可能认为在用户界面中显示数据是一个已经解决的问题,您这样想是正确的。不同之处在于,React 是为大规模用户界面(脸书和 Instagram 规模的界面)服务的,其数据会随着时间的推移而变化。这种接口可以用 React 之外的工具创建和解决。事实上,脸书在创建 React 之前肯定已经解决了这些问题。但脸书确实创造了 React,因为它有有效的推理,并发现 React 可以用来解决构建复杂用户界面时遇到的特定问题。
React 解决什么问题?
React 并没有着手解决你在用户界面设计和前端开发中会遇到的每一个问题。React 解决一组特定的问题,通常是一个问题。正如脸书和 Instagram 所说,React 用随时间变化的数据构建大规模用户界面。
数据随时间变化的大规模用户界面可能是许多 web 开发人员在他们自己的工作或爱好编码经历中可能会涉及到的东西。在现代 web 开发世界中,您通常会将用户界面的大部分职责交给浏览器和 HTML、CSS 和 JavaScript。这些类型的应用通常被称为单页面应用,其中对服务器的常见请求/响应仅限于展示浏览器的强大功能。这是自然的;既然大多数浏览器都能够进行复杂的布局和交互,为什么不这样做呢?
当您的周末项目代码不再可维护时,问题就出现了。您必须“附加”额外的代码片段来使数据正确绑定。有时,您必须重新构建应用,因为次要的业务需求无意中破坏了用户开始一项任务后界面呈现一些交互的方式。所有这些导致用户界面脆弱,高度互联,不容易维护。这些都是反动派试图解决的问题。
以客户端的模型-视图-控制器架构为例,它在前面提到的模板中有双向数据绑定。这个应用必须包含监听模型的视图,然后视图根据用户交互或模型变化独立地更新它们的表示。在一个基本的应用中,这并不是一个明显的性能瓶颈,更重要的是,对于开发人员的工作效率而言。随着新的模型和视图添加到应用中,这个应用的规模将不可避免地增长。这些都是通过微妙而复杂的代码连接在一起的,这些代码可以指导每个视图及其模型之间的关系。这很快变得越来越复杂。位于渲染链深处或远处模型中的项目现在会影响其他项目的输出。在许多情况下,开发人员甚至可能不完全知道发生的更新,因为维护跟踪机制变得越来越困难。这使得开发和测试您的代码更加困难,这意味着开发一个方法或新特性并发布它变得更加困难。代码现在不太容易预测,开发时间也急剧增加。这正是 React 着手解决的问题。
起初,React 是一个思想实验。脸书认为他们已经编写了初始布局代码来描述应用可能和应该是什么样子,那么为什么不在数据或状态改变应用时再次运行启动代码呢?您现在可能正在畏缩,因为您知道这意味着他们将牺牲性能和用户体验。当你在浏览器中完全替换代码时,你将会看到屏幕的闪烁和无样式内容的闪现。这只会显得效率低下。脸书知道这一点,但也指出,它确实创造了一种在数据变化时替代状态的机制,这种机制实际上在某种程度上是有效的。脸书然后决定,如果替换机制可以优化,它将有一个解决方案。这就是 React 作为一组特定问题的解决方案是如何诞生的。
React 不仅仅是另一个框架
在很多情况下,当你学东西的时候,你首先需要意识到你正在学的东西是什么。对于 React,了解哪些概念不是 React 框架的一部分会很有帮助。这将有助于您理解,为了完全理解 React 等新框架的概念,您所学习的哪些标准实践需要被抛弃,或者至少需要被搁置。那么是什么让 React 与众不同,为什么它很重要?
许多人认为 React 是一个全面的 JavaScript 框架,与其他框架相比,如 Backbone、Knockout.js、AngularJS、Ember、CanJS、Dojo 或现有的众多 MVC 框架中的任何一个。图 1-1 显示了一个典型 MVC 框架的例子。
图 1-1。
A basic MVC architecture
图 1-1 显示了模型-视图-控制器架构中每个组件的基础。模型处理应用的状态,并向视图发送状态改变事件。视图是面向用户的外观和最终用户的交互界面。视图可以向控制器发送事件,在某些情况下,还可以向模型发送事件。控制器是事件的主要调度器,可以将事件发送到模型以更新状态,发送到视图以更新表示。您可能会注意到,这是 MVC 架构的一般表示,实际上有如此多的变体和定制实现,以至于没有单一的 MVC 架构。重点不是陈述 MVC 结构是什么样子,而是指出 React 不是什么样子。
这种 MVC 结构实际上并不是对 React 是什么或打算成为什么的公平评估。这是因为 React 是这些框架中的一个特殊部分。React 最简单的形式,只是这些 MVC、MVVM 或 MV*框架的观点。正如您在上一节中看到的,React 是一种描述应用用户界面的方法,也是一种随着数据的变化而改变的机制。React 由描述接口的声明性组件组成。React 在构建应用时不使用可观察的数据绑定。React 也很容易操作,因为您可以使用您创建的组件并组合它们来制作定制组件,因为它可以伸缩,所以每次都可以按照您的预期工作。React 可以比其他框架更好地伸缩,因为它从创建之初就遵循这些原则。当创建 React 接口时,您以这样的方式构建它们,即它们是由多个组件构建的。
让我们暂停一分钟,检查几个框架的最基本结构,然后将它们进行比较,以便突出差异。对于每个框架,您将检查为 http://todomvc.com
网站创建的最基本的待办事项列表应用。我不打算嘲笑其他框架,因为它们都有一个目的。相反,我试图展示 React 与其他相比是如何构建的。在这里,我只展示了重要的部分来突出和限制应用的完全重建。如果你想看完整的例子,链接到源包括在内。尽量不要过于关注这些例子的实现细节,包括 React 例子,因为随着你阅读本书的进展,这些概念将会被完整地涵盖,并帮助你完全理解正在发生的事情。
Ember.js
js 是一个流行的框架,它利用了由把手模板形式的视图组成的 MVC 框架。在本节中,请注意,为了便于模板、模型和控制器的集成,还需要做一些工作。这并不是说 Ember.js 是一个不好的框架,因为修改是这样一个框架的副产品。
在清单 1-1 中,它是TodoMVC Ember.js
例子的主体,您可以看到标记由两个用于待办事项列表和待办事项的手柄模板组成。
Listing 1-1. Body of TodoMVC with Ember.js
<body>
<script type="text/x-handlebars" data-template-name="todo-list">
/* Handlebars todo-list template */
</script>
<script type="text/x-handlebars" data-template-name="todos">
/* Handlebars todos template */
</script>
<script src="node_modules/todomvc-common/base.js"></script>
<script src="node_modules/jquery/dist/jquery.js"></script>
<script src="node_modules/handlebars/dist/handlebars.js"></script>
<script src="node_modules/components-ember/ember.js"></script>
<script src="node_modules/ember-data/ember-data.js"></script>
<script src="node_modules/ember-localstorage-adapter/localstorage_adapter.js"></script>
<script src="js/app.js"></script>
<script src="js/router.js"></script>
<script src="js/models/todo.js"></script>
<script src="js/controllers/todos_controller.js"></script>
<script src="js/controllers/todos_list_controller.js"></script>
<script src="js/controllers/todo_controller.js"></script>
<script src="js/views/todo_input_component.js"></script>
<script src="js/helpers/pluralize.js"></script>
</body>
除此之外还有三个控制器——一个app.js
入口点、一个路由和一个todo
输入视图组件。这看起来像很多文件,但是在生产环境中,这将被最小化。注意控制器和视图的分离。视图,包括清单 1-2 中显示的待办列表视图,非常冗长,很容易确定代码做了什么。
Listing 1-2. Ember.js Handlebars Template
{{#if length}}
<section id="main">
{{#if canToggle}}
{{input type="checkbox" id="toggle-all" checked=allTodos.allAreDone}}
{{/if}}
<ul id="todo-list">
{{#each}}
<li {{bind-attr class="isCompleted:completed isEditing:editing"}}>
{{#if isEditing}}
{{todo-input
type="text"
class="edit"
value=bufferedTitle
focus-out="doneEditing"
insert-newline="doneEditing"
escape-press="cancelEditing"}}
{{else}}
{{input type="checkbox" class="toggle" checked=isCompleted}}
<label {{action "editTodo" on="doubleClick"}}>{{title}}</label>
<button {{action "removeTodo"}} class="destroy"></button>
{{/if}}
</li>
{{/each}}
</ul>
</section>
{{/if}}
这是一个清晰的例子,可以作为一个可读的视图。如您所料,有几个属性是由控制器指定的。控制器在router.js
文件中命名,该文件还命名了要使用的视图。该控制器如清单 1-3 所示。
Listing 1-3. Ember.js TodosListController
(function () {
'use strict';
Todos.TodosListController = Ember.ArrayController.extend({
needs: ['todos'],
allTodos: Ember.computed.alias('controllers.todos'),
itemController: 'todo',
canToggle: function () {
var anyTodos = this.get('allTodos.length');
var isEditing = this.isAny('isEditing');
return anyTodos && !isEditing;
}.property('allTodos.length', '@each.isEditing')
});
})();
你可以看到这个TodosListController
采用了一个待办事项模型,并随着'todo'
的itemController
添加了一些属性。这个todo
控制器实际上是大多数 JavaScript 驻留的地方,它规定了在本节前面看到的视图中可见的动作和条件。作为一个熟悉 Ember.js 的人,这是一个定义良好、组织有序的 Ember.js 的例子。然而,它与 React 完全不同,您很快就会看到 React。首先,让我们检查一下 AngularJS TodoMVC
的例子。
安古斯
AngularJS 可能是世界上最流行的 MV*框架。它非常容易上手,并且有 Google 以及许多开发人员的支持,这些开发人员已经参与进来并创建了很棒的教程、书籍和博客帖子。它与 React 当然不是同一个框架,您很快就会看到 React。清单 1-4 展示了 AngularJS TodoMVC
应用。
Listing 1-4. AngularJS Body
<body ng-app="todomvc">
<ng-view />
<script type="text/ng-template" id="todomvc-index.html">
<section id="todoapp">
<header id="header">
<h1>todos</h1>
<form id="todo-form"``ng-submit="addTodo()"
<input
id="new-todo"
placeholder="What needs to be done?"
ng-model="newTodo"
ng-disabled="saving" autofocus
>
</form>
</header>
<section id="main" ng-show="todos.length" ng-cloak>
<input id="toggle-all" type="checkbox" ng-model="allChecked" ng-click="markAll(allChecked)">
<label for="toggle-all">Mark all as complete</label>
<ul id="todo-list">
<li ng-repeat="todo in todos | filter:statusFilter track by $index"
ng-class="{
completed: todo.completed,
editing: todo == editedTodo}"
>
<div class="view">
<input class="toggle" type="checkbox" ng-model="todo.completed" ng-change="toggleCompleted(todo)">
<label ng-dblclick="editTodo(todo)">{{todo.title}}</label>
<button class="destroy" ng-click="removeTodo(todo)"></button>
</div>
<form ng-submit="saveEdits(todo, 'submit')">
<input class="edit" ng-trim="false" ng-model="todo.title" todo-escape="revertEdits(todo)" ng-blur="saveEdits(todo, 'blur')" todo-focus="todo == editedTodo">
</form>
</li>
</ul>
</section>
<footer id="footer" ng-show="todos.length" ng-cloak>
/* footer template */
</footer>
</section>
</script>
<script src="node_modules/todomvc-common/base.js"></script>
<script src="node_modules/angular/angular.js"></script>
<script src="node_modules/angular-route/angular-route.js"></script>
<script src="js/app.js"></script>
<script src="js/controllers/todoCtrl.js"></script>
<script src="js/services/todoStorage.js"></script>
<script src="js/directives/todoFocus.js"></script>
<script src="js/directives/todoEscape.js"></script>
</body>
您已经可以看到,与 Ember.js 相比,Angular 在其模板中更具有声明性。您还可以看到,控制器、指令和服务等概念都与这个应用相关联。todoCtrl
文件保存了驱动这个视图的控制器值。清单 1-5 中显示的下一个例子只是这个文件的一个片段,但是您可以看到它是如何工作的。
Listing 1-5. Todo Controller for AngularJS
angular.module('todomvc')
.controller('TodoCtrl', function TodoCtrl(``$scope
/* omitted */
$scope.``addTodo
var newTodo = {
title: $scope.newTodo.trim(),
completed: false
};
if (!newTodo.title) {
return;
}
$scope.saving = true;
store.insert(newTodo)
.then(function success() {
$scope.newTodo = '';
})
.finally(function () {
$scope.saving = false;
});
};
/* omitted */
});
这个例子展示了todoCtrl
并展示了它如何构建一个$scope
机制,然后允许您将方法和属性附加到 AngularJS 视图。下一节将深入 React 并解释它如何以不同于 Ember.js 和 AngularJS 的方式作用于用户界面。
React
正如您在其他例子中看到的,TodoMVC
应用有一个基本的结构,这使它们成为展示差异的一个简单选择。Ember.js 和 AngularJS 是两个流行的框架,我认为它们有助于证明 React 不是一个 MV*框架,而只是一个用于构建用户界面的基本 JavaScript 框架。本节将详细介绍 React 示例,并向您展示如何从组件级别构建 React 应用,然后反向解释组件是如何组成的。现在,关于 React 的书已经有很多页了,你终于可以在清单 1-6 中看到 React 代码。
Note
提供的代码将从 web 服务器上运行。这可以是 Python 中的一个SimpleHTTPServer
,一个 Apache 服务器,或者任何你熟悉的东西。如果这不可用,您可以在您的浏览器中提供 HTML 文件,但是您需要确保相关的文件是本地的,并且可以被您的 web 浏览器获取。
Listing 1-6. The Basic HTML of the React Todo App
<!-- some lines removed for brevity -->
<body>
<section id="todoapp"></section>
<script src="react.js"></script>
<script src="JSXTransformer.js"></script>
<script src="js/utils.js"></script>
<script src="js/todoModel.js"></script>
<script type="text/jsx" src="js/todoItem.jsx"></script>
<script type="text/jsx" src="js/footer.jsx"></script>
<script type="text/jsx" src="js/app.jsx"></script>
</body>
在清单 1-6 中,您可以看到基本 React todoMVC
应用的主体。请注意该部分及其id
属性。将这个主体与 AngularJS 和 Ember.js 示例进行比较,注意,对于这种类型的应用,脚本标记的数量以及您需要处理的文件数量要少得多。有人可能会说,文件的数量不是一个公平的比较,因为从理论上讲,你可以构建一个 AngularJS 应用,使每个文件包含不止一个控制器,或者找到类似的方法来限制脚本元素的数量。关键是 React 似乎很自然地分裂成这些类型的结构,因为组件的编写方式不同。这并不意味着 React 一定更好,甚至更简洁,而是 React 创建的范例至少使创建组件看起来更简洁。
该部分将是渲染 React 组件时放置它们的目标。包含的脚本是 React 库和 JSX 转换器文件。接下来的两项是每个todoMVC
应用中包含的数据模型和实用程序。这些项目之后是三个 JSX 文件,它们构成了整个应用。该应用是由包含在app.jsx
文件中的组件呈现的,您将在清单 1-7 中检查该组件。
Listing 1-7. app.jsx Render Function
var model = new app.TodoModel('react-todos');
function render() {
React.render(
<TodoApp model={model}/>,
document.getElementById('todoapp')
);
}
model.subscribe(render);
render();
清单 1-7 展示了 React 如何工作的有趣视图。在本书的其余部分,您将了解到这是如何实现的,但在示例中,基本内容以粗体显示。首先,您会看到看起来像 HTML 或 XML 元素的东西<TodoApp model={model}/>
。这是 JSX,或 JavaScript XML transpiler,它是为了与 React 集成而创建的。JSX 并不要求与 React 一起使用,但它可以使创作应用更加容易。它不仅使编写 React 应用变得更容易,而且当您阅读和更新代码时,它允许更清晰的语法。前面的 JSX 转换成如下所示的 JavaScript 函数:
React.createElement(TodoApp, {model: model});
这是目前值得注意的一点,你会在第三章中读到更多关于 JSX 及其转变的内容。
从这个例子中可以看出,您可以创建一个组件,然后通过在 DOM 中命名要附加的元素作为 render 方法的第二个参数,将它附加到 DOM。在前一个例子中,这个命名元素是document.getElementById('todoapp')
。在接下来的几个例子中,您将看到如何创建TodoApp
组件,并了解代表 React 组件如何组成的基本思想,所有这些都将在本书的后面详细介绍。
var TodoApp =``React.createClass
/* several methods omitted for brevity */
render: function() {
/* see next example */
}
});
从这个例子中,你可以看到这个TodoApp
组件的组成的几个核心概念。首先使用一个名为React.createClass()
的函数创建它。这个函数接受一个对象。createClass
方法将在下一章深入讨论,以及如何使用 ES6 类创作这样一个组件。在这个对象中,TodoMVC
应用中有几个方法,但是在这个例子中,突出显示 render 方法是很重要的,这是所有 React 组件所必需的。您将在清单 1-8 中更仔细地检查它们。这是一个很大的方法,因为它处理了 React 所做的很大一部分工作,所以在通读时要有耐心。
Listing 1-8. React TodoMVC Render Method
render: function() {
var footer;
var main;
var todos =
this.props.model.todos;
var showTodos = todos.filter(function (todo) {
switch (this.state.nowShowing) {
case app.ACTIVE_TODOS:
return !todo.completed;
case app.COMPLETED_TODOS:
return todo.completed;
default:
return true;
}, this);
var todoItems = shownTodos.map(function (todo) {
return (
<TodoItem
key={todo.id}
todo={todo}
onToggle={this.toggle.bind(this, todo)}
onDestroy={this.destroy.bind(this, todo)}
onEdit={this.edit.bind(this, todo)}
editing={this.stat.editing === todo.id}
onSave={this.save.bind(this, todo)}
onCancel={this.cancel}
/>
);
}, this);
var activeTodoCount = todos.reduce(function (accum, todo) {
return todo.completed ? accum : accum + 1;
}, 0);
var completedCount = todos.length - activeTodoCount;
if (activeTodoCount || completedCount) {
footer =
<TodoFooter
count={activeTodoCount}
completedCount={completedCount}
nowShowing={this.state.nowShowing}
onClearCompleted={this.clearCompleted}
/>
;
}
if (todos.length) {
main = (
<
section id="main">
<input
id="toggle-all"
type="checkbox"
onChange={this.toggleAll}
checked={activeTodoCount === 0}
/>
<ul id="todo-list">
{todoItems}
</ul>
);
}
return (
<div>
<header id="header">
<h1>todos</h1>
<input
ref="newField"
id="new-todo"
placeholder="What needs to be done?"
onKeyDown={this.handleNewTodoKeyDown}
autoFocus={true}
/>
</header>
{main}
{footer}
</div>
);
}
如您所见,这里发生了很多事情,但是我希望您也看到从开发的角度来看这是多么简单和声明性。它展示了 React 比其他框架(包括 AngularJS 示例)更具声明性。这种声明式方法准确地显示了在应用呈现时您将在页面上看到的内容。
让我们回到这一节的开头,在那里您看到了<TodoApp model={model} />
组件。该组件充当位于app.jsx
文件末尾的渲染函数的主要组件。在最近的例子中,我加粗了代码中的一些关键点。首先,请注意,model={model}
被传递到函数中,然后在TodoApp
类的开始处被称为this.props.model.todos
。这是 React 声明性的一部分。您可以在组件上声明属性,并在组件方法内的this.props
对象中使用它们。
接下来是子组件的概念。创建并引用另一个名为<TodoItem/>. TodoItem
的 React 组件的变量todoItems
是在自己的 JSX 文件中创建的另一个 React 组件。拥有一个专门描述特定TodoItems
行为的TodoItem
组件,并将其作为TodoApp
组件中的命名元素,这是一个非常强大的概念。当您使用 React 构建越来越复杂的应用时,您会发现准确地知道您需要更改什么组件,并且它是独立的和自包含的,这将使您对应用的稳定性充满信心。清单 1-9 是来自TodoItems
组件的渲染函数。
Listing 1-9. TodoItems Render Method
app.``TodoItem
/* omitted code for brevity */
render: function () {
return (
<li
className={React.addons.classSet({
completed: this.props.todo.completed,
editing: this.props.editing
})}>
<div className="view">
<input
className="toggle"
type="checkbox"
checked={this.props.todo.completed}
onChange={this.props.onToggle}
/>
<label onDoubleClick={this.handleEdit}>
{this.props.todo.title}
</label>
<button className="destroy" onClick={this.props.onDestroy} />
</div>
<input
ref="editField"
className="edit"
value={this.state.editText}
onBlur={this.handleSubmit}
onChange={this.handleChange}
onKeyDown={this.handleKeyDown}
/>
</li>
);
}
});
在这个例子中,您可以看到TodoItem
组件的呈现,它是TodoApp
的子组件。这只是一个处理包含在TodoApp
中的单个列表项的组件。这被分割成它自己的组件,因为它代表了应用中它自己的一组交互。它可以处理编辑以及标记项目是否完成。由于该功能不一定需要了解应用的其余部分或与之交互,因此它是作为独立的组件构建的。最初可能很容易添加到TodoApp
本身,但是在 React 的世界中,正如您将在后面看到的,让事情更加模块化通常更好。这是因为在将来,维护成本将通过利用这种交互的逻辑分离而得到补偿。
现在,您已经在较高层次上理解了 React 应用中的子组件通常包含交互。TodoApp
呈现函数的代码显示了TodoItem
作为一个子组件存在,并且显示了TodoFooter
,它本身包含在一个 JSX 中,容纳了它自己的交互。下一个重要的概念是关注这些子组件是如何重新组装的。TodoItems
被添加到一个无序列表中,该列表包含在一个名为main
的变量中,该变量返回TodoApp
的主要部分的 JSX 标记。类似地,footer
变量包含TodoFooter
组件。这两个变量footer
和main
被添加到TodoApp
的返回值中,您可以在示例的最后看到。在 JSX 中,这些变量是用花括号来访问的,所以您会看到它们如下所示:
{main}
{footer}
现在,您已经对 React 应用和组件的构建有了全面的了解,尽管只是一个基本的概述。您还可以通过访问todomvc.com
,将这些想法与用 Ember.js 和 Angular 或任何其他框架构建的相同应用的概述进行比较。React 作为一个框架与其他框架有很大的不同,因为它只是一种利用 JavaScript 来制作复杂用户界面的方法。这意味着交互都包含在声明性组件中。像其他框架一样,没有用于创建数据绑定的直接可观察对象。该标记是或者至少可以是利用嵌入式 XML 语法 JSX 生成的。最后,您可以将所有这些放在一起创建定制组件,如 singular <TodoApp />
。
React 概念和术语
这一部分强调了一些你将在本书中看到的关键术语和概念,并帮助你更清楚地理解后面几章所写的内容。您还将获得一个工具和实用程序列表,这些工具和实用程序将帮助您立即熟悉 React。第二章深入解释了 React 核心的许多概念,并深入到构建 React 应用和实现 React 附加组件和附件。
做出 React
既然您已经阅读了 React 的简要概述,知道它是什么以及它为什么重要,那么了解您可以获得 React 并开始使用它的方法是很重要的。在 React 文档中,有指向可破解的 JSFiddle 演示的链接,您可以在那里进行体验。这些应该足够开始跟随这本书。
JSFiddle with JSX:
http://jsfiddle.net/reactjs/69z2wepo/
JSFiddle without JSX:
http://jsfiddle.net/reactjs/5vjqabv3/
除了浏览器内开发之外,获得 React 的最简单方法之一是浏览 React 入门网站,然后单击标有 Download Starter Kit 的大按钮。
当然,您可以获取源文件并将其添加到应用的脚本标记中。事实上,脸书在其 CDN 上有一个版本,链接可以在 React 下载页面的 https://facebook.github.io/react/downloads.html
找到。当您将 React 作为脚本标签时,变量React
将是一个全局对象,一旦页面加载了 React 资产,您就可以访问它。
越来越常见的是,你会看到人们使用 Browserify 或 WebPack 工具将 React 集成到他们的工作流中。这样做允许您以一种与 CommonJS 模块加载系统兼容的方式require('React')
。要开始这个过程,您需要通过npm
安装 React:
npm install react
成分
组件是 React 的核心,是应用的视图。这些通常通过调用React.createClass()
来创建,如下所示:
var MyClass = React.createClass({
render: function() {
return (
<div>hello world</div>
);
}
});
或者使用 ES6 类,例如:
class MyClass extends React.Component {
render() {
return <div>hello world</div>;
}
}
在下一章你会看到更多关于 React 组件的内容。
虚拟 DOM
也许 React 最重要的部分是虚拟 DOM 的概念。这一点在本章开始时就提到过,你在那里读到过脸书在每次数据改变或用户与应用交互时都重建界面。有人指出,尽管脸书意识到新框架的性能并不符合其标准,但它仍然希望按照这一理想进行工作。所以脸书开始改变框架,从每次数据改变时一组 DOM 突变的框架,到它所谓的协调。脸书通过创建一个虚拟 DOM 来做到这一点,他们每次遇到更新时都使用这个虚拟 DOM 来计算更新应用的实际 DOM 所需的最小更改集。你将在第二章中了解更多关于这个过程的信息。
小艾
您之前已经了解到,JSX 是转换层,它将用于编写 React 组件的 XML 语法转换为 React 用于在 JavaScript 中呈现元素的语法。这不是 React 的必需元素,但它肯定会受到高度重视,可以使构建应用更加流畅。该语法不仅可以接受自定义的 React 类,还可以接受普通的 HTML 标签。它将标记转换成适当的 React 元素,如下例所示。
// JSX version
React.render(
<div>
<h1>Header</h1>
</div>
);
// This would translate to
React.render(
React.createElement('div', null,
React.createElement('h1', null, 'Header')
);
);
当你通读第三章中对 JSX 的深入概述时,你会看到所有细节。
性能
属性在 React 中通常被称为this.props
,因为这是访问属性最频繁的方式。属性是组件拥有的一组选项。this.props
是 React 中的普通 JavaScript 对象。这些属性在组件的整个生命周期中不会改变,因此您不应该将它们视为不可变的。如果你想改变组件上的某些东西,你将改变它的状态,你应该利用状态对象。
状态
状态是在每个组件初始化时设置的,并且在组件的整个生命周期中也会改变。除非父组件正在添加或设置组件的初始状态,否则不应从组件外部访问该状态。不过,一般来说,您应该尝试使用尽可能少的状态对象来创作组件。这是因为当您添加状态时,组件的复杂性会增加,因为 React 组件不会根据状态随时间而改变。如果可以避免的话,组件中根本没有任何状态也是可以接受的。
流量
Flux 是一个与 React 密切相关的项目。理解它如何与 React 一起工作很重要。Flux 是脸书的应用架构,用于如何让数据以一种有组织、有意义的方式与 React 组件进行交互。Flux 不是模型-视图-控制器体系结构,因为它们利用双向数据流。助焊剂对 React 至关重要,因为它有助于促进 React 组件按预期方式使用。Flux 通过创建单向数据流来实现这一点。数据流经 Flux 架构的三个主要部分:调度程序、存储和最终的 React 视图。关于 Flux 这里没有太多要说的,但是在第五章和第六章中,你将得到对 Flux 的全面介绍,然后学习将它集成到你的 React 应用中,以完成对 React 的介绍。
工具
有几个工具可以帮助 React 开发变得更加有趣。要通过npm
访问可以安装在命令行上的 JSX 转换器,使用以下命令:
npm install -g react-tools
有几个实用程序和编辑器集成,其中大部分在 https://github.com/facebook/react/wiki/Complementary-Tools#jsx-integrations
中列出。你可能会在那里找到你需要的工具。例如,如果您使用 Sublime Text 或 vim 创作 JavaScript,那么这两者都有一个语法高亮器。
另一个有用的工具是 lint 你的代码。JSX 为林挺你的文件提供了一些特殊的挑战,还有一个jsxhint
项目,它是流行的 JSHint 林挺工具的 JSX 版本。
在开发过程中,您很可能最终需要在浏览器中检查 React 项目。目前在 https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi
有 Chrome 扩展,可以让你直接检查你的 React 组件。在调试或优化 React 应用时,您可以获得关于属性、状态和所有需要的细节的有价值的信息。
附加组件
脸书已经提供了几个实验插件来对React.addons
物体做出 React。当您开发应用时,这些只能通过使用/react-with-addons.js
文件来访问。或者,如果您通过 React npm
包使用 Browserify 或 WebPack,您可以将您的require()
语句从require('react');
改为require('react/addons')
。您可以在 React 网站的 https://facebook.github.io/react/docs/addons.html
找到关于哪些附加组件当前可用的文档。
除了这些附加组件,还有几个社区附加组件对 React 开发非常有用。这种类型的数量正在增加,但是一个有用的例子是一个名为 react-router 的项目,它为 react 应用提供路由。
var App = React.createClass({
getInitialState: function() {
},
render: function () {
return (
<div>
<ul>
<li><Link to="main">Demographics</Link></li>
<li><Link to="profile">Profile</Link></li>
<li><Link to="messages">Messages</Link></li>
</ul>
<UserSelect />
</div>
<RouteHandler name={this.state.name}/>
</div>
);
}
});
var routes = (
<Route name="main" path="/" handler={App}>
<Route name="profile" handler={Profile}/>
<Route name="messages" handler={Messages}/>
<DefaultRoute handler={ Demographics }/>
</Route>
);
Router``.run(``routes
React.render(<Handler />, document.getElementById("content"));
});
此示例显示了路由如何处理菜单选择,并将从路由移动到适当的组件。这是 React 的强大扩展。没有它你也能过,但它让事情变得更简单。React 社区很大,而且发展很快。在构建 React 应用的过程中,您可能会遇到新的插件,甚至可以创建自己的插件。在下一章中,您将看到 React 的更多核心内容,并了解它是如何工作的,这将帮助您进一步理解 React 是什么以及它为什么重要。
摘要
本章介绍了允许脸书构建 React 的概念。您了解了 React 的概念如何被普遍视为偏离了用户界面开发中通常接受的最佳实践。挑战现状和测试理论使 React 成为一个高性能和可伸缩的 JavaScript 框架,用于构建用户界面。
通过几个例子,您还直接看到了 React 如何通过以一种新的方式解决这些框架的视图部分而不同于一些领先的模型-视图-控制器框架。
最后,您能够了解组成 React 框架及其社区的术语、概念和工具。在下一章,你将更深入地了解如何使用 React 以及它是如何工作的。
二、React 的核心
Electronic supplementary material The online version of this chapter (doi:10.1007/978-1-4842-1245-5_2) contains supplementary material, which is available to authorized users.
在最后一章中,你已经了解了 React 是什么,以及为什么它对你这样的开发人员很重要。它展示了 React 与其他框架的比较,并强调了它的不同之处。有几个概念被介绍了,但没有详细介绍一本介绍性的书应该做的。本章将深入讨论 React 的构建模块——它的核心结构和架构。
在本章和接下来的其他章节中,您将看到 React 代码,包括应用示例和 React 的一些内部工作方式。对于组成库的 React 代码,您会注意到代码被标记为这样,并带有一个标题,说明它在源代码中的出处。示例代码至少以两种形式之一编写。一种形式(目前在开发人员中很常见)是 ECMAScript 5 语法。在适用的地方,您会看到使用 ECMAScript 2015 (ES6)语法的重复示例,这种语法在 React 中变得越来越普遍,并且正在作为一等公民融入 React 环境中。你会发现大多数例子都使用了 JSX 语法,这在第三章中有详细介绍。
React
当我们开始查看 React 时,最好从 React 对象本身开始。react 对象包含几个方法和属性,允许您最大限度地利用 React。对于jsfiddle.net
或jsbin.com
上的大多数示例,本章的来源都是可用的。这些示例的链接(如果有)包含在列表标题中。
createClass React
createClass
方法将在 React 中创建新的组件类。createClass
可以用一个对象创建,这个对象必须有一个render()
函数。在本节的稍后部分,您将获得关于组件的更深入的信息,但是createClass
的基本实现如下,其中specification
是将包含render()
方法的对象。
React.createClass( specification );
清单 2-1 展示了如何使用createClass
创建一个简单的组件。该组件只是创建一个div
元素,并将一个名称属性传递给要呈现的div
。
Listing 2-1. createClass
. Example Available Online at https://jsfiddle.net/cgack/gmfxh6yr/
var MyComponent = React.createClass({
render: function() {
return (
<div>
{this.props.name}
</div>
);
}
});
React.render(<MyComponent name="frodo" />, document.getElementById('container'));
正如你将在本章后面详细讨论组件时看到的,通过从React.Component
继承,使用 ES6 类创建组件是可能的。这可以在清单 2-2 中看到。
Listing 2-2. ES6 Class Component. Available Online at http://jsbin.com/hezewe/2/edit?html,js,output
class MyComponent extends React.Component {
render() {
return (
<div>
{this.props.name}
</div>
);
}
};
React.render(<MyComponent name="frodo" />, document.getElementById('container'));
做出 React。儿童地图
React.Children.map
是React.Children
内的函数。它是一个包含几个帮助函数的对象,这些函数允许你轻松地使用你的组件属性this.props.children
,它将对包含的每个直接子对象执行一个函数,并将返回一个对象。React.Children.map
的用法如下
React.Children.map( children, myFn [, context])
在这里,children
参数是一个包含您想要定位的子对象的对象。然后将函数myFn
应用于每个孩子。最后一个参数context
是可选的,它将在映射函数上设置this
。
清单 2-3 通过在一个简单的组件中创建两个子元素展示了这是如何工作的。然后,在组件的render
方法中,设置了一个console.log()
语句,这样您就可以看到子对象ReactElements
被显示出来。
Listing 2-3. Using React.Children.map
. Available Online at https://jsfiddle.net/cgack/58u139vd/
var MyComponent = React.createClass({
render: function() {
React.Children.map(this.props.children, function(child){
console.log(child)
});
return (
<div>
{this.props.name}
</div>
);
}
});
React.render(<MyComponent name="frodo" >
<p key="firsty">a child</p>
<p key="2">another</p>
</MyComponent>, document.getElementById('container'));
做出 React。Children.forEach
forEach
是另一个可以在 React 中的this.props.children
上使用的实用程序。它类似于React.Children.map
函数,只是它不返回对象。
React.Children.forEach( children, myFn [, context])
清单 2-4 展示了如何使用forEach
方法。类似于 map 方法,这个例子将ReactElement
子对象记录到控制台。
Listing 2-4. Using React.Children.forEach
. Available Online at https://jsfiddle.net/cgack/vd9n6weg/
var MyComponent = React.createClass({
render: function() {
React.Children.forEach(this.props.children, function(child){
console.log(child)
});
return (
<div>
{this.props.name}
</div>
);
}
});
React.render(<MyComponent name="frodo" >
<p key="firsty">a child</p>
<p key="2">another</p>
</MyComponent>, document.getElementById('container'));
做出 React。儿童.计数
count
方法将返回包含在this.props.children
中的组件数量。该函数执行如下,并接受一个参数,一个对象。
React.Children.count( children );
清单 2-5 显示了一个调用React.Children.count
()
并将计数记录到控制台的例子。
Listing 2-5. React.Children.count()
. Also Available Online at https://jsfiddle.net/cgack/n9v452qL/
var MyComponent = React.createClass({
render: function() {
var cnt =
React.Children.count(this.props.children);
console.log(cnt);
return (
<div>
{this.props.name}
</div>
);
}
});
React.render(<MyComponent name="frodo" >
<p key="firsty">a child</p>
<p key="2">another</p>
</MyComponent>, document.getElementById('container'));
做出 React。仅限儿童
only
方法将返回this.props.children
中唯一的子节点。它接受children
作为单个对象参数,就像count
函数一样。
React.Children.only( children );
清单 2-6 展示了如何利用这种方法。请记住,如果您的组件有多个子组件,React 将不允许您调用此方法。
Listing 2-6. React.Children.only
. Available Online at https://jsfiddle.net/cgack/xduw652e/
var MyComponent = React.createClass({
render: function() {
var only = React.Children.only(this.props.children);
console.log(only);
return (
<div>
{this.props.name}
</div>
);
}
});
React.render(<MyComponent name="frodo" >
<p key="firsty">a child</p>
</MyComponent>, document.getElementById('container'));
react . createelement
createElement
方法将生成一个新的ReactElement
。它是使用函数的至少一个、可选的最多三个参数创建的——一个字符串type
,可选的一个对象props
,可选的children
。在本章的后面,您将了解更多关于createElement
功能的信息。
React.createElement( type, [props[, [children ...] );
清单 2-7 展示了如何使用这个函数创建一个元素。在这种情况下,不使用 JSX <div>
标签,而是显式地创建一个元素。
Listing 2-7. createElement
var MyComponent = React.createClass({
displayName: "MyComponent",
render: function render() {
return
React.createElement(
"div",
null,
this.props.name
)
;
}
});
React.render(``React.createElement(MyComponent, { name: "frodo" })
React.cloneElement
该方法将基于作为参数提供的目标基数element
克隆一个ReactElement
。可选地,您可以接受第二个和第三个参数— props
和children
。当我们在本章后面更详细地讨论元素和工厂时,你会看到更多关于cloneElement
函数的内容。
React.cloneElement( element, [props], [children ...] );
做出 React。数字正射影像图
如果不使用 JSX,这个对象提供了帮助创建 DOM 元素的实用函数。除了在 JSX 编写<div>my div</div>
之外,您还可以通过编写如下代码来创建元素。
React.DOM.div(null, "my div");
由于本书中的大多数例子都将利用 JSX,所以在写代码的时候你可能不会看到太多的React.DOM
。只要理解 JSX 转换到的底层 JavaScript 将包含这些方法。
React.createFactory
React.createFactory
是一个在给定的ReactElement type
上调用createElement
的函数。在本章后面深入讨论元素和工厂时,你会学到更多关于工厂的知识。
React.createFactory( type );
React.渲染
React.render
将获取一个ReactElement
并将其呈现给 DOM。React 只知道通过提供一个container
来放置元素,它是一个 DOM 元素。可选地,您可以提供一个callback
函数,一旦ReactElement
被呈现给 DOM 节点,该函数就会被执行。
React.render( element, container [, callback ] );
清单 2-8 突出了一个简单 React 组件的渲染方法。注意,ID 为container
的 DOM 元素是 React 将呈现该组件的地方。
Listing 2-8. React.render
. Available Online at https://jsfiddle.net/cgack/gmfxh6yr/
var MyComponent = React.createClass({
render: function() {
return (
<div>
{this.props.name}
</div>
);
}
});
React.render(<MyComponent name="frodo" />, document.getElementById('container'));
React.renderToString
React.renderToString
是一个函数,允许你将一个ReactElement
渲染到它的初始 HTML 标记。正如您可能会想到的,这在 web 浏览器中不如在 React 应用的服务器端呈现版本中有用。此功能用于从服务器为您的应用提供服务。事实上,如果你在一个已经用服务器上的React.renderToString
渲染过的元素上调用React.render()
,React 足够聪明,只需要给那个元素附加事件处理程序,而不需要重新移植整个 DOM 结构。
React.renderToString( reactElement );
React。findDOMNode
React.findDOMNode
是一个函数,它将返回所提供的 React 组件的 DOM 元素或传递给该函数的元素:
React.findDOMNode( component );
它首先检查组件或元素是否是null
。如果是,它将返回 null。然后,它检查传递的组件本身是否是 DOM 节点,如果是,它将返回该元素作为节点。然后,它将利用内部的ReactInstanceMap
,然后从该映射中获取 DOM 节点。
在接下来的章节中,我们将获得关于 React 组件和元素工厂的更深入的信息,并讨论它们如何应用于您的 React 应用。
发现 React 组件
在构建 React 应用时,React 组件是主要的构建块。在本节中,您将演示如何创建组件,以及可以用它们做什么。
React 组件是在使用 ES6 从基类React.Component
扩展时创建的。或者,更传统地,你可以使用React.createClass
方法(参见清单 2-9 和 2-10 )。
Listing 2-9. myComponent
class Created Using ES6. Example Found Online at https://jsbin.com/jeguti/2/edit?html,js,output
class myComponent extends React.Component {
render() {
return ( <div>Hello World</div> );
}
}
Listing 2-10. myComponent
Created Using React.createClass
. An interactive Version of this Example Can Be Found Online at https://jsbin.com/wicaqe/2/edit?html,js,output
var myComponent React.createClass({
render: function() {
return ( <div>Hello World</div> );
}
});
React 组件有自己的 API,其中包含几个方法和帮助器,如下所述。在撰写本文时,这些函数中的一些在 React v 0.13.x 中不可用或被弃用,但在 React 框架的遗留版本中存在。你会看到这些被提及,但重点将是最未来友好的功能,尤其是那些使用 ECMAScript 2015 (ES6)可访问的功能。
基类React.Component
是组件 API 的未来友好版本。这意味着它只实现了 ES6 特性,setState
和forceUpdate
。要使用setState
,您可以向setState
方法传递一个函数或一个普通对象。或者,您可以添加一个回调函数,该函数将在设置状态后执行。见清单 2-11 。
Listing 2-11. setState
Using a Function, the currentState
Passed into the Function Will Alter the Returned (New) State Being Set
setState( function( currState, currProps ) {
return { X: currState.X + "state changed" };
});
setState using an object directly setting the state.
setState( { X: "state changed" } );
当调用setState
时,您实际上是将新对象排队到 React 更新队列中,这是 React 用来控制何时发生变化的机制。一旦状态准备好改变,新的状态对象或部分状态将与组件状态的剩余部分合并。实际的更新过程是批量更新中的一个句柄,所以在使用setState
函数时有几个注意事项。首先,不能保证您的更新将以任何特定的顺序处理。因此,如果在执行完setState
后您希望依赖某些东西,那么在回调函数中这样做是个好主意,您可以选择将回调函数传递给setState
函数。
关于状态的一个重要注意事项是,永远不要通过直接设置this.state
对象来直接改变组件的状态。这里的想法是,您希望将状态对象视为不可变的,并且只允许 React 和排队并合并状态的setState
过程来控制状态的更改。
在React.Component
类的原型中出现的另一个核心 API 方法是一个名为forceUpdate
的函数。forceUpdate
所做的正是你所期待的;它会强制组件更新。它通过再次利用 React 的队列系统,然后强制组件更新来实现这一点。它通过绕过组件生命周期的一部分ComponentShouldUpdate
来做到这一点,但是您将在后面的章节中了解关于组件生命周期的更多信息。为了强制更新,你需要做的就是调用函数。您可以选择添加一个回调函数,该函数将在强制更新后执行。
forceUpdate( callback );
组件 API 还有其他几个值得一提的部分,因为尽管它们是不推荐使用的特性,但它们在许多实现中仍然很普遍,并且您将看到的许多关于 React 的文档可能包括这些特性。请注意,这些功能已被弃用。在 React 的未来版本中,如高于 0.13.x 的版本,它们很可能会被删除。这些方法将在下一节中介绍。
了解组件属性和方法
你现在已经看到了forceUpdate
和setState
,这两个核心函数是React.Component
类原型的 ES6 版本的一部分。使用 ES6 时,有几个方法不可用,因为它们已被弃用。尽管在使用 React 创建组件时它们不是必需的,但是您会发现许多文档和示例都包含了它们,所以我们在这本介绍性的书中提到了它们。这些方法只有在您使用React.createClass
作为您的函数来创作组件时才可用。它们以一种巧妙的方式被添加到 React 代码中,我认为这值得一提,因为它强调了这是一个真正的附加解决方案,在未来的版本中很容易被放弃。添加这些额外函数的代码如下:
var ReactClassComponent = function() {};
assign(
ReactClassComponent.prototype,
ReactComponent.prototype,
ReactClassMixin
);
这里你可以看到ReactClassComponent
——当你调用React.createClass
时,它变成了你的组件——被创建,然后assign
方法被调用。assign
方法基于Object.assign( target, ...sources )
,它将获取sources
的所有可枚举的自身属性,并将它们分配给target
。这基本上是深度合并。最后,ReactClassMixin
被添加到组件中,它有几个方法。一种方法是setState
的表亲,叫做replaceState
。replaceState
函数将完全覆盖当前属于组件的任何状态。
replaceState( nextState, callback );
方法签名包括一个表示nextState
的对象和一个可选的callback
函数,一旦状态被替换,该函数将被执行。通常,您希望您的状态在组件的整个生命周期中保持一致的签名类型。正因为如此,replaceState
在大多数情况下应该避免,因为它违背了一般的想法,而状态仍然可以利用setState
来操纵。
另一个函数是ReactClassMixin
的一部分,因此当你使用React.createClass
创建一个组件时可以使用,如果你引用的组件已经被渲染到 DOM,布尔函数isMounted. isMounted
将返回 true。
bool isMounted();
getDOMNode
是一个不推荐使用的特性,可以从用React.createClass
创建的组件中访问。这实际上只是一个访问React.findDOMNode
的实用程序,这应该是查找组件或元素所在的 DOM 节点的首选方法。
使用 React 组件时,您可能会发现有必要触发组件的另一个渲染。您将会看到,最好的方法是简单地调用组件上的render()
函数。还有另一种方式来触发你的组件的渲染,类似于setState
,叫做setProps
。
setProps( nextProps, callback );
setProps
所做的是允许你将下一组属性传递给对象形式的组件。或者,您可以添加一个回调函数,该函数将在组件再次呈现后执行。
与setProps
方法类似的是replaceProps
函数。该函数接受一个对象,并将完全覆盖组件上现有的一组属性。replaceProps
还允许一个可选的回调函数,一旦组件在 DOM 中完全重新呈现,该函数就会执行。
这里总结了 React 组件的基本特性,以及开发人员可以使用的基本属性和功能。在研究 React 元素和工厂之前,下一节将着眼于组件的生命周期,包括它是如何呈现的。
组件生命周期和渲染
在深入 React 组件生命周期之前,您首先应该了解组件规范功能。当您创建一个组件时,这些功能将会或者可以包含在您的规范对象中。这些规范函数的一部分是生命周期函数,当遇到这些函数时,将显示它们在组件生命周期中何时执行的细节。
提出
正如在本章开始的核心 API 回顾中提到的,每个 React 组件都必须有一个render
函数。这个render
函数将接受一个ReactElement
,并提供一个容器位置,组件将在这个位置被添加或挂载到 DOM。
getInitialState
这个函数将返回一个对象。该对象的内容将在组件最初呈现时设置组件的状态。该函数在组件呈现之前被调用一次。当使用 ES6 类创建一个组件时,你实际上将通过this.state
在类的构造函数中设置state
。清单 2-12 展示了如何在非 ES6 组件和 ES6 组件中处理这个问题。
getDefaultProps
当第一次创建ReactClass
时,getDefaultProps
被调用一次,然后被缓存。该函数返回一个对象,该对象代表组件上this.props
的默认状态。不存在于父组件中,但存在于组件映射中的this.props
的值将被添加到这里的this.props
中。当您使用 ES6 设置创建一个组件时,默认的 props 是在组件类的constructor
函数中完成的。
清单 2-12 展示了创作组件的React.createClass
方法和使用 ES6 的getInitialState
和getDefaultProps
。
Listing 2-12. getDefaultProps
and getInitialState
in Action
var GenericComponent = React.createClass({
getInitialState: function() {
return { thing: this.props.thingy };
},
getDefaultProps: function() {
return { thingy: "cheese" }
}
});
// ES6
class GenericComponent extends React.Component {
constructor(props) {
super(props);
this.state = { thing: props.thingy };
}
}
GenericComponent.defaultProps = { thingy: "cheese" };
混入类
组件规范中的 mixin 是一个数组。mixin 可以共享您的组件的生命周期事件,并且您可以确信该功能将在组件生命周期的适当时间执行。mixin 的一个例子是一个定时器控件,它将一个SetIntervalMixin
的生命周期事件与名为TickTock
的主组件合并。如清单 2-13 所示。
Listing 2-13. Using a React Mixin. An Interactive Example Is Found Online at https://jsfiddle.net/cgack/8b055pcn/
var SetIntervalMixin = {
componentWillMount: function() {
this.intervals = [];
},
setInterval: function() {
this.intervals.push(setInterval.apply(null, arguments));
},
componentWillUnmount: function() {
this.intervals.map(clearInterval);
}
};
var TickTock = React.createClass({
mixins: [SetIntervalMixin], // Use the mixin
getInitialState: function() {
return {seconds: 0};
},
componentDidMount: function() {
this.setInterval(this.tick, 1000); // Call a method on the mixin
},
tick: function() {
this.setState({seconds: this.state.seconds + 1});
},
render: function() {
return (
<p>
React has been running for {this.state.seconds} seconds.
</p>
);
}
});
类型(p)
propTypes
是一个对象,您可以为传递给组件的每个属性添加类型检查。propTypes
是基于一个名为React.PropTypes
的 React 对象设置的,它们的类型将在下面讨论。
如果你想强制一个特定类型的属性,你可以用几种方法。首先让属性有一个类型,但是让它成为一个可选的属性。您可以通过在您的propTypes
对象中指定属性的名称并将其设置为相应的React.PropTypes
类型来实现。例如,一个可选布尔值属性如下所示:
propTypes: {
optionalBoolean: React.PropTypes.bool
}
同样的格式也适用于其他 JavaScript 类型:
React.PropTypes.array
React.PropTypes.bool
React.PropTypes.func
React.PropTypes.number
React.PropTypes.object
React.PropTypes.string
React.PropTypes.any
除了这些类型,您还可以通过将isRequired
标签附加到React.PropType
声明来使它们成为必需的属性。所以在你的布尔型propType
的情况下,你现在需要它如下:
propTypes: {
requiredBoolean: React.PropTypes.bool.isRequired
}
在 JavaScript 类型之外,您可能希望实施一些更特定于 React 的东西。您可以使用React.PropType.node
来做到这一点,它表示 React 可以呈现的任何内容,比如数字、字符串、元素或这些类型的数组。
myNodeProp: React.PropTypes.node
也有React.PropTypes.element
型。它将强制该属性是一个 React 元素:
myNodeProp: React.PropTypes.element
这里也有几个PropType
助手。
//enforces that your prop is an instance of a class
React.PropTypes.instanceOf( MyClass ).
// Enforces that your prop is one of an array of values
React.PropTypes.oneOf( [ 'choose', 'cheese' ])
// Enforces a prop to be any of the listed types
React.PropTypes.onOfType( [
React.PropTypes.string,
React.PropTypes.element,
React.PropTypes.instanceOf( MyClass )
])
// Enforce that the prop is an array of a given type
React.PropTypes.arrayOf( React.PropTypes.string )
// Enforce the prop is an object with values of a certain type
React.PropTypes.objectOf( React.PropTypes.string )
静力学
在您的组件规范中,您可以在statics
属性中设置一个静态函数对象。静态函数存在于组件中,无需创建函数的实例就可以调用。
显示名称
displayName
是当您看到来自 React 应用的调试消息时使用的属性。
组件将安装
componentWillMount
是 React 在获取组件类并将其呈现到 DOM 的过程中使用的生命周期事件。在组件的初始渲染之前,componentWillMount
方法被执行一次。关于componentWillMount
的独特之处在于,如果你在这个函数中调用你的setState
函数,它不会导致组件的重新呈现,因为初始的render
方法将接收修改后的状态。
componentWillMount()
组件安装
componentDidMount
是一个函数,在组件被呈现到 DOM 之后,它只在 React 处理的客户端被调用。此时,React 组件已经成为 DOM 的一部分,您可以使用本章前面提到的React.findDOMNode
函数来访问它。
componentDidMount()
componentWillReceiveProps
从名字就可以看出,componentWillReceiveProps
是在组件接收属性时执行的。该函数在每次有适当的变化时被调用,但从不在第一次渲染时调用。你可以在这个函数中调用setState
,并且不会导致额外的渲染。您提供的函数将为下一个 props 对象提供一个参数,该对象将成为组件的 props 的一部分。在这个函数中,你仍然可以使用this.props
访问当前的属性,所以你可以在这个函数中对this.props
和nextProps
进行任何逻辑比较。
componentWillReceiveProps( nextProps )
shouldComponentUpdate
在组件渲染之前以及每次收到属性或状态的更改时,都会调用此函数。在初始渲染之前,或者在使用forceUpdate
时,它不会被调用。如果您知道对属性或状态的更改实际上不需要更新组件,则可以使用此函数来跳过渲染。为了缩短渲染过程,您需要根据您确定的任何标准在函数体中返回 false。这样做将绕过组件的呈现,不仅跳过了render()
函数,还跳过了生命周期中的下一步— componentWillUpdate
和componentDidUpdate
shouldComponentUpdate( nextProps, nextState );
组件将更新
在组件呈现之前调用componentWillUpdate
。在此功能中不能使用setState
。
componentWillUpdate( nextProps, nextState )
componentDidUpdate
在所有渲染更新都被处理到 DOM 中之后,立即执行componentDidUpdate
。因为这是基于更新的,所以它不是组件初始渲染的一部分。该函数可用的参数是 previous props 和 previous state。
componentDidUpdate( prevProps, prevState );
组件将卸载
如前所述,当一个组件被呈现到 DOM 时,它被称为挂载。因此,这个函数componentWillUnmount
将在组件不再被挂载到 DOM 之前立即被调用。
componentWillUnmount()
既然您已经看到了创建 React 组件时可用的所有属性和生命周期方法,现在是时候来看看不同的生命周期是什么样子了。清单 2-14 显示了 React 在初始渲染期间的生命周期。
Listing 2-14. Lifecycle During Initial Render
var GenericComponent = React.createClass({
// Invoked first
getInitialProps: function() {
return {};
},
// Invoked Second
getInitialState: function() {
return {};
},
// Third
componentWillMount: function() {
},
// Render – Fourth
render: function() {
return ( <h1>Hello World!</h1> );
},
// Lastly
componentDidMount: function() {
}
});
清单 2-14 的可视化表示如图 2-1 所示,在这里你可以看到 React 组件在初始渲染时所遵循的过程。
图 2-1。
Function invocation order during the initial render of a React component
在状态改变时,React 也有特定的生活方式。这显示在清单 2-15 中。
Listing 2-15. Lifecycle During Change of State
var GenericComponent = React.createClass({
// First
shouldComponentUpdate: function() {
},
// Next
componentWillUpdate: function() {
},
// render
render: function() {
return ( <h1>Hello World!</h1> );
},
// Finally
componentDidUpdate: function() {
}
});
正如清单 2-15 向您展示了 React 在状态变化期间的代码生命周期一样,图 2-2 直观地展示了相同状态变化过程的生命周期。
图 2-2。
Component lifecycle that happens when the state changes on a component
清单 2-16 显示了一个代码示例,突出显示了在 React 组件中,随着属性的改变而处理的生命周期事件。
Listing 2-16. Component Lifecycle for Props Alteration
var GenericComponent = React.createClass({
// Invoked First
componentWillReceiveProps: function( nextProps ) {
},
// Second
shouldComponentUpdate: function( nextProps, nextState ) {
// if you want to prevent the component updating
// return false;
return true;
},
// Third
componentWillUpdate: function( nextProps, nextState ) {
},
// Render
render: function() {
return ( <h1> Hello World! </h1> );
},
// Finally
componendDidUpdate: function() {
}
});
清单 2-16 中的代码展示了 React 组件的属性变更过程。如图 2-3 所示。
图 2-3。
Lifecycle of a component when it has altered props
理解功能在组件生命周期中的位置很重要,但是同样重要的是要注意到render()
仍然是组件规范中唯一需要的功能。让我们再看一个例子,使用代码来查看 React 组件以及所有显示的规范方法。
React 元素
你可以使用 JSX 创建一个 React 元素,你将在下一章中看到细节,或者你可以使用React.createElement. React.createElement
创建一个和 JSX 一样的元素,因为这是 JSX 在转换成 JavaScript 后使用的,正如你在本章开始时看到的。但是,有一点需要注意,使用createElement
时支持的元素并不是所有 web 浏览器都支持的全套元素。清单 2-17 中显示了支持的 HTML 元素。
Listing 2-17. HTML Elements That Are Supported When Creating a ReactElement
a abbr address area article aside audio b base bdi bdo big blockquote body br button canvas caption cite code col colgroup data datalist dd del details dfn dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 head header hr html i iframe img input ins kbd keygen label legend li link main map mark menu menuitem meta meter nav noscript object ol optgroup option output p param picture pre progress q rp rt ruby s samp script section select small source span strong style sub summary sup table tbody td textarea tfoot th thead time title tr track u ul var video wbr
当然,除了这些元素之外,您还可以利用React.createElement
来创建一个名为ReactClass
的自定义组合,如果您希望满足某个特定的元素,它可以填补这个空白。对这些 HTML 元素的其他补充是支持的 HTML 属性,如清单 2-18 所示。
Listing 2-18. HTML Attributes Available To Be Used When Creating React Elements
accept acceptCharset accessKey action allowFullScreen allowTransparency alt
async autoComplete autoFocus autoPlay cellPadding cellSpacing charSet checked classID className colSpan cols content contentEditable contextMenu controls coords crossOrigin data dateTime defer dir disabled download draggable encType form formAction formEncType formMethod formNoValidate formTarget frameBorder headers height hidden high href hrefLang htmlFor httpEquiv icon id label lang list loop low manifest marginHeight marginWidth max maxLength media mediaGroup method min multiple muted name noValidate open optimum pattern placeholder poster preload radioGroup readOnly rel required role rowSpan rows sandbox scope scoped scrolling seamless selected shape size sizes span spellCheck src srcDoc srcSet start step style tabIndex target title type useMap value width wmode data-* aria-*
React 工厂
正如你在本章开始时看到的,这基本上是你创建 React 元素的另一种方式。因此,它能够呈现所有前面的部分、HTML 标记、HTML 属性以及定制的ReactClass
元素。需要工厂的一个基本例子是在没有 JSX 的情况下实现一个元素。
// button element module
class Button {
// class stuff
}
module.exports = Button;
// using the button element
var Button = React.``createFactory``(require(``'Button'
class App {
render() {
return``Button
}
}
工厂的主要用例是当你决定不像前面的例子那样使用 JSX 编写应用时。这是因为当您利用 JSX 创建一个ReactClass
时,transpiler 进程将创建正确呈现元素所需的必要工厂。因此,JSX 版本,包括与工厂的前一个代码等效的代码,看起来如下。
var Button = require('Button');
class App {
render() {
return <Button prop="foo" />; // ReactElement
}
}
摘要
本章涵盖了 React 的核心。您了解了 React API,包括如何使用React.createClass
创建组件。您还学习了在使用 ES6 工具构建应用时如何使用React.Component
类。其他 API 方法是所有的React.Children
实用程序、React.DOM
、React.findDOMNode
和React.render
。
在对 React 核心进行了初步介绍之后,您了解了实现 React 组件的细节,包括它们可以包含哪些属性和特性,以及生命周期功能和呈现过程中的各种差异。
最后,你会读到更多关于ReactElements
和工厂的细节,这样你就能在下一章深入了解 JSX。一旦你了解了 JSX,你将能够一步一步地创建一个 React 应用,并把所有这些信息放在一起。
三、JSX 基础
Electronic supplementary material The online version of this chapter (doi:10.1007/978-1-4842-1245-5_3) contains supplementary material, which is available to authorized users.
在第一章中,您了解了为什么应该使用 React,以及 React 带来的好处。第二章展示了 React 的重要核心功能,以及如何利用 React 的内部 API—ReactElements
和ReactComponents
—来理解如何构建一个功能强大的 React 应用。
在这些章节的每一章中,JSX 都被展示或至少被提及。JSX 并不需要使用 React,但是它可以使组件的创作更加容易。JSX 允许您创建 JavaScript 对象,这些对象要么是 DOM 元素,要么是来自类似 XML 的语法的ReactComponents
。本章将展示 JSX 能做什么,以及如何在 React 应用中利用它。
为什么使用 JSX 而不是传统的 JavaScript?
你现在明白了,JSX 并不需要编写好的 React 代码。然而,这是一种普遍接受的写 React 的方式。之所以如此,正是因为这个或这些原因,你可能想用 JSX 而不是普通的 JavaScript 来编写你的 React 代码。
首先,JSX 有着开发人员和设计人员喜欢的熟悉的外观和感觉。它的结构类似于 XML,或者更恰当地说,类似于 HTML。这意味着您可以像构建 HTML 一样构建 React 代码。这不是一个新概念。能够从 JavaScript 呈现应用,同时保持类似 HTML 的文档的布局和结构,这是当今 web 上几乎所有模板语言的基础。
第二,JSX 仍然是 JavaScript。可能看起来不是这样,但是 JSX 是通过 JSX 编译器编译的,无论是在构建时还是在浏览器开发期间,它都将一切转换为可维护的 React JavaScript。JSX 带来的是一种不那么冗长的方式来做你无论如何都要写的相同的 JavaScript。
最后,引用脸书页面为 JSX 起草的官方规范作为 ECMAScript 的扩展,它定义了这种规范的目的:
... is to define a concise and familiar syntax to define a tree structure with attributes. A universal but well-defined grammar enables an independent parser and grammar highlighting community to conform to a single specification. Embedding a new grammar into an existing language is a risk. Other grammar implementers or existing languages may introduce another incompatible grammar extension. Through an independent specification, we make it easier for other implementers of grammar extension to consider JSX when designing their own grammar. This will hopefully allow various new grammar extensions to coexist. Our aim is to require minimum grammar space while keeping grammar concise and familiar. This opens the door for other expansion. This specification does not attempt to conform to any XML or HTML specification. JSX is designed as an ECMAScript feature, and the similarity with XML is just for familiarity.
https://facebook.github.io/jsx/
需要注意的是,脸书并不打算在 ECMAScript 本身中实现 JSX,而是利用这个文档来提议将 JSX 作为一个扩展。但是调用 ECMAScript 主体会导致一个问题,即您是否应该接受已经在 ES6 中实现的模板文字。这是一个有效的论点,但是如果你检查下面的例子,清单 3-1 和 3-2 ,你会看到简洁的 JSX 语法的好处。
Listing 3-1. Template Literals
`var box = jsx``
<${Box}>
${
shouldShowAnswer(user) ?
jsx
<\({Answer} value=\){false}>no</${Answer}> :
`jsx``
<${Box.Comment}>
Text Content
</${Box.Comment}>
`}``</${Box}>```;`Listing 3-2\. JSX`var box =``<Box>``{``shouldShowAnswer(user) ?``<Answer value={false}>no</Answer> :``<Box.Comment>``Text Content``</Box.Comment>``}``</Box>;`模板文字表明,如果脸书想要利用这个特性将他们的模板构建到 React 中,这种可能性是存在的。然而,很明显,这种语法更加冗长,而且似乎带有额外的特征,如反勾号、美元符号和花括号。将此语法与 JSX 版本进行比较。熟悉 XML 语法和层次结构的任何人都可以立即阅读和访问 JSX 版本。脸书认为,如果他们走模板文字这条路,那么现有的和正在创建的用于处理模板文字的工具将需要更新,包括模板文字的 ECMAScript 定义。这是因为 JSX 将变得与 ECMAScript 标准和处理紧密结合,使这个简单的扩展成为更大的语言定义的一部分,因此 JSX 是它自己的实体。你看到了 JSX 带来的好处。它写起来更简洁,但仍然有助于转换成支持 React 应用所必需的健壮的 JavaScript。一般来说,熟悉 HTML 语法的人也更容易理解它。这不仅是您的主要 JavaScript 开发人员,而且您项目的设计人员(如果您以这种方式划分角色)也可以使用 JSX 来构建 React 组件的输出。## 使用 JSX 变压器在前一章中,您简要地学习了如何在浏览器或文本编辑器中启动和运行 React。这一节将提供更多关于如何着手建立一个可以利用 JSX 转换器的开发环境的细节。可以简单地通过 [`https://facebook.github.io/react/docs/getting-started.html`](https://facebook.github.io/react/docs/getting-started.html) 访问 React 网站,并转到他们的“React JSFiddle”链接。这将允许您在浏览器中使用 React 和 JSX。如果您不想建立一个成熟的开发环境,这是一个很好的方法来学习本书中的例子。另一种方法是将 React 和 JSX 转换器脚本集成到一个 HTML 文件中,您将利用该文件来开发 React 应用。一种方法是从 React 网站下载 React 和 JSX 变压器,或者从脸书 CDN 链接。这种方法非常适合开发,但是由于包含它会在客户端产生额外的脚本和处理,建议您在进入生产环境之前预编译您的 JSX。`<script src="``https://fb.me/react-0.13.2.js"></script``<script src="``https://fb.me/JSXTransformer-0.13.2.js"></script`当然,还有其他方法来建立一个环境。你需要在你的机器上安装`Node.js`和`npm`。首先,要获得 JSX 工具,你只需要安装来自`npm`的`react-tools`包。`npm install –g react-tools`然后,您可以观察任何填充有`*.jsx`文件的目录,并将它们输出到编译后的 JavaScript 目录,如下所示。这将查看`src`目录并输出到`build`目录。`jsx --watch src/ build/`另一种方式,也允许 ES6 模块和组件的简单传输,是利用几个`npm`包和一个`Node.js`实用程序来动态观察变化和传输。为了开始这个设置,您需要通过`npm`安装一些全局工具。首先是 browserify,这是一个允许你在浏览器环境中使用 CommonJS `require("module")`语法的工具,不在`Node.js`之内。除了 browserify 之外,您还可以利用 watchify 来查看目录或文件,并使用一个名为 babel 的模块对其进行转换。Babel 是一个模块,它可以转换你用 ES6 写的任何东西,并使它与 ECMAScript 第 5 版(ES5)语法兼容。Babel 还会将 JSX 转换成合适的 JavaScript。下面的代码概述了完成此任务所需的命令。`# first you need to globally acquire browserify, watchify, and babel``npm install –g browserify watchify babel``# next in the directory in which you wish to develop your application``npm install react browserify watchify babelify --save-dev`现在您已经有了工具链,它将允许您创建您的`.jsx`文件,并在浏览器中将它们呈现为完整的 JavaScript 和 React 组件。为此,您可以创建一个`src`和一个`dist`目录,其中`src`目录将保存您的 JSX 文件。对于这个例子,假设有一个名为`app.jsx`的文件。这将编译成一个位于`dist`文件夹中的`bundle.js`文件。要指示 watchify 和 babelify 将 JSX 文件转换成`bundle.js`,命令应该如下。`watchify –t babelify ./src/app.jsx –o ./dist/bundle.js –v`这里您调用 watchify,它将监视一个文件并使用 babel (babelify)转换来转换(`-t`)该文件。下一个参数是要转换的源文件,后跟输出(`-o`)文件和目标目录和文件(`bundle.js`)。`–v`标志表示冗长,因此每次输入文件(`app.jsx`)被更改时,您将在控制台中看到转换的输出。这将看起来像下面这样:`624227 bytes written to ./dist/bundle.js (0.29 seconds)``624227 bytes written to ./dist/bundle.js (0.18 seconds)``624183 bytes written to ./dist/bundle.js (0.32 seconds)`然后,您可以创建一个包含对`bundle.js`的引用的 HTML 文件,该文件将包含 React 的 JavaScript 和转换后的 JSX。`<!DOCTYPE html>``<html lang="en">``<head>``<meta charset="UTF-8">``<title>Introduction to React<title>``</head>``<body>``<div id="container"></div>``<script src="dist/bundle.js"></script>``</body>``</html>`Note使用 babelify 将您的 JSX 转换成 JavaScript 并不是唯一的方法。事实上,还有许多其他解决方案可供您使用,包括但不限于 gulp-jsx ( [`https://github.com/alexmingoia/gulp-jsx`](https://github.com/alexmingoia/gulp-jsx) )和 react fy([`https://github.com/andreypopp/reactify`](https://github.com/andreypopp/reactify))。如果本节概述的方法不适合您的工作流,您应该能够找到适合您的工作流的工具。## JSX 如何将类似 XML 的语法转换成有效的 JavaScript对于 JSX 如何能够采用类似 XML 的语法并将其转换为用于生成 React 元素和组件的 JavaScript,最基本的解释是,它只是扫描类似 XML 的结构,并用 JavaScript 中所需的函数替换标签。您将看到几个示例,展示如何将 JSX 的一个片段转换成适当的 JavaScript,以便在 React 应用中使用。清单 3-3 显示了一个简单的“Hello World”应用。Listing 3-3\. Original JSX Version of a Simple Hello World Application`var Hello = React.createClass({``render: function() {``return <div>Hello {this.props.name}</div>;``}``});``React.render(<Hello name="World" />, document.getElementById('container'));`在这个例子中,您可以看到 JSX 创建了一个`div`元素,该元素还保存了对`this.props.name`属性的引用。这些都包含在一个叫做`Hello`的组件中。稍后在应用中,您调用`Hello`组件上的`React.render()`,将`"World"`的值传递给`name`属性,该属性将成为`this.props.name`。清单 3-4 展示了在 React JSX 转换器将这种类似 XML 的语法转换成 JavaScript 之后,它会转换成什么。Listing 3-4\. Post-JSX Transformation of the Hello World Example`var Hello = React.createClass({displayName: "Hello",``render: function() {``return React.createElement("div", null, "Hello ", this.props.name);``}``});``React.render(React.createElement(Hello, {name: "World"}), document.getElementById('container'));`首先,请注意,在这个简单的例子中,这两个例子与一个可能比另一个更受青睐的方式没有什么不同。后 JSX 时代的源阅读方式多了一点冗长。需要指出的是,有一个自动注入的`displayName`。只有当您在创建 React 组件时还没有在您的`ReactComponent`规范上设置这个属性时,才会发生这种情况。它通过检查组件对象的每个属性并检查`displayName`的值是否存在来实现这一点。如果没有,它会将`displayName`附加到组件上,正如您在示例中看到的。这个组件的另一个值得注意的地方是 JSX 语法如何被分解成一个`ReactElement`的实际结构。这就是 JSX 转换器采用类似 XML 的结构的地方`<div>Hello {this.props.name}</div>`并把它变成一个 JavaScript 函数:`React.createElement("div", null, "Hello ", this.props.name);`在前一章中,`React.createElement`的细节表明它至少接受一个参数、一个类型和几个可选参数。在这种情况下,类型是一个`div`,一个`null`对象指定了对象的任何属性。那么孩子的`"Hello"`文本就会变成`div`元素的`innerHTML`。属性,具体来说就是`this.props.name`,也是通过的。这都是在 JSX 转换器代码中处理的,目标是构建一个表示元素功能 JavaScript 的字符串。它的源代码读起来很有趣,所以如果您愿意通读 transformer 源代码,就可以对转换的工作有所了解。转换的主要思想是考虑到一切,并智能地编译表示 JSX 组件的 JavaScript 版本的字符串。这个例子的另一个被转换的部分是最终的渲染,它来自于:`<Hello name="World" />`对此:`React.createElement(Hello, {name: "World"})`对您来说,这似乎与发生在您的`div`元素上的转换相同,但是不同之处在于,在这种情况下,有一个非空的 property 对象表示`name`属性。这就是来自`Hello`组件的`this.props.name`的来源。当调用`React.render()`时,它是元素上的一个直接属性。您会看到有一个逻辑过程,其中这些 JSX 元素被解析,然后被重新构建成有效的 JavaScript,React 可以利用它将组件安装到页面上。这只是一个微不足道的例子。接下来,您将看到当您开始将 JSX 元素嵌套在一起时会发生什么。为了展示嵌套的自定义`ReactComponents`是如何从 JSX 转换到 JavaScript 的,现在您将在清单 3-5 中看到一个同样的“Hello World”问候的复杂示例。Listing 3-5\. Hello World Greeting`var GreetingComponent = React.createClass({``render: function() {``return <div>Hello {this.props.name}</div>;``}``});``var GenericComponent = React.createClass({``render: function() {``return <GreetingComponent name={this.props.name} />;``}``});``React.render(<GenericComponent name="World" />, document.getElementById('container'));`在这里你可以看到一个`GenericComponent`,它只是一个容器`div`,用来容纳另一个组件`GreetingComponent`。通过调用一个属性来呈现`GenericComponent`,类似于前面例子中的`Hello`组件。然而,这里有一个第二层`ReactComponent`,它将`this.props.name`作为属性传递给它的子元素。自然,你不会想在现实世界中制作这样的界面,但是你也不会从事制作组件的工作。你可以假设这只是一个展示 JSX 如何传输嵌套组件的例子。JSX 变换的结果如清单 3-6 所示。Listing 3-6\. JSX Transform of GenericComponent`var GreetingComponent = React.createClass({displayName: "GreetingComponent",``render: function() {``return React.createElement("div", null, "Hello ", this.props.name);``}``});``var GenericComponent = React.createClass({displayName: "GenericComponent",``render: function() {``return React.createElement(GreetingComponent, {name: this.props.name});``}``});``React.render(React.createElement(GenericComponent, {name: "World"}), document.getElementById('container'));`乍一看,这与无关紧要的 Hello World 示例的转换没有太大的不同,但是经过更仔细的研究,您会发现这里有更多的东西可以帮助您更好地理解 JSX。对于这个例子,您可以从检查`React.render()`函数调用开始,它转换如下内容:`<GenericComponent name="World" />`变成这样:`React.createElement(GenericComponent, {name: "World"})`这正是在 Hello World 示例中创建`Hello`组件时发生的事情,其中`name`属性被转换成一个要传递给`GenericComponent`的属性。这种分歧出现在下一个层次,在这个层次中,`GenericComponent`被创建并引用`GreetingComponent`,从而将`name`属性直接传递给`GreetingComponent`。`<GreetingComponent name={this.props.name} />`这展示了如何处理一个属性并将其传递给子元素。从`GenericComponent`的顶级属性开始,您可以使用`this.props`将该属性传递给子元素`GreetingComponent`。看到`GreetingComponent`被创建也很重要,就像任何其他的`ReactComponent`或 HTML 标签一样,在 React 组件结构中嵌套组件和嵌套 HTML 标签没有什么本质上的特殊。与普通的 HTML 标签相比,关于`ReactComponents`需要注意的一点是,按照惯例,React 使用大写字母作为组件名称的开头,小写字母作为 HTML 标签的开头。有时候你需要一个设计更巧妙的界面,即使是最简单的表单也是以结构化的方式构建的。在 JSX 中,您可以通过创建每个组件,然后根据添加到作用域中的变量的嵌套来构建层次结构,例如当您创建一个带有嵌套标签和输入的表单时。虽然这种方法工作得很好,但是它确实有一些限制,并且在为属于同一分组的项目创建单独的组件变量名时可能没有必要。在这种情况下,表单是作为父表单`FormComponent`的一部分构建的。好消息是,React 知道这是不必要的,并允许您创作与父组件同名的组件。在清单 3-7 中,您将看到一个`FormComponent`名称空间的创建,其中嵌套了多个相关组件。然后利用组件别名对其进行渲染,然后为渲染对组件命名空间。Listing 3-7\. Creating FormComponent`var React = require("react");``var FormComponent = React.createClass({``render: function() {``return <form>{this.props.children}</form>;``}``});``FormComponent.Row = React.createClass({``render: function() {``return <fieldset>{this.props.children}</fieldset>;``}``});``FormComponent.Label = React.createClass({``render: function() {``return <label htmlFor={this.props.for}>{this.props.text}{this.props.children}</label>;``}``});``FormComponent.Input = React.createClass({``render: function() {``return <input type={this.props.type} id={this.props.id} />;``}``});``var Form = FormComponent;``var App = (``<Form>``<Form.Row>``<Form.Label text="label" for="txt">``<Form.Input id="txt" type="text" />``</Form.Label>``</Form.Row>``<Form.Row>``<Form.Label text="label" for="chx">``<Form.Input id="chx" type="checkbox" />``</Form.Label>``</Form.Row>``</Form>``);``React.render(App, document.getElementById("container"));`一旦 JSX 被转换成 JavaScript,就会得到清单 3-8 中所示的例子。Listing 3-8\. JSX Transformed for the FormComponent`var React = require("react");``var FormComponent = React.createClass({``displayName: "FormComponent",``render: function render() {``return React.createElement(``"form",``null,``this.props.children``);``}``});``FormComponent.Row = React.createClass({``displayName: "Row",``render: function render() {``return React.createElement(``"fieldset",``null,``this.props.children``);``}``});``FormComponent.Label = React.createClass({``displayName: "Label",``render: function render() {``return React.createElement(``"label",``{ htmlFor: this.props["for"] },``this.props.text,``this.props.children``);``}``});``FormComponent.Input = React.createClass({``displayName: "Input",``render: function render() {``return React.createElement("input", { type: this.props.type, id: this.props.id });``}``});``var Form = FormComponent;``var App = React.createElement(``Form,``null,``React.createElement(``Form.Row,``null,``React.createElement(``Form.Label,``{ text: "label", "for": "txt" },``React.createElement(Form.Input, { id: "txt", type: "text" })``)``),``React.createElement(``Form.Row,``null,``React.createElement(``Form.Label,``{ text: "label", "for": "chx" },``React.createElement(Form.Input, { id: "chx", type: "checkbox" })``)``)``);``React.render(App, document.getElementById("container"));`从这个例子中可以学到很多东西,不仅仅是嵌套和命名空间,还有 React 如何传递`this.props.children`中的 children 元素。需要注意的是,当您处理嵌套元素时,您需要在前一个元素的 JSX 中保存对它们的引用。如果您要创建一个如下所示的`FormComponent`元素,它将永远不会包含嵌套的子元素。`var FormComponent = React.createComponent({``render: function() {``return <form></form>;``}``});`在本例中,即使您已经将呈现设置为如下例所示,它仍然只返回表单,因为没有对元素的子元素的引用。`<FormComponent>``<FormRow />``</FormComponent>`正如您在正确的示例中看到的,有一种简单的方法可以使用`this.props.children`让这些元素正确嵌套:`var FormComponent = React.createClass({``render: function() {``return <form>``{this.props.children}``}``});`一旦您有能力传递子组件,您就可以像清单 3-9 所示那样构建您的 React 应用组件。所有的嵌套都将如您所期望的那样工作。Listing 3-9\. Passing Children`var App = (``<Form>``<Form.Row>``<Form.Label text="label" for="txt">``<Form.Input id="txt" type="text" />``</Form.Label>``</Form.Row>``<Form.Row>``<Form.Label text="label" for="chx">``<Form.Input id="chx" type="checkbox" />``</Form.Label>``</Form.Row>``</Form>``);`## JSX 的传播属性和其他注意事项到目前为止,您可能已经意识到 JSX 本质上是 React 和编写 React 组件的定制模板引擎。它使编写和构建应用变得更加容易,并允许团队的所有成员更容易访问用户界面的代码。本节概述了在 React 中使用 JSX 时的一些模板注意事项以及其他一些特殊特征。扩展属性是一个源自 ES6 阵列和 ES7 规范早期工作的概念。它们在 React 的 JSX 代码中扮演了一个有趣的角色,因为它们允许您添加最初编写组件时可能不知道的属性。对于本例,假设您的普通 Hello World 应用接受参数名,现在需要在问候语后添加一条自定义消息。在这种情况下,您可以添加另一个命名参数`message`,它将像`name`属性一样被使用,或者您可以利用 spread 属性并创建一个包含`name`和`message`的 greeting 对象。清单 3-10 显示了这在实践中的样子。Listing 3-10\. Using Spread Attributes`var greeting = {``name: "World",``message: "all your base are belong to us"``};``var Hello = React.createClass({``render: function() {``return <div>Hello {this.props.name}, {this.props.greeting}</div>;``}``});``React.render(<Hello {...greeting} />, document.getElementById("container"));`您可以看到,您现在使用了三个点和一个对象的名称来表示扩展属性,而不是在`render`函数中的组件上命名的属性。则附加到该对象的每个属性都可以在组件中访问。`this.props.name`和`this.props.greeting`正在 JSX 组件中使用。清单 3-11 显示了同一应用的另一个版本。这次请注意,它是在 ES6 中创作的,来自 JSX 组件的 JavaScript 输出略有不同。它比使用`React.createClass`创作的组件更加冗长。Listing 3-11\. Spread Attributes with ES6`var greeting = {}``greeting.name = "World";``greeting.message = "All your base are belong to us.";``class Hello extends React.Component {``render() {``return (``<div>Hello {this.props.name}, {this.props.message}</div>``);``}``}``React.render(<Hello {...greeting} />, document.getElementById("container"));`你可以看到,用 JSX 创建一个简单组件的方式没有太大的不同。清单 3-12 显示了来自 ES6 模块的 JSX 变换的实际结果看起来略有不同。Listing 3-12\. Transform of the Component`var greeting = {};``greeting.name = "World";``greeting.message = "All your base are belong to us.";``var Hello = (function (_React$Component) {``function Hello() {``_classCallCheck(this, Hello);``if (_React$Component != null) {``_React$Component.apply(this, arguments);``}``}``_inherits(Hello, _React$Component);``_createClass(Hello, [{``key: "render",``value: function render() {``return React.createElement(``"div",``null,``"Hello ",``this.props.name,``", ",``this.props.message``);``}``}]);``return Hello;``})(React.Component);``React.render(React.createElement(Hello, greeting), document.getElementById("container"));`关于传播属性的渲染方式,您可能会注意到令人印象深刻的一点是,转换后的组件中没有内置任何额外的功能来指示属性来自传播属性。对于每个使用 React 的人来说,这样做的好处可能不是很大,但是您可以看到,这样可以更简洁地向组件添加多个属性,而不是在 JSX 中将每个属性指定为自己的属性。如果您看一下前面的表单示例,其中每个`HTMLfor`、`id`和`input`类型都是显式声明的。通过利用 spread 属性,您可以看到,如果输入数据来自 API 或 JSON 对象,它可以很容易地被组合到组件中。这显示在清单 3-13 中。Listing 3-13\. Input Types and Spread Attributes`var input1 = {``"type": "text",``"text": "label",``"id": "txt"``};``var input2 = {``"type": "checkbox",``"text": "label",``"id": "chx"``};``var Form = FormComponent;``var App = (``<Form>``<Form.Row>``<Form.Label {...input1} >``<Form.Input {...input1} />``</Form.Label>``</Form.Row>``<Form.Row>``<Form.Label {...input2}>``<Form.Input {...input2} />``</Form.Label>``</Form.Row>``</Form>``);`在使用 JSX 构建 React 应用时,您可能会遇到的一个特殊用例是,如果您想要向组件添加一些逻辑。这类似于 JSX 中的`if-else`或`for`循环。当呈现像`for`循环这样的项目时,你只需要记住你可以在你的组件的`render`函数中写 JavaScript。清单 3-14 中所示的简单循环示例遍历一个数组,并将列表项添加到一个无序列表中。一旦你意识到你不需要学习任何技巧,渲染就很容易了。Listing 3-14\. Looping in JSX`class ListItem extends React.Component {``render() {``return <li>{this.props.text}</li>;``}``}``class BigList extends React.Component {``render() {``var items = [ "item1", "item2", "item3", "item4" ];``var formattedItems = [];``for (var i = 0, ii = items.length; i < ii; i++ ) {``var textObj = { text: items[i] };``formattedItems.push(<ListItem {...textObj} />);``}``return <ul>{formattedItems}</ul>;``}``}``React.render(<BigList />, document.getElementById("container"));`这个 JSX 接受格式化项的数组,该数组调用`ListItem`组件,并将 spread 属性对象传递给该组件。然后这些被添加到通过`render`函数返回的无序列表中。改造后的 JSX 看起来就像你期望的那样,包括创作时的`for`循环。如清单 3-15 所示。Listing 3-15\. Transformed JSX for BigList`var ListItem = (function (_React$Component) {``function ListItem() {``_classCallCheck(this, ListItem);``if (_React$Component != null) {``_React$Component.apply(this, arguments);``}``}``_inherits(ListItem, _React$Component);``_createClass(ListItem, [{``key: "render",``value: function render() {``return React.createElement(``"li",``null,``this.props.text``);``}``}]);``return ListItem;``})(React.Component);``var BigList = (function (_React$Component2) {``function BigList() {``_classCallCheck(this, BigList);``if (_React$Component2 != null) {``_React$Component2.apply(this, arguments);``}``}``_inherits(BigList, _React$Component2);``_createClass(BigList, [{``key: "render",``value: function render() {``var items = ["item1", "item2", "item3", "item4"];``var formattedItems = [];``for (var i = 0, ii = items.length; i < ii; i++) {``var textObj = { text: items[i] };``formattedItems.push(React.createElement(ListItem, textObj));``}``return React.createElement(``"ul",``null,``formattedItems``);``}``}]);``return BigList;``})(React.Component);``React.render(React.createElement(BigList, null), document.getElementById("container"));`使用模板语言的另一个常见任务是`if-else`语句。在 React 中,这种条件可能以几种方式发生。首先,正如您可能会想到的,您可以在组件的 JavaScript 内处理应用逻辑中的`if`条件,就像您在前面的`for`循环中看到的那样。这将看起来像清单 3-16 ,如果用户没有登录,他们将得到一个“登录”按钮;否则,他们会得到用户的菜单。Listing 3-16\. Using Conditionals in JSX`var SignIn = React.createClass({``render: function() {``return <a href="/signin">Sign In</a>;``}``});``var UserMenu = React.createClass({``render: function() {``return <ul className="usermenu"><li>Item</li><li>Another</li></ul>;``}``});``var userIsSignedIn = false;``var MainApp = React.createClass({``render: function() {``var navElement;``if (userIsSignedIn) {``navElement = <UserMenu />;``} else {``navElement = <SignIn />;``}``return <div>{navElement}</div>;``}``});``React.render(<MainApp />, document.getElementById("container"));`这个例子一旦被转换成适当的 JavaScript,就会如清单 3-17 所示。Listing 3-17\. Transformed Conditional`var SignIn = React.createClass({``displayName: "SignIn",``render: function render() {``return React.createElement(``"a",``{ href: "/signin" },``"Sign In"``);``}``});``var UserMenu = React.createClass({``displayName: "UserMenu",``render: function render() {``return React.createElement(``"ul",``{ className: "usermenu" },``React.createElement(``"li",``null,``"Item"``),``React.createElement(``"li",``null,``"Another"``)``);``}``});``var userIsSignedIn = false;``var MainApp = React.createClass({``displayName: "MainApp",``render: function render() {``var navElement;``if (userIsSignedIn) {``navElement = React.createElement(UserMenu, null);``} else {``navElement = React.createElement(SignIn, null);``}``return React.createElement(``"div",``null,``navElement``);``}``});``React.render(React.createElement(MainApp, null), document.getElementById("container"));`总之,您可以使用 JavaScript 来操作您的组件。然而,如果您想将逻辑更紧密地嵌入到组件中,也可以通过在代码中使用三元运算符来实现,如清单 3-18 所示。Listing 3-18\. Ternary Operators in JSX`var SignIn = React.createClass({``render: function() {``return <a href="/signin">Sign In</a>;``}``});``var UserMenu = React.createClass({``render: function() {``return <ul className="usermenu"><li>Item</li><li>Another</li></ul>;``}``});``var userIsSignedIn = true;``var MainApp = React.createClass({``render: function() {``return <div>{ userIsSignedIn ? <UserMenu /> : <SignIn /> }</div>;``}``});``React.render(<MainApp />, document.getElementById("container"));`JSX 变换后的 JavaScript 如清单 3-19 所示。Listing 3-19\. Ternaries Transformed`var SignIn = React.createClass({``displayName: "SignIn",``render: function render() {``return React.createElement(``"a",``{ href: "/signin" },``"Sign In"``);``}``});``var UserMenu = React.createClass({``displayName: "UserMenu",``render: function render() {``return React.createElement(``"ul",``{ className: "usermenu" },``React.createElement(``"li",``null,``"Item"``),``React.createElement(``"li",``null,``"Another"``)``);``}``});``var userIsSignedIn = true;``var MainApp = React.createClass({``displayName: "MainApp",``render: function render() {``return React.createElement(``"div",``null,``userIsSignedIn ? React.createElement(UserMenu, null) : React.createElement(SignIn, null)``);``}``});``React.render(React.createElement(MainApp, null), document.getElementById("container"));`## 摘要在这一章中,你看到了 JSX 的行动。您了解了 JSX 如何将许多人熟悉的类似 XML 的语法转换成 React 在创建组件和构建应用时使用的 JavaScript。您还看到了如何在构建应用时将 JSX 整合到您的工作流中,或者在您刚刚开发和学习 React 时利用许多工具来整合 JSX。最后,您不仅看到了它是如何工作的,还看到了几个如何利用 JSX 在 React 应用中构建逻辑模板和嵌套元素的例子。所有这些将有助于您理解下一章发生的事情,届时您将从线框化到最终产品完成完整 React 应用的创建。# 四、构建 React Web 应用Electronic supplementary material The online version of this chapter (doi:[10.1007/978-1-4842-1245-5_4](http://dx.doi.org/10.1007/978-1-4842-1245-5_4)) contains supplementary material, which is available to authorized users.在前三章中,您已经获得了关于 React 的大量信息。从 React 是什么以及它与其他 JavaScript 和用户界面框架有何不同开始,您就为理解 React 的工作方式打下了坚实的基础。从那里,您了解了 React 的核心概念和它的特性。引入了组件创建和渲染生命周期之类的东西。在最后一章中,你被介绍给了 React 世界中一个强大的租户,JSX。通过 JSX,您看到了与普通的 JavaScript 实现相比,如何以一种更易接近、更易维护的方式简洁地创建 React 组件。本章将展示如何通过考虑一个非 React 应用并将其分解成你需要的组件来构建一个 React 应用。然后,你就可以将它拆分到 React 应用中,你会看到 React 甚至可以为一个规模不及脸书或 Instagram 的应用带来的价值。## 概述应用的基本功能有几种方法可以概述应用的基本功能,这些功能将被转移到 React 应用中。一种方法是用线框设计。如果您没有一个活动的 web 应用,而是考虑用 React 从头开始创建应用结构,这将非常有用。这个线框化过程对于任何应用来说显然都是重要的,但是对于确定应该在哪里将应用拆分成不同的组件有很大的帮助。在开始绘制线框之前,您需要一个应用的概念。我创建了一个锻炼日记/日志,在那里我可以存储各种锻炼并查看我的努力历史。这类项目是不同框架如何一起工作并集成到工作流中的一个很好的例子。您的示例应用可能有所不同,但出于本书的目的,您将遵循锻炼应用主题。接下来是头脑风暴和构建应用的思考过程。现在,您对自己的应用有了一个想法。您需要确定代表该应用全貌的主要功能领域。对于这个锻炼应用,您需要一种方法让用户通过应用的身份验证,因为每个用户都希望记录自己的锻炼数据。一旦用户通过身份验证,还应该有一个页面或表单,用户可以定义和分类他们将记录的锻炼。这将是一些允许定义的名称和类型,如“时间”、“最大重量”、“重复次数”。这些不同的类型将在下一部分发挥作用,允许用户存储他们的锻炼。当他们存储健身程序且类型与时间相关时,您可以选择使用特定的表格字段来记录完成工作所需的时间。最大重量和重复次数的类似特定字段也是可用的,但仅针对特定工作类型显示。这种类型的特殊性允许用户在应用的历史记录部分对他们的锻炼进行不同的分类。也许他们甚至可以随着时间的推移为每次锻炼规划不同的努力。现在你所拥有的是一个基本的、散文式的应用功能概述。现在,您可能在 React 思维中看到了这一点,但是您需要进入下一步,将该应用视为一个线框。## 从组件的角度思考基于上一节中创建的大纲,您现在将遇到如何构建应用的两个场景。如前所述,一种方法是创建遵循应用轮廓的线框。这给了您一个新的开始,以确定在哪里可以创建适合新 React 应用的组件。另一种方法是将结构建立在现有应用及其源代码的基础上,以便将功能分解成组件。您将首先看到应用的一组线框,然后您将看到一个现有应用的示例,该应用需要重写为 React 应用。### 线框创建线框时,您可以选择使用餐巾背面、MS Paint 或任何数量的工具来帮助您以描述体验的图像表达您的想法。下面是我决定分成 React 组件的应用部分。所有组件的根是应用,它将是以下所有嵌套组件的父组件。如果您选择不使用线框,而是更喜欢使用现有的代码来剖析您的应用,那么您可以浏览这一小节,并从“重写现有的应用”开始,来发现从组件的角度来思考的见解。图 4-1 所示的登录界面是一个简单的认证组件。这实际上是一个完整的组件。实际上,您可以选择将该组件作为两部分身份验证组件之一。![A978-1-4842-1245-5_4_Fig1_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-frontend-framework-zh/raw/master/docs/intro-react/img/A978-1-4842-1245-5_4_Fig1_HTML.jpg)图 4-1。Sign In component wireframe该登录组件可能不需要任何子组件,因为您可能会将该表单发送到您的身份验证服务器进行验证。可能构成该身份验证部分的另一个 React 组件是创建帐户屏幕。在创建帐户组件中,如图 4-2 所示,您可以看到还有一个简单的表单,就像登录表单一样。这里的区别在于您需要一个密码验证组件。这将确保您拥有的任何密码规则都得到实施,并且还会检查第二个密码字段以确保值匹配。在您的应用中,您还可以选择包含一个 reCAPTCHA 或其他组件,以确保创建帐户的人不是机器人。![A978-1-4842-1245-5_4_Fig2_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-frontend-framework-zh/raw/master/docs/intro-react/img/A978-1-4842-1245-5_4_Fig2_HTML.jpg)图 4-2。Account creation component wireframe与帐户创建组件中的密码验证子组件一起,您需要确保输入的用户名是惟一的,并且在您的系统中可用。因此,即使这个简单的形式也可以使用 React 分解成更多的原子组件。这将允许你维护一个特定的功能,并保持每个动作与应用的其他部分相分离(图 4-2 )。应用线框的下一部分是定义健身程序部分(图 4-3 )。这可以分为至少两个决定性的部分。一旦通过身份验证,应用的每个视图都将包含一个导航菜单。这个菜单本身就是一个组件,它将控制应用的哪个部分被呈现。除了导航菜单组件,还有健身程序定义组件。这将保存允许您存储新健身程序定义的表单,当您决定记录您已完成的健身程序时,您将能够返回该表单。这个表单也是您想要为 React 应用创建的一个组件。![A978-1-4842-1245-5_4_Fig3_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-frontend-framework-zh/raw/master/docs/intro-react/img/A978-1-4842-1245-5_4_Fig3_HTML.jpg)图 4-3。Define a Workout component wireframe在定义健身程序部分之后,下一部分(称为记录健身程序)将保留您在上一部分看到的相同导航组件(图 4-4 )。除了导航组件之外,还有一个表格,用于控制您要记录的锻炼以及您要记录的运动量。这可能是一个单一的组成部分,但你可能会发现创建一个下拉菜单的可用锻炼更好。![A978-1-4842-1245-5_4_Fig4_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-frontend-framework-zh/raw/master/docs/intro-react/img/A978-1-4842-1245-5_4_Fig4_HTML.jpg)图 4-4。Store workout component wireframe应用的最后一部分是“健身程序历史记录”部分(图 4-5 )。此部分保留了导航组件,并显示了一个表格,或一个列表视图,如果你选择,所有你的锻炼。这个表本身就是一个组件,所以请记住,在将来的版本中,您可能希望用一个子组件来扩展这个组件。这个子组件可以对历史进行搜索或排序,因此它应该具有处理该功能的可用属性。![A978-1-4842-1245-5_4_Fig5_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-frontend-framework-zh/raw/master/docs/intro-react/img/A978-1-4842-1245-5_4_Fig5_HTML.jpg)图 4-5。Workout History component wireframe### 重写现有的应用在本节中,您将看到一个可以使用 React 重写的现有应用。同样,第一步是确定在应用中何处可以创建组件或子组件,就像您在线框示例中看到的那样。第一部分是身份验证组件,由登录子组件和创建帐户组件组成。如果您研究清单 4-1 中显示基本 HTML 和 jQuery 应用的例子,您应该能够确定在哪里可以创建组件。Listing 4-1\. Basic Markup for Authentication in Your Existing Application`<div id="signInForm" class="notSignedIn">``<label for="username">Username:</label>``<input type="text" id="username">``<label for="password">Password:</label>``<input type="text" id="password">``<button id="signIn">Sign In</button>``</div>``<div id="createAccount" class="notSignedIn">``<label for="username">Username:</label>``<input type="text" id="username">``<label for="password">Password:</label>``<input type="text" id="password">``<label for="password">Confirm Password:</label>``<input type="text" id="confpassword">``<button id="signIn">Create Account</button>``</div>`使用 jQuery 的认证机制`$("#signIn").on("click", function() {``// do authentication``$(".notSignedIn").hide();``$(".signedIn").show();``});`你可以看到,这显然有两个部分。也许您可以想象创建一个如下所示的组件。`<Authentication>``<SignIn />``<CreateAccount />``</Authentication>`这正是将在下一节中创建的组件。当然,还有实际执行身份验证的功能,这是您必须考虑的,但从基本意义上来说,这就是组件的样子。下一部分是导航菜单,一旦认证完成,导航菜单将在整个应用中共享。`<ul id="navMenu">``<li><a href="#defineWorkouts">Define Workouts</a></li>``<li><a href="#logWorkout">Log Workout</a></li>``<li><a href="#viewHistory">View History</a></li>``<li><a href="#logout" id="logout">Logout</a></li>``</ul>`这个导航菜单将在 JSX 中被重写,这样它就可以在每个需要它的组件中被重用。应用的 jQuery/HTML 版本的下一部分是基本的可提交区域,它们从特定字段中获取值,并在单击时提交这些值。例如,“定义健身程序”部分类似于清单 4-2 。Listing 4-2\. Save a Workout Definition in HTML/jQuery`<div id="defineWorkouts" class="tabview">``<label for="defineName">Define Name</label>``<input type="text" id="defineName">``<label for="defineType">Define Type</label>``<input id="defineType" type="text">``<label for="defineDesc">Description</label>``<textarea id="defineDesc" ></textarea>``<button id="saveDefinition">Save Definition</button>``</div>`其他两个部分,记录锻炼和锻炼历史,遵循相同的形式,除了有一部分组件来自存储的锻炼(列表 4-3 和 4-4 )。Listing 4-3\. The Record a Workout Section—Different Workouts Are Available from Defined Workouts in #chooseWorkout and Pulled from a Data Store`<div id="logWorkout" class="tabview">``<label for="chooseWorkout">Workout:</label>``<select name="" id="chooseWorkout">``<!-- populated via script -->``</select>``<label for="workoutResult">Result:</label>``<!-- input based on the type of the workout chosen -->``<input id="workoutResult" type="text" />``<input id="workoutDate" type="date" />``<label for="notes">Notes:</label>``<textarea id="notes"></textarea>``</div>`Listing 4-4\. Workout History Based on All the Work Recorded and Pulled from a Data Store`<div id="viewHistory" class="tabview">``<!-- dynamically populated -->``<ul id="history">``</ul>``</div>`现在你可以看到,这些原子或亚原子的代码片段中的每一个都代表了生成用户界面组件的单一代码路径。这正是您想要的,以便将这些功能部分分割成它们自己的组件。这个例子是一个简单的锻炼日志应用。尝试检查您自己的源代码,并通过编目您需要创建哪些组件来为重写做准备。## 为您的应用创建必要的组件在前面的部分中,您检查了一个线框和一个现有的应用,以确定您希望将应用的哪些功能拆分为 React 组件,或者您至少可以直观地看到这样做有什么意义。在本节中,您将采取下一步,开始使用 React 代码隔离这些组件,以便开始构建您的应用。首先,您将创建授权组件。从线框或代码示例中可以看出,这个组件由两个子组件组成— `SignIn`和`CreateAccount`。如果您愿意,整个应用可以放在一个文件中,但是出于可维护性的考虑,谨慎的做法是将组件分离到它们自己的文件中,并利用 browserify 或 webpack 之类的工具将这些文件模块化。首先是`signin.jsx`档,其次是`createaccount.jsx`(列表 4-5 和 4-6 )。Listing 4-5\. The signin.jsx File`var React = require("react");``var SignIn = React.createClass({``render: function() {``return (``<div>``<label htmlFor="username">Username``<input type="text" id="username" />``</label>``<label htmlFor="password">Password``<input type="text" id="password" />``</label>``<button id="signIn" onClick={this.props.onAuthComplete.bind(null, this._doAuth)}>Sign In</button>``</div>``);``},``_doAuth: function() {``return true;``}``});``module.exports = SignIn;`Listing 4-6\. The createaccount.jsx File`var React = require("react");``var CreateAccount = React.createClass({``render: function() {``return (``<div>``<label htmlFor="username">Username:``<input type="text" id="username" />``</label>``<label htmlFor="password">Password:``<input type="text" id="password" />``</label>``<label htmlFor="password">Confirm Password:``<input type="text" id="confpassword" />``</label>``<button id="signIn" onClick={this.props.onAuthComplete.bind( null, this._createAccount)}>Create Account</button>``</div>``);``},``_createAccount: function() {``// do creation logic here``return true;``}``});``module.exports = CreateAccount;`这两个组件都很简单,JSX 标记看起来类似于上一节中在 jQuery 和 HTML 应用中创建的标记。不同的是,您不再看到使用 jQuery 绑定到按钮。在它的位置,有一个`onClick`绑定,然后调用对`this.props.onAuthComplete`的引用。这可能看起来很奇怪,但是一旦您看到父应用组件,它将指示如何通过每个子组件处理授权状态。清单 4-7 提供了一个简单的组件——`Authentication`——它包含两个子认证组件。这些子组件是可用的,因为在它们被定义的文件中,我们通过利用`module.exports. module.exports`导出了组件对象,这是一种 CommonJS 机制,允许您导出您定义的对象。一旦在后续模块中使用`require()`加载了该对象,您就可以访问它了。Listing 4-7\. The auth.jsx File`var React = require("react");``var SignIn = require("./signin.jsx");``var CreateAccount = require("./createaccount.jsx");``var Authentication = React.createClass({``render: function() {``return (``<div>``<SignIn onAuthComplete={this.props.onAuthComplete}/>``<CreateAccount onAuthComplete={this.props.onAuthComplete}/>``</div>``);``}``})``module.exports = Authentication;`现在您有了认证,它由两个子组件组成— `SignIn`和`CreateAccount`。从这里开始,您需要应用的下一个主要部分,这是在您对应用进行身份验证之后发生的所有事情。同样,这个过程将被分成适当的组件,每个组件都包含在自己的模块中(列表 4-8 )。Listing 4-8\. The navigation.jsx File`var React = require("react");``var Navigation = React.createClass({``render: function() {``return (``<ul>``<li><a href="#" onClick={this.props.onNav.bind(null, this._nav("define"))}>Define A Workout</a></li>``<li><a href="#"onClick={this.props.onNav.bind(null, this._nav("store"))}>Record A Workout</a></li>``<li><a href="#"onClick={this.props.onNav.bind(null, this._nav("history"))}>View History</a></li>``<li><a href="#" onClick={this.props.onLogout}>Logout</a></li>``</ul>``);``},``_nav: function( view ) {``return view;``}``});``module.exports = Navigation;`清单 4-8 显示了`Navigation`组件。您会注意到每个导航元素都有一个到`onClick`事件的绑定。对于`Logout`,这是一个对注销机制的简单调用,它作为属性传递给这个`Navigation`组件。对于其他导航部分,这个示例展示了如何在本地设置一个值并将其传递给父组件。这是通过在`_nav`功能中设置一个值来实现的。一旦我们编写了它,您将会看到它在父组件中被引用。现在,您需要创建用于定义、存储和查看锻炼历史的模块和组件。这些如清单 4-9 至 4-11 所示。Listing 4-9\. The define.jsx File`var React = require("react");``var DefineWorkout = React.createClass({``render: function() {``return (``<div id="defineWorkouts" >``<h2>Define Workout</h2>``<label htmlFor="defineName">Define Name``<input type="text" id="defineName" />``</label>``<label htmlFor="defineType">Define Type``<input id="defineType" type="text" />``</label>``<label htmlFor="defineDesc">Description</label>``<textarea id="defineDesc" ></textarea>``<button id="saveDefinition">Save Definition</button>``</div>``);``}``});``module.exports = DefineWorkout;``DefineWorkout`组件只是简单的输入和一个保存定义按钮。如果您通过 API 将这个应用连接到一个数据存储中,那么您会希望向 Save Definition 按钮添加一个`onClick`函数,以便将数据存储在适当的位置。Listing 4-10\. The store.jsx File`var React = require("react");``var Option = React.createClass({``render: function() {``return <option>{this.props.value}</option>;``}``});``var StoreWorkout = React.createClass({``_mockWorkouts: [``{``"name": "Murph",``"type": "fortime",``"description": "Run 1 Mile \n 100 pull-ups \n 200 push-ups \n 300 squats \n Run 1 Mile"``},``{``"name": "Tabata Something Else",``"type": "reps",``"description": "4 x 20 seconds on 10 seconds off for 4 minutes \n pull-ups, push-ups, sit-ups, squats"``}``],``render: function() {``var opts = [];``for (var i = 0; i < this._mockWorkouts.length; i++ ) {``opts.push(<Option value={this._mockWorkouts[i].name} />);``}``return (``<div id="logWorkout" class="tabview">``<h2>Record Workout</h2>``<label htmlFor="chooseWorkout">Workout:</label>``<select name="" id="chooseWorkout">``{opts}``</select>``<label htmlFor="workoutResult">Result:</label>``<input id="workoutResult" type="text" />``<input id="workoutDate" type="date" />``<label htmlFor="notes">Notes:</label>``<textarea id="notes"></textarea>``<button>Store</button>``</div>``);``}``});``module.exports = StoreWorkout;``StoreWorkout`是一个组件,同样包含简单的表单输入,帮助您记录锻炼情况。有趣的是,现有锻炼的模拟数据会动态填充`<select/>`标签。该标签包含您在`DefineWorkout`组件中定义的锻炼。Listing 4-11\. The history.jsx File`var React = require("react");``var ListItem = React.createClass({``render: function() {``return <li>{this.props.name} - {this.props.result}</li>;``}``});``var History = React.createClass({``_mockHistory: [``{``"name": "Murph",``"result": "32:18",``"notes": "painful, but fun"``},``{``"name": "Tabata Something Else",``"type": "reps",``"result": "421",``"notes": ""``}``],``render: function() {``var hist = this._mockHistory;``var formatedLi = [];``for (var i = 0; i < hist.length; i++) {``var histObj = { name: hist[i].name, result: hist[i].result };``formatedLi.push(<ListItem {...histObj} />);``}``return (``<div>``<h2>History</h2>``<ul>``{formatedLi}``</ul>``</div>``);``}``});``module.exports = History;``History`还获取模拟数据,并以`<ListItem />`组件的`formattedLi`数组的形式将其添加到应用的表示层。在将所有这些组件放在一起并运行它们之前,让我们停下来思考一下测试 React 应用需要什么。## 测试应用React 使得将测试框架集成到应用中变得容易。这是因为 React 附加组件在`React.addons.testUtils`被称为`testUtils`。本节概述了此附加组件中可用的测试实用程序。要使用附加组件,您必须通过拨打`require("react/addons")`等电话或在`<script src="` `https://fb.me/react-with-addons-0.13.3.js"></script` `>`从脸书 CDN 获取 React with add-ons 源来请求 React 附加组件。### 模仿Simulate 是一种利用模拟事件的方法,这样您就能够模拟 React 应用中的交互。利用 Simulate 的方法签名如下:`React.addons.TestUtils.Simulate.{eventName}(DOMElement, eventData)``DOMElement`是元素,`eventData`是对象。一个例子是这样的:`var node = React.findDOMNode(this.refs.input);``React.addons.TestUtils.Simulate.click(node);`#### 渲染成文档`renderIntoDocument`获取一个组件,并将其呈现在文档中一个分离的 DOM 节点中。由于该方法呈现为一个 DOM,因此该方法需要一个 DOM。因此,如果您在 DOM 之外进行测试,您将无法利用这种方法。#### 模拟组件这个方法允许您创建一个假的 React 组件。这将成为应用中的一个简单的`<div>`,除非您对该对象使用可选的`mockTagName`参数。当您想要在测试场景中创建一个组件并向其添加有用的方法时,这尤其有用。#### 解决这个函数只是返回一个布尔值,表明作为目标的 React 元素是否确实是一个元素:`isElement` `(ReactElement element)`#### iselemontoftype该方法接受一个 React 元素和一个 component 类函数,如果您提供的元素属于`componentClass`的类型,它将返回`True`。`isElementOfType` `( element, componentClass)`#### isDOMComponent该方法返回布尔值,该值确定 React 组件的实例是否是 DOM 元素,如`<div>`或`<h1>`。#### isCompositeComponent这是另一个布尔检查,如果提供的 React 组件是一个复合组件,将返回`True`,这意味着它是使用`React.createClass`或在 ES6 扩展`ReactComponent`中创建的。#### isCompositeComponentWithType类似于`isCompositeComponent`,该方法将检查`ReactComponent`实例,并将其与提供给该方法的`componentClass`进行比较。如果实例和提供的类类型匹配,这将返回`True`。#### findAllInRenderedTree该方法返回存在于树或基础组件中的组件数组,前提是提供给该方法的函数测试为`True`。`findAllInRenderedTree` `( tree, test )`#### scryrrendereddomcomponentswithsclass这个方法在呈现的树中寻找 DOM 组件,比如带有匹配的`className`的`<span>`。`scryRenderedDOMComponentsWithClass` `( tree, className)`#### findrendeddomcomponentswithsclass这个方法和`scryRenderedDOMComponentsWithClass`是一样的,唯一的区别是期望的结果是一个单一的组件而不是一个数组。这意味着如果返回多个组件,将会出现错误。#### scrrendereddomcomponentswithtag返回一个从树组件开始的数组,匹配所有共享相同`tagName`的实例。`scryRenderedDOMComponentsWithTag( tree, tagName)`#### findRenderedDOMComponentsWithTag这与前面的方法相同,除了它预期只有一个结果而不是一个数组。如果返回多个结果,此方法将产生错误。#### scryRenderedComponentsWithType类似于前面的例子,但是基于`componentClass`进行比较,这是提供给该方法的一个函数。`scryRenderedComponentsWithType( tree, componentClass )`#### findRenderedComponentsWithType与前一个方法相同,再次预测一个单一的结果,如果找到多个结果,则抛出一个错误。您可以采用所有这些方法,并利用它们来扩充您选择的测试工具。对脸书来说,这个工具就是笑话。为了在您的机器上设置 Jest,只需如下使用`npm`:`npm install jest-cli –save-dev`一旦安装完毕,您就可以更新您的应用的`package.json`并命名测试框架。`{``...``"scripts": {``"test": "jest"``}``...``}`现在每次运行`npm test`时,位于`__tests__`文件夹中的测试都会被执行。测试可以以一种需要一个模块的方式来构建,然后你可以在这个模块上运行测试。对`SignIn`组件的测试可能如下所示:`jest.dontMock("../src/signin.jsx");``describe("SignIn", function() {``it("will contain a Sign In button to submit", function() {``var React = require("react/addons");``var SignIn = require("../src/signin.jsx");``var TestUtils = React.addons.TestUtils;``var signin = TestUtils.renderIntoDocument(``<SignIn />;``);``var username = TestUtils.findRenderedDOMComponentWithTag( signin, "button" );``expect( username.getDOMNode().textContent).equalTo("Sign In");``});``});`您可以看到,您可以利用 React 附加组件中包含的`TestUtils`来构建测试,这将允许您在构建应用的测试套件时断言测试。## 运行您的应用在本节中,您将把构建的组件拼凑成一个工作应用。现在,您将获得每个组件,并将其组装起来。在这种情况下,您将使用 browserify 来组合您的脚本,这些脚本是使用 CommonJS 模块模块化的。当然,您可以将它们合并成一个文件,或者您可以将它们编写在类似于清单 4-12 的 ES6 模块中。Listing 4-12\. signin.jsx as an ES6 Module`var React = require("react");``class SignIn extends React.Component {``constructor(props) {``super(props);``}``render() {``return (``<div>``<label htmlFor="username">Username``<input type="text" id="username" />``</label>``<label htmlFor="password">Password``<input type="text" id="password" />``</label>``<button id="signIn" onClick={this.props.onAuthComplete.bind( null, this._doAuth)}>Sign In</button>``</div>``);``}``_doAuth() {``return true;``}``}``module.exports = SignIn;`因此,您也可以在 ES6 中创作您的应用,但是对于本例,应用将使用使用`React.createClass();`编写的现有源代码进行组装。首先需要做的是,需要有一个包含代码的核心`app.jsx`文件,并成为应用的主要入口点。这个文件应该包括构建应用所必需的组件。在这种情况下,您需要主应用(您将在一秒钟内构建)和身份验证模块。`var React = require("react");``var Authentication = require("./auth.jsx");``var WorkoutLog = require("./workoutlog.jsx");``var App = React.createClass({``getInitialState: function() {``return { signedIn: false }``},``render: function() {``return (``<div>{ this.state.signedIn ? <WorkoutLog onLogout={this._onLogout} /> : <Authentication onAuthComplete={this._onAuthComplete}/> }</div>``);``},``_onAuthComplete: function( result ) {``// let the child auth components control behavior here``if (result()) {``this.setState( { signedIn: true } );``}``},``_onLogout: function() {``this.setState( { signedIn: false } )``}``})``React.render(<App/>, document.getElementById("container"));`这是实现`Authentication`和`WorkoutLog`组件的单个组件。有一个单一状态参数,指示用户是否登录。正如您之前看到的,这是通过传递属性从子组件传递的。`SignIn`组件绑定到按钮的点击,然后它将与 _ `onAuthComplete`函数共享点击的结果。这与`_onLogout`相同,在`WorkoutLog`组件的导航菜单中处理。说到`WorkoutLog`组件——现在是时候看看它了,因为它是由所有剩余的组件组成的(清单 4-13 )。Listing 4-13\. The workoutlog.jsx File`var React = require("react");``var Nav = require("./navigation.jsx");``var DefineWorkout = require("./define.jsx");``var StoreWorkout = require("./store.jsx");``var History = require("./history.jsx");``var WorkoutLog = React.createClass({``getInitialState: function() {``return { view: "define" };``},``render: function() {``return (``<div>``<h1>Workout Log</h1>``<Nav onLogout={this.props.onLogout} onNav={this._onNav}/>``{this.state.view === "define" ? <DefineWorkout /> : "" }``{this.state.view === "store" ? <StoreWorkout /> : "" }``{this.state.view === "history" ? <History /> : "" }``</div>``);``},``_onNav: function( theView ) {``this.setState( { view: theView });``}``});``module.exports = WorkoutLog;``WorkoutLog`是一个包含`Nav`的组件,然后通过属性`onLogout`来控制`<App>`组件的状态。`<DefineWorkout />, <StoreWorkout />`和`<History />`组件都可用,但是渲染机制中的可见性由`state.view`控制,这是在`WorkoutLog`组件级别维护的唯一状态参数。当点击`<Nav/>`组件中的链接时,设置该状态。只要您的所有路径都是正确的,并且您正在使用这样的命令:`$ watchify -t babelify ./src/app.jsx -o ./dist/bundle.js –v`结果将被捆绑到`bundle.js`中。您将能够导航到您的`index.html`(或者您命名的 HTML 文档)并查看您的工作 React 应用。恭喜你!## 摘要在本章中,您研究了 React web 应用从概念化到最终表示的过程。这包括利用线框化的思想来可视化应用的组件将被拆分的位置,或者剖析现有的应用以便为 React 重写做准备。然后,您看到了如何利用 CommonJS 模块实际创建这些组件,以便保持组件的隔离和可维护性。最后,您将所有这些放在一个工作应用中。在接下来的章节中,您将会遇到一些辅助工具,它们将会帮助您在 React 开发中走得更远。现在,您已经成功地构建了一个 React 应用,并且可能正在享受 React 所展示的 web 开发世界的新视图。# 五、Flux 简介:React 的应用架构Electronic supplementary material The online version of this chapter (doi:[10.1007/978-1-4842-1245-5_5](http://dx.doi.org/10.1007/978-1-4842-1245-5_5)) contains supplementary material, which is available to authorized users.本书的前四章介绍了 React,这是用于创建用户界面的 JavaScript 框架,是脸书工程团队的产品。到目前为止,您所看到的已经足以使用 React 创建健壮的用户界面,并将 React 实现到新的或现有的应用框架中。然而,React 生态系统不仅仅是 React。其中之一是 Flux,这是一个由脸书创建的应用框架,以取代标准的模型-视图-控制器(MVC)框架的方式来补充 React。这并不是因为 MVC 本身有什么问题,而是因为当您开始用 React 构建应用并将应用逻辑分解成组件时,您会发现一个类似于典型 MVC 的框架不如 Flux 那样高效或可维护,Flux 在设计时就考虑到了 React,并且还具有在不增加维护成本的情况下扩展应用的能力。本章将概述什么是 Flux 以及如何开始使用 Flux,并探讨 Flux 和 React 如何配合使用。在下一章用 Flux 构建应用之前,你将熟悉 Flux 的概念。## 什么是 Flux,为什么它不同于典型的 MVC 框架Flux 是专门为 React 设计的。这是一个应用架构,旨在避免典型 MVC 框架中常见的多向数据流和绑定的概念。相反,它提供单向数据流,React 是中间的用户界面层。为了得到一个更好的例子,让我们研究一下典型的 MVC 框架,看看当试图将应用扩展到超出其设计容量时会出现什么问题。在图 5-1 中,你可以看到方向从一个动作开始,通过控制器到达模型。![A978-1-4842-1245-5_5_Fig1_HTML.gif](https://gitee.com/OpenDocCN/vkdoc-frontend-framework-zh/raw/master/docs/intro-react/img/A978-1-4842-1245-5_5_Fig1_HTML.gif)图 5-1。Typical Model-View-Controller data flow model模型和视图可以来回交换数据。这是相对直接的,但是如果您添加一些额外的模型和视图会发生什么呢?然后事情变得稍微复杂一点,但仍然是你可以处理的事情,如图 5-2 所示。![A978-1-4842-1245-5_5_Fig2_HTML.gif](https://gitee.com/OpenDocCN/vkdoc-frontend-framework-zh/raw/master/docs/intro-react/img/A978-1-4842-1245-5_5_Fig2_HTML.gif)图 5-2。Additional models and views added to the MVC data model这显然更加复杂,因为有多个视图和模型,其中一些甚至在彼此之间共享数据。然而,这种结构不会变得完全笨拙,直到有如此多的模型和视图,以至于您甚至不能在一个简单的模型图中跟踪依赖关系,更不用说弄清楚模型和视图如何在代码本身中相互交互。当它开始变得难以处理时,你看到的是最初导致我们做出 React 的相同场景。这些依赖关系的嵌套和耦合导致您有足够的机会失去对特定变量或关系的跟踪。这意味着更新单个模型或视图可能会对未知的相关视图产生不利影响。这既不有趣也不可维护。它会增加您的开发时间,或者以糟糕的用户体验甚至无限更新循环的形式导致严重的错误。这就是 Flux 的好处,尤其是当您有多个模型和视图时。Flux 在最基本的层面上看起来如图 5-3 所示,有一个动作、调度、存储和视图层。![A978-1-4842-1245-5_5_Fig3_HTML.gif](https://gitee.com/OpenDocCN/vkdoc-frontend-framework-zh/raw/master/docs/intro-react/img/A978-1-4842-1245-5_5_Fig3_HTML.gif)图 5-3。Basic Flux data flow这是数据流通过 Flux 应用的基本结构。数据流的初始状态来自一个动作。这个动作然后被转移到调度器。Flux 应用中的调度员就像一名交通官员。这个调度程序将确保流经应用的数据不会导致任何级联效应,这种效应可能会在多模型和视图 MVC 设置中看到。调度程序还必须确保动作按照它们到达的顺序执行,以防止出现竞争情况。商店接管每项活动的调度员。一旦一个动作进入存储区,在存储区完成当前动作的处理之前,不允许该动作进入存储区。一旦存储表明数据中的某些内容发生了变化,视图就会对存储做出响应。视图本身可以通过实例化另一个动作来为这个数据流做出贡献,然后这个动作通过 dispatcher 传递到商店并返回到视图,如图 5-4 所示。![A978-1-4842-1245-5_5_Fig4_HTML.gif](https://gitee.com/OpenDocCN/vkdoc-frontend-framework-zh/raw/master/docs/intro-react/img/A978-1-4842-1245-5_5_Fig4_HTML.gif)图 5-4。Flux with a view creating its own action and passing that to the dispatcher您可能想知道这个数据流的视图组件是否是 React 适合 Flux 的地方。这正是 React 符合 Flux 模型的地方。您可以将应用中的 React 组件视为基于从数据模型的存储部分传输的数据呈现的项目。从视图本身创建的操作呢?React 如何创建一个发送给调度程序的动作?这可能只是用户交互的结果。例如,如果我有一个聊天应用,想要过滤朋友列表或类似的东西,React 将在我与组件的该部分交互时创建新的动作,这些动作将传递给 dispatcher 以启动另一个 Flux 流程,如图 5-5 所示。![A978-1-4842-1245-5_5_Fig5_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-frontend-framework-zh/raw/master/docs/intro-react/img/A978-1-4842-1245-5_5_Fig5_HTML.jpg)图 5-5。Full Flux architecture, including calls from data stores图 5-5 显示了 Flux 架构的完整生命周期。这从某种数据 API 开始,然后将信息或数据发送给动作创建者。顾名思义,动作创建者创建传递给调度程序的动作。然后,调度程序控制这些操作,并将它们过滤到商店。存储处理动作并将它们推送到视图层,在本例中,视图层是 React 组件的集合。然后,这些 React 组件可以进行用户交互,将它们的事件或活动传递给动作创建者,以便继续流程。接下来,您将看到这些 Flux 组件的更详细的分解。## 焊剂的基本成分Flux 由四个主要部分组成,或者至少可以被认为是核心概念。这些是 dispatcher、stores、actions 和 views,正如您在上一节中所学的。在接下来的章节中会对它们进行更详细的描述。### 分配器调度程序是 Flux 应用中数据流的中心。这意味着它控制流入 Flux 应用的存储的内容。它这样做是因为存储创建了链接到调度程序的回调,所以调度程序充当这些回调的存放位置。应用中的每个存储都创建一个回调,并向调度程序注册它。当一个动作创建者向调度程序发送一个新的动作时,调度程序将确保所有注册的存储都获得该动作,因为提供了回调。对于较大规模的应用来说,调度程序通过回调将操作实际调度到商店的能力是必不可少的,因为回调可以被管理到以特定顺序执行的程度。此外,存储可以在更新自己之前显式等待其他存储完成更新。### 商店存储包含 Flux 应用的逻辑和状态。您可能认为这些基本上是传统 MVC 应用的模型部分。区别在于,与传统模型表示单一数据结构不同,Flux 中的存储实际上可以表示许多对象的状态管理。这些对象代表了 Flux 应用中的一个特定的域子集。如前一节所述,存储将向调度程序注册自己,并为其提供回调。传入的回调将有一个参数,该参数是通过 dispatcher 传递给它的动作。回调还将包含一个基于动作类型的`switch`语句,并允许适当地委托给存储内部包含的函数或方法。这允许存储通过调度程序提供的操作来更新状态。然后,商店必须广播一个指示状态已经改变的事件,以便视图可以获取新的状态并更新应用的呈现。### 行动动作实际上是已经发送到商店的任何形式的数据。在本章的后面,你会看到一个使用 Flux 架构的简单`TODO`应用的动作和动作创建者的基本例子。### 视图视图层是 React 适合这个架构的地方。React 具有呈现虚拟 DOM 和最小化复杂 DOM 更新的能力,在创建 Flux 应用时特别有用。React 不仅仅是视图本身。事实上,在视图层次结构的最高级别上的 React 可以成为一种控制器视图,它可以控制用户界面并呈现应用的任何特定子集。当视图或控制器视图从存储层接收到一个事件时,它将首先通过访问存储的 getter 方法来确保它保存最新的数据。然后,它将使用`setState()`或`forceUpdate()`将`render()`正确地放入 DOM。一旦发生这种情况,控制器视图的渲染将传播到它所控制的所有子视图。将应用的状态传递给控制器视图并随后传递给其子视图的常见范例是将整个状态作为单个对象传递。这为您提供了两个好处。首先,您可以看到将到达视图层次结构所有部分的状态,从而允许您作为一个整体来管理它;其次,它将减少您需要传递和维护的属性的数量,本质上使您的应用更容易维护。## React 和 Flux 看起来如何现在你已经对 Flux 和 React 如何协同工作以及如何使用它们有了基本的了解,本章的剩余部分将集中在一个简单的`TODO`应用上。正如前几章对 React 的介绍集中在`TodoMVC.com`上一样,本章将研究利用 Flux 的基本`TodoMVC`应用,然后在下一章讨论更复杂的聊天应用。HTML 与您之前看到的类似,您将把所有的 JavaScript 资源构建到一个单独的`bundle.js`文件中。接下来,您可以在 [`https://github.com/facebook/flux.git`](https://github.com/facebook/flux.git) 克隆 Flux repository 并导航到`examples/flux-todomvc`目录。然后,您可以使用`npm install`和`npm start`命令,并在浏览器中导航到`index.html`文件来查看示例。这些命令的作用是利用`npm`来安装 Flux 示例的依赖项。这包括实际的 Flux `npm`包,它虽然不是一个框架,但包含了调度程序和其他允许 Flux 架构正常工作的模块。Note清单 5-1 到 5-10 中显示的代码是由脸书根据 BSD 许可证授权的。Listing 5-1\. Index.html for TodoMVC with Flux`<!doctype html>``<html lang="en">``<head>``<meta charset="utf-8">``<title>Flux • TodoMVC</title>``<link rel="stylesheet" href="todomvc-common/base.css">``<link rel="stylesheet" href="css/app.css">``</head>``<body>``<section id="todoapp"></section>``<footer id="info">``<p>Double-click to edit a todo</p>``<p>Created by <a href="``http://facebook.com/bill.fisher.771">Bill``<p>Part of <a href="``http://todomvc.com">TodoMVC</a></p``</footer>``<script src="js/bundle.js"></script>``</body>``</html>``bundle.js`文件所基于的主引导文件是清单 5-2 中所示的`app.js`文件。该文件需要 React 并包含对`TodoApp.react`模块的引用,该模块是`TODO`应用的主要组件。Listing 5-2\. Main Entry app.js for the TodoMVC Flux Application`var React = require('react');``var TodoApp = require('./components/TodoApp.react');``React.render(``<TodoApp />,``document.getElementById('todoapp')``);`清单 5-3 中所示的`TodoApp.react.js`模块需要该 Flux 模块的`Footer`、`Header`和`MainSection`组件。另外,你看`stores` / `TodoStore`模块的介绍。Listing 5-3\. Todoapp.js: A Controller-View for the TodoMVC Flux Application`var Footer = require('./Footer.react');``var Header = require('./Header.react');``var MainSection = require('./MainSection.react');``var React = require('react');``var TodoStore = require('../stores/TodoStore');``/**``* Retrieve the current TODO data from the TodoStore``*/``function getTodoState() {``return {``allTodos: TodoStore.getAll(),``areAllComplete: TodoStore.areAllComplete()``};``}``var TodoApp = React.createClass({``getInitialState: function() {``return getTodoState();``},``componentDidMount: function() {``TodoStore.addChangeListener(this._onChange);``},``componentWillUnmount: function() {``TodoStore.removeChangeListener(this._onChange);``},``/**``* @return {object}``*/``render: function() {``return (``<div>``<Header />``<MainSection``allTodos={this.state.allTodos}``areAllComplete={this.state.areAllComplete}``/>``<Footer allTodos={this.state.allTodos} />``</div>``);:``},``/**``* Event handler for 'change' events coming from the TodoStore``*/``_onChange: function() {``this.setState(getTodoState());``}``});:``module.exports = TodoApp;`清单 5-4 中的`MainSection`组件,正如标题所示——控制`TODO`应用主要部分的组件。注意,它还包含了对`TodoActions`模块的第一次引用,您将在本例的后面看到。除此之外,这是一个您期望看到的 React 组件;它渲染主要部分,处理一些 React 属性,并插入`TodoItems`,就像您在前面章节中看到的基于非 Flux 的 React `TodoMVC`应用一样。Listing 5-4\. The MainSection.js Module`var React = require('react');``var ReactPropTypes = React.PropTypes;``var TodoActions = require('../actions/TodoActions');``var TodoItem = require('./TodoItem.react');``var MainSection = React.createClass({``propTypes: {``allTodos: ReactPropTypes.object.isRequired,``areAllComplete: ReactPropTypes.bool.isRequired``},``/**``* @return {object}``*/``render: function() {``// This section should be hidden by default``// and shown when there are TODOs.``if (Object.keys(this.props.allTodos).length < 1) {``return null;``}``var allTodos = this.props.allTodos;``var todos = [];``for (var key in allTodos) {``todos.push(<TodoItem key={key} todo={allTodos[key]} />);``}``return (``<section id="main">``<input``id="toggle-all"``type="checkbox"``onChange={``this._onToggleCompleteAll``checked={this.props.areAllComplete ? 'checked' : ''}``/>``<label htmlFor="toggle-all">Mark all as complete</label>``<ul id="todo-list">{todos}</ul>``</section>``);``},``/**``* Event handler to mark all TODOs as complete``*/``_onToggleCompleteAll: function() {``TodoActions.toggleCompleteAll();``}``});``module.exports = MainSection;``TodoItems`组件(清单 5-5 )与该应用的非 Flux 版本非常相似。注意,绑定到 DOM 的事件,就像在`MainSection`中一样,现在链接到一个`TodoActions`函数(在示例中用粗体文本显示)。这允许将动作绑定到 Flux 数据流,并适当地从调度程序传播到存储,然后最终传播到视图。在`Header`(清单 5-7 )和`Footer`(清单 5-6 )组件中也可以找到与`TodoActions`类似的绑定。Listing 5-5\. TodoItem.react.js`var React = require('react');``var ReactPropTypes = React.PropTypes;``var TodoActions = require('../actions/TodoActions');``var TodoTextInput = require('./TodoTextInput.react');``var cx = require('react/lib/cx');``var TodoItem = React.createClass({``propTypes: {``todo: ReactPropTypes.object.isRequired``},``getInitialState: function() {``return {``isEditing: false``};``},``/**``* @return {object}``*/``render: function() {``var todo = this.props.todo;``var input;``if (this.state.isEditing) {``input =``<TodoTextInput``className="edit"``onSave={``this._onSave``value={todo.text}``/>;``}``// List items should get the class 'editing' when editing``// and 'completed' when marked as completed.``// Note that 'completed' is a classification while 'complete' is a state.``// This differentiation between classification and state becomes important``// in the naming of view actions toggleComplete() vs. destroyCompleted().``return (``<li``className={cx({``'completed': todo.complete,``'editing': this.state.isEditing``})}``key={todo.id}>``<div className="view">``<input``className="toggle"``type="checkbox"``checked={todo.complete}``onChange={``this._onToggleComplete``/>``<label onDoubleClick={this._onDoubleClick}>``{todo.text}``</label>``<button className="destroy" onClick={``this._onDestroyClick``</div>``{input}``</li>``);``},``_onToggleComplete: function() {``TodoActions.toggleComplete(this.props.todo);``},``_onDoubleClick: function() {``this.setState({isEditing: true});``},``/**``* Event handler called within TodoTextInput.``* Defining this here allows TodoTextInput to be used in multiple places``* in different ways.``* @param {string} text``*/``_onSave: function(text) {``TodoActions.updateText(this.props.todo.id, text);``this.setState({isEditing: false});``},``_onDestroyClick: function() {``TodoActions.destroy(this.props.todo.id);``}``});``module.exports = TodoItem;`Listing 5-6\. footer.react.js`var React = require('react');``var ReactPropTypes = React.PropTypes;``var TodoActions = require('../actions/TodoActions');``var Footer = React.createClass({``propTypes: {``allTodos: ReactPropTypes.object.isRequired``},``/**``* @return {object}``*/``render: function() {``var allTodos = this.props.allTodos;``var total = Object.keys(allTodos).length;``if (total === 0) {``return null;``}``var completed = 0;``for (var key in allTodos) {``if (allTodos[key].complete) {``completed++;``}``}``var itemsLeft = total - completed;``var itemsLeftPhrase = itemsLeft === 1 ? ' item ' : ' items ';``itemsLeftPhrase += 'left';``// Undefined and thus not rendered if no completed items are left.``var clearCompletedButton;``if (completed) {``clearCompletedButton =``<button``id="clear-completed"``onClick={``this._onClearCompletedClick``Clear completed ({completed})``</button>;``}``return (``<footer id="footer">``<span id="todo-count">``<strong>``{itemsLeft}``</strong>``{itemsLeftPhrase}``</span>``{clearCompletedButton}``</footer>``);``},``/**``* Event handler to delete all completed TODOs``*/``_onClearCompletedClick: function() {``TodoActions.destroyCompleted();``}``});``module.exports = Footer;`Listing 5-7\. header.react.js`var React = require('react');``var TodoActions = require('../actions/TodoActions');``var TodoTextInput = require('./TodoTextInput.react');``var Header = React.createClass({``/**``* @return {object}``*/``render: function() {``return (``<header id="header">``<h1>todos</h1>``<TodoTextInput``id="new-todo"``placeholder="What needs to be done?"``onSave={``this._onSave``/>``</header>``);``},``/**``* Event handler called within TodoTextInput.``* Defining this here allows TodoTextInput to be used in multiple places``* in different ways.``* @param {string} text``*/``_onSave: function(text) {``if (text.trim()){``TodoActions.create(text);``}``}``});``module.exports = Header;`既然您已经看到了 React 组件如何向`TodoActions`模块发送事件或动作,那么您可以在这个示例中检查一下`TodoActions`模块是什么样子的。它只是一个带有与`AppDispatcher`(清单 5-8 )相关的方法的对象。Listing 5-8\. appdispatcher.js`var Dispatcher = require('flux').Dispatcher;``module.exports = new Dispatcher();`正如您在前面的例子中看到的,`AppDispatcher`是基本 Flux 分配器的一个简单实例。您会看到清单 5-9 中所示的`TodoActions`函数,每一个都与`AppDispatcher`有关。他们调用了`dispatch`函数,该函数保存了一个对象,该对象描述了从调度器`AppDispatcher.dispatch( /* object describing dispatch */ );`发送的内容。您可以看到,根据所调用的动作,所发送的对象会有所不同。这意味着`create`函数将生成一个 dispatch,其中包含传递了`TodoItem`文本的`TodoConstants.TODO_CREATE actionType`。Listing 5-9\. Todoactions.js`var AppDispatcher = require('../dispatcher/AppDispatcher');``var TodoConstants = require('../constants/TodoConstants');``var TodoActions = {``/**``* @param {string} text``*/``create: function(text) {``AppDispatcher.dispatch({``actionType: TodoConstants.TODO_CREATE,``text: text``});``},``/**``* @param {string} id The ID of the TODO item``* @param {string} text``*/``updateText: function(id, text) {``AppDispatcher.dispatch({``actionType: TodoConstants.TODO_UPDATE_TEXT,``id: id,``text: text``});``},``/**``* Toggle whether a single TODO is complete``* @param {object} todo``*/``toggleComplete: function(todo) {``var id = todo.id;``var actionType = todo.complete ?``TodoConstants.TODO_UNDO_COMPLETE :``TodoConstants.TODO_COMPLETE;``AppDispatcher.dispatch({``actionType: actionType,``id: id``});``},``/**``* Mark all TODOs as complete``*/``toggleCompleteAll: function() {``AppDispatcher.dispatch({``actionType: TodoConstants.TODO_TOGGLE_COMPLETE_ALL``});``},``/**``* @param {string} id``*/``destroy: function(id) {``AppDispatcher.dispatch({``actionType: TodoConstants.TODO_DESTROY,``id: id``});``},``/**``* Delete all the completed TODOs``*/``destroyCompleted: function() {``AppDispatcher.dispatch({``actionType: TodoConstants.TODO_DESTROY_COMPLETED``});``}``};``module.exports = TodoActions;`最后,在清单 5-10 中,您会遇到`TodoStore.js file`,它是动作、调度程序和视图之间的中介。您看到的是,在这个模块的函数中处理的每个事件也是从回调注册表中调用的。这个注册表在下面的例子中被加粗,它为调度程序和视图之间的所有委托提供了动力。每个函数都将完成更新`TODOs`的值所需的工作,之后调用方法`TodoStore.emitChange()`。这个方法将告诉 React 视图,是时候协调视图并相应地更新 DOM 了。Listing 5-10\. TodoStore.js`var AppDispatcher = require('../dispatcher/AppDispatcher');``var EventEmitter = require('events').EventEmitter;``var TodoConstants = require('../constants/TodoConstants');``var assign = require('object-assign');``var CHANGE_EVENT = 'change';``var _todos = {};``/**``* Create a TODO item.``* @param {string} text The content of the TODO``*/``function create(text) {``// Hand waving here -- not showing how this interacts with XHR or persistent``// server-side storage.``// Using the current timestamp + random number in place of a real id.``var id = (+new Date() + Math.floor(Math.random() * 999999)).toString(36);``_todos[id] = {``id: id,``complete: false,``text: text``};``}``/**``* Update a TODO item.``* @param {string} id``* @param {object} updates An object literal containing only the data to be``* updated.``*/``function update(id, updates) {``_todos[id] = assign({}, _todos[id], updates);``}``/**``* Update all of the TODO items with the same object.``* the data to be updated. Used to mark all TODOs as completed.``* @param {object} updates An object literal containing only the data to be``* updated.``*/``function updateAll(updates) {``for (var id in _todos) {``update(id, updates);``}``}``/**``* Delete a TODO item.``* @param {string} id``*/``function destroy(id) {``delete _todos[id];``}``/**``* Delete all the completed TODO items.``*/``function destroyCompleted() {``for (var id in _todos) {``if (_todos[id].complete) {``destroy(id);``}``}``}``var TodoStore = assign({}, EventEmitter.prototype, {``/**``* Tests whether all the remaining TODO items are marked as completed.``* @return {boolean}``*/``areAllComplete: function() {``for (var id in _todos) {``if (!_todos[id].complete) {``return false;``}``}``return true;``},``/**``* Get the entire collection of TODOs.``* @return {object}``*/``getAll: function() {``return _todos;``},``emitChange: function() {``this.emit(CHANGE_EVENT);``},``/**``* @param {function} callback``*/``addChangeListener: function(callback) {``this.on(CHANGE_EVENT, callback);``},``/**``* @param {function} callback``*/``removeChangeListener: function(callback) {``this.removeListener(CHANGE_EVENT, callback);``}``});``// Register callback to handle all updates``AppDispatcher.register(function(action) {``var text;``switch(action.actionType) {``case TodoConstants.TODO_CREATE:``text = action.text.trim();``if (text !== '') {``create(text);``TodoStore.emitChange();``}``break;``case TodoConstants.TODO_TOGGLE_COMPLETE_ALL:``if (TodoStore.areAllComplete()) {``updateAll({complete: false});``} else {``updateAll({complete: true});``}``TodoStore.emitChange();``break;``case TodoConstants.TODO_UNDO_COMPLETE:``update(action.id, {complete: false});``TodoStore.emitChange();``break;``case TodoConstants.TODO_COMPLETE:``update(action.id, {complete: true});``TodoStore.emitChange();``break;``case TodoConstants.TODO_UPDATE_TEXT:``text = action.text.trim();``if (text !== '') {``update(action.id, {text: text});``TodoStore.emitChange();``}``break;``case TodoConstants.TODO_DESTROY:``destroy(action.id);``TodoStore.emitChange();``break;``case TodoConstants.TODO_DESTROY_COMPLETED:``destroyCompleted();``TodoStore.emitChange();``break;``default:``// no op``}``});``module.exports = TodoStore;`## 摘要这一章不同于纯粹的 React,它开始向你展示 React 生态系统作为一个整体是如何工作的。从描述 Flux 体系结构如何提供一种有意义和有用的机制来构建 React 应用,使其不仅可维护,而且可有效扩展开始,您看到了如何单向路由数据流,从而为 React 应用提供最佳的开发实践。然后,您快速浏览了一个简单 Flux `TodoMVC`应用的脸书版本,该版本展示了如何开始以 Flux 架构的方式构建 React 应用。在下一章,React 入门书的最后一章,您将剖析一个用 React 和 Flux 构建的全功能聊天应用,这样您就可以全面理解如何以可维护和可伸缩的方式创建一个复杂的应用。# 六、使用 Flux 构建 React 应用Electronic supplementary material The online version of this chapter (doi:[10.1007/978-1-4842-1245-5_6](http://dx.doi.org/10.1007/978-1-4842-1245-5_6)) contains supplementary material, which is available to authorized users.前一章向您介绍了 Flux 项目。Flux 代表了 React 应用的高效应用架构。您了解了 Flux 如何使用 dispatcher 将动作发送到存储,然后使用 React 组件将这些动作呈现到 DOM 中。这一切都是通过查看一个利用 Flux 架构构建的普通`TodoMVC`应用来完成的。在本章中,您将创建一个比`TODO`应用更复杂的 React 应用,并根据 Flux 架构来构建它。## 构建您的应用在你开始为你要构建的应用创建组件和 Flux 架构之前,你需要定义你要做什么。在这个例子中,我们将展示当您使用 React 和 Flux 时,数据是如何单向流动的。一个很好的例子就是聊天应用。聊天应用可以有很多变化,但在这种情况下,你想要的聊天应用看起来就像脸书界面中的聊天功能。你有一个线索列表,显示你正在和一个朋友交流。通过消息窗格,您可以选择特定主题,跟踪该主题的历史记录,然后创建新消息。模拟起来,这个应用可能看起来类似于图 6-1 所示。![A978-1-4842-1245-5_6_Fig1_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-frontend-framework-zh/raw/master/docs/intro-react/img/A978-1-4842-1245-5_6_Fig1_HTML.jpg)图 6-1。Wireframe of your application看一下这个线框,你可以理解在哪里你可以为你的应用创建 React 组件。整个应用将成为父组件。然后,您可以创建一个消息组件。单一的消息组件不适合您熟悉的 React 的原子组件架构,因此您需要将消息部分分成三个 React 组件。一个用于创建消息,第二个用于管理列表中的单个消息项,第三个是这些消息项的容器。当考虑线框左侧的消息线程时,可以看到类似的设计。在这里,您将拥有 threads 容器,该容器的子容器是 thread 项。## 为应用创建 Dispatcher、Stores、Actions 和 React 组件现在,您已经对将要创建的应用有了大致的了解,您可以构建 React 应用,并利用您选择的任何机制将数据加载到组件中,以便呈现它们。这是一个有效的方法,但是正如你在前一章中看到的,Flux 为 React 提供了一个架构,使得构建一个聊天应用比不使用 React 和 Flux 更容易。因此,现在您可以开始利用不断变化的思维来设计您的应用。### 分配器首先,您需要创建一个 dispatcher,正如您之前看到的,它只是 Flux dispatcher 模块的一个新实例,您可以在您的应用中共享它(清单 6-1 )。Listing 6-1\. Dispatcher for the Chat Application`var Dispatcher = require('flux').Dispatcher;``module.exports = new Dispatcher();`### 商店如果你还记得,变化中的商店被认为是一种你可能在典型的 MVC 框架中发现的模型,只是更大一些。存储代表逻辑域中所有数据的位置,而不是特定元素的模型表示。因此,就聊天应用而言,您可以将所有的消息数据封装到一个单独的存储中,如清单 6-2 所示。Listing 6-2\. The MessageStore Component`var ChatAppDispatcher = require('../dispatcher/ChatAppDispatcher');``var ChatConstants = require('../constants/ChatConstants');``var ChatMessageUtils = require('../utils/ChatMessageUtils');``var EventEmitter = require('events').EventEmitter;``var ThreadStore = require('../stores/ThreadStore');``var assign = require('object-assign');``var ActionTypes = ChatConstants.ActionTypes;``var CHANGE_EVENT = 'change';``var _messages = {};``function _addMessages(rawMessages) {``rawMessages.forEach(function(message) {``if (!_messages[message.id]) {``_messages[message.id] = ChatMessageUtils.convertRawMessage(``message,``ThreadStore.getCurrentID()``);``}``});``}``function _markAllInThreadRead(threadID) {``for (var id in _messages) {``if (_messages[id].threadID === threadID) {``_messages[id].isRead = true;``}``}``}``var MessageStore = assign({}, EventEmitter.prototype, {``emitChange: function() {``this.emit(CHANGE_EVENT);``},``/**``* @param {function} callback``*/``addChangeListener: function(callback) {``this.on(CHANGE_EVENT, callback);``},``removeChangeListener: function(callback) {``this.removeListener(CHANGE_EVENT, callback);``},``get: function(id) {``return _messages[id];``},``getAll: function() {``return _messages;``},``/**``* @param {string} threadID``*/``getAllForThread: function(threadID) {``var threadMessages = [];``for (var id in _messages) {``if (_messages[id].threadID === threadID) {``threadMessages.push(_messages[id]);``}``}``threadMessages.sort(function(a, b) {``if (a.date < b.date) {``return -1;``} else if (a.date > b.date) {``return 1;``}``return 0;``});``return threadMessages;``},``getAllForCurrentThread: function() {``return this.getAllForThread(ThreadStore.getCurrentID());``}``});``MessageStore.dispatchToken = ChatAppDispatcher.register(function(action) {``switch(action.type) {``case ActionTypes.CLICK_THREAD:``ChatAppDispatcher.waitFor([ThreadStore.dispatchToken]);``_markAllInThreadRead(ThreadStore.getCurrentID());``MessageStore.emitChange();``break;``case ActionTypes.CREATE_MESSAGE:``var message = ChatMessageUtils.getCreatedMessageData(``action.text,``action.currentThreadID``);``_messages[message.id] = message;``MessageStore.emitChange();``break;``case ActionTypes.RECEIVE_RAW_MESSAGES:``_addMessages(action.rawMessages);``ChatAppDispatcher.waitFor([ThreadStore.dispatchToken]);``_markAllInThreadRead(ThreadStore.getCurrentID());``MessageStore.emitChange();``break;``default:``// do nothing``}``});``module.exports = MessageStore;``MessageStore`代表您将在聊天应用中创建或获取的消息的所有数据。商店必须做的第一件事是向调度程序注册一个回调,这是通过`ChatAppDispatcher.register()`完成的。该回调成为向存储区输入数据的唯一方法。您将看到回调包含一个大的`switch`语句,在这种情况下,该语句与发送给回调的不同动作类型无关。一旦遇到`switch`中的适用情况,商店将能够对该动作做些什么,然后可以发送`emitChange()`,它将与视图通信,然后它们可以从商店获取新数据。值得注意的是,该存储不包含任何设置数据的公共方法,这意味着一切都是通过 getters 访问的。这意味着您不必担心数据从应用的另一部分泄漏到您的存储中。这使得存储成为数据的文字存储箱。它将能够处理您的消息,并通过 dispatcher 回调更新它们,然后通知您发生的变化。这可以从`MessageStore`中看出,这里的`actionType`就是`ActionTypes.RECEIVE_RAW_MESSAGES`。一旦收到这个消息,`MessageStore`将通过其私有的`_addMessages`函数添加消息,将该线程中的消息标记为已读,并最终通过`EventEmitter`发出更改。现在您已经看到了`MessageStore`,您需要能够控制您的聊天应用中哪些线程可用。这是通过`ThreadStore`(列表 6-3 )完成的。Listing 6-3\. The ThreadStore Component`var ChatAppDispatcher = require('../dispatcher/ChatAppDispatcher');``var ChatConstants = require('../constants/ChatConstants');``var ChatMessageUtils = require('../utils/ChatMessageUtils');``var EventEmitter = require('events').EventEmitter;``var assign = require('object-assign');``var ActionTypes = ChatConstants.ActionTypes;``var CHANGE_EVENT = 'change';``var _currentID = null;``var _threads = {};``var ThreadStore = assign({}, EventEmitter.prototype, {``init: function(rawMessages) {``rawMessages.forEach(function(message) {``var threadID = message.threadID;``var thread = _threads[threadID];``if (!(thread && thread.lastTimestamp > message.timestamp)) {``_threads[threadID] = {``id: threadID,``name: message.threadName,``lastMessage: ChatMessageUtils.convertRawMessage(message, _currentID)``};``}``}, this);``if (!_currentID) {``var allChrono = this.getAllChrono();``_currentID = allChrono[allChrono.length - 1].id;``}``_threads[_currentID].lastMessage.isRead = true;``},``emitChange: function() {``this.emit(CHANGE_EVENT);``},``/**``* @param {function} callback``*/``addChangeListener: function(callback) {``this.on(CHANGE_EVENT, callback);``},``/**``* @param {function} callback``*/``removeChangeListener: function(callback) {``this.removeListener(CHANGE_EVENT, callback);``},``/**``* @param {string} id``*/``get: function(id) {``return _threads[id];``},``getAll: function() {``return _threads;``},``getAllChrono: function() {``var orderedThreads = [];``for (var id in _threads) {``var thread = _threads[id];``orderedThreads.push(thread);``}``orderedThreads.sort(function(a, b) {``if (a.lastMessage.date < b.lastMessage.date) {``return -1;``} else if (a.lastMessage.date > b.lastMessage.date) {``return 1;``}``return 0;``});``return orderedThreads;``},``getCurrentID: function() {``return _currentID;``},``getCurrent: function() {``return this.get(this.getCurrentID());``}``});``ThreadStore.dispatchToken = ChatAppDispatcher.register(function(action) {``switch(action.type) {``case ActionTypes.CLICK_THREAD:``_currentID = action.threadID;``_threads[_currentID].lastMessage.isRead = true;``ThreadStore.emitChange();``break;``case ActionTypes.RECEIVE_RAW_MESSAGES:``ThreadStore.init(action.rawMessages);``ThreadStore.emitChange();``break;``default:``// do nothing``}``});``module.exports = ThreadStore;``ThreadStore`和`MessageStore`一样,只有公共的 getter 方法,没有 setter 方法。`ThreadStore`向调度程序注册一个回调,其中包含`switch`语句,该语句将控制存储如何对调度程序发送的动作做出 React。`switch`语句响应通过调度程序发送的特定`ActionTypes`,然后发送`emitChange()`事件。与`ThreadStore`相关的是`UnreadThreadStore`(列表 6-4 )。这个商店将在`ThreadSection`组件中被引用,并绑定到`_onChange`事件。这样,当线程被标记为未读时,组件可以更新状态。Listing 6-4\. The UnreadThreadStore Component`var ChatAppDispatcher = require('../dispatcher/ChatAppDispatcher');``var ChatConstants = require('../constants/ChatConstants');``var EventEmitter = require('events').EventEmitter;``var MessageStore = require('../stores/MessageStore');``var ThreadStore = require('../stores/ThreadStore');``var assign = require('object-assign');``var ActionTypes = ChatConstants.ActionTypes;``var CHANGE_EVENT = 'change';``var UnreadThreadStore = assign({}, EventEmitter.prototype, {``emitChange: function() {``this.emit(CHANGE_EVENT);``},``/**``* @param {function} callback``*/``addChangeListener: function(callback) {``this.on(CHANGE_EVENT, callback);``},``/**``* @param {function} callback``*/``removeChangeListener: function(callback) {``this.removeListener(CHANGE_EVENT, callback);``},``getCount: function() {``var threads = ThreadStore.getAll();``var unreadCount = 0;``for (var id in threads) {``if (!threads[id].lastMessage.isRead) {``unreadCount++;``}``}``return unreadCount;``}``});``UnreadThreadStore.dispatchToken = ChatAppDispatcher.register(function(action) {``ChatAppDispatcher.waitFor([``ThreadStore.dispatchToken,``MessageStore.dispatchToken``]);``switch (action.type) {``case ActionTypes.CLICK_THREAD:``UnreadThreadStore.emitChange();``break;``case ActionTypes.RECEIVE_RAW_MESSAGES:``UnreadThreadStore.emitChange();``break;``default:``// do nothing``}``});``module.exports = UnreadThreadStore;`商店到此为止。它们通过注册的回调以对象文本的形式从调度程序获取数据,然后发出事件。接下来,您将检查将从 React 视图中调用的操作,或者在本例中是操作创建者。### 行动动作驱动 Flux Chat 应用的单向数据流。如果没有这些操作,视图将不会接收到来自商店的更新,因为没有向调度程序传递任何东西来调用商店的回调。这个例子中的动作以动作创建者的形式出现。这些创建者可以从 React 的视图中创建一个动作,或者您可以从服务器上的 WebAPI 中获得一条消息。在清单 6-5 中,您不需要创建一个服务器来服务聊天请求,但是下面的代码片段强调了如何创建一个`ServerAction`。这导出了一些方法,这些方法可以从你的服务器获取数据,然后通过`.` `dispatch()`函数将数据发送给 Flux 应用。Listing 6-5\. This ServerActionCreator Can Receive Messages from an API and Dispatch to the Rest of the Flux Application`var ChatAppDispatcher = require('../dispatcher/ChatAppDispatcher');``var ChatConstants = require('../constants/ChatConstants');``var ActionTypes = ChatConstants.ActionTypes;``module.exports = {``receiveAll: function(rawMessages) {``ChatAppDispatcher.dispatch({``type: ActionTypes.RECEIVE_RAW_MESSAGES,``rawMessages: rawMessages``});``},``receiveCreatedMessage: function(createdMessage) {``ChatAppDispatcher.dispatch({``type: ActionTypes.RECEIVE_RAW_CREATED_MESSAGE,``rawMessage: createdMessage``});``}``};`动作创建者的另一个功能是,他们可以成为一个实用程序,将视图中的信息传递给服务器和调度程序。这正是这个 Flux 例子中的`MessageAction`所发生的事情(列表 6-6 )。您将在下一节看到的`MessageComposer`组件调用`MessageAction`来创建消息。这将首先把消息数据发送给调度程序,并调用应用中的 API 实用程序来更新服务器上的数据,如清单 6-6 所示。Listing 6-6\. The MessageActionCreator Will Dispatch Message Data via the Dispatcher and Update the Server via an API Method`var ChatAppDispatcher = require('../dispatcher/ChatAppDispatcher');``var ChatConstants = require('../constants/ChatConstants');``var ChatWebAPIUtils = require('../utils/ChatWebAPIUtils');``var ChatMessageUtils = require('../utils/ChatMessageUtils');``var ActionTypes = ChatConstants.ActionTypes;``module.exports = {``createMessage: function(text, currentThreadID) {``ChatAppDispatcher.dispatch({``type: ActionTypes.CREATE_MESSAGE,``text: text,``currentThreadID: currentThreadID``});``var message = ChatMessageUtils.getCreatedMessageData(text, currentThreadID);``ChatWebAPIUtils.createMessage(message);``}``};`在您的聊天应用中,唯一需要考虑的是当有人点击一个线程时会发生什么。这个动作由`ThreadActionCreator`处理,如清单 6-7 所示。Listing 6-7\. ThreadActionCreator Verifies that the Thread of a Given ID Has Been Clicked in the Application`var ChatAppDispatcher = require('../dispatcher/ChatAppDispatcher');``var ChatConstants = require('../constants/ChatConstants');``var ActionTypes = ChatConstants.ActionTypes;``module.exports = {``clickThread: function(threadID) {``ChatAppDispatcher.dispatch({``type: ActionTypes.CLICK_THREAD,``threadID: threadID``});``}``};`### React 组分React 组件与您之前在本书中看到的组件没有什么不同;然而,它们确实涉及到更积极地利用状态来解释聊天应用及其不断变化的体系结构。让我们从创建`ThreadSection`组件开始。为此,您需要创建`ThreadListItem`(清单 6-8 ,它将在`ThreadSection`的`render()`过程中被添加。`ThreadListItem`还调用`ThreadAction`让`ThreadClick`将事件发送给调度程序。Listing 6-8\. ThreadListItem—Note the _onClick Binding to the ThreadAction for clickThread`var ChatThreadActionCreators = require('../actions/ChatThreadActionCreators');``var React = require('react');``// Note: cx will be obsolete soon so you can use``//`[`https://github.com/JedWatson/classnames`](https://github.com/JedWatson/classnames)`var cx = require('react/lib/cx');``var ReactPropTypes = React.PropTypes;``var ThreadListItem = React.createClass({``propTypes: {``thread: ReactPropTypes.object,``currentThreadID: ReactPropTypes.string``},``render: function() {``var thread = this.props.thread;``var lastMessage = thread.lastMessage;``return (``<li``className={cx({``'thread-list-item': true,``'active': thread.id === this.props.currentThreadID``})}``onClick={``this._onClick``<h5 className="thread-name">{thread.name}</h5>``<div className="thread-time">``{lastMessage.date.toLocaleTimeString()}``</div>``<div className="thread-last-message">``{lastMessage.text}``</div>``</li>``);``},``_onClick: function() {``ChatThreadActionCreators.clickThread(this.props.thread.id);``}``});``module.exports = ThreadListItem;`Note组件`cx`已被弃用,但是可以在 [`https://github.com/JedWatson/classnames`](https://github.com/JedWatson/classnames) 找到一个用于类操作的独立组件。如果你选择利用这种类操作,你也可以在`http:// reactcss.com`找到解决方案。现在您已经有了`ThreadListItems`,您可以将这些集合到您的`ThreadSection`中,如清单 6-9 所示。这个`ThreadSection`在组件的生命周期事件`getInitialState`期间从`ThreadStore`和`UnreadThreadStore`获取线程。这将设置状态来控制在`render`函数中创建多少个`ThreadListItem`。Listing 6-9\. The ThreadSection Component`var React = require('react');``var MessageStore = require('../stores/MessageStore');``var ThreadListItem = require('../components/ThreadListItem.react');``var ThreadStore = require('../stores/ThreadStore');``var UnreadThreadStore = require('../stores/UnreadThreadStore');``function``getStateFromStores``return {``threads: ThreadStore.getAllChrono(),``currentThreadID: ThreadStore.getCurrentID(),``unreadCount: UnreadThreadStore.getCount()``};``}``var ThreadSection = React.createClass({``getInitialState: function() {``return``getStateFromStores()``},``componentDidMount: function() {``ThreadStore.addChangeListener(this._onChange);``UnreadThreadStore.addChangeListener(this._onChange);``},``componentWillUnmount: function() {``ThreadStore.removeChangeListener(this._onChange);``UnreadThreadStore.removeChangeListener(this._onChange);``},``render: function() {``var threadListItems = this.state.threads.map(function(thread) {``return (``<ThreadListItem``key={thread.id}``thread={thread}``currentThreadID={this.state.currentThreadID}``/>``);``}, this);``var unread =``this.state.unreadCount === 0 ?``null :``<span>Unread threads: {this.state.unreadCount}</span>;``return (``<div className="thread-section">``<div className="thread-count">``{unread}``</div>``<ul className="thread-list">``{``threadListItems``</ul>``</div>``);``},``/**``* Event handler for 'change' events coming from the stores``*/``_onChange: function() {``this.setState(getStateFromStores());``}``});``module.exports = ThreadSection;`现在,您已经使用 React 和 Flux 创建了应用的线程部分。接下来是`MessageSection`,它要求您创建一个`MessageListItem`组件和一个`MessageComposer`组件,如清单 6-10 所示。Listing 6-10\. MessageComposer—Binds to the Textarea and Sends the Text to the MessageActionCreators`var ChatMessageActionCreators = require('../actions/ChatMessageActionCreators');``var React = require('react');``var ENTER_KEY_CODE = 13;``var MessageComposer = React.createClass({``propTypes: {``threadID: React.PropTypes.string.isRequired``},``getInitialState: function() {``return {text: ''};``},``render: function() {``return (``<textarea``className="message-composer"``name="message"``value={this.state.text}``onChange={this._onChange}``onKeyDown={this._onKeyDown}``/>``);``},``_onChange: function(event, value) {``this.setState({text: event.target.value});``},``_onKeyDown: function(event) {``if (event.keyCode === ENTER_KEY_CODE) {``event.preventDefault();``var text = this.state.text.trim();``if (text) {``ChatMessageActionCreators.createMessage(text, this.props.threadID);``}``this.setState({text: ''});``}``}``});``module.exports = MessageComposer;``MessageComposer`组件是一个`textarea`,它将把它的变更事件绑定到`state.text`和`keydown`事件。`keydown`事件将寻找键盘上的回车键。如果已经按下,`MessageComposer`将调用`ChatMessageActionCreators.createMessage()`函数创建发送给 API 服务器和调度程序的动作。`MessageListItems`(清单 6-11 )只是一个 HTML 列表项,包含从`MessageSection`传递给它的消息数据。Listing 6-11\. MessageListItems Contain Message Details`var React = require('react');``var ReactPropTypes = React.PropTypes;``var MessageListItem = React.createClass({``propTypes: {``message: ReactPropTypes.object``},``render: function() {``var message = this.props.message;``return (``<li className="message-list-item">``<h5 className="message-author-name">{message.authorName}</h5>``<div className="message-time">``{message.date.toLocaleTmeString()}``</div>``<div className="message-text">{message.text}</div>``</li>``);``}``});``module.exports = MessageListItem;`清单 6-12 中的`MessageSection`首先通过`getInitialState` React 生命周期从存储中获取状态。这将获取当前线程并得到它的消息。一旦组件挂载,在`componentDidMount`,`MessageStore`和`ThreadStore`都将监听器绑定到`_onChange`事件。这个改变事件`this.setState(getStateFromStores());`被再次调用,就像初始状态被设置一样。这是 React 和 Flux 的精髓。这是一个单向数据流,其中每次渲染都来自于从存储中获取状态,并且只有一种方法来更新存储。`MessageSection`还聚合添加到状态对象的消息,并为每个消息创建新的`MessageListItems`。Listing 6-12\. The MessageSection Component`var MessageComposer = require('./MessageComposer.react');``var MessageListItem = require('./MessageListItem.react');``var MessageStore = require('../stores/MessageStore');``var React = require('react');``var ThreadStore = require('../stores/ThreadStore');``function getStateFromStores() {``return {``messages: MessageStore.getAllForCurrentThread(),``thread: ThreadStore.getCurrent()``};``}``function getMessageListItem(message) {``return (``<MessageListItem``key={message.id}``message={message}``/>``);``}``var MessageSection = React.createClass({``getInitialState: function() {``return getStateFromStores();``},``componentDidMount: function() {``this._scrollToBottom();``MessageStore.addChangeListener(this._onChange);``ThreadStore.addChangeListener(this._onChange);``},``componentWillUnmount: function() {``MessageStore.removeChangeListener(this._onChange);``ThreadStore.removeChangeListener(this._onChange);``},``render: function() {``var messageListItems = this.state.messages.map(getMessageListItem);``return (``<div className="message-section">``<h3 className="message-thread-heading">{this.state.thread.name}</h3>``<ul className="message-list" ref="messageList">``{messageListItems}``</ul>``<MessageComposer threadID={this.state.thread.id}/>``</div>``);``},``componentDidUpdate: function() {``this._scrollToBottom();``},``_scrollToBottom: function() {``var ul = this.refs.messageList.getDOMNode();``ul.scrollTop = ul.scrollHeight;``},``/**``* Event handler for 'change' events coming from the MessageStore``*/``_onChange: function() {``this.setState(getStateFromStores());``}``});``module.exports = MessageSection;`您现在已经有了应用的完整 MessageSection 和 ThreadSection。剩下的唯一一项就是将这些全部放入清单 6-13 所示的`ChatApp`组件中。Listing 6-13\. The ChatApp Component`var MessageSection = require('./MessageSection.react');``var React = require('react');``var ThreadSection = require('./ThreadSection.react');``var ChatApp = React.createClass({``render: function() {``return (``<div className="chatapp">``<ThreadSection />``<MessageSection />``</div>``);``}``});``module.exports = ChatApp;`## 写作测试正如您在本书前面看到的,Jest 是编写测试的有用工具。清单 6-14 是一个简单的测试,您可以将其作为一个模型。这个测试是为`UnreadThreadStore`编写的,它确保了未读线程的正确计数。它还确保回调注册到调度程序。Listing 6-14\. UnreadThreadCount Tests`jest.dontMock('../UnreadThreadStore');``jest.dontMock('object-assign');``describe('UnreadThreadStore', function() {``var ChatAppDispatcher;``var UnreadThreadStore;``var callback;``beforeEach(function() {``ChatAppDispatcher = require('../../dispatcher/ChatAppDispatcher');``UnreadThreadStore = require('../UnreadThreadStore');``callback = ChatAppDispatcher.register.mock.calls[0][0];``});``it('registers a callback with the dispatcher', function() {``expect(ChatAppDispatcher.register.mock.calls.length).toBe(1);``});``it('provides the unread thread count', function() {``var ThreadStore = require('../ThreadStore');``ThreadStore.getAll.mockReturnValueOnce(``{``foo: {lastMessage: {isRead: false}},``bar: {lastMessage: {isRead: false}},``baz: {lastMessage: {isRead: true}}``}``);``expect(UnreadThreadStore.getCount()).toBe(2);``});``});`## 运行应用您可以从位于 [`https://github.com/cgack/flux/tree/master/examples/flux-chat`](https://github.com/cgack/flux/tree/master/examples/flux-chat) 的库的根目录运行这个应用。一旦你克隆了这个库,或者这个分支的父库,你就可以从`flux-chat`目录运行`npm install`。这将下拉所有需要的依赖项。之后,只需运行`npm test`来运行测试。要最终运行应用,使用`npm start`命令。这样做将启动观察器,并将所有的`app.js`引导代码转换成`bundle.js`文件。然后,您只需要在浏览器中导航到`index.html`文件,就可以看到聊天应用的运行。## 摘要在这一章中,你看到了一个关于 React 和 Flux 如何一起工作的更加人为的例子。这个例子展示了更多的 React 组件和状态的使用,并说明了一个单向数据流。无论有没有 Flux,这个例子都是开始构建自己的 React 组件和应用的良好起点。这本书旨在向您介绍 React,并让您习惯于通过 React 组件的视角来看待 web 开发。我希望你喜欢这本书,并发现它很有用。