现在创建一个简单的 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?这是因为我们的 web 应用不是静态的。它们的状态由 web 浏览器呈现的用户界面(**UI】**表示,并且状态可以在事件发生时更改。我们在谈论什么样的事件?我们感兴趣的活动有两种:
- 用户事件:当用户键入、单击、滚动、调整大小等时
- 服务器事件:当应用从服务器接收数据或错误时
处理这些事件时会发生什么?通常,我们更新应用所依赖的数据,这些数据表示数据模型的状态。反过来,当数据模型的状态发生变化时,我们可能希望通过更新 UI 的状态来反映这种变化。看起来我们想要的是一种同步两种不同状态的方法:UI 状态和数据模型状态。我们希望一方对另一方的变化做出反应,反之亦然。我们如何才能做到这一点?
将应用的 UI 状态与基础数据模型的状态同步的方法之一是双向数据绑定。有不同类型的双向数据绑定。其中一个是键值观测(KVO),用于Ember.js、敲除、主干、iOS 等。另一个是脏的检查,这是用于角度。
React 提供了一种称为虚拟 DOM的不同解决方案,而不是双向数据绑定。虚拟 DOM 是真实 DOM 的一种快速内存表示,它是一种抽象,允许我们将 JavaScript 和 DOM 视为反应式。让我们来看看它是如何工作的:
- 每当数据模型的状态发生更改时,虚拟 DOM 和 React 都会将 UI 重新呈现为虚拟 DOM 表示。
- 然后计算两个虚拟 DOM 表示之间的差异:更改数据之前计算的前一个虚拟 DOM 表示和更改数据之后计算的当前虚拟 DOM 表示。这两种虚拟 DOM 表示形式之间的差异是实际 DOM 中实际需要更改的内容。
- 仅更新真实 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>标记:
-
导航到
~/snapterest/目录并运行此命令:npm install --save react react-dom
-
然后,在文本编辑器中打开
~/snapterest/source/app.js文件,将 React 和 ReactDOM 库分别导入React和ReactDOM变量: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 元素!
我们将从熟悉基本术语开始。它将帮助我们清楚地了解 React 库是由什么组成的。这个术语很可能会随着时间的推移而更新,所以请注意位于的官方文档 https://facebook.github.io/react/docs/react-api.html 。
就像 DOM 是一棵节点树一样,React 的虚拟 DOM 也是一棵 React 节点树。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') );
-
Navigate to the
~/snapterest/directory and run this command:npm start
您将看到以下输出:
Hash: 826f512cf95a44d01d39 Version: webpack 3.8.1 Time: 1851ms
-
导航到
~/snapterest/build/目录,并在 web 浏览器中打开index.html。您将看到一个空白网页。在 web 浏览器中打开开发者工具,检查空白网页的 HTML 标记。您应该看到这一行,以及其他内容:<h1 data-reactroot></h1>
做得好!我们刚刚渲染了您的第一个 React 元素。让我们看看我们是怎么做到的。
React 库的入口点是React对象。此对象有一个名为createElement()的方法,该方法接受三个参数:type、props和children:
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 属性的属性(如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')
);前面的代码将创建一个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 React的h1HTML 元素:
<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 元素:h1、p和section。h1和p都有子文本节点'This is React'和'And that is how it works.'。section标记有一个子元素,它是两种ReactElement类型h1和p的数组,称为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的变量中。然后,我们调用这个函数三次,每次只传递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。让我们仔细看看它是如何工作的。
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 树,还知道如何将其呈现给客户机和服务器。我们的下一个问题是,我们是否能够以更直观的方式快速完成。
当我们通过不断调用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 组件的世界。