React16 高级教程(一)
原文:Pro React 16
一、您的第一个 React 应用
开始使用 React 的最佳方式是深入研究。在这一章中,我将带你通过一个简单的开发过程来创建一个跟踪待办事项的应用。在第五章第五章–第八章中,我将向您展示如何创建一个更加复杂和真实的应用,但是,现在,一个简单的例子将足以展示 React 应用是如何创建的,以及基本功能是如何工作的。如果您不理解本章的所有内容,也不要担心——这是为了对 React 的工作原理有一个总体的了解。我会在后面的章节中详细解释一切。
注意
如果您需要对 React 特性的常规描述,可以跳到本书的第二部分,在那里我将开始深入描述各个特性的过程。在您开始之前,确保您安装了本章中描述的开发工具和软件包。
准备开发环境
React 开发需要一些准备工作。在接下来的部分中,我将解释如何设置和准备创建您的第一个项目。
安装 Node.js
用于 React 开发的工具依赖于 Node.js,也称为 Node,它创建于 2009 年,是用 JavaScript 编写的服务器端应用的简单高效的运行时。Node.js 基于 Chrome 浏览器中使用的 JavaScript 引擎,并提供了一个在浏览器环境之外执行 JavaScript 代码的 API。
Node.js 作为应用服务器已经取得了成功,但对于本书来说,它很有趣,因为它为新一代跨平台开发和构建工具提供了基础。
重要的是,您下载的 Node.js 版本与我在本书中使用的版本相同。尽管 Node.js 相对稳定,但仍不时会有突破性的 API 变化,这可能会使我在本章中包含的示例无法工作。我使用的版本是 10.14.1,这是我撰写本文时的最新长期支持版本。在您阅读本文时,可能会有更高的版本,但是对于本书中的示例,您应该坚持使用 10.14.1 版本。在 https://nodejs.org/dist/v10.14.1 可以获得完整的 10.14.1 版本,包括 Windows 和 macOS 的安装程序以及其他平台的二进制包。
安装 Node.js 时,请确保选择了将 Node.js 可执行文件添加到路径的选项。安装完成后,运行清单 1-1 中所示的命令。
node -v
Listing 1-1Checking the Node Version
如果安装正常进行,您将会看到下面显示的版本号:
v10.14.1
Node.js 安装包括节点包管理器(NPM),用于管理项目中的包。运行清单 1-2 中所示的命令,确保 NPM 正在工作。
npm -v
Listing 1-2Checking NPM Works
如果一切正常,您将看到以下版本号:
6.4.1
安装 create-react-app 包
create-react-app包是创建和管理复杂的 React 包的标准方式,为开发人员提供了完整的工具链。开始使用 React 还有其他方法,但这是最适合大多数项目的方法,也是我在本书中一直使用的方法。
要安装这个包,打开一个新的命令提示符并运行清单 1-3 中所示的命令。如果使用的是 Linux 或者 macOS,可能需要使用sudo。
npm install --global create-react-app@2.1.2
Listing 1-3Installing the create-react-app Package
安装 Git
需要 Git 修订版控制工具来管理 React 开发所需的一些包。如果您使用的是 Windows 或 macOS,那么从 https://git-scm.com/downloads 下载并运行安装程序。(在 macOS 上,您可能需要更改安全设置才能打开安装程序,开发人员尚未对该安装程序进行签名。)
Git 已经包含在大多数 Linux 发行版中。如果您想安装最新版本,请查阅 https://git-scm.com/download/linux 上的安装说明。举个例子,对于我使用的 Linux 发行版 Ubuntu,我使用了清单 1-4 中所示的命令。
sudo apt-get install git
Listing 1-4Installing Git
一旦完成安装,打开一个新的命令提示符并运行清单 1-5 中所示的命令,检查 Git 是否已安装并可用。
git --version
Listing 1-5Checking Git
这个命令打印出已经安装的 Git 包的版本。在撰写本文时,针对 Windows 和 Linux 的 Git 最新版本是 2.20.1,针对 macOS 的 Git 最新版本是 2.19.2。
安装编辑器
React 开发可以用任何一个程序员的编辑器来完成,从中有数不尽的选择。一些编辑器增强了对使用 React 的支持,包括突出显示关键字和表达式。如果您还没有 web 应用开发的首选编辑器,那么您可以考虑表 1-1 中的一些流行选项。对于这本书,我不依赖任何特定的编辑器,你应该使用任何你觉得舒服的编辑器。
表 1-1
流行的编程编辑
|名字
|
描述
|
| --- | --- |
| 崇高的文本 | Sublime Text 是一个商业跨平台编辑器,它有支持大多数编程语言、框架和平台的包。详见 www.sublimetext.com 。 |
| 原子 | Atom 是一个开源的跨平台编辑器,特别强调定制和可扩展性。详见atom.io。 |
| 括号 | 括号是 Adobe 开发的免费开源编辑器。详见brackets.io。 |
| Visual Studio 代码 | Visual Studio Code 是微软的一款开源的跨平台编辑器,强调可扩展性。详见code.visualstudio.com。 |
| 可视化工作室 | Visual Studio 是微软的旗舰开发工具。有免费版和商业版可用,它附带了大量集成到 Microsoft 生态系统中的附加工具。 |
安装浏览器
最后要选择的是在开发过程中用来检查工作的浏览器。所有当代浏览器都有良好的开发人员支持,并且与 React 配合良好,但 Chrome 和 Firefox 有一个名为react-devtools的有用扩展,它提供了对 React 应用状态的洞察,在复杂项目中特别有用。安装分机详见 https://github.com/facebook/react-devtools 。在这本书里,我一直使用谷歌浏览器,这是我推荐你使用的浏览器。
创建项目
从命令行创建和管理项目。打开一个新的命令提示符,导航到一个方便的位置,运行清单 1-6 中所示的命令,为本章创建项目。
小费
你可以从 https://github.com/Apress/pro-react-16 下载本章以及本书其他章节的示例项目。
npx create-react-app todo
Listing 1-6Creating the Project
在上一节中,npx命令是作为 Node.js/NPM 包的一部分安装的,用于运行 Node.js 包。create-react-app参数告诉npx运行create-react-app包,该包用于创建新的 React 项目,安装在清单 1-3 中。最后一个参数是todo,这是要创建的项目的名称。当您运行这个命令时,项目将被创建,并且开发和运行 React 项目所需的所有包都将被下载和安装。安装过程可能需要一段时间,因为有大量的软件包要下载。
注意
创建新项目时,您可能会看到有关安全漏洞的警告。React 开发依赖于大量的包,每个包都有自己的依赖关系,不可避免的会发现安全问题。对于本书中的示例,使用指定的包版本以确保获得预期的结果是很重要的。对于您自己的项目,您应该查看警告并更新到解决问题的版本。
了解项目结构
使用您喜欢的编辑器打开todo文件夹,您将看到如图 1-1 所示的项目结构。该图显示了我首选的编辑器(Visual Studio)中的布局,如果您选择了不同的编辑器,您可能会看到项目内容的呈现略有不同。
图 1-1
项目结构
这是所有项目的起点,虽然每个文件的目的目前可能不明显,但在本书结束时,你会知道每个文件和文件夹的用途。目前,表 1-2 简要描述了本章重要的文件,我在第九章提供了 React 项目的详细说明。
表 1-2
本章为项目中的重要文件
|名字
|
描述
|
| --- | --- |
| public/index.html | 这是浏览器加载的 HTML 文件。它包含一个显示应用的元素和一个加载应用的 JavaScript 文件的script元素。 |
| src/index.js | 这是负责配置和启动 React 应用的 JavaScript 文件。在下一节中,我使用这个文件将引导 CSS 框架添加到应用中。 |
| src/App.js | 这是 React 组件,它包含将显示给用户的 HTML 内容和 HTML 所需的 JavaScript 代码。组件是 React 应用中的主要构件,您将在本书中看到它们的使用。 |
添加引导 CSS 框架
我使用优秀的 Bootstrap CSS 框架来设计本书中的例子所展示的 HTML。我在第三章中描述了 Bootstrap 的基本用法,但是在本章开始之前,运行清单 1-7 中所示的命令来导航到todo文件夹并将 Bootstrap 包添加到项目中。
小费
用于管理一个项目中的包的命令是npm,它与npx很容易混淆,后者仅在创建一个新项目时使用。重要的是不要混淆这两个命令。
cd todo
npm install bootstrap@4.1.2
Listing 1-7Adding the Bootstrap CSS Framework
要在应用中包含 Bootstrap,请将清单 1-8 中所示的语句添加到index.js文件中。
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import 'bootstrap/dist/css/bootstrap.css';
ReactDOM.render(<App />, document.getElementById('root'));
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: http://bit.ly/CRA-PWA
serviceWorker.unregister();
Listing 1-8Including Bootstrap in the index.js File in the src Folder
正如我在第四章中解释的那样,import语句用于声明一个依赖项,这样它就成为应用的一部分。import关键字最常用于声明对 JavaScript 代码的依赖,但是它也可以用于 CSS 样式表。
启动开发工具
当您使用create-react-app包创建一个项目时,会安装一套完整的开发工具,这样项目就可以被编译、打包并交付给浏览器。使用命令提示符,运行todo文件夹中清单 1-9 所示的命令来启动开发工具。
npm start
Listing 1-9Starting the Development Tools
开发工具启动时有一个初始准备过程,可能需要一段时间才能完成。不要因为准备所花费的时间而推迟,因为这个过程只有在您开始开发会话时才需要。启动过程完成后,您将看到如下消息,确认应用正在运行,并告诉您要连接到哪个 HTTP 端口:
Compiled successfully!
You can now view todo in the browser.
Local: http://localhost:3000/
On Your Network: http://192.168.0.77:3000/
Note that the development build is not optimized.
To create a production build, use npm run build.
用于监听 HTTP 请求的默认端口是 3000,但是如果使用 3000,将会选择不同的端口。一旦项目的初始准备工作完成,一个新的浏览器窗口将打开并显示 URL http://localhost:3000和占位符内容,如图 1-2 所示。
图 1-2
运行示例应用
替换占位符内容
图 1-2 中显示的内容是一个占位符,用于确保开发工具正常工作。为了替换默认内容,我修改了App.js文件,如清单 1-10 所示。
import React, { Component } from 'react';
//import logo from './logo.svg';
//import './App.css';
export default class App extends Component {
render() {
return (
<div>
<h4 className="bg-primary text-white text-center p-2">
To Do List
</h4>
</div>
)
};
}
Listing 1-10Removing the Placeholder in the App.js File in the src Folder
App.js文件包含一个 React 组件,命名为App。组件是 React 应用的主要构建块,它们是使用 JSX 编写的,它是 JavaScript 的超集,允许 HTML 包含在代码文件中,而不需要任何特殊的引号。我在第三章中更详细地描述了 JSX,但是在这个清单中,App组件定义了一个render方法,该方法调用 React 来获取显示给用户的内容。
小费
React 支持 JavaScript 语言中最近增加的内容,比如清单 1-10 中使用的class关键字。我在第四章中提供了最有用的 JavaScript 特性的初级读本。
当您保存App.js文件时,React 开发工具会自动检测更改,重建应用,并指示浏览器重新加载,显示图 1-3 中的内容。
图 1-3
替换占位符内容
React 开发中使用的 JSX 文件使得混合 HTML 和 JavaScript 变得容易,但是与常规的 HTML 文件有一些重要的不同。您可以在清单 1-10 的h4元素中看到一个常见的例子,如下所示:
...
<h4 className="bg-primary text-white text-center p-2">
To Do List
</h4>
...
在常规 HTML 中,class属性用于将元素分配给类,这就是使用 Bootstrap CSS 框架时元素的样式。即使看起来不是这样,JSX 文件是 JavaScript 文件,JavaScript 通过className属性配置类。当您第一次开始 React 开发时,纯 HTML 和 JSX 之间的差异可能会很不协调,但它们很快就会成为您的第二天性。
小费
我在第三章中提供了一个关于使用引导 CSS 框架的简要概述,在那里我解释了在清单 1-10 中h4元素被赋予的类的含义,比如bg-primary、text-white和p-2。但是,您可以暂时忽略这些类,只关注应用的基本结构。
如果你忘记了你正在使用 JSX,而是使用标准 HTML,React 将向浏览器的 JavaScript 控制台写一条警告消息。例如,如果您使用class属性而不是className,您将会看到Invalid DOM property 'class'. Did you mean 'className'?警告。要查看浏览器的 JavaScript 控制台,请按 F12 键并选择控制台或 JavaScript 控制台选项卡。
显示动态内容
所有的 web 应用都需要向用户显示动态内容,React 通过支持表达式特性使这变得简单。表达式是 JavaScript 的一个片段,在组件的render方法被调用时被计算,并提供向用户显示数据的方法。许多表达式用于显示组件定义的数据值,以跟踪应用的状态,称为状态数据。当您看到一个例子时,状态数据和表达式更容易理解,清单 1-11 将两者都添加到了App组件中。
import React, { Component } from 'react';
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
userName: "Adam"
}
}
render() {
return (
<div>
<h4 className="bg-primary text-white text-center p-2">
{ this.state.userName }'s To Do List
</h4>
</div>
)
};
}
Listing 1-11Adding State Data and Data Bindings in the App.js File in the src Folder
正如我在第十一章中解释的那样,constructor是一个特殊的方法,当组件被初始化时,调用构造函数中的super方法是确保组件被正确设置所必需的。由构造函数定义的props参数在 React 开发中很重要,因为它允许一个组件配置另一个组件,您很快就会看到这一点。
小费
术语 props 是属性的缩写,它反映了 React 创建显示在浏览器中的 HTML 内容的方式,正如我在第三章中解释的那样。
React 组件有一个名为state的特殊属性,用于定义状态数据,如下所示:
...
this.state = {
userName: "Adam"
}
...
this关键字引用当前对象,并用于访问其属性和方法。突出显示的语句将一个具有userName属性的对象分配给this.state,这是设置状态数据所需的全部内容。一旦定义了状态数据,它就可以包含在由表达式中的组件生成的内容中,如下所示:
...
<h4 className="bg-primary text-white text-center p-2">
{ this.state.userName }'s To Do List
</h4>
...
表达式用花括号表示({和}字符)。当调用render方法时,表达式被求值,其结果包含在呈现给用户的内容中。清单 1-11 中的表达式读取userName状态数据属性的值,产生如图 1-4 所示的结果。
图 1-4
使用 src 文件夹中 App.js 文件中的状态数据和表达式
了解状态数据更改
React 应用的动态特性基于对状态数据的更改,React 通过再次调用组件的render方法来响应这些更改,这将导致使用新的状态数据值重新计算表达式。在清单 1-12 中,我已经更新了App组件,因此userName状态数据属性的值被更改。
import React, { Component } from 'react';
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
userName: "Adam"
}
}
changeStateData = () => {
this.setState({
userName: this.state.userName === "Adam" ? "Bob" : "Adam"
})
}
render() {
return (
<div>
<h4 className="bg-primary text-white text-center p-2">
{ this.state.userName }'s To Do List
</h4>
<button className="btn btn-primary m-2"
onClick={ this.changeStateData }>
Change
</button>
</div>
)
};
}
Listing 1-12Changing State Data in the App.js File in the src Folder
将更改保存到App.js文件,您将在浏览器窗口中看到一个按钮。点击按钮改变用户名,如图 1-5 所示。
图 1-5
更改用户名
这个例子包含了几个协同工作的重要 React 特性。第一个是button元素上的onClick属性。
...
<button className="btn btn-primary m-2" onClick={ this.changeStateData }>
Change
</button>
...
onClick属性被赋予一个表达式,当按钮被单击时,React 对该表达式求值。点击一个按钮会触发一个事件,而onClick就是一个事件处理程序 prop 的例子。每次点击按钮时,都会调用由onClick指定的函数或方法。清单 1-12 中的表达式指定了changeStateData方法,该方法是使用粗箭头语法定义的,这允许简洁地表达函数,如下所示:
...
changeStateData = () => {
this.setState({ userName: this.state.userName === "Adam" ? "Bob" : "Adam" })
}
...
正如我在第四章中解释的那样,粗箭头函数用于简化对事件的响应,但是它们的使用范围更广,有助于在 React 应用中保持 HTML 和 JavaScript 的混合可读性。changeStateData方法使用setState方法为userName属性设置一个新值。当调用setState方法时,React 用新值更新组件的状态数据,然后调用render方法,这样表达式将生成更新的内容。这就是为什么点击按钮会将浏览器窗口中显示的名称从Adam更改为Bob。我不必显式地告诉 React 表达式使用的值发生了变化——我只是调用了setState方法来设置新值,并让 React 更新浏览器中的内容。
小费
无论何时使用组件定义的属性和方法,包括setState方法,都需要this关键字。忘记使用this是 React 开发中常见的错误,如果没有得到预期的行为,这是首先要检查的。
使用粗箭头语法定义的函数不使用return关键字,也不需要用花括号括住函数体,这可以产生更简单、更清晰的render方法,例如,如清单 1-13 所示。
import React, { Component } from 'react';
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
userName: "Adam"
}
}
changeStateData = () => {
this.setState({
userName: this.state.userName === "Adam" ? "Bob" : "Adam"
})
}
render = () =>
<div>
<h4 className="bg-primary text-white text-center p-2">
{ this.state.userName }'s To Do List
</h4>
<button className="btn btn-primary m-2"
onClick={ this.changeStateData }>
Change
</button>
</div>
}
Listing 1-13Redefining a Method Using a Fat Arrow Function in the App.js File in the src Folder
在本书中,我使用这两种风格来定义函数和方法。大多数情况下,您可以在传统的 JavaScript 函数和粗箭头函数之间进行选择,尽管在第十二章中解释了一些重要的注意事项。
添加待办事项应用功能
现在您已经看到了 React 如何显示动态内容,是时候开始添加应用所需的特性了,从附加的状态数据和表达式开始,如清单 1-14 所示。
import React, { Component } from 'react';
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
userName: "Adam",
todoItems: [{ action: "Buy Flowers", done: false },
{ action: "Get Shoes", done: false },
{ action: "Collect Tickets", done: true },
{ action: "Call Joe", done: false }],
newItemText: ""
}
}
updateNewTextValue = (event) => {
this.setState({ newItemText: event.target.value });
}
createNewTodo = () => {
if (!this.state.todoItems
.find(item => item.action === this.state.newItemText)) {
this.setState({
todoItems: [...this.state.todoItems,
{ action: this.state.newItemText, done: false }],
newItemText: ""
});
}
}
render = () =>
<div>
<h4 className="bg-primary text-white text-center p-2">
{this.state.userName}'s To Do List
({ this.state.todoItems.filter(t => !t.done).length} items to do)
</h4>
<div className="container-fluid">
<div className="my-1">
<input className="form-control"
value={ this.state.newItemText }
onChange={ this.updateNewTextValue } />
<button className="btn btn-primary mt-1"
onClick={ this.createNewTodo }>Add</button>
</div>
</div>
</div>
}
Listing 1-14Adding Application Features in the App.js File in the src Folder
因为 React 表达式是 JavaScript,所以它们可用于检查数据值并动态生成结果,就像下面的表达式:
...
<h4 className="bg-primary text-white text-center p-2">
{this.state.userName}'s To Do List
({ this.state.todoItems.filter(t => !t.done).length} items to do)
</h4>
...
该表达式过滤todoItems状态数据数组中的对象,以便只选择不完整的项目,然后读取length属性的值,这是绑定将向用户显示的值。JSX 格式使得像这样混合 HTML 元素和代码变得容易,尽管复杂的表达式可能难以阅读,并且通常在属性或方法中定义,以使 HTML 尽可能简单。
清单 1-14 中的变化引入了一个input元素,允许用户输入新待办事项的文本。input 元素有两个属性,用于管理元素的内容和响应更改,如下所示:
...
<input className="form-control"
value={ this.state.newItemText } onChange={ this.updateNewTextValue } />
...
属性用于设置元素的内容。在这种情况下,value属性包含的表达式将返回newItemText状态数据属性的值,这意味着对状态数据属性的任何更改都将更新input元素的内容。onChange prop 告诉 React 当change事件被触发时做什么,这将在用户输入input元素时发生。这个表达式告诉 React 调用组件的updateNewTextValue方法,它使用setState方法更新newItemText状态数据属性。这似乎是一种循环的方法,但是它确保了 React 知道如何处理由代码和用户执行的更改。
button元素使用onClick属性告诉 React 调用createNewTodo方法来响应click事件。createNewTodo方法检查是否存在具有相同文本的现有项目,如果没有,则使用setState方法向todoItems数组添加一个新项目,并重置newItemText属性,这具有清除input元素的效果。向数组中添加新项的语句是通过 JavaScript 的 spread 操作符来完成的,spread 操作符是 JavaScript 语言的一个新特性。
...
todoItems: [...this.state.todoItems,
{ action: this.state.newItemText, done: false }],
...
spread 运算符是三个句点,它扩展了一个数组。用于 React 开发的工具允许使用最新的 JavaScript 特性,并将它们转换成旧的 web 浏览器可以理解的兼容代码。我在第四章中描述了 spread 操作符和其他有用的 JavaScript 特性。
要查看清单 1-14 中更改的效果,请在文本字段中输入任务的描述,然后单击 Add 按钮。React 通过调用按钮的onClick属性指定的方法来响应事件,该方法使用input元素的值来创建一个新的待办事项。现在还看不到任务的描述,但是会看到未完成任务的数量增加,如图 1-6 。
图 1-6
添加新任务
显示待办事项
下一步是向用户显示每个待办事项,以便他们可以看到任务的细节,并在完成后将它们标记为完成,如清单 1-15 所示。
import React, { Component } from 'react';
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
userName: "Adam",
todoItems: [{ action: "Buy Flowers", done: false },
{ action: "Get Shoes", done: false },
{ action: "Collect Tickets", done: true },
{ action: "Call Joe", done: false }],
newItemText: ""
}
}
updateNewTextValue = (event) => {
this.setState({ newItemText: event.target.value });
}
createNewTodo = () => {
if (!this.state.todoItems
.find(item => item.action === this.state.newItemText)) {
this.setState({
todoItems: [...this.state.todoItems,
{ action: this.state.newItemText, done: false }],
newItemText: ""
});
}
}
toggleTodo = (todo) => this.setState({ todoItems:
this.state.todoItems.map(item => item.action === todo.action
? { ...item, done: !item.done } : item) });
todoTableRows = () => this.state.todoItems.map(item =>
<tr key={ item.action }>
<td>{ item.action}</td>
<td>
<input type="checkbox" checked={ item.done }
onChange={ () => this.toggleTodo(item) } />
</td>
</tr> );
render = () =>
<div>
<h4 className="bg-primary text-white text-center p-2">
{this.state.userName}'s To Do List
({ this.state.todoItems.filter(t => !t.done).length} items to do)
</h4>
<div className="container-fluid">
<div className="my-1">
<input className="form-control"
value={ this.state.newItemText }
onChange={ this.updateNewTextValue } />
<button className="btn btn-primary mt-1"
onClick={ this.createNewTodo }>Add</button>
</div>
<table className="table table-striped table-bordered">
<thead>
<tr><th>Description</th><th>Done</th></tr>
</thead>
<tbody>{ this.todoTableRows() }</tbody>
</table>
</div>
</div>
}
Listing 1-15Displaying To-Do Items in the App.js File in the src Folder
到目前为止,App.js文件的重点是在 HTML 片段中嵌入 JavaScript 表达式。但是 JSX 格式允许 HTML 和 JavaScript 自由混合,这意味着 JavaScript 方法可以返回 HTML 内容。您可以在清单 1-15 中看到一个例子,其中todoTableRows方法使用 JavaScript map方法为todoItems数组中的每个对象生成一系列 HTML 元素,如下所示:
...
todoTableRows = () => this.state.todoItems.map(item =>
<tr key={ item.action }>
<td>{ item.action}</td>
<td>
<input type="checkbox" checked={ item.done }
onChange={ () => this.toggleTodo(item) } />
</td>
</tr> );
...
数组中的每一项都映射到一个tr元素,这是一个表格行的 HTML 元素。在tr元素中有一组定义 HTML 表格单元格的td元素。由map方法生成的 HTML 内容包含更多的 JavaScript 表达式,这些表达式用状态数据值或函数填充td元素,这些值或函数将被调用来处理事件。
React 确实对它处理的内容施加了一些限制,比如通过todoTableRows方法添加到每个tr元素的key属性,如下所示:
...
<tr key={ item.action }>
...
正如你将在第十三章中详细了解到的,当有变化时,React 调用组件的render方法,并将结果与浏览器中显示的 HTML 进行比较,以便只应用差异。React 需要key prop,以便它能够将显示的内容与产生它的数据相关联,并有效地管理更改。
清单 1-15 中变化的结果是,每个待办事项都显示有一个复选框,用户可以切换该复选框来指示任务已经完成。由todoTableRows方法生成的每个表格行包含一个配置为复选框的input元素。
清单 1-15 中变化的结果是待办事项列表显示在一个表格中,勾选一项为完成会减少标题中显示的数字,如图 1-7 所示。
图 1-7
显示待办事项
引入附加组件
目前,示例应用的所有功能都包含在一个组件中,随着新功能的增加,这个组件会变得难以管理。为了帮助保持组件的可管理性,功能被委托给负责特定功能的独立组件。这些被称为子组件,而委托功能的组件被称为父组件。
在本节中,我将介绍几个子组件,每个子组件负责一个特性。我首先将一个名为TodoBanner.js的文件添加到src文件夹中,并使用它来定义清单 1-16 中所示的组件。
import React, { Component } from 'react';
export class TodoBanner extends Component {
render = () =>
<h4 className="bg-primary text-white text-center p-2">
{ this.props.name }'s To Do List
({ this.props.tasks.filter(t => !t.done).length } items to do)
</h4>
}
Listing 1-16The Contents of the TodoBanner.js File in the src Folder
该组件负责显示横幅。父组件使用 props 为其子组件提供数据,数据值通过props属性访问,通过this关键字访问。这个名为TodoBanner的组件期望接收两个属性:一个name属性,包含用户的名字,另一个tasks属性,包含一组任务,过滤后显示未完成的任务数。例如,为了显示name属性的值,组件使用包含this.props.name的表达式,如下所示:
...
{ this.props.name }'s To Do List
...
当 React 调用TodoBanner组件的render方法时,父组件提供的name属性的值将包含在结果中。TodoBanner组件的render方法中的另一个表达式使用 JavaScript filter方法来选择不完整的项目并确定有多少,这表明 props 可以用在表达式中,而不仅仅是显示它们的值。
接下来,我在src文件夹中创建了一个名为TodoRow.js的文件,并用它来定义清单 1-17 中所示的组件。
import React, { Component } from 'react';
export class TodoRow extends Component {
render = () =>
<tr>
<td>{ this.props.item.action}</td>
<td>
<input type="checkbox" checked={ this.props.item.done }
onChange={ () => this.props.callback(this.props.item) }
/>
</td>
</tr>
}
Listing 1-17The Contents of the TodoRow.js File in the src Folder
该组件将负责在表中显示一行,显示待办事项的详细信息。子组件通过其 props 接收的数据是只读的,不得更改。为了进行更改,父组件可以使用函数 props 为子组件提供回调函数,当重要事件发生时调用这些函数。这种组合允许组件之间的协作:数据属性允许父母向孩子提供数据,而功能属性允许孩子与父母通信。
清单 1-17 中的组件定义了一个名为item的数据属性,用于接收要显示的待办事项,它还定义了一个名为callback的函数属性,提供了一个当用户切换复选框时调用的函数。对于最后一个子组件,我在src文件夹中添加了一个名为TodoCreator.js的文件,并添加了清单 1-18 中所示的代码。
import React, { Component } from 'react';
export class TodoCreator extends Component {
constructor(props) {
super(props);
this.state = { newItemText: "" }
}
updateNewTextValue = (event) => {
this.setState({ newItemText: event.target.value});
}
createNewTodo = () => {
this.props.callback(this.state.newItemText);
this.setState({ newItemText: ""});
}
render = () =>
<div className="my-1">
<input className="form-control" value={ this.state.newItemText }
onChange={ this.updateNewTextValue } />
<button className="btn btn-primary mt-1"
onClick={ this.createNewTodo }>Add</button>
</div>
}
Listing 1-18The Contents of the TodoCreator.js File in the src Folder
子组件可以有自己的状态数据,这是该组件用来处理其input元素内容的数据。当用户单击 Add 按钮时,组件调用 function prop 来通知其父组件。
使用子组件
我在上一节中定义的组件负责待办事项应用的特定功能。在清单 1-19 中,我更新了App组件以使用三个新组件,每个组件都是使用 props 配置的,为它们提供所需的数据和回调函数。
import React, { Component } from 'react';
import { TodoBanner } from "./TodoBanner";
import { TodoCreator } from "./TodoCreator";
import { TodoRow } from "./TodoRow";
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
userName: "Adam",
todoItems: [{ action: "Buy Flowers", done: false },
{ action: "Get Shoes", done: false },
{ action: "Collect Tickets", done: true },
{ action: "Call Joe", done: false }],
//newItemText: ""
}
}
updateNewTextValue = (event) => {
this.setState({ newItemText: event.target.value });
}
createNewTodo = (task) => {
if (!this.state.todoItems.find(item => item.action === task)) {
this.setState({
todoItems: [...this.state.todoItems, { action: task, done: false }]
});
}
}
toggleTodo = (todo) => this.setState({ todoItems:
this.state.todoItems.map(item => item.action === todo.action
? { ...item, done: !item.done } : item) });
todoTableRows = () => this.state.todoItems.map(item =>
<TodoRow key={ item.action } item={ item } callback={ this.toggleTodo } />)
render = () =>
<div>
<TodoBanner name={ this.state.userName } tasks={this.state.todoItems } />
<div className="container-fluid">
<TodoCreator callback={ this.createNewTodo } />
<table className="table table-striped table-bordered">
<thead>
<tr><th>Description</th><th>Done</th></tr>
</thead>
<tbody>{ this.todoTableRows() }</tbody>
</table>
</div>
</div>
}
Listing 1-19Applying Child Components in the App.js File in the src Folder
新的import语句声明了对子组件的依赖,这确保了它们在构建过程中包含在应用中。子组件用作自定义 HTML 元素,其属性和表达式定义了组件将接收的属性,如下所示:
...
<TodoBanner name={ this.state.userName } tasks={this.state.todoItems } />
...
用于设置属性值的表达式为子组件提供了对由其父组件定义的特定数据和方法的访问。在这种情况下,name和tasks属性用于向TodoBanner组件提供userName和todoItems状态数据属性的值。
添加最后的润色
应用的基本功能已经就绪,提供这些功能的一组组件正在协同工作。在这一部分,我添加了一些收尾工作来完成待办事项应用。
管理已完成任务的可见性
目前,即使任务已经完成,它们仍然对用户可见。为了解决这个问题,我将为用户提供已完成和未完成任务的单独列表,并允许隐藏未完成的任务。我在src文件夹中添加了一个名为VisibilityControl.js的文件,并用它来定义清单 1-20 中所示的组件。
import React, { Component } from 'react';
export class VisibilityControl extends Component {
render = () =>
<div className="form-check">
<input className="form-check-input" type="checkbox"
checked={ this.props.isChecked }
onChange={ (e) => this.props.callback(e.target.checked) } />
<label className="form-check-label">
Show { this.props.description }
</label>
</div>
}
Listing 1-20The Contents of the VisibilityControl.js File in the src Folder
使用 props 从父节点接收数据和回调函数使得向应用添加新特性变得容易。清单 1-20 中定义的组件是一个通用的特性,它不知道被用来管理的内容,它完全通过它的属性工作:description属性提供它显示的标签文本,isChecked属性提供复选框的初始状态,callback属性提供当用户切换复选框并触发change事件时调用的函数。
在清单 1-21 中,我已经更新了App组件,将VisibilityControl组件作为子组件应用,同时还做了必要的修改,以分别显示已完成和未完成的任务。
import React, { Component } from 'react';
import { TodoBanner } from "./TodoBanner";
import { TodoCreator } from "./TodoCreator";
import { TodoRow } from "./TodoRow";
import { VisibilityControl } from "./VisibilityControl";
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
userName: "Adam",
todoItems: [{ action: "Buy Flowers", done: false },
{ action: "Get Shoes", done: false },
{ action: "Collect Tickets", done: true },
{ action: "Call Joe", done: false }],
showCompleted: true
}
}
updateNewTextValue = (event) => {
this.setState({ newItemText: event.target.value });
}
createNewTodo = (task) => {
if (!this.state.todoItems.find(item => item.action === task)) {
this.setState({
todoItems: [...this.state.todoItems, { action: task, done: false }]
});
}
}
toggleTodo = (todo) => this.setState({ todoItems:
this.state.todoItems.map(item => item.action === todo.action
? { ...item, done: !item.done } : item) });
todoTableRows = (doneValue) => this.state.todoItems
.filter(item => item.done === doneValue).map(item =>
<TodoRow key={ item.action } item={ item }
callback={ this.toggleTodo } />)
render = () =>
<div>
<TodoBanner name={ this.state.userName }
tasks={this.state.todoItems } />
<div className="container-fluid">
<TodoCreator callback={ this.createNewTodo } />
<table className="table table-striped table-bordered">
<thead>
<tr><th>Description</th><th>Done</th></tr>
</thead>
<tbody>{ this.todoTableRows(false) }</tbody>
</table>
<div className="bg-secondary text-white text-center p-2">
<VisibilityControl description="Completed Tasks"
isChecked={this.state.showCompleted}
callback={ (checked) =>
this.setState({ showCompleted: checked })} />
</div>
{ this.state.showCompleted &&
<table className="table table-striped table-bordered">
<thead>
<tr><th>Description</th><th>Done</th></tr>
</thead>
<tbody>{ this.todoTableRows(true) }</tbody>
</table>
}
</div>
</div>
}
Listing 1-21Managing Completed Tasks in the App.js File in the src Folder
对VisibilityControl组件进行了配置,因此当用户切换复选框时,它会更改App组件的名为showCompleted的状态数据属性的值。为了区分完成的和未完成的任务,我向todoTableRows方法添加了一个参数,并使用filter方法根据done属性的值从状态数据数组中选择对象。
为了显示已完成的任务,我添加了第二个table元素。只有当showCompleted属性为true时,该表才会显示,所以我将table及其内容放在一个数据绑定表达式中,并使用了&&运算符,如下所示:
...
{ this.state.showCompleted && <table className="table table-striped table-bordered">
...
当表达式求值时,只有当showCompleted属性为true时,table元素才会包含在组件的内容中。这是 JSX 如何混合内容和代码的另一个例子。在很大程度上,JSX 在混合元素和代码语句方面做得很好,但它并不擅长所有事情,条件语句所需的语法也很笨拙,正如这个例子所示。
当您保存对App.js文件的更改时,您将看到不同的任务集。当您切换任务的复选框时,它将被移动到另一个表格,如图 1-8 所示。当您切换“显示已完成的任务”复选框时,第二个表将被隐藏。
图 1-8
更改任务显示
持久存储数据
最后一个变化是存储数据,以便在离开应用时保留用户列表。在本书的后面,我将演示处理存储在服务器上的数据的不同方法,但是在本章中,我将保持应用的简单性,并要求浏览器使用本地存储 API 来存储数据,如清单 1-22 所示。
小费
本地存储 API 是一个标准的浏览器特性,并不特定于 React 开发。参见 https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage 了解本地存储如何工作的详细描述。
import React, { Component } from 'react';
import { TodoBanner } from "./TodoBanner";
import { TodoCreator } from "./TodoCreator";
import { TodoRow } from "./TodoRow";
import { VisibilityControl } from "./VisibilityControl";
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
userName: "Adam",
todoItems: [{ action: "Buy Flowers", done: false },
{ action: "Get Shoes", done: false },
{ action: "Collect Tickets", done: true },
{ action: "Call Joe", done: false }],
showCompleted: true
}
}
updateNewTextValue = (event) => {
this.setState({ newItemText: event.target.value });
}
createNewTodo = (task) => {
if (!this.state.todoItems.find(item => item.action === task)) {
this.setState({
todoItems: [...this.state.todoItems, { action: task, done: false }]
}, () => localStorage.setItem("todos", JSON.stringify(this.state)));
}
}
toggleTodo = (todo) => this.setState({ todoItems:
this.state.todoItems.map(item => item.action === todo.action
? { ...item, done: !item.done } : item) });
todoTableRows = (doneValue) => this.state.todoItems
.filter(item => item.done === doneValue).map(item =>
<TodoRow key={ item.action } item={ item }
callback={ this.toggleTodo } />)
componentDidMount = () => {
let data = localStorage.getItem("todos");
this.setState(data != null
? JSON.parse(data)
: {
userName: "Adam",
todoItems: [{ action: "Buy Flowers", done: false },
{ action: "Get Shoes", done: false },
{ action: "Collect Tickets", done: true },
{ action: "Call Joe", done: false }],
showCompleted: true
});
}
render = () =>
<div>
<TodoBanner name={ this.state.userName }
tasks={this.state.todoItems } />
<div className="container-fluid">
<TodoCreator callback={ this.createNewTodo } />
<table className="table table-striped table-bordered">
<thead>
<tr><th>Description</th><th>Done</th></tr>
</thead>
<tbody>{ this.todoTableRows(false) }</tbody>
</table>
<div className="bg-secondary text-white text-center p-2">
<VisibilityControl description="Completed Tasks"
isChecked={this.state.showCompleted}
callback={ (checked) =>
this.setState({ showCompleted: checked })} />
</div>
{ this.state.showCompleted &&
<table className="table table-striped table-bordered">
<thead>
<tr><th>Description</th><th>Done</th></tr>
</thead>
<tbody>{ this.todoTableRows(true) }</tbody>
</table>
}
</div>
</div>
}
Listing 1-22Persistently Storing Data in the App.js File in the src Folder
通过localStorage对象访问本地存储 API,当创建新的待办事项时,组件使用setItem方法存储待办事项。本地存储特性只能存储字符串值,所以在存储之前,我将数据对象序列化为 JSON。如第十一章所述,setState方法可以接受一个函数,一旦状态数据被更新,该函数将被更新,这确保了最新的数据被存储。
组件有一个明确定义的生命周期,这在第十三章中有描述,并且可以实现接收重要事件通知的方法。清单中的组件实现了componentDidMount方法,该方法在组件生命周期的早期被调用,并提供了执行诸如加载数据等任务的好机会。
为了检索存储的数据,我使用了本地存储 API 的getItem方法。我使用setState方法用存储的数据更新组件,或者如果没有可用的存储数据,用一些默认数据更新组件。
视觉上没有变化,但应用将持久存储您创建的任何待办事项,这意味着当您重新加载浏览器窗口或导航到不同的 URL(如 Apress 主页),然后返回到http://localhost:3000,它们仍然可用,如图 1-9 所示。
图 1-9
存储数据
摘要
在本章中,我创建了一个简单的示例应用,向您介绍 React 开发过程,并演示一些重要的 React 概念。您看到了 React 开发关注于组件,这些组件是在结合了 JavaScript 代码和 HTML 内容的 JSX 文件中定义的。创建项目时,包括了处理 JSX 文件、构建应用以及将其交付给浏览器进行测试所需的一切,以便您可以快速轻松地开始。
您还了解了 React 应用可以包含多个组件,每个组件负责一个特定的特性,并使用 props 接收它们所需的数据和回调函数。
从这本书的篇幅中可以看出,React 还有更多的特性可用,但是我在本章中创建的基本应用已经向您展示了 React 开发的最基本特征,并将为后面的章节提供基础。在下一章,我将 React 放在上下文中,描述这本书的结构和内容。
二、理解 React
React 是一个灵活而强大的开源框架,用于开发客户端应用;它从服务器端开发的世界中获得线索,并将它们应用于 HTML 元素,并且它创建了一个基础,使得构建富 web 应用变得更加容易。在本书中,我解释了 React 是如何工作的,并演示了它提供的不同特性。
这本书和 React 发布时间表
React 团队频繁发布,这意味着会有持续的补丁和特性流。次要版本倾向于不破坏现有的特性,并且主要包含错误修复。主要版本可能包含重大更改,并且可能不提供向后兼容性。
要求读者每隔几个月就购买这本书的新版本似乎不公平也不合理,尤其是因为即使在一个主要版本中,React 的大多数特性也不太可能改变。取而代之的是,我将在本书 https://github.com/Apress/pro-react-16 的 GitHub 资源库中发布主要版本的更新。
这是我(和 press)正在进行的实验,我还不知道这些更新会采取什么形式——尤其是因为我不知道 React 的主要版本会包含什么——但目标是通过补充书中包含的示例来延长这本书的寿命。
我不承诺更新会是什么样的,它们会采取什么形式,或者在我把它们折叠成这本书的新版本之前,我会花多长时间来制作它们。当新的 React 版本发布时,请保持开放的心态并检查这本书的资源库。如果你有关于如何改进更新的想法,请发电子邮件到 adam@adam-freeman.com 告诉我。
我应该使用 React 吗?
React 不是所有问题的解决方案,知道何时应该使用 React 以及何时应该寻求替代方案是很重要的。React 提供了以前只有服务器端开发人员才能使用的功能,但现在完全在浏览器中提供。每次加载应用了 React 的 HTML 文档时,浏览器都必须做大量的工作:必须加载数据,必须创建和编写组件,必须计算表达式,等等,为我在第一章中描述的功能以及我在本书其余部分解释的功能创建基础。
这种工作需要时间来完成,时间的长短取决于 React 应用的复杂性,关键取决于浏览器的质量和设备的处理能力。在功能强大的台式机上使用最新的浏览器时,您不会注意到任何性能问题,但功能不足的智能手机上的旧浏览器确实会降低 React 应用的初始设置速度。
因此,目标是尽可能少地执行这种设置,并在执行时向用户交付尽可能多的应用。这意味着仔细考虑您构建的 web 应用的类型。从广义上讲,web 应用有两种基本类型:往返和单页。
了解往返应用
很长一段时间以来,web 应用的开发都遵循一个往返模型。浏览器向服务器请求一个初始的 HTML 文档。用户交互(如单击链接或提交表单)会导致浏览器请求并接收一个全新的 HTML 文档。在这种应用中,浏览器本质上是 HTML 内容的呈现引擎,所有的应用逻辑和数据都驻留在服务器上。浏览器发出一系列无状态的 HTTP 请求,服务器通过动态生成 HTML 文档来处理这些请求。
许多当前的 web 开发仍然是针对往返应用的,尤其是业务线项目,尤其是因为它们对浏览器的要求很少,并且拥有尽可能广泛的客户端支持。但是往返应用也有一些严重的缺点:它们让用户在请求和加载下一个 HTML 文档时等待,它们需要一个大型的服务器端基础设施来处理所有请求和管理所有应用状态,并且它们需要更多的带宽,因为每个 HTML 文档都必须是自包含的,这可能导致服务器的每个响应中都包含相同的内容。React 不太适合往返应用,因为浏览器必须为从服务器收到的每个新 HTML 文档执行初始设置过程。
理解单页应用
单页应用采取了不同的方法。一个初始的 HTML 文档被发送到浏览器,但是用户交互会导致对插入到向用户显示的现有元素集中的 HTML 或数据的小片段的 HTTP 请求。初始的 HTML 文档永远不会被重新加载或替换,当 HTTP 请求被异步执行时,用户可以继续与现有的 HTML 交互,即使这只是意味着看到一个“数据加载”消息。
React 非常适合单页面应用,因为浏览器初始化应用的工作只需执行一次,之后应用在浏览器中运行,响应用户交互并请求后台所需的数据或内容。
比较 React to Vue.js 和 Angular
有两个主要的竞争对手需要应对:Angular 和 Vue.js。它们之间存在差异,但是,在大多数情况下,所有这些框架都非常优秀,它们都以相似的方式工作,并且它们都可以用于创建丰富而流畅的客户端应用。
这些框架之间的真正区别在于开发人员的体验。例如,Angular 需要使用 TypeScript 才能有效,而它只是 React 和 Vue.js 项目的一个选项。React 和 Vue.js 将 HTML 和 JavaScript 混合在一个文件中,这不是每个人都喜欢的,尽管每个框架的实现方式不同。
我的建议很简单:选择你最喜欢的框架,如果你不喜欢,就换一个。这可能看起来是一种不科学的方法,但是这是一个不错的选择,并且您会发现许多核心概念会在框架之间延续,即使您改变了您使用的框架。
了解应用的复杂性
在决定 React 是否适合某个项目时,应用的类型并不是唯一的考虑因素。项目的复杂性也很重要,我经常从那些使用客户端框架(如 React、Angular 或 Vue.js)开始项目的读者那里了解到,简单得多的框架就足够了。像 React 这样的框架需要投入大量的时间来掌握(正如本书的篇幅所展示的),如果你只是需要验证一个表单或者编程填充一个select元素,这种努力是不值得的。
在围绕客户端框架的兴奋中,很容易忘记浏览器提供了一组丰富的可以直接使用的 API,并且 React 的所有功能都依赖这些 API。如果您有一个简单且独立的问题,那么您应该考虑直接使用浏览器 API,从文档对象模型(DOM) API 开始。你会看到本书中的一些例子直接使用了浏览器 API,但是如果你是浏览器开发新手,那么 https://developer.mozilla.org 是一个很好的起点,它包含了浏览器支持的所有 API 的良好文档。
浏览器 API 的缺点,尤其是 DOM API,是它们可能很难使用,并且旧的浏览器倾向于以不同的方式实现特性。jQuery ( https://jquery.org )是直接使用浏览器 API 的一个很好的替代方法,尤其是如果您必须支持旧的浏览器。jQuery 简化了 HTML 元素的使用,并为处理事件、动画和异步 HTTP 请求提供了出色的支持。
React 在大型应用中发挥了自己的作用,在这些应用中,要实现复杂的工作流,要处理不同类型的用户,还要处理大量的数据。在这些情况下,您可以直接使用浏览器 API,但是管理代码和扩展应用会变得很困难。React 提供的特性使得构建大型复杂的应用变得更加容易,并且不会陷入大量不可读的代码中,而不采用框架的复杂项目往往会陷入这种困境。
我需要知道什么?
如果您认为 React 是您项目的正确选择,那么您应该熟悉 web 开发的基础知识,了解 HTML 和 CSS 的工作原理,并掌握 JavaScript 的工作知识。如果你对这些细节有一点模糊,我在第三章 3 和第四章 4 中提供了我在本书中使用的特性的初级读本。 https://developer.mozilla.org 是温习 HTML、CSS 和 JavaScript 基础知识的好地方。
如何设置我的开发环境?
React 开发所需的唯一开发工具是您在第一章创建第一个应用时安装的工具。后面的一些章节需要额外的软件包,但是提供了完整的说明。如果你在第一章成功地构建了应用,那么你就为 React 开发和本书的其余章节做好了准备。
这本书的结构是什么?
这本书分为三部分,每一部分都涵盖了一系列相关的主题。
第一部分:React 入门
本书的第一部分提供了开始 React 开发所需的信息。它包括本章和 React 开发中使用的关键技术的入门/复习章节,包括 HTML、CSS 和 JavaScript。第一章向您展示了如何创建一个简单的 React 应用,第 5–8 章带您了解构建一个更真实的应用的过程,这个应用叫做 SportsStore。
第二部分:使用 React
本书的第二部分涵盖了大多数项目中需要的核心 React 特性。React 提供了许多内置功能,我将深入描述这些功能,以及将自定义代码和内容添加到项目中以创建定制功能的方式。
第三部分:创建完整的 React 应用
React 依靠额外的包来提供大多数复杂应用所需的高级功能。在本书的第三部分,我介绍了其中最重要的包,向您展示了它们是如何工作的,并解释了它们是如何添加到 React 核心特性中的。
有很多例子吗?
有个载荷的例子。学习 React 的最好方法是通过例子,我已经尽可能多地将它们打包到本书中,并附有截图,以便您可以看到每个特性的效果。为了最大限度地增加本书中的示例数量,我采用了一个简单的约定来避免重复列出相同的代码或内容。当我创建一个文件时,我会显示它的全部内容,就像我在清单 2-1 中所做的那样。我在清单的标题中包含了文件及其文件夹的名称,并且用粗体显示了我所做的更改。
import React, { Component } from "react";
export class SimpleButton extends Component {
constructor(props) {
super(props);
this.state = {
counter: 0,
hasButtonBeenClicked: false
}
}
render = () =>
<button onClick={ this.handleClick }
className={ this.props.className }
disabled={ this.props.disabled === "true"
|| this.props.disabled === true }>
{ this.props.text} { this.state.counter }
{ this.state.hasButtonBeenClicked &&
<div>Button Clicked!</div>
}
</button>
}
handleClick = () => {
this.setState({ counter: this.state.counter + 1 },
() => this.setState({ hasButtonBeenClicked: this.state.counter > 0 }));
this.props.callback();
}
}
Listing 2-1Using a Callback in the SimpleButton.js File in the src Folder
这是第十一章的清单,显示了在src文件夹中可以找到的一个名为SimpleButton.js的文件的内容。不要担心清单的内容或文件的目的;请注意,这种类型的清单包含文件的完整内容,您需要按照示例进行的更改以粗体显示。
React 应用中的一些文件可能很长,但是我描述的特性只需要很小的改变。我没有列出完整的文件,而是使用省略号(三个句点串联)来表示部分列表,它只显示了文件的一部分,如清单 2-2 所示。
...
handleClick = () => {
for (let i = 0; i < 5; i++) {
this.setState({ counter: this.state.counter + 1});
}
this.setState({ hasButtonBeenClicked: true });
this.props.callback();
}
...
Listing 2-2Making Multiple Updates in the SimpleButton.js File in the src Folder
这是第十一章的后续清单,它显示了一组只应用于一个大得多的文件的一部分的更改。当您看到部分清单时,您会知道文件的其余部分不必更改,只有粗体部分不同。
在某些情况下,需要在文件的不同部分进行更改,这使得很难显示为部分列表。在这种情况下,我省略了文件的部分内容,如清单 2-3 所示。
import React, { Component } from "react";
import { ActionButton } from "./ActionButton";
export class Message extends Component {
// ...other methods omitted for brevity...
componentDidMount() {
console.log("componentDidMount Message Component");
}
componentDidUpdate() {
console.log("componentDidUpdate Message Component");
}
}
Listing 2-3Implementing a Lifecycle Method in the Message.js File in the src Folder
更改仍然用粗体标记,清单中省略的文件部分不受此示例的影响。
从哪里可以获得示例代码?
你可以从 https://github.com/Apress/pro-react-16 下载本书所有章节的范例项目。该下载是免费的,它包含了您学习示例所需的一切,而不必键入所有的代码。
你在哪里可以得到这本书的修改?
你可以在 https://github.com/Apress/pro-react-16 找到这本书的勘误表。
你怎么联系我?
如果你在使用本章中的例子时有问题,或者你在书中发现了问题,那么你可以给我发电子邮件到 adam@adam-freeman.com,我会尽力帮助你。在联系我之前,请检查这本书的勘误表,看看它是否包含您的问题的解决方案。
摘要
在这一章中,我解释了 React 何时是项目的好选择,并概述了备选方案和竞争对手。我还概述了这本书的内容和结构,解释了从哪里获得更新,并解释了如果您对本书中的示例有问题,如何与我联系。在下一章中,我将介绍本书中用来解释 React 开发的 HTML 和 CSS 特性。
三、HTML、JSX 和 CSS 入门
在这一章中,我提供了 HTML 的简要概述,并解释了如何在使用 JSX 时将 HTML 内容与 JavaScript 代码混合,这是 React 开发工具支持的 JavaScript 的超集,允许 HTML 与代码混合。我还介绍了 Bootstrap CSS 框架,我用它来设计本书示例中的内容。
注意
如果本章中描述的所有特性都没有直接意义,也不要担心。有些依赖于 JavaScript 语言中最近添加的你以前可能没有遇到过的东西,这些在第四章中有描述或者在其他章节中有详细解释。
为本章做准备
为了创建本章的项目,打开一个新的命令提示符,导航到一个方便的位置,并运行清单 3-1 中所示的命令。
小费
你可以从 https://github.com/Apress/pro-react-16 下载本章以及本书所有其他章节的示例项目。
npx create-react-app primer
Listing 3-1Creating the Example Project
一旦创建了项目,运行清单 3-2 中所示的命令,导航到项目文件夹并安装引导 CSS 框架。
注意
创建新项目时,您可能会看到有关安全漏洞的警告。React 开发依赖于大量的包,每个包都有自己的依赖关系,不可避免的会发现安全问题。对于本书中的示例,使用指定的包版本以确保获得预期的结果是很重要的。对于您自己的项目,您应该查看警告并更新到解决问题的版本。
cd primer
npm install bootstrap@4.1.2
Listing 3-2Adding the Bootstrap Package to the Project
要在应用中包含 Bootstrap,将清单 3-3 中所示的语句添加到index.js文件中。
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import 'bootstrap/dist/css/bootstrap.css';
ReactDOM.render(<App />, document.getElementById('root'));
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: http://bit.ly/CRA-PWA
serviceWorker.unregister();
Listing 3-3Including Bootstrap in the index.js File in the src Folder
准备 HTML 文件和组件
为了准备本章中的例子,用清单 3-4 中显示的内容替换public文件夹中index.html文件的内容。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Primer</title>
</head>
<body>
<h4 class="bg-primary text-white text-center p-2 m-1">
Static HTML Element
</h4>
<div id="domParent"></div>
<div id="root"></div>
</body>
</html>
Listing 3-4Replacing the Contents of the index.html File in the public Folder
用清单 3-5 中所示的代码替换src文件夹中App.js文件的内容。
import React, { Component } from "react";
export default class App extends Component {
render = () =>
<h4 className="bg-primary text-white text-center p-2 m-1">
Component Element
</h4>
}
Listing 3-5Replacing the Contents of the App.js File in the src folder
运行示例应用
确保所有的更改都已保存,并使用命令提示符运行primer文件夹中清单 3-6 所示的命令。
npm start
Listing 3-6Starting the Development Tools
React 开发工具将会启动,一旦初始准备工作完成,一个新的浏览器窗口将会打开并显示如图 3-1 所示的内容。
图 3-1
运行示例应用
理解 HTML 和 DOM 元素
所有 React web 应用的核心都是 HTML 元素,用于描述将呈现给用户的内容。在 React 应用中,public文件夹中静态index.html文件的内容与 React 动态创建的 HTML 元素相结合,生成一个浏览器显示给用户的 HTML 文档。
HTML 元素告诉浏览器 HTML 文档的每个部分代表什么样的内容。以下是来自public文件夹中的index.html文件的 HTML 元素:
...
<h4 class="bg-primary text-white text-center p-2 m-1">
Static HTML Element
</h4>
...
如图 3-2 所示,这个元素有几个部分:开始标签、结束标签、属性和内容。
图 3-2
HTML 元素的剖析
这个元素的名称(也称为标签名称或者仅仅是标签)是h4,它告诉浏览器标签之间的内容应该被当作一个头。有一系列的头元素,从h1到h6,其中h1通常用于最重要的内容,h2用于稍微不太重要的内容,等等。
定义 HTML 元素时,首先将标记名放在尖括号中(<和>字符),然后以类似的方式使用标记结束元素,除了在左尖括号(<)后添加一个/字符,以创建开始标记和结束标记。
标签表明元素的用途,HTML 规范定义了大量的元素类型。在表 3-1 中,我描述了我在本书中最常用的元素。要获得标签类型的完整列表,您应该查阅 HTML 规范。
表 3-1
示例中使用的常见 HTML 元素
|元素
|
描述
|
| --- | --- |
| a | 一个链接(更正式的说法是锚点),用户单击它可以导航到当前文档中的新 URL 或新位置 |
| button | 一个按钮,用户可以点击它来启动一个动作 |
| div | 通用元素;通常用于为文档添加结构,以用于演示目的 |
| h1 to h6 | 头球 |
| input | 用于从用户处收集单个数据项的字段 |
| table | 表格,用于将内容组织成行和列 |
| tbody | 表格的正文(与页眉或页脚相对) |
| td | 表格行中的内容单元格 |
| th | 表格行中的标题单元格 |
| thead | 表格的标题 |
| tr | 表格中的一行 |
了解元素内容
出现在开始和结束标签之间的就是元素的内容。一个元素可以包含文本(比如本例中的Static HTML Element)或其他 HTML 元素。在清单 3-7 中,我添加了一个包含另一个元素的新 HTML 元素。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Primer</title>
</head>
<body>
<h4 class="bg-primary text-white text-center p-2 m-1">
Static HTML Element
</h4>
<div class="text-center m-2">
<div>This is a span element</div>
<div>This is another span element</div>
</div>
<div id="domParent"></div>
<div id="root"></div>
</body>
</html>
Listing 3-7Adding a New Element in the index.html File in the public Folder
外部元素被称为父元素,而它包含的元素被称为子元素。清单 3-7 中的附加内容定义了一个父div元素,它有两个子元素,也是div元素。每个子div元素的内容是一条文本消息,产生如图 3-3 所示的结果。能够创建元素的层次结构是 HTML 的一个基本特性。它是 React 应用的关键构建块之一,允许创建复杂的内容。
图 3-3
添加父元素和子元素
了解元素内容限制
有些元素对可以成为其子元素的元素类型有限制。示例中的div元素可以包含任何其他元素,并用于向 HTML 文档添加结构,通常这样可以很容易地对内容进行样式化。其他元素具有更具体的角色,需要将特定类型的元素用作子元素。例如,一个tbody元素,你将在后面的章节中看到,它代表一个表格的主体,可以只包含一个或多个tr元素,每个元素代表一个表格行。
小费
不要担心学习所有的 HTML 元素和它们之间的关系。当你按照后面章节中的例子学习时,你会学到你需要知道的一切,如果你试图创建无效的 HTML,大多数代码编辑器会显示一个警告。
了解空元素
有些元素根本不允许包含任何内容。这些被称为 void 或自闭元素,它们没有单独的结束标记,就像这样:
...
<input />
...
在单个标记中定义了一个 void 元素,并在最后一个尖括号(>字符)前添加了一个/字符。这里显示的元素是 void 元素最常见的例子,它用于在 HTML 表单中收集来自用户的数据。在后面的章节中,你会看到很多关于 void 元素的例子。
了解属性
通过向元素添加属性,可以向浏览器提供额外的信息。下面是应用于图 3-2 中所示的h4元素的属性:
...
<h4 class="bg-primary text-white text-center p-2 m-1">
Static HTML Element
</h4>
...
属性总是被定义为开始标签的一部分,并且大多数属性都有一个名称和一个值,用等号分隔,如图 3-4 所示。
图 3-4
属性的名称和值
该属性的名称是class,用于对相关元素进行分组,这样它们的外观就可以得到一致的管理。这就是为什么在这个例子中使用了class属性,属性值将h4元素与许多类相关联,这些类与引导 CSS 包提供的样式相关,我将在本章的后面描述。
动态创建 HTML 元素
在index.html文件中定义的 HTML 元素是静态的。浏览器接收并显示这些元素,就像它们被定义一样,您可以通过在浏览器窗口中单击鼠标右键并从弹出菜单中选择“检查”或“检查元素”来查看它们。F12 开发人员工具将打开并显示 HTML 文档的内容,其中包括以下元素:
...
<h4 class="bg-primary text-white text-center p-2 m-1">
Static HTML Element
</h4>
...
HTML 元素也可以使用 JavaScript 和所有现代浏览器都支持的域对象模型(DOM) API 来动态创建。在清单 3-8 中,我向index.html文件添加了一些 JavaScript,该文件使用 DOM API 向 HTML 文档添加新元素。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Primer</title>
</head>
<body>
<h4 class="bg-primary text-white text-center p-2 m-1">
Static HTML Element
</h4>
<div class="text-center m-2">
<div>This is a span element</div>
<div>This is another span element</div>
</div>
<div id="domParent"></div>
<div id="root"></div>
<script>
let element = document.createElement("h4")
element.className = "bg-primary text-white text-center p-2 m-1";
element.textContent = "DOM API HTML Element";
document.getElementById("domParent").appendChild(element);
</script>
</body>
</html>
Listing 3-8Creating an Element Dynamically in the index.html File in the public Folder
script元素表示一段 JavaScript 代码,浏览器在处理index.html文件的内容时会执行这段代码,并创建一个新的 HTML 元素,如图 3-5 所示。
图 3-5
使用 DOM API 创建元素
清单 3-8 中的第一个 JavaScript 语句创建了一个新的h4元素。
...
let element = document.createElement("h4")
...
document对象代表浏览器正在显示的 HTML 文档,而createElement方法返回一个代表新 HTML 元素的对象。DOM API 提供的表示新 HTML 元素的对象具有与定义静态 HTML 时使用的属性相对应的属性。清单 3-8 中的第二个 JavaScript 语句使用了对应于class属性的属性。
...
element.className = "bg-primary text-white text-center p-2 m-1";
...
元素对象定义的大多数属性都与它们对应的属性同名。也有一些例外,包括className,使用它是因为class关键字在很多编程语言中都是保留的,包括 JavaScript。
其余的 JavaScript 语句设置 HTML 元素的文本内容,并将其添加到 HTML 文档中,以便由浏览器显示。如果您通过在浏览器窗口中右键单击并从弹出菜单中选择 Inspect 来检查新元素,您将会看到由清单 3-8 中的 JavaScript 语句创建的对象已经像来自index.html文件的静态元素一样被表示。
...
<h4 class="bg-primary text-white text-center p-2 m-1">DOM API HTML Element</h4>
...
值得强调的是,index.html文件不包含这个 HTML 元素。相反,它包含一系列 JavaScript 语句,指示浏览器创建元素并将其添加到呈现给用户的内容中。
使用 React 组件动态创建元素
如果您检查App.js文件的内容,您会看到App组件的render方法结合了前面章节中静态和动态 HTML 元素的一些方面:
...
import React, { Component } from "react";
export default class App extends Component {
render = () =>
<h4 className="bg-primary text-white text-center p-2 m-1">
Component Element
</h4>
}
...
React 使用 DOM API 创建由render方法指定的 HTML 元素,这是通过创建一个通过其属性配置的对象来实现的。用于 React 开发的 JSX 格式允许以声明方式定义 HTML 元素,但当文件被开发工具处理时,结果仍然是 JavaScript,这就是为什么在App呈现方法中使用className而不是class来配置h4元素。JSX 让元素看起来像是使用属性配置的,但它们只是为属性指定值的手段,这也是为什么术语 prop 在 React 开发中如此频繁使用的原因。
注意
使用 JSX 不需要特殊的步骤,它由create-react-app包添加到项目中的工具支持。我会在第九章中解释使用 JSX 定义的元素是如何转换成 JavaScript 的。
在 React 元素中使用表达式
使用表达式配置元素的能力是 React 和 JSX 的关键特性之一。表达式用花括号表示({和}字符),结果被插入到组件生成的内容中。在清单 3-9 中,我使用了一个表达式来设置由App组件呈现的h4元素的内容。
import React, { Component } from "react";
const message = "This is a constant"
export default class App extends Component {
render = () =>
<h4 className="bg-primary text-white text-center p-2 m-1">
{ message }
</h4>
}
Listing 3-9Using an Expression in the App.js File in the src Folder
我定义了一个名为message的常量,并使用一个表达式将message值作为h4元素的内容。为了简化这个例子,我从index.html文件中注释掉了静态 HTML 元素和 DOM API 代码,如清单 3-10 所示。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Primer</title>
</head>
<body>
<!-- <h4 class="bg-primary text-white text-center p-2 m-1">
Static HTML Element
</h4>
<div class="text-center m-2">
<div>This is a span element</div>
<div>This is another span element</div>
</div>
<div id="domParent"></div> -->
<div id="root"></div>
<!-- <script>
let element = document.createElement("h4")
element.className = "bg-primary text-white text-center p-2 m-1";
element.textContent = "DOM API HTML Element";
document.getElementById("domParent").appendChild(element);
</script> -->
</body>
</html>
Listing 3-10Removing Elements in the index.html File in the public Folder
保存修改,你会看到清单 3-9 中定义的常量的值显示在App组件产生的h4元素中,如图 3-6 所示。
图 3-6
使用表达式设置元素的内容
混合表达式和静态内容
表达式可以与静态值结合来创建更复杂的结果,如清单 3-11 所示,它使用一个表达式来设置h4元素的部分内容。
import React, { Component } from "react";
const count = 4
export default class App extends Component {
render = () =>
<h4 className="bg-primary text-white text-center p-2 m-1">
Number of things: { count }
</h4>
}
Listing 3-11Mixing an Expression with Static Content in the App.js File in the src Folder
表达式将count值包含在h4元素的内容中,该值与静态内容相结合,产生如图 3-7 所示的结果。
图 3-7
混合表达式和静态内容
在表达式中执行计算
表达式不仅可以将值注入到组件呈现的内容中,还可以用于任何计算,如清单 3-12 所示。
import React, { Component } from "react";
const count = 4
export default class App extends Component {
render = () =>
<h4 className="bg-primary text-white text-center p-2 m-1">
Number of things: { count % 2 === 0 ? "Even" : "Odd" }
</h4>
}
Listing 3-12Performing a Computation in the App.js File in the src Folder
该示例使用三元运算符来确定count值是奇数还是偶数,并产生如图 3-8 所示的结果。
图 3-8
在表达式中执行计算
表达式非常适合简单的操作,但是试图在表达式中包含太多代码会导致令人困惑的组件。对于更复杂的操作,函数应该由表达式定义和调用,以便函数结果被合并到组件产生的内容中,如清单 3-13 所示。
import React, { Component } from "react";
const count = 4
function isEven() {
return count % 2 === 0 ? "Even" : "Odd";
}
export default class App extends Component {
render = () =>
<h4 className="bg-primary text-white text-center p-2 m-1">
Number of things: { isEven() }
</h4>
}
Listing 3-13Defining a Function in the App.js File in the src Folder
当您在表达式中使用函数时,您必须用圆括号((和)字符)调用它,如清单所示,这样函数的结果就包含在组件生成的内容中。
访问组件属性和方法
需要关键字this来指定组件定义的属性和方法,如清单 3-14 所示。正如我在第二部分中解释的,创建组件有不同的方法,但是我在本书中使用的技术如清单所示,它提供了最广泛的特性,适合大多数项目。
import React, { Component } from "react";
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
count: 4
}
}
isEven() {
return this.state.count % 2 === 0 ? "Even" : "Odd";
}
render = () =>
<h4 className="bg-primary text-white text-center p-2 m-1">
Number of things: { this.isEven() }
</h4>
}
Listing 3-14Using the this Keyword in an Expression in the App.js File in the src Folder
这个清单中的组件定义了一个构造函数,正如我在第四章中解释的,这就是组件初始状态的配置方式。构造函数给state属性分配一个对象,其count值为4。该组件还定义了一个名为isEven的方法,该方法以this.state.count的形式访问count值。this关键字指的是组件实例,如第四章所述;state是指在构造函数中创建的状态属性;并且count选择在计算中使用的值。这个this关键字也用于调用表达式中的isEven方法。结果与前面的清单相同。一些方法需要参数,这些参数可以被指定为表达式的一部分,如清单 3-15 所示。
import React, { Component } from "react";
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
count: 4
}
}
isEven(val) {
return val % 2 === 0 ? "Even" : "Odd";
}
render = () =>
<h4 className="bg-primary text-white text-center p-2 m-1">
Number of things: { this.isEven(this.state.count) }
</h4>
}
Listing 3-15Passing an Argument to a Method in the App.js File in the src Folder
本例中的表达式调用isEven方法,使用count值作为参数。结果与前面的清单相同。
使用表达式设置属性值
表达式还可以用来设置 props 的值,这允许配置 HTML 元素和子组件。在清单 3-16 中,我为App组件添加了一个方法,其结果用于设置h4元素的className属性。
import React, { Component } from "react";
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
count: 4
}
}
isEven(val) {
return val % 2 === 0 ? "Even" : "Odd";
}
getClassName(val) {
return val % 2 === 0
? "bg-primary text-white text-center p-2 m-1"
: "bg-secondary text-white text-center p-2 m-1"
}
render = () =>
<h4 className={this.getClassName(this.state.count)}>
Number of things: { this.isEven(this.state.count) }
</h4>
}
Listing 3-16Setting a Prop Value in the App.js File in the src Folder
结果与前面的清单相同。
使用表达式处理事件
表达式用于告诉 React 当事件被元素触发时如何响应事件。在清单 3-17 中,我为App组件返回的内容添加了一个按钮,并使用onClick属性告诉 React 当click事件被触发时如何响应。
import React, { Component } from "react";
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
count: 4
}
}
isEven(val) {
return val % 2 === 0 ? "Even" : "Odd";
}
getClassName(val) {
return val % 2 === 0
? "bg-primary text-white text-center p-2 m-1"
: "bg-secondary text-white text-center p-2 m-1"
}
handleClick = () => this.setState({ count: this.state.count + 1});
render = () =>
<h4 className={this.getClassName(this.state.count)}>
<button className="btn btn-info m-2" onClick={ this.handleClick }>
Click Me
</button>
Number of things: { this.isEven(this.state.count) }
</h4>
}
Listing 3-17Handling an Event in the App.js File in the src Folder
使用onClick属性配置button元素,它告诉 React 调用handleClick方法来响应click事件。注意,该方法不是用括号指定的。另外,请注意,handleClick方法是使用粗箭头语法定义的;正如我在第十二章中解释的,处理事件是定义方法的方式很重要的少数情况之一。点击按钮更新count属性的值,这将改变render方法中其他表达式的结果,产生如图 3-9 所示的效果。
图 3-9
处理事件
了解引导程序
HTML 元素告诉浏览器它们代表什么样的内容,但是它们不提供任何关于内容应该如何显示的信息。关于如何显示元素的信息是使用级联样式表 (CSS)提供的。CSS 由一组全面的属性和一组选择器组成,前者可用于配置元素外观的各个方面,后者允许应用这些属性。
CSS 的一个主要问题是,一些浏览器对属性的解释略有不同,这可能导致 HTML 内容在不同设备上的显示方式有所不同。跟踪和纠正这些问题可能很困难,CSS 框架已经出现,以帮助 web 应用开发人员以简单和一致的方式设计他们的 HTML 内容。
最流行的 CSS 框架是 Bootstrap,它最初是在 Twitter 上开发的,但已经成为一个广泛使用的开源项目。Bootstrap 由一组 CSS 类和一些可选的 JavaScript 代码组成,这些 CSS 类可以应用于元素以保持一致的样式,这些可选的 JavaScript 代码执行额外的增强功能(但我在本书中没有用到)。我在自己的项目中使用 Bootstrap 它跨浏览器运行良好,并且使用简单。我在本书中使用了 Bootstrap CSS 样式,因为它们让我不必在每一章中定义和列出我自己的定制 CSS 就可以设计我的例子。Bootstrap 提供了比我在本书中使用的更多的特性;详见 http://getbootstrap.com 。
关于 Bootstrap,我不想说得太详细,因为这不是本书的主题,但是我想给你足够的信息,这样你就可以知道例子的哪些部分是 React 特性,哪些与 Bootstrap 相关。
应用基本引导类
引导样式是通过className属性应用的,它是class属性的对应属性,用于对相关元素进行分组。className属性不仅用于应用 CSS 样式,而且是最常见的用途,它支撑着 Bootstrap 和类似框架的运行方式。下面是一个带有classNae属性的 HTML 元素,取自清单 3-9 :
...
<h4 className="bg-primary text-white text-center p-2 m-1">
{ message }
</h4>
...
className prop 将h4元素分配给五个类,它们的名称由空格分隔:bg-primary、text-white、text-center、p-2和m-1。这些类对应于 Bootstrap 定义的样式集合,如表 3-2 所述。
表 3-2
h4 元素类
|名字
|
描述
|
| --- | --- |
| bg-primary | 该类应用样式上下文来提供关于元素用途的视觉提示。请参见“使用上下文类”一节。 |
| text-white | 这个类应用一种样式,将元素内容的文本颜色设置为白色。 |
| text-center | 这个类应用一种水平居中元素内容的样式。 |
| p-2 | 该类应用一种样式,在元素内容周围增加间距,如“使用边距和填充”一节所述。 |
| m-1 | 这个类应用一种样式,在元素周围增加间距,如“使用边距和填充”一节所述。 |
使用上下文类
使用像 Bootstrap 这样的 CSS 框架的主要优点之一是简化了在整个应用中创建一致主题的过程。Bootstrap 定义了一组样式上下文,用于一致地设计相关元素的样式。这些上下文在表 3-3 中描述,用于将引导样式应用于元素的类的名称中。
表 3-3
自举风格的上下文
|名字
|
描述
|
| --- | --- |
| primary | 表示主要动作或内容区域 |
| secondary | 指明内容的支持领域 |
| success | 表示成功的结果 |
| info | 显示附加信息 |
| warning | 显示警告 |
| danger | 提出严重警告 |
| muted | 不强调内容 |
| dark | 通过使用深色来增加对比度 |
| white | 通过使用白色增加对比度 |
Bootstrap 提供了允许样式上下文应用于不同类型元素的类。我在开始本节时使用的h4元素已经被添加到了bg-primary类中,它设置了元素的背景颜色,以表明它与应用的主要目的相关。其他类特定于某一组元素,例如btn-primary,它用于配置button和a元素,使它们显示为按钮,其颜色与主上下文中的其他元素一致。其中一些上下文类必须与配置元素基本样式的其他类结合使用,比如与btn-primary类结合使用的btn类。
使用边距和填充
Bootstrap 包括一组实用程序类,用于添加填充,即元素边缘与其内容之间的空间,以及边距,即元素边缘与其周围元素之间的空间。使用这些类的好处是它们在整个应用中应用一致的间距。
这些类的名称遵循一种定义良好的模式。下面是清单 3-9 中的h4元素:
...
<h4 className="bg-primary text-white text-center p-2 m-1">
{ message }
...
将边距和填充应用于元素的类遵循一个定义良好的命名模式:首先是字母m(用于边距)或p(用于填充),接着是一个可选的字母,用于选择特定的边缘(t用于顶部、b用于底部、l用于左侧、或r用于右侧),然后是一个连字符,最后是一个数字,用于指示应该应用多少空间(0用于无间距,或1、2、3、4或5用于增加数量)。如果没有字母来指定边缘,则边距或填充将应用于所有边缘。为了帮助将这个模式放在上下文中,添加了h4元素的p-2类将填充级别 2 应用于元素的所有边缘。
使用引导程序创建网格
Bootstrap 提供了样式类,可以用来创建不同种类的网格布局,从一列到十二列不等。我在本书的许多例子中使用了网格布局,并且在清单 3-18 中创建了一个简单的网格布局。
import React, { Component } from "react";
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
count: 4
}
}
isEven(val) {
return val % 2 === 0 ? "Even" : "Odd";
}
getClassName(val) {
return val % 2 === 0
? "bg-primary text-white text-center p-2 m-1"
: "bg-secondary text-white text-center p-2 m-1"
}
handleClick = () => this.setState({ count: this.state.count + 1});
render = () =>
<div className="container-fluid p-4">
<div className="row bg-info text-white p-2">
<div className="col font-weight-bold">Value</div>
<div className="col-6 font-weight-bold">Even?</div>
</div>
<div className="row bg-light p-2 border">
<div className="col">{ this.state.count }</div>
<div className="col-6">{ this.isEven( this.state.count) }</div>
</div>
<div className="row">
<div className="col">
<button className="btn btn-info m-2"
onClick={ this.handleClick }>
Click Me
</button>
</div>
</div>
</div>
}
Listing 3-18Creating a Grid in the App.js File in the src Folder
自举网格布局系统易于使用。一个顶级的div元素被分配给container类(或者是container-fluid类,如果你想让它跨越可用空间的话)。通过将row类应用到div元素来指定列,这具有为div元素包含的内容设置网格布局的效果。
每行定义 12 列,您可以通过指定一个名为col-后跟列数的类来指定每个子元素将占用多少列。例如,类col-1指定一个元素占据一列,col-2指定两列,依此类推,直到col-12,它指定一个元素填充整个行。如果您省略了列数,而只是将一个元素分配给了col类,那么 Bootstrap 将分配等量的剩余列。清单 3-18 中的网格产生如图 3-10 所示的布局。
图 3-10
使用网格布局
使用引导程序设计表格
Bootstrap 包括对样式化table元素及其内容的支持,这是我在后面章节的一些例子中使用的一个特性。表 3-4 列出了使用表的关键引导类。
表 3-4
表格的引导 CSS 类
|名字
|
描述
|
| --- | --- |
| table | 对一个table元素及其行应用常规样式 |
| table-striped | 对table正文中的行应用隔行条带化 |
| table-bordered | 将边框应用于所有行和列 |
| table-sm | 减少表格中的间距以创建更紧凑的布局 |
所有这些类都直接应用于table元素,如清单 3-19 所示,其中我用表格替换了网格布局。
import React, { Component } from "react";
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
count: 4
}
}
isEven(val) {
return val % 2 === 0 ? "Even" : "Odd";
}
getClassName(val) {
return val % 2 === 0
? "bg-primary text-white text-center p-2 m-1"
: "bg-secondary text-white text-center p-2 m-1"
}
handleClick = () => this.setState({ count: this.state.count + 1});
render = () =>
<table className="table table-striped table-bordered table-sm">
<thead className="bg-info text-white">
<tr><th>Value</th><th>Even?</th></tr>
</thead>
<tbody>
<tr>
<td>{ this.state.count }</td>
<td>{ this.isEven(this.state.count) } </td>
</tr>
</tbody>
<tfoot className="text-center">
<tr>
<td colSpan="2">
<button className="btn btn-info m-2"
onClick={ this.handleClick }>
Click Me
</button>
</td>
</tr>
</tfoot>
</table>
}
Listing 3-19Using a Table Layout in the App.js File in the src Folder
小费
注意,在定义清单 3-19 中的表格时,我使用了thead元素。如果一个tbody元素没有被使用,浏览器会自动添加任何tr元素,这些元素是table元素的直接后代。如果在使用 Bootstrap 时依赖于这种行为,您将会得到奇怪的结果,并且在定义表时使用完整的元素集总是一个好主意。
图 3-11 显示了用表格代替网格的结果。
图 3-11
设计表格
使用引导程序设计表单
Bootstrap 包括表单元素的样式,允许它们与应用中的其他元素保持一致。在清单 3-20 中,我向由App组件产生的内容添加了表单元素。
import React, { Component } from "react";
export default class App extends Component {
render = () =>
<div className="m-2">
<div className="form-group">
<label>Name:</label>
<input className="form-control" />
</div>
<div className="form-group">
<label>City:</label>
<input className="form-control" />
</div>
</div>
}
Listing 3-20Adding Form Elements in the App.js File in the src Folder
表单的基本样式是通过将form-group类应用于包含label和input元素的div元素来实现的,其中input元素被分配给form-control类。Bootstrap 对元素进行样式化,使label显示在input元素上方,而input元素占据 100%的可用水平空间,如图 3-12 所示。
图 3-12
样式表单元素
摘要
在这一章中,我提供了 HTML 的简要概述,并解释了它如何在 React 开发中与 JavaScript 代码混合,尽管有一些变化和限制。我还介绍了 Bootstrap CSS 框架,我在本书中一直使用它,但它与 React 没有直接关系。您需要很好地掌握 HTML 和 CSS,以便在 web 应用开发中真正有效,但最好的学习方法是通过第一手经验,本章中的描述和示例将足以让您入门,并为前面的示例提供足够的背景信息。在下一章,我将继续初级主题,介绍本书中使用的最重要的 JavaScript 特性。