React16 高级教程(二)
原文:Pro React 16
四、JavaScript 优先
在这一章中,我快速浏览了 JavaScript 语言应用于 React 开发的最重要的特性。我没有足够的空间来完整地描述 JavaScript,所以我把重点放在了你需要快速掌握并遵循本书中的例子的要点上。
近年来,JavaScript 已经现代化,增加了方便的语言特性,并对常见任务(如数组处理)可用的实用函数进行了大量扩展。并非所有的浏览器都支持最新的特性,因此 React 开发工具包括 Babel 包,它负责将使用最新特性编写的 JavaScript 转换成可以在大多数主流浏览器中工作的代码。这意味着您可以享受现代开发体验,而无需关注处理浏览器之间的差异和跟踪每个浏览器支持的功能。表 4-1 总结了本章内容。
表 4-1
章节总结
|问题
|
解决办法
|
列表
|
| --- | --- | --- |
| 提供将由浏览器执行的指令 | 使用 JavaScript 语句 | four |
| 将语句的执行延迟到需要的时候 | 使用 JavaScript 函数 | 5–7, 10–12 |
| 定义参数数量可变的函数 | 使用默认和 rest 参数 | 8, 9 |
| 简洁地表达功能 | 使用粗箭头功能 | Thirteen |
| 定义变量和常数 | 使用let和const关键字 | 14, 15 |
| 使用 JavaScript 基本类型 | 使用string、number或boolean关键字 | 16, 17, 19 |
| 定义包含其他值的字符串 | 使用模板字符串 | Eighteen |
| 有条件地执行语句 | 使用if、else和switch关键字 | Twenty |
| 比较价值观和身份 | 使用等式和标识运算符 | 21, 22 |
| 转换类型 | 使用类型转换关键字 | 23–25 |
| 分组相关项目 | 定义一个数组 | 26, 27 |
| 读取或更改数组中的值 | 使用索引访问器符号 | 28, 29 |
| 枚举数组的内容 | 使用for循环或forEach方法 | Thirty |
| 展开数组的内容 | 使用扩展运算符 | 31, 32 |
| 处理数组的内容 | 使用内置数组方法 | Thirty-three |
| 将相关值收集到一个单元中 | 使用文本或类定义对象 | 34–36, 40 |
| 定义可以对对象的值执行的操作 | 定义一种方法 | 37, 39, 43, 44 |
| 将属性和值从一个对象复制到另一个对象 | 使用Object.assign方法或使用扩展运算符 | 41, 42 |
| 群组相关功能 | 定义一个 JavaScript 模块 | 45–54 |
| 观察异步操作 | 定义一个Promise并使用async和await关键字 | 55–58 |
为本章做准备
在这一章中,我继续使用在第三章中创建的primer项目。为了准备本章,我在src文件夹中添加了一个名为example.js的文件,并添加了清单 4-1 中所示的代码。
小费
你可以从 https://github.com/Apress/pro-react-16 下载本章以及本书其他章节的示例项目。
console.log("Hello");
Listing 4-1The Contents of the example.js File in the src Folder
为了将example.js文件合并到应用中,我将清单 4-2 中所示的语句添加到了src文件夹中的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';
import "./example";
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 4-2Importing a File in the index.js File in the src Folder
打开命令提示符,导航到primer文件夹,运行清单 4-3 中所示的命令来启动 React 开发工具。
npm start
Listing 4-3Starting the Development Tools
项目的初始准备需要一段时间,之后将打开一个新的浏览器窗口或选项卡并导航到http://localhost:3000,显示如图 4-1 所示的内容。
图 4-1
运行示例应用
打开浏览器的 F12 开发工具,这通常可以通过按键盘上的 F12 或在浏览器窗口中右键单击并从弹出菜单中选择 Inspect 来完成。检查控制台选项卡,您会看到清单 4-1 中的example.js文件中的语句产生了一个简单的结果,如图 4-2 所示。
图 4-2
浏览器控制台中的结果
本章中的所有示例都会产生文本输出,因此我将只使用文本,而不是显示控制台选项卡的屏幕截图,如下所示:
Hello
使用语句
基本的 JavaScript 构建块是语句。每条语句代表一条命令,语句通常以分号(;)结束。分号是可选的,但是使用分号会使代码更容易阅读,并且允许在一行中有多个语句。在清单 4-4 中,我向 JavaScript 文件添加了语句。
console.log("Hello");
console.log("Apples");
console.log("This is a statement");
console.log("This is also a statement");
Listing 4-4Adding JavaScript Statements in the example.js File in the src Folder
浏览器依次执行每条语句。在本例中,所有语句都只是将消息写入控制台。结果如下:
Hello
Apples
This is a statement
This is also a statement
定义和使用函数
当浏览器收到 JavaScript 代码时,它会按照定义的顺序执行其中包含的语句。这就是上一个示例中发生的情况。example.js文件中的语句被逐一执行,所有语句都向控制台写入一条消息,所有语句都是按照它们在example.js中定义的顺序执行的。您还可以将语句打包到一个函数中,直到浏览器遇到一个调用该函数的语句,该函数才会被执行,如清单 4-5 所示。
const myFunc = function () {
console.log("This statement is inside the function");
};
console.log("This statement is outside the function");
myFunc();
Listing 4-5Defining a JavaScript Function in the example.js File in the src Folder
定义一个函数很简单:使用const关键字,后跟您想要给函数起的名字,再加上等号(=)和function关键字,再加上括号((和)字符)。您希望函数包含的语句用大括号括起来(字符{和})。
在清单中,我使用了名称myFunc,该函数包含一个向 JavaScript 控制台写入消息的语句。在浏览器到达另一个调用myFunc函数的语句之前,函数中的语句不会被执行,如下所示:
...
myFunc();
...
当您保存对example.js文件的更改时,更新的 JavaScript 代码将被发送到浏览器,在浏览器中执行并产生以下输出:
This statement is outside the function
This statement is inside the function
您可以看到函数内部的语句并没有立即执行,但是除了演示函数是如何定义的以外,这个例子并不是特别有用,因为函数是在定义后立即被调用的。当响应某种变化或事件(如用户交互)而调用函数时,函数会更有用。
您还可以定义函数,这样就不必显式地创建和分配变量,如清单 4-6 所示。
function myFunc() {
console.log("This statement is inside the function");
}
console.log("This statement is outside the function");
myFunc();
Listing 4-6Defining a Function in the example.js File in the src Folder
代码的工作方式与清单 4-5 相同,但对大多数开发人员来说更熟悉。这个例子产生了与清单 4-5 相同的结果。
用参数定义函数
JavaScript 允许您为函数定义参数,如清单 4-7 所示。
function myFunc(name, weather) {
console.log("Hello " + name + ".");
console.log("It is " + weather + " today.");
}
myFunc("Adam", "sunny");
Listing 4-7Defining Functions with Parameters in the example.js File in the src Folder
我给myFunc函数添加了两个参数,称为name和weather。JavaScript 是一种动态类型语言,这意味着在定义函数时不必声明参数的数据类型。当我在本章后面讲述 JavaScript 变量时,我会回到动态类型。要调用带参数的函数,需要在调用函数时提供值作为参数,如下所示:
...
myFunc("Adam", "sunny");
...
该清单的结果如下:
Hello Adam.
It is sunny today.
使用默认和 Rest 参数
调用函数时提供的参数数量不需要与函数中的参数数量相匹配。如果调用函数时使用的参数少于它拥有的参数,那么任何没有提供值的参数的值都是undefined,这是一个特殊的 JavaScript 值。如果调用函数时使用的参数多于实际参数,那么多余的参数将被忽略。
这样做的结果是,您不能创建两个具有相同名称和不同参数的函数,并期望 JavaScript 根据您在调用函数时提供的参数来区分它们。这被称为多态性,尽管它在 Java 和 C#等语言中受支持,但在 JavaScript 中不可用。相反,如果您定义了两个同名的函数,那么第二个定义将替换第一个定义。
有两种方法可以修改函数,以响应函数定义的参数数量和用于调用函数的参数数量之间的不匹配。默认参数处理实参比参数少的情况,允许你为没有实参的参数提供默认值,如清单 4-8 所示。
function myFunc(name, weather = "raining") {
console.log("Hello " + name + ".");
console.log("It is " + weather + " today.");
}
myFunc("Adam");
Listing 4-8Using a Default Parameter in the example.js File in the src Folder
函数中的weather参数已被赋予默认值raining,如果仅使用一个参数调用该函数,将使用该值,产生以下结果:
Hello Adam.
It is raining today.
Rest 参数用于在用附加参数调用函数时捕获任何附加参数,如清单 4-9 所示。
function myFunc(name, weather, ...extraArgs) {
console.log("Hello " + name + ".");
console.log("It is " + weather + " today.");
for (let i = 0; i < extraArgs.length; i++) {
console.log("Extra Arg: " + extraArgs[i]);
}
}
myFunc("Adam", "sunny", "one", "two", "three");
Listing 4-9Using a Rest Parameter in the example.js File in the src Folder
rest 参数必须是函数定义的最后一个参数,其名称以省略号为前缀(三个句点,...)。rest 参数是一个数组,任何额外的参数都将被赋给它。在清单中,该函数将每个额外的参数打印到控制台,产生以下结果:
Hello Adam.
It is sunny today.
Extra Arg: one
Extra Arg: two
Extra Arg: three
定义返回结果的函数
您可以使用return关键字从函数中返回结果。清单 4-10 显示了一个返回结果的函数。
function myFunc(name) {
return ("Hello " + name + ".");
}
console.log(myFunc("Adam"));
Listing 4-10Returning a Result from a Function in the example.js File in the src Folder
这个函数定义了一个参数,并用它来产生一个结果。我调用函数并将结果作为参数传递给console.log函数,如下所示:
...
console.log(myFunc("Adam"));
...
请注意,您不必声明该函数将返回一个结果或表示结果的数据类型。该清单的结果如下:
Hello Adam.
将函数用作其他函数的参数
JavaScript 函数可以被视为对象,这意味着您可以使用一个函数作为另一个函数的参数,如清单 4-11 所示。
function myFunc(nameFunction) {
return ("Hello " + nameFunction() + ".");
}
console.log(myFunc(function () {
return "Adam";
}));
Listing 4-11Using a Function as an Arguments in the example.js File in the src Folder
myFunc函数定义了一个名为nameFunction的参数,它调用这个参数来获取插入到它返回的字符串中的值。我将一个返回Adam作为参数的函数传递给myFunc,它产生以下输出:
Hello Adam.
函数可以链接在一起,从小而容易测试的代码片段中构建更复杂的功能,如清单 4-12 所示。
function myFunc(nameFunction) {
return ("Hello " + nameFunction() + ".");
}
function printName(nameFunction, printFunction) {
printFunction(myFunc(nameFunction));
}
printName(function () { return "Adam" }, console.log);
Listing 4-12Chaining Functions Calls in the example.js File in the src Folder
此示例产生以下输出:
Hello Adam.
使用箭头功能
箭头函数——也称为胖箭头函数或λ表达式——是定义函数的另一种方式,通常用于定义仅用作其他函数参数的函数。清单 4-13 用箭头函数替换了前一个例子中的函数。
const myFunc = (nameFunction) => ("Hello " + nameFunction() + ".");
const printName = (nameFunction, printFunction) =>
printFunction(myFunc(nameFunction));
printName(function () { return "Adam" }, console.log);
Listing 4-13Using Arrow Functions in the example.js File in the src Folder
这些函数与清单 4-12 中的函数执行相同的工作。箭头函数有三个部分:输入参数、等号和大于号(“箭头”),最后是函数结果。只有当 arrow 函数需要执行多条语句时,才需要关键字return和花括号。在这一章的后面有更多的箭头函数的例子,你会在整本书中看到它们的使用。
注意
在 React 开发中,您可以决定您更喜欢使用哪种风格的函数,您将会看到我在本书的示例中两者都使用。然而,如第十二章所述,在定义响应事件的函数时必须小心。
使用变量和类型
let关键字用于声明变量,也可以在一条语句中为变量赋值——与我在前面的例子中使用的const关键字相反,它创建一个不可修改的常量值。
当您使用let或const时,您创建的变量或常量只能在定义它们的代码区域中被访问,这被称为变量或常量的作用域,如清单 4-14 所示。
function messageFunction(name, weather) {
let message = "Hello, Adam";
if (weather === "sunny") {
let message = "It is a nice day";
console.log(message);
} else {
let message = "It is " + weather + " today";
console.log(message);
}
console.log(message);
}
messageFunction("Adam", "raining");
Listing 4-14Using let to Declare Variables in the example.js File in the src Folder
在这个例子中,有三个语句使用let关键字来定义一个名为message的变量。每个变量的范围限于定义它的代码区域,产生以下结果:
It is raining today
Hello, Adam
这似乎是一个奇怪的例子,但是还有另一个关键字可以用来声明变量:var。let和const关键字是 JavaScript 规范中相对较新的补充,旨在解决var行为方式中的一些奇怪之处。清单 4-15 以清单 4-14 为例,将let替换为var。
使用 Let 和 Const
对于您不希望更改的任何值,使用const关键字是一个很好的实践,这样,如果试图进行任何修改,您都会收到一个错误。然而,这是我很少遵循的一种做法——一部分是因为我仍然在努力适应不使用var关键字,另一部分是因为我用一系列语言编写代码,并且有一些我避免的功能,因为当我从一种语言切换到另一种语言时它们会绊倒我。如果你是 JavaScript 新手,那么我建议你试着正确使用const和let,避免步我后尘。
function messageFunction(name, weather) {
var message = "Hello, Adam";
if (weather === "sunny") {
var message = "It is a nice day";
console.log(message);
} else {
var message = "It is " + weather + " today";
console.log(message);
}
console.log(message);
}
messageFunction("Adam", "raining");
Listing 4-15Using var to Declare Variables in the example.js File in the src Folder
当您保存列表中的更改时,您将看到以下结果:
It is raining today
It is raining today
有些浏览器会将重复的语句显示为一行,旁边有一个数字,表示该输出发生了多少次。这意味着您可能会看到一个旁边带有数字 2 的语句,表明它出现了两次。
问题是var关键字创建的变量的作用域是包含函数,这意味着所有对message的引用都是指同一个变量。这甚至会给有经验的 JavaScript 开发人员带来意想不到的结果,这也是引入更传统的let关键字的原因。React 开发工具包括常见问题的警告,这就是为什么您还会在 JavaScript 控制台中看到以下消息:
Line 4: 'message' is already defined no-redeclare
Line 7: 'message' is already defined no-redeclare
在您熟悉这些消息之前,它们可能是神秘的,了解它们的最简单方法是查阅 ESLint 包的文档,该包将一组规则应用于 JavaScript 代码,React 开发工具使用它来创建警告。规则的名称包含在警告中,产生清单 4-15 警告的规则名称是 no-redeclare,在 https://eslint.org/docs/rules/no-redeclare 中有描述。
使用可变闭包
如果你在另一个函数内部定义一个函数——创建内部和外部函数——那么内部函数能够访问外部函数的变量,使用一个叫做闭包的特性,就像这样:
function myFunc(name) {
let myLocalVar = "sunny";
let innerFunction = function () {
return ("Hello " + name + ". Today is " + myLocalVar + ".");
}
return innerFunction();
}
console.log(myFunc("Adam"));
这个例子中的内部函数能够访问外部函数的局部变量,包括它的参数。这是一个强大的特性,意味着您不必在内部函数上定义参数来传递数据值,但是需要小心,因为当使用像counter或index这样的普通变量名时,很容易得到意外的结果,您可能没有意识到您正在重用外部函数中的变量名。
使用基本类型
JavaScript 定义了一组基本的原语类型:string、number、boolean。这似乎是一个很短的列表,但是 JavaScript 设法将很多灵活性融入到这些类型中。
小费
我在这里简化。您可能会遇到另外三种原语。已经声明但没有赋值的变量是undefined,而null值用来表示一个变量没有值,就像其他语言一样。最后一个原语类型是Symbol,它是一个不可变的值,表示一个惟一的 ID,但是在编写本文时还没有广泛使用。
使用布尔值
boolean类型有两个值:true和false。清单 4-16 显示了正在使用的两个值,但是这种类型在条件语句中使用时最有用,比如一个if语句。这个清单没有控制台输出,尽管您会看到警告,因为变量已经定义并且没有使用。
let firstBool = true;
let secondBool = false;
Listing 4-16Defining boolean Values in the example.js File in the src Folder
使用字符串
您可以使用双引号或单引号字符来定义string值,如清单 4-17 所示。
let firstString = "This is a string";
let secondString = 'And so is this';
Listing 4-17Defining string Variables in the example.js File in the src Folder
您使用的引号字符必须匹配。例如,你不能用单引号开始一个字符串,然后用双引号结束。此列表没有控制台输出。JavaScript 为string对象提供了一组基本的属性和方法,其中最有用的在表 4-2 中有描述。
表 4-2
有用的字符串属性和方法
|名字
|
描述
|
| --- | --- |
| length | 此属性返回字符串中的字符数。 |
| charAt(index) | 此方法返回包含指定索引处的字符的字符串。 |
| concat(string) | 此方法返回一个新字符串,该字符串将调用该方法的字符串和作为参数提供的字符串连接在一起。 |
| indexOf(term, start) | 该方法返回第一个索引,在该索引处term出现在字符串中,如果没有匹配,则返回-1。可选的start参数指定搜索的起始索引。 |
| replace(term, newTerm) | 该方法返回一个新字符串,其中所有的term实例都被替换为newTerm。 |
| slice(start, end) | 此方法返回包含起始和结束索引之间的字符的子字符串。 |
| split(term) | 这个方法将一个字符串分割成一个由term分隔的值数组。 |
| toUpperCase()``toLowerCase() | 这些方法返回所有字符都是大写或小写的新字符串。 |
| trim() | 此方法返回一个新字符串,其中所有的前导和尾随空白字符都已被删除。 |
使用模板字符串
一个常见的编程任务是将静态内容与数据值结合起来,以生成可以呈现给用户的字符串。传统的方法是通过字符串连接,这是我在本章的例子中一直使用的方法,如下所示:
...
let message = "It is " + weather + " today";
...
JavaScript 还支持模板字符串,它允许内联指定数据值,这有助于减少错误,带来更自然的开发体验。清单 4-18 展示了模板字符串的使用。
function messageFunction(weather) {
let message = `It is ${weather} today`;
console.log(message);
}
messageFunction("raining");
Listing 4-18Using a Template String in the example.js File in the src Folder
模板字符串以反斜杠(```jsx 字符)开始和结束,数据值由花括号表示,前面有一个美元符号。例如,这个字符串将变量weather的值合并到模板字符串中:
...
let message = `It is ${weather} today`;
...
```jsx
此示例产生以下输出:
It is raining today
#### 使用数字
`number`类型用于表示*整数*和*浮点*(也称为*实数*)。清单 4-19 提供了一个演示。
let daysInWeek = 7; let pi = 3.14; let hexValue = 0xFFFF;
Listing 4-19Defining number Values in the example.js File in the src Folder
您不必指定使用哪种号码。您只需表达您需要的值,JavaScript 就会相应地执行。在清单中,我定义了一个整数值、一个浮点值,并在一个值前面加上了`0x`来表示一个十六进制值。
## 使用 JavaScript 运算符
JavaScript 定义了一组非常标准的操作符。我在表 4-3 中总结了最有用的。
表 4-3
有用的 JavaScript 运算符
<colgroup><col class="tcol1 align-left"> <col class="tcol2 align-left"></colgroup>
|
操作员
|
描述
|
| --- | --- |
| `++, --` | 前或后递增和递减 |
| `+, -, *, /, %` | 加法、减法、乘法、除法、余数 |
| `<, <=, >, >=` | 小于,小于等于,大于,大于等于 |
| `==, !=` | 平等和不平等测试 |
| `===, !==` | 同一性和非同一性测试 |
| `&&, ||` | 逻辑 AND 和 OR (||用于合并空值) |
| `=` | 分配 |
| `+` | 串并置 |
| `?:` | 三操作数条件语句 |
### 使用条件语句
许多 JavaScript 操作符与条件语句一起使用。在本书中,我倾向于使用`if/else`和`switch`语句。清单 4-20 展示了两者的用法,这对大多数开发者来说都是熟悉的。
let name = "Adam";
if (name === "Adam") { console.log("Name is Adam"); } else if (name === "Jacqui") { console.log("Name is Jacqui"); } else { console.log("Name is neither Adam or Jacqui"); }
switch (name) { case "Adam": console.log("Name is Adam"); break; case "Jacqui": console.log("Name is Jacqui"); break; default: console.log("Name is neither Adam or Jacqui"); break; }
Listing 4-20Using Conditional Statements in the example.js File in the src Folder
此示例产生以下结果:
Name is Adam Name is Adam
### 相等运算符与相同运算符
等式和等式运算符特别值得注意。相等运算符将尝试将操作数强制(转换)为相同的类型来评估相等性。这是一个方便的特性,只要你意识到它正在发生。清单 4-21 展示了等式操作符的作用。
let firstVal = 5; let secondVal = "5";
if (firstVal == secondVal) { console.log("They are the same"); } else { console.log("They are NOT the same"); }
Listing 4-21Using the Equality Operator in the example.js File in the src Folder
该示例的输出如下:
They are the same
JavaScript 将两个操作数转换成相同的类型,并对它们进行比较。本质上,相等运算符测试值是否相同,而不管它们的类型如何。这造成了足够的混乱,以至于您还会在 JavaScript 控制台中看到一条警告:
Line 4: Expected '===' and instead saw '==' eqeqeq
一种更容易预测的比较方式是使用恒等运算符(`===`,三个等号,而不是两个等号),如清单 4-22 所示。
let firstVal = 5; let secondVal = "5";
if (firstVal === secondVal) {
console.log("They are the same");
} else { console.log("They are NOT the same"); }
Listing 4-22Using the Identity Operator in the example.js File in the src Folder
在本例中,identity 运算符将认为这两个变量是不同的。该运算符不强制类型。结果如下:
They are NOT the same
### 显式转换类型
字符串连接操作符(`+`)优先于加法操作符(还有`+`),这意味着 JavaScript 将优先于加法连接变量。这可能会造成混乱,因为 JavaScript 也会自由地转换类型以产生结果——而不总是预期的结果,如清单 4-23 所示。
let myData1 = 5 + 5;
let myData2 = 5 + "5";
console.log("Result 1: " + myData1); console.log("Result 2: " + myData2);
Listing 4-23String Concatenation Operator Precedence in the example.js File in the src Folder
这些语句会产生以下结果:
Result 1: 10 Result 2: 55
第二种结果是引起混乱的那种。通过运算符优先级和过急类型转换的组合,原本应该是加法运算的操作被解释为字符串串联。为了避免这种情况,可以显式转换值的类型,以确保执行正确的操作,如以下部分所述。
#### 将数字转换为字符串
如果您正在处理多个数字变量,并希望将它们连接成字符串,那么您可以使用`toString`方法将数字转换成字符串,如清单 4-24 所示。
let myData1 = (5).toString() + String(5);
console.log("Result: " + myData1);
Listing 4-24Using the number.toString Method in the example.js File in the src Folder
注意,我将数值放在括号中,然后调用了`toString`方法。这是因为在调用`number`类型定义的方法之前,您必须允许 JavaScript 将文字值转换成`number`。我还展示了实现相同效果的另一种方法,即调用`String`函数,并将数值作为参数传入。这两种技术具有相同的效果,都是将一个`number`转换成一个`string`,这意味着`+`操作符用于字符串连接而不是加法。该脚本的输出如下:
Result: 55
还有一些其他的方法可以让你更好地控制一个数字如何被表示成一个字符串。我在表 4-4 中简要描述了这些方法。表格中显示的所有方法都由`number`类型定义。
表 4-4
有用的数字到字符串的方法
<colgroup><col class="tcol1 align-left"> <col class="tcol2 align-left"></colgroup>
|
方法
|
描述
|
| --- | --- |
| `toString()` | 此方法返回一个表示以 10 为基数的数字的字符串。 |
| `toString(2)``toString(8)``toString(16)` | 此方法返回以二进制、八进制或十六进制表示法表示数字的字符串。 |
| `toFixed(n)` | 该方法返回一个表示小数点后有`n`位的实数的字符串。 |
| `toExponential(n)` | 该方法返回一个字符串,该字符串使用指数表示法表示一个数字,小数点前有一位数字,小数点后有`n`位数字。 |
| `toPrecision(n)` | 该方法返回一个字符串,该字符串表示一个具有`n`个有效数字的数字,如果需要,可以使用指数符号。 |
#### 将字符串转换为数字
补充技术是将字符串转换为数字,这样您就可以执行加法而不是连接。你可以用`Number`函数来实现,如清单 4-25 所示。
let firstVal = "5"; let secondVal = "5";
let result = Number(firstVal) + Number(secondVal); console.log("Result: " + result);
Listing 4-25Converting Strings to Numbers in the example.js File in the src Folder
该脚本的输出如下:
Result: 10
`Number`函数解析字符串值的方式非常严格,但是您可以使用另外两个更灵活的函数,它们会忽略后面的非数字字符。这些功能是`parseInt`和`parseFloat`。我已经在表 4-5 中描述了所有三种方法。
表 4-5
对数字方法有用的字符串
<colgroup><col class="tcol1 align-left"> <col class="tcol2 align-left"></colgroup>
|
方法
|
描述
|
| --- | --- |
| `Number(str)` | 此方法分析指定的字符串以创建整数或实数值。 |
| `parseInt(str)` | 此方法分析指定的字符串以创建整数值。 |
| `parseFloat(str)` | 此方法分析指定的字符串以创建整数或实数值。 |
## 使用数组
JavaScript 数组的工作方式类似于大多数其他编程语言中的数组。清单 4-26 展示了如何创建和填充一个数组。
let myArray = new Array();
myArray[0] = 100;
myArray[1] = "Adam";
myArray[2] = true;
Listing 4-26Creating and Populating an Array in the example.js File in the src Folder
我通过调用`new Array()`创建了一个新数组。这创建了一个空数组,我将它赋给了变量`myArray`。在随后的语句中,我为数组中的不同索引位置赋值。(这个清单没有输出。)
在这个例子中有一些事情需要注意。首先,在创建数组时,我不需要声明数组中的项数。JavaScript 数组会自动调整大小以容纳任意数量的项目。第二点是,我不必声明数组将保存的数据类型。任何 JavaScript 数组都可以包含任何混合的数据类型。在这个例子中,我给数组分配了三个项目:一个`number`、一个`string`和一个`boolean`。
### 使用数组文本
清单 4-26 中的例子产生了一个警告,因为使用`new Array()`不是创建数组的标准方式。相反,array literal 风格允许您在一条语句中创建和填充一个数组,如清单 4-27 所示。
let myArray = [100, "Adam", true];
Listing 4-27Using the Array Literal Style in the example.js File in the src Folder
在这个例子中,我通过在方括号(`[`和`]`)之间指定我想要的数组中的项目,指定了应该给`myArray`变量分配一个新的数组。(这个清单没有控制台输出,尽管会有一个警告,因为数组已经定义但没有使用。)
### 读取和修改数组的内容
使用方括号(`[`和`]`)读取给定索引处的值,将所需的索引放在括号之间,如清单 4-28 所示。
let myArray = [100, "Adam", true];
console.log(Index 0: ${myArray[0]});
Listing 4-28Reading the Data from an Array Index in the example.js File in the src Folder
只需给索引赋值,就可以修改 JavaScript 数组中任何位置的数据。就像常规变量一样,您可以在索引处切换数据类型,不会有任何问题。清单的输出如下所示:
Index 0: 100
清单 4-29 演示了如何修改数组的内容。
let myArray = [100, "Adam", true];
myArray[0] = "Tuesday";
console.log(Index 0: ${myArray[0]});
Listing 4-29Modifying the Contents of an Array in the example.js File in the src Folder
在这个例子中,我将一个`string`赋值给数组中的位置`0`,这个位置以前是由一个`number`持有的,并产生以下输出:
Index 0: Tuesday
### 枚举数组的内容
使用一个`for`循环或者使用`forEach`方法来枚举数组的内容,该方法接收一个被调用来处理数组中每个元素的函数。两种方法如清单 4-30 所示。
let myArray = [100, "Adam", true];
for (let i = 0; i < myArray.length; i++) {
console.log(Index ${i}: ${myArray[i]});
}
console.log("---");
myArray.forEach((value, index) => console.log(Index ${index}: ${value}));
Listing 4-30Enumerating the Contents of an Array in the example.js File in the src Folder
JavaScript `for`循环的工作方式与许多其他语言中的循环一样。使用`length`属性确定数组中有多少个元素。
传递给`forEach`方法的函数有两个参数:要处理的当前项的值和该项在数组中的位置。在这个清单中,我使用了一个 arrow 函数作为`forEach`方法的参数,这是它们擅长的一种用法(您将在本书中看到这种用法)。清单的输出如下所示:
Index 0: 100 Index 1: Adam Index 2: true
Index 0: 100 Index 1: Adam Index 2: true
### 使用扩展运算符
spread 运算符用于扩展数组,以便其内容可以用作函数参数。清单 4-31 定义了一个函数,该函数接受多个参数,并使用数组中的值调用它,使用或不使用 spread 运算符。
function printItems(numValue, stringValue, boolValue) {
console.log(Number: ${numValue});
console.log(String: ${stringValue});
console.log(Boolean: ${boolValue});
}
let myArray = [100, "Adam", true];
printItems(myArray[0], myArray[1], myArray[2]);
printItems(...myArray);
Listing 4-31Using the Spread Operator in the example.js File in the src Folder
spread 操作符是一个省略号(三个句点的序列),它导致数组被解包并作为单独的参数传递给`printItems`函数。
... printItems(...myArray); ...
spread 操作符也使得连接数组变得容易,如清单 4-32 所示。
let myArray = [100, "Adam", true]; let myOtherArray = [200, "Bob", false, ...myArray];
myOtherArray.forEach((value, index) => console.log(Index ${index}: ${value}));
Listing 4-32Concatenating Arrays in the example.js File in the src Folder
使用 spread 操作符,我可以在定义`myOtherArray`时将`myArray`指定为一个项,结果是第一个数组的内容将被解包并作为项添加到第二个数组中。此示例产生以下结果:
Index 0: 200 Index 1: Bob Index 2: false Index 3: 100 Index 4: Adam Index 5: true
### 注意
数组也可以被去结构化,数组的各个元素被分配给不同的变量,这样`[var1, var2] = [3, 4]`将值`3`分配给`var1`,将值`4`分配给`var2`。钩子特性使用数组分解,这在第十一章中有描述。
### 使用内置数组方法
JavaScript `Array`对象定义了许多可以用来处理数组的方法,表 4-6 中描述了其中最有用的方法。
表 4-6
有用的数组方法
<colgroup><col class="tcol1 align-left"> <col class="tcol2 align-left"></colgroup>
|
方法
|
描述
|
| --- | --- |
| `concat(otherArray)` | 此方法返回一个新数组,该数组将调用它的数组与指定为参数的数组连接起来。可以指定多个数组。 |
| `join(separator)` | 该方法将数组中的所有元素连接起来形成一个字符串。该参数指定用于分隔各项的字符。 |
| `pop()` | 此方法移除并返回数组中的最后一项。 |
| `shift()` | 此方法移除并返回数组中的第一个元素。 |
| `push(item)` | 此方法将指定的项追加到数组的末尾。 |
| `unshift(item)` | 此方法在数组的开头插入一个新项。 |
| `reverse()` | 此方法返回一个新数组,该数组包含逆序排列的项。 |
| `slice(start,end)` | 此方法返回数组的一部分。 |
| `sort()` | 此方法对数组进行排序。可选的比较功能可用于执行自定义比较。 |
| `splice(index, count)` | 该方法从指定的`index`开始,从数组中移除`count`项。移除的项作为方法的结果返回。 |
| `unshift(item)` | 此方法在数组的开头插入一个新项。 |
| `every(test)` | 该方法为数组中的每一项调用`test`函数,如果函数为所有项返回`true`,则返回`true`,否则返回`false`。 |
| `some(test)` | 如果为数组中的每一项调用`test`函数至少返回一次`true`,则该方法返回`true`。 |
| `filter(test)` | 该方法返回一个新数组,其中包含了`test`函数返回的`true`项。 |
| `find(test)` | 该方法返回数组中第一个项目,对于该项目,`test`函数返回`true`。 |
| `findIndex(test)` | 该方法返回数组中第一项的索引,对于该数组,`test`函数返回`true`。 |
| `forEach(callback)` | 这个方法为数组中的每一项调用`callback`函数,如前一节所述。 |
| `includes(value)` | 如果数组包含指定的值,这个方法返回`true`。 |
| `map(callback)` | 该方法返回一个新数组,其中包含为数组中的每一项调用`callback`函数的结果。 |
| `reduce(callback)` | 该方法返回通过调用回调函数为数组中的每一项生成的累计值。 |
由于表 4-6 中的许多方法返回一个新数组,这些方法可以链接在一起处理数据,如清单 4-33 所示。
let products = [ { name: "Hat", price: 24.5, stock: 10 }, { name: "Kayak", price: 289.99, stock: 1 }, { name: "Soccer Ball", price: 10, stock: 0 }, { name: "Running Shoes", price: 116.50, stock: 20 } ];
let totalValue = products .filter(item => item.stock > 0) .reduce((prev, item) => prev + (item.price * item.stock), 0);
console.log(Total value: $${totalValue.toFixed(2)});
Listing 4-33Processing an Array in the example.js File in the src Folder
我使用`filter`方法选择数组中`stock`值大于零的项目,并使用`reduce`方法确定这些项目的总值,产生以下输出:
Total value: $2864.99
## 使用对象
有几种方法可以在 JavaScript 中创建对象。清单 4-34 给出了一个简单的例子。
let myData = new Object(); myData.name = "Adam"; myData.weather = "sunny";
console.log(Hello ${myData.name}.);
console.log(Today is ${myData.weather}.);
Listing 4-34Creating an Object in the example.js File in the src Folder
我通过调用`new Object()`创建一个对象,并将结果(新创建的对象)赋给一个名为`myData`的变量。一旦创建了对象,我就可以通过赋值来定义对象的属性,就像这样:
...
myData.name = "Adam";
...
在这个语句之前,我的对象没有名为`name`的属性。当语句执行后,该属性确实存在,并被赋予了值`Adam`。您可以通过将变量名和属性名与句点组合来读取属性值,如下所示:
...
console.log(Hello ${myData.name}.);
...
清单的结果如下:
Hello Adam. Today is sunny.
### 使用对象文字
前面的例子产生了一个警告,因为定义对象的标准方法是使用对象文字格式,这也允许在一个步骤中定义属性,如清单 4-35 所示。
let myData = {
name: "Adam",
weather: "sunny"
};
console.log(Hello ${myData.name}.);
console.log(Today is ${myData.weather}.);
Listing 4-35Using the Object Literal Format in the example.js File in the src Folder
使用冒号(`:`)将您要定义的每个属性与其值分开,使用逗号(`,`)将属性分开。效果与前面的示例相同,清单的结果如下:
Hello Adam. Today is sunny.
#### 使用变量作为对象属性
如果使用变量作为对象属性,JavaScript 将使用变量名作为属性名,变量值作为属性值,如清单 4-36 所示。
let name = "Adam"
let myData = { name,
weather: "sunny" };
console.log(Hello ${myData.name}.);
console.log(Today is ${myData.weather}.);
Listing 4-36Using a Variable in an Object Literal in the example.js File in the src Folder
`name`变量用于向`myData`对象添加一个属性,这样该属性从变量`name`中获取,作为其值`Adam`。当您想要将一组数据值组合成一个对象时,这是一种有用的技术,您将在后面章节的示例中看到它的使用。清单 4-37 中的代码产生以下输出:
Hello Adam. Today is sunny.
### 将函数用作方法
我最喜欢 JavaScript 的一个特性是可以向对象添加函数。定义在对象上的函数被称为*方法*。清单 4-37 展示了如何以这种方式添加方法。
let myData = { name: "Adam", weather: "sunny", printMessages: function () {
console.log(`Hello ${myData.name}.`);
console.log(`Today is ${myData.weather}.`);
}
};
myData.printMessages();
Listing 4-37Adding Methods to an Object in the example.js File in the src Folder
在这个例子中,我使用了一个函数来创建一个名为`printMessages`的方法。注意,为了引用对象定义的属性,我必须使用`this`关键字。当一个函数作为一个方法使用时,该函数通过特殊变量`this`被隐式传递给调用该方法的对象作为参数。清单的输出如下所示:
Hello Adam. Today is sunny.
您也可以不使用`function`关键字来定义方法,如清单 4-38 所示。
let myData = { name: "Adam", weather: "sunny", printMessages() {
console.log(`Hello ${myData.name}.`);
console.log(`Today is ${myData.weather}.`);
}
};
myData.printMessages();
Listing 4-38Defining a Method in the example.js File in the src Folder
该清单的输出如下:
Hello Adam. Today is sunny.
粗箭头语法也可以用来定义方法,如清单 4-39 所示。
let myData = { name: "Adam", weather: "sunny", printMessages: () => {
console.log(`Hello ${myData.name}.`);
console.log(`Today is ${myData.weather}.`);
}
};
myData.printMessages();
Listing 4-39Defining a Fat Arrow Method in the example.js File in the src Folder
### 小费
如果您从胖箭头函数返回一个对象文字,那么您必须将该对象括在括号中,例如`myFunc = () => ({ data: "hello"})`。如果省略括号,您将收到一个错误,因为构建工具将假设对象文字的花括号是函数体的开始和结束。
### 使用类
类是对象的模板,定义新实例将拥有的属性和方法。类是 JavaScript 语言的新成员,它们在 React 开发中用于定义具有状态数据的组件,如第十一章所述。在清单 4-40 中,我用一个类替换了对象文字。
class MyData {
constructor() {
this.name = "Adam";
this.weather = "sunny";
}
printMessages = () => {
console.log(`Hello ${this.name}.`);
console.log(`Today is ${this.weather}.`);
}
}
let myData = new MyData();
myData.printMessages();
Listing 4-40Using a Class in the example.js File in the src Folder
使用关键字`class`定义类。`constructor`是一个特殊的方法,当从类中创建一个对象时,这个方法被自动调用,这就是所谓的*实例化类*。从一个类创建的对象被称为该类的一个*实例*。
在 JavaScript 中,构造函数用于定义实例将拥有的属性,当前对象使用关键字`this`引用。清单 4-40 中的构造函数通过给`this.name`和`this.weather`赋值来定义`name`和`weather`属性。类通过给名字分配函数来定义方法,在清单 4-40 中,类定义了一个`printMessages`方法,该方法使用胖箭头语法来定义,并将消息打印到控制台。注意,访问`name`和`weather`变量的值需要`this`关键字。
### 小费
使用 JavaScript 类还有其他方法,但是我主要关注的是它们在 React 开发和本书示例中的使用方式。详见 [`https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes) 。
使用`new`关键字创建该类的一个新实例,一个类可以用来创建多个对象,每个对象都有自己的数据值,这些数据值与其他实例是分开的。在清单中,`new`关键字用于从`MyData`类创建一个对象,然后将该对象赋给一个名为`myData`的变量。调用对象的`printMessages`方法,产生以下输出:
Hello Adam. Today is sunny.
在其他语言和框架中,类用于继承,其中一个类建立在另一个类定义的方法和属性上。React 开发不直接使用类继承,而是使用一种替代方法,称为*组合*,来创建复杂的特性,如第十四章所述。一个例外是使用类定义 React 组件,其中必须使用关键字`extends`来确保类继承组件所需的核心特性。如果您检查`App.js`文件的内容,您会看到组件是使用`class`和`extends`关键字定义的,就像这样:
... 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>
} ...
### 将属性从一个对象复制到另一个对象
React 和我在第三部分中描述的包提供的一些重要特性依赖于将属性从一个对象复制到另一个对象。JavaScript 为此提供了`Object.assign`方法,如清单 4-41 所示。
class MyData {
constructor() {
this.name = "Adam";
this.weather = "sunny";
}
printMessages = () => {
console.log(`Hello ${this.name}.`);
console.log(`Today is ${this.weather}.`);
}
}
let myData = new MyData();
let secondObject = {};
Object.assign(secondObject, myData);
secondObject.printMessages();
Listing 4-41Copying Object Properties in the example.js File in the src Folder
这个示例使用文本形式创建一个没有属性的新对象,并使用`Object.assign`方法从`myData`对象中复制属性及其值。此示例产生以下输出:
Hello Adam. Today is sunny.
析构操作符——与 spread 操作符相同——可用于将属性从一个对象复制到另一个对象,我在后面章节中使用的一种技术是使用析构操作符复制所有现有属性,然后为其中一些属性定义一个新值,如清单 4-42 所示。
class MyData {
constructor() {
this.name = "Adam";
this.weather = "sunny";
}
printMessages = () => {
console.log(`Hello ${this.name}.`);
console.log(`Today is ${this.weather}.`);
}
}
let myData = new MyData();
let secondObject = { ...myData, weather: "cloudy"};
console.log(myData: ${ myData.weather}, secondObject: ${secondObject.weather});
Listing 4-42Copying Using a Spread in the example.js File in the src Folder
该示例从`myData`对象复制属性,并为`weather`属性提供一个新值,产生以下输出:
myData: sunny, secondObject: cloudy
### 从对象中捕获参数名
当一个对象作为函数或方法参数被接收时,浏览属性以获取所需的数据可能会很困难。举个简单的例子,清单 4-43 定义了一个对象结构,通过导航来获取数据值。
const myData = { name: "Bob", location: { city: "Paris", country: "France" }, employment: { title: "Manager", dept: "Sales" } }
function printDetails(data) {
console.log(Name: ${data.name}, City: ${data.location.city}, Role: ${data.employment.title});
}
printDetails(myData);
Listing 4-43Navigating Object Properties in the example.js File in the src Folder
`printDetails`函数必须在对象中导航,以获得它需要的`name`、`city`和`title`属性。通过将特定属性捕获为命名参数,可以更好地实现相同的结果,如清单 4-44 所示。
const myData = { name: "Bob", location: { city: "Paris", country: "France" }, employment: { title: "Manager", dept: "Sales" } }
function printDetails({ name, location: { city }, employment: { title }}) {
console.log(`Name: ${name}, City: ${city}, Role: ${title}`);
}
printDetails(myData);
Listing 4-44Capturing Named Parameters in the example.js File in the src Folder
这个例子应用了清单 4-36 中描述的技术来从对象中选择特定的属性。此清单和清单 4-43 产生相同的输出。
Name: Bob, City: Paris, Role: Manager
## 理解 JavaScript 模块
React 应用太复杂,无法在一个 JavaScript 文件中定义。为了将应用分成更易管理的块,JavaScript 支持*模块*,其中包含应用其他部分所依赖的 JavaScript 代码。在接下来的部分中,我将解释定义和使用模块的不同方式。
### 创建和使用 JavaScript 模块
示例项目中已经有 JavaScript 模块,但是理解它们如何工作的最好方法是创建并使用一个新模块。我在`src`文件夹中添加了一个名为`sum.js`的文件,并添加了清单 4-45 中所示的代码。
export default function(values) { return values.reduce((total, val) => total + val, 0); }
Listing 4-45The Contents of the sum.js File in the src Folder
`sum.js`文件包含一个函数,该函数接受一组值,并使用 JavaScript array `reduce`方法对它们求和并返回结果。这个例子的重要之处不在于它做了什么,而在于函数是在自己的文件中定义的,这是模块的基本构造块。
清单 4-45 中使用了两个你在定义模块时会经常遇到的关键词:`export`和`default`。`export`关键字用于表示模块外部可用的特性。默认情况下,JavaScript 文件的内容是私有的,必须使用`export`关键字显式共享,然后才能在应用的其余部分使用。当模块包含单个特性时,使用`default`关键字,例如清单 4-45 中定义的函数。`export`和`default`关键字一起用于指定`sum.js`文件中的唯一函数可用于应用的其余部分。
#### 使用 JavaScript 模块
使用模块需要另一个关键字:`import`关键字。在清单 4-46 中,我使用了`import`关键字来访问上一节中定义的函数,以便它可以在`example.js`文件中使用。
import additionFunction from "./sum";
let values = [10, 20, 30, 40, 50];
let total = additionFunction(values);
console.log(Total: ${total});
Listing 4-46Using a JavaScript Module in the example.js File in the src Folder
`import`关键字用于声明对模块的依赖。`import`关键字可以有多种不同的用法,但这是你在处理自己创建的模块时最常使用的格式,关键部分如图 4-3 所示。

图 4-3
声明对模块的依赖
`import`关键字后面是一个标识符,它是函数被使用时的名字,在这个例子中标识符是`additionFunction`。
### 小费
请注意,应用标识符的是`import`语句,这意味着使用模块中函数的代码选择它将被识别的名称,并且应用不同部分中同一模块的多个`import`语句可以使用不同的名称来引用同一函数。请参阅下一节,了解模块如何指定它所包含的特性的名称。
`from`关键字跟在标识符后面,然后是模块的位置。密切关注位置很重要,因为不同的位置格式会产生不同的行为,如侧栏中所述。
在构建过程中,React 工具将检测到`import`语句,并将来自`sum.js`文件的函数包含在发送到浏览器的 JavaScript 文件中,以便浏览器可以执行应用。在`import`语句中使用的标识符可以用来访问模块中的函数,就像使用本地定义的函数一样。
... let total = additionFunction(values); ...
如果您检查浏览器的 JavaScript 控制台,您会看到清单 4-42 中的代码使用该模块的函数产生以下结果:
Total: 150
### 了解模块位置
模块的位置改变了构建工具在创建发送到浏览器的 JavaScript 文件时查找模块的方式。对于自己定义的模块,位置指定为相对路径;它以一个或两个句点开始,这表示该路径相对于当前文件或当前文件的父目录。在清单 4-46 中,位置以句点开始。
... import additionFunction from "./sum"; ...
这个位置告诉构建工具存在对`sum`模块的依赖,该模块可以在包含`import`语句的文件所在的文件夹中找到。请注意,文件扩展名不包括在位置中。
如果您省略了初始阶段,那么`import`语句声明了对`node_modules`文件夹中的模块的依赖,该文件夹是项目设置期间安装包的位置。这种位置用于访问第三方包提供的特性,包括 React 包,这就是为什么您会在 React 项目中看到这样的语句:
... import React, { Component } from "react"; ...
这个`import`语句的位置不是以句点开始的,它将被解释为对项目的`node_modules`文件夹中的`react`模块的依赖,该文件夹是提供核心 React 应用特性的包。
### 从模块中导出命名特征
一个模块可以为它输出的特性命名,这是我在本书的大部分例子中采用的方法。在清单 4-47 中,我给由`sum`模块导出的函数起了一个名字。
export function sumValues (values) {
return values.reduce((total, val) => total + val, 0);
}
Listing 4-47Exporting a Named Feature in the sum.js File in the src Folder
该函数提供了相同的特性,但是使用名称`sumValues`导出,不再使用`default`关键字。在清单 4-48 中,我在`example.js`文件中导入了使用新名称的特征。
import { sumValues } from "./sum";
let values = [10, 20, 30, 40, 50];
let total = sumValues(values);
console.log(Total: ${total});
Listing 4-48Importing a Named Feature in the example.js File in the src Folder
要导入的特征的名称在花括号中指定(字符`{`和`}`),并在代码中由该名称使用。一个模块可以导出默认的和命名的特征,如清单 4-49 所示。
export function sumValues (values) { return values.reduce((total, val) => total + val, 0); }
export default function sumOdd(values) {
return sumValues(values.filter((item, index) => index % 2 === 0));
}
Listing 4-49Exporting Named and Default Features in the sum.js File in the src Folder
使用`default`关键字导出新特征。在清单 4-50 中,我从模块中导入了新的特性作为默认导出。
import oddOnly, { sumValues } from "./sum";
let values = [10, 20, 30, 40, 50];
let total = sumValues(values);
let odds = oddOnly(values);
console.log(Total: ${total}, Odd Total: ${odds});
Listing 4-50Importing a Default Feature in the example.js File in the src Folder
这是您将在本书示例的 React 组件开头看到的模式,因为 JSX 所需的核心 React 特性是从`react`模块默认导出的,而`Component`类是一个命名特性:
... import React, { Component } from "react"; ...
清单 4-50 中的示例产生以下输出:
Total: 150, Odd Total: 90
### 在模块中定义多个命名特征
模块可以包含多个命名函数或值,这对于相关功能的分组非常有用。为了演示,我在`src`文件夹中创建了一个名为`operations.js`的文件,并添加了清单 4-51 中所示的代码。
export function multiply(values) { return values.reduce((total, val) => total * val, 1); }
export function subtract(amount, values) { return values.reduce((total, val) => total - val, amount); }
export function divide(first, second) { return first / second; }
Listing 4-51The Contents of the operations.js File in the src Folder
该模块定义了三个应用了关键字`export`的函数。与前面的例子不同,没有使用`default`关键字,每个函数都有自己的名称。当从包含多个特性的模块导入时,所需特性的名称被指定为大括号之间的逗号分隔列表,如清单 4-52 所示。
import oddOnly, { sumValues } from "./sum";
import { multiply, subtract } from "./operations";
let values = [10, 20, 30, 40, 50];
let total = sumValues(values); let odds = oddOnly(values);
console.log(Total: ${total}, Odd Total: ${odds});
console.log(Multiply: ${multiply(values)});
console.log(Subtract: ${subtract(1000, values)});
Listing 4-52Importing Named Features in the example.js File in the src Folder
`import`关键字后面的括号包围了我要使用的函数列表,在本例中是用逗号分隔的`multiply`和`subtract`函数。我只声明对我需要的函数的依赖,对`divide`函数没有依赖,它在模块中定义了,但没有使用。此示例产生以下输出:
Total: 150, Odd Total: 90 Multiply: 12000000 Subtract: 850
#### 更改模块功能名称
从模块中导入命名特性时,您可能会发现有两个模块使用相同的名称,或者模块使用的名称在导入时不会生成可读的代码。您可以使用`as`关键字选择一个新名称,如清单 4-53 所示。
import oddOnly, { sumValues } from "./sum";
import { multiply, subtract as deduct } from "./operations";
let values = [10, 20, 30, 40, 50];
let total = sumValues(values); let odds = oddOnly(values);
console.log(Total: ${total}, Odd Total: ${odds});
console.log(Multiply: ${multiply(values)});
console.log(Subtract: ${deduct(1000, values)});
Listing 4-53Assigning a Name to a Feature in the example.js File in the src Folder
我使用了`as`关键字来指定`subtract`函数在导入到`example.js`文件时应该被命名为`deduct`。此清单产生与清单 4-53 相同的输出。
#### 导入整个模块
列出一个模块中所有函数的名称对于复杂的模块来说是无法控制的。一种更优雅的方法是导入一个模块提供的所有特性,并只使用您需要的特性,如清单 4-54 所示。
import oddOnly, { sumValues } from "./sum";
import * as ops from "./operations";
let values = [10, 20, 30, 40, 50];
let total = sumValues(values); let odds = oddOnly(values);
console.log(Total: ${total}, Odd Total: ${odds});
console.log(Multiply: ${ops.multiply(values)});
console.log(Subtract: ${ops.subtract(1000, values)});
Listing 4-54Importing an Entire Module in the example.js File in the src Folder
星号用于导入模块中的所有内容,后跟关键字`as`和一个标识符,通过它可以访问模块函数和值。在这种情况下,标识符是`ops`,这意味着`multiply`、`subtract`和`divide`功能可以作为`ops.multiply`、`ops.subtract`和`ops.divide`来访问。该清单产生与清单 4-53 相同的输出。
## 理解 JavaScript 承诺
承诺是将在未来某个时间点完成的后台活动。在本书中,承诺最常见的用法是使用 HTTP 请求来请求数据,这是异步执行的,当从 web 服务器收到响应时会产生一个结果。
### 理解异步操作问题
web 应用的经典异步操作是 HTTP 请求,通常用于获取用户需要的数据和内容。我在本书的第三部分解释了如何发出 HTTP 请求,但是本章我需要更简单的东西,所以我用清单 4-55 所示的代码在`src`文件夹中添加了一个名为`async.js`的文件。
import { sumValues } from "./sum";
export function asyncAdd(values) {
setTimeout(() => {
let total = sumValues(values);
console.log(Async Total: ${total});
return total;
}, 500);
}
Listing 4-55The Contents of the async.js File in the src Folder
`setTimeout`函数在指定的延迟后异步调用一个函数。在清单中,`asyncAdd`函数接收一个参数,该参数在 500 毫秒的延迟后被传递给在`sum`模块中定义的`sumValues`函数,为本章中的示例创建一个不会立即完成的后台操作,并作为更有用操作的占位符,比如发出一个 HTTP 请求。在清单 4-56 中,我已经更新了`example.js`文件以使用`asyncAdd`函数。
import { asyncAdd } from "./async";
let values = [10, 20, 30, 40, 50];
let total = asyncAdd(values);
console.log(Main Total: ${total});
Listing 4-56Performing Background Work in the example.js File in the src Folder
这个例子说明的问题是,`asyncAdd`函数的结果直到`example.js`文件中的语句被执行后才产生,这可以在浏览器的 JavaScript 控制台的输出中看到:
Main Total: undefined Async Total: 150
浏览器执行`example.js`文件中的语句,并按照指示调用`asyncAdd`函数。浏览器移动到`example.js`文件中的下一条语句,该语句使用`asyncAdd`提供的结果向控制台写入一条消息——但这发生在异步任务完成之前,这就是为什么输出是`undefined`。异步任务随后完成,但是结果被`example.js`文件使用已经太晚了。
### 使用 JavaScript Promise
为了解决上一节中的问题,我需要一种机制,允许我观察异步任务,以便我可以等待它完成,然后写出结果。这就是 JavaScript `Promise`的作用,我已经将它应用于清单 4-57 中的`asyncAdd`函数。
import { sumValues } from "./sum";
export function asyncAdd(values) { return new Promise(callback =>
setTimeout(() => {
let total = sumValues(values);
console.log(`Async Total: ${total}`);
callback(total);
}, 500));
}
Listing 4-57Using a Promise in the async.js File in the src Folder
在这个例子中很难解开函数。`new`关键字用于创建一个`Promise`,它接受要观察的函数。观察到的函数提供了一个回调,当异步任务完成时调用该回调,并接受任务的结果作为参数。调用`callback`函数被称为*解析承诺*。
已经成为`asyncAdd`函数结果的`Promise`对象允许观察异步任务,以便在任务完成时执行后续工作,如清单 4-58 所示。
import { asyncAdd } from "./async";
let values = [10, 20, 30, 40, 50];
asyncAdd(values).then(total => console.log(Main Total: ${total}));
Listing 4-58Observing a Promise in the example.js File in the src Folder
`then`方法接受一个函数,该函数将在使用回调时被调用。传递给回调的结果被提供给`then`函数。在这种情况下,这意味着在异步任务完成并产生以下输出之前,总数不会写入浏览器的 JavaScript 控制台:
Async Total: 150 Main Total: 150
### 简化异步代码
JavaScript 提供了两个关键字——`async`和`await`——支持异步操作,而不必直接使用承诺。在清单 4-59 中,我在`example.js`文件中应用了这些关键字。
### 警告
理解使用`async` / `await`不会改变应用的行为方式是很重要的。操作仍然是异步执行的,直到操作完成,结果才可用。这些关键字只是为了简化异步代码的工作,这样你就不必使用`then`方法了。
import { asyncAdd } from "./async";
let values = [10, 20, 30, 40, 50];
async function doTask() {
let total = await asyncAdd(values);
console.log(`Main Total: ${total}`);
}
doTask();
Listing 4-59Using async and await in the example.js File in the src Folder
这些关键字只能应用于函数,这就是为什么我在清单中添加了`doTask`函数。`async`关键字告诉 JavaScript 这个函数依赖于需要承诺的功能。当调用一个返回`Promise`的函数时,使用`await`关键字,其作用是将提供的结果分配给`Promise`对象的回调,然后执行后面的语句,产生以下结果:
Async Total: 150 Main Total: 150
## 摘要
在这一章中,我提供了一个关于 JavaScript 的简单入门,重点是让你开始 React 开发的核心功能。在下一章中,我将开始构建一个更加复杂和现实的项目,名为 SportsStore。
# 五、SportsStore:一个真正的应用
在第二章中,我构建了一个快速简单的 React 应用。小而集中的例子允许我展示具体的特征,但是它们可能缺乏上下文。为了帮助克服这个问题,我将创建一个简单但现实的电子商务应用。
我的应用名为 SportsStore,将遵循各地在线商店采用的经典方法。我将创建一个客户可以按类别和页面浏览的在线产品目录,一个用户可以添加和删除产品的购物车,以及一个客户可以输入详细信息并下订单的收银台。我还将创建一个管理区域,其中包括用于管理产品和订单的创建、读取、更新和删除(CRUD)工具——我将保护它,以便只有登录的管理员才能进行更改。最后,我将向您展示如何为部署准备 React 应用。
我在这一章和后面几章的目标是通过创建尽可能真实的例子,让你对真正的 React 开发有所了解。当然,我想把重点放在 React 和大多数项目中使用的相关包上,所以我简化了与外部系统的集成,比如数据库,并完全省略了其他部分,比如支付处理。
我在所有的书中都使用了 SportsStore 的例子,尤其是因为它展示了使用不同的框架、语言和开发风格来实现相同结果的方法。你不需要阅读我的任何其他书籍来理解这一章,但是如果你已经拥有我的*Pro ASP.NET 核心 MVC 2* 或 *Pro Angular 6* 书籍,你会发现这些对比很有趣。
我在 SportsStore 应用中使用的 React 特性将在后面的章节中详细介绍。我不会在这里重复所有的内容,我告诉您的内容足以让您理解示例应用,并让您参考其他章节以获得更深入的信息。您可以从头到尾阅读 SportsStore 章节,了解 React 的工作原理,或者在详细章节之间跳转,深入了解。
无论哪种方式,都不要指望马上理解所有的东西——React 应用有许多活动的部分,并依赖于许多软件包,而 SportsStore 应用旨在向您展示它们是如何组合在一起的,而不会深入到本书其余部分描述的细节中。
### 小费
你可以从 [`https://github.com/Apress/pro-react-16`](https://github.com/Apress/pro-react-16) 下载本章以及本书其他章节的示例项目。
## 准备项目
要创建项目,打开一个新的命令提示符,导航到一个方便的位置,并运行清单 5-1 中所示的命令。
```jsx
npx create-react-app sportsstore
Listing 5-1Creating the SportsStore Project
create-react-app工具将创建一个名为sportsstore的新 React 项目,其中包含开始开发所需的包、配置文件和占位符内容。项目设置过程可能需要一些时间来完成,因为有大量的 NPM 软件包需要下载和安装。
注意
创建新项目时,您可能会看到有关安全漏洞的警告。React 开发依赖于大量的包,每个包都有自己的依赖关系,不可避免的会发现安全问题。对于本书中的示例,使用指定的包版本以确保获得预期的结果是很重要的。对于您自己的项目,您应该查看警告并更新到解决问题的版本。
安装附加的 NPM 软件包
除了由create-react-app包安装的核心 React 库和开发工具之外,SportsStore 项目还需要额外的包。运行清单 5-2 中所示的命令,导航到sportsstore文件夹并添加包。(npm install命令可以用来一次添加多个包,但是结果是一个很长的命令,很容易忽略一个包。为了避免错误,我在本书中单独添加了包。)
注意
使用清单中显示的版本号很重要。在添加包时,您可能会看到关于未满足对等依赖关系的警告,但是这些可以忽略。
cd sportsstore
npm install bootstrap@4.1.2
npm install @fortawesome/fontawesome-free@5.6.1
npm install redux@4.0.1
npm install react-redux@6.0.0
npm install react-router-dom@4.3.1
npm install axios@0.18.0
npm install graphql@14.0.2
npm install apollo-boost@0.1.22
npm install react-apollo@2.3.2
Listing 5-2Installing Additional Packages
不要被所需的额外软件包的数量所吓倒。React 专注于 web 应用所需的一组核心功能,并依赖支持包来创建完整的应用。为了提供一些背景知识,在列表 5-2 中添加的包在表 5-1 中有描述,我将在本书的第三部分深入讨论它们。
表 5-1
SportsStore 项目所需的包
|名字
|
描述
|
| --- | --- |
| bootstrap | 这个包提供了我在整本书中用来呈现 HTML 内容的 CSS 样式。 |
| fontawesome-free | 这个包提供了可以包含在 HTML 内容中的图标。我用过免费软件,但也有更全面的付费选项。 |
| redux | 这个包提供了一个数据存储,它简化了协调应用不同部分的过程。详见第十九章。 |
| react-redux | 这个包将 Redux 数据存储集成到 React 应用中,如第 19 和 20 章所述。 |
| react-router-dom | 这个包提供 URL 路由,允许根据浏览器的当前 URL 选择呈现给用户的内容,如第 21 和 22 章所述。 |
| axios | 这个包用于发出 HTTP 请求,并将用于访问 RESTful 和 GraphQL 服务,如第 23–25 章所述。 |
| graphql | 这个包包含 GraphQL 规范的参考实现。 |
| apollo-boost | 这个包包含一个用于消费 GraphQL 服务的客户端,如第二十五章所述。 |
| react-apollo | 这个包用于将 GraphQL 客户端集成到 React 应用中,如第二十五章所述。 |
需要更多的包来创建 SportsStore 应用将使用的后端服务。使用命令提示符,运行sportsstore文件夹中清单 5-3 所示的命令。这些包是使用--save-dev参数安装的,这表明它们是在开发过程中使用的,在部署时不会成为 SportsStore 应用的一部分。
npm install --save-dev json-server@0.14.2
npm install --save-dev jsonwebtoken@8.1.1
npm install --save-dev express@4.16.4
npm install --save-dev express-graphql@0.7.1
npm install --save-dev cors@2.8.5
npm install --save-dev faker@4.1.0
npm install --save-dev chokidar@2.0.4
npm install --save-dev npm-run-all@4.1.3
npm install --save-dev connect-history-api-fallback@1.5.0
Listing 5-3Adding Further Packages
对于使用来自现有服务的数据的应用,您不需要这些包,但是我需要为 SportsStore 应用创建一个完整的基础设施。表 5-2 简要描述了清单 5-3 中安装的每个包的用途。
表 5-2
SportsStore 项目所需的附加包
|名字
|
描述
|
| --- | --- |
| json-server | 这个包将用于在第六章中提供一个 RESTful web 服务。 |
| jsonwebtoken | 该包将用于在第八章中验证用户。 |
| graphql | 这个包将用于在第七章中定义 GraphQL 服务器的模式。 |
| express | 这个包将用于托管后端服务器。 |
| express-graphql | 这个包将用于创建一个 GraphQL 服务器。 |
| cors | 此包用于启用跨来源请求共享(CORS)请求。 |
| faker | 该包生成用于测试的假数据,并在第六章中使用。 |
| chokidar | 这个包监控文件的变化。 |
| npm-run-all | 这个包用于在一个命令中运行多个 NPM 脚本。 |
| connect-history-api-fallback | 该包用于响应带有index.html文件的 HTTP 请求,并用于第八章中的生产服务器。 |
将 CSS 样式表添加到项目中
为了使用 Bootstrap 和 Font Awesome 包,我需要将import语句添加到应用的index.js文件中。index.js文件的目的是启动应用,如第九章所述,添加清单 5-4 中所示的导入语句确保我需要的样式可以应用到 SportsStore 应用显示的 HTML 内容。
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";
import "@fortawesome/fontawesome-free/css/all.min.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 5-4Adding CSS Stylesheets in the index.js File in the src Folder
准备 Web 服务
一旦应用的基本结构就绪,我将添加对从 web 服务消费数据的支持。在准备过程中,我在sportsstore文件夹中添加了一个名为data.js的文件,其内容如清单 5-5 所示。
module.exports = function () {
return {
categories: ["Watersports", "Soccer", "Chess"],
products: [
{ id: 1, name: "Kayak", category: "Watersports",
description: "A boat for one person", price: 275 },
{ id: 2, name: "Lifejacket", category: "Watersports",
description: "Protective and fashionable", price: 48.95 },
{ id: 3, name: "Soccer Ball", category: "Soccer",
description: "FIFA-approved size and weight", price: 19.50 },
{ id: 4, name: "Corner Flags", category: "Soccer",
description: "Give your playing field a professional touch",
price: 34.95 },
{ id: 5, name: "Stadium", category: "Soccer",
description: "Flat-packed 35,000-seat stadium", price: 79500 },
{ id: 6, name: "Thinking Cap", category: "Chess",
description: "Improve brain efficiency by 75%", price: 16 },
{ id: 7, name: "Unsteady Chair", category: "Chess",
description: "Secretly give your opponent a disadvantage",
price: 29.95 },
{ id: 8, name: "Human Chess Board", category: "Chess",
description: "A fun game for the family", price: 75 },
{ id: 9, name: "Bling Bling King", category: "Chess",
description: "Gold-plated, diamond-studded King", price: 1200 }
],
orders: []
}
}
Listing 5-5The Contents of the data.js File in the sportsstore Folder
清单 5-5 中的代码创建了应用将使用的三个数据集合。products集合包含销售给客户的产品,categories集合包含产品被组织成的类别集,orders集合包含客户已经下的订单(但是当前是空的)。
我用清单 5-6 中所示的代码在sportsstore文件夹中添加了一个名为server.js的文件。这是创建为应用提供数据的 web 服务的代码。在后面的章节中,我将为后端服务器添加一些特性,比如认证和对 GraphQL 的支持。
const express = require("express");
const jsonServer = require("json-server");
const chokidar = require("chokidar");
const cors = require("cors");
const fileName = process.argv[2] || "./data.js"
const port = process.argv[3] || 3500;
let router = undefined;
const app = express();
const createServer = () => {
delete require.cache[require.resolve(fileName)];
setTimeout(() => {
router = jsonServer.router(fileName.endsWith(".js")
? require(fileName)() : fileName);
}, 100)
}
createServer();
app.use(cors());
app.use(jsonServer.bodyParser)
app.use("/api", (req, resp, next) => router(req, resp, next));
chokidar.watch(fileName).on("change", () => {
console.log("Reloading web service data...");
createServer();
console.log("Reloading web service data complete.");
});
app.listen(port, () => console.log(`Web service running on port ${port}`));
Listing 5-6The Contents of the server.js File in the sportsstore Folder
为了确保 web 服务与 React 开发工具一起启动,我修改了package.json文件的scripts部分,如清单 5-7 所示。
...
"scripts": {
"start": "npm-run-all --parallel reactstart webservice",
"reactstart": "react-scripts start",
"webservice": "node server.js",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
...
Listing 5-7Enabling the Web Service in the package.json File in the sportsstore Folder
这个变化使用npm-run-all包来同时运行 React 开发服务器和 web 服务。
运行示例应用
要启动应用和 web 服务,使用命令提示符运行清单 5-8 中的命令。
npm start
Listing 5-8Starting the Application
初始编译完成后会有一个暂停,然后会打开一个新的浏览器窗口,显示如图 5-1 所示的占位符内容。
图 5-1
运行示例应用
为了确保 web 服务正在运行,打开一个新的浏览器窗口并请求 URL http://localhost:3500/api/products/1。浏览器将显示清单 5-5 中定义的产品之一的 JSON 表示,如下所示:
{ "id":1, "name":"Kayak", "category":"Watersports",
"description":"A boat for one person","price":275 }
创建数据存储
SportsStore 的起点是数据存储,它将是呈现给用户的数据以及协调应用功能(如分页)所需的支持细节的存储库。
我将从使用本地占位符数据的数据存储开始。稍后,我将添加对从 web 服务获取数据的支持,但是静态数据是一个很好的起点,因为它将焦点放在 React 应用上。将使用 Redux 包创建 SportsStore 数据存储,这是 React 项目中最流行的数据存储,我在第 19 和 20 章中对此进行了描述。首先,我创建了src/data文件夹,并在其中添加了一个名为placeholderData.js的文件,其内容如清单 5-9 所示。
export const data = {
categories: ["Watersports", "Soccer", "Chess", "Running"],
products: [
{ id: 1, name: "P1", category: "Watersports",
description: "P1 (Watersports)", price: 3 },
{ id: 2, name: "P2", category: "Watersports",
description: "P2 (Watersports)", price: 4 },
{ id: 3, name: "P3", category: "Running",
description: "P3 (Running)", price: 5 },
{ id: 4, name: "P4", category: "Chess",
description: "P4 (Chess)", price: 6 },
{ id: 5, name: "P5", category: "Chess",
description: "P6 (Chess)", price: 7 },
]
}
Listing 5-9The Contents of the placeholderData.js File in the src/data Folder
创建数据存储操作和操作创建者
Redux 数据将读取数据与更改数据的操作分开存储。这一开始可能会感觉很尴尬,但它类似于 React 开发的其他部分,如组件状态数据和使用 GraphQL,并且它很快成为第二天性。
动作是发送到数据存储以对其包含的数据进行更改的对象。动作有类型,动作对象是使用动作创建器创建的。目前我需要的唯一动作是将数据加载到存储中,最初使用清单 5-9 中定义的占位符数据,但最终来自 web 服务。有不同的方法可以构造数据存储的操作,但是有必要确定不同类型的数据之间共享的共同主题,以避免以后的代码重复。我在src/data文件夹中添加了一个名为Types.js的文件,并使用它来列出存储中的数据类型以及可以对它们执行的一组操作,如清单 5-10 所示。
export const DataTypes = {
PRODUCTS: "products",
CATEGORIES: "categories"
}
export const ActionTypes = {
DATA_LOAD: "data_load"
}
Listing 5-10The Contents of the Types.js File in the src/data Folder
有两种数据类型——PRODUCTS和CATEGORIES——以及一个动作DATA_LOAD,它将填充数据存储。不要求以这种方式定义动作类型,但是在应用的其他地方指定动作类型时,使用常量值可以避免输入错误。
接下来,我需要定义一个动作创建器函数,它将创建一个动作对象,该对象可以被数据存储处理以改变它包含的数据。我在src/data文件夹中添加了一个名为ActionCreators.js的文件,代码如清单 5-11 所示。
import { ActionTypes} from "./Types";
import { data as phData} from "./placeholderData";
export const loadData = (dataType) => ({
type: ActionTypes.DATA_LOAD,
payload: {
dataType: dataType,
data: phData[dataType]
}
});
Listing 5-11The Contents of the ActionCreators.js File in the src/data Folder
在第十九章中描述了动作创建器的使用,但是对动作创建器产生的对象的唯一要求是它们必须有一个type属性,该属性的值指定了数据存储所需的变化类型。在动作对象中使用一组公共属性是一个好主意,这样它们可以被一致地处理,在清单 5-11 中定义的动作创建器返回一个具有payload属性的动作对象,这是我将用于所有 SportsStore 数据存储动作的约定。
清单 5-11 中动作对象的payload属性有一个dataType属性,指示动作所涉及的数据类型,还有一个data属性,提供要添加到数据存储中的数据。此时,data属性的值是从占位符数据中获得的,但是在第六章中,我用从 web 服务中获得的数据替换它。
动作由数据存储库还原器处理,还原器是接收数据存储库和动作对象的当前内容并使用它们进行更改的功能。我在src/data文件夹中添加了一个名为ShopReducer.js的文件,并定义了清单 5-12 中所示的减速器。
import { ActionTypes } from "./Types";
export const ShopReducer = (storeData, action) => {
switch(action.type) {
case ActionTypes.DATA_LOAD:
return {
...storeData,
[action.payload.dataType]: action.payload.data
};
default:
return storeData || {};
}
}
Listing 5-12The Contents of the ShopReducer.js File in the src/data Folder
Reducers 需要创建并返回包含任何所需更改的新对象。如果动作类型没有被识别,reducer 必须返回它接收到的数据存储对象。清单 5-12 中的 reducer 通过创建一个新对象来处理DATA_LOAD动作,这个新对象包含旧存储的所有属性以及动作中接收到的新数据。第十九章对减速器进行了更详细的描述。
作为创建数据存储的最后一步,我在src/data文件夹中添加了一个名为DataStore.js的文件,并添加了清单 5-13 中所示的代码。
import { createStore } from "redux";
import { ShopReducer } from "./ShopReducer";
export const SportsStoreDataStore = createStore(ShopReducer);
Listing 5-13The Contents of the DataStore.js File in the src/data Folder
Redux 包提供了createStore函数,它使用一个 reducer 建立一个新的数据存储。这足以创建一个数据存储,但是我将在以后添加额外的特性,以便可以执行进一步的操作,并且可以从 web 服务加载数据。
创建购物功能
用户将看到的应用的第一部分是店面,它将以两列布局呈现可用的产品,允许按类别进行过滤,如图 5-2 所示。
图 5-2
应用的基本结构
我将构建应用,以便浏览器的 URL 用于选择呈现给用户的内容。首先,该应用将支持表 5-3 中描述的 URL,这将允许用户查看待售产品并按类别过滤它们。
表 5-3
SportsStore 网址
|名字
|
描述
|
| --- | --- |
| /shop/products | 这个 URL 将向用户显示所有的产品,不考虑类别。 |
| /shop/products/chess | 该 URL 将显示特定类别的产品。在这种情况下,URL 将选择象棋类别。 |
注意
对于应用中向客户提供销售产品的部分,我采用了英国术语 shop 。我想避免在保存应用数据的数据存储和用户购物的产品存储之间产生混淆。
在应用中响应浏览器的 URL 被称为 URL 路由,它由清单 5-2 中添加的 React Router 包提供,在章节 21 和 22 中有详细描述。
创建产品和类别组件
组件是 React 应用的构建块,负责呈现给用户的内容。我创建了src/shop文件夹,并在其中添加了一个名为ProductList.js的文件,其内容如清单 5-14 所示。
import React, { Component } from "react";
export class ProductList extends Component {
render() {
if (this.props.products == null || this.props.products.length === 0) {
return <h5 className="p-2">No Products</h5>
}
return this.props.products.map(p =>
<div className="card m-1 p-1 bg-light" key={ p.id }>
<h4>
{ p.name }
<span className="badge badge-pill badge-primary float-right">
${ p.price.toFixed(2) }
</span>
</h4>
<div className="card-text bg-white p-1">
{ p.description }
</div>
</div>
)
}
}
Listing 5-14The Contents of the ProductList.js File in the src/shop Folder
创建组件是为了执行小任务或显示少量内容,而组合组件是为了创建更复杂的功能。清单 5-14 中定义的ProductList组件负责显示产品列表的细节,其细节通过名为product的属性接收。Props 用于配置组件并允许它们完成工作——例如显示产品的细节——而不涉及数据的来源。ProductList组件生成包含每个产品的name、price和description属性值的 HTML 内容,但是它不知道这些产品是如何在应用中定义的,也不知道它们是在本地定义的还是从远程服务器检索的。
接下来,我在src/shop文件夹中添加了一个名为CategoryNavigation.js的文件,并定义了清单 5-15 中所示的组件。
import React, { Component } from "react";
import { Link } from "react-router-dom";
export class CategoryNavigation extends Component {
render() {
return <React.Fragment>
<Link className="btn btn-secondary btn-block"
to={ this.props.baseUrl }>All</Link>
{ this.props.categories && this.props.categories.map(cat =>
<Link className="btn btn-secondary btn-block" key={ cat }
to={ `${this.props.baseUrl}/${cat.toLowerCase()}`}>
{ cat }
</Link>
)}
</React.Fragment>
}
}
Listing 5-15The Contents of the CategoryNavigation.js File in the src/shop Folder
类别的选择将通过导航到一个新的 URL 来处理,这是使用 React Router 包提供的Link组件来完成的。当用户点击一个Link时,浏览器被要求导航到一个新的 URL,而无需发送任何 HTTP 请求或重新加载应用。新 URL 中包含的细节,比如本例中的选定类别,允许应用的不同部分协同工作。
CategoryNavigation组件通过一个名为categories的属性接收类别数组。该组件检查以确保数组已被定义,并使用map方法为每个数组项生成内容。React 要求将一个key prop 应用到由map方法生成的元素上,以便可以有效地处理数组的变化,如第十章所述。结果是数组中接收的每个类别都有一个Link组件,还有一个额外的Link,这样用户就可以选择所有产品,而不考虑类别。Link组件被设计成按钮,浏览器将导航到的 URL 是一个名为baseUrl的属性和类别名称的组合。
为了将产品表和类别按钮放在一起,我在src/shop文件夹中添加了一个名为Shop.js的文件,并添加了清单 5-16 中所示的代码。
import React, { Component } from "react";
import { CategoryNavigation } from "./CategoryNavigation";
import { ProductList } from "./ProductList";
export class Shop extends Component {
render() {
return <div className="container-fluid">
<div className="row">
<div className="col bg-dark text-white">
<div className="navbar-brand">SPORTS STORE</div>
</div>
</div>
<div className="row">
<div className="col-3 p-2">
<CategoryNavigation baseUrl="/shop/products"
categories={ this.props.categories } />
</div>
<div className="col-9 p-2">
<ProductList products={ this.props.products } />
</div>
</div>
</div>
}
}
Listing 5-16The Contents of the Shop.js File in the src/shop Folder
组件可以将其部分内容的责任委托给其他组件。在它的render方法中,清单 5-16 中定义的Shop组件包含使用引导 CSS 类建立网格结构的 HTML 元素,但是将填充一些网格单元的责任委托给了CategoryNavigation和ProductList组件。这些委托的组件在render方法中被表示为定制的 HTML 元素,其中元素标记与组件的名称相匹配,如下所示:
...
<ProductList products={ this.props.products } />
...
在两个组件之间创建了一个关系:Shop组件是ProductList的父组件,而ProductList组件是Shop的子组件。父组件通过提供属性来配置它们的子组件,在清单 5-16 中,Shop组件将从其父组件收到的products属性传递给它的ProductList子组件,后者将用于向用户显示产品列表。本书的第二部分描述了组件之间的关系以及使用它们创建复杂特征的方式。
连接到数据存储和 URL 路由
组件Shop及其CategoryNavigation和ProductList子组件需要访问数据存储。为了将这些组件与它们需要的特性联系起来,我在src/shop文件夹中添加了一个名为ShopConnector.js的文件,代码如清单 5-17 所示。
import React, { Component } from "react";
import { Switch, Route, Redirect }
from "react-router-dom"
import { connect } from "react-redux";
import { loadData } from "../data/ActionCreators";
import { DataTypes } from "../data/Types";
import { Shop } from "./Shop";
const mapStateToProps = (dataStore) => ({
...dataStore
})
const mapDispatchToProps = {
loadData
}
const filterProducts = (products = [], category) =>
(!category || category === "All")
? products
: products.filter(p => p.category.toLowerCase() === category.toLowerCase());
export const ShopConnector = connect(mapStateToProps, mapDispatchToProps)(
class extends Component {
render() {
return <Switch>
<Route path="/shop/products/:category?"
render={ (routeProps) =>
<Shop { ...this.props } { ...routeProps }
products={ filterProducts(this.props.products,
routeProps.match.params.category) } />} />
<Redirect to="/shop/products" />
</Switch>
}
componentDidMount() {
this.props.loadData(DataTypes.CATEGORIES);
this.props.loadData(DataTypes.PRODUCTS);
}
}
)
Listing 5-17The Contents of the ShopConnector.js File in the src/shop Folder
如果清单 5-17 中的代码目前看起来难以理解,也不用担心。代码比早期的清单更复杂,因为这个组件集合并合并了几个特性,所以它们可以更容易地在项目的其他地方使用,如图 5-3 所示。
图 5-3
将应用连接到其服务
这种方法的优点是,它简化了添加功能或对应用进行更改,因为向用户呈现内容的组件通过 props 接收数据,而不需要直接从数据存储或 URL 路由系统获取数据。缺点是,将应用的其余部分连接到其服务的组件可能很难编写和维护,因为它必须组合不同包的功能并将其呈现给其子代。这个组件的复杂性将会增加,直到第六章结束,那时我将围绕最后一组 SportsStore 购物功能整合代码。
清单 5-17 中的组件将 Redux 数据存储和 URL 路由连接到Shop组件。Redux 包提供了connect函数,用于将一个组件链接到一个数据存储,这样它的属性要么是来自数据存储的值,要么是在被调用时调度数据存储动作的函数,如第二十章所述。正是connect函数产生了清单 5-17 中的大部分代码,因为它需要数据存储和组件属性之间的映射,这可能会很冗长。清单 5-17 中的映射让Shop组件可以访问数据存储中定义的所有属性,数据存储目前包含产品和类别数据,但以后会包含其他特性。
小费
您可以在映射到 props 的数据存储属性中更加具体,如第二十章所演示的,但是我已经映射了所有的产品,这在您开始开发新项目时是一个有用的方法,因为这意味着您不必在每次增强数据存储时都记得映射新的属性。
必须使用选定的类别来过滤产品数据,该类别可通过 React 路由包提供的功能来访问。一个Route用于选择当浏览器导航到一个特定的 URL 时将向用户显示的组件。列表 5-17 中的Route匹配表 5-3 中的 URL,如下所示:
...
<Route path="/shop/products/:category?" render={ (routeProps) =>
...
path prop 告诉Route等待,直到浏览器导航到/shop/products URL。如果 URL 中有一个额外的段,比如/shop/products/running,那么该段的内容将被分配给一个名为category的参数,这就是用户的类别选择将如何被确定。
当浏览器导航到与path属性匹配的 URL 时,Route会显示由render属性指定的内容,如下所示:
...
<Route path="/shop/products/:category?" render={ (routeProps) =>
<Shop { ...this.props } { ...routeProps }
products={ filterProducts(this.props.products,
routeProps.match.params.category) } />} />
...
这是数据存储和 URL 路由功能的结合点。Shop组件需要知道用户选择了哪个类别,这可以通过传递给Route组件的 render prop 的参数获得。类别与来自数据存储的数据相结合,两者都被传递给Shop组件。props 应用于组件的顺序允许覆盖 props,我依赖它用来自filterProduct函数的结果替换从数据存储中获得的products数据,该函数只选择用户选择的类别中的产品。
Route与Switch和Redirect组件结合使用,这两个组件都是 React 路由包的一部分,如果浏览器的当前 URL 与Route不匹配,它们会将浏览器重定向到/shop/products。
ShopConnector组件使用componentDidMount方法将数据加载到数据存储中。componentDidMount方法是 React 组件生命周期的一部分,在第十三章中有详细描述。
将商店添加到应用中
在清单 5-18 中,我已经设置了数据存储和 URL 路由特性,并将ShopConnector组件合并到应用中。
import React, { Component } from "react";
import { SportsStoreDataStore } from "./data/DataStore";
import { Provider } from "react-redux";
import { BrowserRouter as Router, Route, Switch, Redirect }
from "react-router-dom";
import { ShopConnector } from "./shop/ShopConnector";
export default class App extends Component {
render() {
return <Provider store={ SportsStoreDataStore }>
<Router>
<Switch>
<Route path="/shop" component={ ShopConnector } />
<Redirect to="/shop" />
</Switch>
</Router>
</Provider>
}
}
Listing 5-18Adding Routing and a Data Store to the App.js File in the src Folder
使用Provider将数据存储应用于应用,其中store属性被分配给在清单 5-13 中创建的数据存储。使用Router组件将 URL 路由特性应用于应用,我已经使用Switch、Route和Redirect组件对其进行了补充。Redirect将导航到/shop URL,该 URL 匹配Route的path属性并显示ShopConnector组件,产生如图 5-4 所示的结果。单击一个类别按钮会将浏览器重定向到一个新的 URL,比如/shop/products/watersports,它具有过滤所显示产品的作用。
图 5-4
创建基本购物功能
改进类别选择按钮
类别选择按钮可以工作,但是不能清楚地向用户反映当前的类别。为了解决这个问题,我在src文件夹中添加了一个名为ToggleLink.js的文件,并用它来定义清单 5-19 中所示的组件。
小费
我将这个组件添加到src文件夹中,因为一旦商店完成,我将在应用的其他部分使用它。关于如何组织 React 项目,没有严格的规则,但是我倾向于将相关的文件放在文件夹中。
import React, { Component } from "react";
import { Route, Link } from "react-router-dom";
export class ToggleLink extends Component {
render() {
return <Route path={ this.props.to } exact={ this.props.exact }
children={ routeProps => {
const baseClasses = this.props.className || "m-2 btn btn-block";
const activeClass = this.props.activeClass || "btn-primary";
const inActiveClass = this.props.inActiveClass || "btn-secondary"
const combinedClasses =
`${baseClasses} ${routeProps.match ? activeClass : inActiveClass}`
return <Link to={ this.props.to } className={ combinedClasses }>
{ this.props.children }
</Link>
}} />
}
}
Listing 5-19The Contents of the ToggleLink.js File in the src Folder
React Router 包提供了一个组件来指示特定的 URL 何时匹配,但是它不能很好地与引导 CSS 类一起工作,正如我在第二十二章中描述的,在那里我详细解释了ToggleLink组件是如何工作的。对于本章来说,知道可以使用Route组件来提供对 URL 路由系统的访问,以便获得关于当前路由的细节就足够了。在清单 5-20 中,我已经更新了CategoryNavigation组件来使用ToggleLink组件。
import React, { Component } from "react";
//import { Link } from "react-router-dom";
import { ToggleLink } from "../ToggleLink";
export class CategoryNavigation extends Component {
render() {
return <React.Fragment>
<ToggleLink to={ this.props.baseUrl } exact={ true }>All</ToggleLink>
{ this.props.categories && this.props.categories.map(cat =>
<ToggleLink key={ cat }
to={ `${this.props.baseUrl}/${cat.toLowerCase()}`}>
{ cat }
</ToggleLink>
)}
</React.Fragment>
}
}
Listing 5-20Using ToggleLinks in the CategoryNavigation.js File in the src/shop Folder
其作用是清楚地表明选择了哪个类别,如图 5-5 所示。
图 5-5
突出显示选定的组件
添加购物车
购物车将允许用户在结帐前选择几个产品。在接下来的小节中,我将添加扩展数据存储以跟踪用户的产品选择,并创建提供详细和概要购物车视图的组件。
扩展数据存储
为了扩展数据存储以添加对跟踪用户产品选择的支持,我添加了清单 5-21 中所示的动作类型。
export const DataTypes = {
PRODUCTS: "products",
CATEGORIES: "categories"
}
export const ActionTypes = {
DATA_LOAD: "data_load",
CART_ADD: "cart_add",
CART_UPDATE: "cart_update",
CART_REMOVE: "cart_delete",
CART_CLEAR: "cart_clear"
}
Listing 5-21Defining Action Types in the Types.js File in the src/data Folder
新的操作将允许在购物车中添加和删除产品,以及清除整个购物车的内容。
您可以在同一个文件中为应用的不同部分定义动作创建者和减少者,但是将它们分成单独的文件可以使开发更容易,尤其是在大型项目中。我在src/data文件夹中添加了一个名为CartActionCreators.js的文件,并用它来定义新动作类型的动作创建者,如清单 5-22 所示。
import { ActionTypes} from "./Types";
export const addToCart = (product, quantity) => ({
type: ActionTypes.CART_ADD,
payload: {
product,
quantity: quantity || 1
}
});
export const updateCartQuantity = (product, quantity) => ({
type: ActionTypes.CART_UPDATE,
payload: { product, quantity }
})
export const removeFromCart = (product) => ({
type: ActionTypes.CART_REMOVE,
payload: product
})
export const clearCart = () => ({
type: ActionTypes.CART_CLEAR
})
Listing 5-22The Contents of the CartActionCreators.js File in the src/data Folder
清单 5-22 中的函数创建的动作对象有一个payload属性,携带执行动作所需的数据。为了定义一个将处理购物车相关动作的 reducer,我在src/data文件夹中添加了一个名为CartReducer.js的文件,并定义了清单 5-23 中所示的函数。
import { ActionTypes } from "./Types";
export const CartReducer = (storeData, action) => {
let newStore = { cart: [], cartItems: 0, cartPrice: 0, ...storeData }
switch(action.type) {
case ActionTypes.CART_ADD:
const p = action.payload.product;
const q = action.payload.quantity;
let existing = newStore.cart.find(item => item.product.id === p.id);
if (existing) {
existing.quantity += q;
} else {
newStore.cart = [...newStore.cart, action.payload];
}
newStore.cartItems += q;
newStore.cartPrice += p.price * q;
return newStore;
case ActionTypes.CART_UPDATE:
newStore.cart = newStore.cart.map(item => {
if (item.product.id === action.payload.product.id) {
const diff = action.payload.quantity - item.quantity;
newStore.cartItems += diff;
newStore.cartPrice+= (item.product.price * diff);
return action.payload;
} else {
return item;
}
});
return newStore;
case ActionTypes.CART_REMOVE:
let selection = newStore.cart.find(item =>
item.product.id === action.payload.id);
newStore.cartItems -= selection.quantity;
newStore.cartPrice -= selection.quantity * selection.product.price;
newStore.cart = newStore.cart.filter(item => item !== selection );
return newStore;
case ActionTypes.CART_CLEAR:
return { ...storeData, cart: [], cartItems: 0, cartPrice: 0}
default:
return storeData || {};
}
}
Listing 5-23The Contents of the CartReducer.js File in the src/data Folder
购物车动作的 reducer 通过向数据存储添加一个cart属性并为其分配一个具有product和quantity属性的对象数组来跟踪用户的产品选择。还有cartItems和cartPrice属性记录购物车中的商品数量及其总价。
小费
保持数据存储的结构扁平很重要,因为对象层次结构中的深层变化不会被检测到,也不会显示给用户。正是由于这个原因,cart、cartItems和cartPrice属性在数据存储中与products和categories属性一起定义,而不是组合在一个结构中。
默认情况下,Redux 数据存储只使用一个 reducer,但是很容易组合多个 reducer 来适应您的项目。如第十九章所述,有在多个缩减器之间划分数据存储责任的内置支持,但是这分割了数据,所以每个缩减器只能看到模型的一部分。对于 SportsStore 应用,我希望每个 reducer 都能访问完整的数据存储,所以我在src/data文件夹中添加了一个名为CommonReducer.js的文件,并用它来定义清单 5-24 中所示的函数。
export const CommonReducer = (...reducers) => (storeData, action) => {
for (let i = 0; i < reducers.length; i++ ) {
let newStore = reducersi;
if (newStore !== storeData) {
return newStore;
}
}
return storeData;
}
Listing 5-24The Contents of the CommonReducer.js File in the src/data Folder
commonReducer函数将多个 reducerss 组合成一个函数,并要求每个 reducer 处理动作。Reducers 在修改数据存储的内容时会返回新的对象,这使得在处理动作时很容易检测到。结果是,SportsStore 数据存储可以支持多个 reducerss,其中第一个更改数据存储的 reducer 被视为已经处理了操作。在清单 5-25 中,我已经更新了数据存储配置,以使用commonReducer函数来组合商店和手推车减速器。
import { createStore } from "redux";
import { ShopReducer } from "./ShopReducer";
import { CartReducer } from "./CartReducer";
import { CommonReducer } from "./CommonReducer";
export const SportsStoreDataStore
= createStore(CommonReducer(ShopReducer, CartReducer));
Listing 5-25Combining Reducers in the DataStore.js File in the src/data Folder
创建购物车摘要组件
为了向用户显示他们购物车的摘要,我在src/shop文件夹中添加了一个名为CartSummary.js的文件,并用它来定义清单 5-26 中所示的组件。
import React, { Component } from "react";
import { Link } from "react-router-dom";
export class CartSummary extends Component {
getSummary = () => {
if (this.props.cartItems > 0) {
return <span>
{ this.props.cartItems } item(s),
${ this.props.cartPrice.toFixed(2)}
</span>
} else {
return <span>Your cart: (empty) </span>
}
}
getLinkClasses = () => {
return `btn btn-sm bg-dark text-white
${ this.props.cartItems === 0 ? "disabled": ""}`;
}
render() {
return <div className="float-right">
<small>
{ this.getSummary() }
<Link className={ this.getLinkClasses() }
to="/shop/cart">
<i className="fa fa-shopping-cart"></i>
</Link>
</small>
</div>
}
}
Listing 5-26The Contents of the CartSummary.js File in the src/shop Folder
清单 5-26 中定义的组件通过cartItems和cartPrice props 接收它所需要的数据,这两个 props 用于创建组件的摘要,还有一个Link,当点击时它将导航到/shop/cart URL。当items属性的值为零时,Link被禁用,以防止用户在没有选择至少一个产品的情况下继续操作。
小费
用作Link内容的i元素应用了添加到清单 5-2 项目中的字体 Awesome 包中的购物车图标。参见 https://fontawesome.com 了解更多详情和所有可用图标。
React 很好地处理了 web 应用开发的许多方面,但是有一些常见的任务比您可能习惯的更难完成。一个例子是条件呈现,其中数据值用于选择不同的内容呈现给用户,或者为属性选择不同的值。React 中最干净的方法是定义一个使用 JavaScript 返回以 HTML 表示的结果的方法,就像清单 5-26 中的getSummary和getLinkClasses方法,它们在组件的render方法中被调用。另一种方法是使用内联的&&操作符,这对于简单的表达式很有效。
在清单 5-27 中,我将数据存储中与购物车相关的添加内容与应用的其余部分连接起来,同时还连接了 action creator 函数。
import React, { Component } from "react";
import { Switch, Route, Redirect }
from "react-router-dom"
import { connect } from "react-redux";
import { loadData } from "../data/ActionCreators";
import { DataTypes } from "../data/Types";
import { Shop } from "./Shop";
import { addToCart, updateCartQuantity, removeFromCart, clearCart }
from "../data/CartActionCreators";
const mapStateToProps = (dataStore) => ({
...dataStore
})
const mapDispatchToProps = {
loadData,addToCart, updateCartQuantity, removeFromCart, clearCart
}
const filterProducts = (products = [], category) =>
(!category || category === "All")
? products
: products.filter(p => p.category.toLowerCase() === category.toLowerCase());
export const ShopConnector = connect(mapStateToProps, mapDispatchToProps)(
class extends Component {
render() {
return <Switch>
<Route path="/shop/products/:category?"
render={ (routeProps) =>
<Shop { ...this.props } { ...routeProps }
products={ filterProducts(this.props.products,
routeProps.match.params.category) } />} />
<Redirect to="/shop/products" />
</Switch>
}
componentDidMount() {
this.props.loadData(DataTypes.CATEGORIES);
this.props.loadData(DataTypes.PRODUCTS);
}
}
)
Listing 5-27Connecting the Cart in the ShopConnector.js File in the src/shop Folder
在清单 5-28 中,我向由Shop组件呈现的内容添加了一个CartSummary,这将确保用户选择的细节显示在产品列表的上方。
import React, { Component } from "react";
import { CategoryNavigation } from "./CategoryNavigation";
import { ProductList } from "./ProductList";
import { CartSummary } from "./CartSummary";
export class Shop extends Component {
render() {
return <div className="container-fluid">
<div className="row">
<div className="col bg-dark text-white">
<div className="navbar-brand">SPORTS STORE</div>
<CartSummary { ...this.props } />
</div>
</div>
<div className="row">
<div className="col-3 p-2">
<CategoryNavigation baseUrl="/shop/products"
categories={ this.props.categories } />
</div>
<div className="col-9 p-2">
<ProductList products={ this.props.products }
addToCart={ this.props.addToCart } />
</div>
</div>
</div>
}
}
Listing 5-28Adding the Summary in the Shop.js File in the src/shop Folder
为了允许用户向购物车添加产品,我在由ProductList组件生产的每个产品的描述旁边添加了一个button,如清单 5-29 所示。
import React, { Component } from "react";
export class ProductList extends Component {
render() {
if (this.props.products == null || this.props.products.length === 0) {
return <h5 className="p-2">No Products</h5>
}
return this.props.products.map(p =>
<div className="card m-1 p-1 bg-light" key={ p.id }>
<h4>
{ p.name }
<span className="badge badge-pill badge-primary float-right">
${ p.price.toFixed(2) }
</span>
</h4>
<div className="card-text bg-white p-1">
{ p.description }
<button className="btn btn-success btn-sm float-right"
onClick={ () => this.props.addToCart(p) } >
Add To Cart
</button>
</div>
</div>
)
}
}
Listing 5-29Adding a Button in the ProductList.js File in the src/shop Folder
React 提供了用于注册事件处理程序的属性,如第十二章所述。单击元素时触发的click事件的处理程序是onClick,指定的函数调用addToCart属性,该属性映射到同名的数据存储操作创建者。
结果是每个产品都显示有一个“添加到购物车”按钮。当点击按钮时,数据存储被更新,用户选择的摘要反映了附加项目和新的总价,如图 5-6 所示。
图 5-6
将产品添加到购物车
添加购物车详细信息组件
为了向用户提供他们选择的详细视图,我在src/shop文件夹中添加了一个名为CartDetails.js的文件,并用它来定义清单 5-30 中所示的组件。
import React, { Component } from "react";
import { Link } from "react-router-dom";
import { CartDetailsRows } from "./CartDetailsRows";
export class CartDetails extends Component {
getLinkClasses = () => `btn btn-secondary m-1
${this.props.cartItems === 0 ? "disabled": ""}`;
render() {
return <div className="m-3">
<h2 className="text-center">Your Cart</h2>
<table className="table table-bordered table-striped">
<thead>
<tr>
<th>Quantity</th>
<th>Product</th>
<th className="text-right">Price</th>
<th className="text-right">Subtotal</th>
<th/>
</tr>
</thead>
<tbody>
<CartDetailsRows cart={ this.props.cart}
cartPrice={ this.props.cartPrice }
updateQuantity={ this.props.updateCartQuantity }
removeFromCart={ this.props.removeFromCart } />
</tbody>
</table>
<div className="text-center">
<Link className="btn btn-primary m-1" to="/shop">
Continue Shopping
</Link>
<Link className={ this.getLinkClasses() } to="/shop/checkout">
Checkout
</Link>
</div>
</div>
}
}
Listing 5-30The Contents of the CartDetails.js File in the src/shop Folder
CartDetails组件向用户呈现一个表格,同时还有Link组件返回产品列表或导航到/shop/checkout URL,这将启动结帐过程。
CartDetails组件依赖于CartDetailsRows组件来显示用户产品选择的细节。为了创建这个组件,我在src/shop文件夹中添加了一个名为CartDetailsRows.js的文件,并用它来定义清单 5-31 中所示的组件。
import React, { Component } from "react";
export class CartDetailsRows extends Component {
handleChange = (product, event) => {
this.props.updateQuantity(product, event.target.value);
}
render() {
if (!this.props.cart || this.props.cart.length === 0) {
return <tr>
<td colSpan="5">Your cart is empty</td>
</tr>
} else {
return <React.Fragment>
{ this.props.cart.map(item =>
<tr key={ item.product.id }>
<td>
<input type="number" value={ item.quantity }
onChange={ (ev) =>
this.handleChange(item.product, ev) } />
</td>
<td>{ item.product.name }</td>
<td>${ item.product.price.toFixed(2) }</td>
<td>${ (item.quantity * item.product.price).toFixed(2) }</td>
<td>
<button className="btn btn-sm btn-danger"
onClick={ () =>
this.props.removeFromCart(item.product)}>
Remove
</button>
</td>
</tr>
)}
<tr>
<th colSpan="3" className="text-right">Total:</th>
<th colSpan="2">${ this.props.cartPrice.toFixed(2) }</th>
</tr>
</React.Fragment>
}
}
}
Listing 5-31The Contents of the CartDetailsRows.js File in the src/shop Folder
render方法必须返回一个单一的顶级元素,当 HTML 文档生成时,该元素被插入到 HTML 中,代替组件的元素,如第九章所述。在不破坏内容布局的情况下,返回一个 HTML 元素并不总是可能的,比如在这个例子中,需要多个表格行。对于这些情况,使用了React.Fragment元素。当内容被处理并且它包含的元素被添加到 HTML 文档中时,这个元素被丢弃。
将购物车 URL 添加到路由配置中
在清单 5-32 中,我更新了ShopConnector组件中的路由配置,以添加对/shop/cart URL 的支持。
import React, { Component } from "react";
import { Switch, Route, Redirect }
from "react-router-dom"
import { connect } from "react-redux";
import { loadData } from "../data/ActionCreators";
import { DataTypes } from "../data/Types";
import { Shop } from "./Shop";
import { addToCart, updateCartQuantity, removeFromCart, clearCart }
from "../data/CartActionCreators";
import { CartDetails } from "./CartDetails";
const mapStateToProps = (dataStore) => ({
...dataStore
})
const mapDispatchToProps = {
loadData,
addToCart, updateCartQuantity, removeFromCart, clearCart
}
const filterProducts = (products = [], category) =>
(!category || category === "All")
? products
: products.filter(p => p.category.toLowerCase() === category.toLowerCase());
export const ShopConnector = connect(mapStateToProps, mapDispatchToProps)(
class extends Component {
render() {
return <Switch>
<Route path="/shop/products/:category?"
render={ (routeProps) =>
<Shop { ...this.props } { ...routeProps }
products={ filterProducts(this.props.products,
routeProps.match.params.category) } />} />
<Route path="/shop/cart" render={ (routeProps) =>
<CartDetails { ...this.props } { ...routeProps } />} />
<Redirect to="/shop/products" />
</Switch>
}
componentDidMount() {
this.props.loadData(DataTypes.CATEGORIES);
this.props.loadData(DataTypes.PRODUCTS);
}
}
)
Listing 5-32Adding a New URL in the ShopConnector.js File in the src/shop Folder
新的Route通过显示CartDetails组件来处理/shop/cart URL,该组件从数据存储和路由系统接收属性。在清单 5-33 中,我已经更新了Shop组件来定义一个包装器函数,它围绕着addToCart动作创建器,也导航到新的 URL。
import React, { Component } from "react";
import { CategoryNavigation } from "./CategoryNavigation";
import { ProductList } from "./ProductList";
import { CartSummary } from "./CartSummary";
export class Shop extends Component {
handleAddToCart = (...args) => {
this.props.addToCart(...args);
this.props.history.push("/shop/cart");
}
render() {
return <div className="container-fluid">
<div className="row">
<div className="col bg-dark text-white">
<div className="navbar-brand">SPORTS STORE</div>
<CartSummary { ...this.props } />
</div>
</div>
<div className="row">
<div className="col-3 p-2">
<CategoryNavigation baseUrl="/shop/products"
categories={ this.props.categories } />
</div>
<div className="col-9 p-2">
<ProductList products={ this.props.products }
addToCart={ this.handleAddToCart } />
</div>
</div>
</div>
}
}
Listing 5-33Navigating to the Cart in the Shop.js File in the src/shop Folder
结果是,单击产品的“添加到购物车”按钮会显示更新的购物车,这为用户提供了返回产品列表并进行进一步选择、编辑购物车内容或开始结账过程的选择,如图 5-7 所示。
图 5-7
将购物车集成到 SportsStore 项目
此时,结帐按钮将用户返回到/store/products URL,但是我在第六章中添加了对结帐的支持。
摘要
在这一章中,我开始开发一个真实的 React 项目。本章的第一部分是建立 Redux 数据存储,它引入了一系列术语——动作、动作创建者、减少者——这些术语您可能不熟悉,但很快就会成为习惯。我还设置了 React 路由包,以便浏览器的 URL 可以用来选择呈现给用户的内容和数据。这些功能提供的基础需要时间来建立,但随着我向 SportsStore 添加更多功能,你会看到它开始产生回报。在下一章中,我将向 SportsStore 应用添加更多功能。