服务端渲染的-React-应用构建指南-全-
服务端渲染的 React 应用构建指南(全)
一、JavaScript 先决条件
本章提供了开始使用 React 所必需的 JavaScript 基础知识。本章的目的是向您介绍 JavaScript 遵循的基本编程范式,以便您在下一章介绍 React 时能够更好地理解它。
即使你是 JavaScript 新手,你也不必担心,因为本章将为你提供入门所需的所有知识。您将从学习简单的概念开始,如常数、变量和控制循环,然后一直学习复杂的主题,如 rest 参数、扩展语法、HTTP 请求和承诺。到本章结束时,你将对这种语言有一个透彻的理解,并将能够开始用 JavaScript 构建 web 应用。
JavaScript 简介
JavaScript 是 web 开发中最流行的语言之一,为了创建在 web 浏览器上运行的应用,学习这种语言是非常必要的。除了 web 应用之外,JavaScript 还可以用于创建桌面、移动以及服务器端应用,这些应用使用各种框架,如 Meteor、React Native 和 Node.js。
JavaScript 由 Brendan Eich 于 1995 年创建,并于 1997 年由 ECMA(欧洲计算机制造商协会)标准化。因此,JavaScript 也被称为 ECMAScript (ES)。随着网络浏览器的发展,JavaScript 也随之发展,1999 年发布了 ES3,2009 年发布了 ES5,2015 年发布了 ES6。在 ES6 之后,JavaScript 每年都会有小的更新,但是 ES6 是目前为止最新的主要版本。
现在让我们设置我们的开发环境,这样我们就可以从 JavaScript 编程的实际例子开始。
设置环境
为了开始用 JavaScript 编程,我将使用可以从 https://code.visualstudio.com/download
下载的 Visual Studio 代码编辑器。但是,您可以使用自己选择的任何编辑器。
一旦编辑器启动并运行,我们将使用 index.html 文件创建我们的启动工作区。该文件将包含我们的页面模板和对 JavaScript 文件(index.js)的引用,该文件将驻留在 scripts 文件夹中。我们将使用
谈到单个文件,index.html 应该包含以下代码:
<html>
<head>
<title>intro-to-js</title>
<link rel="stylesheet" type="text/css"
href="css/style.css"></script>
</head>
<body>
<h1>Introduction to JavaScript</h1>
<hr/>
<div id="ResultContainer"></div>
<script type="text/javascript"
src="scripts/index.js"></script>
</body>
</html>
这里,我们添加了对 JavaScript 文件(index.js)和 css 文件(style.css)的引用。除此之外,该模板包含一个页面标题和一个我们将使用 JavaScript 代码操作的部分。现在让我们检查对 JavaScript 文件的引用是否有效。为此,将以下代码添加到 index.js 文件中:
var ResultContainer = document.getElementById("ResultContainer");
ResultContainer.innerHTML = "Setting up the environment!";
请注意,我们已经使用 JavaScript 的 getElementById()方法从模板中获取了一个部分,然后通过设置 innerHTML 属性更改了它的文本。还可以使用 getElementsByClassName()方法和 getElementsByTagName()方法,以便通过类名和标记名访问元素。因为我们已经在 HTML 模板中设置了
元素的 ID 属性,所以我们使用 getElementById()方法来获取该部分。我们最初将对该部分的引用存储在一个变量中,然后使用该变量访问它的属性。当我们有多个属性需要修改时,这尤其有用。您可能不希望每次修改属性时都搜索该部分。因此,如果需要多次引用,将引用存储在变量中总是一个好的编程实践。
您可以将以下代码添加到 css 文件(style.css)中,以便将样式应用于 HTML 模板:
body{
margin-top:20px;
margin-left:20px;
}
h1{
font-size:50px;
}
#ResultContainer{
margin-top:30px;
padding:10px;
width:450px;
height:200px;
border:1px solid black;
font-size:30px;
}
现在让我们运行我们的项目,看看输出。Visual Studio 代码没有在浏览器中运行 HTML 文件的内置方法。因此,我们必须做一些配置来运行我们的项目。查看您正在使用的编辑器的文档,以找到有关启动配置的帮助。如果您使用的是 Visual Studio 代码,下面的步骤应该可以帮助您入门:
-
按 Ctrl+Shift+P 打开命令选项板。
-
键入“config”并选择“Tasks: Configure Task”命令打开 tasks.json。
-
如果 tasks.json 文件不存在,编辑器将要求您使用默认模板创建一个。继续使用“其他”模板。
-
用以下代码替换 tasks.json 文件内容:
{ "version": "2.0.0", "command": "Chrome", "windows": { "command": "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe" }, "args": ["${file}"], "group": { "kind": "build", "isDefault": true } }
前面的过程如图 1-2 所示。请注意,该图显示了默认情况下生成的代码。我们需要将它更改为上述代码,以便为我们的应用配置启动设置。
图 1-2
启动配置
要测试配置,打开 index.html 文件并按 Ctrl+Shift+B。该文件应该在 Chrome 中打开,您应该看到类似于图 1-3 所示的输出。
图 1-3
起始项目的输出
既然我们的开发环境已经启动并运行,让我们来探索一些基本的 JavaScript 概念。
常量和变量声明
常量是标识符,其值在整个程序范围内保持不变。另一方面,变量是标识符,其值可以随时更改。需要注意的一点是,您可以声明一个变量,然后在代码中初始化它,但是对于常量,您必须在声明过程中赋值。可以使用“const”关键字来声明常数。例如:
const weightInKilos = 100;
JavaScript 中的变量可以使用“let”或“var”关键字来声明。虽然这两个关键字都用于变量声明,但是使用这两个关键字声明的变量范围有很大的不同。用“var”关键字声明的变量在整个程序中都是可访问的,而用“let”关键字声明的变量只在声明它们的块中可用。让我们用一个例子来理解这一点:
...
if(true){
let letVariable = "Variable using let";
}
ResultContainer.innerHTML = letVariable;
如果您尝试执行前面的代码,您可能会在控制台中得到一个错误,指出“letVariable is not defined”。这是因为您试图访问其范围之外的 letVariable。将代码更改如下,您应该会看到类似于图 1-4 的输出:
图 1-4
使用 let 和 var 的变量声明
...
if(true){
var varVariable = "Variable using var";
}
ResultContainer.innerHTML = varVariable;
let 和 var 的另一个区别是,如果你试图在声明之前访问一个“let”变量,系统会抛出一个未定义的错误,但是对于“var”变量,系统不会抛出任何错误。例如,考虑图 1-5 中的这段代码。最后两行可能会给你一个错误,因为你访问了一个从未声明过的变量。然而,前两行不会给你任何错误。当我们试图在声明变量之前访问变量时,我们总是希望系统抛出一个错误。因此,使用“let”关键字而不是“var”关键字来声明变量始终是一个好的做法。
图 1-5
let 与 var
休息参数
Rest 参数是 ES6 中引入的 JavaScript 的一个特性。它允许我们将多个函数输入参数作为一个数组来处理。这在函数的输入参数数量不确定的情况下特别有用。
Note
ES6 是 ECMAScript 的第六个版本,旨在标准化 JavaScript。由于它是在 2015 年发布的,因此也被称为 ECMAScript 2015。
让我们借助下面的例子来理解这一点:
...
function sum(...inputs) {
var result = 0;
for(let i of inputs){
result += i;
}
return result;
}
ResultContainer.innerHTML = sum(5, 10, 5, 5);
这将在您的 HTML 模板上给出“25”的输出。现在让我们明白这里发生了什么。当我们用 rest 参数声明一个函数并调用它时,JavaScript 会自动接受我们传递给该函数的所有参数,并将其放入一个数组中。然后,该函数可以遍历数组,并对提供的所有输入元素执行操作。Rest 参数也可以与常规参数一起使用。但是,rest 参数应该始终是最后一个参数,这样 JavaScript 就可以收集所有剩余的元素并将其放入一个数组中。考虑以下示例:
...
function sum(input1, input2, ...remainingInputs) {
var result = input1 + input2;
for(let i of remainingInputs){
result += i;
}
return result;
}
ResultContainer.innerHTML = sum(5, 10, 5, 5);
前面的代码也将在 HTML 模板上给出“25”的输出。这里唯一的区别是,只有最后两个输入参数被认为是 rest 参数,而前两个是常规参数。rest parameter 的一个主要优点是可以很容易地对输入参数执行数组操作,比如过滤、排序、弹出、推送、反转等等。
析构和扩展语法
析构是 ES6 中引入的 JavaScript 的另一个特性,与 rest 参数正好相反。rest parameter 是将多个值赋给单个数组,而 destructuring 是从单个数组或对象中获取值并将其赋给多个变量。让我们借助一个例子来理解这一点:
...
let fruits = ['Apple', 'Watermelon', 'Grapes'];
let [fruit1, fruit2, fruit3] = fruits;
ResultContainer.innerHTML = fruit2;
前面的代码将输出“西瓜”。这是因为当我们使用析构语法(左边方括号中的变量用逗号分隔,右边是数组或对象)时,JavaScript 自动从右边的数组中提取值,并开始将它们赋给左边的变量。请注意,这些值是从左到右分配的。例如,如果左边有两个变量,右边有四个数组元素,那么数组中的前两个值将被赋给变量,最后两个值将被忽略。相反,如果左边有四个变量,右边只有两个数组元素,那么值将被赋给前两个变量,最后两个变量将是未定义的。
在给变量赋值时,我们也可以跳过一些数组元素。为此,在左侧添加一个额外的逗号分隔符。考虑以下示例:
...
let fruits = ['Apple', 'Watermelon', 'Grapes'];
let [fruit1, , fruit2] = fruits;
ResultContainer.innerHTML = fruit2;
这一次,显示在 HTML 模板上的输出将是“Grapes”。这是因为当 JavaScript 试图找到第二个变量来分配第二个数组元素时,它会因为逗号分隔符而找到一个空条目,并跳过那个特定的数组元素。使用析构可以做的另一件有趣的事情是,可以使用 rest 参数语法将前几个数组元素分配给单独的变量,将剩余的数组元素分配给单个变量。请看下面的例子,以便更好地理解:
...
let fruits = ['Apple', 'Watermelon', 'Grapes',
'Guava'];
let [fruit1, ...OtherFruits] = fruits;
ResultContainer.innerHTML = OtherFruits;
前面的代码会给出“西瓜,葡萄,番石榴”作为输出,因为 rest 参数语法会将所有剩余的数组元素赋给“OtherFruits”变量。
对象的析构方式与数组类似,唯一的例外是在左侧使用花括号而不是方括号来指定变量。考虑下面这个析构对象的例子:
...
let Fruits = {Fruit1: 'Apple', Fruit2: 'Watermelon'};
let {Fruit1, Fruit2} = Fruits;
ResultContainer.innerHTML = Fruit1;
前面的代码将输出“Apple”。现在让我们试着在函数中使用析构。我们将尝试传递一个数组作为输入参数,并在函数定义中析构它。请看下面这段代码:
...
function sum(a, b, c){
return a+b+c;
}
let input = [5,9,6];
ResultContainer.innerHTML = sum(...input);
前面代码的输出应该是“20”。我们在这里做的与 rest 参数完全相反。我们正在创建一个输入元素的数组,并将其直接传递给一个接受三个不同参数的函数。函数声明将类似于常规函数的声明。但是,请注意我们在调用函数时使用的语法(参数名前的三个点)。这就是所谓的扩展语法,它将为我们做所有的工作。它与 rest 参数的语法相同。但是,如果您在调用函数时使用它,它将以相反的方式工作。因此,它不是收集输入参数并将其放入一个数组,而是析构输入参数的数组,并将值赋给函数声明中提到的变量。也可以同时使用 rest 参数和 spread 语法。它的行为方式将取决于环境。现在让我们来看看控制回路。
控制回路
JavaScript 提供了多种遍历循环的方法。让我们用例子来看看它们中的每一个。
为
for 循环接受三个参数:第一个参数用于控制变量的初始化,第二个参数是为 true 提供循环入口的条件,最后一个参数是增量或减量参数,它将修改每个循环中控制变量的值。这三个参数后面是循环体:
...
for(let i=0;i<8;i++){
if(i==1){
continue;
}
console.log("i: " + i);
if(i==4){
break;
}
}
我们可以在各种 JavaScript 循环中使用 break 和 continue 操作符。continue 操作符用于从循环体中跳过剩余的语句并跳到下一次迭代,而 break 操作符用于终止循环的所有剩余迭代。
请注意图 1-6 中的前一段代码及其输出。循环运行八次迭代,并打印每次执行的迭代次数。但是,对于第二次迭代,循环体中 print 语句之前的 if 条件将计算为 true,continue 运算符的执行将使循环跳转到下一次迭代。因此,我们在输出中看不到值“1”。类似地,对于第五次迭代,print 语句后的 if 条件将计算为 true,break 运算符的执行将终止循环的剩余迭代。因此,我们在输出中看不到“4”之后的剩余值。
图 1-6
JavaScript 中的 for 循环
为每一个
forEach 循环在数组或列表上调用,并对每个数组元素执行一个函数。该函数接受三个参数:当前值(fruit)、当前值的索引(index)和当前值所属的数组对象。第二个和第三个参数是可选的,而第一个参数是必需的。使用这种控制循环的主要好处之一是,不会对空数组元素执行该函数,从而为最终应用带来更好的响应时间:
...
let fruits = ['Apple','Grapes','Watermelon'];
fruits.forEach((fruit, index) => {
console.log(index + ': ' + fruit);
})
正在…
while 循环是一个类似于 for 循环的入口控制循环,这意味着在迭代开始时检查验证循环入口的条件。然而,与 for 循环不同,您不必初始化或修改控制变量以及条件。初始化在循环开始前完成,其值在循环体中修改:
...
let fruits = ['Apple', 'Grapes', 'Watermelon'];
let i = 0;
while (i < fruits.length) {
console.log(i + ': ' + fruits[i]);
i++;
}
做...正在…
做...while 循环是由退出控制的 while 循环的变体,这意味着在迭代完成后检查验证循环入口的条件。如果为真,循环将执行下一次迭代:
...
let fruits = ['Apple', 'Grapes', 'Watermelon'];
let i = 0;
do{
console.log(i + ': ' + fruits[i]);
i++;
}while (i < fruits.length);
forEach、while 和 do 的输出...而控制回路示例应类似于图 1-7 所示。
图 1-7
JavaScript 中的 forEach、while 和 do…while 循环
forEach 循环还有更多变体,例如 for…in 和 for…of。然而,前面列出的都是主要的,对于本章的范围来说已经足够了。现在让我们看看 JavaScript 中的类型转换。
类型变换
通常在编程期间,我们需要显式地将一种数据类型的成员转换成另一种数据类型。这可以通过使用 JavaScript 内置的类型转换方法来实现。考虑以下 JavaScript 中的类型转换示例:
...
let input = [5,9,6];
console.log("Type Of [5,9,6]: " + typeof(input));
console.log("Type Of [5,9,6]: " +
typeof(input.toString()));
console.log("Type Of '2': " + typeof(Number('2')));
console.log("'true' to Number: " + Number(true));
console.log("'hi' to Boolean: " + Boolean('hi'));
console.log("'NaN' to Number: " + Boolean(NaN));
首先,我们使用 toString()方法将对象转换成字符串。数据成员的类型可以通过将其传递给 Type of()方法来确定,如前面的示例所示。然后,我们使用 Number()方法将字符串和布尔数据类型转换为数值。我们还可以使用 boolean()方法将值转换为 Boolean,这将在示例中进一步演示。请注意,在执行此操作时,诸如 0、NaN、Undefined 等空值将被转换为 false,而所有其他值将被转换为 true。请注意,空字符串将被转换为 false,而值为“0”的字符串将被转换为 true。前面代码的输出应该类似于图 1-8 所示。
图 1-8
JavaScript 中的类型转换
这就是类型转换。现在让我们看看 JavaScript 中的操作符和函数。
经营者
运算符用于修改程序中的值。我们使用运算符修改的值称为操作数。JavaScript 提供了多种类别的操作符。让我们详细讨论每一个问题。
算术运算符
算术运算符是对数字操作数执行数学运算的运算符。加法(+)、减法(–)、乘法(∵)、除法(/)、模数(%)、增量(++)和减量(-)都是算术运算符的例子。
比较运算符
比较运算符比较两个操作数的值,并根据运算符的真实性返回一个布尔值。等式(),类型等式(=),不等式(!=)、大于(>)、大于或等于(> =)、小于(
赋值运算符
赋值运算符用于给操作数赋值。" = "运算符将右操作数的值赋给左操作数,"+= "运算符将右操作数的值与左操作数相加,然后赋给左操作数,"–= "运算符从左操作数中减去右操作数的值,然后赋给左操作数,"∫= "运算符将两个操作数的值相乘,然后赋给左操作数,"/= "运算符将左操作数的值除以右操作数,然后赋给左操作数,最后," %= "运算符计算左操作数除以右操作数后的模,然后赋给左操作数。
逻辑运算符
逻辑运算符用于组合两个或多个条件,并找出它们的组合真值。该运算符返回一个布尔值。逻辑 AND (&&)和逻辑 OR (||)是 JavaScript 中的两种逻辑运算符。不是(!)是另一个逻辑运算符,用于对返回的布尔值求反。
三元运算符
三元运算符由三部分组成:条件、主体 1 和主体 2。条件和正文 1 由“?”分隔运算符,而两个主体由“:”运算符分隔。如果条件为真,将执行主体 1,而如果条件为假,将执行主体 2。
参考下面这段代码,它演示了 JavaScript 中的所有操作符。在执行时,它应该给出类似于图 1-9 所示的输出:
图 1-9
JavaScript 中的运算符
...
var a=16, b=17;
console.log('Arithmetic Operators');
console.log('16+2 = ' + (16+2));
console.log('16-2 = ' + (16-2));
console.log('16∗2 = ' + (16∗2));
console.log('16/2 = ' + (16/2));
console.log('17%2 = ' + (17%2));
console.log('Comparison Operators');
console.log('1 == "1" ' + ('1' == 1));
console.log('1 === "1" ' + ('1' === 1));
console.log('1 != 2 ' + (1 != 2));
console.log('1 < 2 ' + (1 < 2));
console.log('1 > 2 ' + (1 > 2));
console.log('3 <= 3 ' + (3 <= 3));
console.log('3 >= 3 ' + (3 >= 3));
console.log('Assignment Operators');
console.log('16+=2 ' + (a+=2));
console.log('16–=2 ' + (a–=2));
console.log('16∗=2 ' + (a∗=2));
console.log('16/=2 ' + (a/=2));
console.log('17%=2 ' + (b%=2));
console.log('Logical Operators');
console.log('true && false: ' + (true && false));
console.log('true || false: ' + (true || false));
console.log('!true: ' + (!true));
console.log('Ternary Operator');
console.log('true?T:F --- ' + (true?'T':'F'));
功能
JavaScript 中的函数是自包含的代码片段,可以编写一次,并在需要时通过调用函数来执行。函数可能接受参数并返回值。但是,这不是强制性的。" function "关键字用于定义一个函数。下面给出了函数定义的语法:
function function_name(input_paramaters)
{
function_body
}
函数也可以赋给变量。这就是所谓的函数表达式。它允许我们定义匿名函数,没有名字的函数。这些类型的函数可以通过使用它们被分配到的变量的名称来调用。函数表达式的语法如下:
let variable_name = function(input_paramaters)
{
function_body
}
在 JavaScript 中定义函数的另一种方法是使用箭头函数。它们类似于函数表达式,但语法更短,如下所示:
let variable_name = (input_paramaters) =>
{
function_body
}
JavaScript 中的一切都是借助函数来完成的。例如,当我们使用 console.log 将值打印到浏览器控制台时,log()只不过是一个内置的 JavaScript 函数,为我们完成这项工作。考虑下面这个用 JavaScript 演示函数的例子:
...
function fun()
{
console.log('Regular JS Function.');
}
let functionExpr = function(){
console.log('Function Expression.');
}
let arrFunction = () => {
console.log('Arrow Function.');
}
fun();
functionExpr();
arrFunction();
前面一段代码在浏览器控制台中的输出应该类似于图 1-10 所示。
图 1-10
JavaScript 中的函数
关闭
闭包是一个内部函数,即使在执行了父函数之后,它也可以访问父函数的范围。让我们理解闭包的必要性。假设你的程序中有一个计数器。可以使用一个全局变量和一个函数来增加计数器的值。但是,这种情况下的问题是,代码的任何部分都可以在不访问函数的情况下修改全局变量的值。为了解决这个问题,我们需要一个函数的局部变量。但是如果你尝试这样做,每次函数被调用时,变量都会被初始化,这并不能达到我们的目的。这就是结束进入画面的地方。为了更好地理解,请考虑以下示例:
...
var increment = (function () {
var counter = 0;
return function () {
counter += 1;
console.log(counter);
}
})();
increment();
increment();
increment();
前面代码的结果应该类似于图 1-11 所示。
图 1-11
JavaScript 中的闭包
现在,让我们明白这里发生了什么。当我们将函数的值赋给“increment”变量时,函数执行一次,内部函数的整个主体都赋给变量,因为这是函数返回的内容。现在,当您使用变量名调用函数时,只会执行内部函数。这样,变量将保持为函数的私有变量,并且在将函数赋值给变量的过程中只初始化一次,从而实现了我们的目的,即拥有一个私有的计数器变量,该变量只能通过调用指定的函数来修改。
这都是关于闭包的;现在让我们看看 JavaScript 中的数组。
数组
数组是在单个变量中存储多个值的 JavaScript 对象。您可以通过在数组中指定索引来访问单个值,也可以轻松地遍历所有值来查找特定值。以下是定义数组的语法:
var fruits = ['Watermelon','Apple','Grapes'];
我们也可以在数组中存储其他对象。例如,考虑下面的例子:
...
var fruits = ['Watermelon','Grapes'];
fruits[2] = {
"Apple1": "Red Apple",
"Apple2":"Green Apple"
};
console.log(fruits);
前面代码的输出应该类似于图 1-12 。
图 1-12
在 JavaScript 数组中存储对象
可以使用本章前面讨论的控制循环来迭代数组值。您可以通过在数组名称后面的方括号中提供数组中的索引来访问特定的数组元素。您也可以使用赋值运算符来更改它的值。一旦你看了下一节中的例子,事情会变得更清楚。您可以使用“长度”属性来获取数组元素的计数。除此之外,还有几个内置的数组方法可以用来在 JavaScript 中执行数组操作。以下是方法列表:
-
arr . sort()–这个方法对数组进行排序。
-
arr . foreach()–该方法用于迭代所有数组元素。
-
arr . push(value)–该方法用于在数组的最后一个索引处添加新元素。
-
arr . pop()–这个方法从数组中删除最后一个值。
-
arr . shift()–此方法从数组中删除第一个值,并将剩余的值移动一个索引。
-
arr . un shift(value)–此方法在数组的第一个索引处添加一个新元素,并将剩余的值移动一个索引。
-
array . isarray(arr)–如果“arr”是一个数组,则该方法返回 true。
-
arr . tostring()–此方法将数组转换为值的字符串。
-
arr . join(separator)–该方法类似于 toString 方法,但是您可以为值指定一个分隔符。
-
arr1 . concat(arr2)–该方法用于连接两个数组:arr 1 和 arr 2。
-
arr.splice(position,deletecount,value1,value2,…)–此方法用于在数组的特定位置添加一组新值。第一个参数指定需要添加值的位置,第二个参数是要从数组中删除的元素的计数,其余参数是需要添加到数组中的值。
-
arr2 = arr1.slice(firstindex,last index)–此方法用于从现有数组创建新数组。这些参数指定需要为新数组提取的值的起始和结束索引。如果不指定 lastindex,JavaScript 将采用所有剩余的值。
考虑下面的代码,它将帮助您理解 JavaScript 数组的属性和方法:
...
var fruits = ['Watermelon','Apple','Grapes'];
console.log('Array: ' + fruits.toString());
fruits.sort();
console.log('Sorted Array: ' + fruits.toString());
console.log('forEach:');
fruits.forEach(element => {
console.log(element);
});
fruits.push('Strawberry');
console.log('Push: ' + fruits.toString());
fruits.pop();
console.log('Pop: ' + fruits.toString());
fruits.shift();
console.log('Shift: ' + fruits.toString());
fruits.unshift('Apple')
console.log('Unshift: ' + fruits.toString());
console.log('isArray? ' + Array.isArray(fruits));
var moreFruits = ['Strawberry'];
fruits = fruits.concat(moreFruits);
console.log('Concatenate: ' + fruits.toString());
fruits.splice(0,0,'Guava');
console.log('Splice: ' + fruits.toString());
var top3fruits = fruits.slice(0,3);
console.log('Slice: ' + top3fruits.toString());
前面一段代码的输出应该类似于图 1-13 。
图 1-13
JavaScript 中的数组
这都是关于数组的;现在让我们看看 JavaScript 中的类和模块中的数组。
类别和模块
JavaScript 中的类类似于 Java 和 C++等其他编程语言中的类。它们帮助我们创建构造函数。另一方面,模块是组织代码的一种方式。我们可以将代码分成多个部分,每个部分都将成为一个独立的模块。让我们从课程开始。我们使用“class”关键字,后跟类名,在 JavaScript 中创建一个类。为了实例化该类的一个对象,我们使用了 new 关键字。下面是一个例子:
class Dog
{
}
let dog = new Dog();
我们的类目前没有属性或方法,所以当您实例化该类时,它将创建一个空对象。我们现在将使用一个构造函数向我们的类添加一些属性。构造函数是为类创建对象时执行的方法。在大多数编程语言中,它与类名同名,并且没有返回类型。然而,在 JavaScript 中,使用“constructor”关键字创建构造函数。在使用 new 关键字实例化对象的过程中,可以将参数传递给构造函数。对于属性,您可以使用“this”关键字在构造函数中简单地定义它们,而不是传统地声明它们,然后在构造函数中定义它们。通过使用类的对象访问赋值运算符,可以在程序的任何位置借助该运算符更改属性值。考虑以下示例:
class Dog
{
constructor(id){
this.id = id;
}
}
let dog = new Dog(100);
console.log(dog.id);
dog.id = 200;
console.log(dog.id);
在执行前面的代码时,您将在浏览器控制台中得到“100”和“200”作为输出。我们还可以在类中添加方法来执行操作。让我们添加一个返回“id”属性值的方法。考虑以下示例:
class Dog
{
constructor(id){
this.id = id;
}
}
let dog = new Dog(100);
console.log(dog.getId());
前面的代码应该在浏览器控制台上显示“100”。注意,在 JavaScript 中定义方法时,我们不需要“function”关键字。我们可以简单地使用方法名并继续定义方法。现在让我们看看 JavaScript 中的继承。考虑以下示例:
class Animal
{
constructor(type){
this.type = type;
}
getType(){
return this.type;
}
}
class Dog extends Animal{
constructor(){
super('dog');
}
}
let dog = new Dog();
console.log(dog.getType());
我们使用“extends”关键字来继承子类中基类的属性和方法。我们可以使用“super”关键字来访问子类中基类的成员。在类之外,我们可以使用子类的对象来访问基类和子类的成员。前面的代码应该在浏览器控制台中给出“dog”作为输出。当我们创建一个“dog”类的对象时,它的构造函数被调用,然后调用“Animal”类的构造函数,并将“type”属性值设置为“Dog”。然后,当我们试图调用方法“getType()”时,它在父类和子类中搜索该方法,如果找到就调用它。请注意,如果您没有在子类中定义任何构造函数,父构造函数会被自动调用,但是如果您在子类中定义了构造函数,则需要使用“super”关键字手动调用父类构造函数。那都是关于阶级的。现在让我们来谈谈模块。
一个应用可能有数百个类。组织此类应用代码的最佳方式是在模块中定义类。我们可以为每个模块创建一个单独的文件,然后将该模块导入到其他文件中,以使用该模块中的类。让我们在“脚本”文件夹中创建一个“模块”文件夹。在这个文件夹中,让我们创建一个名为 Animals.js 的文件。现在让我们将“Animal”类从 script.js 文件移动到 Animals.js 文件。为了使它成为一个模块,我们需要在类定义前面使用“export”关键字。因此,Animals.js 文件应该包含以下代码:
export class Animal
{
constructor(type){
this.type = type;
}
getType(){
return this.type;
}
}
但是,您仍然不能在 index.js 文件中使用该类。为此,您必须从 Animals.js 文件中导入“Animal”类。这可以通过在 index.js 文件中使用“import”关键字来完成,如下所示:
import { Animal } from './modules/Animals.js';
let dog = new Animal('dog');
console.log(dog.getType());
您可以从一个文件中导出多个对象,并以类似于上例所示的方式从不同的文件中导入多个对象。请注意,为了在项目中使用模块,您还必须在 html 文件中将 index.js 脚本注册为“模块”类型,而不是“文本/JavaScript”。理想情况下,前面的代码应该在您的浏览器控制台中显示“dog”。然而,如果你使用的是 chrome 浏览器,由于 CORS 政策,你肯定会得到一个错误。这意味着你不能从没有 CORS 头文件的其他跨源文件导入模块,如果你从本地文件系统运行你的应用,这是不可能的。这个问题的解决方案是从服务器上运行你的应用。为此,我们将使用 node.js 创建一个本地服务器。
创建本地服务器
这样做的先决条件是您的系统中已经安装了 Node.js 和 npm。可以从 https://nodejs.org/
下载安装。安装完成后,您可以在编辑器中打开一个终端,并确保当前目录是项目的基本文件夹。从终端执行以下命令:
npm init –y
这应该会在项目的基本文件夹中创建一个“package.json”文件。这是一个保存项目元数据和依赖项信息的文件。更新文件以添加开发服务器依赖项和脚本来启动服务器。更新后的文件应该包含以下信息:
{
"name": "intro-to-js",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"lite": "lite-server –-port 10001",
"start": "npm run lite"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"lite-server":"¹.3.1"
}
}
在更新“package.json”文件之后,从终端运行“npm install”命令来安装我们刚刚添加到 json 文件中的依赖项。将在您的项目目录中创建一个名为“node_modules”的文件夹。该文件夹将包含所有项目依赖项的文件。既然我们已经将开发服务器添加到我们的项目依赖项中并安装了它,我们可以在终端中使用“npm start”命令启动我们的应用。请注意,我们之前在模块示例中遇到的 CORS 策略错误现在已经消失了,我们在浏览器控制台中看到了“dog”输出,这是意料之中的。这是因为我们的应用现在运行在本地服务器上,而不是文件系统上。请注意,浏览器中的 URL 已经从文件路径更改为“localhost”。另请注意,服务器现在正在侦听项目文件中的更改。这意味着,只要您在项目文件中进行任何更改并保存它,它就会自动反映在浏览器中,并且您不需要在每次更改文件时手动刷新浏览器。
DOM 修改
DOM 指的是包含网页中所有元素的文档对象模型。可以使用 JavaScript 中的“document”对象修改 DOM。DOM 是一个庞大的主题,所以我们将把范围限制在选择元素和修改它们的值上。以下是可用于从 DOM 中选择元素的方法:
-
getElementById(' elementId ')–该方法返回一个与输入参数中指定的 ID 相同的元素。
-
getElementsByClassName(' class name ')–该方法返回与输入参数中指定的类名相同的元素列表。请注意,与 getElementById 方法不同,该方法可以返回多个元素,因为 Id 对于元素是唯一的,而类名不是。
-
getElementsByTagName(' tagName ')–该方法返回与输入参数中指定的类名相同的元素列表。因为标记名对于一个元素来说不是唯一的,所以这个方法可以返回多个元素。
让我们看一个例子。我在 index.html 文件中添加了以下部分:
...
<h1>Introduction to JavaScript</h1>
<hr/>
<div ID="ResultContainer"></div>
<br/>
<div class="Footer">Footer Content</div>
...
现在让我们使用文档对象方法获取这些元素。考虑以下 JavaScript 代码:
var header = document.getElementsByTagName("h1");
var body = document.getElementById("ResultContainer");
var footer = document.getElementsByClassName("Footer");
console.log(header);
console.log(body);
console.log(footer);
前面一段代码的输出应该类似于图 1-14 。
图 1-14
在 JavaScript 中访问 DOM 元素
现在让我们修改这些元素。我们可以改变这些元素的内容,为它们添加事件处理程序,还可以改变它们的视觉效果。考虑以下示例:
var header = document.getElementsByTagName("h1");
header[0].textContent = "Header Text from JS";
header[0].setAttribute('isHeader','True');
header[0].style.border = '2px solid black';
console.log(header[0]);
修改 DOM 后,您的 web 页面看起来应该类似于图 1-15 。
图 1-15
JavaScript 中的 DOM 修改
请注意,名为“isheader”的属性被添加到元素中,其值被设置为“true”。这是借助 setAttribute()方法完成的。此外,因为在 JavaScript 代码中使用了 style.border 属性,所以在 header 部分添加了一个边框。DOM 元素还有其他几个属性可以修改。更多信息请访问 https://developer.mozilla.org
。
错误处理
当我们的 JavaScript 代码出错时,我们希望平稳地处理它。这就是 JavaScript 的错误处理机制发挥作用的地方。让我们看一个系统出错的例子:
var fruit = new Fruit();
console.log('rest of the code!');
前面的代码试图实例化“Fruit ”,但是程序中没有定义这样的类。因此,系统将遇到如图 1-16 所示的参考错误,并终止程序的执行。因此,其余的代码将不会被执行。
图 1-16
JavaScript 中的引用错误
处理错误最常见的方法之一是使用 try-catch 块。容易出错的代码写在 try 块中。如果在执行 try 块时发生错误,控制将被转移到下一个处理执行的 catch 块。在执行 catch 块之后,其余的代码照常执行。考虑以下代码:
try{
var fruit = new Fruit();
}
catch(e){
console.log('ERROR: ' + e.message);
}
console.log('rest of the code!');
该代码的输出应类似于图 1-17 。
图 1-17
JavaScript 中的错误处理
请注意,尽管有错误,程序并没有终止,在错误处理之后,代码的其余部分照常执行。可以选择在 try-catch 之后添加一个“finally”块,以添加一段无论错误如何都会执行的代码。这是关于内置在 JavaScript 中的错误。但是,作为开发人员,您可能希望在某些情况下抛出自定义错误。这可以通过使用“throw”关键字来完成。考虑以下代码:
...
try{
throw new Error('Custom Developer Error!');
}
catch(e){
console.log('ERROR: ' + e.message);
}
...
前面的代码应该显示“错误:自定义开发人员错误!”在浏览器控制台中。这就是 JavaScript 中的错误处理。现在让我们看看 HTTP 请求和承诺。
HTTP 请求
JavaScript 中的 HTTP 请求主要用于从远程服务器或 API 获取数据和资源。我们将首先使用 XMLHttpRequest(),这是 JavaScript 用于 HTTP 请求的内置技术。以下是使用 XMLHttpRequest()创建 HTTP 请求的步骤:
-
实例化 XMLHttpRequest()的一个对象。
-
将函数绑定到请求对象的状态更改事件。该函数中的代码将监控请求的成功或失败,并对返回的数据执行操作。
-
使用请求对象创建到 HTTP 资源的连接。
-
发送请求。
考虑下面的例子来更好地理解它:
let request = new XMLHttpRequest();
request.onreadystatechange = function(){
if(request.readyState==4 && request.status==200){
console.log(request.response);
}
}
request.open('GET', 'https://api.github.com/users/msthakkar121');
request.send();
上例的输出应该类似于图 1-18 。这里,我们使用 GitHub 的 API,通过指定用户的登录名来获取用户数据。我们使用绑定到请求的 onReadyStateChange 事件的匿名函数不断监视状态更改请求。readyState 为 4,状态代码为 200,这意味着我们已经成功地收到了请求的响应。因此,在这种情况下,我们将响应对象记录到控制台。XMLHttpRequest()的主要问题是,为了处理响应,您需要知道 readyState 和 status code 的结果的状态代码。由于这个缺点,XMLHttpRequest()很少被直接使用。如果我们使用 JQuery 这样的库,HTTP 请求就容易处理得多。
图 1-18
JavaScript 中的 XMLHttpRequest
让我们看看使用 JQuery 的 HTTP 请求。要在我们的项目中使用 JQuery,我们必须首先在“package.json”文件中注册它,并将其安装在项目的“node_modules”文件夹中。这两者都可以通过从终端执行“npm install jquery”命令来完成。一旦我们将 JQuery 安装到项目中,我们就可以使用 import 语句将它导入到 JavaScript 文件中。考虑下面的例子:
import '../node_modules/jquery/dist/jquery.js';
$.get("https://api.github.com/users/msthakkar121", data => console.log(data));
“$”符号在 JQuery 中是一个常量,被定义为静态的。我们使用这个符号来访问 get 方法。此方法的第一个参数是一个字符串,其中包含请求要发送到的 URL。第二个参数是一个函数,如果请求成功,将执行该函数。第三个参数是可选的,可能包含需要随请求一起发送的数据。在前面的示例中,我们使用了一个箭头函数,它将响应数据记录到浏览器控制台。前面代码的输出应该类似于图 1-18 。尽管这似乎是处理 HTTP 请求的好方法,但它不是理想的方法。get()方法返回一个承诺,帮助我们以更好的方式处理请求。
承诺
承诺旨在处理异步请求。它们是存储异步请求响应的对象。考虑下面这段代码:
let promise = new Promise(function(resolve, reject){
setTimeout(resolve,100,'Resolved');
//setTimeout(reject,100,'Rejected');
});
promise.then(
value => console.log('Success: ' + value),
error => console.log('Error: ' + error)
);
Promise 构造函数接受一个带有两个参数的函数。这两个参数也是分别用于成功和失败的函数。在前面的例子中,我们用匿名函数创建了 promise。这个函数有两个参数:resolve 和 reject。在匿名函数的主体中,我们调用超时为 100 毫秒的“resolve”函数,并将“Resolved”作为一个值传递给 promise。因此,承诺将处于成功状态,并将存储我们作为响应传递的消息。如果我们调用“拒绝”函数而不是 resolve 函数,承诺将处于错误状态。
这就是创造承诺的全部。现在,考虑这样一个场景,您希望根据一个承诺的成功或失败来执行一些操作。在这种情况下,您必须使用 then()函数来结算一个承诺。then()是 promise 对象中的一个函数,它接受两个参数。第一个参数是一个带有一个参数的函数,当承诺处于成功状态时执行该函数。第二个参数是一个带有一个参数的函数,当 promise 处于失败状态时执行该函数。这些函数的参数将包含解析承诺时返回的值。
在前面的代码中,如果您解决了承诺,浏览器控制台上的输出应该是“Success: Resolved”,如果您拒绝了承诺,应该是“Error: Rejected”。注意,您可能需要等待一段时间才能确定承诺,因为您将在这里处理异步操作。
现在让我们看看我们在上一节(“HTTP 请求”)中处理的例子:
$.get("https://api.github.com/users/msthakkar121", data => console.log(data));
前面的代码可以使用 promise 重写,如下所示:
let promise = $.get("https://api.github.com/users/msthakkar100");
promise.then(
data => console.log(data),
error => console.log(error)
);
这就是承诺。随着这个话题的结束,我们也来到了本章的结尾。在下一章中,我们将使用到目前为止学到的 JavaScript 概念来学习 React.js,一个基于 JavaScript 的库。
摘要
-
JavaScript 是 web 开发最重要的语言,它是创建在 web 浏览器上运行的应用所必需的。
-
常量是 JavaScript 中的标识符,其值在整个程序中保持不变,而变量是其值易于改变的标识符。
-
使用“let”或“var”关键字定义变量。建议使用“let”关键字,因为使用“let”定义的变量是有作用域的,并且会严格检查不适当的用法。
-
Rest 参数帮助我们将多个函数输入参数组合成一个数组。另一方面,spread 语法与 rest 参数正好相反,它帮助我们将一个输入数组分解成多个变量。
-
控制循环,如 for、forEach、while 和 do...while 用于处理 JavaScript 中的迭代。
-
类型转换是 JavaScript 中内置的一项规定,可用于显式地将一种数据类型的成员转换为另一种数据类型。
-
JavaScript 中提供了各种算术、比较、赋值、逻辑和三元运算符,可用于修改操作数的值。
-
函数是一段可以在程序中编写一次并多次使用的代码。
-
类和模块帮助我们获得 JavaScript 中面向对象编程的精髓。模块帮助我们将代码组织成文件,并使用 export 和 import 关键字在整个项目中共享代码。
-
我们可以使用 JavaScript 的 document 对象访问和修改网页的 HTML 元素。
-
JavaScript 中的错误可以使用 try-catch 块来处理。也可以选择使用“finally”块。使用“throw”关键字可以生成自定义错误。
-
XML http 请求可用于从远程服务器或 API 获取数据。但是,JQuery 库的 get()方法要方便得多。
-
在处理异步请求时,承诺可以用来方便地处理响应的成功和错误状态。
二、React.js 简介
React.js 是脸书在 2013 年 5 月创建的开源 JavaScript 库。它用于构建用户界面。React 最棒的地方在于它使用声明式编程风格,而不是命令式。前者指定编译器做什么,后者也必须指定如何做。因此,用 React 编程可以减少代码量。
在本章中,我们将一次一个地理解 React 的基本原理。我们还将看到每个原理在实践中是如何工作的。如果你学习了前一章,这一章应该不难理解。如果你还没有,我建议你浏览一下前一章。
设置环境
为了开始用 React 编程,我将使用 Visual Studio 代码编辑器,它可以从 https://code.visualstudio.com/download
下载。但是,您可以使用自己选择的任何编辑器。
安装 Node.js
Node.js 是一个开源的跨平台运行时环境,帮助我们编写 JavaScript 应用并执行它。在前一章中,我们安装了 node.js,同时创建了一个本地服务器,用于处理 JavaScript 中模块导入的跨源请求。如果你还没有安装 node.js,可以从 https://nodejs.org/
下载安装。安装完成后,您可以在编辑器中打开一个终端并运行“node -v”命令来检查 node.js 是否安装正确。如果是,终端将显示 node.js 的安装版本号。
Node.js 运行时有数千个随时可用的模块。这些模块只是预先编写的 JavaScript 应用,可以在您的代码中重用。这些模块可以使用 npm(节点包管理器)添加到您的代码中。npm 随 node.js 一起提供,如果您已经安装了 node.js,它应该已经安装在您的系统中。您可以在终端中执行“npm -v”命令。如果安装正确,此命令将显示系统中安装的 npm 版本。
安装 React
为了使用 react,您必须首先将其安装到您的项目中。有多种方法可以做到这一点。一种方法是简单地使用
<script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>
请注意,前面的文件是开发版本,为了在生产环境中增强性能,您必须使用缩小的生产版本。以下是缩小版:
<script src="https://unpkg.com/react@16/umd/react.production.min.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js" crossorigin></script>
另一种方法是为您的项目手动创建一个文件夹,并添加一个“package.json”文件,其中包含所有依赖项的列表。然后,您可以使用“npm-install”命令将所有依赖项安装到项目文件夹的“node_modules”文件夹中。一旦安装了依赖项,就可以开始在 JavaScript 和 HTML 文件中引用依赖项了。
然而,有一种更好的方法来开始 React 项目。我们将使用“create-react-app”,这是一个现有的适用于初学者的 react 节点模块。在终端中,您可以导航到要在其中创建应用的目录,并执行以下命令:
npx create-react-app my-app
请注意,我们使用了 npx 而不是 npm。npx 是 npm 版本 5.2 及更高版本附带的节点包运行程序。npm 用于安装包,而 npx 用于执行包。在这种情况下,我们实际上并没有在我们的系统上安装“create-react-app”包,但是我们正在执行这个包,它反过来会在我们的系统上安装一个 react 应用。如果您使用 npm,您必须首先使用以下命令将“create-react-app”软件包安装到您的系统中:
npm install create-react-app
然后,使用该包通过以下命令创建一个 react 应用:
create-react-app my-app
因此,我们将简单地使用 npx 而不是 npm。成功执行该命令后,您应该会看到类似如下的输出:
C:\Users\Mohit.Thakkar\...\Chapter_02> npx create-react-app my-app
npx: installed 91 in 35.563s
Creating a new React app in C:\Users\Mohit.Thakkar\...\Chapter_02\my-app.
Installing packages. This might take a couple of minutes.
Installing react, react-dom, and react-scripts...
> core-js@2.6.10 postinstall C:\Users\Mohit.Thakkar\...\Chapter_02\my-app\node_modules\babel-runtime\node_modules\core-js
> node postinstall || echo "ignore"
> core-js@3.2.1 postinstall C:\Users\Mohit.Thakkar\...\my-app\node_modules\core-js
> node scripts/postinstall || echo "ignore"
+ react-dom@16.12.0
+ react-scripts@3.2.0
+ react@16.12.0
added 1475 packages from 693 contributors and audited 904933 packages in 264.267s
found 0 vulnerabilities
Success! Created my-app at C:\Users\Mohit.Thakkar\...\Chapter_02\my-app
Inside that directory, you can run several commands:
npm start
Starts the development server.
npm run build
Bundles the app into static files for production.
npm test
Starts the test runner.
npm run eject
Removes this tool and copies build dependencies, configuration files and scripts into the app directory. If you do this, you can’t go back!
We suggest that you begin by typing:
cd my-app
npm start
Happy hacking!
如果您导航到 my-app 文件夹并打开 package.json 文件,您会注意到您的项目依赖于 react、react-dom 和 react-scripts。需要时,您可以向该列表添加更多依赖项。现在让我们在终端中使用“npm start”命令启动这个应用。确保在终端中将您的目录更改为“my-app”文件夹。您会注意到该应用将在端口 3000 上的本地开发服务器上运行。您将看到类似于图 2-1 的输出。
图 2-1
创建-React-应用
如果您查看文件资源管理器,您会在“public”文件夹中看到一个“index.html”文件和许多。“src”文件夹中的 js 文件。这些 JavaScript 文件是 react 的真正精髓。他们创建组件并在浏览器上呈现它们。由于我们将一个接一个地学习 react 组件,我们现在可以删除“src”文件夹中的所有文件。请注意,一旦您删除了这些文件,您将开始得到“index.js”文件的未找到错误。这是因为“create-react-app”依赖于 react-scripts,react-scripts 使用 webpack 配置文件将应用的入口点指定为“index.js”。因此,当我们删除文件时,它将不再能够找到应用的入口点,并将抛出一个错误。为了解决这个错误,我们现在将添加一个空的“index.js”文件。如果您在添加文件后查看浏览器屏幕,您会注意到输出将是一个空白窗口,但不会有错误。
React 的基本概念
组件是 React 的主要支柱。它们类似于函数,接受属性(输入),输出 UI 元素,并且可以在需要时在其他文件中重用。即使它们类似于函数,您也不需要调用它们。它们可以像 HTML 元素(
它的 React 式质是 React 中的另一个重要概念。这与数据绑定有关。react 组件中的数据来自 props,它是组件输入。当这些数据改变时,用户界面也会改变。这由 React 自动处理。
React 的另一个伟大概念是 HTML 是使用 JavaScript 生成的。这是合理的,因为当您的应用从服务器接收数据时,您需要在呈现数据之前对其进行处理。你可以使用 HTML 或者 JavaScript,这是一个更好的选择。既然您使用 JavaScript 来处理数据,那么您也可以使用 JavaScript 来呈现它。这正是 React 所做的。
这让我们想到了 React 的下一个概念:虚拟 DOM。由于 HTML 是使用 JavaScript 生成的,React 在 DOM 生成之前无法访问它;因此,它保留了视图的虚拟表示,然后用它来比较 UI 中的变化。
现在让我们看看 React 的更多概念,这些概念是构建 React 应用所必需理解的。
单页应用
传统上,当您的应用包含多个页面,并且您单击链接导航到另一个页面时,一个新的请求被发送到服务器,然后浏览器在收到来自服务器的响应时加载新页面。这个过程非常耗费时间和资源。它可以通过实现单页应用(spa)来改进。这是由 React 等 JavaScript 框架提供的客户端呈现所普及的东西。在 SPAs 中,您只需加载页面一次。稍后,当用户请求新页面时,JavaScript 解释请求,从服务器异步获取数据,获取需要更新的 UI 组件,并使用新数据更新页面的一部分,而无需重新加载整个页面。
由于此类请求的异步特性,用户可能需要等待一段时间才能更改 UI。然而,用户体验可以通过使用有吸引力的加载器来增强。SPAs 的一个主要优点是,在整个应用中保持不变的资源(如样式表和脚本)不需要在每次发出请求时重新加载,从而缩短了响应时间。然而,搜索引擎很难索引温泉。这就是为什么如果您的应用需要索引,完全依赖客户端渲染不是一个好主意。您需要记住的另一点是,与大多数服务器端编程语言不同,JavaScript 没有处理内存泄漏的内置机制。处理内存问题是你的责任,因为如果存在的话,它们可能会耗尽浏览器的内存并显著降低你的应用的速度。在本书的后面部分,我们将看到如何通过在 React 应用中实现服务器端渲染来解决这些问题。
不变
不变性是对象的属性,因此一旦定义了它,就不能更改它的值。为了修改此类对象的值,您必须创建一个新对象,并为其指定相同的名称。在 React 的许多地方都可以看到不变性。例如,作为函数输入传递的 React 中的状态对象永远不会被直接修改。它的值只能通过 setState()方法来更改。为了跟踪组件状态的变化,这在 React 中是必要的。如前所述,React 将虚拟 DOM 与旧版本进行比较,以查看哪些值发生了变化,然后只更新 UI 中需要更新的部分。这就是所谓的调和过程。要检查状态是否已经改变,您必须在 React 中实现以下方法:
shouldComponentUpdate(nextProps, nextState) {
if(this.props.myProp !== nextProps.myProp) {
return true;
}
return false;
}
如果方法返回 true,React 将更新组件的 UI。如果状态对象是可变的,那么当状态改变时,“this.props.myProp”的值将立即被修改,并且没有先前值的痕迹可以比较。因为 state 对象是不可变的,所以当您修改包含新值的状态时,将会创建一个新对象(nextProps)。因此,在更新 UI 之前,React 可以很容易地将新值与旧值进行比较。
注意,不变性也有一些缺点。例如,您必须确保使用 setState()这样的方法来修改像 React 中的 State 对象这样的对象。如果不小心使用,不变性可能会损害应用的性能。
纯洁
在计算机科学中,纯度指的是函数对于同一组输入参数总是返回相同值而不会引起任何副作用或外部修改的能力。纯函数从不修改输入参数的值。而是在每次被调用时返回一个新的对象。考虑下面这个纯函数的例子:
function add(a, b) {
return a + b;
}
不管你调用这个函数多少次,它总会返回参数之和。现在考虑下面这个不纯函数的例子:
function GetTodayDate() {
const date = new Date();
return date;
}
上述函数的输出取决于时间,因此每次调用它时都会有所不同。这类函数是不纯函数。不是所有的函数都可以是纯的。有时,您可能希望从外部世界获得一些输入,或者对外部环境做出一些改变。在这种情况下,我们使用不纯的函数。
在 React 中,如果一个组件的输出只依赖于它的 props(函数输入),那么它就被称为纯组件。如果在计算其输出时涉及到组件的状态,则称该组件是不纯的。
作文
在 React 中,组合指的是如何使用属性创建组件的模式。它允许我们有几个优点,例如创建一个通用组件的专用版本,将一个方法作为属性传递,以及使用 props.children 属性将组件传递给其他组件。考虑以下示例:
const GenericButton = props => {
return <button> {props.text} </button>
}
const ResetButton = () => {
return <GenericButton text="Reset"/>
}
在前面的例子中,我们创建了一个通用的按钮组件,它的文本会根据它接收到的属性而不同。此外,我们正在创建一个重置按钮组件,该组件将调用带有文本属性“reset”的通用按钮组件。以类似的方式,我们也可以创建其他类型的按钮,比如登录按钮、提交按钮等等。请考虑下面的示例,该示例演示了如何将方法作为属性进行传递:
const GenericButton = props => {
return <button onClick={props.onClickHandler}>
{props.text}
</button>
}
const ResetButton = () => {
const onClickHandler = () => {
alert('reset button clicked')
}
return <GenericButton text="Reset"
onClickHandler={onClickHandler}/>
}
前面的例子与上一个相似。唯一的区别是,我们在重置按钮组件中为点击事件创建了一个方法,并将其与属性一起传递。该方法可以在通用按钮组件中使用。现在让我们考虑下面的例子,它演示了 props.children 属性的使用:
const GenericButton = props => {
return <div>
<button> {props.text} </button>
{props.children}
</div>
}
const ResetButton = () => {
return <GenericButton text="Reset">
<p>Click to reset text.</p>
</GenericButton>
}
在前面的示例中,我们在呈现通用按钮组件时传递了一个段落标记。我们不会将它作为属性内部的属性进行传递。所以问题是属性会存放在哪里?React 对此有一个简单的解释。在组件的开始和结束标记之间写入的任何内容都存储在一个特殊的属性中:props.children。如果我们为 props.children 属性使用了占位符,React 将只呈现子元素。React 中的成分就是这样。如果你现在觉得图片有点模糊,不要担心。随着我们进入这一章,事情会变得清楚。现在让我们看看 React 中的组件类型。
创建您的第一个 React 组件
当我们在本章开始时使用“create-react-app”命令安装 React 到我们的项目时,它为我们的项目在“src”文件夹下创建了多个 JavaScript 文件,我们删除了其中的大部分。我们还清理了 index.js 文件,这是我们应用的起点。我们将在“src”文件夹中为我们的组件创建一个新的 JavaScript 文件。然后,我们将在 index.js 文件中导入这个组件,并呈现新创建的组件。我们将创建一个带有输入框、按钮和标签的组件。按下按钮时,组件应该用输入框中的文本覆盖标签文本。
为此,我们必须将这些元素呈现给浏览器。如前所述,React 使用 JavaScript 将 HTML 呈现给浏览器。我们将看到两种不同的方法——一种使用 JavaScript,另一种使用 JSX。
使用 JavaScript 创建元素
在使用 JavaScript 时,我们使用两种主要方法来创建和呈现组件——createElement()和 render()。
createElement()是 React 提供的一个方法,它接受三个参数。以下是方法签名:
-
“type”参数可以是一个标记名,如“div”或“p ”,也可以是一个 React 组件。
-
“props”参数可用于以 JavaScript 对象的形式为元素指定任何属性值,如“id”或“name”。
-
“…children”参数指定了我们正在创建的元素的子元素。它可以是数字、文本或其他 React 元素的形式。
React.createElement(
type, [props], [...children]
)
考虑下面这段将创建段落元素的代码:
React.createElement(
'p',
{id: 'para1'},
'Hello from React'
);
React 将编译前面的代码并创建以下等效的 HTML:
<p id="para1">Hello from React</p>
现在我们已经创建了元素,我们想在浏览器上呈现它。为此,我们将使用“react-dom”库提供的 render()方法。以下是方法签名:
-
“element”参数是使用 React.createElement()方法创建的元素。您可以将 createElement()方法的输出存储在一个变量中,并将其作为“Element”参数传递。
-
“容器”参数是您希望在其中呈现当前元素的父元素。您可以使用 JavaScript 的 document.getElementById()、document . getelementsbyclassname()或 document.getElementsByTagName()来获取父元素并将其作为“容器”参数传递。
ReactDOM.render(element, container)
将以下代码添加到 index.js 文件中,以便使用 JavaScript 方法创建和呈现元素:
import React from 'react';
import ReactDOM from 'react-dom';
var reactElement = React.createElement(
'p',
{id: 'para1'},
'Hello from React'
);
ReactDOM.render(reactElement, document.getElementBy('root'));
我们首先从库中导入 React 和 ReactDOM 模块(这是我们在前一章的“类和模块”一节中遇到的)。然后,我们使用 createElement()方法创建一个段落元素,最后,使用 render()方法呈现它。注意,在我们的 index.html 文件中已经有一个“div”元素,id 为“root”。因此,我们使用 same 作为父元素来呈现我们创建的段落。如果您在浏览器中运行代码,您应该会看到类似于图 2-2 的输出。
图 2-2
使用 JavaScript 和 JSX 创建元素
如果您检查浏览器窗口并检查源代码,您会发现以下 HTML 代码:
<div id="root">
<p id="para1">Hello from React</p>
</div>
现在您已经熟悉了创建和呈现元素的 JavaScript 方法,让我们来看看更好的方法。
使用 JSX 创建元素
使用 JavaScript 方法创建 HTML 元素会大大降低代码的可读性,尤其是在创建大量元素时。JSX 是 JavaScript 中一种类似 HTML 的语法,它允许我们在不调用 React.createElement()方法的情况下创建 HTML 元素。然而,在后台,JSX 元素会自动编译为 createElement()方法调用。使用 JSX 的主要好处是它显著增强了代码的可读性,并允许开发人员编写结构化代码。为了呈现 JSX 元素,我们使用 ReactDOM 提供的相同 render()方法。让我们看看下面的代码来理解 JSX 创建与上一个例子中相同的段落标记的方法:
import React from 'react';
import ReactDOM from 'react-dom';
const ParaText = 'Hello from React';
const reactElement = <p id="para1">{ParaText}</p>
ReactDOM.render(reactElement, document.getElementById('root'));
在浏览器中执行前面的代码,您将得到与图 2-2 类似的输出,并且将生成以下等效的 HTML:
<div id="root">
<p id="para1">Hello from React</p>
</div>
正如您所看到的,JSX 方法给我们的输出与 JavaScript 方法相同,但有更好的开发人员体验。因此,我们将使用 JSX 来创建我们的第一个 react 组件。
函数与类组件
有两种方法可以在 React 中创建组件——一种是使用函数,另一种是使用类。两者的主要区别是语法。函数组件是使用普通的 JavaScript 函数创建的,这些函数将 props 作为输入参数,并返回一个 react 组件。另一方面,类组件是使用 JavaScript 类语法创建的,需要从 React 扩展。组件类。使用类组件的一个主要好处是可以使用 React 提供的 React 的状态对象。组件类。因为函数组件是普通的 JavaScript 函数,所以不能在函数组件中使用 state 对象。我们将在本章的后面学习状态的概念。
让我们回到最初的计划,创建一个带有输入框、按钮和标签的组件。在“src”文件夹下创建一个文件“MyComponent.js”。将以下代码添加到文件中:
import React from 'react';
function MyComponent(){
return(
<div>
<input id="inputTextbox"></input>
<button type="submit"
onClick={UpdateText}>
Update
</button>
<br/>
<label id="output"></label>
</div>
);
}
function UpdateText(){
document.getElementById('output').innerText = document.getElementById('inputTextbox').value;
}
export default MyComponent;
前面的代码是创建我们需要的组件的函数方法。它被创建为一个名为“MyComponent”的独立模块,并被导出。我们还创建了一个函数“UpdateText()”,它包含在单击按钮时更新标签值的逻辑。我们已经使用 JSX 语法将这个函数作为属性传递给了按钮元素。React 自动将其绑定到按钮的 onClick 事件。现在让我们看看创建相同组件的类方法。考虑将放入“MyComponent.js”文件中的以下代码:
import React from 'react';
class MyComponent extends React.Component{
UpdateText(){
document.getElementById('output').innerText = document.getElementById('inputTextbox').value;
}
render(){
return(
<div>
<input id="inputTextbox"></input>
<button type="button"
onClick={this.UpdateText}>
Update
</button>
<br/>
<label id="output"></label>
</div>
);
}
}
export default MyComponent;
前面的代码表示创建组件的类方法。它将给出与我们之前看到的函数方法相同的输出。在这种方法中,我们将函数定义为类的成员,并使用“this”关键字来访问类中的那些函数。当我们传递“UpdateText()”函数作为按钮的“onClick”事件的属性时,也会观察到同样的情况。做出 React。组件类提供了“render()”方法,为了编写组件将返回的 JSX 代码,我们覆盖了该方法。现在让我们将这个模块导入 index.js 文件,并将这个组件呈现给浏览器。将以下代码添加到 index.js 文件中:
import React from 'react';
import ReactDOM from 'react-dom';
import MyComponent from './MyComponent';
ReactDOM.render(<MyComponent/>, document.getElementById('root'));
就是这样。我们刚刚创建了第一个 react 组件。如果您在浏览器中执行上述代码,您会看到一个类似于图 2-3 的文本框和按钮。单击按钮时,文本框的值将被复制到下面的标签中。
图 2-3
第一 React 组分
传递属性
属性只不过是在使用 JSX 调用组件时,在 HTML 属性的帮助下传递给 React 组件的值。除非在组件中使用它们,否则传递属性是不必要的。但是,React 建议您始终传递 props,以确保与未来更改的兼容性。可以使用“this.props.propName”语法访问传递给组件的属性。需要注意的一点是,如果您使用类语法创建一个组件,并且您已经定义了一个构造函数,那么向构造函数传递 props 和 React 总是明智的。通过 super()方法的组件类。考虑以下示例:
支原体. js
import React from 'react';
class MyComponent extends React.Component{
render(){
return(
<div>
<label>{this.props.text}</label>
</div>
);
}
}
export default MyComponent;
Index.js
import React from 'react';
import ReactDOM from 'react-dom';
import MyComponent from './MyComponent';
ReactDOM.render(<MyComponent text='Hello from React'/>, document.getElementById('root'));
Note
React 属性是只读的,你不能修改它们的值。
在前面的例子中,我们传递一个简单的字符串作为属性,并显示在一个标签上,但是您也可以通过将变量和对象放在花括号中来传递它们。
无状态和有状态 React 组件
到目前为止,我们已经学习了如何使用 JSX 创建 react 组件并将其渲染到浏览器中。这足以创建静态网站。然而,React 以创建用户交互界面而闻名,这是一种对用户事件做出 React 的界面。要构建这样的接口,有必要了解 React 如何管理状态。现在,您可能想知道我们已经构建了一个 react 应用,它对用户的按钮单击事件做出 React,以便更新标签。我们使用 JavaScript 的内置事件处理程序来实现这一点。另一方面,React 有一个更好的管理方法,那就是使用状态。
状态是包含一组控制组件行为的属性的对象。这些属性的值可能会随着组件的生命周期而改变。我们可以将这个值存储在状态对象中,每当状态对象更新时,React 将部分更新视图,而不是每次值改变时都重新呈现整个视图。使用状态对象的组件是有状态组件,不使用状态对象的组件是无状态组件。如前所述,因为状态对象是通过从 React 扩展而来的。组件类,它只能在类组件中使用,不能在函数组件中使用。
Presentational vs. Container Components
请注意,无状态组件也称为表示性组件,有状态组件称为容器组件。
使用状态对象
让我们看一个如何使用“状态”对象的例子。考虑下面这段将放入 MyComponent.js 文件中的代码。您可以随意创建一个单独的组件,但一定要记住在使用它之前将其导入到 index.js 文件中:
import React from 'react';
class MyComponent extends React.Component{
constructor(props){
super(props);
this.state = {outputText: 'Placeholder'};
}
UpdateText = () => {
this.setState({
outputText: document.getElementById('inputTextbox').value
});
}
render(){
return(
<div>
<input id="inputTextbox" type="text"></input>
<button type="button"
onClick={this.UpdateText}>
Update
</button>
<br/>
<label id="output">
{this.state.outputText}
</label>
</div>
);
}
}
export default MyComponent;
前面代码的输出应该类似于图 2-3 。我们正在做我们在第一个 React 应用中所做的事情。我们有一个文本框、一个标签和一个按钮。单击按钮时,文本框中输入的文本应该反映在标签上。然而,这里的主要区别是,我们使用 React 的内置状态对象来存储初始标签值,而不是直接更新标签,我们将更新状态对象,而 React 将处理视图中的更改。
我们已经在类构造函数中初始化了状态对象。注意,我们已经扩展了 React。组件类到我们的类。这就是状态对象的来源。在构造函数中初始化 state 对象之前,有必要调用 super()方法,该方法允许我们访问 React 的成员。我们班的组件类。该组件返回的 JSX 代码与我们之前创建第一个 React 应用时编写的代码相同。唯一的区别是标签的值现在使用花括号绑定到状态对象属性。这样做有助于 React 理解当状态对象改变时需要改变标签的值。
Note
使用状态对象时,请记住它是区分大小写的。所以,如果你写的是“state”而不是“state”,React 会把它当成普通的 JavaScript 对象,而不是内置的 State 对象。
您将注意到的另一个区别是在按钮的 click 事件上执行的 UpdateText()方法。与上次不同,我们现在使用 setState()方法来更新 State 对象的属性值,而不是直接更新标签。React 提供了 setState()方法。组件类来修改状态对象,并且可以使用“this”关键字来访问。当状态改变时,React 会自动更新视图中的标签值。这就是使用状态对象的好处。
Note
在修改状态对象时,应该始终使用 setState()方法,而不是使用赋值运算符直接更新值。因为当调用这个方法时,React 会将新的虚拟 DOM 与现有的进行比较,并对 UI 进行必要的更改。如果使用赋值操作符直接更新这些值,React 将永远不会知道虚拟 DOM 中的变化,也不会反映在 UI 上。
使用状态的一个主要好处是它支持交互式组件的创建。例如,如果用户正在与一个具有多个组件的屏幕上的一个特定组件进行交互,react 将只对该特定组件进行更改,而其他组件将完全不受这些更改的影响。
关于状态对象需要知道的另一件事是它是不可变的。因此,当您使用 setState()方法修改 state 对象的值时,会创建该对象的新副本。这允许在先前状态和新状态之间进行比较。这就是 React 跟踪变化和更新 UI 的方式。
尽管状态对象类似于属性对象,但两者之间的一个显著区别是,与属性对象不同,状态对象是组件的私有对象,完全由组件控制。当您将属性传递给组件时,它不能修改它们,因为它们是只读的。但是在状态对象的情况下,组件有完全的自由进行任何种类的改变。
尽管有这些好处,人们可能会选择不使用状态对象。您可以决定是构建有状态的组件还是表示的组件。这就是关于状态对象的内容。现在让我们看看 React 组件的生命周期。
React 生命周期
React 中的每个组件都经历特定的生命周期方法。您可以在代码中重写这些方法,以便在应用中实现某些功能。但是在这样做之前,有必要了解 React 组件的生命周期。图 2-4 显示了一个 React 组件的生命周期图。
图 2-4
React 组件的生命周期
React 组件生命周期的三个主要阶段是安装、更新和卸载。
增加
挂载是创建 React 组件实例并将其插入 DOM 的过程。当一个组件被安装时,下列方法被分别调用:
-
构造函数(props)–构造函数是组件实例化时调用的第一个方法。这是设置组件初始状态和绑定方法的地方。如果不必初始化状态和绑定任何方法,则不需要为组件实现构造函数。然而,如果你实现了构造函数,你需要做的第一件事就是使用 super(props)方法从你的构造函数中调用父构造函数。这将允许您的组件继承 React 的属性。组件类。如果不这样做,您将无法访问构造函数中的“this.props”对象。需要注意的一点是,构造函数是唯一可以使用赋值操作符直接给状态对象赋值的地方。在所有其他地方,您需要使用 setState()方法。
注意我们总是建议你把 props 传递给你的组件的构造函数和父类,即使你没有使用任何 props 值。这将确保与未来变化的兼容性。
-
render()–这是 React 组件中唯一需要的方法。它检查“state”和“props”对象,以便将 HTML 返回到 DOM。注意,这个函数应该是纯函数,也就是说,它应该在每次被调用时返回相同的输出,并且不应该篡改组件的状态。如果您需要执行可能会修改组件状态的操作,那么应该用 componentDidMount()方法编写这样的代码。
-
componentDidMount()–在组件挂载到 DOM 树后,立即调用该方法。需要元素出现在 DOM 上的操作应该在这里执行。例如,如果您需要从远程资源加载数据,这个方法是发起网络请求的好地方。如果你的组件需要订阅一些事件,你可以在这里写代码。但是,请确保不要忘记在 componentWillUnmount()方法中取消订阅。此外,如果您想要更新组件的状态,应该从这里调用 setState()方法。
更新
每当组件的“属性”或“状态”对象发生变化时,都会重新呈现组件。如果你的组件有一定的依赖关系,你也可以通过调用 forceUpdate()方法让 React 强制重新渲染它。在所有情况下,更新组件时都会调用以下方法:
-
getDerivedStateFromProps(props,state)–这是组件更新时调用的第一个方法。如果状态需要更新,这个方法应该返回一个对象,否则应该返回 null。此方法用于需要根据对 props 对象的更新来更新状态的情况。
-
shouldcomponentdupdate(next props,next state)–这个方法返回一个布尔值,让 React 知道视图是否应该重新渲染。默认情况下,它返回“True ”,因此 React 将在每次状态改变时重新呈现视图。您可以使用这种方法来优化性能。通过比较 this.props 和 nextProps 以及 this.state 和 nextState,可以手动决定视图是否需要更新。
-
render()–这与第一次挂载组件时调用的相同。但是,在这种情况下,如果 shouldComponentUpdate()方法返回“False”,则不会调用 render()方法。
-
getSnapshotBeforeUpdate(prev props,prev state)–在更新的值提交到 DOM 树之前调用此方法。它让我们有机会捕捉与组件当前状态相关的任何信息。此方法返回的值将作为参数传递给 componentDidUpdate()方法。
-
componentDidUpdate(prev props,prevState,snapshot)–此方法在 DOM 更新后立即调用。如果 shouldComponentUpdate()方法返回“False”,则不会调用它。如果需要,这是发起网络请求的好地方。例如,您可能希望将更新的数据值发送到某个远程数据服务器。为此,您可以将 prevProps 与 this.props 进行比较,以确定值是否已更改,并根据比较结果启动 POST 请求。
卸载
这是 React 组件生命周期的最后一个阶段。在这个阶段,组件从 DOM 中被删除(卸载)。React 只有一个在卸载过程中被调用的方法。
- componentWillUnmount()–这个方法在组件即将从 DOM 树中删除之前被调用。这是执行清理操作的好地方。例如,您可能希望取消订阅组件在 componentDidMount()方法期间订阅的任何事件,或者取消管道中的任何网络请求。
既然您已经对 React 组件的生命周期有了透彻的理解,那么让我们来研究一下 React 中的钩子。
钩住
我们在本章前面已经讨论过,React 提供了某些功能,比如状态对象和生命周期方法。只能在类组件中使用的组件类。随着 React 16.8 中钩子的引入,这种情况不再存在。
钩子是 React 函数,它允许我们从函数组件中钩住 React 状态和生命周期特性,因此不需要使用类方法来构建有状态 React 组件。在 React 中使用钩子时,您需要遵守以下两条规则:
-
不要在循环、条件或嵌套函数中调用钩子。只在函数组件的顶层调用钩子。
-
不要从常规的 JavaScript 函数中调用钩子。只能从 React 函数组件或自定义挂钩中调用它们。
Note
钩子在类组件中不起作用。它们只能在函数组件中使用。
React 首先提供了一些内置的挂钩。然而,开发人员可以编写他们自己的定制钩子,并在他们的应用中使用它们。在下一节中,我们将了解两个最常用的 React 挂钩——状态挂钩和效果挂钩,这两个挂钩都是 React 内置的。
状态挂钩
React 库提供了一个名为“useState()”的函数,可以在函数组件内部调用该函数来添加有状态行为。useState()返回一个状态值和一个可用于更新状态值的函数。它接受初始状态值作为输入参数。让我们考虑一下前面看到的同一个例子——有一个输入框、一个按钮和一个标签的例子。我们将把标签的初始值设置为“Placeholder ”,并在单击按钮时用输入框中的文本更新它。我们将使用 useState()方法来定义一个存储标签值的状态变量和一个更新状态值的方法。考虑下面这段将放入 MyComponent.js 文件中的代码:
import React, {useState} from 'react';
function MyComponent(props){
const [outputValue, setOutputValue] =
useState('Placeholder');
function UpdateText(){
setOutputValue(
document.getElementById('inputTextbox').value
);
}
return(
<div>
<input id="inputTextbox"></input>
<button type="button"
onClick={UpdateText}>
Update
</button>
<br/>
<label>{outputValue}</label>
</div>
);
}
export default MyComponent;
当您在 index.js 文件中使用 ReactDOM.render()方法呈现这个组件时,您应该得到类似于图 2-3 的浏览器输出。我们首先从 React 库中导入 useState()方法,然后声明常量“outputValue”和“setOutputValue”,前者将存储标签文本,后者将是更新状态的方法。为了获得这两个常量的值,我们调用 useState()方法,将初始值作为输入参数。
每当我们想要更新状态值时,我们调用 setOutputValue()方法,将新值作为输入参数,在本例中,是在单击按钮时。
您会注意到,我们没有从代码中手动更新视图。我们刚刚更新了状态对象的值。需要注意的是,这是一个函数组件,我们没有扩展组件类。尽管如此,我们仍然能够使用组件类提供的状态对象,并且一旦状态对象被修改,React 就会自动重新呈现视图。这就是 React 提供的状态钩子的威力。现在让我们看看 React 中另一个广泛使用的内置钩子。
效果挂钩
效果挂钩允许您在功能组件中执行副作用。这意味着,如果您想基于 react 组件的生命周期事件执行一些操作,Effect Hook 是理想的选择。
发起网络数据请求、订阅事件和手动更新 DOM 等操作被称为副作用,因为这些操作可能会影响其他组件,并且无法在渲染期间执行。
React 库中的 useEffect()方法帮助我们实现效果挂钩。它类似于 React 类的 componentDidMount()、componentDidUpdate()和 componentWillUnmount()方法。让我们看看下面的例子,以便更好地理解:
import React, {useState, useEffect} from 'react';
function MyComponent(props){
const [outputValue, setOutputValue] =
useState('Placeholder');
function UpdateText(){
setOutputValue(
document.getElementById('inputTextbox').value
);
}
useEffect(
() => {
alert('Component Updated');
return () => {
alert('Component will be removed');
};
}
);
return(
<div>
<input id="inputTextbox"></input>
<button type="button"
onClick={UpdateText}>
Update
</button>
<br/>
<label>{outputValue}</label>
</div>
);
}
export default MyComponent;
在前面的示例中,我们从 React 库中导入 useEffect()方法,并在组件中调用该方法,将匿名函数作为输入参数。React 将在每次渲染组件时执行该函数。因此,每次更新标签的值时,您都会看到一个警告。现在,您可能想知道这解决了我们在组件被挂载或更新时执行订阅操作的问题,但是当组件将要被卸载时,我们需要执行的清理或取消订阅操作呢?
您可能认为可能有一个单独的方法或钩子来完成清理操作,但是订阅和取消订阅是如此紧密地联系在一起,以至于 React 将它们放在一起。清理操作是在匿名函数返回的函数中执行的,我们将该函数作为输入参数传递给了 useEffect()方法。从技术上讲,在组件即将从 DOM 中卸载之前,您应该会看到一个警告“组件将被删除”。如果您想对此进行测试,请在 index.js 文件中编写以下代码:
import React from 'react';
import ReactDOM from 'react-dom';
import MyComponent from './MyComponent';
ReactDOM.render(
<MyComponent/>,
document.getElementById('root')
);
ReactDOM.unmountComponentAtNode(
document.getElementById('root')
);
在执行这段代码时,您会注意到看到两个警告框,一个接一个。第一个指示组件已经被挂载,第二个告诉您组件即将被卸载。
因此,总的来说,您向 useEffect()方法传递一个匿名函数。当一个组件被渲染时,你在这个函数体中写的任何东西都会被执行。当组件将要从 DOM 树中卸载时,这个函数返回的任何内容都将被执行。
定制挂钩
React 中的自定义钩子是普通的 JavaScript 函数,它们的名字都带有前缀“use”。这些函数或挂钩可用于跨组件共享有状态逻辑。现在让我们考虑一个场景,我们希望每次视图中有变化时都显示一个警告框。我们可能希望在几个组件中这样做,但是在我们编写的每个组件中重复这个逻辑不是一个好的做法。因此,我们可以在一个定制的钩子中编写这个逻辑,并在组件间共享它。让我们借助一个例子来理解它。让我们首先回顾一下在呈现组件时使用 useEffect()钩子显示警告的代码:
import React, {useState, useEffect} from 'react';
function MyComponent(props){
const [outputValue, setOutputValue] =
useState('Placeholder');
function UpdateText(){
setOutputValue(
document.getElementById('inputTextbox').value
);
}
useEffect(
() => {
alert('Component Updated');
}
);
return(
<div>
<input id="inputTextbox"></input>
<button type="button"
onClick={UpdateText}>
Update
</button>
<br/>
<label>{outputValue}</label>
</div>
);
}
export default MyComponent;
我们在这里做的是调用 useEffect()钩子,每当组件被更新时,它显示一个警告。我们知道一个钩子可以调用另一个钩子。因此,我们现在将创建一个自定义钩子,它将接受一个输入参数,并在调用组件更新时为我们显示警告。我在“src”文件夹下创建了一个新的文件夹“Hooks ”,它将包含我所有的定制钩子。你可以遵循你选择的结构。让我们为我们的钩子创建一个新文件“useChangeAlert.js”。它将包含以下代码:
useChangeAlert.js
import {useEffect} from 'react';
export const useChangeAlert = (text) => {
useEffect(
() => {
alert(text);
}
);
}
我们已经创建了一个简单的 JavaScript 函数,每当调用组件被更新时,它将显示一个带有输入参数的警告消息。这将作为我们的自定义挂钩。注意,我们使用了 React 内置的 useEffect()钩子来知道调用组件何时被重新呈现。现在让我们在组件中使用这个定制钩子。查看 MyComponent.js 文件中的以下代码:
支原体. js
import React, {useState} from 'react';
import {useChangeAlert} from './Hooks/useChangeAlert'
function MyComponent(props){
const [outputValue, setOutputValue] =
useState('Placeholder');
function UpdateText(){
setOutputValue(
document.getElementById('inputTextbox').value
);
}
useChangeAlert(`New Label Value: ${outputValue}`);
return(
<div>
<input id="inputTextbox"></input>
<button type="button"
onClick={UpdateText}>
Update
</button>
<br/>
<label>{outputValue}</label>
</div>
);
}
export default MyComponent;
我们已经从 Hooks 文件夹中导入了 useChangeAlert()钩子,我们没有直接调用 useEffect()钩子,而是调用了带有新标签值的定制钩子作为输入参数。还要注意,因为我们没有在组件中使用 useEffect()钩子,所以我们不需要从 React 库中导入它。如果您运行代码,您会注意到,每当您更改输入框中的值并单击按钮时,标签都会更新,并显示一个带有新标签值的警告。这是由我们的定制钩子完成的。您可以在多个组件中使用这个自定义挂钩。让我们创建另一个组件来演示这一点。我已经创建了一个与现有组件完全相似的组件。我刚刚修改了一些 id 和输入,我们将它们传递给我们的自定义钩子。请随意创建您选择的组件。请看下面的新组件代码,它将放在新文件“MyComponent2.js”中:
import React, {useState} from 'react';
import {useChangeAlert} from './Hooks/useChangeAlert'
function MyComponent2(props){
const [outputValue, setOutputValue] =
useState('Placeholder');
function UpdateText(){
setOutputValue(
document.getElementById('inputTextbox2').value
);
}
useChangeAlert(`New Label2 Value: ${outputValue}`);
return(
<div>
<input id="inputTextbox2"></input>
<button type="button"
onClick={UpdateText}>
Update
</button>
<br/>
<label>{outputValue}</label>
</div>
);
}
export default MyComponent2;
我们已经导入了自定义钩子,并以与前面的组件类似的方式使用它。在单击 update 按钮时,标签应该被更新,并且应该显示一个带有新标签值的警告框,就像我们之前的组件一样。现在下一步是将我们的两个组件一起呈现在同一个页面上,看看我们的定制钩子如何对更新操作做出 React。考虑下面这段将进入我们索引的代码。JS 文件:
import React from 'react';
import ReactDOM from 'react-dom';
import MyComponent from './MyComponent';
import MyComponent2 from './MyComponent2';
ReactDOM.render(
<React.Fragment>
<MyComponent/>
<MyComponent2/>
</React.Fragment>,
document.getElementById('root')
);
注意,每当我们想要向 DOM 呈现一个元素数组时,我们都使用<react.fragment>。如果您执行代码,您将看到类似于图 2-5 的浏览器输出。</react.fragment>
图 2-5
自定义钩子在 React
在更新任一值并单击其相应的按钮时,您将看到一个警告框,其中显示了相应的警告消息以及新的标签值。这就是使用定制钩子在 React 组件之间共享有状态逻辑的方式。现在让我们学习如何在 React 组件中处理数据。
使用数据
到目前为止,我们只处理静态数据。然而,如今从远程服务器获取数据在 web 应用中非常普遍。我们将在下一个主题中学习如何使用异步 JavaScript 请求从远程 API 获取数据。现在,让我们在应用中定义一个静态 JSON 对象,并在浏览器上显示数据。考虑以下示例:
import React from 'react';
const data = [
{
web_page: "http://www.davietjal.org/",
state_province: "Punjab",
name: "DAV Institute of Technology",
country: "India"
},
{
web_page: "http://www.lpu.in/",
state_province: "Punjab",
name: "Lovely Professional University",
country: "India"
},
{
web_page: "http://www.ddu.ac.in/",
state_province: "Gujarat",
name: "Dharamsinh Desai University",
country: "India"
}];
function MyComponent(props) {
return (
<div>
<h1>Universities in India</h1>
<br />
{
data.map(
(item, index) => (
<div key={index}>
<h2>{item.name}</h2>
<p>{item.state_province},
{item.country} </p>
<a href={item.web_page}>Website</a>
</div>
)
)
}
</div>
);
}
export default MyComponent;
在呈现前面的组件时,您应该会看到类似于图 2-6 的浏览器输出。
图 2-6
在 React 中使用数据
我已经对元素应用了一些基本的 CSS,所以如果你的输出和我的不完全一样,请不要担心。您可以使用自己的 CSS 自定义样式。我们在这个组件中使用了 JSX 和 JavaScript 的组合。我们使用 JavaScript 的 array.map()函数迭代数据数组,并对数据数组中的每个元素执行 JSX 代码。这就是我们在 React 组件中处理数据的方式。现在,让我们学习如何使用异步 JavaScript 调用从远程服务器获取数据。
AJAX 调用
React 为我们提供了使用多个 AJAX 库的选项,包括 Axios、jQuery AJAX 和浏览器内置的 window.fetch。为此,我们需要使用以下命令将库安装到我们的项目中:
$ npm install axios
一旦 Axios 安装到我们的项目中,我们就可以将它导入到组件文件中,并使用它的 get()方法来发起一个网络请求。我们将使用 GitHub 的公共 API 来获取 GitHub 用户列表,并在我们的应用中显示它们。考虑以下示例:
import React from 'react';
import axios from 'axios';
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
error: null,
isLoaded: false,
data: []
};
}
componentDidMount() {
axios.get('https://api.github.com/users')
.then(response => {
// success
this.setState({ data: response.data });
this.setState({ isLoaded: true });
})
.catch(error => {
// error
this.setState({ error: error });
})
}
render() {
if (this.state.error) {
return <div>
Error: {this.state.error.message}
</div>;
}
else if (!this.state.isLoaded) {
return <div>Loading...</div>;
}
else {
return (
<div>
<h1>Github Users</h1>
<br />
{this.state.data.map((item, index) => (
<div key={index} className="UserBlock">
<img src={item.avatar_url}
alt='User Icon'>
</img>
<div className="UserDetails">
<p>Username: {item.login}</p>
<p>ID: {item.id}</p>
</div>
</div>
))}
</div>
);
}
}
}
export default MyComponent;
请注意,我们已经创建了一个类组件,因为我们将使用 componentDidMount()方法来执行数据操作。我们向构造函数中的状态对象添加了三个成员:
-
data–这个对象将存储我们从远程 API 获取的数据。最初设置为空对象。
-
error–这个对象将存储数据操作过程中发生的任何错误的详细信息。最初设置为空。
-
is loaded–这是一个布尔属性,告诉我们数据是否已经加载到数据对象中。最初设置为假。
我们使用 React 生命周期方法 componentDidMount()来编写数据获取逻辑。我们已经使用 Axios 提供的 get()方法向 GitHub API 发出网络请求。注意,为了使用 Axios 库,有必要将它导入到我们的组件文件中。您还可以使用 Axios 提供的 post()方法来发起 POST 请求。
图 2-7
AJAX 请求正在进行中
then()方法是 get()方法的扩展,如果承诺被 get()方法实现,就会被执行。因此,在这个方法中,我们已经编写了网络请求成功时要执行的代码。我们已经用 get 请求返回的数据填充了数据对象,并将 isLoaded 属性设置为 true。
catch()方法将捕获网络请求执行期间发生的任何错误。在这个方法中,我们用错误的细节填充了 error 对象。
图 2-8
AJAX 请求期间出错
然后是 render()方法。在呈现数据之前,我们可能需要检查数据是否被正确加载。我们还可能希望检查在网络操作期间是否发生了任何错误。因此,如果发生了错误,我们将简单地在浏览器上显示错误消息。如果数据仍在加载,我们将在浏览器上显示一条简单的加载消息。最后,如果数据已经加载到数据对象中,我们将使用 JavaScript 的 array.map()方法对数据进行迭代,并在浏览器上显示 GitHub 用户的详细信息。图 2-7 、 2-8 和 2-9 分别显示了请求进行中、错误和成功的场景。
图 2-9
成功的 AJAX 请求
就这样,你成功地创建了一个面向数据的 React 应用。您可以使用公共领域中的其他 API。例如,通过将 username 作为参数传递给 API 调用,您可以接受用户的输入并获取特定 GitHub 用户的详细信息。你玩得越多,你就越能理解它。
现在让我们看看如何向 React 应用添加样式。
样式 React 组件
如果您已经执行了之前的 GitHub 用户应用代码,您一定会注意到您的输出与图 2-9 略有不同。这是因为我在应用代码中添加了一些 CSS,以便为元素添加样式,这可能是您的代码中所没有的。
有多种方法来设计 React 组件的样式。其中一些将在下一节讨论。
React 中的 CSS
我们称之为层叠样式表或 CSS,是我们在应用中经常使用的东西。使用它的一种方法是简单地添加符合 JSX 代码的样式属性,如下所示:
<div style={{ display: 'inline-block',
marginLeft: '15px'}} >
</div>
使用内联 CSS 时,有一件事你需要注意。CSS 元素键中遇到的所有“-”必须替换为 camelCase 格式。所以,你必须写“marginLeft”而不是“margin-left”。注意,您不需要在 CSS 元素的值中使用 camelCase。这种限制只针对按键。因此,键“display”的值“inline-block”将保持原样,即使它包含“-”。另外,请注意,我们使用了双花括号来指定 style 属性的值。这是因为它接受在大括号内定义的 JavaScript 对象。您也可以将它重写如下:
const divStyle = { display: 'inline-block', marginLeft: '15px'}
<div style={divStyle}>
</div>
内联 CSS 是样式化 React 组件的最不可取的方式,因为它使代码结构非常混乱,难以阅读。使用 CSS 的一个更好的方法是创建一个单独的样式表,并将其导入到组件文件或根应用文件中。这是我在前面的例子中采用的方法。以下是我为 GitHub 用户示例编写的样式表:
Index.css
body{
font-family: sans-serif;
}
#root div h1{
text-align: center;
}
img{
height:50px;
width:50px;
border: 1px solid black;
}
.UserBlock{
display: inline-block;
border: 1px solid black;
border-radius: 5px;
padding: 10px;
margin: 15px;
width:255px;
}
.UserDetails{
display: inline-block;
margin-left:15px;
}
我使用以下代码行将这个样式表导入到根 JavaScript 文件“index.js”中:
...
import './index.css'
...
在组件文件“MyComponent.js”中,可以使用“className”属性为 JSX 代码中的元素指定 CSS 类的名称,如下所示:
<div className="UserBlock">
</div>
与内联 CSS 相比,这是一种更好的方法,由于样式表的可重用性,它可以产生更好的代码结构和更小的代码。
萨斯和 SCSS 在作出 React
在设计 React 应用时,SASS 是最受欢迎的选择。它代表语法上令人敬畏的样式表,是将输入编译成 CSS 代码的预处理器。较新版本的 SASS 被称为 SCSS (Sassy CSS ),与 SASS 相比语法略有不同。萨斯和 SCSS 都类似于 CSS 样式表,但对 CSS 变量和数学运算的支持更强大。
SASS 有一个宽松的语法,使用缩进而不是花括号来表示选择器的嵌套,使用换行符而不是分号来分隔属性。这些样式表有“.sass "文件扩展名。
另一方面,SCSS 更接近 CSS 语法,它使用花括号来表示选择器的嵌套和分号来分隔属性。这就是为什么每个 CSS 样式表都是具有相同解释的有效 SCSS 样式表。这些样式表有“.scss”文件扩展名。考虑下面这个演示萨斯和 SCSS 的例子:
Index.sass
$br: 5px
.UserBlock
border-radius: $br
指数. SCS
$br: 5px;
.UserBlock{
border-radius: $br;
}
您可以使用“$”符号定义变量,并在整个样式表中使用它。SCSS 提供的另一个有趣的概念是 Mixin。Mixin 允许我们创建一个 CSS 代码块,我们可以在整个样式表中重用它。考虑以下示例:
@mixin black-border {
border: 1px solid black;
}
div {
@include black-border;
}
p {
@include black-border;
}
这里需要注意的两个重要关键词是“@mixin”和“@include”。在 SASS 中,这些关键字被替换为“=”和“+”。我们使用前一个关键字创建一个 Mixin,并在后一个关键字的帮助下在整个样式表中使用它。前述 SCSS 码的 SASS 等价物如下:
=black-border
border: 1px solid black
div
+black-border
p
+black-border
您也可以将参数传递给 Mixin,如下所示:
风格. SCS
@mixin custom-border($color) {
border: 1px solid $color;
}
div {
@include custom-border(black);
}
p {
@include custom-border(blue);
}
Style.sass
=custom-border($color)
border: 1px solid $color
div
+custom-border(black)
p
+custom-border(blue)
萨斯和 SCSS 依赖于一个叫做“萨斯加载器”的模块。该模块依赖于“React 脚本”。如果您已经使用“create-react-app”命令创建了 React 应用,那么“react-scripts”已经作为一个依赖项添加到 JSON 文件中,并且“react-scripts”的所有依赖项(包括“sass-loader ”)都安装在 node_modules 文件夹中。但是,您仍然需要安装一个模块。那就是“node-sass”。它负责将 SASS 或 SCSS 编译成 CSS。您可以使用以下命令安装它:
npm install node-sass
一旦它被安装到你的项目中,你就可以开始使用 SASS 和 SCSS,无需任何进一步的配置。在这种情况下,你只需要一个“.萨斯“或者”。scss”文件,您就可以开始了。
但是,如果您没有使用“create-react-app”命令来创建 react 应用,您将需要使用以下命令手动安装依赖项:
npm install react-scripts
npm install node-sass
还有许多其他方法可以为应用添加样式,比如样式化组件、Less、CSS 模块等等。然而,在我看来,萨斯和 SCSS 比其他方法更好。
关于造型就是这样。现在让我们来看看 Babel 和 Webpack。
巴别塔和网络包
Babel 是一个 JavaScript 编译器,它为整个开发者社区解决了一个非常大的问题——向后兼容性。我们都面临着像 Internet Explorer 和 Edge 这样的浏览器无法支持最新 JavaScript 功能的问题。例如,大多数现代浏览器都支持 ES6 中引入的箭头功能,但 IE 11 不支持。在这种情况下,巴别塔来拯救。它接受用一种标准编写的代码,并把它编译成用另一种标准编写的代码。然而,Babel 不会自己编译任何东西。我们将不得不安装几个插件来支持老版本浏览器的特定功能。
另一方面,Webpack 是一个模块捆绑器,它处理应用文件的捆绑和缩小。它检查我们的应用,并创建一个我们的应用正常运行所依赖的所有模块的列表。然后,它创建一个可插入的包或包,其中包含我们的应用所需的最少数量的文件。它通常需要一个 webpack.config.js 文件,我们在其中指定应用的入口点以及关于应用输出的其他相关信息。
可以使用以下 npm 命令轻松安装 Babel:
npm install @babel/core @babel/cli babel-loader
@babel/core 是允许我们运行 babel 的模块。@babel/cli 用于从终端运行 babel。babel-loader 是一个模块,允许我们教 webpack 如何识别和运行与 babel 相关的文件。如前所述,要让 babel 将我们的代码翻译成向后兼容的版本,我们必须为 babel 添加某些插件。让我们尝试安装将箭头函数转换为常规 JavaScript 函数的插件。使用以下命令来完成此操作:
npm install @babel/plugin-transform-arrow-functions
现在,为了配置 babel,我们需要在应用的根目录下创建一个. babelrc 文件,并向其中添加以下代码:
{
"plugins": ["@babel/transform-arrow-functions "]
}
现在让我们创建以下 script.js 文件来测试 babel 是否正常工作:
Script.js
var a = () => {};
var b = (c) => c;
如果您在终端中运行“npx babel src\Script.js”命令,您将看到以下输出:
var a = function () {};
var b = function (c) {
return c;
};
正如您所看到的,babel 成功地将箭头函数转换为普通的 JavaScript 函数,以便使它们与旧浏览器兼容。您可以在应用中安装和使用许多其他插件。然而,在一个大的应用中,你可能需要大量的插件,一个一个地安装它们是不实际的。为了解决这个问题,我们有一个叫做 babel 预置的东西,它将特定类型的应用所需的某些插件进行分组。可以直接安装一个巴别塔预置,而不是一个一个安装多个插件。以下是创建 React 应用时很重要的两个 babel 预设:
-
@ babel/preset-env–将 ES6、ES7 和 ES8 代码转换为 ES5
-
@ babel/preset-react–将 JSX 转换成 JavaScript
让我们使用以下代码安装这两个预置:
npm install @babel/preset-env
npm install @babel/preset-react
既然我们已经安装了这些预置,我们还需要将它添加到。babelrc 文件。有关相同信息,请参考以下代码:
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
]
}
现在让我们将下面的 JSX 代码添加到 Script.js 文件中,看看 babel 是如何编译它的:
ReactDOM.render(<MyComponent/>,
document.getElementById('root'));
如果在终端中再次运行“npx babel src\Script.js”命令,您将看到以下输出:
ReactDOM.render(
React.createElement(MyComponent, null),
document.getElementById('root')
);
正如您所看到的,babel 将 JSX 代码转换成等价的 JavaScript 代码,以便旧版本的浏览器能够理解它。到目前为止,我们一直在通过终端测试巴别塔。让我们看看如何在 webpack 的帮助下将它添加到我们的应用包中。使用以下命令将 webpack 安装到您的应用中:
npm install webpack webpack-cli
安装后,webpack 需要在应用的根目录下有一个配置文件“webpack.config.js”。让我们创建该文件,并向其中添加以下代码:
const path = require('path');
module.exports = {
entry: './src/app.js',
output: {
path: path.join(__dirname, 'public'),
filename: 'bundle.js'
}
};
现在,是时候让 webpack 学习如何在运行时使用 babel,以便将 JSX 代码编译成 JavaScript 代码了。为此,我们必须在配置文件中添加一些代码。请参考 webpack 的以下更新配置文件:
const path = require('path');
module.exports = {
entry: './src/app.js',
output: {
path: path.join(__dirname, 'public'),
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader'
}
]
}
};
加载器允许我们定制 webpack 在加载某些文件时的行为。我们可以通过在 webpack.config 文件中将 module 属性设置为一组规则来定义加载器。在本例中,我们将规则的“loader”属性设置为 babel-loader,这将告诉 webpack 将 JSX 代码转换为 JavaScript 代码。“test”属性允许我们指定想要在什么类型的文件上运行加载程序。我们为以“.”结尾的文件指定一种模式。js”。我们可以使用“exclude”属性来排除一组文件。在我们的例子中,我们排除了 node_modules 文件夹,因为我们不希望 babel 扫描这些文件。
Note
您在本主题中看到的代码片段不足以创建一个成熟的 React 应用。您还需要进行许多其他 webpack 配置。但是,如果您已经使用“create-react-app”命令创建了您的应用,那么一切都是预先配置好的,您不需要担心 webpack 的配置。
就是这样。Webpack 现在设置为识别 JSX 代码,并使用 babel 将其编译成 JavaScript 代码。就这样,我们来到了本章的结尾。让我们总结一下我们所学到的东西。
摘要
-
React.js 是一个用于构建用户界面的开源 JavaScript 库。
-
React 使用 JavaScript 生成 HTML。它以组件的形式呈现一切。
-
React 中的组件类似于 JavaScript 函数。它们接受输入参数并输出 UI 元素。组件可以像 HTML 元素一样使用。
-
当组件的状态改变时,UI 会自动改变。
-
UI 中的更改使用虚拟 DOM 进行比较,因为实际的 DOM 在 UI 呈现之前是不可访问的。
-
单页应用、不变性、纯度和组成是与 React 相关的一些重要概念。
-
“create-react-app”命令可用于在 react 中创建一个启动应用。为此,您需要在系统中安装 Node.js 环境和节点包管理器(npm)。
-
您可以使用传统的 JavaScript 语法创建 React 组件,也可以使用更接近 HTML 的现代 JSX 语法。
-
创建 React 组件有两种方法——类和函数。
-
类组件允许您扩展 React。组件类和继承 React 生命周期方法以及状态对象。在函数组件中这样做,你需要钩子。
-
您可以使用状态挂钩来创建有状态的功能组件,而要在功能组件中单步执行 React 的生命周期方法,您可以使用效果挂钩。您还可以创建适合您的应用需求的定制挂钩。
-
React 组件的生命周期包括安装、更新和卸载。
-
render()方法在每次组件更新或第一次挂载时被调用。
-
componentDidMount()方法告诉我们组件已经挂载,componentDidUpdate()方法告诉我们组件已经更新。
-
componentWillUnmount()方法在组件即将从 DOM 中卸载之前被调用。
-
您可以使用 AJAX 调用的 Axios 库从远程资源获取应用的数据。您可以使用 JavaScript 的 array.map()函数来迭代数组数据。
-
componentDidMount()是发起获取数据的网络请求的理想场所。
-
SASS 和 SCSS 是设计 React 应用最流行的替代方法。
-
Babel 是一个 JavaScript 编译器,用于增加代码的向后兼容性。
-
Webpack 是一个模块捆绑器,处理应用文件的捆绑和缩小。
三、Next.js
在前一章中,我们学习了一个叫做 React.js 的 JavaScript 框架,以及如何使用 React.js 框架创建一个客户端渲染应用。在这一章中,我们将学习一个叫做“Next.js”的框架,它用于构建在服务器端呈现的应用。
我们将了解 Next.js 框架的特性,路由使用 Next.js 构建的应用,动态加载内容,配置 webpack 和 Babel,等等。作为本章的一部分,我们还将从头开始创建一个交互式 Next.js 应用。
随后,我们将学习如何将用于状态管理的 Redux 和用于 API 查询的 GraphQL 等框架集成到 Next.js 应用中。让我们开始吧。
Next.js 简介
Next.js 是一个帮助我们在服务器端呈现应用的框架。正如上一章所讨论的,当您创建一个 React 应用时,所有的内容都使用客户端 JavaScript 呈现给浏览器。与此相关的有几个问题。下面是一个简单的列表:
-
浏览器中未启用 JavaScript 的客户端可能无法查看内容。
-
出于安全原因,我们可能只想在服务器端呈现某些内容,这在普通的 React.js 应用中是不可能的。
-
在客户端呈现所有内容会显著增加应用的加载时间。
-
搜索引擎很难索引使用普通 React.js 构建的单页面应用。
所有这些问题都可以在服务器端渲染的帮助下解决。Next.js 就是这样一个框架。每次收到请求时,它都会在服务器的帮助下,在运行时动态生成一个页面。它被网飞、Docker、GitHub、优步、星巴克等领先公司使用。让我们看看 Next.js 框架的特性。
Next.js 的特性
以下是 Next.js framework 的一些主要功能:
-
热重新加载–每次在页面上检测到更改时,Next.js 都会重新加载页面,以便立即反映更改。
-
基于页面的路由–URL 被映射到文件系统上的“pages”文件夹,无需任何配置即可使用。但是,也支持动态路由。
-
自动代码分割–页面只加载所需的代码,从而加快加载速度。
-
页面预取–您可以在链接页面时使用<链接>标签上的“预取”属性,以便在后台预取页面。
-
热模块替换(HMR)–您可以使用 HMR 在运行时替换、添加或删除应用中的模块。
-
服务器端呈现(SSR)–您可以从服务器端呈现页面,而不是在客户端生成整个 HTML。这使得内容丰富的页面的加载时间更短。SSR 还可以确保你的页面很容易被搜索引擎索引。
让我们从自己的 Next.js 应用开始,看看这些功能的实际应用。
入门指南
为了开始使用自己的 Next.js 应用,必须在系统上安装 Node.js。您必须在前一章练习 React.js 示例时安装它。如果没有,可以从 https://nodejs.org/
下载安装。安装完成后,您可以在编辑器中打开一个终端并运行“node -v”命令来检查 node.js 是否安装正确。如果是,终端将显示 Node.js 的安装版本号。我们将使用 npm(节点包管理器)来初始化我们的应用并安装我们项目的依赖项。npm 与 Node.js 捆绑在一起,如果您已经安装了 Node.js,它应该已经安装在您的系统中了。您可以在终端中执行“npm -v”命令。如果安装正确,此命令将显示系统中安装的 npm 版本。
我将使用可以从 https://code.visualstudio.com/download
下载的 Visual Studio 代码编辑器。但是,您可以使用自己选择的任何编辑器。
一旦完成安装,就可以为 Next.js 应用创建一个目录。我已经创建了一个名为“我的下一个应用”的目录。我们现在将从终端导航到这个新创建的目录,并运行“npm init”命令来创建 package.json 文件。运行这个命令时,您可能需要为 JSON 文件输入一些值,比如包名、版本、描述、git 存储库、关键字等等。您可以选择继续使用默认值,或者输入一些您自己的值。成功执行“npm init”命令后,您可能会注意到在目录中创建了一个 package.json 文件。它应该具有以下代码:
{
"name": "my-next-app",
"version": "1.0.0",
"description": "My Next.js Application",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" &&
exit 1"
},
"author": "Mohit Thakkar",
"license": "ISC"
}
如果您在初始化期间指定了一组不同的值,这些值可能会有所不同。现在,我们将使用以下命令安装应用的 next、react 和 react-dom:
npm install react react-dom next --save
“- save”命令将指示 npm 将已安装的软件包作为依赖项添加到 package.json 文件中。如果您检查该文件,将会向其中添加以下部分:
"dependencies": {
"next": "⁹.1.5",
"react": "¹⁶.12.0",
"react-dom": "¹⁶.12.0"
}
为了启动服务器,我们必须在 package.json 文件中指定启动脚本。让我们通过用以下代码替换文件中的脚本部分来添加它:
"scripts": {
"start": "next"
}
现在您已经指定了启动脚本,您可能想要启动服务器并启动您的应用。您可以使用“npm start”命令来完成此操作。但是,在这个时间点上,您将不能这样做,因为 Next.js 应用在应用启动时会在“pages”文件夹中查找启动页面。因为我们还没有一个“pages”文件夹,所以当我们试图启动我们的应用时,会得到一个编译时错误。让我们在应用的根目录下创建一个空的“pages”文件夹。创建之后,您可以使用“npm start”命令启动您的应用。当您导航到服务器上的应用 URL–http://localhost:3000/
时,您会注意到一个 404 错误。如图 3-1 所示。
图 3-1
Next.js 应用启动
请注意这个错误页面圆滑且用户友好的设计。这就是 Next.js 处理错误的方式。它还可以处理其他错误,如 500-内部服务器错误。404 错误的原因是我们的应用中没有页面。在应用启动时,Next.js 试图在页面中找到“index.js”文件,并在默认情况下呈现它。在我们的例子中,由于它无法找到它,我们会遇到一个 404 错误。现在让我们创建我们的第一页。使用以下代码将“index.js”文件添加到“pages”文件夹中:
Pages/index.js
import React from "react";
function MyComponent(){
return(
<div>Hello from Next.js!</div>
);
}
export default MyComponent;
现在,如果您使用“npm start
”命令启动服务器,并浏览到http://localhost:3000/
或http://localhost:3000/Index
,您将能够看到您刚刚创建的页面,如图 3-2 所示。
图 3-2
Next.js 的第一页
现在让我们测试内容是否真的在服务器端呈现。如果右键单击页面并查看页面源代码,您会注意到代码中生成的 HTML 内容直接填充到了根 HTML 标记中。这是因为一切都是在服务器端呈现的。如果对使用 plain React.js 构建的应用进行同样的操作,您将会注意到页面源代码中的根 HTML 标记,而不是代码生成的内容。这是因为,在 React.js 应用中,内容是在页面加载后在客户端呈现的。因此,我们可以确信 Next.js 会在服务器端呈现我们的页面。
在下一节中,我们将为我们的应用创建另一个页面,并了解如何使用 Next.js 路由在页面之间导航。
Next.js 中的路由
到目前为止,我们只有一个页面,但一个应用可能有多个页面,在这些页面之间轻松导航是任何应用的一个重要方面。让我们用下面的代码创建一个“关于”页面:
Pages/about.js
import React from "react";
function About(){
return(
<div>
This is an application built using next.js to demonstrate the effectiveness of server-side rendering!
</div>
);
}
export default About;
如果您运行应用并导航到http://localhost:3000/About
,您将看到您刚刚创建的页面,类似于图 3-3 。并不是说您不需要重新启动 npm 进程就可以看到变化。只要您保存了任何更改,Next.js 就会执行热重装,您无需重新启动服务器就可以看到这些更改。但是,您可能需要刷新浏览器页面。
图 3-3
Next.js 中的第二页
现在,为了在两个页面之间建立链接,您可能会想到创建一个锚标记,将页面 URL 传递给“href
”属性。让我们这样做,看看会发生什么:
Pages/index.js
import React from "react";
function MyComponent(){
return(
<div>
<p>Hello from Next.js!</p>
<a href='/About'>About</a>
</div>
);
}
export default MyComponent;
如果你运行这个应用,你会看到一个“关于”页面的链接。但是,如果您单击该链接,您会注意到整个页面被重新加载。这是因为锚标签向服务器发送新的请求,并且路由将发生在服务器端。这可能会导致性能问题,因此您可能希望保持到客户端的路由。
Next.js 提供了用于为客户端路由创建链接的<Link>
组件。它是一个可以与任何接受“onClick
”属性的组件一起工作的包装器。因此,我们将它与一个空的锚标记一起使用。考虑代码中的以下变化:
Pages/index.js
import React from "react";
import Link from 'next/link'
function MyComponent(){
return(
<div>
<p>Hello from Next.js!</p>
<Link href='/About'>
<a>About</a>
</Link>
</div>
);
}
export default MyComponent;
为了使用<Link>
组件,您必须首先从“下一个/链接”模块导入它。在执行前面的代码时,您将得到与图 3-4 相同的输出,但是当您单击链接时,您会注意到网络上没有生成额外的服务器请求。你可以在 Chrome 浏览器的开发者工具窗口的“网络”标签中验证这一点。这是因为客户端路由在这里起作用。将预取在<链接>组件中指定的页面,并且导航将在没有服务器请求的情况下发生。
图 3-4
Next.js 中的路由
大多数客户端路由场景都会破坏浏览器导航按钮。然而,Next.js 完全支持历史 API,所以它不会破坏你的浏览器导航按钮。
Note
历史 API 允许您与浏览器历史交互,触发浏览器导航方法,以及更改地址栏内容。这在单页应用中特别有用,在这种应用中,您永远不会真正改变页面,只是内容发生了变化。维护一个栈。每当用户在同一个网站中导航时,新页面的 URL 被放在栈的顶部。每当用户触发浏览器导航按钮时,就会调整栈的指针,并呈现适当的内容。
这就是 Next.js 中的路由。现在让我们看看 Next.js 中的动态页面。
动态页面
大多数实时应用都有动态生成的内容。因此,在实际场景中,我们不能依赖静态页面。让我们看看如何为我们的应用生成动态内容。首先,我们将创建一个文件 DynamicRouter.js,它将基于属性创建链接。考虑以下代码:
shared components/dynamic router . js
import React from "react";
import Link from 'next/link'
function GetLink(props) {
return (
<div>
<Link href=">
<a>{props.title}</a>
</Link>
</div>
);
}
export default GetLink;
Pages/index.js
import React from "react";
import GetLink from "../SharedComponents/DynamicRouter";
function MyComponent(){
return(
<div>
<GetLink title='Page 1'></GetLink>
<GetLink title='Page 2'></GetLink>
<GetLink title='Page 3'></GetLink>
</div>
);
}
export default MyComponent;
如果您看到浏览器窗口,您会注意到在索引页面上生成了三个链接,如图 3-5 所示。但是,这些是空链接,不会导航到任何页面。
图 3-5
Next.js 中的动态链接
现在让我们创建一个页面,它将根据接收到的参数动态显示内容。然后,我们将设置这三个链接,用不同的参数导航到这个动态页面。考虑新页面的以下代码:
页/秒。js
export default (props) => (
<h1>
Welcome to {props.url.query.content}
</h1>
);
shared components/dynamic router . js
...
<Link href={`/SecondPage?content=${props.title}`}>
<a>{props.title}</a>
</Link>
...
现在,如果你点击链接,你将被重定向到一个动态加载内容的页面,如图 3-6 所示。
图 3-6
Next.js 中的动态页面
如果您注意到生成的 URL,您会看到查询参数显示在地址栏上。您可能希望用户看到不显示查询参数的干净 URL。这在 Next.js 中可以通过使用“Link”组件的“as”属性来实现。传递给“as”属性的任何内容都将显示在地址栏中。让我们试试这个:
Pages/index.js
...
<GetLink title='Page 1' Disp='page-1'>
</GetLink>
<GetLink title='Page 2' Disp='page-2'>
</GetLink>
<GetLink title='Page 3' Disp='page-3'>
</GetLink>
...
shared components/dynamic router . js
...
<Link href={`/SecondPage?content=${props.title}`}
as={props.Disp}>
<a>{props.title}</a>
</Link>
...
现在,如果你点击链接并导航到其中一个页面,你会看到一个没有任何参数的干净的 URL,如图 3-7 所示。
图 3-7
Next.js 页面的自定义 URL
这就是 Next.js 中的动态页面。现在让我们学习如何在 Next.js 应用中处理多媒体内容。
使用 CSS 添加多媒体内容
有时,您可能希望在应用中添加多媒体内容,如图像和视频。一般来说,最好在 CSS 本身中添加这些内容的 URL,以便于维护。让我们在索引页面的链接旁边添加图片。我已经下载了三张图片,并将它们添加到应用根目录下的“static/Images”文件夹中。js 提供了一个叫做 JSS(JS 中的 CSS)的东西,它允许我们在 JSX 代码中直接定义样式。让我们将以下代码添加到“index.js”文件中,以便使用 CSS 添加图像:
index.js
...
return (
<div>
...
<style jsx global>
{`
a{
color:blue;
}
.img{
height: 50px;
width: 50px;
background-size: cover!important;
background-repeat: no-repeat!important;
background-position: center!important;
border: 1px solid black;
border-radius: 10px;
display: inline-block;
margin-top: 10px;
}
.p1{
background: url(../statimg/1.jpg);
}
.p2{
background: url(../statimg/2.jpg);
}
.p3{
background: url(../statimg/3.jpg);
}
`}
</style>
</div>
);
...
既然我们已经定义了样式,我们可能想在我们的页面上使用这些样式。为此,我们需要将“index.js”文件中的类名作为属性传递给<Link>
组件,并在“DynamicRouter.js
”文件中使用它来为图像创建一个<div>
,并为其设置类名。请考虑以下代码更改:
Index.js
...
<GetLink title='Page 1'
Disp='page-1'
Class='img p1'>
</GetLink>
<GetLink title='Page 2'
Disp='page-2'
Class='img p2'>
</GetLink>
<GetLink title='Page 3'
Disp='page-3'
Class='img p3'>
</GetLink>
...
DynamicRouter.js
...
return (
<div>
<div className={props.Class}></div>
<Link
href={`/SecondPage?content=${props.title}`}
as={props.Disp}>
<a>{props.title}</a>
</Link>
</div>
);
...
如果保存更改,您将看到类似于图 3-8 的浏览器输出。
图 3-8
Next.js 中的多媒体内容
您可能希望为所有样式创建一个单独的 CSS 文件。但是,您不能在 Next.js 应用中直接这样做。你必须首先安装一个 CSS 加载器。您可以使用以下命令将@zeit/next-css 模块安装到您的应用中:
npm install @zeit/next-css --save
安装后,您必须使用以下代码将配置文件“next.config.js
”添加到应用的根目录中:
Next.config.js
const withCSS = require('@zeit/next-css')
module.exports = withCSS({})
现在,您必须在应用的根目录下创建一个文件“style.css
”。然后可以从“index.js”文件中删除<style jsx global>
组件,并将样式表代码移动到“style.css”文件中,如下所示:
a{
color:blue;
}
.img{
height: 50px;
width: 50px;
background-size: cover!important;
background-repeat: no-repeat!important;
background-position: center!important;
border: 1px solid black;
border-radius: 10px;
display: inline-block;
margin-top: 10px;
}
.p1{
background: url(/statimg/1.jpg);
}
.p2{
background: url(/statimg/2.jpg);
}
.p3{
background: url(/statimg/3.jpg);
}
请注意,重要的是,您的图像放置在“静态”文件夹中,该文件夹在您的应用中与“页面”文件夹处于同一级别。在您的“index.js”文件中,您将能够使用以下代码行像导入任何其他文件一样导入 CSS 文件:
import "../style.css";
如果保存更改并转到浏览器窗口,您将看到类似于图 3-8 的输出。包括视频在内的所有其他多媒体内容都可以以类似的方式呈现到您的应用中。现在让我们看看如何在 Next.js 应用中从远程服务器获取数据。
从远程服务器获取数据
您可能还记得,在前一章中,我们使用 Axios 库来执行 AJAX 请求,以便从远程端点获取数据。我们将在 Next.js 应用中做同样的事情。这里的区别是 AJAX 调用将在服务器端执行,而不是在客户端。首先,我们将使用以下命令将 Axios 库安装到我们的项目中:
$ npm install axios
一旦安装完毕,我们可以在页面中使用它的get()
方法从远程端点获取数据。然而,在 Next.js 应用中,事情会有一些变化。之前,我们在组件的componentDidMount()
方法中执行了 AJAX 调用。但是在这种情况下,我们将使用由 Next.js 提供的特殊方法getInitialProps()
,它帮助我们设置组件的属性。我们将在getInitialProps()
方法中启动我们的 Axios 请求。考虑下面的页面,它使用 GitHub 的公共 API 来获取 GitHub 用户列表,并在我们的应用中显示这些用户:
Pages/GithubUsers.js
import React from 'react'
import axios from 'axios';
import '../style.css';
export default class extends React.Component {
static async getInitialProps() {
try {
const res = await axios.get('https://api.github.com/users');
return { data: res.data }
}catch(e){
return {error:e}
}
}
render() {
if (this.props.error) {
return (
<div>
Error: {this.props.error.message}
</div>
);
}
else {
return (
<div>
<h1>Github Users</h1>
<br />
{this.props.data.map((item, index) => (
<div key={index}
className='UserBlock'>
<img src={item.avatar_url}
alt='User Icon'>
</img>
<div className="UserDetails">
<p>Username: {item.login}</p>
<p>ID: {item.id}</p>
</div>
</div>
))}
</div>
);
}
}
}
因为 Axios 请求是异步的,所以我们需要一种方法来捕捉可用的响应。之前,我们使用 Axios 库的get()
方法的then()
扩展方法来完成这项工作。这一次,我们在 Axios 方法调用中使用了 await 关键字,并将getInitialProps()
方法标记为异步。async…await 关键字帮助我们处理异步请求,而不必使用回调或承诺,并且对于我们的应用来说很方便。我们将这个请求包装在一个 try…catch 块中,这样我们就可以知道网络上是否发生了错误。一旦请求被处理,我们从getInitialProps()
方法返回一个包含数据或错误的对象。此方法返回的对象将被设置为 props 对象。在 render 方法中,我们通过使用“this.props.error
”检查 error 属性来检查错误是否存在。如果是这样,用户将在浏览器上看到一条错误消息,类似于图 3-9 。
图 3-9
Axios 请求中的错误
如果没有错误,那么我们将使用 JavaScript 的array.map()
方法迭代“this.props.data
”对象,并在浏览器上显示 GitHub 用户的详细信息。输出应该类似于图 3-10 。
图 3-10
成功的 Axios 请求
如果您的输出与图 3-10 中的略有不同,不要担心。您的代码中还缺少一点。我已经在我们之前创建的“style.css”文件中添加了一些样式,并将其导入到我们的页面中。照着做,你就能很好地搭配这个造型了。可以参考下面的样式表代码:
style.css
body {
font-family: sans-serif;
}
body div h1 {
text-align: center;
border-bottom: 1px solid grey;
}
img {
height: 50px;
width: 50px;
border: 1px solid black;
}
.UserBlock {
display: inline-block;
border: 1px solid black;
border-radius: 5px;
padding: 10px;
margin: 15px;
width: 255px;
}
.UserDetails {
display: inline-block;
margin-left: 15px;
}
.error {
color: red;
font-weight: bold;
font-size: 26px;
text-align: center;
}
就这样。您现在可以从任何远程端点获取数据,并在您的应用中使用它。
使用 Next.js 创建交互式应用
让我们尝试在应用中添加一些用户交互。我们将从浏览器获得作为文本输入的 GitHub 用户 id,并显示该特定 GitHub 用户的详细信息。为此,我们需要从 props 获取初始数据,并使用构造函数将其设置在我们的状态对象中,就像我们在前一章中对传统的 React 应用所做的那样。这里唯一的区别就是属性会来自getInitialProps()
法。
Note
我们需要将属性转移到状态对象,因为属性对象是不可编辑的,因此,我们不能直接使用它进行数据操作。
当从浏览器输入一个 id 时,我们将进行一个 API 调用来获取用户详细信息并修改状态对象中的数据。状态对象一改变,React 就会重新渲染 UI。考虑以下代码:
Pages/GithubUsers.js
import React from 'react';
import axios from 'axios';
import '../style.css';
export default class extends React.Component {
static async getInitialProps() {
try {
const res = await axios.get('https://api.github.com/users');
return { data: res.data }
} catch (e) {
return { error: e }
}
}
constructor(props) {
super(props);
this.state = {data: props.data,
error: props.error };
}
GetUser = async () =>
{
try {
const res = await axios.get('https://api.github.com/users/' + document.getElementById('inputTextbox').value);
this.setState({
data: [res.data],
error: null
});
} catch (e) {
this.setState({
data: null,
error: e
});
}
}
render() {
if (this.state.error) {
return(
<div>
<h1>Github Users</h1>
<br />
<div className="center">
<input id="inputTextbox" type="text">
</input>
<button type="button"
onClick={this.GetUser}>
Get User
</button>
</div>
<br />
<p className="error">
Error: {this.state.error.message}</p>
</div>
);
}
else {
return (
<div>
<h1>Github Users</h1>
<br />
<div className="center">
<input id="inputTextbox" type="text">
</input>
<button type="button"
onClick={this.GetUser}>
Get User
</button>
</div>
<br />
{this.state.data.map((item, index) => (
<div key={index} className="UserBlock">
<img src={item.avatar_url}
alt='User Icon'></img>
<div className="UserDetails">
<p>Username: {item.login}</p>
<p>ID: {item.id}</p>
</div>
</div>
))}
</div>
);
}
}
}
下面是我们在前面的代码中所做的事情的列表:
-
getInitialProps()
方法获取 Github 用户的初始列表,并返回设置为页面属性的数据。这些属性可以使用“this.props
”访问,并且不可编辑。 -
方法用作为 props 传递的值初始化状态对象。每当我们获取特定用户请求的 GitHub 用户详细信息时,这个状态对象就会更新。
-
GetUser()
方法处理按钮的 click 事件,并在每次用户请求特定 GitHub 用户的详细信息时进行 API 调用。GitHub 的用户 id 从输入框中获取,并作为参数发送给 API 调用。用 API 调用返回的数据更新状态对象。状态对象一更新,React 就会重新渲染视图。 -
方法检查状态对象,如果请求成功,则显示用户详细信息,如果请求中有错误,则显示错误消息。
如果您导航到浏览器上的“GithubUsers”页面并搜索有效用户,您将看到该用户的详细信息,如图 3-11 所示。
图 3-11
交互式 Next.js 应用
如果您搜索一个不存在的用户,您将看到如图 3-12 所示的错误信息。您可以根据自己的喜好定制错误消息。
图 3-12
找不到 GitHub 用户
就是这样。我们刚刚创建了一个交互式应用,它使用 Next.js 和 React.js 在服务器端呈现内容。
对 Next.js 使用 Redux
市场上的大规模应用大多使用 MVC 架构进行状态管理。然而,在使用客户端库构建的应用中实现 MVC 是一项痛苦的任务,因为与传统 MVC 中的中心模型不同,客户端应用中的状态分散在页面上,而不是在应用级别。为了在客户端库中实现 MVC 风格的状态管理,我们使用 Redux。Redux 的架构如图 3-13 所示。
图 3-13
Redux 架构
为了理解 Redux 架构,您需要知道以下内容:
-
视图触发一个动作,该动作在 Reducers 的帮助下更新存储。然后,存储隐式地将更新后的数据发送回视图。
-
动作将信息传递给缩减器,然后缩减器根据收到的信息决定在存储中更新什么数据。
-
商店可以被视为应用级别的状态对象。此对象中的更改将触发视图更新。
-
动作是特殊的方法,每当视图中的变化触发这些方法时,它们就更新应用状态。
-
与传统的 MVC 模式不同,这里的数据流是单向的。这意味着商店不能触发任何操作。只有视图可以触发操作。这大大降低了无限循环的可能性。
当我们开始用一个例子工作时,你会更好地理解它。让我们看看 Redux 的三个基本原则:
-
单一事实来源–整个应用的状态驻留在单一存储对象中。
-
状态是只读的–改变状态的唯一方法是触发一个动作。视图没有直接更新状态的权限。它们触发一个动作,告诉 Reducer 更新状态。这确保了对状态的所有更改都集中地一个接一个地发生,以便出于调试目的可以跟踪它们。
-
用纯函数进行修改–为了指定状态如何被动作修改,编写了纯 Reducers。这些函数将当前状态和动作作为输入,并将下一个状态作为输出返回。还记得我们在上一章学习的作为 React 基本概念的纯度概念吗?这正是减肥药所坚持的。因为它们是纯函数,所以要确保它们返回一个新的状态对象,而不是修改现有的状态对象。
Note
纯函数从不修改输入参数的值。而是在每次被调用时返回一个新的对象。此外,无论您调用一个 pure 函数多少次,对于相同的输入参数集,它总是返回相同的输出。最后,一个纯函数只依赖于它的输入参数,从不修改它范围之外的任何东西。
让我们更详细地了解一下存储、归约器和动作。
商店
Store 是存储整个应用状态的对象。如前所述,它是“真理的唯一来源”。使用以下代码片段可以轻松创建商店:
import {createStore} from 'redux';
import reducer from 'reducer';
const store = createStore(reducer);
以下是 Store 对象提供的一些方法:
-
store . getstate()–该方法返回当前状态。
-
store . dispatch(action)–通过调度动作来更新状态。将使用当前状态和动作调用与商店相关联的 Reducer 函数。它的返回值将被认为是下一个状态。一旦状态改变,改变监听器也将被立即通知。
-
store . subscribe(listener)–用于向状态添加一个更改监听器。您可以将函数作为参数传递。每次调度动作时都会调用这个函数。您可以在侦听器中使用 getState()方法来获取更新后的状态值。
-
unsubscribe()–当状态改变时,如果不想再调用监听器方法,就使用这个方法。当您订阅侦听器时会返回此方法,因此您可能希望在订阅期间将它保存在一个变量中,以便能够取消订阅。考虑下面的代码片段:
// Subscribing
const unsubscribe = store.subscribe(someListener);
// Unsubscribing
unsubscribe();
行动
动作是将数据从应用发送到商店的信息负载。它们是普通的 JavaScript 对象,包含一个类型和一个可选的有效载荷。他们是商店唯一的信息来源。下面的代码片段演示了如何创建和调度一个操作:
...
const action = {
type: 'Multiply',
payload: { value: 10 },
};
store.dispatch(action)
Redux 没有严格的规则集来定义您的操作。这意味着,除了“type
”属性之外,你如何构造你的动作完全取决于你自己。您可以直接在 Action 对象中定义“value
属性,而不是在“payload
属性中定义。事实上,您可以定义自己的属性和值。但是在“payload
”属性中定义您的所有属性是推荐的方法之一。
还原剂
Reducers 是指定状态如何根据由动作分派的信息的类型和有效负载而改变的函数。这些函数将当前状态和动作作为输入参数,在处理信息后生成一个新状态,并将这个新状态作为输出返回。现在,即使我们整个应用的状态是一个单一的 Store 对象,我们也可能想要编写多个 Reducers 来修改这个对象。Redux 为您提供了这样做的灵活性。您可以为每个场景编写一个小的缩减器,而不是编写一个处理所有场景的缩减器函数。这将帮助我们最小化代码的复杂性。
Note
因为所有的动作都是顺序执行的,所以我们永远不会面临多个 reducers 试图同时修改状态的情况。
以下是返回初始状态的示例 Reducer 函数的代码片段:
function sampleReducer(state, action) {
return state
}
让我们创建一个基本的例子来理解 Redux 的概念。首先,我们将使用以下命令将 redux 安装到我们的应用中:
npm install redux --save
完成后,我们将创建两个单独的文件夹——“Actions”和“Reducers”。让我们修改“pages”文件夹中的“index.js”文件。我们将有一个输入框,一个按钮,和一个标签。单击按钮时,应该用输入框中的值更新状态,标签应该用状态值更新自身。将为状态设置一些初始值。考虑以下代码:
Pages/index.js
import React from "react";
import '../style.css';
export default class extends React.Component {
static async getInitialProps() {
return { text: 'Initial label value.' }
}
constructor(props) {
super(props);
this.state = { text: props.text };
}
render() {
return(
<div>
<h1>Redux Demo</h1>
<br />
<div className="center">
<input id="inputTextbox" type="text">
</input>
<button type="button"
onClick={this.GetUser}>
Update Label
</button>
</div>
<br />
<p>{this.state.text}</p>
</div>
);
}
}
如果您在浏览器中访问该页面,您将看到类似于图 3-14 的输出。
图 3-14
Redux 演示
请注意,我们刚刚创建了一个带有标签使用的初始状态值的页面。我们使用getInitialProps()
方法传递一个静态字符串作为属性。在构造函数中,我们获取这个属性的值,并将其设置为状态。请注意,我们没有编写任何更改更新状态的逻辑。我们将使用 Redux 来实现。但是在我们这样做之前,我们必须安装一些依赖项来帮助我们。使用以下命令执行相同的操作:
npm install redux react-redux next-redux-wrapper redux-thunk redux-devtools-extension --save
我们总共安装了五个新的依赖项。我们已经拥有的其他依赖项有react
、react-dom
、next
、axios
和@zeit/next-css
。安装新的依赖项后,我的“package.json”如下所示:
package.json
{
"name": "my-next-app",
"version": "1.0.0",
"description": "My Next.js Application",
"main": "index.js",
"scripts": {
"start": "next"
},
"author": "Mohit Thakkar",
"license": "ISC",
"dependencies": {
"@zeit/next-css": "¹.0.1",
"axios": "⁰.19.0",
"next": "⁹.1.6",
"next-redux-wrapper": "⁴.0.1",
"react": "¹⁶.12.0",
"react-dom": "¹⁶.12.0",
"react-redux": "⁷.1.3",
"redux": "⁴.0.5",
"redux-devtools-extension": "².13.8",
"redux-thunk": "².3.0"
}
}
如果您正在从头开始创建一个新的应用,请确保您更新了前面代码片段中提到的“package.json ”,并从终端运行“npm install
”命令来更新依赖项。是时候创建我们的第一个动作了。我在应用的根目录下创建了一个“Actions”文件夹,其中包含我们的操作。考虑以下代码:
Actions/**Actions . js
export const InitialState = {
text: 'Initial label value.'
}
export const changeState = () => dispatch => {
return dispatch({
type:'ChangeLabel',
text: document.getElementById('inputTextbox').value
})
}
这里,我们定义了一个InitialState
,它是一个普通的 JavaScript 对象,以及一个在点击按钮时用输入文本更新状态值的动作。我们将在 Reducer 函数中直接使用InitialState
对象,在第一次创建商店时也是如此。我们稍后会看到。然而,为了更新状态,我们必须向 Reducer 发送一个动作。为此,我们创建了一个方法,changeState()
,它将在按钮点击时被调用。这个方法调度我们的动作。
对于我们正在调度的操作,我们已经定义了一个强制的"type
"属性,它决定了正在执行的操作的类型,还定义了一个"text
"属性,它将新数据发送到 reducer。是时候创建我们的 Reducer 函数了,它将基于从动作接收到的数据来更新存储。我在应用的根目录下创建了一个“Reducers”文件夹,其中包含了我们的 reducer。考虑以下代码:
减速器/ 减速器. js
import { InitialState } from '../Actions/actions'
export const reducer = (state = InitialState, action) => {
if (action.type == 'ChangeLabel') {
return Object.assign({}, state, {
text: action.text
})
}
else {
return state;
}
}
如前所述,减速器接受两个输入参数——当前状态和动作。在定义我们的缩减器时,我们将“InitialState
”指定为第一个输入参数 state 的默认值。如果 reducer 在空状态下被触发,我们的“InitialState
”对象中定义的初始状态值将被设置为该状态。
Note
“InitialState”是从我们的操作文件中导入的,它与我们之前创建的对象相同。
如果动作的类型是“ChangeLabel
”,reducer 将知道状态值需要用动作分派的数据来更新。在这种情况下,Reducer 函数创建一个新的对象,将当前状态值赋给该对象,并用动作分派的新值替换“text”属性的值。这个新对象将被 Reducer 返回,并被视为应用的新状态。React 将在检测到状态变化时自动更新视图。因此,一旦执行了 Reducer,视图就会反映状态的变化。我们没有定义任何其他动作,所以如果动作的类型不是“ChangeLabel
”,我们将只返回收到的状态对象。现在是时候编写第一次创建我们的商店的代码了。我在应用的根目录下创建了一个“Store
”文件夹,其中包含我们的商店初始化代码。考虑以下代码:
Store/??【Store . js
import { createStore, applyMiddleware } from 'redux'
import thunkMiddleware from 'redux-thunk'
import { reducer } from '../Reducers/reducer'
import { InitialState } from '../Actions/actions.js'
export const initStore = (initialState = InitialState) => {
return createStore(
reducer,
initialState,
applyMiddleware(thunkMiddleware)
)
}
这里,我们再次使用了在我们的 Actions 文件中创建的“InitialState
”对象,这一次,当第一次创建存储时,用初始应用状态初始化存储。createStore()
是由“redux”库提供的方法,以便第一次创建和初始化 Redux 存储。
Note
你的应用中应该只有一个商店。
我们将以下三个参数传递给createStore()
方法:
-
reducer (Function)
–这是我们为商店创建的减压器功能。给定当前状态和一个操作,它返回下一个应用状态。 -
这是我们应用的初始状态。它是我们在动作文件中创建的普通 JavaScript 对象。
-
enhancer (Function)
–您可以选择指定一些使用第三方代码的功能来增强您的应用。在我们的例子中,我们使用了“redux-thunk”库提供的“thunkMiddleware”。这个中间件帮助我们编写与存储交互的异步逻辑。对于没有这个中间件的基本 Redux 存储,我们将只能通过分派一个动作来执行对存储的同步更新。我们将使用“redux”库提供的 applyMiddleware()将“thunkMiddleware”转换为增强器。
请注意,商店尚未创建。我们刚刚定义并导出了“initStore
”函数,可以调用该函数来创建商店。
您一定已经注意到,我们已经创建了使用 Redux 执行状态管理所需的一切。是时候在我们的应用生命周期中注入 Redux 功能了。为此,我们将不得不使用“react-redux
”库,它是我们之前作为依赖项之一安装的。这是 Redux 的官方 React 绑定。它帮助 React 组件从 Redux 存储中读取数据,并将操作分派给存储以更新数据。因为我们需要我们的状态对象在整个应用中都可用,所以我们必须将它注入到一个组件中,其余的组件都是从这个组件继承的。该父组件可以被称为高阶组件(HOC)。在 Next.js 中,我们可以创建一个特殊的组件“_App.js
”,它包装了所有的页面,并可用于共享整个应用中常见的内容。我们将使用这个“_App
”组件向应用生命周期注入 Redux。使用以下代码将"_App.js"
文件添加到“Pages”文件夹中:
页/_App.js
import React from 'react'
import { Provider } from 'react-redux'
import App from 'next/app'
import withRedux from 'next-redux-wrapper'
import { initStore } from '../Store/store'
export default withRedux(initStore)(
class MyApp extends App {
static async getInitialProps({ Component, ctx }){
return {
pageProps: Component.getInitialProps
? await Component.getInitialProps(ctx)
: {},
}
}
render() {
const { Component, pageProps, store } = this.props
return (
<Provider store={store}>
<Component {...pageProps} />
</Provider>
)
}
}
)
这里,我们已经创建了一个组件,它被包装在一个特殊的withRedux()()
包装器中,这个包装器是由“next-redux-wrapper”库提供的,我们之前把它作为一个依赖项安装的。作为这个包装器的第一个输入,我们传递创建初始存储的方法,在我们的例子中是来自“??”的“??”。这个包装器的第二个输入是我们的高阶组件(HOC)。
该组件从“下一个”库提供的“应用”组件扩展而来。这是 Next.js 用来初始化页面的组件。因为我们覆盖了页面的默认初始化,所以我们必须在组件中编写getInitialProps()
方法,并让它调用页面的getInitialProps()
方法。它接受两个参数——“组件”和“ctx”。“组件”是页面组件,“ctx”是上下文。如果页面的getInitialProps()
方法返回任何数据,我们从我们的 HOC 的getInitialProps()
方法返回该数据,否则我们返回一个空对象。
然后我们写我们的 HOC 的render
()方法。在 Props 对象中我们已经有了 Component 和 PageProps。Component 是正在呈现的页面的页面组件,PageProps 是我们在该页面中拥有的属性。因为我们将我们的 HOC 封装在一个 Redux 包装器中,所以当执行getInitialProps()
方法时,Store 对象也被创建并传递给render()
方法。我们已经指定了创建初始存储的方法。同样的将被用来创建商店,并通过它到特设属性。我们将使用析构语法从 Props 对象中获取 Component、pageProps 和 store 的值。
我们将使用“react-redux
”库提供的<Provider>
组件来包装我们的页面组件。我们将把 Store 对象传递给这个组件,它将对我们所有的容器组件可用。在这个组件中,我们将放置页面组件,并将页面属性传递给它。页面组件将根据所呈现的页面动态地保持变化。
这就是如何使用高阶组件(HOC)将 Redux 注入下一个. js 生命周期的方法。现在让我们在索引页面中使用我们的商店,并在点击按钮时发送一个动作。您必须对“index.js”文件进行以下修改:
Pages/index.js
import React from "react";
import "../style.css";
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { changeState } from '../Actions/actions'
class ReduxDemo extends React.Component {
render() {
return (
<div>
<h1>Redux Demo</h1>
<br />
<div className="center">
<input id="inputTextbox" type="text">
</input>
<button type="button"
onClick={this.props.changeState}>
Update Label
</button>
</div>
<br />
<p>{this.props.text}</p>
</div>
);
}
}
const mapDispatchToProps = dispatch => {
return {
changeState: bindActionCreators(changeState, dispatch)
}
}
export default connect((state) => ({ text: state.text }), mapDispatchToProps)(ReduxDemo)
你一定已经注意到我们已经从代码中移除了getInitialProps()
方法。这是因为属性现在将使用由“react-redux
”库提供的特殊“connect()()
”包装器来注入。第一个参数包含要注入到页面中的与状态相关的实体,第二个参数是页面本身。我们正在注入驻留在状态对象中的文本属性。我们还注入了用于调度修改 Store 对象的操作的方法。我们通过使用“this.props.changeState
”在页面中访问按钮,将该方法传递给按钮的 click 事件。我们还使用“this.props.text
”将标签(
)绑定到“
”、“状态对象的属性”。如果您执行应用并访问浏览器,您将看到类似于图 3-14 的输出。以下是运行应用时发生的事情的顺序列表:
-
当浏览器第一次请求页面时,
"_App.js"
中编写的代码被执行。 -
用初始值创建商店对象,并将其注入页面。
initStore()
首次使用方法创建商店。我们已在“Store/store.js
”中定义了该方法,并在特设中提供了参考。 -
"_App.js"
中的 HOC 呈现页面组件。现在控制转移到“Pages/index.js”文件中编写的代码。 -
"
connect()()
" wrapper 通过 props 将状态数据注入页面。更新状态值的方法也作为 props 传递。然后,页面就像普通的 Next.js 页面一样呈现出来。您将看到标签上显示的初始状态值。 -
只要你输入一些文本并点击按钮,就会调用“Actions/action.js”中定义的
changeState()
方法。 -
该方法将使用您在输入框中输入的文本作为数据来调度类型为"
ChangeLabel
"的操作。 -
现在,控制将被转移到写在“
Reducers/reducer.js
”的 Reducer 方法。在检查了动作的类型之后,Reducer 将使用动作分派的数据更新 State 中的“text”属性。 -
一旦状态对象被更改,React 将重新呈现视图,并更新其数据已被修改的所有字段。因此,UI 上绑定到状态的“text”属性的标签将被重新呈现,您将在 UI 上看到更新后的值。
注意,我们没有在页面中的任何地方直接使用 React 的内置状态对象。这里所有的状态管理都是由 Redux 完成的。这就是关于 Redux 的工作。现在让我们了解一下 GraphQL,以及如何在 Next.js 应用中使用它。
将 GraphQL 与 Next.js 一起使用
GraphQL 是 API 的查询语言。它给了客户确切地提出要求的权力。我们可以向 API 发送一个 GraphQL 查询,并向服务器传达我们在响应中需要的确切字段。请看图 3-15 以便更好地理解。
图 3-15
GraphQL 查询变量
图 3-15 完美的描绘了 GraphQL 的概念。一个 API 可能返回多个参数,但是我们的应用可能不需要所有这些参数。在这种情况下,我们可以发送我们需要的参数名作为查询变量,API 将返回同样多的参数。
为了理解工作中的 GraphQL,我们必须首先在我们的应用中创建一个常规 API 并使用它。然后我们将看到如何将 GraphQL 与该 API 一起使用。Next.js 为在应用中构建 API 提供了一个简单的解决方案。“Pages/api”文件夹中的所有文件都被视为 api 端点,而不是页面,可以在“/API/∫”处使用。为了让 API 工作,您必须从您的文件中导出一个请求处理程序,它只是一个接受以下两个参数的函数:
-
req–传入请求的一个实例。您可以使用这个对象来标识请求类型、输入参数、请求头和生成请求的 URL 等。
-
RES–传出响应的实例。您可以使用此参数设置响应的状态代码、标头和数据。
让我们从一个只有索引页面的基本 Next.js 应用开始。我们将创建“Pages/api”文件夹,并添加我们的第一个 API“testapi . js”文件,代码如下:
页/API/test pi . js
const data = {
name: 'Jhon Doe',
address: '7th Avenue, Brooklyn',
contact: '099251456',
bloodgroup: 'A +ve',
favouriteSnack: 'Hotdog',
vehicle: 'Hyundai Tucson'
}
export default (req, res) => {
res.statusCode = 200
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify(data))
}
我们在这里做的是定义一个静态数据对象并发送它作为响应。我们在响应的end()
方法中传递这些数据。这个方法向服务器发出信号,表明已经设置了响应头和响应体,服务器应该认为这个响应是完整的。我们可以通过在浏览器中访问 URL“http://localhost:*/api/testapi
”来使用这个 API。或者,我们可以在索引页面中使用这个 API。让我们使用下面的代码来实现它:
页/ 索引. js
import React from "react";
import axios from 'axios';
import "../style.css";
export default class extends React.Component {
static async getInitialProps() {
try {
const res = await axios.get('http://localhost:3000/api/testapi');
return { data: res.data, error: null }
} catch (e) {
return { data: ", error: e }
}
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<table>
{Object.keys(this.props.data).map((key, index) => (
<tr key={index}>
<td>{key}:</td>
<td>{this.props.data[key]}</td>
</tr>
))}
</table>
</div>
);
}
}
我们使用页面的 getInitialProps()方法中的 Axios 库向 API 发出请求。然后,我们将 API 响应作为属性发送到页面的其余部分。在 render 方法中,我们迭代数据并将其呈现给浏览器。很简单。我们已经创建了一个 API 并使用它。您将看到类似于图 3-16 的输出。
图 3-16
Next.js API 响应
现在让我们考虑一个场景,我们只需要 API 中的姓名和地址字段。目前,您必须从 API 获取所有数据,然后在消费端缩小所需的字段。然而,使用 GraphQL 有一种更好的方法来处理这种情况。让我们使用以下命令将 GraphQL 库安装到我们的应用中:
npm install graphql --save
我们现在必须稍微修改一下我们的 API。我们将为我们的数据定义一个模式。该模式将指定我们将从 API 返回的数据类型。然后,我们将模式、GraphQL 查询和数据对象传递给 GraphQL,graph QL 将根据收到的查询为我们过滤数据。考虑以下 API 代码:
页/API/**test pi . js
import { graphql, buildSchema } from 'graphql'
const schema = buildSchema(`
type Query {
name: String,
address: String,
contact: String,
bloodgroup: String,
favouriteSnack: String,
vehicle: String
}
`);
const data = {
name: 'Jhon Doe',
address: '7th Avenue, Brooklyn',
contact: '099251456',
bloodgroup: 'A +ve',
favouriteSnack: 'Hotdog',
vehicle: 'Hyundai Tucson'
}
export default async (req, res) => {
const response = await graphql(schema, req.body.query, data);
res.end(JSON.stringify(response.data))
}
是 GraphQL 提供的帮助我们构建模式的方法。它是我们的数据对象中的每个属性与其对应的数据类型的映射。我们将从请求中获取 GraphQL 查询。然后,我们将模式、查询和数据传递给 GraphQL,并等待过滤后的响应。最后,我们将把响应发送给用户。为了从消费端使用 GraphQL,您需要做的就是在您的 Axios 请求中添加一个“query”参数。将索引页中的 Axios 调用替换为以下内容:
...
const res = await axios.get('http://localhost:3000/api/testapi', { data: { query: `{ name, address }` }});
...
如果在所有的更改之后,您访问我们的应用的索引页面,您将看到类似于图 3-17 的输出。您会注意到只显示了两个字段,而不是之前显示的所有字段。这就是 GraphQL 在我们的应用中的作用。
图 3-17
GraphQL API 响应
这就是 GraphQL。随着这个话题的结束,我们来到本章的结尾。让我们总结一下我们所学到的东西。
摘要
-
Next.js 是一个帮助我们在服务器端呈现应用的框架。
-
它解决了诸如加载时间长、索引能力差以及与客户端渲染相关的安全漏洞等问题。
-
它提供了热重载、基于页面的路由、自动代码分割、页面预取、热模块替换和服务器端呈现等功能。
-
Next.js 提供了组件,帮助我们向应用添加链接。当我们导航到这样的链接时,不会对资源发出额外的服务器请求。这要归功于 Next.js 的客户端路由功能。
-
由于使用了历史 API,Next.js 中的客户端路由不会破坏浏览器的后退按钮。
-
我们应该在应用根目录的“static”文件夹中添加媒体文件。向我们的应用添加多媒体内容的最佳方法是在 CSS 文件中添加这些内容的 URL。
-
js 提供了 JSS(JS 中的 CSS ),允许我们直接在 JSX 代码中定义样式。我们将不得不在我们的应用中使用@zeit/next-css 库(或任何其他 css 加载器)来在单独的 CSS 文件中编写样式代码。
-
我们可以在页面中使用 getInitialProps()方法将属性传递给组件。我们可以在这个方法中调用 Axios API 来从远程服务器获取页面数据。作为 props 传递的数据可用于初始化构造函数中的状态对象。
-
Redux 可以在 Next.js 应用中使用,以便在应用级别管理状态。它使用动作、缩减器和存储来模仿 MVC 架构。
-
我们使用高阶组件(HOC)将 Redux 注入下一个. js 生命周期。
-
可以在 Next.js APIs 中使用 GraphQL,为客户端提供查询响应中所需字段的能力。
四、向 React 应用添加服务器端呈现
在前一章中,我们学习了如何使用 Next.js 框架创建服务器端应用。但是,我们可能希望创建一个部分在客户端呈现,部分在服务器端呈现的应用,这样我们就可以利用客户端呈现和服务器端呈现的优势。在本章中,我们将创建一个客户端渲染的 React 应用,并学习如何使用 Next.js 框架将服务器端渲染集成到应用中。在这个过程中,我们还将学习服务器端渲染的重要性,设计我们的应用,为我们的应用添加引导程序,以及其他一些主题。大多数情况下,我们将使用我们之前已经学过的东西来创建一个功能应用。
服务器端渲染的重要性
在前一章中,我们讨论了与客户端渲染相关的问题。在单页应用(SPA)的开发过程中面临的主要问题是。尽管 spa 提供了惊人的用户体验,但所有这些都是基于浏览器的。用户停留在同一个页面上,而不是从一个页面导航到另一个页面,并且页面上的内容基于用户交互而动态变化。这些更改是由浏览器使用客户端编写的 JavaScript 代码完成的。
虽然这是在应用启动并运行后更改页面的一个很好的方法,但对于第一次加载应用来说,这不是一个推荐的方法。在 SPA 可供用户交互之前,浏览器需要进行大量处理。此外,web 服务器和用户浏览器之间需要多次交互。正如您在图 4-1 中看到的,当用户第一次访问应用时,一个请求被发送到 web 服务器以获取页面。服务器返回一个 HTML 页面,其中包含一个空的根元素(< div >)和一些 JavaScript 代码。然后,浏览器发送更多对数据、样式表、脚本文件和其他可能需要的资源的请求。一旦所有的资源对浏览器可用,它就处理 JavaScript 代码,以确保 JSX 代码被正确编译,JSON 数据使用 REST API 调用被加载,所有的事件被绑定,承诺被履行。只有这样,用户才可以使用该页面。
图 4-1
单页应用的客户端呈现
Note
不要混淆用于客户端渲染的服务器和用于服务器端渲染的服务器。这里使用的 web 服务器可以称为“瘦服务器”。这是因为,在客户端呈现的情况下,所有的逻辑都是以 JavaScript 代码的形式编写的,这些代码将由浏览器处理。服务器充当纯数据 API,只是将 JavaScript 代码交付给浏览器。另一方面,在服务器端呈现的情况下,服务器处理所有的逻辑,并向浏览器提供一个随时可以呈现的 HTML 页面。
采用这种方法可能会花费大量的时间,并由于页面加载时的等待而导致糟糕的用户体验。这就是服务器端渲染介入的地方。我们可以在服务器端准备初始页面,并将其提供给用户的浏览器,然后浏览器可以轻松地下载页面并呈现它。这样,初始应用加载可以通过一个 web 请求来执行。在前一章中,我们已经学习了如何使用 Next.js 进行服务器端渲染。在本章的后面,我们将会看到如何使用客户端渲染和服务器端渲染来创建一个全功能的应用。让我们从创建一个简单的 React 应用开始。
构建一个简单的 React 应用
我们将创建一个简单的应用,在浏览器上显示时间。使用以下命令创建一个 starter React 应用:
npx create-react-app my-app
成功执行该命令后,导航到“my-app”文件夹,从“src”目录中删除除“index.js”文件之外的所有文件。用以下代码替换“index.js”文件中的代码:
import React from 'react';
import ReactDOM from 'react-dom';
ReactDOM.render(<h1>Hello from React.</h1>, document.getElementById('root'));
在使用“npm-start
”命令执行应用时,您应该会看到“Hello from React”打印在您的浏览器窗口上。我们的 starter React 应用已经启动并运行。现在让我们在 React 组件的帮助下完成这项工作。
创建功能 React 组件
我们的组件文件将位于应用根目录下的“src/Components”文件夹中。让我们使用以下代码将“App.js”文件添加到“Components”文件夹中:
src/Components/App.js
import React from 'react';
function App(){
return(
<div>
<h1>Hello from React.</h1>
</div>
);
}
export default App;
我们还需要对“index.js”文件进行一些修改,以便呈现组件,而不是直接呈现 JSX 代码。按照以下代码更新“index.js”文件:
src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './Components/App';
ReactDOM.render(<App/>, document.getElementById('root'));
如果您运行应用并访问浏览器,您应该会看到“React 的 Hello”印在窗户上。到目前为止,我们还没有在浏览器上显示时间。让我们使用 React 属性来实现这一点。
将属性传递给功能 React 组件
我们将简单地把当前时间作为属性传递给 App 组件。该组件将从 props 中获取值,并将其呈现给浏览器。这很简单。让我们通过如下方式修改代码来实现这一点:
src/Components/App.js
import React from 'react';
function App(props){
return(
<div>
<h1>Time: {props.time}</h1>
</div>
);
}
export default App;
src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './Components/App';
ReactDOM.render(
<App time={new Date().toLocaleTimeString()}/>,
document.getElementById('root')
);
如您所见,我们将时间字符串作为属性传递给了组件,然后呈现给浏览器。然而,React 还不会更新 DOM,因为我们已经传递了一个静态时间字符串,React 还不知道时间何时改变。为了实现这个功能,我们将不得不编写一些 JavaScript 代码来随着实际时间的流逝更新存储在“props”中的时间。但是我们不能这样做,因为“属性”是只读的。因此,我们将不得不使用 React 生命周期提供的“状态”功能。
将功能组件转换为类组件
让我们将函数组件转换成类组件。一旦我们这样做了,我们将能够使用 React 的“state”属性来跟踪时间的变化。我们的应用的当前目录结构如图 4-2 所示。
图 4-2
当前目录结构
您必须对代码进行以下更改:
src/Components/App.js
import React from 'react';
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
time: new Date().toLocaleTimeString()
}
}
tick() {
this.setState(() => {
return ({
time: new Date().toLocaleTimeString()
});
});
}
componentDidMount() {
this.timer = setInterval(() => this.tick(), 1000);
}
componentWillUnmount(){
clearInterval(this.timer);
}
render() {
return (
<div>
<h1>Time: {this.state.time}</h1>
</div>
);
}
}
export default App;
src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './Components/App';
ReactDOM.render(<App/>, document.getElementById('root'));
如您所见,我们不再将时间字符串作为属性传递给组件。我们在类组件的构造函数中将 state 属性设置为当前时间。然后我们使用 render 方法将其呈现给浏览器。这里唯一的区别是,我们现在从“状态”中获取值,而不是从“props
”中获取值。使用“state
”属性是必要的,因为正如我们前面讨论的,“props
”是只读的,我们不能直接修改它们。
接下来,我们必须找到一种方法来随着时间的变化更新状态属性。为此,我们使用 JavaScript 的setInterval()
方法创建了一个间隔。我们在 React 生命周期的componentDidMount()
方法中这样做,是为了确保只有在组件被挂载到 DOM 时才设置时间间隔。
Note
如果你已经创建了一个函数组件,你可以使用 React 钩子来钩住 React 生命周期方法componentDidMount()
。你可以回到这本书的第二章来看看 React hooks 的工作原理。
如果你已经创建了一个函数组件,你可以使用 React 钩子来钩住 React 生命周期方法componentDidMount()
。
interval 每秒调用一次tick()
方法,最终用新时间更新“state
”对象。一旦状态被修改,React 就会重新渲染视图。因此,用户在浏览器上看到一个每秒滴答作响的数字时钟。我们可能希望清除在componentWillUnmount()
方法中安装组件时创建的计时器,以避免在组件从 DOM 中移除时出现内存泄漏。
就是这样。我们的应用功能齐全。我们已经完全通过使用 React 的客户端渲染方法实现了这一点。让我们看看如何使用 Next.js 框架向该应用添加服务器端呈现。
使用 Next.js 进行服务器端呈现
为了使用 Next.js 框架进行服务器端渲染,我们必须使用以下命令将其安装到我们的应用中:
npm install next -–save
安装完成后,我们需要按如下方式更改“package.json”文件中的“scripts”部分:
...
"scripts": {
"start": "next",
"build": "next build",
"test": "echo \"Error: no test specified\" && exit 1"
}
...
如果您尝试启动该应用,您将遇到一个错误,指出没有找到“Pages”目录。我们必须创建它,并将我们的“app.js”文件添加到该目录,因为 Next.js 从那里加载页面。我们可以删除我们的“index.js”文件(它包含呈现组件的代码),因为 Next.js 会为我们处理呈现工作。我们也可以删除“src”和“public”文件夹,因为它们对我们没有用。为了简单起见,我们可以将“Pages”目录中的“app.js”文件重命名为“index.js ”,因为正如我们在前一章中了解到的,Next.js 遵循基于页面的路由,它在应用启动时查找“index.js”页面。现在,如果您启动应用,您将看到一个与使用客户端渲染创建的计时器相同的计时器。如果您想验证内容是否在服务器端呈现,您可以右键单击页面并查看页面源代码。您会注意到计时器的 HTML 代码的出现,而不是一个空的<div>
标签。这告诉我们计时器不是使用客户端 JavaScript 生成的,而是在服务器端生成的。但是,页面并不是每秒都被重新加载。这意味着客户端的 React 正在处理状态的更新。这正是我们想要的,服务器端的初始应用呈现和客户端的其他 DOM 更改。现在我们的应用已经启动并运行了,让我们给它添加一些样式。
将 CSS 添加到 Next.js
正如上一章所讨论的,为了给 Next.js 应用添加样式,我们必须使用一个外部 CSS 加载器。让我们将"@zeit/next-css"
模块安装到我们的应用中,它将作为 CSS 加载器。使用以下命令:
npm install @zeit/next-css --save
安装后,我们必须使用以下代码将配置文件“next.config.js”添加到应用的根目录中:
next.config.js
const withCSS = require('@zeit/next-css')
module.exports = withCSS({})
这个配置文件充当我们的应用的 webpack 配置的入口点,默认情况下,它被 Next.js 框架隐藏。配置完成后,我们可以为应用创建一个样式表,并将其与其他导入内容一起导入到页面中。考虑我在“Resources”文件夹中创建的以下样式表:
Resources/style.css
body{
margin-top: 45vh;
text-align: center;
}
h1{
display:inline-block;
border: 5px solid black;
border-radius: 10px;
padding: 10px;
}
我们的应用的当前目录结构如图 4-3 所示。
图 4-3
当前目录结构
我使用下面的语句将它导入到我们的索引页面:
import '../Resources/style.css'
如果您启动应用并访问浏览器,您将看到类似于图 4-4 的输出。
图 4-4
使用 Next.js 和 React 的数字时钟
我们还可以在应用中添加 Bootstrap,以提高应用的响应能力。让我们看看如何做到这一点。
将 Bootstrap 集成到您的应用中
为了使用 bootstrap,我们必须首先使用以下命令将其安装到我们的应用中:
npm install --save bootstrap
成功执行该命令后,您将看到引导模块被添加到我们的“node_modules”文件夹中。因为我们已经安装了 Zeit CSS loader 并在我们的应用中配置了它,所以我们将能够直接在我们的页面中导入 bootstrap CSS 文件并使用 bootstrap 框架提供的类。请参考下面的代码,以了解它是如何完成的:
Note
如果您需要在应用中全局应用 CSS,您应该创建一个包装所有组件的高阶组件(HOC ),然后在 HOC 中导入 CSS 文件。这将省去您在创建的每个组件中导入 CSS 文件的麻烦。我们在前一章中学习了如何创建一个 HOC,同时学习了 Redux 中的 Reducers。如果你不记得了,你可以回去看看。
Pages/index.js
import React from 'react';
import 'bootstrap/dist/css/bootstrap.min.css';
class App extends React.Component {
constructor(props) {
super(props);
...
}
tick() {
...
}
componentDidMount() {
...
}
componentWillUnmount() {
...
}
render() {
return (
<div>
<div className="jumbotron text-center">
<h1>Digital Clock with React, Next.js, and Bootstrap</h1>
</div>
<div className="text-center">
<p>Time: {this.state.time}</p>
</div>
</div>
);
}
}
export default App;
我们已经从页面中完全删除了自定义样式表“style.css ”,并替换为引导样式表。我们使用了 bootstrap 中的两个类——“jumbotron ”,它允许我们为应用定义一个标题部分,以及“text-center ”,它确保内容居中对齐。前面代码的输出应该类似于图 4-5 。
图 4-5
带自举的数字钟
就这样,我们到了这一章的结尾。您可以向应用添加更多的页面,在它们之间建立链接,并使用引导类。你探索得越多,你学到的就越多。让我们总结一下本章所学的内容。
摘要
-
第一次加载单页面应用时,服务器端呈现非常有用。由于等待时间更短,它带来了更好的用户体验。后续的 DOM 更改可以在客户端进行。
-
可以使用函数组件创建一个简单的客户端 React 应用。我们可以将属性传递给组件,然后组件可以将属性数据呈现给浏览器窗口。
-
由于属性是只读的,我们不能直接修改它们。如果我们在处理页面生命周期中需要修改的属性,更好的方法是使用 React 的状态对象。
-
componentDidMount()是一个 React 生命周期方法,当类组件安装到 DOM 时,它可以用来触发事件。在我们的例子中,我们使用这个方法以每秒更新的时间来修改状态对象。
-
为了将渲染转移到服务器端,我们使用 Next.js 框架。
-
因为 Next.js 负责组件的渲染,所以我们不需要担心这个问题。我们可以简单地将我们的客户端代码移动到 Next.js 页面,它将呈现在服务器端。
-
我们可以使用外部加载器在应用中添加自定义 CSS 和引导程序。
五、使用 Jest 的单元测试
在前几章中,我们学习了如何使用 React 和 Next.js 等库创建 web 应用。现在我们知道了如何使用这些库开发应用。接下来呢?
一旦开发了一个应用,知道它按预期工作是很重要的。为了做到这一点,我们可以编写自动化单元测试来验证我们的应用的每个组件都恰当地完成了它的工作。这正是我们在本章要学习的内容。我们将使用 Jest 框架在 React 应用上执行单元测试。
Note
作为一名开发人员,编写单元测试可能看起来交付了非常小的价值,同时给已经很紧张的时间表增加了很多工作。然而,从长远来看,当您的应用规模扩大时,它将帮助您减少工作量,提供一种非常有效的方法来检测代码中的错误和漏洞。
市场上还有很多其他的 JavaScript 测试框架,比如 Mocha 和 Jasmine。然而,由于 Jest 框架越来越受欢迎和实用,我们将使用它。我们将学习如何在我们的应用中安装和设置 Jest,然后我们将创建一个基本的单元测试来熟悉 Jest 的概念,最后,我们将学习有助于我们测试 React 组件的匹配器和酶。让我们从建立 Jest 框架开始。
设置笑话
Jest ( https://jestjs.io/
)是一个 JavaScript 测试框架,我们将使用它来测试 React 应用。让我们从创建应用目录“jest-testing-app”开始。导航到目录并执行“npm init
”命令,在目录中创建一个 package.json 文件。确保系统中安装了该节点,以便该命令能够运行。一旦成功执行,您将看到一个包含以下代码的“package.json”文件:
{
"name": "jest-testing-app",
"version": "1.0.0",
"description": "My Jest Application",
"main": "index.js",
"scripts": {
"test": "jest"
},
"author": "Mohit Thakkar",
"license": "ISC"
}
如果您在初始化期间指定了一组不同的值,这些值可能会有所不同。因为我们将使用 jest 进行测试,所以请确保您将“Jest”指定为“scripts”部分中“test”属性的值。我们现在将使用以下命令将 Jest 安装到我们的应用中:
npm install jest --save
安装后,您将看到以下依赖项部分被添加到您的“package.json”文件中:
"dependencies": {
"jest": "²⁴.9.0"
}
就是这样。Jest 现已成功安装。让我们使用 Jest 编写我们的第一个测试。
使用 Jest 编写您的第一个测试
让我们首先创建一个包含一些基本函数的简单 JavaScript 文件。我用下面的代码在应用的根目录下添加了一个名为“functions.js”的文件:
functions.js
const functions = {
add: (n1, n2) => n1 + n2
}
module.exports = functions
这个文件包含一个简单的“add”函数,它接受两个数字作为输入,并返回它们的和作为输出。注意,我们使用简单的 JavaScript 语法将函数列表导出为一个模块。避免使用 ES6 语法,因为 Jest 希望文件在导入时是普通的 JavaScript。现在我们已经创建了一个 JavaScript 函数,让我们使用 Jest 来测试它。我们将在“tests”目录中添加我们的测试文件。将测试文件命名为您正在测试的相同的 JavaScript 文件是一个很好的实践,带有".test.js"
后缀。考虑下面的测试文件,它包含测试“add”功能的代码:
tests/function.test.js
functions = require('../functions.js')
test('Test Add Function',()=>{
expect(functions.add(2,3)).toBe(5)
})
就是这样。我们已经使用 Jest 框架创建了我们的第一个测试。让我们理解这里发生了什么:
-
在测试代码中,我们简单地调用“
test()
”函数,它接受两个输入参数——第一个是测试的描述,第二个是实际的测试函数。 -
在测试函数中,我们使用了"
expect()
"函数,它接收我们正在测试的函数并对其求值,在我们的例子中,是" add()"函数。 -
我们使用 JavaScript 的"
require()
"方法从"functions.js"
导入函数列表,因为为了调用"add()
"函数,我们必须从定义它的文件中导入它。 -
我们在“
expect()
”函数上使用一个匹配器,在本例中是“toBe()
”函数,以便将评估值与期望值进行比较。我们将期望值作为输入参数传递给匹配器。我们将在下一个主题中学习更多关于匹配器的知识。
使用以下命令运行测试:
npm test
成功执行命令后,您将在终端中看到测试执行的摘要,如图 5-1 所示。
图 5-1
使用 Jest 的首次测试(成功)
Note
在一个文件中编写多个测试是可能的。
“测试套件”表示测试文件的数量,而“测试”表示这些文件中测试的组合数量。在前面的例子中,如果我们将期望值更改为其他值,比如说“4”,测试执行将会失败。让我们试试。考虑对“function.test.js”文件的以下更改:
tests/function.test.js
functions = require('../functions.js')
test('Test Add Function',()=>{
expect(functions.add(2,3)).toBe(4)
})
现在,如果您运行“npm test”命令,您将在终端中看到测试执行失败。如图 5-2 所示,您还会看到测试执行失败的原因,在这种情况下,接收值不等于预期值。您还将在测试执行总结中看到接收值和期望值。
图 5-2
使用 Jest 的首次测试(失败)
既然我们已经学习了如何使用 Jest 为 JavaScript 函数编写测试,那么让我们更深入地研究一下,了解一下可以用来测试代码的不同匹配器。
匹配项
匹配器是 Jest 使用的函数,用于将评估值与期望值进行比较。在前面的例子中,我们使用了 Jest 提供的“toBe()
”匹配器。注意,我们使用“expect()
”函数来计算实际值。这个函数返回一个期望对象,我们在这个对象上调用我们的匹配器函数,将它与期望值进行比较。让我们看看 Jest 提供的所有匹配器。
常见匹配器
以下是一些非常常用的通用匹配器:
-
toBe(expected value)–这是完全相等的匹配器。它检查“expect()”函数返回的值是否与“expectedValue”完全匹配。
-
to equal(expected value)–这类似于“toBe()”匹配器,只是它用于比较对象的值。它递归地检查对象的每个属性。
让我们看一个例子来理解工作中常见的匹配器。考虑对“functions.test.js”文件进行以下更改:
tests/**functions . test . js
functions = require('../functions.js')
test('toBe Demo',()=>{
expect(functions.add(2,3)).toBe(5)
})
test('toEqual Demo',()=>{
var data = {name:'Mohit'}
data['country'] = 'India'
expect(data).toEqual({
name:'Mohit',
country:'India'
})
})
为了演示"toBe()
"匹配器的效用,我们使用了与上一个例子中测试的"add()
"函数相同的函数。该函数返回“5”,并且“toBe()
”匹配器断言它为真,因为它是我们所期望的值。
对于“toEqual()”
匹配器,我们定义了一个新的测试“toEqual Demo
”。我们用一个属性定义一个“数据”对象,然后向该对象添加一个新属性。我们现在将“数据”对象传递给“expect()
”函数,并使用“toEqual()
”匹配器将它与预期的输出进行比较。由于两个值匹配,Jest 将断言测试为真。上例的输出应该类似于图 5-3 。
图 5-3
笑话中常见的媒人
如果您想尝试更多的场景,您可以更改前面示例中的期望值,并注意到测试失败了。
Note
如果您使用的是 Visual Studio 代码编辑器,则可以通过“Orts”来使用“Jest”扩展。它为 Jest 提供了 IntelliSense,对于调试您编写的测试也非常有帮助。
真理匹配者
这些匹配器允许您检查评估值是 null、未定义、已定义、true 还是 false。您不需要向这些匹配器传递任何输入参数:
-
tobe null()–匹配空值
-
tobe undefined()–匹配未定义的值
-
tobe defined()–匹配未定义的值
-
tobe truthy()–匹配评估为 true 的值
-
toBeFalsy()–匹配评估为 false 的值
让我们看一个例子来理解真理匹配器的工作。以下新测试需要添加到“functions.test.js”文件中:
tests/functions.test.js
...
test('truth of null', () => {
const n = null;
expect(n).toBeNull();
expect(n).toBeDefined();
expect(n).not.toBeUndefined();
expect(n).not.toBeTruthy();
expect(n).toBeFalsy();
});
test('truth of zero', () => {
const n = 0;
expect(n).not.toBeNull();
expect(n).toBeDefined();
expect(n).not.toBeUndefined();
expect(n).not.toBeTruthy();
expect(n).toBeFalsy();
});
...
在前面的例子中,我们编写了两个新的测试,一个测试“null”的真实性,另一个检查数字零的真实性。null 值应评估为 null、defined 和 not true。另一方面,数字 0 应该计算为 not null、defined 和 false。如果您使用除零以外的任何数字,它的计算结果应该为 true。
请注意,我们使用了“not”关键字来否定某些匹配器。因此,如果表达式的计算结果为“false”,我们可以使用“not”关键字和“toBeTruthy()
”匹配器来断言它。上例的输出应该类似于图 5-4 。
图 5-4
玩笑中的真理匹配者
比较匹配器
这些匹配器允许您将实际值与另一个值进行比较。要与实际值进行比较的值将作为输入参数传递给比较匹配器:
-
toBeGreaterThan(value)–如果实际值大于提供的值,则置位。
-
toBeGreaterThanOrEqual(value)–如果实际值大于或等于提供的值,则置位。
-
tobe less than(value)–如果实际值小于规定值,则置位。
-
tobelesthanorequal(value)–如果实际值小于或等于提供的值,则置位。
-
tobe closeto(value)–如果实际值接近提供的值,则断言。这是在处理浮点值时专门使用的。在这种情况下,期望值和实际值的精度可能不同,因此“
toBe()
”匹配器(精确相等)将不起作用。
为了理解工作中的比较匹配器,让我们看一个例子。以下是添加到“functions.test.js”文件中的新测试:
tests/functions.test.js
...
test('comparison', () => {
const value = 4 + 0.2;
expect(value).toBeGreaterThan(3);
expect(value).toBeGreaterThanOrEqual(3.5);
expect(value).toBeLessThan(5);
expect(value).toBeLessThanOrEqual(4.5);
expect(value).toBeCloseTo(4.2);
});
...
前面示例中的实际值将计算为“4.2”。使用比较匹配器将它与多个值进行比较。请注意,为了断言确切的值,我们使用了“toBeCloseTo()
”匹配器,而不是“toBe()
”匹配器,这可能是因为精度上的差异。上例的输出应该类似于图 5-5 。
图 5-5
玩笑中的比较匹配器
字符串匹配
此匹配器用于将实际值与正则表达式进行比较:
- to match(regex)–断言计算出的字符串是否与提供的正则表达式匹配
考虑以下示例:
tests/functions.test.js
...
test('String Matcher', () => {
expect('Mohit is a Developer').toMatch(/Mohit/);
});
...
前面的测试断言子字符串“Mohit”存在于计算出的字符串中。输出应类似于图 5-6 。
图 5-6
笑话中的字符串匹配器
Iterables 匹配器
这个匹配器用于检查一个项目是否存在于一个 iterable 中,比如一个列表或一个数组:
- to contain(item)–如果计算的 iterable 包含提供的项目,则断言
考虑以下示例:
tests/functions.test.js
...
const countries = [
'India',
'United Kingdom',
'United States',
'Japan',
'Canada',
];
test('Matcher for Iterables', () => {
expect(countries).toContain('India');
expect(new Set(countries)).toContain('Canada');
});
...
在前面的测试中,我们定义了一个状态列表,并使用“toContain()
”匹配器来检查“印度”是否出现在列表中。我们还将列表转换为不同的 iterable,即集合,并检查“Canada”是否出现在新的集合中。两个匹配器都应该断言 true。输出应类似于图 5-7 。
图 5-7
笑话中可重复的匹配者
Matcher 异常
此匹配器用于断言在评估特定代码段时是否引发了特定异常:
- to throw(expected exception)–如果被评估的代码段抛出给定的异常,则断言
为了测试这个匹配器,我们将回到我们的“function.js”文件并定义一个抛出错误的函数。然后,我们将在“functions.test.js”文件中添加一个测试,它将调用函数并断言异常。考虑以下示例:
function.js
const functions = {
add: (n1, n2) => n1 + n2,
invalidOperation: () => {
throw new Error('Operation not allowed!')
}
}
module.exports = functions
tests/functions.test.js
functions = require('../functions.js')
...
test('Exception Matcher', () => {
expect(functions.invalidOperation)
.toThrow(Error);
expect(functions.invalidOperation)
.toThrow('Operation not allowed!');
expect(functions.invalidOperation)
.toThrow(/not allowed/);
});
...
在前面的示例中,我们调用了抛出错误的函数,并使用“toThrow()
”匹配器将计算值与预期值进行匹配。请注意,我们可以将其与一般的错误对象、错误返回的特定字符串或正则表达式进行比较。上例的输出应该类似于图 5-8 。
图 5-8
玩笑中的异常匹配器
就是这样。我们已经介绍了大多数常用的笑话匹配器。现在让我们学习如何使用我们到目前为止所学的知识来测试我们的 React 组件。
使用 Jest 和酶测试 React 组分
为了测试 React 组件,我们必须首先创建一个 React 组件。让我们使用以下命令创建一个 starter React 应用:
npx create-react-app react-jest-app
一旦创建了 starter 应用,您就可以删除所有不必要的文件。我已经删除了“src”文件夹中除“index.js”以外的所有文件,以及“public”文件夹中除“index.html”和“favicon.ico”以外的所有文件。我还清理了“index.html”文件。以下是代码,供您参考:
public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon"
href="%PUBLIC_URL%/favicon.ico" />
<title>React App</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
</body>
</html>
现在我们已经清理了我们的 starter 应用,让我们将列表组件添加到“src”文件夹中。考虑以下代码:
src/list . js
import React from 'react';
function List(props) {
const { items } = props;
if (!items.length) {
return(
<span className="empty-message">
No items in list
</span>;
);
}
return (
<ul className="list-items">
{items.map(item =>
<li key={item} className="item">{item}</li>
)}
</ul>
);
}
export default List;
前面的代码是一个简单的函数组件,它从 props 中获取项目,并将它们显示为一个列表。既然我们的组件已经创建,我们可能希望指示“index.js”文件在浏览器上呈现它。考虑“index.js”文件的以下代码:
src/index . js
import React from 'react';
import ReactDOM from 'react-dom';
import List from './List';
const data = ['one', 'two', 'three']
ReactDOM.render(<List items={data} />, document.getElementById('root'));
如果您启动应用并访问浏览器窗口,您应该会看到类似于图 5-9 的输出。
图 5-9
使用 React 列出组件
现在我们已经创建了一个 React 组件,让我们学习如何测试它。让我们首先使用以下命令安装 Jest 框架:
npm install jest@24.9.0 --save
Note
我们已经安装了一个特定版本的 Jest 框架。这是因为使用“create-react-app”命令初始化的应用依赖于这个版本的 Jest。如果您的 Jest 框架版本与所需的版本不匹配,您将在应用启动期间得到一个错误,指出您需要的版本。您可以通过安装应用所需的版本来解决该错误。
在安装 Jest 框架之后,您还必须在“package.json”文件中添加测试脚本,如下所示:
package.json
{
...
"scripts": {
...
"test": "jest",
...
},
...
}
在测试简单的 JavaScript 函数时,我们习惯于简单地调用测试中的函数,并使用 Jest 匹配器将评估值与期望值进行比较。但是您可能想知道在 react 组件的情况下应该做什么,因为我们不能仅仅调用一个组件。
我们将完全按照 React 所做的去做。我们将组件呈现在 DOM 上,但它不是实际的浏览器 DOM;它将是由一个叫做 Enzyme 的框架创建的一个代表性的 DOM。这个框架帮助我们模拟运行时环境,以便我们可以测试我们的组件。让我们使用以下命令安装 Enzyme framework 的依赖项:
npm install enzyme enzyme-adapter-react-16 –save
请注意,我们还安装了一个适配器以及与我们正在使用的 React 版本相对应的酶框架。在使用这个框架之前,我们必须在 Enzyme 中配置这个适配器。为此,我们将在应用的根目录下创建一个“enzyme.js”文件,并向其中添加以下配置代码:
enzyme.js
import Enzyme, { configure, shallow, mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter() });
export { shallow, mount };
export default Enzyme;
在前面的代码中,我们从酶框架中导入“酶”和“配置”,从酶适配器中导入“适配器”。然后,我们使用酶框架提供的“configure()
”方法为我们将要使用的酶实例设置适配器。配置完 Enzyme 后,我们只需导出它。我们还从酶框架中导入 shallow 和 mount,并按原样导出它们。这些是我们将用来呈现 React 组件进行测试的方法。最后,我们测试所需的酶的所有实体都将从文件“enzyme.js”中导入,而不是直接从框架中导入。如果您尝试直接从 Enzyme framework 安装文件夹导入模块,您可能会遇到错误,因为它们没有配置适配器。
现在一切都配置好了,让我们为列表组件编写测试。让我们将测试写在“src”文件夹中的“List.test.js”文件中。请参考以下代码:
src/??【list . test . js
import React from "react";
import { shallow, mount } from '../enzyme';
import List from './List';
test('List Component Test', () => {
const items = ['one', 'two', 'three'];
const wrapperShallow = shallow(<List items={items} />);
const wrapperFull = mount(<List items={items} />);
console.log(wrapperFull.debug());
expect(wrapperShallow.find('.list-items'))
.toBeDefined();
expect(wrapperShallow.find('.item'))
.toHaveLength(items.length);
expect(wrapperFull.find('.list-items'))
.toBeDefined();
expect(wrapperFull.find('.item'))
.toHaveLength(items.length);
})
请注意,我们使用了两种不同的方法来呈现我们的组件-浅层和挂载。让我们了解一下两者的区别。顾名思义,“shallow()
”方法将呈现范围限制在指定的组件上,不呈现其子组件。另一方面,“mount()
”方法呈现整个组件树。在这种情况下,我们没有任何子组件,因此两种情况下的渲染是相同的。
我们将三个项目传递给组件进行呈现。其余的语法类似于我们测试 JavaScript 函数的语法。“shallow()
”和“mount()
”方法返回一个 React 包装器。如果您想查看包装器包含的内容,您可以调用包装器上的“debug()
”方法,并将输出记录到控制台,就像我们在前面的代码中所做的一样。您可以在前面测试的输出中看到,如图 5-10 所示,我们组件的整个 HTML 呈现都记录在控制台上。我们可以在这个包装器上使用“find()
”方法来查找呈现的代码中的元素。我们可以将多种选择器传递给“find()
”方法。这应该在"expect()
"方法中完成,这样我们就可以为断言使用匹配器。在前面的例子中,我们使用了类选择器来评估和断言列表项的存在和长度。以下是您可以使用的一些其他选择器:
图 5-10
使用 Jest 测试列表组件
ID 选择器
wrapper.find('#item1')
标签和类别的组合
wrapper.find('div.item')
标签和 ID 的组合
wrapper.find('div#item1')
属性选择器
wrapper.find('[htmlFor="checkbox"]')
如果您使用“npm test”命令执行测试,您可能仍然会在渲染时收到有关意外标记的错误。这是因为 Enzyme 不理解我们提供给“shallow()
”和“mount()
”方法的 JSX 代码。我们将不得不安装和配置一个巴别塔变压器插件,将为我们转换 JSX 代码。使用以下命令安装它:
npm install babel-plugin-transform-export-extensions --save
还有,我们需要创造一个”。babelrc "文件,并提供以下配置:
.babelrc
{
"env": {
"test": {
"presets": ["@babel/preset-env",
"@babel/preset-react"],
"plugins": ["transform-export-extensions"],
"only": [
"./∗∗/∗.js",
"node_modules/jest-runtime"
]
}
}
}
如果您在安装和配置 babel transform 插件后运行测试,测试应该会成功运行,并且输出应该类似于图 5-10 。
就是这样。我们已经使用 Jest 和酶成功测试了我们的 React 组件。随着这个话题的结束,我们来到本章的结尾。
让我们总结一下我们所学到的东西。
摘要
-
Jest 是一个测试框架,可以用来测试使用 JavaScript 构建的应用。
-
用“. test.js”作为测试文件的后缀是一个很好的习惯。
-
测试时,“expect()”方法用于评估 JavaScript 函数或指定需要测试的值。
-
Jest 提供了各种匹配器,可以在“expect()”方法上使用这些匹配器来断言计算值是否与期望值匹配。
-
由于 React 组件不能像函数一样被直接调用,我们将不得不使用 Enzyme 框架,该框架为我们提供了在为测试而创建的代表性 DOM 上呈现组件的功能。
-
Enzyme 框架提供了两种主要的方法来呈现组件——shallow()和 mount()。
-
我们可以使用带有“find()”方法的选择器来查找呈现组件中的特定内容。
六、将您的应用部署到服务器
在前面的章节中,我们已经学习了如何构建和测试 React 应用。一旦我们完成了这些,我们可能想知道我们的应用在生产环境中的表现如何。这就是部署的时候了。
在本章中,我们将学习如何使用 Docker 容器部署我们的应用。让我们从了解部署流程开始。
部署流程
到目前为止,我们已经在本地执行了应用。然而,为了将我们的应用部署到生产服务器,我们需要遵循某些步骤。考虑以下过程:
-
首先,我们需要通过在代码中设置一些环境变量来配置应用。这将有助于应用识别其运行的环境,并做出相应的行为。
-
然后我们需要在我们的系统上安装 Docker 来封装我们的应用。
-
一旦配置完成并且安装了 Docker,我们需要为我们的应用构建一个 Docker 映像。我们将在本章的后续主题中了解更多关于 Docker 的内容。
-
一旦我们知道 Docker 映像在本地工作正常,我们就可以将它部署到面向公众的服务器上。
现在我们已经了解了部署过程,让我们从设置应用的配置开始。如果您不明白或对我们刚刚讨论的过程感到困惑,请不要担心。一旦我们详细讨论每一步,你会有更好的理解。
设置环境变量
环境变量是键值对,React 应用可以读取它们,以便根据应用运行的环境在运行时配置值。它促进了应用的动态行为。为了在工作中理解这一点,我们将使用在第三章中开发的 GraphQL 应用。以下是代码,供您参考:
package.json
{
"name": "my-next-app",
"version": "1.0.0",
"description": "My Next.js Application",
"main": "index.js",
"scripts": {
"start": "next"
},
"author": "Mohit Thakkar",
"license": "ISC",
"dependencies": {
"@zeit/next-css": "¹.0.1",
"axios": "⁰.19.0",
"graphql": "¹⁴.5.8",
"next": "⁹.1.6",
"react": "¹⁶.12.0",
"react-dom": "¹⁶.12.0"
}
}
next . config . js
const withCSS = require('@zeit/next-css')
module.exports = withCSS({})
页/API/**test pi . js
const data = {
name: 'Jhon Doe',
address: '7th Avenue, Brooklyn',
contact: '099251456',
bloodgroup: 'A +ve',
favouriteSnack: 'Hotdog',
vehicle: 'Hyundai Tucson'
}
export default (req, res) => {
res.statusCode = 200
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify(data))
}
页/ 索引. js
import React from "react";
import axios from 'axios';
import "../style.css";
export default class extends React.Component {
static async getInitialProps() {
try {
const res = await axios.get('http://localhost:3000/api/testapi');
return { data: res.data, error: null }
} catch (e) {
return { data: “, error: e }
}
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<table>
{Object.keys(this.props.data).map((key, index) => (
<tr key={index}>
<td>{key}:</td>
<td>{this.props.data[key]}</td>
</tr>
))}
</table>
</div>
);
}
}
在这个应用中,我们创建了一个 Next.js API,并在应用的索引页面上使用它。在使用 API 时,我们在 API URL 中使用“localhost”。这在生产环境中是行不通的。因此,让我们添加环境变量并在代码中使用它们。加一个”。env "文件,其中包含我们的环境变量:
。环境
URL_TestAPI_Dev = http://localhost:3000/api/testapi
URL_TestAPI_Prod = http://www.testapplication001.com/api/testapi
现在,该”。env”文件对于我们的应用来说是未知的。我们必须在应用的 webpack 配置中添加一些代码。我们还必须通过从终端执行以下命令,将“dotenv-webpack”插件添加到我们的应用中:
npm install dotenv-webpack path --save
我们已经使用“next.config.js”文件将我们的自定义 CSS 加载器添加到 webpack 中。我们将使用相同的文件来添加“的配置。env”文件复制到 webpack。将以下代码添加到“next.config.js”文件中:
next.config.js
const withCSS = require('@zeit/next-css')
require('dotenv').config();
const path = require('path');
const Dotenv = require('dotenv-webpack');
module.exports = withCSS({
webpack(config, options){
config.plugins = config.plugins || [];
config.plugins = [
...config.plugins,
new Dotenv({
path: path.join(__dirname, '.env'),
systemvars: true
})
]
return config;
}
})
一旦配置完成,我们添加到。env”文件将在整个应用中可用。现在让我们修改“index.js”文件,根据应用环境动态地使用 API URL。如果您以前使用过 Node.js 应用,您会意识到 Node 提供了一个静态类“Process ”,该类在一个名为“env”的属性中为我们提供了对用户环境的访问。我们将使用这个类来确定应用环境。考虑对“index.js”文件的以下更改:
pages/index.js
...
static async getInitialProps() {
try {
const res = await axios.get('http://localhost:3000/api/testapi');
return { data: res.data, error: null }
} catch (e) {
return { data: ", error: e }
}
}
static getAPIURL(){
if(process.env.NODE_ENV === 'production'){
return process.env.URL_TestAPI_Prod;
}
else{
return process.env.URL_TestAPI_Dev;
}
}
render() {
return (
<div>
<h1>
Hello from {process.env.NODE_ENV} server
</h1>
<table>
...
</table>
</div>
);
}
...
当我们使用“npm start”命令启动应用时,我们将看到类似于图 6-1 的输出。数据是从 Next.js API 获取的,它是我们的应用的一部分。这是因为“npm start”命令在开发环境中的本地服务器上运行应用。我们还将看到环境(开发)打印在标题中。一旦我们部署了应用,我们将在生产环境中进行同样的测试。
图 6-1
开发环境中的应用
这就是关于设置环境变量的内容。现在让我们了解一下 Docker。
Docker 简介
Docker 是一个平台,它允许我们在直接运行在主机上的容器中打包和执行我们的应用。这允许我们在一台主机上同时运行多个容器化的应用。当我们需要运行针对不同操作系统的应用时,这尤其有用。特别是在这种情况下,我们必须为每个操作系统创建虚拟机,然后将我们的应用部署到它们各自的虚拟机上。然而,使用 Docker 容器,情况就不一样了。因为 Docker 容器直接与主机的内核交互,所以它们不需要额外的管理程序(虚拟机)负载。我们只需要为我们的应用创建一个图像,该图像将用于为我们的应用创建一个容器。然后,这个容器化的应用将与其他容器化的应用一起在主机操作系统的 Docker 引擎上运行。这就是如何使用 Docker 在一台主机上运行针对不同操作系统的应用。容器化应用和虚拟机实现的区别可以在图 6-2 中看到。
图 6-2
容器化应用与虚拟机实施
Docker 架构中包含多个组件,如图 6-3 所示。让我们简要地了解一下他们每一个人:
图 6-3
码头建筑
-
Docker 客户端——这是用户与 Docker 沟通的方式。它提供了调用 Docker API 的命令,可以用来与 Docker 守护进程进行通信。
-
Docker 守护进程–这是 Docker 主机的一部分,它监听 API 请求并管理容器和图像等对象。它还可以与其他守护程序通信。
-
图像–Docker 图像是一个只读模板,包含一组分层的指令,用于创建 Docker 容器。我们可以创建自己的图像或使用他人创建并发布到注册表的图像。
-
容器–Docker 容器是 Docker 图像的一个实例。我们可以使用 Docker API 或 CLI 在容器上执行启动、停止、移动或删除等操作。
-
注册表–Docker 注册表存储 Docker 图像。Docker Hub 是一个任何人都可以使用的公共注册表。默认情况下,Docker 被配置为在 Docker Hub 上查找图像。您也可以运行自己的私有注册表。当您运行“docker run”或“docker pull”命令时,将从您配置的注册表中提取所需的映像。当您运行“docker push”命令时,您的映像会被推送到您配置的注册表中。
关于 Docker 平台,我们可以了解更多的东西。然而,我们不会深入细节,因为这一章是关于部署而不是关于 Docker 的。让我们学习如何将我们的应用容器化。
为您的应用创建 Docker 容器
首先,我们需要在我们的机器上安装 Docker Desktop。它适用于 Windows 和 Mac。我们需要在 https://hub.docker.com/
注册码头工人。一旦我们注册,我们可以登录到我们的帐户,并访问仪表板找到 Docker 桌面的下载链接。下载后,你可以像安装其他软件一样把它安装在你的机器上。由于我使用的是 Windows 操作系统,所以我下载了 Docker Desktop for Windows。要检查它是否已正确安装在我们的系统上,我们可以在终端中使用以下命令来检查 Docker 的版本:
docker --version
现在,让我们回到我们的项目。我们必须使用以下一组指令将“Dockerfile”添加到我们的应用的根目录,这些指令将充当图像模板,并用于为我们的应用创建容器:
码头式码头式码头式码头式码头式码头式码头式码头式码头式码头式码头式码头式码头式码头式码头式码头式码头式码头式码头式码头式码头式码头式码头式码头式码头式码头式码头式码头式码头式码头式码头式码头式码头式码头
# base image
FROM node:alpine
# create & set working directory
RUN mkdir -p /usr/src
WORKDIR /usr/src
# Install dependencies
COPY package.json /usr/src
RUN npm install
# Bundle app source
COPY . /usr/src
# start app
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]
让我们明白这里发生了什么。首先,我们指示 Docker 从“alpine
”开始,这是 Linux 的一个通用实现。然后我们创建一个新的源目录并切换到它。我们将“package.json”文件复制到新的源目录,并在其中安装所有的依赖项。最后,我们将文件从节点应用复制到新的源目录。然后我们构建应用,公开运行节点服务器的端口,并启动服务器。
但是,我们需要修改我们的“package.json”文件。确保您的文件中有以下脚本:
package.json
...
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
...
一旦“Dockerfile”包含所有指令,我们就可以使用 Docker 提供的以下命令为我们的应用创建一个 Docker 映像:
docker build -t myfirstdockerrepo .
在执行这个命令之前,确保 Docker Desktop 正在您的系统上运行。Docker 桌面的一个常见问题是错误——“由于内存不足,无法启动”。如果您遇到类似的问题,请尝试通过导航到 Docker ➤设置➤资源来减少分配给 Docker 的内存量。一旦 Docker 启动并运行,执行上述命令。根据您的机器和应用的大小,为您的应用构建 Docker 映像可能需要几分钟时间。命令成功执行后,您可以使用以下命令查看所有可用的 Docker 图像:
docker images
请注意,我们已经使用目标选项(–t)和“docker build
”命令指定了存储库名称。我们指定的名称用作 Docker 图像的标签,并可用于引用图像容器。请注意,Docker 容器只是 Docker 图像的活实例。所以当我们执行“docker images
”命令时,我们看到的列表是 Docker 容器的列表。术语“图像”和“容器”经常互换使用。然而,不要混淆。在执行该命令时,我们将看到类似于图 6-4 的容器列表。
图 6-4
码头集装箱
就是这样。我们已经成功地为我们的应用创建了一个容器。现在让我们学习如何托管 Docker 容器。
承载容器
让我们首先将我们在本地创建的图像发布到 Docker Hub。在将容器推送到 Docker Hub 之前,我们需要登录 Docker Hub。为此,请从终端执行以下命令:
docker login
如果您已经登录到系统上的 Docker 桌面,前面的命令应该会自动对您进行身份验证。如果没有,将要求您提供登录凭据。登录后,我们可以使用“push”命令来发布我们的容器。但是在此之前,我们需要在容器的存储库名称前面加上 Docker 用户名。这是因为 Docker 会自动生成创建存储库的 URL 来发布您的容器。如果您不在您的存储库名称前加前缀,Docker 将自动尝试在 docker.io/library 上创建一个存储库,我们无权在公共域上创建存储库。因此,如果没有存储库名称作为前缀,我们将会遇到一个认证错误。让我们使用以下命令添加前缀:
docker tag e65 msthakkar121/myfirstdockerrepo
docker tag e65 msthakkar121/myfirstdockerrepo
我们在这里做的是向现有容器添加一个标记名,然后删除旧的标记名。“e65”是我们的图像 ID 的前三个字符,这是我们在这种情况下所需要的。
Note
“msthakkar121”是我的 Docker ID,所以我用它作为前缀。但是,您需要使用各自的码头工人 id。
一旦存储库名称有了前缀,并且我们登录到 Docker,我们需要使用下面的命令将容器推送到 Docker Hub:
docker push msthakkar121/myfirstdockerrepo:latest
成功执行上述命令后,Docker Hub 上将创建一个公共存储库,链接到我们的概要文件。我们可以通过访问 https://hub.docker.com/
并用我们的凭据登录来验证这一点。在访问“存储库”页面时,我们将看到“myfirstdockerrepo”被添加到列表中。同样如图 6-5 所示。
图 6-5
docker hub 上的存储库
现在,为了运行我们的容器,我们需要使用以下命令:
docker run -p 3000:3001 -d msthakkar121/myfirstdockerrepo
这个命令将在我们的系统上运行应用的生产版本。我们在命令中使用“-p”选项指定了两个端口。第一个端口用于命令行操作,第二个端口用于 web 应用。成功执行该命令后,应用将启动并运行在端口 3001 上。您可以通过访问浏览器上的 URL“http://localhost:3001/
”进行验证。输出应类似于图 6-6 。
图 6-6
生产环境中的应用
如您所见,我们打印在索引页标题上的环境变量现在显示“生产”而不是“开发”。这表明我们的应用正在生产服务器上成功运行。请注意,目前没有获取 API 数据,因为我们还没有在面向公众的云上托管我们的应用。你可以使用像数字海洋( www.digitalocean.com/
)这样的平台提供的服务,将你的容器化应用托管到面向公众的云上。但是,如果您创建这个应用是为了学习,并且不想在托管服务上花钱,那么使用 Docker Desktop 在本地运行应用的生产版本就足够了。
至此,我们来到了本章的结尾。让我们总结一下我们所学到的东西。
摘要
-
将我们的应用部署到生产服务器对于确保它在部署后按预期工作是至关重要的。
-
我们需要定义一些环境变量,并在代码中使用它们来根据应用运行的环境改变运行时的应用行为。
-
为了将我们的应用容器化,我们使用了 Docker。
-
容器化的应用直接与主机的内核交互,因此不需要管理程序。
-
由于容器化,面向不同操作系统的多个应用可以在同一台主机上同时运行,而无需任何虚拟机管理程序。
-
我们在名为“Dockerfile”的文件中指定构建 Docker 容器的指令,该文件位于我们的应用的根目录中。
-
然后我们使用“docker build”命令,根据“dockerfile”中的指令构建一个 Docker 容器。
-
可以使用“docker run”命令在本地模拟生产环境中测试容器。
-
像“DigitalCloud”这样的服务可以用来将我们的应用发布到面向公众的云上。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!