React-入门指南-一-

92 阅读1小时+

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 框架的例子。

A978-1-4842-1245-5_1_Fig1_HTML.gif

图 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组件。这两个变量footermain被添加到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 的大按钮。

A978-1-4842-1245-5_1_Figa_HTML.jpg

当然,您可以获取源文件并将其添加到应用的脚本标记中。事实上,脸书在其 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.netjsbin.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.mapReact.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。可选地,您可以接受第二个和第三个参数— propschildren。当我们在本章后面更详细地讨论元素和工厂时,你会看到更多关于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 特性,setStateforceUpdate。要使用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 的版本,它们很可能会被删除。这些方法将在下一节中介绍。

了解组件属性和方法

你现在已经看到了forceUpdatesetState,这两个核心函数是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的表亲,叫做replaceStatereplaceState函数将完全覆盖当前属于组件的任何状态。

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 的getInitialStategetDefaultProps

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.propsnextProps进行任何逻辑比较。

componentWillReceiveProps( nextProps )

shouldComponentUpdate

在组件渲染之前以及每次收到属性或状态的更改时,都会调用此函数。在初始渲染之前,或者在使用forceUpdate时,它不会被调用。如果您知道对属性或状态的更改实际上不需要更新组件,则可以使用此函数来跳过渲染。为了缩短渲染过程,您需要根据您确定的任何标准在函数体中返回 false。这样做将绕过组件的呈现,不仅跳过了render()函数,还跳过了生命周期中的下一步— componentWillUpdatecomponentDidUpdate

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 组件在初始渲染时所遵循的过程。

A978-1-4842-1245-5_2_Fig1_HTML.gif

图 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 直观地展示了相同状态变化过程的生命周期。

A978-1-4842-1245-5_2_Fig2_HTML.gif

图 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 所示。

A978-1-4842-1245-5_2_Fig3_HTML.gif

图 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.DOMReact.findDOMNodeReact.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—ReactElementsReactComponents—来理解如何构建一个功能强大的 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<Answervalue={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 应用的创建。