今天创建一个简单的 web 应用需要编写 HTML、CSS 和 JavaScript 代码。我们使用三种不同技术的原因是我们希望将三种不同的关注点分开:
- 内容(HTML)
- 样式(CSS)
- 逻辑(JavaScript)
这种分离对于创建网页非常有用,因为通常情况下,我们有不同的人处理网页的不同部分:一个人使用 HTML 构建内容并使用 CSS 对其进行样式化,然后另一个人使用 JavaScript 实现网页上各种元素的动态行为。这是一种以内容为中心的方法。
今天,我们基本上不再将网站视为网页的集合。相反,我们构建的 web 应用可能只有一个网页,而该网页并不代表我们内容的布局,而是代表我们 web 应用的容器。这种具有单个网页的 web 应用被称为(毫不奇怪的)Single Page Application (SPA) ——单页应用。你可能想知道我们如何在 SPA 中表示其余内容?当然,我们需要使用 HTML 标签创建一个额外的布局。否则,web 浏览器如何知道要渲染什么呢?
这些都是有效的问题。让我们看看它是如何工作的。在 web 浏览器中加载网页后,它将创建该网页的 Document Object Model (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)表示的状态,并且当事件发生时可以更改该状态。我们在谈论什么样的事件?我们感兴趣的事件有两种类型:
- User events(用户事件):当用户键入、单击、滚动、调整大小等时
- Server events(服务器事件):当应用从服务器接收数据或错误时
处理这些事件时会发生什么?通常,我们更新应用所依赖的数据,这些数据表示数据模型的状态。相反的,当数据模型的状态发生变化时,我们可能希望通过更新 UI 的状态来反映这种变化。看起来我们想要的是一种同步两种不同状态的方法:UI 状态和数据模型状态。我们希望一方对另一方的变化做出响应,反之亦然。我们如何能做到这一点呢?
将应用的 UI 状态与基础数据模型的状态同步的方法之一是使用双向数据绑定。有不同类型的双向数据绑定。其中之一是 Ember 中使用的 Key-Value Observation(KVO) ——关键值观察、Node.js、Knockout、Backbone 和 iOS 等。另一个是 Angular 中使用的脏检查。
React 提供了一种称为 virtua(虚拟)DOM.虚拟 DOM 的不同解决方案,而不是双向数据绑定。虚拟 DOM 是真实 DOM 的快速内存表示,它是一种抽象,允许我们将 JavaScript 和 DOM 视为响应式的。让我们看看它是如何工作的:
- 每当数据模型的状态发生变化时,虚拟 DOM 和 React 都会将 UI 重新渲染为虚拟 DOM 表示。
- 然后,它计算两个虚拟 DOM 表示之间的差异:在数据更改之前计算的先前虚拟 DOM 表示和在数据更改之后计算的当前虚拟 DOM 表示。这两个虚拟 DOM 表示之间的差异实际上需要在真实 DOM 中进行更改。
- 只更新真实 DOM 中需要更新的内容。
找到虚拟 DOM 的两个表示之间的差异并在真实 DOM 中仅重新渲染更新补丁的过程很快。此外,作为 React 开发人员,最好的地方是你不必担心实际需要重新渲染的内容。React 允许你编写代码,就像每次应用状态更改时都重新渲染整个 DOM 一样。
如果你想了解更多关于虚拟 DOM 背后的原理,以及如何将其与数据绑定进行比较,那么我强烈建议你在 Facebook 上观看 Pete Hunt 的这篇内容丰富的 演讲。
既然你已经了解了虚拟 DOM,那么让我们通过安装 React 并创建你的第一个 React 元素来改变真实的 DOM。
安装 React
要开始使用 React 库,我们需要首先安装它。
在撰写本文时,React 库的最新版本是 16.0.0。随着时间的推移,React 会得到更新,因此确保你使用最新版本,除非它引入了与本书中提供的代码示例不兼容的突破性更改。参看 https://github.com/PacktPublishing/React-Essentials,了解代码示例与 React 最新版本之间的任何兼容性问题。
在第 2 章为你的项目安装强大的工具中,我向你介绍了 Webpack
,它允许我们使用导入功能导入应用的所有依赖模块。我们还将使用 import 来导入 React 库,这意味着不需要在 index.html
文件中添加 <script>
标签,我们将使用 npm install
命令来安装 React:
- 转到
~/snapterest/
目录并运行以下命令:
npm install --save react react-dom
- 然后,打开
~/snapterest/source/app.js
文件,并将 React 和 ReactDOM 库分别导入React
和React DOM
变量:
import React from 'react';
import ReactDOM from 'react-dom';
react
模块包含与 react 背后的关键思想有关的方法,即以声明式的方式描述要渲染的内容。另一方面,react dom
模块提供了负责渲染到 dom 的方法。你可以阅读更多关于为什么 Facebook 的开发者认为 将 React 库分成两个包 是个好主意。
现在,我们准备开始在项目中使用 React 库。接下来,让我们创建你的第一个 React 元素!
使用 JavaScript 创建 React 元素
我们将首先熟悉 React 的基本术语。它将帮助我们清楚地了解 React 库是由什么组成的。这个术语很可能会随着时间的推移而更新,因此请关注以下网站的 官方文档。
就像 DOM 是一棵节点树一样,React 的虚拟 DOM 也是一棵 ReactNode
树。React 中的核心类型之一叫做 ReactNode
。它是虚拟 DOM 的构建块,可以是以下任何一种核心类型:
ReactElement
:这是 React 中的主要类型。它是DOMElement
的一个轻量级、无状态、不可变的虚拟表示。ReactText
:这是一个字符串或数字。它表示文本内容,是 DOM 中文本节点的虚拟表示。
ReactElement
和 ReactText
都是 ReactNode
。ReactNode
的数组称为 ReactFragment
。在本章中,你将看到所有这些的示例。让我们从 ReactElement
的一个示例开始:
- 将以下代码添加到
~/snapterest/source/app.js
中文件:
const reactElement = React.createElement('h1');
ReactDOM.render(reactElement, document.getElementById('react-application'));
- 现在是你的应用
app.js
文件应该完全像这样:
import React from 'react';
import ReactDOM from 'react-dom';
const reactElement = React.createElement('h1');
ReactDOM.render(reactElement, document.getElementById('react-application'));
- 转到
~/snapterest/
目录并运行以下命令:
npm start
你将看到以下输出:
Hash: 826f512cf95a44d01d39
Version: webpack 3.8.1
Time: 1851ms
- 转到
~/snapterest/build/
目录,然后打开index.html
在 web 浏览器中。你将看到一个空白网页。在 web 浏览器中打开 Developer 工具,检查空白网页的 HTML 标签。你应该看到这一行,其中包括:
<h1 data-reactroot></h1>
干得不错!我们刚刚渲染了第一个 React 元素。让我们看看我们是怎么做到的。
React 库的入口是 React
对象。此对象有一个名为 createElement()
的方法,它接受三个参数:type
、props
和 children
:
React.createElement(type, props, children);
让我们更详细地了解每个参数。
类型参数
类型参数可以是字符串或 ReactClass
:
- 字符串可以是 HTML 标签名,例如
div
、p
和h1
。React 支持所有常见的 HTML 标签和属性。有关 React 支持的 HTML 标签和属性的完整列表,请参阅 https://facebook.github.io/React/docs/dom-elements.html。 ReactClass
是通过 React.createClass() 方法创建的。我将在第 4 章创建你的第一个 React 组件中更详细地介绍这一点。
type
参数描述如何渲染 HTML 标签或 ReactClass
类。在我们的示例中,我们渲染 h1
HTML 标签。
props 参数
props
参数是一个从父元素传递到子元素(而不是相反)的 JavaScript 对象,它具有一些被认为是不可变的属性,即那些不应该更改的属性。
在使用 React 创建 DOM 元素时,我们可以传递带有表示 HTML 属性(如 class
和 style
)的属性的 props
对象。例如,运行以下代码:
import React from 'react';
import ReactDOM from 'react-dom';
const reactElement = React.createElement('h1', { className: 'header' });
ReactDOM.render(reactElement, document.getElementById('react-application'));
前面的代码将创建一个 h1
HTML 元素,其 class
属性设置为 header
:
`<h1 data-reactroot className="header"></h1>`;
请注意,我们将属性命名为
className
而不是class
。原因是class
关键字在 JavaScript 中保留。如果你使用class
作为属性名,React 将忽略它,并在 web 浏览器的控制台上打印一条有用的警告消息:警告:Unknown DOM property class. Did you mean className?
Use className instead.
你可能想知道这个 data-reactroot
属性在 h1
标签中做什么?我们没有将它传递给我们的道 props
对象,所以它是从哪里来的?React 添加并使用它来跟踪 DOM 节点。
children 参数
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'));
前面的代码将创建一个 h1
HTML 元素,该元素带有 class
属性和文本节点 This is React
:
<h1 data-reactort 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 元素:h1
、p
和 section
。h
1 和 p
都有子文本节点,分别是 This is React
和 And that is how it works.
。section
标签有一个子元素,它是两个 ReactElement
类型 h1
和 p
的数组,称为 reactFragment
。这也是 ReactNode 的数组。reactFragment
数组中的每个 ReactElement
类型都必须有一个键属性,该属性可以帮助 React 识别该 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)
所作的:创建了一个生成指定 React 元素类型的的函数。
参考以下示例:
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()
方法,并传递一个 li
HTML 标签名作为类型参数。React.createFactory()
方法会返回一个新函数,我们可以使用它作为创建 li
类型元素的便捷速记。我们将对该函数的引用存储在名为 createListItemElement
的变量中。然后,我们调用这个函数三次,每次只传递 props
和 children
参数,这些参数对于每个元素都是唯一的。
React.createElement()
和React.createFactory()
都需要 HTML 标签名字符串(如li
)或ReactClass
对象作为类型参数。
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。让我们仔细看看它是如何工作的。
渲染 React 元素
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()
,仅用于执行 this.props:
import ReactDOMServer from 'react-dom/server';
ReactDOMServer.renderToString(ReactElement);
ReactDOMServer.renderToString()
方法将 ReactElement
作为参数,并将其渲染为初始 HTML。这不仅比在客户端上改变 DOM 更快,而且还提高了 web Search Engine Optimization (SEO)——应用的搜索引擎优化。
说到生成静态网页,我们也可以使用 React 做到这一点:
import ReactDOMServer from 'react-dom/server';
ReactDOMServer.renderToStaticMarkup(ReactElement);
类似于 ReactDOMServer.renderToString()
,该方法还将 ReactElement
作为参数并输出 HTML 字符串。然而,它不会创建 React 内部使用的额外 DOM 属性,从而生成更短的 HTML 字符串,我们可以快速将其传输到网络。
现在,你不仅知道如何使用 ReactElement
创建虚拟 DOM 树,还知道如何将其渲染给客户端和服务器。我们的下一个问题是,我们是否能以更直观的方式快速创建 ReactElement
。
使用 JSX 创建 React 元素
当我们通过不断调用 React.createElement()
方法来构建虚拟 DOM 时,将这些多个函数调用可视化地转换为 HTML 标签的层次结构变得非常困难。不要忘记,即使我们使用的是虚拟 DOM,我们仍然在为内容和用户界面创建结构布局。只需查看我们的 React 代码,就能轻松地可视化布局,这不是很好吗?
JSX 是一种可选的类似 HTML 的语法,它允许我们在不使用 React.createElement()
方法的情况下创建虚拟 DOM 树。
让我们看一下我们在没有 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
现在转到 ~/snapterest/build/
目录,并打开 index.html
在 web 浏览器中。你将看到三个项目的列表。结果是一样的,只是这次你使用 JSX 创建了这个 HTML 标签。
为了更好地理解 JSX 语法,我建议你使用 Babel REPL 工具:——它将你的 JSX 语法快速转换为纯 JavaScript。
使用 JSX,一开始可能会觉得很不寻常,但它可以成为一个非常直观和方便的工具。最好的地方是你可以选择是否使用它。我发现 JSX 节省了我的开发时间,所以我选择在我们正在构建的项目中使用它。
如果你对我们在本章中讨论的内容有疑问,可以参考 https://github.com/PacktPublishing/React-Essentials-Second-Edition 并新建问题。
总结
本章开始时,我们讨论了单个网页应用的问题以及如何解决这些问题。然后,你了解了什么是虚拟 DOM,以及 React 如何允许我们构建虚拟 DOM。我们还安装了 React,并仅使用 JavaScript 创建了第一个 React 元素。然后,你还学习了如何在 web 浏览器和服务器上渲染 React 元素。最后,我们研究了使用 JSX 创建 React 元素的一种更简单的方法。
下一章我们将深入了解 React 组件的世界。
评论 (0)