三、创建第一个 React 元素

现在创建一个简单的 web 应用需要编写 HTML、CSS 和 JavaScript 代码。我们使用三种不同技术的原因是,我们希望将三种不同的关注点分开:

  • 内容(HTML)
  • 样式(CSS)
  • 逻辑(JavaScript)

这种分离对于创建 web 页面非常有效,因为传统上,我们有不同的人处理 web 页面的不同部分:一个人使用 HTML 构建内容并使用 CSS 设置样式,然后另一个人使用 JavaScript 实现 web 页面上各种元素的动态行为。这是一种以内容为中心的方法。

今天,我们基本上不再认为网站是网页的集合。相反,我们构建的 web 应用可能只有一个网页,而该网页并不代表我们内容的布局,它代表了 web 应用的容器。这样一个只有一个网页的 web 应用被称为单页应用SPA),这并不奇怪。您可能想知道我们如何在 SPA 中表示其余内容?当然,我们需要使用 HTML 标记创建额外的布局。否则,web 浏览器如何知道要渲染什么?

这些都是有效的问题。让我们来看看它是如何工作的。在 web 浏览器中加载网页后,它将创建该网页的文档对象模型DOM。DOM 以树状结构表示您的网页,此时,它反映了您仅使用 HTML 标记创建的布局结构。无论您是在构建传统网页还是 SPA,都会发生这种情况。两者的区别在于接下来会发生什么。如果您正在构建一个传统的网页,那么您将完成网页布局的创建。另一方面,如果您正在构建 SPA,那么您需要通过使用 JavaScript 操纵 DOM 来开始创建其他元素。web 浏览器为您提供了JavaScript DOM API来实现这一点。您可以在了解更多信息 https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model

但是,使用 JavaScript 操纵(或变异)DOM 有两个问题:

  • 如果您决定直接使用 JavaScript DOM API,那么您的编程风格将是必不可少的。正如我们在前一章中所讨论的,这种编程风格导致了更难维护的代码库。
  • DOM 突变速度很慢,因为与其他 JavaScript 代码不同,它们无法针对速度进行优化。

幸运的是,React 为我们解决了这两个问题。

了解虚拟 DOM

为什么我们首先需要操纵 DOM?这是因为我们的 web 应用不是静态的。它们的状态由 web 浏览器呈现的用户界面UI】表示,并且状态可以在事件发生时更改。我们在谈论什么样的事件?我们感兴趣的活动有两种:

  • 用户事件:当用户键入、单击、滚动、调整大小等时
  • 服务器事件:当应用从服务器接收数据或错误时

处理这些事件时会发生什么?通常,我们更新应用所依赖的数据,这些数据表示数据模型的状态。反过来,当数据模型的状态发生变化时,我们可能希望通过更新 UI 的状态来反映这种变化。看起来我们想要的是一种同步两种不同状态的方法:UI 状态和数据模型状态。我们希望一方对另一方的变化做出反应,反之亦然。我们如何才能做到这一点?

将应用的 UI 状态与基础数据模型的状态同步的方法之一是双向数据绑定。有不同类型的双向数据绑定。其中一个是键值观测KVO),用于Ember.js、敲除、主干、iOS 等。另一个是脏的检查,这是用于角度。

React 提供了一种称为虚拟 DOM的不同解决方案,而不是双向数据绑定。虚拟 DOM 是真实 DOM 的一种快速内存表示,它是一种抽象,允许我们将 JavaScript 和 DOM 视为反应式。让我们来看看它是如何工作的:

  1. 每当数据模型的状态发生更改时,虚拟 DOM 和 React 都会将 UI 重新呈现为虚拟 DOM 表示。
  2. 然后计算两个虚拟 DOM 表示之间的差异:更改数据之前计算的前一个虚拟 DOM 表示和更改数据之后计算的当前虚拟 DOM 表示。这两种虚拟 DOM 表示形式之间的差异是实际 DOM 中实际需要更改的内容。
  3. 仅更新真实 DOM 中需要更新的内容。

查找虚拟 DOM 的两种表示形式之间的差异并仅在真实 DOM 中重新渲染更新的补丁的过程非常快。另外,最好的部分是作为 React 开发人员,您不需要担心实际需要重新渲染的内容。React 允许您编写代码,就像每次应用状态更改时都重新呈现整个 DOM 一样。

如果您想了解更多关于虚拟 DOM、其背后的原理,以及如何将其与数据绑定进行比较,那么我强烈建议您观看 Pete Hunt 在 Facebook 上发表的这篇内容丰富的演讲,网址为https://www.youtube.com/watch?v=-DX3vJiqxm4

既然您已经了解了虚拟 DOM,那么让我们通过安装 React 并创建第一个 React 元素来改变一个真正的 DOM。

安装反应器

要开始使用 React 库,我们需要首先安装它。

在撰写本文时,React 库的最新版本是 16.0.0。随着时间的推移,React 会不断更新,因此请确保使用可用的最新版本,除非它引入了与本书中提供的代码示例不兼容的破坏性更改。访问https://github.com/PacktPublishing/React-Essentials-Second-Edition 了解代码样本与 React 最新版本之间的兼容性问题。

第 2 章为您的项目安装强大的工具时,我向您介绍了网页包,它允许我们使用import功能导入应用的所有依赖模块。我们还将使用import导入 React 库,这意味着我们将使用npm install命令安装 React,而不是在index.html文件中添加<script>标记:

  1. 导航到~/snapterest/目录并运行此命令:

    ```jsx npm install --save react react-dom

    ```

  2. 然后,在文本编辑器中打开~/snapterest/source/app.js文件,将 React 和 ReactDOM 库分别导入ReactReactDOM变量:

    jsx import React from 'react'; import ReactDOM from 'react-dom';

react包包含与 React 背后的关键思想有关的方法,即以声明的方式描述要呈现的内容。另一方面,react-dom包提供了负责呈现给 DOM 的方法。你可以在上阅读更多关于 Facebook 开发者为什么认为将 React 库分成两个包是个好主意的信息 https://facebook.github.io/react/blog/2015/07/03/react-v0.14-beta-1.html#two-套餐

现在,我们准备在项目中开始使用 React 库。接下来,让我们创建第一个 React 元素!

使用 JavaScript 创建 React 元素

我们将从熟悉基本术语开始。它将帮助我们清楚地了解 React 库是由什么组成的。这个术语很可能会随着时间的推移而更新,所以请注意位于的官方文档 https://facebook.github.io/react/docs/react-api.html

就像 DOM 是一棵节点树一样,React 的虚拟 DOM 也是一棵 React 节点树。React 中的一种核心类型称为ReactNode。它是虚拟 DOM 的构建块,可以是以下任何一种核心类型:

  • ReactElement:这是 React 中的主要类型。它是一个DOMElement的轻量级、无状态、不变的虚拟表示。
  • ReactText:这是一个字符串或数字。它表示文本内容,是 DOM 中文本节点的虚拟表示。

ReactElementReactTextReactNodeReactNode的数组称为ReactFragment。您将在本章中看到所有这些的示例。

让我们从一个ReactElement的例子开始:

  1. 将以下代码添加到您的~/snapterest/source/app.js文件中:

    jsx const reactElement = React.createElement('h1'); ReactDOM.render(reactElement, document.getElementById('react-application'));

  2. 现在您的app.js文件应该与以下内容完全相同:

    ```jsx import React from 'react'; import ReactDOM from 'react-dom';

    const reactElement = React.createElement('h1'); ReactDOM.render( reactElement, document.getElementById('react-application') ); ```

  3. Navigate to the ~/snapterest/ directory and run this command:

    ```jsx npm start

    ```

    您将看到以下输出:

    ```jsx Hash: 826f512cf95a44d01d39 Version: webpack 3.8.1 Time: 1851ms

    ```

  4. 导航到~/snapterest/build/目录,并在 web 浏览器中打开index.html。您将看到一个空白网页。在 web 浏览器中打开开发者工具,检查空白网页的 HTML 标记。您应该看到这一行,以及其他内容:

    jsx <h1 data-reactroot></h1>

做得好!我们刚刚渲染了您的第一个 React 元素。让我们看看我们是怎么做到的。

React 库的入口点是React对象。此对象有一个名为createElement()的方法,该方法接受三个参数:typepropschildren

React.createElement(type, props, children);

让我们更详细地看一下每个参数。

类型参数

type参数可以是字符串,也可以是ReactClass

  • 字符串可以是 HTML 标记名,例如'div''p''h1'。React 支持所有常见的 HTML 标记和属性。有关 React 支持的 HTML 标记和属性的完整列表,请参考https://facebook.github.io/react/docs/dom-elements.html
  • 通过React.createClass()方法创建ReactClass类。我将在第 4 章中更详细地介绍这一点,创建您的第一个 React 组件

type参数描述如何呈现 HTML 标记或ReactClass类。在我们的示例中,我们呈现的是h1HTML 标记。

道具参数

props参数是一个从父元素传递到子元素(而不是相反)的 JavaScript 对象,其某些属性被认为是不可变的,即不应更改的属性。

在使用 React 创建 DOM 元素时,我们可以使用表示 HTML 属性的属性(如classstyle)传递props对象。例如,运行以下代码:

import React from 'react';
import ReactDOM from 'react-dom';

const reactElement = React.createElement(
  'h1', { className: 'header' }
);
ReactDOM.render(
  reactElement,
  document.getElementById('react-application')
);

前面的代码将创建一个h1HTML 元素,其class属性设置为header

<h1 data-reactroot class="header"></h1>

请注意,我们将我们的财产命名为className而不是class。原因是class关键字在 JavaScript 中是保留的。如果您使用class作为属性名称,React 将忽略它,并在 web 浏览器控制台上打印一条有用的警告消息:

警告:未知的 DOM 属性类。你是说班名吗?

改用类名。

您可能想知道这个data-reactroot属性在我们的h1标记中做了什么?我们没有把它传给我们的props对象,那么它是从哪里来的呢?React 添加并使用它来跟踪 DOM 节点。

子参数

children参数描述这个 html 元素应该有哪些子元素(如果有的话)。子元素可以是任何类型的ReactNode:由ReactElement表示的虚拟 DOM 元素,由ReactText表示的字符串或数字,或其他ReactNode节点的数组,也称为ReactFragment

让我们来看看这个例子:

import React from 'react';
import ReactDOM from 'react-dom';

const reactElement = React.createElement(
  'h1',
  { className: 'header' },
  'This is React'
);
ReactDOM.render(
  reactElement,
  document.getElementById('react-application')
);

前面的代码将创建一个具有class属性和文本节点This is Reacth1HTML 元素:

<h1 data-reactroot class="header">This is React</h1>

h1标记用ReactElement表示,This is React字符串用ReactText表示。

接下来,让我们创建一个 React 元素,并将许多其他 React 元素作为其子元素:

import React from 'react';
import ReactDOM from 'react-dom';

const h1 = React.createElement(
  'h1',
  { className: 'header', key: 'header' },
  'This is React'
);
const p = React.createElement(
  'p', 
  { className: 'content', key: 'content' },
  'And that is how it works.'
);
const reactFragment = [ h1, p ];
const section = React.createElement(
  'section',
  { className: 'container' },
  reactFragment
);

ReactDOM.render(
  section,
  document.getElementById('react-application')
);

我们已经创建了三个 React 元素:h1psectionh1p都有子文本节点'This is React''And that is how it works.'section标记有一个子元素,它是两种ReactElement类型h1p的数组,称为reactFragment。这也是ReactNode的一个数组。reactFragment数组中的每个ReactElement类型必须具有一个key属性,该属性有助于对标识该ReactElement类型做出反应。因此,我们得到以下 HTML 标记:

<section data-reactroot class="container">
  <h1 class="header">This is React</h1>
  <p class="content">And that is how it works.</p>
</section>

现在我们了解了如何创建 React 元素。如果我们想创建许多相同类型的 React 元素,该怎么办?这是否意味着我们需要为相同类型的每个元素反复调用React.createElement('type')?我们可以,但我们不需要,因为 React 为我们提供了一个名为React.createFactory()的工厂函数。工厂函数是创建其他函数的函数。这正是React.createFactory(type)所做的:它创建一个函数,生成给定类型的ReactElement

考虑下面的例子:

import React from 'react';
import ReactDOM from 'react-dom';

const listItemElement1 = React.createElement(
  'li',
  { className: 'item-1', key: 'item-1' },
  'Item 1'
);
const listItemElement2 = React.createElement(
  'li',
  { className: 'item-2', key: 'item-2' },
  'Item 2'
);
const listItemElement3 = React.createElement(
  'li',
  { className:   'item-3', key: 'item-3' },
  'Item 3'
);

const reactFragment = [
  listItemElement1,
  listItemElement2,
  listItemElement3
];
const listOfItems = React.createElement(
  'ul',
  { className: 'list-of-items' },
  reactFragment
);

ReactDOM.render(
  listOfItems,
  document.getElementById('react-application')
);

前面的示例生成以下 HTML:

<ul data-reactroot class="list-of-items">
  <li class="item-1">Item 1</li>
  <li class="item-2">Item 2</li>
  <li class="item-3">Item 3</li>
</ul>

我们可以先创建一个工厂函数来简化它:

import React from 'react';
import ReactDOM from 'react-dom';

const createListItemElement = React.createFactory('li');

const listItemElement1 = createListItemElement(
  { className: 'item-1', key: 'item-1' },
  'Item 1'
);
const listItemElement2 = createListItemElement(
  { className: 'item-2', key: 'item-2' },
  'Item 2'
);
const listItemElement3 = createListItemElement(
  { className: 'item-3', key: 'item-3' },
  'Item 3'
);

const reactFragment = [
  listItemElement1,
  listItemElement2,
  listItemElement3
];
const listOfItems = React.createElement(
  'ul',
  { className: 'list-of-items' },
  reactFragment
);

ReactDOM.render(
  listOfItems,
  document.getElementById('react-application')
);

在前面的示例中,我们首先调用React.createFactory()函数并将liHTML 标记名作为类型参数传递。然后,React.createFactory()函数返回一个新的函数,我们可以用它作为创建li类型元素的方便快捷方式。我们将对该函数的引用存储在名为createListItemElement的变量中。然后,我们调用这个函数三次,每次只传递propschildren参数,这两个参数对于每个元素都是唯一的。请注意,React.createElement()React.createFactory()都期望 HTML 标记名字符串(如liReactClass对象作为类型参数。

React 为我们提供了许多内置的工厂函数来创建常见的 HTML 标记。您可以从React.DOM对象调用它们;例如,React.DOM.ul()React.DOM.li()React.DOM.div()。使用它们,我们可以进一步简化前面的示例:

import React from 'react';
import ReactDOM from 'react-dom';

const listItemElement1 = React.DOM.li(
  { className: 'item-1', key: 'item-1' },
  'Item 1'
);
const listItemElement2 = React.DOM.li(
  { className: 'item-2', key: 'item-2' },
  'Item 2'
);
const listItemElement3 = React.DOM.li(
  { className: 'item-3', key: 'item-3' },
  'Item 3'
);

const reactFragment = [
  listItemElement1,
  listItemElement2,
  listItemElement3
];
const listOfItems = React.DOM.ul(
  { className: 'list-of-items' },
  reactFragment
);

ReactDOM.render(
  listOfItems,
  document.getElementById('react-application')
);

现在,我们知道了如何创建ReactNode的树。然而,在我们继续前进之前,我们需要讨论一行重要的代码:

ReactDOM.render(
  listOfItems,
  document.getElementById('react-application')
);

正如您可能已经猜到的,它将我们的ReactNode树呈现给 DOM。让我们仔细看看它是如何工作的。

渲染元素

ReactDOM.render()方法有三个参数:ReactElement、一个常规DOMElement容器和一个callback函数:

ReactDOM.render(ReactElement, DOMElement, callback);

ReactElement类型是您创建的ReactNode树中的根元素。常规的DOMElement参数是该树的容器 DOM 节点。callback参数是树呈现或更新后执行的函数。需要注意的是,如果此ReactElement类型之前已呈现给父DOMElement容器,则ReactDOM.render()将对已呈现的 DOM 树执行更新,并仅对 DOM 进行变异,因为需要反映ReactElement类型的最新版本。这就是为什么虚拟 DOM 需要更少的 DOM 突变。

到目前为止,我们假设我们总是在 web 浏览器中创建虚拟 DOM。这是可以理解的,因为 React 毕竟是一个用户界面库,所有用户界面都在 web 浏览器中呈现。您能想到在客户端上呈现用户界面会很慢的情况吗?你们中的一些人可能已经猜到了,我说的是初始页面加载。初始页面加载的问题是我在本章开头提到的,我们不再创建静态网页。相反,当 web 浏览器加载 web 应用时,它只接收通常用作 web 应用的容器或父元素的最小 HTML 标记。然后,我们的 JavaScript 代码创建 DOM 的其余部分,但为了实现这一点,它通常需要从服务器请求额外的数据。然而,获取这些数据需要时间。一旦接收到这些数据,我们的 JavaScript 代码就开始改变 DOM。我们知道 DOM 突变很慢。我们如何解决这个问题?

解决方案有些出乎意料。我们没有在 web 浏览器中改变 DOM,而是在服务器上改变它,就像我们在静态网页中所做的那样。然后,web 浏览器将接收一个 HTML,该 HTML 在初始页面加载时完全表示 web 应用的用户界面。听起来很简单,但我们不能改变服务器上的 DOM,因为它不存在于 web 浏览器之外。或者我们可以吗?

我们有一个只是 JavaScript 的虚拟 DOM,使用 Node.js,我们可以在服务器上运行 JavaScript。因此,从技术上讲,我们可以在服务器上使用 React 库,并在服务器上创建ReactNode树。问题是我们如何将其呈现为可以发送给客户端的字符串?

React 有一个名为ReactDOMServer.renderToString()的方法来执行此操作:

import ReactDOMServer from 'react-dom/server';
ReactDOMServer.renderToString(ReactElement);

ReactDOMServer.renderToString()方法将ReactElement作为参数,并将其呈现为初始 HTML。这不仅比在客户端上修改 DOM 更快,而且还改进了 web 应用的搜索引擎优化SEO)。

说到生成静态网页,我们也可以使用 React:

import ReactDOMServer from 'react-dom/server';
ReactDOMServer.renderToStaticMarkup(ReactElement);

ReactDOMServer.renderToString()类似,该方法也以ReactElement为参数,输出 HTML 字符串。但是,它不会创建内部使用的额外 DOM 属性,从而生成更短的 HTML 字符串,我们可以快速将其传输到网络。

现在,您不仅知道如何使用 React 元素创建虚拟 DOM 树,还知道如何将其呈现给客户机和服务器。我们的下一个问题是,我们是否能够以更直观的方式快速完成。

使用 JSX 创建 React 元素

当我们通过不断调用React.createElement()方法来构建虚拟 DOM 时,要将这些多个函数调用可视化地转换为 HTML 标记的层次结构就变得非常困难了。不要忘记,即使我们使用虚拟 DOM,我们仍然在为内容和用户界面创建结构布局。通过简单地查看我们的 React 代码,就可以轻松地可视化布局,这不是很好吗?

JSX是一种类似 HTML 的可选语法,允许我们创建虚拟 DOM 树,而无需使用React.createElement()方法。

让我们看一下我们在没有 JSX 的情况下创建的例子:

import React from 'react';
import ReactDOM from 'react-dom';

const listItemElement1 = React.DOM.li(
  { className: 'item-1', key: 'item-1' },
  'Item 1'
);
const listItemElement2 = React.DOM.li(
  { className: 'item-2', key: 'item-2' },
  'Item 2'
);
const listItemElement3 = React.DOM.li(
  { className: 'item-3', key: 'item-3' },
  'Item 3'
);

const reactFragment = [
  listItemElement1,
  listItemElement2,
  listItemElement3
];
const listOfItems = React.DOM.ul(
  { className: 'list-of-items' },
  reactFragment
);

ReactDOM.render(
  listOfItems,
  document.getElementById('react-application')
);

将此转换为使用 JSX 的:

import React from 'react';
import ReactDOM from 'react-dom';

const listOfItems = (
  <ul className="list-of-items">
    <li className="item-1">Item 1</li>
    <li className="item-2">Item 2</li>
    <li className="item-3">Item 3</li>
  </ul>
);

ReactDOM.render(
  listOfItems,
  document.getElementById('react-application')
);

如您所见,JSX 允许我们在 JavaScript 代码中编写类似 HTML 的语法。更重要的是,我们现在可以清楚地看到,一旦呈现出来,HTML 布局会是什么样子。JSX 是一个方便的工具,它以附加转换步骤的形式提供价格。在解释“无效”JavaScript 代码之前,必须将 JSX 语法转换为有效的 JavaScript 语法。

在上一章中,我们安装了babel-preset-react模块,将 JSX 语法转换为有效的 JavaScript。每次运行 Webpack 时都会发生这种转换。导航到~/snapterest/并运行此命令:

npm start

为了更好地理解 JSX 语法,我建议您使用 Babel REPL 工具:https://babeljs.io/repl/ -它可以动态地将 JSX 语法转换为纯 JavaScript。

使用 JSX,一开始您可能会感到非常不寻常,但它可以成为一个非常直观和方便的工具。最好的部分是你可以选择是否使用它。我发现 JSX 节省了我的开发时间,所以我选择在我们正在构建的项目中使用它。

如果您对本章中讨论的内容有问题,那么您可以参考https://github.com/fedosejev/react-essentials 并创建新版本。

总结

本章首先讨论了单网页应用的问题以及如何解决这些问题。然后,您了解了什么是虚拟 DOM,以及 React 如何允许我们构建虚拟 DOM。我们还安装了 React,并仅使用 JavaScript 创建了第一个 React 元素。然后,您还学习了如何在 web 浏览器和服务器上渲染 React 元素。最后,我们研究了一种使用 JSX 创建 React 元素的简单方法。

在下一章中,我们将深入了解 React 组件的世界。