JavaScript-入门指南-全-
JavaScript 入门指南(全)
一、JavaScript 简介
这些年来,avaScript 发生了很大变化。我们目前正处于一个 JavaScript 库的时代,你可以构建任何你想构建的东西。JavaScript 存在于客户机和服务器上,存在于桌面和移动设备上。
这本书的目的是帮助你理解语言是如何工作的,可以用它做什么,可用的资源,以及围绕语言和工具的一些生态系统。有时我会指出一些在技术面试中可能会被问到的问题,所有这些都是为了帮助你了解这个不断发展的社区。我将涉及的一些主题是
-
理解 JavaScript 语法和结构
-
创建易于理解和维护的脚本
-
使用工具调试 JavaScript
-
处理事件
-
JavaScript 如何在服务器上工作
-
使 JavaScript 成为强类型语言的框架
-
JavaScript 应用框架及其工作原理
-
从服务器检索数据
JavaScript 在现代 web 开发中是必不可少的;单页应用(spa)构成了大部分被创建的网站。理解 JavaScript 可以让你的网站增加交互性,并降低框架之类的东西的学习曲线。这并不是说你需要所有的框架,但是为了给你的网站增加任何层次的交互性,你需要 JavaScript。
足够的介绍——你已经通过这本书了解了 JavaScript,所以让我们在深入研究之前,先从较高的层面快速讨论一下 JavaScript。
在本章中,您将学习
-
为什么 JavaScript 对开发人员来说很重要
-
如何向 web 文档添加 JavaScript
-
与 JavaScript 相关的面向对象的编程(OOP)
您可能已经接触过 JavaScript,并且已经知道它是什么以及它能做什么,所以我将首先快速地介绍一下该语言的一些基础知识及其功能。如果你已经很了解 JavaScript,而你只是想了解更多更新、更容易理解的特性和概念,你可以跳过一个头。然而,可能有些信息你已经忘记了,稍微回顾一下也无妨。
JavaScript 的原因
如前所述,JavaScript 无处不在。It 加上 HTML 和 CSS 都是你开发一个网站需要的工具。
您可以使用 JavaScript 在客户端和服务器端工作。这就使得对 JavaScript 开发者的要求非常高。对开发者来说,高需求意味着各种工作机会和有竞争力的价格。截至本文撰写之时,根据 Indeed,一家招聘网站( www.indeed.com/salaries/Javascript-Developer-Salaries
)的数据,一名 JavaScript 开发人员在美国的平均年薪为 110841 美元。因此,JavaScript 不仅是一种值得研究的语言,它还是开发人员工具集的一个很好的补充。让我们快速讨论一下什么是 JavaScript,然后继续写一些代码。
JavaScript 是什么?
JavaScript 是一种解释型脚本语言。宿主环境将提供对执行代码所需的所有对象的访问。
JavaScript 的主要例子是向网站添加交互性的能力。这是因为解释器被嵌入到网络浏览器中,所以你不需要添加任何软件。
这使得 JavaScript 成为一种易于使用的语言,因为你所需要的只是一个文本编辑器和一个浏览器。在客户端,您可以添加交互级别,如响应按钮点击和验证表单内容。它还允许您利用浏览器内置的 API(应用编程接口)。给你的网站增加地理定位功能就是一个例子。
另一个用例是使用 Node.js 这样的环境在服务器上执行 JavaScript。服务器端 JavaScript 的一个例子是能够从数据库发出请求、响应 HTTP 请求和创建文件。
本书的大部分内容将集中在客户端;然而,从代码的角度来看,差别很小。
网页中的 JavaScript 和基本语法
将 JavaScript 应用于 web 文档非常容易;你所需要做的就是使用script
标签:
<script>
// Your code here
</script>
虽然这是向页面添加 JavaScript 的最简单方式,但不推荐这样做。这是内联 JavaScript 的一个例子。不采用这种方式建立网站的原因之一是,随着时间的推移,维护起来会变得很困难。想象一下,随着你的网站越来越大,保持一切井然有序是多么困难。
将 JavaScript 添加到 HTML 页面的首选方式是引用外部.js
文件:
<script src="js/myfile.js"></script>
注意
HTML 5 要求script
标签有自己的结束标签。这将确保向后兼容旧的浏览器。此外,在结束标签body
之前添加标签script
被认为是最佳实践。
JavaScript 语法
学习任何编程语言都与学习外语非常相似。有规则可循,这可能需要不同的思维方式。
编程中有很多问题需要解决。你花时间观察一种情况,并试图找出如何使用代码来解决问题。记住这一点,让我们从这个角度来讨论 JavaScript。
如果你想保留一条信息以备后用,这叫做变量。如果你还记得任何高中代数,总有一些例子,其中一个单词或字母代表一个值:
5 + x = 10
在这个例子中,x 是一个代表数字的变量。使用 JavaScript,您可以声明一个变量并赋予它一个值,然后在以后使用它:
var x = 5;
5 + x = 10;
上面的代码不是完美的 JavaScript,但是它说明了变量是如何工作的。第一行使用关键字var
;这是语言内置的,只能在声明变量时使用。每行末尾都是分号,可以认为是句末的句号。JavaScript 解释器并不要求您拥有它。如果您没有添加分号,解释器将为您添加分号。为了更好地控制和阅读,建议您自己添加它们。
关键字var
并不是声明变量的唯一方式。也可以使用其他关键字,如let
和const
。我将在第三章中讲述它们的不同之处以及何时应该使用其中一个。
另一种情况是当你有一些代码,你只想在需要的时候运行它。JavaScript 称之为函数。您可以编写一个函数,向其中添加您希望它执行的所有命令,然后让它等待,直到您需要它,就像这样:
function doMath(num1, num2){
let sum = num1 + num2;
return sum;
}
这个示例函数的名称是doMath
。这允许从代码的其他部分引用或调用该函数。这个函数也接受两个变量或参数,sum1
和sum2
。把它们看作你的函数的变量;它们只是表示函数正确执行所需的数据。在这种情况下,它是一组数字。
接下来是花括号({ })。它们包含您的代码块。你需要让这个函数工作的所有东西都在这里。您可以根据需要添加任意多行代码。一般来说,一个函数应该只执行一件事。以doMath
为例,它只是把数字加在一起。如果你想让别的事情发生,应该写另一个函数。这将有助于调试过程。同样重要的是要注意,JavaScript 语言本身包含执行字符串操作和数学等任务的函数。
假设你想给自己留笔记。在这种情况下,您需要在代码中包含一些不会作为代码执行的内容。向代码中添加注释有两种方式:
// single line comment
*/
multi line
comment
/*
在调试代码时,添加多行注释也很有用。例如,如果您不想删除您拥有的内容,但又不想让代码执行,那么您可以将它设置为注释。
到目前为止,您已经知道了如何解决一些问题:如何保存数据、执行函数以及在代码中留下提醒。
现在让我们花点时间来讨论一下代码是如何在浏览器中执行的。
代码执行
浏览器从上到下读取页面,因此代码执行的顺序取决于脚本块的顺序。一个脚本块是<script>
和</script>
标签之间的代码;如果你有一个外部的.js
文件,它也会从上到下读取。(还要注意,不只是浏览器能读懂你的代码;网站的用户也可以查看你的代码,所以你不应该在里面放任何秘密或敏感的东西。)下一个示例中有三个脚本块:
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript">
alert("First script Block");
alert("First script Block - Second Line");
</script>
</head>
<body>
<h1>Test Page</h1>
<script type="text/javascript">
alert("Second script Block");
</script>
<p>Some more HTML</p>
<script type="text/JavaScript">
alert("Third script Block");
function doSomething() {
alert("Function in Third script Block");
}
</script>
</body>
</html>
如果您尝试一下,您会看到第一个脚本块中的alert()
对话框出现并显示消息
First script Block
接下来是第二行显示消息的下一个alert()
对话框
First script Block - Second Line.
解释器继续向下,来到第二个脚本块,这里显示了alert()
函数
Second script Block
第三个脚本块跟在它后面,显示一个alert()
语句
Third script Block
尽管函数内部还有另一个alert
语句,但它并不执行和显示消息。这是因为它在函数定义(function doSomething()
)中,函数中的代码只有在函数被调用时才会执行。
到目前为止,在本章中,您已经了解了 JavaScript 语言,看到了一些语法规则,了解了该语言的一些主要组成部分(尽管很简单),并运行了一些 JavaScript 脚本。你已经走了相当长的距离。在接下来的两节中更详细地研究 JavaScript 语言之前,让我们先来探索 JavaScript 语言的重要部分:函数和对象。
功能
在上一节中,您看到了一些在您明确请求之前不会执行的代码。JavaScript 中的函数非常灵活。它们可以赋给变量,并作为参数作为属性传递给其他函数。函数也可以返回另一个函数。让我们来看几个例子:
var doMath = function(num1, num2) {
var result = num1 + num2;
return result;
};
var myResult = doMath(2,3);
这个例子说明了如何将一个函数直接赋给一个变量,然后用这个变量调用这个函数。这个函数接受两个数字作为参数。当数字被传递给函数时,它们被加在一起,结果被返回给调用该函数的代码:
function message() {
return 'It's. the information age ';
}
function displayMessage(msgFunction, person){
consoe.log(msgFunction() + person) //It’s the information age brother!
}
displayMessage(message, "brother!");
此示例将一个函数传递给另一个函数。第二个函数接收一个函数作为参数,然后在一个log
命令中执行该函数。这个最初被称为message
的函数返回一个字符串,该字符串将显示在同样在console.log
方法中传递的字符串旁边。
函数是 JavaScript 语言中非常重要的一部分,我将在第五章中详细介绍它们。
我将在这里介绍的另一个概念是对象的概念。我还将在第四章中更详细地讲述对象。
目标
对象是 JavaScript 使用方式的核心。在许多方面,JavaScript 中的对象就像编程之外的世界中的对象。(确实存在;我只是看了一下。)在现实世界中,一个对象只是一个“东西”(很多关于面向对象编程的书把对象比作名词):一辆车,一张桌子,一把椅子,还有我正在敲的键盘。对象有
-
属性 s (类比形容词):车是红色。
-
方法 s (类似句子中的动词):发动汽车的方法可能是转动点火钥匙。
-
事件 : 转动点火钥匙导致汽车启动事件。
面向对象编程试图通过模拟现实世界的对象来简化编程。假设您正在创建一个汽车模拟器。首先,创建一个汽车对象,赋予它类似于颜色和当前速度的属性。然后你需要创建方法:也许一个 start 方法来启动汽车,一个 brake 方法来减速汽车,你需要向其中传递关于刹车应该压得多紧的信息,以便你可以确定减速效果。最后,你需要知道你的车什么时候出了问题。在 OOP 中,这被称为事件。例如,当油箱油量不足时,汽车会发出通知(仪表盘上的灯)让你知道该加油了。在这段代码中,您将监听这样一个事件,以便您可以对此采取一些措施。
面向对象编程使用这些概念。这种设计软件的方式现在非常普遍,并且影响了编程的许多领域——但是对你来说最重要的是,它是 JavaScript 编程的核心。
您将使用的一些对象是语言规范的一部分:例如,String
对象、Date
对象和Math
对象。这些对象提供了许多有用的功能,可以节省您大量的编程时间。例如,您可以使用Date
对象从客户端(比如用户的设备)获取当前日期和时间。它存储日期并提供许多有用的与日期相关的功能,例如将日期/时间从一个时区转换到另一个时区。这些对象通常被称为核心对象,因为它们独立于实现。浏览器还可以通过对象进行编程,您可以使用这些对象来获取有关浏览器的信息并更改应用的外观。例如,浏览器提供了Document
对象,它代表 JavaScript 可用的网页。您可以在 JavaScript 中使用它来向 web 浏览器的用户正在查看的 web 页面添加新的 HTML。如果您在不同的主机上使用 JavaScript,例如 Node.js 服务器,您会发现托管 JavaScript 的服务器公开了一组非常不同的主机对象,因为它们的功能与您想在 web 服务器上做的事情有关。
注意
尽管本节从面向对象的角度介绍了 JavaScript,但该语言本身是一种多范式语言,您可以将它用作函数式、命令式或事件驱动语言。
随着本书的深入,您将更深入地了解对象:JavaScript 语言的核心对象、浏览器使用 JavaScript 访问和操作的对象,以及您自己的自定义对象。不过现在,你只需要知道 JavaScript 中的对象是可以用来给网页添加功能的实体和,并且它们可以有属性和方法。例如,Math
对象在其属性中有一个表示 pi 值的属性,在其方法中有一个生成随机数的属性。
摘要
在这一章中,你学习了高级的 JavaScript。您了解了为什么 JavaScript 是开发人员工具箱中的一个好工具。您还学习了一些基本语法以及如何向代码中添加注释。您看到了如何使用变量保存数据以备后用,以及如何使用函数按需准备代码。另一个主题是代码执行(浏览器如何从上到下读取代码)。最大的部分是对对象的讨论。如果您打算在客户机或服务器上使用 JavaScript,理解对象是 JavaScript 的核心是很重要的。
接下来,作为一名 JavaScript 开发人员,您将看到一些可用的工具。
二、JavaScript 和开发工具
如果您是开发 JavaScript 应用的新手,有很多东西需要考虑。一个经常被问到的问题是“我从哪里开始?”这就是本章的目的,帮助你找到可以帮助你用 JavaScript 开发应用的资源和工具。我还将介绍这些工具的一些基本用法。这一章的目的不是作为一个明确的指南或产品定位,但它应该给你指出正确的方向。
这一章我将介绍几个主题:
-
教程和资源
-
集成开发环境(ide)
-
Node.js 和 npm 快速介绍
-
Git 和 GitHub
教程和资源
有很多网站可以用来收集关于 JavaScript、HTML 和 CSS 的信息。以下是一些更有用的网站:
-
https://developer.mozilla.org/en-US/docs/Web/JavaScript
的 MDN web 文档 -
www.freecodecamp.org/
免费代码营 -
www.khanacademy.org/computing/computer-programming/html-css-js
可汗学院 -
Atlassian 在
www.atlassian.com/git/tutorials
关于 Git 的教程 -
Node.js 和 Express 框架在
https://developer.mozilla.org/en-US/docs/Learn/Server-side/Express_Nodejs
选择这些网站是因为它们提供好的信息并且是免费的。还有其他网站提供了极好的信息。Egghead.io ( https://egghead.io/
)提供一些免费教程和订阅服务。我也推荐网站前端大师( https://frontendmasters.com/
)和复数视线( www.pluralsight.com
)。
集成开发环境
IDE 是用来编辑代码的软件。一些编辑器通过使用扩展来处理任何语言。其他的是为了优化一种语言的体验。和浏览器一样,有很多编辑器可以选择。
一个流行且免费的选择是 Visual Studio 代码( https://code.visualstudio.com/
)。它提供了编辑 HTML、JavaScript 和 CSS 以及其他语言所需的一切。
其他编辑器包括
-
web form(
www.jetbrains.com/webstorm
-
括号(
https://brackets.io/
) -
崇高的文字(
www.sublimetext.com/
)
这些工具将帮助您开发任何类型的软件应用。它们通常具有的特征是
-
自动完成(Autocomplete): IDE 指出如何完成你正在编写的代码行。
-
调试工具:你可以在 ide 中观察变量的值。
-
内联命令行接口:IDE 让你在命令行上执行操作。这对于处理节点模块和版本控制系统(如 Git)非常有用。
-
可扩展的:他们能够添加一些特性,让你可以用除了基本的 HTML、CSS 或 JavaScript 之外的语言进行编辑。
如果您已经安装了 Visual Studio 代码(图 2-1 ,您可以制作一个简单的 HTML 页面,用于本练习的其余部分。
图 2-1
Visual Studio 代码是一个运行在 Windows、MacOS 和 Linux 上的源代码编辑器
在计算机上的任意位置创建一个文件夹,然后在编辑器中打开该文件夹。你将制作你的第一个 HTML 页面。在学习本章中的示例时,您将把该文件夹设置为本地 web 服务器。
我不会深入 HTML 如何工作的所有细节;对于这个练习,我会给你一个快速模板。这应该足够让你开始了。
Control-N(如果您使用的是 MacOS,则为 Command-N)将打开一个新文件。将以下代码添加到您的文档中:
<!DOCKTYPE html>
<html lan="en">
<head>
<meta charset="utf-8">
<title>Chapter 2</title>
<body>
<p>Hello World</p>
</body>
</head>
</html>
另存为index.html
。这是你的第一页。此时,您可以将它拖放到您的浏览器中。这和你从网上得到的服务是不一样的。没关系,因为您将在下一节中解决这个问题。
Node.js
这将是对 Node.js 的一个非常高层次的介绍,在以后的章节中,你将会看到一些展示这个工具强大功能的用例。
Node 是一个开源的 JavaScript 环境,允许您在服务器端执行 JavaScript。这使您能够使用相同的语言在客户端和服务器上运行代码。
节点使用模块集合。模块构成了 Node 的核心功能,允许你使用文件系统、网络协议(HTTP、DNS 等)等。)、二进制数据以及与数据库对话的能力。
获得节点的最快方法是直接从网站( https://nodejs.org/
)。在网站上,下载被描述为“推荐给大多数用户”的版本(在撰写本文时,它是 8.11.3)。安装完成后,您距离将上一个示例中的文件夹设置为 web 服务器又近了一步。
节点没有图形用户界面。要使用它,您需要习惯使用命令行。这个例子中没有很多命令。以下是你要做的事情:
-
下载一个名为 http-server 的模块,它可以让你像从一个 web 服务器上下载文件一样为你的文件夹提供服务。
-
导航到包含源代码的文件夹,使用该模块启动您的站点并在浏览器中查看它。
大多数 ide 都有内置的终端模拟器。如果您使用的是 Visual Studio 代码,请转到“视图”,然后选择“集成终端”。这将是您项目的命令行界面。
您要做的第一件事是确保该节点已经安装。在提示符下,键入
node -v
这应该会返回节点的版本号,如图 2-2 所示。如果这不起作用,请确保安装了该节点。
图 2-2
Visual Studio 代码有一个内置的终端,允许您执行命令
如果一切正常,安装模块,如图 2-3 所示。这是使用节点包管理器。包管理器是一种组织代码库的方式。它可能包含像这样的实用程序或像 Angular 这样的其他库来帮助构建您的项目。
图 2-3 显示加载节点模块的结果。让我们来看一下第一行,因为这是你需要做的事情。节点包管理器命令以npm
开始。要安装一个模块,告诉npm
你想安装的模块的名字。如果你想让那个模块在你电脑的任何地方都能工作,就加上-g
。
图 2-3
安装 http-server 模块。它允许你使用硬盘上的任何文件夹作为本地服务器。
将命令行界面内置到 IDE 中的一个好处是,它确切地知道您正在哪个文件夹中工作。所以现在您可以让您的模块将文件夹转换成本地 web 服务器。键入以下命令:
http-server
您应该会看到类似图 2-4 的内容。
图 2-4
运行 http-server 模块,使硬盘上的任何文件夹成为本地 web 服务器
只需几个简单的步骤,您现在就可以让硬盘上的文件夹显示 HTML 页面了。你看到的两个数字是你文件夹的 IP 地址。第一个永远是你的机器。第二个数字是您网络上的地址。如果你网络上的任何人想看你在做什么,这是很有用的。
冒号后面是一组数字。在图 2-4 中,它是 8080,这是网络上的端口,通过它提供 HTML 页面。数据可以通过不同的端口发送或接收到服务器。这个号码,8080,经常被使用。未来的例子将显示从不同的端口提供 HTML 数据。
您可以按住 control 键并单击任一地址,您的浏览器将会打开并显示如图 2-5 所示的页面。
图 2-5
在浏览器中查看运行 http-server 模块的结果。你可以提供 HTML 页面。
有了新的设置,您现在可以创建和编辑 HTML、JavaScript 和 CSS 了。使用 Node,您可以将硬盘上的任何项目文件夹转换成本地服务器。这为现代前端开发人员工作流提供了一些重要的工具。
现在就出现了跟踪代码的问题。当您对网站进行更新并添加更多文件时,记录您所做的更改是一个很好的习惯。如果你有问题,这不仅仅是做一个“撤销”。想象一下,您向站点添加了一些功能,然后意识到您需要将它备份到以前的状态。版本控制对于项目来说非常重要,尤其是当不止一个人参与项目时。
在下一节中,我将讨论 Git 以及 GitHub 和 Bitbucket 之类的网站。
版本控制系统
使用任何类型的版本控制系统都是一个好主意。项目会随着时间的推移而增长和变化,获得项目的“快照”是很好的。Git 已经成为实现这一点的流行方式。在本节中,您将
-
安装 Git。
-
使用 Git 来跟踪您的文件。
-
创建一个 GitHub 帐户。
-
将您的项目上传到 GitHub。
Git 很容易安装。前往 https://git-scm.com/
。您可以为您正在使用的任何操作系统安装它。该网站提供了学习 Git 如何工作的资源。免费电子书可以在 https://git-scm.com/book/en/v2
找到,其他教程可以在 https://try.github.io/
找到。
这是另一个没有 UI 的工具。与 Node 类似,您可以使用命令行来移动。你可以使用一些工具来解决这个问题。Sourcetree 是一个免费的工具,可以帮助你使用 Git ( www.sourcetreeapp.com/
)。本章中的示例将继续使用命令行。
注意
如果你运行的是 Windows,安装 Git 之后你可能会额外安装一个叫 Git Bash 的东西。该工具模拟 Unix 或 MacOS 上的命令类型。和集成编辑器真的是一回事。唯一的区别是它是一个独立的应用。现在,只需使用您的集成终端。
如果您打开了 IDE,请返回终端。首先要做的是确保已经安装了 Git。一个快速的方法是查看安装的版本。在终端内,键入
git –version
类似于检查 Node 的版本,这将返回当前安装的 Git 版本。
一旦您确认 Git 已经安装并运行,您就可以初始化该文件夹来使用 Git。你这样做:
git init
这将设置您的文件夹使用 Git。通过创建一个名为.git
的不可见文件夹(你可以改变你的文件夹设置来看到它),Git 将不仅能够跟踪文件,而且能够随着时间的推移跟踪文件的内容。
现在您可以检查文件的状态,看看它们是否被跟踪。在命令行中,键入
git status
这应该会输出项目的状态(图 2-6 )。
图 2-6
Git 使用分支模型。当前分支是 master。在这个例子中,index.html 没有被 Git 跟踪。有关 Git 的完整教程,请参考“教程和资源”一节。
此时,Git 没有跟踪您的文件。如果您想更改它,请键入
git add index.html
参见图 2-7 。
图 2-7
Git 现在知道它需要跟踪 index.html。
这告诉 Git 跟踪这个文件,所以如果文件的内容有任何变化,Git 都会知道。确保 Git 知道跟踪什么非常重要。同样重要的是提交。你可以把它们看作是你的文件在任何时候的快照。
在命令行中,键入
git commit -m "my first commit"
看一下图 2-8 。您要求 Git 执行提交并添加了-m
标志。这允许您添加一条关于您将要进行的提交的消息。
图 2-8
使用 git commit 命令创建文件的快照
所有这些都是本地的。Git 知道文件正在被修改。如果您对当前文档进行了任何更改,例如,如果您更改了消息,Git 将会知道。进行更改,保存文件,然后再次检查状态。参见图 2-9 。
图 2-9
Git 识别出文件的内容已经更新
现在文件不同了,您可以再次添加文件并进行新的提交。所有这些提交都保存在一个日志中。如果您想查看提交的历史,请键入
git log
图 2-10 显示了该分支的历史。每次提交都带有一个散列值、作者和提交的日期/时间。
图 2-10
Git 日志将显示所有以前的提交
在图 2-10 中,您可以看到,在向您的提交添加消息时,您确实应该比这里看到的更具描述性。该消息可以让您很好地了解正在发生的事情,而无需查看代码。
此时,您可以在 Git 中创建一个存储库。添加要跟踪的文件,并制作该文件的多个快照。如前所述,所有这些都是本地的,如果您自己工作的话,这是没问题的。如果你和其他人一起工作呢?然后,您需要一个服务器,不仅可以托管您的代码,还可以跟踪更改。这就是 GitHub 和 Bitbucket 等网站的用武之地。
对于你的目的,这些网站做同样的事情。它们是存放代码的地方。您创建的存储库可以是公共的,也可以是私有的。
顾名思义,公共存储库可供任何人查看、下载或克隆。人们也可以通过发送变更和错误修复来为项目做出贡献。
GitHub(在撰写本文时最近被微软收购)允许免费使用公共、私有和开源的存储库。Bitbucket 为小团队(最多五个用户)免费提供无限的私有存储库。
你可以注册这两个网站。本节的截图将使用 GitHub。登录后,您可以创建新的存储库。这将是一次公开回购。你可以看到它附在你的账户上(图 2-11 )。
图 2-11
在 GitHub 上创建新的存储库
一旦创建了 repo,您将获得关于如何将本地机器连接到 repo 的说明。将这个例子命名为 MyFirstProject(图 2-12 )。
图 2-12
关于如何将本地 repo 连接到 github 的说明
当你想让来回购的人知道这个项目是关于什么的时候,你可以更新这个文件。当您第一次开始一个项目时,关于它是如何制作的、如何安装以及使用了什么技术的任何注释通常都在自述文件中。
如果您阅读了关于 Git 的部分,这些命令应该很熟悉。项目的初始化和添加 README 文件应该在那一节中已经熟悉了。这里的新功能是增加了远程功能。
Git 使用了一个叫做 remotes 的概念;这些遥控器是服务器上文件的副本。这将允许您将文件推(或上传)到远程源。
您可能会注意到示例中的-u
。它设置远程分支的默认位置。将来,当您尝试使用git pull
时,这将从远程服务器获取(或下载)任何更新版本的代码。
一旦告诉本地 Git 实例远程位置,下一个命令就是将文件推送到服务器。
如果安装了 IDE 或 GitBash,所有这些命令都可以在终端窗口中执行。
推送到远程回购的代码并没有给你一个功能性的网站。请记住,这些服务是用于跟踪代码随时间的变化,并不显示您的网站所做的结果。
一旦你把你的代码推送到回购,你就可以检查站点了(图 2-13 )。
图 2-13
使用命令行将当前本地项目推送到 GitHub
注意
当尝试将您的代码推送到服务器时,可能会要求您输入密码。这是您刚刚创建的帐户的密码。一旦进入,一切都会起作用。
如果你厌倦了每次添加密码,你可以设置 Git 记住你的密码。进入 https://help.github.com/articles/caching-your-github-password-in-git/
为你正在使用的操作系统设置 Git。
如图 2-14 所示,GitHub 会显示你的项目的所有已知信息。如果您决定拥有一个开源项目,这将为您提供一个可视化的参考和控制更新的方法。如果您创建了一个私有存储库,您也可以获得所有相同的信息。
图 2-14
github 上一个有效的公共存储库
摘要
在这一章中你做了很多工作。到目前为止,您已经选择了一个 IDE,设置了版本控制,甚至创建了一个远程存储库。唯一缺少的是一个你可以向人们展示的现场。第十三章是你将构建一个可以部署到实时服务器上的工作站点的地方。
现在,安装并设置好所有的开发工具后,您就可以深入研究这种语言并让浏览器为您服务了。下一章将介绍 JavaScript 如何处理数据类型。
三、JavaScript 变量
在本章中,你将学习一些基本的 JavaScript 数据类型。当你想让计算机保存一些信息时,这些信息就是某种“类型”例如,电子邮件地址是一种称为字符串的类型。电脑识别出你的电子邮件地址是一系列字母、数字和符号。
如果您要执行计算,计算机会将您用来执行计算的数据视为数字。它可以计算数字,但不能计算字符串。
在阅读本章的过程中,您将探索其中的一些数据类型。此外,您还将看到变量是如何工作的。
在这一章中,我使用术语“环境”它只是表示 JavaScript 在哪里执行,可能是在浏览器中,也可能是使用 Node.js。
在 JavaScript 中声明变量
JavaScript 是一种松散类型或动态语言。这只能说明 JavaScript 非常适合你。取决于你对此的感受,这可能是好的也可能是坏的。当您希望环境保存一个值时,您必须声明一个变量。
如果你创建了一个变量并赋予它一个值,那么你可以把这个值改变成完全不同的值。让我们看看实际情况(图 3-1 )。
图 3-1
使用 Chrome 中的控制台测试 JavaScript 变量
打开 web 浏览器,找到开发人员工具,然后转到控制台。当你在控制台时,输入图 3-1 中的代码。将userName
的值设置为"Violator"
;你可以看到它在引号中,这使它成为一个字符串。然后它被重新分配给数字42
,它的字符周围没有引号,所以它被认为是一个数字。
JavaScript 允许不同的变量声明方式。你需要遵守一些规则。
在创建变量的时候,首先需要让环境知道这是你的意图。这里我将介绍关键字的概念。
本例中使用的关键字是var
(变量的缩写)。这个关键字将告诉环境你想要保留一些信息。
JavaScript 中还内置了其他关键字,可以让环境知道如何处理日期、数字,甚至是浏览器中当前的 HTML 文档。您将在未来的示例中探索它们。
在这里,您创建了一个名为userName
的变量。这种大写叫做骆驼大小写,其中每隔一个单词的第一个字母都有一个大写字母。这不是创建变量的唯一方法,但你会经常看到。
在 JavaScript 中命名变量时,不能以数字或符号开头(考虑像@ a 符号这样的字符)。所有变量都可以以字母开头;无论是大写还是小写都没有关系。其后的任何字符都可以使用数字或符号。变量名称中也不能有空格。看看清单 3-1 中的例子。
var user name = "Violator"; // not a valid variable
var !username = "Violator"; // not a valid variable
var 1user_name = "Violator"; // not a valid variable
var user_name = "Violator"; // valid variable
var userName = "Violator"; // valid variable
var username = "Violator"; // valid variable
Listing 3-1Valid and Invalid Ways to Create a Variable
这并不是一个关于变量命名的详尽列表,但是它应该给你一个在 JavaScript 中如何命名变量的概念。
注意
要获得更详尽的列表,看一看同样由 Apress 出版的 JavaScript 食谱。
一旦您告诉环境您需要保存一些信息,接下来的事情就是将数据赋给该变量。该数据属于某种类型。在本例中,您将字符串"Violator"
赋给了新创建的变量。
字符串是可以以文本形式使用的数据表示。电子邮件的内容、用户名和密码都是字符串。在 JavaScript 中创建字符串变量时,您可以在想要使用的字符周围使用单引号或双引号。重要的是要始终如一。你不能以单引号开始,以双引号结束,或者反过来。
在字符周围使用引号是很重要的,因为将"1"
(引号中的数字)或1
(只是数字)赋给一个变量是有区别的。第一个是字符串;第二个是数字。数字在 JavaScript 中是一种不同的数据类型。符号也是如此;"@"
被认为是一串。
每条语句的末尾都有一个分号。这就像是句尾的句号。如果你不添加一个,环境会很好地试图理解你的意思,并会在它认为必要的地方插入一个。
您可能会遇到的一个问题是,环境将分号插入了错误的位置。这将在运行时产生错误。
然而,如果你添加它,它确实使你的代码更容易阅读。因此,作为最佳实践,在每个语句的末尾添加分号。
总之,在 JavaScript 中声明变量时,使用关键字(在本例中是var
),命名变量,然后赋值。
在 JavaScript 中重新分配变量
JavaScript 中的一些变量可以被重新分配。这意味着,一旦你给了一个变量一个值,很容易,在某些情况下,有必要回到那个变量,给它一个全新的值。
在 JavaScript 中更新现有变量的一个例子是,如果您正在跟踪一个用户是否有权访问某些东西。您可以使用变量作为标志来检查某人的状态。这里有一个例子:
if ( hasAccess === true ){
hasAccess = false;
}
这个例子说明了一个变量可以有多个值。变量hasAccess
的值可以是真或假,这里您使用一个if
语句来检查当前的值。
使用 true 或 false 作为值使您的变量成为布尔数据类型,这意味着它只能有一个值。布尔值只能为真或假。
有时候你可能需要一个变量有一个常量值。这种情况下的关键词是const
。任何时候你都不希望这个变量的值改变。让我们看一个例子。
无法重新分配的变量
如果您有不应该更改的数据怎么办?在这种情况下,您希望变量包含一个无论发生什么都保持不变的值。JavaScript 提供了这样一种变量类型,它被称为常量。
常量是一种变量,在第一次赋值后就不能更改。图 3-2 显示了一个试图改变常量值的例子。
图 3-2
标记为常量的变量在第一次给定值后不能改变
使用关键字const
将允许您做与使用var
相同的事情,不同之处在于您不能在以后重新赋值。在图 3-2 的例子中,你会看到浏览器抛出一个错误。
虽然不能将单个值重新分配给常量,但如果要分配一个对象,则该对象的属性可以更新。清单 3-2 包含了一个例子。
const myObj = {}
myObj.firstName = "Vince"
console.log(myObj)
{firstName: "Vince"}
myObj.lastName = "Clarke"
console.log(myObj)
{firstName: "Vince", lastName: "Clarke"}
myObj = {}
Uncaught TypeError: Assignment to constant variable.
Listing 3-2Assigning an Object to a Constant
因为常数是一个对象,所以您可以访问该对象的属性并更新它们。我将在下一章全面阐述对象的概念。现在,把一个物体想象成名词。
这个“东西”有属性,(高度,宽度,颜色等。).对象的属性(这个“东西”)可以更新。但是变量myObj
不能被重新分配。在最后一行中,您可以看到当试图给对象重新分配一个新值时,浏览器将抛出一个错误。
*到目前为止,我已经讨论了创建变量、为变量赋值以及更新变量的能力。我展示了不能给变量赋值的例子。
下一节将介绍环境如何控制变量的范围、上下文或“作用域”。
只能在单个代码块中使用的变量
我们先定义一个代码块。当您使用 JavaScript 函数时,经常会看到一组花括号({})。这是你的代码块。这些括号中的任何内容都是将要执行的代码。当使用关键字let
来声明你的变量时,浏览器知道你赋给变量的值只能在那个块中看到。清单 3-3 包含了一个例子。
for (let i = 0; i < 10; i++) {
console.log(i); //output = numbers between 0 and 9
}
console.log(i) //error i is undefined
Listing 3-3A let Statement Only Has a Value While Inside the Code Block
这里有一个循环。它创建一个名为i
的变量,从值0
开始,只要i
没有值10
,它就给i
加 1。
当这个循环发生时,您在控制台中打印出i
的当前值。这将显示值0
到9
(只要该值小于10
)。
所有这些都发生在花括号内。一旦这个循环完成,变量i
消失。环境不再考虑它了。所以,当你试图在下一行引用这个变量时,它会抛出一个错误。
所以,概括一下,当代码在花括号中执行时,使用的变量有一个值。当块执行完所有代码后,代码的其他部分就不能再访问该变量了。它已经不存在了。
这是let
和var
的主要区别之一。清单 3-4 显示了一个使用var
的类似例子。
for (var i = 0; i < 10; i++) {
console.log(i); //output = numbers between 0 and 9
}
console.log(i) //returns 10
Listing 3-4A var Statement Will Retain Its Value After the Code Block Has Been Executed
如果var
的行为类似于let
,您将得到相同的结果,其中i
的值将返回undefined
。相反,循环的行为完全相同,但是在这种情况下,您的变量是可以在循环外部访问的,并打印出10
的值。
用var
关键字创建的变量是基于它当前的执行上下文来声明的。在第一种情况下,循环不在函数内部;因此,该变量在范围上成为全局变量。
在它的正下方,有一个相同的循环,但是这次是在一个函数块中。然后,该变量位于不同的执行上下文中。如果你试图在函数之外打印变量的值,浏览器将抛出一个错误。参见列表 3-5 和 3-6 。
for (var i = 0; i < 10; i++) {
console.log(i); //output = numbers between 0 and 9
}
console.log(i) //returns 10
Listing 3-5When Creating a Variable with the var Keyword, It Will Exist Inside the Current Execution Context. The Code Here Is Outside a Function, Making the Context Global.
function goLoop(){
for (var i = 0; i < 10; i++) {
console.log(i); //output = numbers between 0 and 9
}
}
goLoop();
console.log(i) //returns error
Listing 3-6When Creating a Variable Using the var Keyword Inside a Function, the Execution Context is Local to the Function
当处理变量时,在var
上使用let
将确保变量只存在于你创建的代码块中。变量表现不同的原因是因为变量提升。下一节将更详细地解释吊装。
面试问题
var
、const
、let
有什么区别?说出一些 JavaScript 数据类型。
可变提升
在面试中,你可能会被问及variable hoisting
。听起来比实际困难得多。这一节将澄清关于它如何工作的任何困惑。
当浏览器经历其编译阶段时,它会将函数和变量声明放入内存。
下面是一个如何声明变量的示例:
userName = "Stephanie";
这与使用关键字let
初始化变量是不同的。这里,你给一个变量赋值,没有使用任何关键字。因为没有给这个变量分配关键字,所以它被认为是一个全局变量。这和使用var
关键字是一样的。
下一个示例显示了工作代码,尽管它看起来不应该工作:
userName = "Stephanie";
console.log(userName); //returns Stephanie
var userName;
虽然看起来结果应该是undefined
,但它返回了正确的值。与其他示例类似,在范围或执行上下文没有设置为代码块的情况下,使用关键字var
。
被提升的变量移动到当前作用域的顶部。因此,如果没有函数,变量将被移到全局范围。如果一个变量(使用var
关键字)在一个函数中被声明,它将移动到该作用域的顶部。
变量提升在变量被声明时生效,而不是被赋值时生效。如果您声明了一个没有值的变量,即使它没有值,它仍然会被提升。清单 3-7 包含了一个例子。
function checkVars(){
console.log(username); //returns undefined
var username = "Hunter";
console.log(username); //returns Hunter;
}
checkVars() //executes function;
function checkVars(){
var username;
console.log(username); //returns undefined
username = "Hunter";
console.log(username); //returns Hunter;
}
checkVars() //executes function;
Listing 3-7Variables Are Hoisted When They Are Declared, Not When They Are Assigned a Value. These Two Examples Produce the Same Result.
面试问题
什么是吊装,它是如何工作的?
严格模式
严格模式告诉浏览器运行更受限制的 JavaScript 版本。这是你可以选择的。在严格模式下运行您的代码将防止浏览器犯错误,从而难以优化代码。
当某些语法可能会被添加到 JavaScript 的未来版本中时,它还会阻止该语法的执行。它还消除了无声错误(没有浏览器反馈的错误)。
您可以对整个 JavaScript 文件或单个函数调用严格模式。要将其添加到整个脚本的顶部,请将use strict;
添加到您的代码中。
在清单 3-8 中,如果你声明了一个变量而没有初始化它,浏览器会抛出一个错误。
x = "think tank" //Reference Error: x is not defined
Listing 3-8Variables Declared While in Strict Mode Must Be Initalized
如果您试图将一个对象赋给一个尚未初始化的变量,就会出现这种情况:
X = {user:"Player One", score:1000} //Reference Error: x is not defined.
单个函数可以在严格模式下运行,只需在函数体中添加相同的代码行。参见清单 3-9 。
function myFunction(){
"use strict";
// add commands here
}
Listing 3-9Using “use strict” Inside a Function Declaration Will Tell the Browser to Run Just That Function in Strict Mode
使用严格模式可以确保浏览器以最有效的方式执行您的代码,并在出现错误时提供最多的反馈。
摘要
本章讲述了如何创建变量以及何时应该使用某种类型的变量。一些变量存在于代码块中,而另一些则无法更新。
您探索了可变提升的概念。当使用关键字var
时,变量可以被提升或移动到执行上下文的顶部。这可能会导致这样的情况,即使一个变量已经被声明,它可能还没有一个值,所以将返回undefined
作为一个值。
最后一节介绍了在严格模式下运行代码的一些好处。它将执行更高效的 JavaScript 版本,并在浏览器忽略错误时通过返回错误给出更好的反馈。
下一章将更详细地介绍对象是如何工作的,并介绍一种叫做数组的对象。*
四、JavaScript 对象和数组
最后一章介绍了变量。使用变量,您可以保存信息以备后用。随着应用变得越来越复杂,记住用户设置、URL 或表单内容的能力变得越来越重要。
使用变量会遇到的一个问题是,不管是什么类型,一次只能保存一条信息。如果你给一个变量赋一个新值,原来的值就没了。这可以防止您持有复杂类型的数据。
为了模拟复杂的事物而创造大量的变量也是不合理的。例如,如果有人正在填写表单,那么很难管理表单中每个项目的所有变量。
对象是分组数据的好方法。JavaScript 中也使用对象来帮助您在应用中执行操作。
document
对象就是一个很好的例子。它可以让你浏览浏览器中的 HTML 页面。
Math
对象可以帮助执行数学运算,而Date
对象不仅可以让您检索当前日期,还可以帮助计算过去或未来的日期。它还可以帮助您以正确格式输入日期。
本章将讨论什么是 JavaScript 对象,对象如何工作,并解释什么是属性和方法以及它们如何工作。
首先,我们来谈谈主机对象和本机对象的区别。然后,您可以开始学习如何用 JavaScript 创建自己的对象。
宿主对象或本机对象
这不是一个很长的部分,但是它将阐明宿主对象和本机对象之间的区别。由于 JavaScript 是一种可以在多种环境下工作的语言,因此代码本身在不同的环境下可能会有不同的表现。举例来说,浏览器中可能有些东西,如历史或位置,在服务器上是不可用的。还可能有其他环境,如具有独特功能的移动设备。在很大程度上,我将讨论 JavaScript 如何在浏览器中工作。
您可能会遇到术语“同构 JavaScript”在这种情况下,JavaScript 应用能够同时在客户端(浏览器)和服务器上运行。
本机(有时称为内置)对象是 ECMAScript 标准的一部分。ECMAScript 本身不是一种语言,而是一种脚本语言的规范。JavaScript 是该规范最流行的实现。其他实现包括 ActionScript。基于这种理解,ECMAScript 规范将定义独立于环境的对象。这些对象包括
-
目标
-
排列
-
承诺
-
数据
宿主对象是代码运行环境的一部分。例如,浏览器中的宿主对象包括
-
窗户
-
文件
-
历史
-
航海家
注意
如果您对浏览器中有哪些本地对象感兴趣,请查看位于 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects
.
的 MDN web 文档
您构建的许多应用将利用这两种类型的对象。您通常需要一种类型的对象来帮助处理另一种对象。
最常用的主机对象之一是document
对象。您可以使用它来检查当前文档中发生了什么。这方面的一个例子是更新旅行日期。使用document
和Date
对象,您可以按需更新浏览器中的信息。
既然您已经对本机对象和内置对象有了很好的理解,那么让我们深入了解如何使用泛型对象的一些细节。
面试问题
宿主对象和本机对象有什么区别?举几个例子。
解释对象
我提到了能够将数据分组在一起。在这里我将开始解释这是什么意思。
在 JavaScript 中,几乎所有东西都是对象。一个对象代表一个“东西”使用事物(或名词)的概念类似于你如何描述现实生活中的物体。现实生活中的物体有时可以被描述为具有高度、重量或颜色。
记住这一点,当想要获得一个对象的细节时,这些细节被称为属性。JavaScript 有一长串对象的内置属性和方法。让我们从浏览器的document
对象开始进入列表。
文档对象简介
如果您想知道浏览器中当前加载的文档的标题,您可以这样做:
let currentTitle = document.title;
这里,您正在创建一个变量,就像上一章一样,并为它赋予当前文档中标题的值。
为了让这个有意义,让我们从右边开始,然后向后看。在这里,您将带至document
对象。该对象由浏览器(您的宿主环境)提供。
document
对象有一个名为title
的属性。你可以用点符号(在单词之间用一个.,就像在document.title
中一样)来问“你的头衔是什么?”问题。使用点,你分离出你感兴趣的主要事物,然后要求更多的细节。在这种情况下,您需要文档的标题。如果你想保留这些信息以备后用,你需要一个地方来存放它们。
如果您想从浏览器中检索信息,这很好。但是,您可能会发现自己需要更新浏览器中当前的信息。
其语法非常相似:
document.title = "My New Title";
您可以看到代码与您之前的示例非常相似。在这种情况下,您正在设置属性的值,而不是获取值(编程中经常使用术语“getter”和“setter”)。
本地和宿主对象都有能力使用属性和一种叫做方法的东西来处理信息。方法是允许您对特定数据执行某种处理的函数。一个例子是,如果你想在你的 HTML 文档中找到基于 CSS 类的东西。document
对象可以帮你做到这一点:
let myClassElements = document.getElementsByClassName('myCssClass');
与标题示例类似,document
对象将执行一个函数(记住这个词,以后再说),它将查看整个 HTML 文档,并将使用这个类名的每个 HTML 元素发送回来。
这个方法唯一需要的是一个参数(引号中的字符串)。这样,函数就知道要寻找什么了。
JavaScript 中的其他对象更加具体。在下一个例子中,我将讨论什么是数组以及如何使用它。
面试问题
方法和属性有什么不同?使用 getters 和 setters 是什么意思?
数组和堆栈
数组是一个更加专门化的对象。它具有一般对象所没有的功能。数组不仅仅擅长保存数据组;它还擅长将数据保持有序。
形象化阵列工作方式的一种方法是想象一堆盘子。堆栈中的每个板代表数据。当讨论数组中每个元素的位置时,使用的术语是元素。如果你想象从下到上数盘子,通常你会从一开始数,这是有意义的。有些编程语言从 1 开始计数;但是,JavaScript 从零开始计数:
let myArray = new Array();
myArray.push('some data'); //count starts at zero
myArray.push('some other data') //count is now one
console.log(myArray[0]) // some data
这是一个向数组添加数据的例子。每向堆叠中添加一个板,堆叠中的板数量就增加一个。JavaScript arrays 称之为length
。
我把数组描述成一堆盘子。栈也是一种数据结构。关于数据结构的问题也可能成为你在面试中遇到的问题。
下一个示例显示了如何创建数组并开始添加值:
//create an array using the new operator
let myArray = new Array();
//create an array using square braces
let myOtherArray = [];
这里你有不同的方法来得到相同的结果。每一个都将属于一个数组的相同的属性和方法赋给你的变量。这将把你的变量变成一个数组对象。
既然已经有了 array 对象,就可以使用它附带的属性和方法了。此示例显示了如何添加数据(将样品板添加到堆叠中):
let nameArray = new Array();
nameArray[0] = 'Hunter';
nameArray[1] = 'Hayes';
nameArray[2] = 'Grant';
之前,我说过要数你的盘子。在这个例子中,您为每个新创建的元素分配一个索引值;最终结果是你的数组或堆栈。
这是确保为特定元素赋值的好方法。您可能遇到的一个问题是无序添加元素的能力。这里有一个例子:
let nameArray = new Array();
nameArray[0] = 'Hunter';
nameArray[2] = 'Grant';
虽然这段代码是正确的,而且浏览器不会告诉你它有什么问题,但是如果你试图获取一个还没有赋值的元素,浏览器会返回undefined
。
获取数组的长度
其他语言要求在创建数组时在数组中设置许多元素。JavaScript 不会这样做。您可以添加任意多的元素。
在将元素添加到数组中之后,如果您想要获得当前正在处理的元素的总数,您需要使用length
:
var numberOfElements = myArray.length;
在前面的示例中,在向数组中添加数据之前,您通过编号显式地调出了每个元素。如果你只是想给下一个打开的槽添加一些东西,数组对象有一个名为push
的方法。这也是确保元素的编号顺序没有间隔的好方法。
let nameArray = [];
nameArray.push("Norah");
nameArray.push("Emily");
let numOfNames = nameArray.length; //return 2
在这个例子中,使用push
方法向数组中添加两个名字,然后询问数组的长度。如果您想单独访问每个元素,您可以通过元素编号来查询每个元素:
console.log(nameArray[0]); //returns Norah
console.log(nameArray[1]); //returns Emily
你询问关于你的两个元素的信息。记住 JavaScript 对条目排序的方式,你需要索引 0 和 1,而不是索引 1 和 2。
您可以为数组的值分配字符串,但是需要注意的是,数组可以保存您喜欢的任何类型的数据。数组的目的是保存数据组。
既然可以向数组中添加信息,那么数组对象中内置了许多有用的工具供您使用。让我们先过一遍循环。
使用循环和过滤器
现在您的数组中已经有了信息,您会经常发现需要在数组中查找特定值的情况。您可能会寻找一个电子邮件地址,甚至是一个空值。遍历所有这些信息的一种方法是使用循环。由于这是经常发生的事情,JavaScript 在 array 对象中内置了一些特性来允许您这样做。
let myArray = ['one', 'two', 'three', 'four'];
myArray.forEach((value, index, array) => {
console.log(value); //current value
console.log(index); //current index
console.log(array); //entire array
});
您了解了对象既有属性又有方法。属性帮助描述关于对象的事情,方法是对象用来处理数据的函数。
数组对象有一个名为forEach
的方法。它让您迭代或遍历数组中的每个元素。
查看每个项目的方式是使用函数。我将在下一章详细讨论函数。目前,函数是让环境为您工作的一种方式。当你想要某种动作发生时,你创建一个函数并告诉环境你需要它做什么。
在这个例子中,您有一个forEach
方法。在这个方法中,您添加了一个函数。这个函数由一组括号、一个箭头和一些花括号组成。
箭头函数实际上是一个等号,旁边有一个大于号:=>
。
括号包含定义函数可用数据的值。这里的值是占位符,您可以随意称呼它们。为了说明这一点,我将这些参数称为value
、index
和array
( value
是正在循环的元素的当前值,index
是当前索引号,array
是整个数组)。
这个例子显示了一个名为console.log
的命令。这是开发人员经常使用的东西。
使用浏览器开发工具时,您会看到一个称为控制台的部分。在这里你可以输出你正在处理的东西的当前值。
也可以直接在控制台中编写 JavaScript。当你点击回车键时,它将被执行并更新当前页面。
使用forEach
方法,您可以遍历整个数组。如果你想过滤掉数组中的一些信息呢?您可以使用filter
方法来这样做。filter
方法将根据你创建的测试结果返回一个新的数组。
为了完成这项工作,我将引入条件句的概念。条件句问“这是真的吗?”
当您遍历数组并对每个值使用您的条件时,结果将是一个新数组:
let numberArray = [1,2,3,4,5,6,7,8,9];
let oddNumbers = numberArray.filter((value, index, array) => {
if(value % 2){
console.log(value);
return value;
}
});
为了更好地理解它,我们来分解一下。该数组只是一组从 1 到 9 的数字。下一行是保存filter
方法结果的变量。这和你之前做的差不多。方法filter
内部有一个函数。该函数与forEach
方法具有相同的属性。
不同的是,你添加了一个条件:如果当前值不能被 2 整除。如果这是真的,那么取当前值并从中创建一个全新的数组,并将所有其他属于相同参数的元素添加到这个新数组中。
构建到数组中的另外两个方法是map
和reduce
方法。
map
方法让您遍历数组的所有元素,并对每个元素执行某种类型的操作。然后,通过返回函数的结果形成一个新数组,类似于筛选器示例:
let mappedValue = [1,2,3].map( (value, currentValue, currentIndex, array)=> {
return value * 10;
});
console.log(mappedValue) //returns [10,20,30]
使用reduce
方法允许您通过将数组的所有值相加来创建一个值,使用第一个值作为起点,并将所有其他元素的值加到第一个值上:
let reducedValue = [10,1,2,3,4,5,6,7,8,9].reduce( (value, index, array)=> {
return value + currentValue;
});
console.log(reducedValue) //returns 55
面试问题
给定一个数字数组,如何删除所有重复项并只返回唯一值?给定一个数组的长度,哪个数字代表该数组中的最高索引?
摘要
本章讲述了对象的概念。此外,我还谈到了如何存在语言固有的对象(ECMAScript)和环境向您公开的对象(主机对象)。我还说明了对象如何同时具有属性和方法。属性一般被认为是描述数据的方式,而方法是对数据进行操作的方式。我用数组进一步说明了这一点。数组有类似于length
的属性,它描述了数组中有多少元素。
数组也有类似于push
的方法,可以向数组中的下一个可用元素添加新数据。这个对象也有可以处理数组内部数据的方法或函数,使用内置方法为您提供新的数组。
我引入了条件,您可以让您的程序评估您的数据,并根据是否满足某个条件做出选择。我还介绍了箭头函数。
下一章将更详细地介绍常规函数是如何工作的,并将它们与箭头函数进行比较。我还将讨论关键字this
以及上下文的概念。
五、JavaScript 函数和上下文
上一章介绍了对象的概念,并用数组对象来说明如何用对象来帮助你开发应用。
您可以使用主机对象和环境对象。环境变量可能会因环境而异,因此浏览器中可用的对象可能并不总是与服务器上可用的对象相匹配。
本章将介绍功能。函数被用作对数据执行选项的一种方式。函数也很有用,因为它们可以根据需要多次重用。
创建和使用函数有不同的方式。你还会学到一些重要的概念,比如上下文和范围。这些概念在尝试处理其他对象和变量时非常重要。关于范围和背景的问题在面试中经常出现,所以很好的了解它们是如何工作的。首先,让我们创建一个简单的函数。
进行函数声明
这可能有几个名字,比如函数 声明 ,函数定义,或者函数语句。在任何情况下,创建一个基本函数都需要几样东西:
-
函数的名称
-
以逗号分隔的可选参数列表
-
将对数据执行操作的代码,用花括号括起来
这听起来可能比实际情况复杂得多。下面是一些代码:
function myFunction(optional, data, here ){
//some code goes here
}
这个伪代码示例显示了一个基本的函数声明。它以单词 function 开始,然后是你给这个函数起的名字。括号内是可选参数。根据您正在执行的操作,该函数可能需要处理数据。接下来是花括号。在大括号中,您放置了函数完成其工作所需的所有 JavaScript 代码。让我们创建一个更现实的例子:
function addNumbers(num1, num2){
return num1 + num2;
}
let result = addNumbers(2,3);
console.log(result);
这个例子很简单,但是更真实。您有一个名为addNumbers
的函数和两个由逗号分隔的参数。这类似于拥有一个变量。它们表示函数执行时将使用的数字。记住这一点,您执行一个将两个值相加并返回结果的操作。
return
命令是新的,所以让我们花点时间来研究一下。函数总是会返回一个值,并且大多数时候这个值是undefined
。然而,如果你知道你想要一个值返回,那么return
语句是必要的。return
语句将向调用函数的地方返回一个值。
这个例子结合了前几章的一些东西。您使用let
语句创建一个名为result
的变量。这个变量的值将是函数addNumbers
的结果。
这经常被描述为调用或执行一个功能。你通过名字调用一个函数,根据函数的工作方式,你传递参数给它。
本例中的最后一行将输出浏览器控制台中变量的值。
在前一章,我解释了 JavaScript 中几乎所有的东西都是对象。既然知道可以将对象赋给变量,那么也可以将对象赋给函数:
const addNumbers = function(num1, num2){ return num1+ num2 };
addNumbers(2,3);
在这里,您创建一个常量变量,并将您的函数赋给它。然后你可以像调用函数一样使用这个变量。在这种情况下,该函数没有名称,因此是一个匿名函数。这被称为函数表达式。
命名函数也可以做同样的事情。使用命名函数有助于出错时的调试过程。这样更容易找到错误发生的地方。
在审查代码时,你经常会发现大量的函数表达式和声明。然而,有其他方法可以用更短的语法创建函数。这个更短的语法改变了函数与代码其余部分的关系,并很好地介绍了关键字this
和作用域的概念。
使用箭头功能
箭头函数表达式是创建函数表达式的一种较短方式。在这一节中,我将解释其中的一些区别,并介绍作用域的概念和关键字this
的用法。
您将从一个简单的箭头函数开始:
const arrowFun = (num1, num2) => { num1 + num2 };
这里,您像以前一样创建一个常量变量。你丢掉了function
关键字,使用了有时被称为的粗箭头 : = >。花括号仍然存在。
您可能还注意到您删除了关键字return
。当使用箭头函数时,如果函数体只有一行,JavaScript 将使用所谓的隐式返回。
这只是箭头函数与您以前使用的函数类型不同的方式之一。以下是使用箭头函数时要记住的一些其他示例:
const arrowFun = num1 => num1 * 2;
如果只有一个参数,则不需要将它放在括号中。
const arrowFun = _ => 2
const arrowFun = () => 2
这两种说法是一样的。如果没有要传递的参数,可以使用空括号或下划线。使用下划线只是减少了一个需要键入的字符,但对于不熟悉 arrow 函数所有变体的其他开发人员来说,这可能会使他们感到困惑。
const arrowFun = (num1, num2) => 2
就像介绍性的例子一样,如果你的函数需要不止一个参数,那么它需要在括号中:
const arrowFun = (num1, num2)_ => { if (num1 > num2){
return 'first is larger than second';
}};
如果函数的代码块(花括号之间的部分)超过一行,那么它需要一个 return 语句:
const arrowFun = () => ({firstName:Paul});
通常,当使用函数时,代码块位于花括号之间。当试图使用 arrow 函数返回一个对象文本时,这会导致一个问题。这是因为解释器不知道你试图返回一个对象。解决这个问题的方法是在外部使用括号,在内部创建对象文字。
如果你只是想让一个函数为你做一些工作,任何一种方法都可以。使这一个不同的是关键字this
。现在是引入上下文和函数范围概念的好时机。
关键字 this 是如何工作的?
我先解释一下关键词this
是什么,什么时候用。它不是你像变量或函数那样声明的东西。这更像是一个起点。
举例来说,您将创建一个 JavaScript 文件,并从一个空的 JavaScript 文件中查看this
的值。创建一个 HTML 页面,并使用它来加载 JavaScript 文件。你的代码应该如清单 5-1 所示。
console.log(this)
Listing 5-1Load a Simple JavaScript File into the Browser
在 browser developer tools 中打开控制台,您应该会得到如下所示的输出:
Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, ...}
我将关键字this
描述为一个起点。所以,在这个当前实例中,this
指的是window
对象。
在浏览器中,window
对象被认为是一个全局对象。如果使用 Node.js,情况就不一样了。根据 MDN web 文档,“全局对象是始终存在于全局范围内的对象。”此作用域对所有其他作用域都可见。
在清单 5-2 中,您需要函数中this
的值。根据函数调用执行的位置,可能会产生相同的结果。
function getThis()(
console.log(this)
}
getThis(); //returns Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, ...}
Listing 5-2Get the Current Context of a Function in the Global Scope
代码中调用函数的地方称为执行上下文。执行上下文决定了this
的值。
注意
如果在严格模式下运行这段代码,结果将是undefined
。
当试图从对象内部获取this
的值时,执行上下文是不同的。见清单 5-3 。
var myObj = {
getThis : function ()(
console.log(this)
}
}
myObj.getThis() //returns {getThis: ƒ}
Listing 5-3Get the Value from Inside an Object
在这个例子中,起点在对象myObj
内部。方法被调用时,this
的值就是对象本身。
清单 5-4 引入了一些新的东西来帮助解释关键字this
与执行上下文相关联。它将介绍什么叫做生命(立即调用函数表达式)。顾名思义,它是一个会立即执行的函数。
(function () {
var person = "Friend";
console.log(this); // returns Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, ...}
console.log("Hello " + this,.person); //returns undefined
console.log("Hello " + person); //returns Hello Friend
})()
console.log("Hello = " person)_; //returns reference error variable not defined.
Listing 5-4Referencing a Variable from Inside and Outside the Function Scope
无需调用该函数,该函数将在加载到浏览器后立即执行。
在前面的例子中,在全局范围内声明的变量可以在函数内部访问。在当前示例中,立即调用的函数有自己的函数范围。
您可以从输出中看到,该函数位于全局范围内。函数内部声明的所有变量都存在于该函数范围内。
这样做的好处之一是,原本会出现在全局范围内的变量,现在会出现在生命范围内。这是许多开发人员使用的技术,以避免“污染”全局范围。函数和变量永远不会是全局范围的一部分。这也防止了覆盖可能具有相同名称的其他函数或变量。
面试问题
生命是什么,它如何防止函数和变量污染全局范围?
执行上下文是全局的,但变量本身是函数的局部变量,不能被示例中的最后一行访问。
请注意,关键字this
引用的是window
对象。试图使用关键字this
访问变量person
将导致返回一个值undefined
。
您可以看到,如果不小心的话,使用关键字this
引用属性和方法(变量和函数)会产生意想不到的结果。JavaScript 确实提供了一些更可预测的方法来控制执行上下文。
使用调用、应用和绑定方法
当调用一个函数时,调用发生的位置将决定该函数的执行上下文。这也是关键字this
值变化的原因之一。使用bind
、apply
或call
方法可以让您控制上下文,并为您提供一种更可预测的使用this
的方式。
让我们从bind
关键词开始。您可以设置关键字this
将用作起点的对象。清单 5-5 给出了一个例子。
var myObj = {
person: "Friend"
}
function sayHello() {
console.log("Hello " + this.person);
}
sayHello() //returns Hello undefined
var tryAgain = sayHello.bind(myObj) //assigning the value of this to the object myObj
tryAgain() // returns Hello Friend
Listing 5-5Using the Keyword bind to Set the Value of this
使用bind
,你设置你想要使用的对象作为起点。在本例中,您将值设置为名为myObj
的对象。
以myObj
为起点,您可以访问对象的属性(在本例中是person
属性),而不会遇到浏览器声明值未定义的错误。
call
和apply
方法类似。主要区别在于,不仅可以定义执行上下文,还可以将信息传递给想要执行的方法。清单 5-6 展示了一个调用call
方法的例子。
sayHello(message){
console.log(message, this.mainCharacter);
}
const characters = {
mainCharacter: "Elliot"
}
sayHello.call(characters, "Hello "); //returns Hello Elliot
Listing 5-6Using the call Method
JavaScript 中的函数是对象。这些函数对象有自己的一组属性和与之相关联的方法。
在清单 5-7 中,您使用call
方法来指定您想要使用的函数的执行上下文。您告诉函数sayHello
使用call
方法,将执行上下文的起始点分配给 characters 对象,并将一个参数列表传递给sayHello
函数,在那里它可以正确执行。在本例中,您只传递了一个。
apply
方法非常类似。最大的区别是apply
方法需要一个数组。
sayHello(firstName, secondName, thirdName){
console.log(this.message, firstName); //returns Hello Elliot
console.log(this.message, secondName); //returns Hello Tyrell
console.log(this.message, thirdName); //returns Hello Dominique
}
const show = {
message: "Hello"
}
sayHello.apply(show, "Elliot ", " Tyrell ", "Dominique ");
Listing 5-7Using the call Method
面试问题
一个函数的执行上下文是什么,你能改变它吗?在浏览器的全局范围内可以找到什么对象?
这里你的函数类似于你用call
方法做的。传递一个数组可以让你使用该数组的元素作为函数的参数。
从这些例子中你可以看到,根据使用方式的不同,使用关键字this
可以有多种不同的形式。
记住函数也是一个对象(本地或宿主对象)也很重要,这些对象有它们自己的属性和方法。你学到的方法有call
、 apply
、、bind
。这些方法帮助控制执行上下文,执行上下文控制关键字this
的值。
当你和其他开发人员一起工作时,你会发现闭包。一个简单的解释是闭包是函数内部的函数。在下一节中,您将探索闭包是如何工作的。
理解闭包
在上一节中,我解释了闭包作为另一个函数中的一个函数的概念。由于 JavaScript 在使用函数时处理范围的方式,您可以做一些有趣的事情。
在函数中创建的函数和变量存在于所谓的词法范围中。内部函数不仅可以访问自己范围内的变量和函数,还可以访问外部函数范围和全局范围内的变量和函数。同样重要的是要注意,这在另一个方向不起作用。
清单 [5-8 展示了内部函数如何访问自身外部的变量。
sayHello(firstName, lastName){
let msg = "Greetings ";
function intro(){
return msg + firstName = " " + lastName;
}
return into();
}
sayHello("Professor" , "Falken"); //returns "Greetings Professor Falken";
Listing 5-8Using a Closure to Illustrate an Inner Function’s Access to Other Scopes
这个例子说明了内部函数如何访问外部函数。外部函数执行内部函数(intro
),并将结果返回到调用它的地方。内部函数返回由外部函数和内部函数的变量组成的字符串。然后使用console.log
方法在浏览器控制台中显示结果。
如果您来自其他语言,如 Java,您应该熟悉声明公共和私有方法和属性的概念。当调用一个公共方法时,这些语言允许你隐藏一个方法如何在类中处理信息的一些内部工作。
JavaScript 没有提供这种能力,但是通过使用闭包,您可以编写具有相同效果的代码。清单 5-9 展示了一个不使用函数就不能立即访问变量的例子。
const sayHello = function(){
let greetingMsg = "Greetings ";
function msgTo(firstName, lastName){
greetingMsg = greetingMsg + firstName + " " + lastName;
}
return {
sendGreeting: function(firstName, lastName){
msgTo(firstName, lastName);
}
getMsg: function(){
return greetingMsg;
}
}
}
const createMsg = sayHello();
createMsg.sendGreeting(("Professor" , "Falken");
console.log(creatMsg.getMsg()); //returns "Greetings Professor Falken";
Listing 5-9Using a Closure to Create Private Variables
在本例中,您声明了一个闭包,并将其赋给变量sayHello
。调用闭包时,它返回一个对象,该对象的方法可以访问其函数范围之外的变量。
调用函数sayHello
用两个方法返回一个对象,sendGreeting
和getMsg
。
调用sendGreeting
方法更新变量greetingMsg
。除了使用sendGreeting
函数,该变量不可访问。
要查看更新后的变量,可以调用另一个方法getMsg
。它查看内部变量并返回其更新后的值。在这种情况下,您会收到消息“您好,Falken 教授。”
因为变量greetingMsg
在外部函数中,所以不能从全局范围访问它。
同样需要注意的是,如果将sayHello
的结果赋给第二个变量,它将独立于第一个实例。函数的执行方式将完全相同。但是,这些值可能不同。这里有一个例子:
const createMgs2 = sayHello();
createMsg2.sendGreeting("David", "Lightman");
console.log (createMsg2.getMg()); //returns Greetings David Lightman
在这里,你使用完全相同的功能,最终得到不同的结果。
面试问题
如何在 JavaScript 中创建私有变量?
摘要
本章开始概述函数对于应用开发的重要性。您探索了如何使用call
、 apply
和bind
等方法来控制函数的执行上下文。您还了解了关键字this
在不同的执行上下文中有不同的含义。
本章还讨论了函数表达式和箭头函数之间的区别,以及为什么理解它们之间的区别很重要。
还讨论了闭包和寿命。立即调用的函数是将变量和方法排除在全局范围之外的一种方式,而闭包是创建私有变量和方法的一种方式,类似于来自像 Java 这样的强类型语言的人所理解的。
函数是 JavaScript 的重要组成部分。同样重要的是调用函数的能力。通常函数是作为事件的结果而被调用的。下一章将介绍事件在 JavaScript 中是如何工作的。它还将涵盖来自环境的事件和用户创建的事件。
六、JavaScript 和事件
在上一章中,你学习了函数是如何工作的,以及箭头函数和函数表达式之间的区别。您还了解了执行上下文是如何控制关键字this
的,以及用于创建一种更可预测的处理方式的内置方法。
函数经常因为一个事件而被调用。事件可以是页面加载或按钮点击。JavaScript 允许您“监听”事件,然后执行一个函数来响应事件。
在这一章中,你将学习如何使用前几章的知识,并开始使用事件开发一些与浏览器的交互。您还将探索事件冒泡的概念。
有时,内置事件可能不包含您想要做的事情。然后,您可以为您的应用定制事件。
document 对象是许多包含名为addEventListener
的方法的对象之一。这个方法有两个参数,一个是您正在监听的事件,另一个是当事件发生时将执行某项操作的函数。
对于您正在使用的环境( https://developer.mozilla.org/en-US/docs/Web/Events
)有一个很长的事件列表,但是对于这个例子,让我们只计算页面何时被加载。
在本例中,您将监听事件DOMContentLoaded
。事件DOMContentLoaded
让您知道页面的内容已经加载到浏览器中并且已经被解析。清单 6-1 展示了一个这样的例子。
document.addEventListener("DOMContentLoaded", function(){
console.log(this); //returns #document
});
Listing 6-1Adding an Event Listener to Know When the Page Loaded
当您在浏览器开发工具中打开控制台时,您将看到此事件的结果。它应该可以让您访问刚刚加载的整个文档。目前,文档中没有太多内容,但要点是这个匿名函数是在事件发生时执行的。
面试问题
命名一个事件,让您知道页面的内容已经被加载和解析。
可以添加和删除事件侦听器。当您想要为同一个对象分配不同的侦听器时,这为您提供了灵活性。
清单 6-2 展示了一个为按钮添加和移除事件监听器的例子。
document.addEventListener("DOMContentLoaded",onPageLoad);
function onPageLoad(){
let theButton = document.getElementById("myButton");
theButton.addEventListener("click", handleButtonClick);
}
function handleButtonclick(){
console.log("button clicked");
let theButton = document.getElementById("myButton");
theButton.removeEventListener("click", handleButtonClick);
}
Listing 6-2Adding and Removing Event Listeners
此示例使用命名函数而不是匿名函数。逻辑是这样的。事件DOMContentLoaded
被触发并调用函数onPageLoad
。该函数创建一个名为theButton
的局部变量,并通过其 id 引用 HTML 页面中的按钮。然后,它添加一个事件侦听器,等待按钮被单击。
当按钮被点击时,它调用一个名为handelButtonClick
的函数。该函数在控制台中打印出按钮被单击的信息。然后,它创建自己的引用文档中相同按钮的局部变量,并删除事件侦听器。当用户想第二次点击它的时候,这个按钮就不起作用了。
在这里,您使用DOMContentLoaded
事件来设置应用做您想做的一切。
当页面加载时,将事件侦听器添加到按钮。在这个用例中,您只希望它在一次点击中处于活动状态。当单击按钮时,删除事件侦听器,使用命名函数使其不活动。这很有帮助,因为在出现错误的情况下,您可以直接引用函数,如果您使用匿名函数,就不能这样做。
向对象添加事件侦听器还允许您为不同类型的事件添加侦听器。清单 6-3 展示了一个向同一个按钮添加多个事件的例子。
document.addEventListener("DOMContentLoaded",onPageLoad);
function onPageLoad(){
let theButton = document.getElementById("myButton");
theButton.addEventListener("click", handleButtonClick);
theButton.addEventListener("mouseover", mouseOverEvent);
theButton.addEventListener("mouseout", mouseOutEvent);
}
function handleButtonclick(){
console.log("button clicked");
}
function mouseOverEvent (){
console.log("mouseover");
}
function mouseOutEvent (){
console.log("mouseout");
}
Listing 6-3Adding Multiple Events to a Button
您使用DOMContentLoaded
事件告诉应用向您的按钮添加多个事件监听器。这些侦听器知道按钮何时被单击、滚动和关闭。在每个实例中,当事件被触发时,它会触发一个函数,在浏览器控制台中打印出一条消息。
有时候,您可能想让浏览器停止它通常会做的事情,而让 JavaScript 接管。下一节将使用preventDeault
方法解决这个问题。
使用预防默认
一些阻止浏览器执行正常功能的例子是在使用链接或表单时。链接通常被认为是一种进入网站中另一个页面的方式。
如果你想让某人点击一个链接,而不是让浏览器转到另一个页面,你可以使用preventDefault
方法。
当事件发生时,事件对象被传递给事件处理函数。该对象包含关于刚刚发生的事件的信息。
例如,如果你想知道被点击的链接,你可以询问target
。清单 6-4 给出了一个例子。
document.addEventListener("DOMContentLoaded",onPageLoad);
function onPageLoad(){
let theButton = document.getElementById("myLink");
theButton. addEventListener("click", function(evt){
evt.preventDefault();
console.log(evt);
console.log(evt.target);
});
Listing 6-4Finding the Object That Was Clicked
这个例子类似于前面的例子,在前面的例子中,当加载文档时,click 事件侦听器通过它的 id 被添加到 HTML 页面中的 anchor 标记。
单击后,一个事件对象被传递给包含刚刚发生的事件信息的函数。这个对象拥有的方法之一是preventDefault
方法。它阻止浏览器访问分配给该链接的 URL。
还可以通过使用console.log
方法查看整个事件对象。它在浏览器开发工具中打印结果。
下一行显示target
。这是调度事件的对象。在您的情况下,它是被点击的项目。开发者工具显示目标的 HTML。
表格提交后通常会转到另一页。如果您想在提交前对表单内容进行验证,可以阻止表单向服务器提交内容。这将使您有机会在发送数据之前验证表单。
当使用 JavaScript 阻止浏览器在内容被验证之前提交表单时,也可以使用preventDefault
方法。见清单 6-5 。
document.addEventListener("DOMContentLoaded",onPageLoad);
function onPageLoad(){
let myForm = document.querySelector("form");
myForm.addEventListener("submit", onSubmit);
}
function onSubmit(evt){
console.log("form submitted");
evt.,preventDefault();
}
Listing 6-5Using the preventDefault Method with a Form
使用其他例子中的一些代码,等待DOMContentLoaded
事件发生。一旦事件发生,就调用onPageLoad
函数。
方法搜索 HTML 页面中的表单对象。然后在表单中添加一个事件侦听器,希望知道它何时会尝试提交任何数据。
当点击提交按钮时,提交事件触发onSubmit
函数。这将覆盖试图提交数据并通过使用preventDefault
方法转到另一个 HTML 页面的浏览器。
它还在控制台中打印消息“表单已提交”。使用这种方法将确保浏览器不会做任何意外的事情。
面试问题
如何阻止浏览器执行其正常功能?
您已经看到了可以控制浏览器正常工作方式的例子。这使您有机会在提交之前评估数据,或者创建一个单页面应用,而无需转到另一个 HTML 页面。
浏览器中的大量工作都与事件相关联。需要理解的一个重要概念是事件传播。事件传播是指您希望同时讨论事件的冒泡阶段和捕获阶段。下一节将详细解释这一点。
事件传播
事件传播处理浏览器中的各种对象如何能够接收事件。在上一节中,我展示了如何将侦听器分配给一个对象。该对象将等待事件发生,然后调用一个函数来处理该事件。
传播的一个阶段是事件 冒泡 。这种情况的一个例子是点击div
中的一个按钮。点击的target
是按钮本身。
但是,事件会从按钮向上传播到其父级。这个过程将继续,直到事件遇到window
对象。清单 6-6 展示了这是如何工作的。
document.addEventListener("DOMContentLoaded",onPageLoad);
function onPageLoad(){
let myButton = document.getElementById("myButton");
myButton.addEventListener("click", onButtonClick);
let container = document.getElelmentById("container");
container.addEventeListener("click", onContainerClick);
document.addEventListener("click", onDocumentClick);
window.addEventListener("click", onWindowClick);
}
function onButtonClick (evt){
console.log("button clicked");
}
function onContainerClick (evt){
console.log("container clicked");
}
function onDocumentClick (evt){
console.log("document clicked");
}
function onWindowClick (evt){
console.log("window clicked");
}
Listing 6-6Illustrating Event Bubbling
一种模式正在形成:您等待文档加载,然后将事件侦听器分配给 HTML 文档中的项目。
在本例中,您正在寻找 ID 为myButton
的按钮。当侦听器被添加时,它调用函数onButtonClick
并在控制台中打印出按钮被单击的消息。
由于事件冒泡,事件上升到div
标记,它的事件监听器处理该事件。
从这里开始,document
和window
对象都可以处理事件。
您可以在浏览器控制台中看到结果:
button click
container click
documnnt click
window click
您可以看到,一旦按钮被单击,事件就传播到窗口对象,而无需您做任何事情。这是浏览器处理事件的默认方式。您可以通过使用stopPropagation
方法来阻止这种情况发生。它防止其他对象在事件冒泡时监听事件。
清单 6-7 与清单 6-6 大部分相同。唯一的区别是在div
标签接收事件后添加了stopPropagation
方法。这使得document
和window
无法接收事件。
function onContainerClick(evt){
console.log("container clicked");
evt.stopPropagation();
}
Listing 6-7Using the stopPropagation Method
这不是代码的完整示例;我只是展示了重要的部分来说明这个例子。当调用事件时,事件对象被发送到事件处理程序。您使用这个对象来调用stopPropagation
方法。
这将阻止链中更高位置的任何其他对象接收该事件,即使它们有该事件的侦听器。
浏览器处理事件的另一种方式是通过事件捕获。这与冒泡正好相反,在冒泡中,浏览器从文档顶部开始,一路向下到达目标。
要让浏览器使用这种处理事件的方法,您需要告诉addEventListener
方法使用捕获而不是冒泡。清单 6-8 显示了事件如何从window
对象向下传播。
function onPageLoad(){
let myButton = document.getElementById("myButton");
myButton.addEventListener("click", onButtonClick, true);
let container = document.getElementById("container");
container.addEventListener("click", onContainerClick, true);
document.addEventListener("click", onDocumentClick, true);
window.addEventListener("click", onWinowClick, true);
'}
Listing 6-8Using Event Capturing
将最后一个属性添加到addEventListener
方法告诉浏览器启用事件捕获。然后堆栈从上到下开始:
window click
documnnt click
container click
button click
将最后一个属性添加到addEventListener
方法告诉浏览器启用事件捕获。然后堆栈从上往下开始。
这两个例子展示了事件如何在浏览器中移动,要么从事件源移动到顶部,要么从window
对象向下。
此时,您使用的事件已经构建到环境中。有时,您可能需要制作一个自定义事件。下一节将介绍如何实现这一点。
面试问题
事件冒泡和捕获有什么区别?你如何防止它们发生?
创建自定义事件
大多数情况下,当您需要知道应用中发生了什么事情时,您可以使用环境中已经内置的事件。但是,可能会出现内置事件不总是符合您的需求的情况。这就是自定义事件的用武之地。
你工作过的大部分都是一样的。您需要侦听一个事件,并且该事件需要被调度。这里的一个不同之处是,您将调度自己的自定义事件。清单 6-9 是一个定制事件的例子。
function onPageLoad(){
let myButton = document.getElementById("myButton");
myButton.addEventListener("click", onButtonClick, true);
myButton.addEventLIstener("WORLD_ENDING_EVENT", onWorldEnd);
}
function onButtonClick(evt){
let custEvent = new CustomEvent("WORLD_ENDING_EVENT", {
detail: message: { "And I feel fine"}
});
let myButton = document.getElementById("myButton");
myButton.dispatchEvent(custEvent);
};
functon onWorldEnd(evt){
console.log(evt); // returns CustomEvent {isTrusted: false, detail: {...}, type: "WORLD_ENDING_EVENT", target: button#myButton, currentTarget: button#myButton, ...}
console.log(evt.type); //returns WORLD_ENDING_EVENT
console.log(evt.detail); //returns {message: "And I feel fine"}
}
Listing 6-9Creating and Using Custom Events
该按钮监听两种不同类型的事件。一个是正常的点击事件,一个是自定义的WORLD_ENDING_EVENT
。因为它不在环境或 JavaScript 语言提供的事件列表中,所以它必须是一个自定义事件。
监听此事件的代码与监听任何其他事件的代码相同。添加名称,然后添加事件发生时将执行的事件处理函数。
在这种情况下,该函数被称为onWorldEnd
,它打印出发送给该函数的事件对象的不同值。第一个示例打印出整个事件对象。第二个输出事件对象的类型属性。最后一个输出了从定制事件发送的细节对象的值。
面试问题
如何实现自定义事件?
摘要
事件是应用开发的重要组成部分。事件可以让您了解环境中正在发生什么,以及您的用户正在做什么。
本章讲述了什么是事件,环境中的一些事件,以及如何创建自己的自定义事件。您现在知道了如何侦听事件,并且知道了自定义事件和本机事件之间没有区别。
事件处理程序可以使用匿名函数和命名函数。事件对象被传递给函数处理程序来描述函数刚刚发生的一切。使用 event 对象,您可以看到哪个 HTML 元素调度了该事件,并且您能够阻止浏览器执行某些本机操作。您还可以更清楚地了解事件如何通过浏览器传播以及如何控制它们。
下一章将介绍 JavaScript 中继承的概念。您将学习如何创建一个类,以及组合和继承之间的区别。
七、JavaScript 和编程范式
在这一章中,我将解释一些不同的 JavaScript 编程范式。我所说的编程范式是指编写 JavaScript 代码的不同方式或“风格”。
JavaScript 是一种灵活的语言。本章将涵盖两种主要的编程范式:面向对象编程(OOP)和函数式编程。在面试中,你可能会被要求解释这两种范式之间的区别。此外,您可能会发现由其他开发人员编写的代码可能属于这两个类别中的一个或两个。
面试问题
举出两种不同的 JavaScript 编程范式。
用 JavaScript 进行面向对象编程
物体可以被认为是可以为你工作的东西。数组可以保存一组(或一堆)数据。Math
对象可以为您执行计算。在任何可以运行 JavaScript 的环境中,都有一长串可以使用的对象。
面向对象编程是一种范式,在这种范式中,您既可以创建自己的对象来为您执行特定的操作,也可以使用环境中可用的对象。
当以这种方式谈论物体时,它们经常被比作现实生活中的事物。教程经常使用人作为例子来解释一个对象是如何工作的。你可以让一群人在一个房间里。他们有相似的特征,如身高、发色、名和姓。
即使有这些相似之处,他们仍然是个体。这给了你一个机会,把你知道的相似的东西分组,并把它们抽象成所谓的类 。
一个类通常被认为是一个蓝图。在这个蓝图里面是处理你所知道的关于一个人的所有事情的代码,没有任何细节。
在下一个例子中,您将使用 ES6 class
关键字。还有其他方法可以做到这一点,但为了清楚起见,您将使用最新的语法。一旦您习惯了这一点,您将学习用 JavaScript 创建类的其他方法。
如果你想给你的对象添加细节,你可以创建这个类的一个实例。实例是从类实例化的对象。该实例具有在该类中定义的所有方法和属性。如果一个类是蓝图,那么实例可以被认为是根据蓝图构建的东西。
在深入之前,您应该在代码中看到这一点,所以请查看清单 7-1 。
7-1.js
class Robot {
constructor(name, type){
this.name = name;
this.type = type;
this.greeting = function(){
return "I'm " + this.name + " I'm a " + this.type + ".";
}
}
}
Listing 7-1Creating a Robot Class
创建一个指向 JavaScript 文件的 HTML 页面,其中包含以下代码。在浏览器中加载 HTML 页面。这将把类加载到浏览器的内存中。现在,您可以使用浏览器控制台创建实例。开发人员工具打开后,您可以执行从 JavaScript 页面加载的代码。
在这个例子中,您正在基于Robot
类创建一个对象。要创建一个实例,首先要创建一个变量,然后给它分配一个Robot
类的新实例的值。
创建实例时,传递两个参数供类使用。就像向函数传递数据一样,这个类将消耗参数并在内部使用它们。
您可以通过调用属于该类的方法greeting
来查看结果。图 7-1 展示了一个基于同一个类创建两个实例的例子。
在图 7-1 中,你可以看到同一个Robot
类的两个不同实例。每个实例持有自己的数据。
图 7-1
创建机器人类的两个实例
看Robot
类,可以参考关于对象的章节。使用关键字this
可以让您将属性name
和type
的值保存在对象内部。
你可能会注意到这个课程以一个大写字母开始。这是用 JavaScript 创建类的惯例。
创建类实例时,使用关键字new
。这将创建一个包含该类中定义的所有属性和方法的对象。根据类的定义方式,每个实例可以有唯一的值。这使您能够实例化或创建该类的多个实例,每个实例包含不同的属性。
类中的构造函数获取创建实例时传递的属性,并将它们分配给实例。
至此,您已经抽象出了机器人的一些更通用的属性。有时,您可能希望将通用属性与所讨论的机器人的特定属性结合起来。
原子的孩子
在上一节中,您抽象了机器人的一些通用属性,并能够使用该蓝图作为实例化Robot
类的单个实例的方法。现在让这些机器人根据它们的类型执行一些特定的任务。这意味着您需要使子类从父类中继承属性。清单 7-2 探索了扩展父类的概念。
7-2.js
class BendingUnit extends Robot {
constructor(name, type){
super(name, type);
}
}
}
Listing 7-2Inheriating from a Parent Class Using the ES6 extends Keyword
这段代码使用extends
关键字告诉环境,除了当前类中的特性之外,它还想使用来自父类的所有可用特性。
因为您知道您想要使用父类的一些属性和方法,所以您使用关键字super
将父类需要的所有东西传递给函数属性,在您的例子中是相同的两个属性。
此时,您没有向BendingUnit
类添加任何新的东西,但是如果您从这个类创建一个对象并从父类运行相同的方法,您将得到相同的结果。
创建这个对象与上一个示例中的相同。因为这是父对象的子对象,所以您不需要做任何特殊的事情。该类的内部函数将负责这一部分。图 7-2 在这一点上看起来应该非常相似。
图 7-2
创建从父类扩展函数的类
您可以看到,尽管这个类中不存在name
属性和greeting
方法,但是它们是可用的,因为您是从Robot
类扩展而来的。通过使用extends
关键字,您可以继承这些属性和方法。
既然您已经能够重用父类中的函数,那么您也可以为子类创建独特的函数。参见清单 7-3 。
7-3.js
class BendingUnit extends Robot {
constructor(name, type){
super(name, type);
}
primaryFunction(){
cosole.log(this.name + " bends things");
}
}
let bendingUnit = new BendingUnit("Bender", "Bending Robot");
bendingUnit.primaryFunction(); //returns Bender bends things.
Listing 7-3Creating Unique Methods for the Robot Subclass
在这个例子中,您有一个方法,它对于当前类是唯一的,并且不依赖于父类。该方法仍然可以利用父类的属性来执行其独特的功能。
像上一个例子一样创建一个对象,并将其命名为primaryFunction
。结果应该如清单 7-3 所示。
这就是继承的工作方式。您可以将属性和函数抽象到一个类中,然后为了获得更多的细节,您可以创建一个子类,该子类可以利用父类的优势,同时添加自己独特的功能。
使用extends
关键字给你从另一个类继承的能力。JavaScript 只允许你继承一个父类。
因为您在您的类中使用 ES6 语法,所以在幕后有很多事情要做。下一节将为您提供一个深入探索的机会。
JavaScript 类和原型继承
在上一节中,您使用关键字extends
开始了对继承主题的探索。其思想是一个对象可以获得另一个类的属性和方法。这另一个类被认为是它的超级或父类。
其他语言有所谓的基于类的继承。这意味着一个对象可以从其他类继承它的属性和方法。
然而,JavaScript 是基于原型的,这意味着如果属性或方法在当前对象中不存在,它将查看对象的原型属性,并移动到其父对象以查看该属性是否存在。这通常被称为沿着原型链向上移动。
在前面的示例中,您使用了 ES6 语法来创建对象。你用的一个关键词是constructor
。函数构造器实际上只是一个创建函数对象的函数。
为了进行适当的比较,让我们使用函数构造函数重新创建robot
类。参见清单 7-4 。
7-4.js
const Robot = function(name, type) {
this.name = name;
this.type = type;
}
Robot.prototype.greeting = function(){
return "I'm " + this.name + " I'm a " + this.type + ".";
}
let bender = new Robot ("Bender", "Bending Robot");
bender.greeting() // "My name is Bender I'm a Bending Robot."
Listing 7-4Creating the Robot Object Using Function Contructors
在这里,您创建一个常数,并为其分配一个函数。这个函数接受两个属性,并将值赋给它的内部对象。
下一行是您开始了解 JavaScript 中的继承是如何工作的地方。您访问对象的prototype
属性并创建一个名为greeting
的函数。这就像使用类语法创建函数一样。
属性和方法被添加到对象的原型中。这意味着如果有任何类可以从这个类继承,JavaScript 会将原型链从子对象移动到其父对象以访问属性。
清单 7-5 展示了如何创建一个从另一个对象继承方法和属性的对象。
7-5.js
const Robot = function(name, type) {
this.name = name;
this.type = type;
}
Robot.prototype.greeting = function(){
return "I'm " + this.name + " I'm a " + this.type + ".";
}
const BendingUnit = function(){
Robot.apply(this, arguments);
}
BendingUnit.prototype = Object.create(Robot.prototype);
BendingUnit.prototype.constructor = BendingUnit;
let bender = new BendingUnit("Bender", "Bending Unit");
bender.greeting() // "My name is Bender I'm a Bending Unit."
Listing 7-5Creating Inheritance Without Using the ES6 Syntax
这里的一个关键区别是函数BendingUnit
调用原始的Robot
类并使用apply
方法。
apply
方法告诉浏览器BendingUnit
类可以作为访问Robot
类属性的起点。
下一行告诉环境更新BendingUnit
类的原型对象,并将Robot
原型的值赋给它。
既然原型已经更新,构造函数认为它是Robot
类的副本,而不是自己的BendingUnit
类。
要解决这个问题,将 contractor 函数分配给BendingUnit
函数,这样它将知道即使它从Robot
类继承了函数,它本身也不是机器人。
本例中最后两行的功能与使用 ES6 语法时相同。您可以看到,使用 ES5 语法使您更接近 JavaScript 的实际工作方式,但是可能会使解释继承的概念更加困难。
总的来说,继承对于重用代码而不必重写代码是有用的。还有一种模式叫构图。使用复合,你在类内部创建对象,直接使用它们,不需要继承。这不是 JavaScript 语言的特性,而是重用代码以赋予对象更多能力的一种方式。
对继承的批评之一是,你不仅继承了你想要的特性,还继承了你不需要的特性。这是因为它们是父类或父类的一部分。
在下一节中,我将讨论另一种范式:用 JavaScript 进行函数式编程。
JavaScript 函数式编程
如前所述,JavaScript 是一种非常灵活的语言。经常使用的两种范式是面向对象编程和函数式编程。
以前,您使用包含关键字class
的 ES6 语法和依赖于您对原型继承的理解的 ES5 语法在 JavaScript 中创建了对象。
使用 JavaScript 时,理解原型继承非常重要。然而,函数式编程不依赖于原型。
函数式编程有几个概念需要解释,以便充分理解它与面向对象编程的区别。
我将介绍的一些概念是
-
纯函数
-
副作用/共享状态
-
不变
-
命令式代码之上的声明式代码
纯函数
它的纯粹之处在于,无论你调用函数多少次,结果总是一样的。如果你有一个函数,取一个数,乘以 2(x * 2),然后返回结果,无论你传递给函数什么数,结果都是一致的。清单 7-6 显示了一个纯函数的例子。
7-6.js
const timesTwo = (x) => x * 2;
timesTwo(2) //returns 4
timesTwo(3) //returns 6
Listing 7-6Creating a Pure Function
这给了你的函数一种叫做引用透明性的东西。这意味着你可以用它返回的值来替换函数本身,而应用的行为不会改变。如果不是这样,它被称为不透明。
使一个函数不纯的原因是,尽管传递了相同的值,但函数返回不同的结果。例如,开发人员有时希望每次都调用具有唯一属性的服务,以防止浏览器缓存结果。为此,他们将当前时间添加到 URL 中。因为在 24 小时内你只能得到一次准确的时间,所以这不是一个纯粹的函数。任何使函数产生不一致的结果或者除了返回值之外还必须做任何事情的事情都会使函数不纯。
副作用/共享状态
纯函数的另一个特点是它们不会产生副作用,也就是说它们不会改变或者变异在函数之外可以访问的对象。
因为 JavaScript 将对象属性作为引用传递,所以当对象或数组的属性发生变化时,它会改变该对象的状态。同样的对象可以在函数之外访问。纯函数不得改变外部状态。
小费
赋给非原始值(一个对象)的 JavaScript 变量被赋予该值的引用。引用是指对象在计算机内存中的位置。变量本身不包含值。
不变
不变性是函数式编程的一个重要概念。对象不变或突变的想法使得调试之类的事情变得更简单,因为每个状态都是恒定的。
提醒一下,使用关键字const
将保持变量不变,但不会阻止对象被更新。
经常发生的一件事是,你有一种情况,你通常会改变数据,例如一个数组。如果您正在以函数方式编程,您不希望改变数据。你如何解决这个问题?
在这种情况下,您可以制作数据的副本,然后将副本发送回来。这可以防止原始对象发生突变,并提供您需要的结果。
要向数组中添加项目,大多数人会使用push
方法。这会将一个项目添加到数组的末尾。但是,这也会使数组发生变异。要解决这个问题,您可以使用内置的Object
数据类型并使用assign
方法。为了更清楚,请看清单 7-7 。
7-7.js
const twoArray = ["One", "Two"];
const threeArray = Object.assign([...twoArray], {2:"Three"});
console,log(threeArray); //returns (3) ["One", "Two", "Three"]
Listing 7-7Returning a Copy of an Array So Data Is Not Mutated
在本例中,第一行创建了一个数组。第二行使用了assign
方法。此方法接收两个参数。第一个是原数组。您不用遍历整个数组来获取所有元素,而是使用 spread 操作符来传递 iterable 对象(在本例中为数组)的所有属性。第二个参数是要添加到数组中的对象。这将创建一个新的数组,而不会改变原始对象。
命令式代码之上的声明式代码
声明性代码和命令性代码的主要区别在于,命令性代码描述了如何实现预期结果的步骤。虽然声明性代码关注的是应该对数据做什么,但是它应该如何工作的细节却被抽象掉了。清单 7-8 展示了一个基于现有数组创建新数组的例子。
7-8.js
const threeArray = ["One", "Two", "Three"];
//Imperative code describing how a program operates
for(let i = 0; threeArray.length > i; i++){
console.log(threeArray[i]); //returns One, Two, Three
}
//Declarative code showing how a program should work
threeArray.map((value, index, arr) => {
console.log(value); //returns One, Two, Three
});
Listing 7-8Imperative Code vs. Declarative Code
第一个实例展示了一种基于数组长度遍历数据的方法。在for
循环中有检查来判断你能走多远。
第二个实例利用了内置的map
方法。此方法将函数作为参数。该函数接收数组中的每一项,而不需要知道数组的长度。
这里的主要区别是,您不需要描述如何浏览数据。重点只在于你对数据做了什么。
如果您在函数式编程环境中工作,这些是需要理解的重要概念。JavaScript 中的函数可以作为函数的属性传递。它们也可以是从其他函数返回的值。
链接函数的能力是你会经常看到的。使用fetch
方法从远程数据源检索数据需要链接方法,然后和处理从服务器返回的结果。这里有一个例子:
fetch('https://swapi.co/api/people/1').then(results => results.json()).then(json => console.log(json));
//results
//{name: "Luke Skywalker", height: "172", mass: "77", hair_color: "blond", skin_color: "fair", ...}
这里使用fetch
方法从星球大战 API ( https://swapi.co
)中检索数据。这个方法返回所谓的承诺。它将一直等到请求被解决或失败。
当请求被解析时,then
方法被链接到它。您可以在这里处理结果。第一个then
方法获取结果并将它们转换成一个 JSON 对象。使用箭头函数时,如果没有花括号,只有一行代码,结果会自动返回。
这被发送到第二个then
方法,在那里第一个方法的结果变成一个名为json
的参数,并被打印在浏览器控制台中。
摘要
在这一章中,你探索了两种不同的编程风格或范式。第一种是面向对象的编程,在这种编程中,你可以创建代表你想要建模的事物的对象。
记住 JavaScript 使用原型来创建继承是很重要的。当前对象中不存在的属性和方法将沿着原型链向上移动到对象的父类。
第二种是函数式编程,纯函数变得极其重要。如果相同的参数传递给函数,这些函数总是返回相同的结果。它们也不会产生副作用,因为它们不仅仅是返回一个一致的值。
您还学习了不变性的概念。对象不直接更新;这会使物体变异。相反,会制作一个副本,并将更改添加到该副本中,然后发送回应用。
最后一节介绍了声明性代码和命令性代码之间的区别。声明性代码使用代码来确定如何在数据中导航,命令性代码允许代码自己确定它应该如何呈现自己,并让您专注于您想要用它做什么。该示例使用了数组的map
方法;它允许您遍历数组中的每一项,而无需提前知道长度。
下一章将介绍如何使用浏览器中的工具来帮助调试应用。随着您的应用变得越来越复杂,了解这些工具的工作原理将非常有帮助。
八、JavaScript 和调试
到目前为止,我已经讨论了如何安装软件以及使用 JavaScript 时需要知道的一些重要事情。在很大程度上,JavaScript 是您在浏览器中使用的东西。
既然如此,如果你写的代码有效,结果就会显示在浏览器中。更重要的是,如果结果不工作,你需要一种方法来看看哪里出错了。
问题就变成了,“我如何在浏览器中看到我的代码发生了什么?”令人高兴的是,浏览器内置了调试工具。本章将介绍一些在浏览器中调试代码时可以使用的工具。
火狐有专门针对开发者的浏览器,火狐开发者版( www.mozilla.org/en-US/firefox/developer/
)有工具,是那个浏览器特有的。
在撰写本文时,谷歌的 Chrome 浏览器仍然是最受欢迎的。因此,我将使用该浏览器中的开发工具作为参考。
开发人员工具由不同的面板组成。虽然我不会详细讨论每个面板,但是有些面板是针对特定任务的。这些面板包括
-
设备模式
-
元素面板
-
控制盘
-
来源面板
-
网络面板
-
性能面板
-
内存面板
-
应用面板
-
安全面板
控制台面板
在前面的章节中,您使用了控制台面板。这允许您直接在浏览器中执行 JavaScript 并返回结果。您还可以调用加载到浏览器中的 JavaScript 文件中定义的函数。
因为您过去曾经使用过控制台,所以这是一个很好的起点。要打开浏览器控制台,右键单击页面并选择检查选项。这将打开开发人员工具。如果要使用键盘命令,请选择 Command + Option + J(在 Mac 上)或 Control + Shift + J(在 Windows 上)。见图 8-1 。
图 8-1
显示控制台面板
控制台有自己的 API(应用编程接口)。控制台的目的是让您了解正在执行的代码。
到目前为止,您只是使用了console.log
方法让浏览器为您打印出结果。这很有帮助,但不是使用控制台帮助您了解浏览器中发生的事情的唯一方式。
其中一些函数是不言自明的。比如console.clear()
会清除控制台里的一切。
使用clear
方法的唯一例外是保存日志设置被取消。这将在页面刷新之间保留您在控制台中所做工作的历史记录。你可以通过点击开发者工具窗口右边的图标来找到它。见图 8-2 。
图 8-2
显示控制台设置
console 对象有几个方法,这些方法有助于理解代码在执行时发生了什么。例如,count
方法跟踪同一个源调用一个函数的次数。在这种情况下,单击按钮是一样的。见清单 8-1 。
8-1.html
<button id="myButton">Click Me</ button >
8-1.js
function myCount(evt){
console.count(evt);
}
document.addEventListener("DOMContentLoaded", () => {
document.querySelector("#myButton").addEventListner("click", myCount);
});
//result [object MouseEvent]: 1..2..3
Listing 8-1Using the count Method as Part of the console API Class
这个例子等待DOMContentLoaded
事件发生。然后,它查看文档,通过 ID 找到按钮,并为其分配一个事件侦听器。
当按钮被点击时,count
方法跟踪与按钮点击相关的函数被调用的次数。所以,不用在代码中创建变量,你可以看到这个函数被调用了一定的次数。
dir
方法获取一个对象并在控制台中打印出来。在这里,您可以单击查看为对象显示的所有属性和方法,包括任何子属性和方法。
清单 8-2 从清单 8-1 中提取代码,并使用dir
方法来实现不同的结果。
8-2.html
<button id="myButton">Click Me</ button >
8-2.js
function myCount(evt){
console.dir(evt);
}
document.addEventListener("DOMContentLoaded", () => {
document.querySelector("#myButton").addEventListner("click", myCount);
});
Listing 8-2Using the dir Method as Part of the console API Class
这段代码让您可以查看MouseEvent
附带的所有属性和方法。这类似于使用console.log
,在这里您可以看到您正在处理的对象的所有属性和方法。见图 8-3 。
图 8-3
显示 MouseEvent 对象的方法和属性
一个类似于调用console.log
的方法是dirxml
。该方法返回一个对象,但它不是将所有属性和方法显示为一个 JavaScript 对象,而是将其显示为一个基于标记的 XML 文档。
清单 8-3 显示了dir
和dirxml
方法。在本例中,函数myCount
首先显示dirdxml
的结果,然后显示dir
的结果。参见图 8-4 。
图 8-4
从控制台 API 显示 dir 和 dirxml 方法的结果
8-3.html
<button id="myButton">Click Me</ button >
8-3.js
function myCount(evt){
console.dirxml(document);
console.dir(document);
}
document.addEventListener("DOMContentLoaded", () => {
document.querySelector("#myButton").addEventListner("click", myCount);
});
Listing 8-3Using Both the dir and dirxml Methods
图 8-4 显示第一级结果以 HTML/XML 格式显示文档对象。你可以看到所有标签的层次结构,并点击它们。
第二层显示了文档对象的所有方法和属性。在这种情况下,它更像是在查看一个 JavaScript 对象,这比第一个例子更难理解层次结构。
有时您可能有一些事情想要记录,但是仅仅使用log
方法意味着您需要在控制台中滚动结果来找到它们。这就是你可以结合使用group
和groupEnd
方法的地方。您可以将日志的结果组合在一起。需要记住的是,要结束这个组,需要调用groupEnd
方法。
清单 8-4 展示了如何获取一个对象的属性并将它们组合在一起。这为您在控制台中查看结果提供了更多的上下文。
8-4.js
let insocAlbums = {'first': 'Information Socieity', 'second'': 'Hack', 'thrid': 'Peace and Love Inc.'};
function groupBand(albums){
console.group("Album List");
console.log('first:' , albums.first);
console.log('second:' , albums.second);
console.log('thrid:' , albums.third);
console.groupEnd();
}
document.addEventListener("DOMContentLoaded", () => {
groupBand(insocAlbums);
});
Listing 8-4Using the group and groupEnd Methods to Group the Results of the log Method
在本例中,您有一个名为insocAlbums
的对象。您希望看到该对象的属性组合在一起。下一个函数groupBand
查看传递过来的对象的单个属性,并在浏览器控制台中打印出来。
这里重要的区别在于,您用一个标题显式地将这些项目分组,并指定该组的结尾。图 8-5 显示了结果。
图 8-5
使用控制台 API 的 group 和 groupEnd 方法的结果
现在,您可以从分组在一起的对象中看到您感兴趣的所有项目。您可以单击组标题并展开列表。
我将在本节中介绍的最后一种方法是table
方法。顾名思义,它在控制台上一个漂亮的表格中显示结果。让我们以最后一个例子为例,将其显示为表格。参见清单 8-5 。
8-5.js
let insocAlbums = {'first': 'Information Socieity', 'second'': 'Hack', 'thrid': 'Peace and Love Inc.'};
function groupBand(albums){
console.table(albums);
}
document.addEventListener("DOMContentLoaded", () => {
groupBand(insocAlbums);
});
Listing 8-5The table Method
方法在一个格式良好的表格中显示一个对象的所有属性和方法。图 8-6 显示了一个例子。
图 8-6
使用控制台 API 的表方法的结果
您可以看到结果与使用group
方法得到的结果非常相似。其中一个好处是,你不需要做任何工作来实现这一点。
这是您可以通过控制台面板使用的一些方法的概述。在很大程度上,这些方法帮助您查看正在使用的对象的值。
有时,您可能希望更好地了解代码在浏览器中是如何执行的,以及代码执行时变量的值是如何变化的。控制台面板很有帮助,但是还有一个面板可以提供更多的细节。
源代码面板可以帮助你做一些事情,比如暂停程序,让你在代码运行时看到代码发生了什么。让我们看看源代码面板。
来源面板
“源代码”面板有三个部分。首先是文件导航器。它可以让你检查属于你的网站的任何文件。
一旦从文件导航器中选择了一个文件,您就可以使用代码编辑器来查看该文件的内容,并做出一些将实时反映在浏览器中的更改。
第三个面板是 JavaScript 调试面板。这个面板允许你做一些事情,比如观察一段时间内变量的值。当程序执行时,您还可以看到当前范围内所有变量的值。
在下一个示例中,您将使用与之前类似的东西。基本格式是一样的。会有一个按钮可以点击,但不同的是这个按钮会调用一个 web 服务并返回一些数据。在这里,您可以使用“源代码”面板来了解浏览器是如何执行代码的。
首先,你需要理解代码。看一下清单 8-6 。
8-6.js
let jsonResults;
function getData(){
fetch('https://jsonplaceholder.typicoce.com/todos/)
.then(response => response.json())
.then(json => saveData(json)));
}
functdion saveData(json){
jsonResults = json;
console.log(jsonResults);
}
document.addEventListener("DOMContentLoaded", () => {
document.querySelector("#myButton").addEventListner("click", getData);
});
Listing 8-6Using the fetch method to return data from a webservice
在本例中,您正在以不同的方式做一些小事情。点击按钮后,调用getData
功能。这个函数调用fetch
方法。
fetch
方法调用一个远程服务,在您的例子中,这个服务返回一些 JSON 数据。
当结果被返回到浏览器时,你做一些叫做方法链接的事情。这是从一个方法直接调用另一个方法的能力。如果您使用过 JQuery,这是一种常见的做法。
方法返回一个叫做承诺的东西。承诺给了你等待价值的能力。
在这种情况下,因为您正在调用远程服务,所以结果可能需要一些时间来返回值。一个承诺会让你知道数据什么时候被返回给浏览器。
当结果返回时,它可以链接到一个then
方法,该方法将告诉浏览器运行一个函数,该函数将结果解析为应用可以使用的 JSON 数据。然后链接另一个函数,将数据传递给另一个名为saveData
的函数。
面试问题
什么是承诺,它是如何运作的?什么是方法链?
saveData
函数获取传递给它的 JSON 并保存到变量jsonResults
。
您可以使用“源代码”面板中的一些选项来检查这是如何处理的。一种方法是设置所谓的断点。断点停止代码的执行;这使得调试环境能够及时地向您提供关于代码当时正在发生什么的信息。
如果浏览器是展开的,左侧应该有 JavaScript 调试工具。在称为事件侦听器断点的部分,您可以查看鼠标事件。
通过单击箭头,可以展开列表,详细显示可以分配给断点的鼠标事件的类型。参见图 8-7 。
图 8-7
JavaScript 调试窗格
通过选择点击断点,点击按钮将在getData
函数的第一行暂停程序的执行。此时,浏览器正在等待您使用调试工具来告诉它下一步应该做什么。
调试工具可以通知您到这个断点为止发生的所有事情。查看 Scope 面板,您可以看到变量this
的值。
设置断点的另一种方法是直接在代码中设置。使用当前的例子,如果您想在函数执行的中途知道一个变量的值,您可以点击saveData
函数。通过这样做,您可以看到类似于jsonResults
变量的值。
如果不希望有两个断点,请确保关闭 MouseEvent 断点。
在函数内部设置断点后,请确保刷新浏览器,然后单击按钮。程序将再次停止,这一次正好是断点被设置的地方。查看范围面板,您现在可以看到属性this
的当前值现在是window
。
作用域面板上方是调用堆栈面板。该面板显示所有执行功能的顺序。从下往上,您会看到首先调用的是getData
函数,然后移动到当前的saveData
函数。
面板的最顶端是手表面板。它允许您添加任何有效的 JavaScript 表达式,包括变量,并观察其值随时间的变化。
与设置断点不同,在设置断点的情况下,您只能在特定时刻获取值,而 Watch 允许您查看值,即使它们发生了变化。打开这个面板,点击加号按钮,输入"jsonResults"
。
在应用的第 11 行的console.log
方法处添加一个断点。您的应用目前停在第 10 行,这是您的原始断点的位置。您需要一种方法来移动到下一个断点,这样您就可以看到jsonResults
的值发生了变化。这是介绍使用调试器来遍历代码的能力的好时机。见图 8-8 。
图 8-8
deveoper 工具使您能够浏览代码,让您选择是浏览一个函数还是转到下一个函数
这些工具从左到右展示了在设置断点时如何浏览代码。这些工具位于您一直使用的所有面板的顶部。
第一个图标允许您继续执行脚本。它只是继续执行需要执行的下一部分代码。例如,当使用断点时,第一个按钮允许您继续或暂停代码的执行。
在当前示例中,如果您想要移动到下一个断点,您可以单击此按钮,它会将您带到下一行代码。浏览器将在下一个断点处停止,并显示jsonResults
的更新值。
如果您的函数更复杂,您可能希望浏览该函数,以便更好地理解该函数的工作内容。下一个示例将使用调试面板中的一些其他工具来帮助您逐步完成更复杂的函数。
你需要做的第一件事是使你的函数更加复杂。下一个例子将从jsonResults
变量中获取值,并使用内置的map
函数遍历它。
map
函数将使您能够按顺序遍历数组中的每一项,并评估每一个值。它还会告诉你该项目的当前索引。它还会在每次迭代中返回整个数组。
让我们来看看这个。如果您下载了这本书的代码,请打开清单 8-7。否则,遵循图 8-9 。
图 8-9
单击调试面板中的“单步执行”按钮,可以将您从映射函数移动到 console.log 函数
在遍历函数体时,使用console.log
方法输出已经返回的值。您还可以使用这个方法来输出一个名为checkIndex
的函数的结果。
checkIndex
函数查看当前索引,看它是否能被 2 整除,并返回 true 或 false 值。您将使用这个函数来完成调试工具。
如前所述,第一个按钮恢复脚本,让它不间断地运行。“下一步”按钮允许您单步执行下一个函数调用。
如果打开了源代码面板,请在第 10 行设置一个断点。这将停止应用,您可以开始看到这到底意味着什么。
当应用正在执行时,如果你点击按钮,所有的东西都应该停在第 10 行。如果您单击 Step Over 按钮,它会将您带到函数中的下一行,在这里您开始使用map
方法遍历数组中的所有项。如果你再次点击它,它将跳过map
功能,进入列表中的下一个功能,即console.log
功能。
如果您再次单击“跳过”按钮,指针将回到第 10 行。在下一个示例中,您将使用“单步执行”按钮来检查函数内部发生了什么。
如果通过刷新页面然后单击按钮来重新开始,调试器将再次停止在第 10 行。
现在,如果您单击“单步执行”按钮,您仍然会移动到下一个函数,在这里您将开始遍历数组中的所有项。
如果您第二次点击该按钮,而不是移动到下一个函数,您现在将进入当前函数的详细信息,并开始查看在map
函数中正在处理的值。参见图 8-10
图 8-10
单击“调试”面板中的“单步执行”按钮可以进入映射功能
继续使用该按钮时,向下移动功能中的项目列表,直到到达下一个console.log
功能。此时使用“步入”按钮会将您带到checkIndex
功能。
到目前为止,您已经逐步完成了saveData
功能中的每一项。然而,最后一行执行了一个新的函数,让您可以逐步执行。
您现在开始使用相同的工具逐行遍历checkIndex
函数,检查这个函数是如何执行的。
列表中的下一个按钮与“步入”按钮的作用相反。它跳出当前函数。
在这个例子中,如果你正在单步执行checkIndex
函数并选择退出,你将被带回到最初调用checkIndex
函数的saveData
函数。
列表中的下一个按钮是“步进”按钮。这个按钮可以让你一行一行地执行你的函数,并执行所有编写的东西。
最后两个按钮允许您停用或重新激活应用中的所有断点,并在出现异常时暂停。JavaScript 中的异常发生在错误发生的时候。
摘要
本章探讨了一些可以用来调试网站的工具。这里的重点是 JavaScript,但是您也可以检查和编辑 CSS 和 HTML,并且您可以模拟不同的设备来测试您的站点的响应能力。
其他工具包括网络监视器,用于查看代码从服务器发出需要多长时间,或者发出请求和获得响应需要多长时间。“性能”面板让您看到代码执行的速度,而“内存”面板确保您的代码以有效的方式使用浏览器的内存。
所有这些工具都在客户端。您一直在使用浏览器来帮助您理解 JavaScript 如何工作以及如何调试它。
随着项目变得越来越复杂,您可能会开始添加库或框架来帮助组织应用并添加您需要的功能。下一章将介绍如何作为客户端开发人员利用 NodeJS。
九、JavaScript 和客户端开发
在前面的章节中,您安装了 NodeJS 并学习了如何运行本地服务器。之后,您花了大量时间使用浏览器并探索 JavaScript 在其中的工作方式。在这一章中,我将介绍由 NodeJS 支持的工具,它们将增强客户端开发。
即使您决定不在服务器端工作,NodeJS 也是您应该知道的。它已经成为任何使用 JavaScript 的开发者的生态系统的一部分。像节点包管理器(NPM)这样的工具已经成为前端开发人员生活中重要的一部分。
本章将详细介绍 NodeJS(通常称为 Node)是什么,以及作为前端开发人员如何利用它。
node.js 到底是什么
为了理解当前的 JavaScript 框架如何能够执行像创建文件和文件夹这样的操作,重要的是后退一步,讨论 NodeJS 到底是什么,以及为什么它是 JavaScript 开发人员工具箱的重要组成部分。
关于节点的讨论将分为两部分。第一部分,我将在本章中介绍,将展示节点对于客户端开发的重要性。
下一章将介绍如何使用 JavaScript 和服务器端框架(如 Express)来组装 web 服务器。
客户端的节点
安装 Node 后,您就可以使用 Angular、React 或 Vue 等当前框架了。包是可以在任何项目中使用的 JavaScript 代码库。它们是通过一个叫做 NPM 的系统分发的。
NPM 有一个可供开发者使用的 JavaScript 库数据库。如果您想在项目中添加某种类型的功能,可以使用命令行并将其添加到项目中。
要使用 NPM,首先需要安装节点。如果您尚未安装 Node,请转到 NodeJS.org
并安装最新的稳定版本。
我将涉及的一些内容与前几章相似,但作为一个提醒,还是值得一读的。
当您安装了 Node 后,使用 Node 的一种常见方式是使用命令行工具。如果您运行的是 Windows,有一个名为 Cmder 的控制台模拟器。这个工具可以在 cmder.net
下载。安装了它和 Git(可以在 https://git-scm.com/
找到)你就可以使用所有的命令行选项,界面就像使用 Unix 或 MacOS 一样。
如果您运行的是 Windows 并且已经安装了 Git,那么您也可以使用 Git bash 作为替代。
在 Mac 或 Linux 端,可以使用内置的终端窗口。如果你用的是 MacOS,有一个叫 iTerm 的应用,如果你不想用内置的终端可以用。
安装了 Node 后,您可以使用 JavaScript 来完成在浏览器中无法完成的任务。例如,您可以访问文件系统;您还可以执行网络和数据功能。
对于前端开发人员,您可以使用 NPM 作为一种快速组装应用的方式。使用 NPM 时,需要创建一个名为package.json
的文件;该文件跟踪您在项目中使用的所有库和相应的版本号。
可以发布您的包,以便其他人可以使用它。在你的情况下,你不会这样做,但知道你有什么选择是很好的。
在项目中使用 package.json
要在命令行创建这个文件,请键入npm init
。这将启动该过程,并询问您有关您将要创建的项目的问题。您不打算发布此包;但是,命令行界面会像您一样向您提问。这就是为什么理解它试图做什么很重要。
您必须回答的第一个问题是项目的名称。当你给你的包起一个名字的时候,这里有一些事情需要记住(你可以在 https://docs.npmjs.com/files/package.json
找到所有的细节):
-
包名不能超过 214 个字符。
-
名字不能以点(。)或下划线(_)。
-
新包不能有大写字母。
如果您不打算发布包,这并不重要。如果你打算发布,为了更好地了解如何命名你的包,看看 www.npmjs.org
的注册表。
下一个问题是版本号。发布时需要名称和版本号。如果您对软件包进行了更改,那么您应该对版本号进行更改。
所有版本号都应该能够被 node-semver 解析。Semver,或语义版本化,是一种帮助跟踪项目版本的标准。以下是一些规则:
-
当更改与以前的更改不兼容时,使用主版本号。
-
当添加向后兼容的功能时,使用次要编号。
-
添加向后兼容的错误修复时使用补丁号。
使用这种格式,所有版本号看起来都类似于 1.0.0(主版本号、次版本号和补丁号)。
若要继续,请按回车键;这将为您留下默认版本。
接下来,应用将询问项目的描述。这有助于人们在搜索时发现该包。您可以按下 Return 键继续。
入口点是当您使用import
或require
关键字时将会用到的文件。使用这两个关键字中的任何一个都会将该库的功能添加到您的项目中。默认是index.js
。
在 test command 选项中,您可以添加命令来对应用运行任何类型的测试。这可能包括单元测试和集成测试。例如,您可以在持续集成环境中键入npm run tests
,并确保在将代码发送到服务器上运行之前通过应用测试。
面试问题
单元测试和集成测试的区别是什么?回答:单元测试检查一小段代码,以确保它做了它应该做的事情。集成测试检查这些单元是否能一起工作。
添加 Git 存储库有助于人们了解项目的位置。如果您对项目进行开源并希望人们做出贡献,这也很有帮助。
如果您计划公开项目,关键字会很有帮助。当有人使用“npm 搜索”时,它们会有所帮助关键字是字符串数组。在您的案例中,这是一个本地项目。您可以按下 Return 键并移动到下一个项目。
这种情况下的作者是你。如果您想要添加贡献者,请使用包含其他人的数组。这个数组可以包含名称、电子邮件和 URL。输入您的姓名,然后按回车键,或者直接按回车键。
如果这是一个公开的项目,你可以把它放在像麻省理工学院或知识共享这样的开源许可下。许可证的完整列表可在 https://spdx.org/licenses/
找到;在你的情况下,它不是开放的,所以你可以继续前进。
有了这些问题的答案,您就可以预览一下package.json
文件将会是什么样子。你需要做的最后一件事就是批准它。
NPM 已经创建了一个文件,你可以用它来跟踪将成为你的应用的一部分的库。它的核心只是一个 JSON 文件,但是在你要使用它的上下文中,它不仅知道你正在使用什么库,而且还跟踪你未来的应用工作所需的版本和任何依赖库。
下一节将向您展示如何将包添加到项目中。
向 package.json 添加库
现在您有了一个package.json
文件来跟踪您想要使用的库以及这些库在您的项目中的版本。使用这个文件,您还可以运行本地服务器来查看应用的工作情况。您还可以运行脚本来帮助您进行站点的开发、测试和生产。
向项目中添加库时,您可以返回命令行并直接请求它。NPM 将找到 Git 库并将其添加到您的项目中。对于这个例子,让我们将 jQuery 添加到您的项目中,并探索如何使用它。
回到命令行,你应该和package.json
文件在同一个文件夹中;输入npm install jquery
。
这段代码找到装有最新版本 jQuery 的 Git 存储库,并将其下载到您的机器上。它还在该文件夹中创建一个名为node_modules
的文件夹,这是一个包含您刚才请求的库的jquery
文件夹。
该过程创建的最后一个文件是一个package-lock.json
文件。该文件有助于确保每次都安装了您正在使用的所有库的准确版本。
面试问题
你应该投入到你的项目中吗?回答:是的。这将确保团队中的每个人都安装了项目中使用的所有库的相同版本。
到目前为止,您已经创建了一个package.json
文件,并向其中添加了一个库。此时,你还没有一个完整的工作网站。接下来的几个例子将展示如何使用 NPM 来组织一个项目。
在前面的一章中,您添加了一个名为http-server
的包;这为您创建了一个将当前文件夹用作 web 服务器的零配置方式。
让我们将这个包添加到您的项目中。在命令行中,确保您位于项目的根目录,并通过键入"npm install --save http-server"
来安装http-server
。
安装了这个库之后,您可以更新您的脚本,这样您就可以让package.json
告诉服务器它应该何时运行。因为这只是一个 JSON 文件,所以您可以在任何编辑器中打开它,并对其进行一些修改。
在任何编辑器中打开文件。要更新脚本部分,您需要添加一个脚本来启动服务器。在 scripts 部分下,添加一个名为start
的脚本。清单 9-1 有代码。
"scripts":{
"test":"echo\ "Error: no test specified\" && exit 1",
"start": "http-server"
}
Listing 9-1Updating the package.json File so the Start Script Will Run a Local Server
现在,当在命令行时,你只需要键入npm start
,这将启动本地 web 服务器。要停止服务器运行,可以键入 Control + C。
您已经使用 NPM 下载了两个库,但是您还没有任何 HTML 文件可以显示。
要解决这个问题,您需要返回命令行并创建一个文件。键入touch index.html
,这将创建一个名为index.html
的空白文件。
新创建的 HTML 文件中没有任何内容。你可以通过使用一个名为 htmlshell.com
的网站生成的代码来解决这个问题。在默认设置下,它会给你一个 HTML 站点的基本结构,现在这就是你所需要的。
将 HTML 添加到索引文件后,您可以再次启动项目,并查看本地 web 服务器加载的新创建的 HTML 页面。
要做到这一点,在命令行输入npm start
,这将再次启动服务器。
打开浏览器,输入127.0.0.1:8080
。这将告诉浏览器查看本地机器(8080 是默认端口号;其他项目可能使用不同的号码在本地机器上提供网页)。
当前页面为空白,如图 9-1 所示。如果您愿意,您可以在文档正文中键入内容,以确认该页面已被提供。
图 9-1
显示来自本地服务器的新 HTML 页面
现在有了启动本地服务器并为您提供 HTML 页面的package.json
文件。您需要将 JavaScript 添加到项目中。如果您仍然打开命令行工具,创建一个scripts
文件夹来添加一些 JavaScript 到项目中。
在命令行,停止服务器,键入mkdir scripts
;这将创建一个文件夹,您可以在其中添加 JavaScript。现在,您可以进入该文件夹并创建新文件。
键入cd scripts
更改目录,将您放入scripts
文件夹。一旦进入,使用相同的touch
命令并创建一个新的 JavaScript 文件。
在scripts
文件夹中输入touch app.js
。这将创建一个新的空白文件,您可以在其中添加 JavaScript 命令。
在开始编写 JavaScript 之前,您需要确保 HTML 页面知道有 JavaScript 可以使用。
在你的代码编辑器中,打开 HTML 页面(如果它还没有打开的话),添加一个指向scripts
文件夹和你新创建的app.js
文件的script
标签。代码应该如清单 9-2 所示。
/* index.hml*/
<body>
<script src="scripts/app.js"></script>
</body>
//app.js
document.addEventListener('DOMContentLoaded', () => {
console.log('Document Loaded');
});
Listing 9-2Connecting Your HTML Page to the JavaScript File
您在结束的body
标签之前添加了script
标签,以便在浏览器开始呈现任何 JavaScript 之前呈现页面的其余部分。
准备好之后,添加一些 JavaScript 进行检查,确保您知道您的本地服务器正在工作。您可能需要刷新浏览器或再次启动服务器才能看到此工作。但是,当您打开开发人员工具并查看控制台面板时,您应该会在控制台中看到消息“Document Loaded”。
这段代码查看document
对象并监听一个名为DOMContentLoaded
的事件监听器。当该事件发生时,会触发一个匿名函数,向您提供一条消息。
到目前为止,您已经有了一个正在运行的本地服务器和 JavaScript。但是,您还不能将 jQuery 库连接到您的项目。
为了让它工作,您需要添加一个模块构建器。下一节将解释这是如何工作的,以及为什么它很重要。
模块捆扎机介绍(网络包)
到目前为止,您已经使用命令行做了很多工作。您创建了一个package.json
文件,帮助您为将来的项目加载多个库。您还将添加到该文件的脚本部分,以便以后可以使用命令行来运行本地文件服务器。这种能力让你知道你发布的网站会是什么样子。
到目前为止,您还没有使用之前安装在项目中的 jQuery 库。如果您还记得,在安装 jQuery 时,它创建了一个名为node_modules
的文件夹,其中包含您的库。
浏览器理解模块的概念。模块是拥有一些 JavaScript 代码的能力,这些代码将提供一些特定的功能。这个单独的代码可以导入到更大的项目中。
随着您的项目开始变得越来越复杂,您将需要一些不同的模块来使一切正常工作。如果能添加这些模块,而不是为您想要使用的每个模块在文档中添加一长串的script
标签,那就太好了。
Webpack 是一个工具,您可以使用它来帮助导入您在项目中使用的任何库,并确保代码与一些旧的浏览器兼容。
要让 Webpack 工作,您需要在命令行上花更多的时间。如果您当前正在运行应用,请停止运行它,并安装 Webpack 应用。
在命令行中,键入
npm install webpack webpack-cli –-save-dev
这将把 Webpack 库添加到您的项目中。额外的标志-–save-dev
用于将库添加到您的项目中,但不是在您开发前端代码时使用的。
当查看package.json
文件时,您可以看到这个库已经被添加到一个名为 devDependencies 的新部分。这一节只针对那些帮助你使用应用的实用程序,而不是帮助你开发应用。在这一节中,可以结束的一些东西是帮助您运行单元测试或其他开发工具的库。
现在您已经将 Webpack 作为开发工具的一部分,您将使用它来创建您的应用。
返回到您的package.json
文件,向脚本部分添加一个新选项。创建一个名为dev
的脚本,并赋予它webpack
的值。
它应该是这样的:
"dev":"webpack"
编辑该文件后,在命令行中键入
npm run dev
这将抛出一个错误,如图 9-2 所示。
图 9-2
将 Webpack 添加到项目后运行脚本
有时得到一个错误是令人失望的,但在这种情况下,它是好的。你知道 Webpack 已经安装,它只是需要进一步的指导如何工作属性。
没有足够多的人做的事情之一是阅读错误消息。在尝试解决问题时,理解问题非常重要。
这个错误告诉你,你需要添加更多的信息到你的脚本中。现在,它默认为生产模式,这不是你现在需要的。
返回到dev
脚本并更新它:
"dev":"webpack --mode development"
Webpack 有一些缺省值,您需要遵守这些缺省值才能让您的示例正常工作。如果再次运行该脚本,仍然会出现错误。
上一个示例中的文件和文件夹的结构不符合 Webpack 需要的默认结构,因此您需要调整您的文件以使用 Webpack。
Webpack 希望index.js
文件位于src
(源)文件夹中。如果您使用的是前面例子中的代码,将scripts
文件夹重命名为src
,将app.js
文件重命名为index.js
。
如果您再次运行该脚本,您应该不会遇到同样的错误。
当脚本执行完毕时,它会创建一个dist
文件夹。这是存放所有编译代码的文件夹。
当使用像 Webpack 这样的工具时,所有被处理的代码都在dist
文件夹中结束。除了 JavaScript 文件之外,这还包括 HTML 文件和 CSS 文件。
当刷新浏览器时,HTML 文件不识别在dist
文件夹中发生的事情,因为script
标签没有指向该文件夹中的 JavaScript。
您需要更新 HTML 文件以指向dist
文件夹中的 JavaScript 文件。
使用 Webpack 的一个好处是,您不需要在 HTML 文档中为每个要使用的库设置多个脚本标记。
回到 HTML 页面,更新script
标签,指向dist
文件夹和其中包含的main.js
文件。
只是为了确保一切都按您期望的方式运行,通过返回命令行并再次键入npm run dev
来重新编译应用。
您在package.json
文件中编写的第一个脚本是start
脚本。当运行这个脚本时,你可以使用当前文件夹作为 web 服务器和服务器的index.html
文件。
为了使本练习生效,您需要打开终端窗口的两个实例,一个用于查看服务器运行,另一个用于使用 Webpack。
在一个窗口中,运行名为npm start
的脚本。它启动服务器,并告诉您将哪个本地地址放入您的浏览器,以查看网站的运行情况。
在另一个窗口中,您可以继续更新 Webpack 脚本。
此时,您应该能够看到更新后的脚本在本地 web 服务器上运行。在浏览器的开发工具打开的情况下,您仍然会在控制台面板中看到相同的“Document Loaded”消息。
所有这些设置现在都让您能够利用 NPM。
在本课一开始,您将 jQuery 加载到了您的package.json
文件中。现在,您可以将该库添加到项目中,而无需将其添加到 HTML 页面中。
在index.js
文件中,在顶部添加这一行:
import $ from 'jquery'
然后在当前代码的上方添加一些代码行,确保 jQuery 正常工作。完成的代码看起来应该如清单 9-3 所示。
import $ from 'jquery';
$(document).ready(()=> {
console.log('hello from jquery');
});
document.addEventListener('DOMContentListener', ()=>{
console.log('Document Loaded');
});
Listing 9-3Importing jQuery into Your JavaScript Project
再次运行npm run dev
命令,然后刷新浏览器。查看控制台面板,您应该会看到两条消息。
如果您正在使用 Chrome,并且在重新编译应用后没有看到这两条消息,请单击并按住刷新按钮,然后选择清空缓存和硬重新加载选项。只要开发工具是打开的,这应该会给你想要的结果。
您的应用现在可以利用其他库,而无需添加到 HTML 文件中。您可以将它们直接导入到当前文档中,并使用 Webpack 之类的工具将它们打包到 JavaScript 文件中。
你可以把你的 HTML 文件和dist
文件夹的内容上传到一个实时的网络服务器,它应该完全一样。
然而,对于发展来说,你有一些迫在眉睫的问题。首先,应用不知道何时应该重新编译代码。如果您进行了更改,您应该不必手动告诉 Webpack 该做什么。
另一个问题是,如果你想在你的项目中添加 Sass(语法上很棒的样式表), Webpack 应该能够使用它。
第三个问题是,如果你想使用最新版本的 JavaScript。更新的代码库将包括旧浏览器可能不支持的语法更新。
下一节将讨论这些问题的解决方案。
添加 webpack-dev-server
在这里,您将看到上一节中讨论的一些问题。
您知道当您的任何源代码更新时,您需要手动重新编译您的代码。这是不可接受的情况,你需要解决它。您可以通过使用 Webpack webserver 来解决这个问题。这将替换您以前使用的服务器。
如果服务器当前正在运行,请将其停止。返回命令行并安装 Webpack 服务器。类型
npm install webpack-dev-server –save-dev
安装完成后,您可以看到package.json
文件已经更新。
现在它已经更新了,您可以更新您原来的start
脚本,告诉它使用 Webpack dev 服务器而不是 http-server 服务器。
回到您的开发环境,通过键入以下命令更新start
脚本
webpack-dev-server –mode development -–open
在这里,您将指导您的脚本使用 webpack 服务器并添加标志-—open
。此标志将打开默认浏览器并加载索引页面。
这样做的额外好处是,当您对文件进行更改时,它会自动更新和刷新页面。这将在您每次编辑时为您提供页面的更新版本。
为了测试这一点,对 JavaScript 页面进行更改并保存文件。您应该看到终端窗口更新,浏览器刷新页面。
现在,您已经有了 Webpack 工作的默认配置。您的开发服务器正在运行,它将识别您的应用中的文件何时被更新。当文件更改时,Webpack 将重新编译代码并刷新浏览器,这样您就可以看到网站的最新版本,就好像它是由服务器提供的一样。
JavaScript 是一种不断变化的语言。伴随这一变化而来的问题是,并非所有的浏览器都跟上了这一变化。根据项目的不同,您可能需要支持没有您想要的功能的旧浏览器。
下一节将介绍 Babel.js 作为解决这个问题的方法。它允许您编写最新的 JavaScript,但仍然具有您在项目需求中工作所需的向后兼容性。
添加 Babel.js
Babel(位于 https://babeljs.io/
)是一个工具,当你想使用 JavaScript 的最高级特性,并且仍然支持可能还不支持这些特性的浏览器时,你可以使用它。
很容易将这个特性添加到您的项目中。既然已经有了 Webpack,您只需要添加一些配置文件,这样 Webpack 就知道它应该通过 Babel 处理所有的 JavaScript。
您需要做的第一件事是返回命令行,将 Babel 添加到您的项目中。在命令行中,键入
npm install @babel/core babel-loader @babel/preset-env –save-dev
这一行将安装巴别塔的三个部分:
-
巴别塔核心
-
巴比伦装载器
-
Babel 预置环境,在这里你可以将 ES6 代码编译成旧版本的 JavaScript
下一步是配置 Babel。配置 Babel 的方法是使用.babelrc
文件。如果您以前使用过 Git,您可能对此很熟悉。当文件名前面有一个点时,该文件对操作系统是隐藏的。您可能需要更改操作系统的设置才能看到这些文件。
如果您在命令行,您可以通过键入"touch .babelrc"
来创建一个同名的空白文件。该文件通常位于用户目录的根目录下。
您可能需要更新操作系统设置才能看到这些文件。打开.bablerc
文件,添加清单 9-4 中的代码。
{
"presets":[
"@babel/preset-env"
]
}
Listing 9-4Configuring the .babelrc File
这在巴别塔内部建立了一个预设。它允许您使用最新版本的 JavaScript,而不需要在每次语言更新时都更新这个文件。它还将代码转换成一种能在大多数浏览器中工作的方式。
现在,您已经设置了 Webpack 和 Babel。你需要一种方法让这两个部分一起工作。
当运行在package.json
文件中定义的脚本时,您的目标是让 Webpack 使用 Babel 来编译您的 JavaScript。为此,您现在需要为 Webpack 添加一个配置文件。这将把两个库联系在一起。
到目前为止,您已经使用了默认设置,它们为您提供了大量现成的功能。由于 Babel 不是 Webpack 的一部分,您需要使用一个简单的 JavaScript 文件来指导它。
创建一个名为webpack.config.js
的文件。默认情况下,Webpack 会查找该文件。与另一个练习类似,转到命令行并键入"touch webpack.config.js"
。结果将是一个空白的 JavaScript 文件,您可以使用它来配置 Webpack。该文件通常位于项目的根目录下。
这个配置文件为您想与 Webpack 一起使用的每个库设置配置设置。在这个实例中,您导出了一个包含 Babel 所有设置的模块。
将清单 9-5 中的代码添加到您的配置文件中,然后您就可以查看细节了。
module.exports = {
module:{
rules:[{
test:/\.js$/,
exclude: /node_modules/,
use:{
loader:"babel-loader"
}
}]
}
}
Listing 9-5Configuring the webpack.config.js File
该文件导出 Babel 的配置选项。此时此刻,巴别塔是你唯一的配置设置。将来,这可能会被更新,以允许 Webpack 处理 CSS 文件、TypeScript 文件和包括图像在内的其他文件类型。
您的配置设置在module
对象中。这个对象有一个名为rules
的数组。该数组包含规定加载程序如何工作的规则。第一行是test
,这个规则使用了一个正则表达式。正则表达式告诉加载器应该处理哪种类型的文件。在这种情况下,它只适用于 JavaScript 文件。
下一项是可选的。它告诉 Webpack 不要查看某些文件夹。这里您排除了node_modules
文件夹。忽略这个文件夹是有意义的,因为它不是应用代码的一部分。
您的示例中的最后一个选项称为use
。该选项下的一个必选项是loader
。加载程序必须始终是字符串。你的加载器是连接 Babel 和 Webpack 的纽带。现在,它将查看项目中的所有 JavaScript 文件,并参考 Babel 配置文件,以便知道如何处理这些文件。
Webpack 处理 JavaScript 时,您会在dist
文件夹中找到结果。
目前,您的 HTML 页面还没有被处理。你可以在 Webpack 中添加一个插件,确保你的 HTML 文件得到优化,并保存在你的dist
文件夹中。
下一节将详述您在这里所做的工作,并展示如何使用 Webpack 来优化您的 HTML 和 CSS 文件。
添加 HTML 和 CSS 加载器
为了让第一部分工作,您需要添加 HTML Webpack 插件。除了添加这个插件,您还必须添加加载器,它将负责加载 HTML 文件进行处理。
使用命令行,您可以通过键入以下内容来添加 Webpack 插件和加载程序:
npm install html-webpack-plugin html-loader –save-dev
当这一行被执行时,package.json
文件被更新。这使您能够更新您的配置文件,以使用插件和加载程序。
您需要做的第一件事是require
HtmlWebpackPlugin 来使用 Webpack。使用require
类似于导入。使用它,您可以将特性添加到配置文件中,以便与 Webpack 一起工作。
一旦建立了这种关系,您就可以更新角色,以了解加载程序将如何工作。规则告诉 Webpack 它需要加载所有 HTML 文件并优化它们。
你需要做的最后一件事是添加一个插件部分。插件部分中的数组的第一个元素是 HtmlWebpackPlugin。
传递给该函数的对象包含两个属性:HTML 模板(这是您在src
文件夹中的原始文件)和确定 HTML 文件的路径和文件名的属性。现在,保留默认值,继续称它为index.html
。这将处理后的 HTML 文件添加到dist
文件夹中。
此时,您的文件应该如清单 9-6 所示。
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
module:{
rules:[{
test:/\.js$/,
exclude: /node_modules/,
use:{
loader:"babel-loader"
}
}]
},
{
test:/\.html$/,
use:[
loader:"html-loader",
options:{minimize:true}
]
}
},
plugins:[
new HtmlWebpackPlugin({
template:"./index.html",
filename: "./index.html"
})
]
}};
Listing 9-6Adding the HtmlWebpackPlugin to webpack.config.js
现在,运行构建脚本会将index.html
文件添加到dist
文件夹中。
现在 Webpack 可以处理 HTML 和 JavaScript 文件了。优化确保了输出可以在尽可能多的浏览器中工作。
你缺少的一件东西是 CSS。CSS 本身并不是一种编程语言,但是通过使用 Sass 之类的东西,你可以使用类似于编程语言的特性。
例如,如果您想在多个地方重用一种颜色,Sass 会帮助您创建一个保存当前颜色值的变量。一旦萨斯或 SCSS 文件被编译,它们只是浏览器理解的 CSS 文件,可以在你的项目中使用。
为了在您的项目中使用 Sass,您需要向 Webpack 添加一些加载器,以便它可以将 Sass 转换为 CSS。
在命令行中,键入以下内容:
npm install –-save-dev style-loader css-loader node-sass mini-css-extract-plugin sass-loader
应该用已经下载的库的引用来更新package.json
文件。下一步是配置 Webpack 以利用这些新的更新。
随着这些新功能添加到您的项目中,您现在可以更新 Webpack 来利用它们。您需要更新webpack.config.js
文件,以便您的 JavaScript 文件可以导入 SCSS 文件并应用样式。
要完成这项工作,格式与其他示例完全相同。您向对象数组中添加了一个测试和一个使用部分,这将负责您如何处理 SCSS 文件。您的webpack.config.js
文件应该如清单 9-7 所示。
const HtmlWebPackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
modules.export = {
module:{
rules:[
test:/\.js$/,
exclude: /node_modules/,
use:{
loader:"babel-loader"
}
]
},
{
test:/\.html$/,
use:[
loader:"html-loader",
options:{minimize:true}
]
},
{
test:/\/scss$/,
use:[
"style-loader",
"css-loader",
"sass-loader"
]
}
},
plugins:[
new HtmlWepPackPlugin({
template:"./index.html",
filename: "./index.html"
}),
new MiniCSSExtractPlugin({
filename: "[name].css",
chunkfile: "[id].css"
})
]
}};
Listing 9-7Adding the Sass Features to webpack.config.js
设置好更新后的配置文件后,现在可以创建一个 Sass 文件,并将其导入到 JavaScript 文件中。
您可以通过创建一个保存颜色值的变量来利用 Sass,然后在声明元素的颜色时使用该变量。您的 Sass 文件应该如下所示:
//_sass/main.scss
$header-color: #b7cbcb;
.header{
background-color: $header-color;
width:100%;
height: 200px;
}
首先用一个值定义变量background-color
,在本例中是一个十六进制颜色。下一次使用该变量是在定义名为header
的 CSS 类时。
既然已经创建了.scss
文件,您需要将它导入到您的 JavaScript 文件中,以便应用可以使用它。
回到最初的index.js
文件,您可以在顶部添加这一行:
import './_scss/main.scss';
你有大部分的工作;最后一部分是将它绑定到 HTML 元素。
打开index.html
文件,添加一个div
标签,在添加class
属性时,给它赋值header
。应该是这样的:
<div id="app" class="header"></div>
还好div
里面什么都没有。您只想看看在您的应用中使用 Sass 的效果。
完成所有这些后,您可以返回到命令行。如果您没有运行本地服务器,请键入
npm start
这应该运行本地 Webpack 服务器,编译所有代码,包括.scss
文件,并将其转换成 CSS。
浏览器应该如图 9-3 所示。
图 9-3
编译的 Sass 在浏览器中呈现为 CSS
摘要
您的项目现在能够导入外部库,在文件发生变化时自动更新,并输出可以在尽可能多的浏览器上工作的 JavaScript,尽管是使用最新的 JavaScript 开发技术编写的。
您还使用了 Sass,这意味着您的样式可以在尽可能多的浏览器中工作。您获得这些特性是因为您使用 NodeJS 作为基础。Webpack 使用 Node 给你一个本地服务器来使用。您的脚本告诉服务器监视这些文件,这样页面就会随着变化而刷新。
这只是如何在客户端使用 Node 的开始。节点是大量客户端工具的构建块。例如,使用最新版本的 Angular,您可以使用命令行来构建一个基本的应用。
React 有一个类似的工具叫做create-react-app
。这两种工具都可以为您完成大量工作,因为 Node 可以访问文件系统,并且可以为您创建文件和文件夹。
这还不是你能用 Node 做的所有事情的结束。你可以探索一个全新的领域。Node 还使您能够使用 JavaScript 运行应用服务器。下一章将探讨 Express 框架,以及如何使用它不仅服务于网页,还能访问数据库中的数据。
十、JavaScript 和服务器端开发
在上一章中,您使用了 NodeJS 作为帮助客户端开发的一种方式。您使用的工具都是由 Node 驱动的。如果没有 Node,创建文件和将 Sass 和高级 JavaScript 等语言转换成浏览器可以理解的内容的能力是不可能的。
这一章将采用这一思想并加以扩展。您将使用 Node 作为 web 服务器。不仅仅是一个本地服务器,这是你在上一章做的,而是一个成熟的生产服务器。
为此,您将使用上一章中的大多数工具。在命令行中,创建一个文件夹,进入该文件夹,使用 NPM 创建一个新的package.json
文件。您的命令应该如下所示:
mkdir nodeProject
npm init
在这一点上,您将会被问到一些问题,这些问题将会被用来构建这个文件。有关所有选项的详细分类,请参阅上一章。
对于本练习,您可以通过按 Return 键并接受结果来跳过选项。最后,向项目中添加 Express 框架。这与您对前端项目所做的完全相同。在命令行中,安装 Express 并将其另存为依赖项:
npm install express –save
这将更新package.json
文件,并赋予您使用 Express 的能力。
基本快速设置
设置完成后,您现在可以创建一些文件来告诉 Express 启动,监听您的请求,并返回一个字符串。这是一个“Hello World”的例子。在命令行中,创建一个名为app.js
的文件。这是你的起点。
在这个文件中,需要 Express 框架,启动应用,监听请求,并返回响应。清单 10-1 显示了代码。
//app.js
const express = require('express');
const app = express();
const port = 3000;
app.get('/', (req, res) => {
res.send("Hello World");
});
app.listen(port, () => {
console.,log(`Running on port ${port}!`);
});
Listing 10-1Setting Up a Basic Express Server
您的文件现在已经包含了 web 服务器的所有基本组件。您可以监听来自浏览器的请求。该请求发送一个带有文本“Hello World”的响应。所有这些都在端口 3000 上。要查看这一过程,请返回命令行并键入
node app.js
这告诉 Node 查看文件app.js
。然后这个文件执行代码并开始运行 Express 服务器。现在,如果你打开浏览器并输入localhost:3000
,它应该看起来像图 10-1 。
图 10-1
用 Node 和 Express framework 启动一个简单的 web 服务器
您添加到应用中的代码允许服务器在端口 3000 上运行。这很重要,因为如果您选择不同的号码,您将不会从服务器得到响应。请注意这一点,因为其他应用使用不同的数字进行开发。web 服务器的默认数量是 8080。
将 nodemon 和路由添加到 Express 应用
在上一章中,您添加了一个特性,每次文件更新时站点都会刷新。这使得开发变得更加容易,因为您不必在每次更改后停止并手动重新编译应用。
为了让您的节点应用做到这一点,您需要添加一个名为nodemon
的库。它提供了类似的功能。要安装它,请在命令行键入以下内容:
npm install -g nodemon
在安装节点监视器时使用-g
标志来确保它是全局可用的。安装后,您可以在硬盘上的任何地方使用该库。
安装了这个库之后,您可以更新您的package.json
脚本,以便在您的开发中使用nodemon
。start
脚本现在应该是这样的:
nodemon app.js
这将监视您的应用,并在您对文件进行更改时重新启动服务器。
现在,您不必在每次进行更改时手动重启服务器,您现在可以扩展您的应用了。
扩展应用的第一种方法是创建一个叫做 route 的东西。路由是服务器上返回 HTML 页面等内容的路径。
例如,在第一个练习中,只需查看服务器的根目录。如果你想要一个名为localhost:3000/users
的路由,目前它会抛出一个错误,说它不能“获得”用户。要解决这个问题,您需要创建一个路由。下一节将解释如何做到这一点。
使用节点创建路线
向您的站点添加路由将设置您希望服务器执行的操作。为了全面理解路由是如何工作的,让我们首先研究 HTTP 是如何工作的,然后研究它如何与您的路由相关联。有了充分的理解,你就可以进入代码。
HTTP(超文本传输协议)有一个请求-响应模型。浏览器向服务器发出请求,然后服务器用资源做出响应。该资源通常由 HTML 页面组成,但也可以是其他类型的数据。
HTTP 有一系列描述需要执行的动作类型的方法或动词。
大多数时候浏览器向服务器发出请求,只是想“获取”信息。GET 是当您想要从服务器检索数据时使用的动词之一。以下是动词列表:
-
GET:从资源请求数据
-
POST:请求服务器在数据库中创建资源
-
PUT:请求服务器更新资源。如果该资源尚不存在,则创建它。
-
删除:请求从数据库中删除资源
有了这种理解,你现在可以想出使用这些 HTTP 动词时路由看起来是什么样子的例子。
在下一个示例中,您将从大处着手,深入到更多细节。如果您想要了解美国各州的信息,路线如下所示:
localhost:3000/states
这里,您发出一个 GET 请求,请求所有州的信息。根据数据库的设置方式,如果您想要第 11 个州的信息(纽约),路线如下所示:
localhost:3000/states/11
在这两个例子中,您引用的资源是states
。您知道团队中有人在数据库中创建了关于状态的信息。
选择保留“states”而不是“state ”,这为您提供了灵活性。例如,第二条路线缩小了范围,您希望查看第 11 个个州。
延伸一下,如果要删除第 11 个个状态的所有信息呢?路径将是相同的,但是您将发送一个删除请求。
如果您想获得纽约所有的行政区信息,您的路线应该是这样的:
localhost:3000/states/11/boroughs/
如果你只是想了解布鲁克林,路线会是这样的:
localhost:3000/states/11/boroughs/3
现在,您有了一种描述如何访问数据库中的项目的通用语言。像这样格式化路线使您能够创建、读取、更新或删除信息。这通常被称为积垢。
到目前为止,我已经讨论了通常所说的 REST 服务(代表性的状态转移)。记住这一点,您可以请求服务器对数据库执行操作。现在让我们看看如何用 Node 实现它。
使用前面的例子,创建路线并不困难。如果您想发出一个 GET 请求,它将类似于清单 10-2 。
app.get('/states/, (req, res) => {
res.send("This is the States Page");
});
Listing 10-2Creating a GET Route Using Node
这里您可以看到,当请求路由时,Node 将运行一个函数并发送一个响应。
现在您已经对 Node 如何处理 HTTP 动词有了一个基本的了解,其他的函数也就非常熟悉了。参见清单 10-3 。
app.post('/states/, (req, res) => {
res.send("This is the States Page POST request");
});
app.put('/states/, (req, res) => {
res.send("This is the States Page PUT request");
});
app.delete('/states/, (req, res) => {
res.send("This is the States Page DELETE request");
});
Listing 10-3Using the Other HTTP Verbs with Node
现在您已经理解了如何对 Node 使用 HTTP 动词。清单 10-4 展示了如何服务位于states
文件夹中的 HTML 页面。
//app.js
const express = require('express');
const app = express();
const path = __dirname + '/views/';
const port = 3000;
app.get('/states, (req, res) => {
res.sendFile( path + 'states/index.html');
});
app.listen(port, () => {
console.log(`Running on port ${port}`);
});
Listing 10-4Setting the Path to Load HTML Pages
这个例子展示了如何从浏览器接收一个请求,然后将一个 HTML 文件发送回浏览器。这个例子之所以有效,是因为您创建了一个views
文件夹,并在其中创建了一个名为states
的子文件夹。该文件夹包含 HTML 页面。
使用__dirname
给 Node 当前运行文件的路径。在这个例子中,您从根目录开始添加views
文件夹。从那里,当请求路由时,Node 可以运行一个函数来查看states
文件夹,并将states.html
发送回浏览器。
现在您已经了解了路由是如何工作的,并且知道如何将响应或 HTML 页面发送回浏览器,让我们来看看如何将数据从数据库发送回浏览器。
设置 MySQL 的本地实例
在上一节中,我介绍了 REST 服务的概念,使用服务器路径和 HTTP 动词的组合来描述需要对数据做什么。
在某些情况下,返回一个 HTML 页面正是我们所需要的。一些信息是静态的,不需要引用数据库内部的信息。
在其他情况下,您可能正在开发类似单页面应用(SPA)的东西。在这些情况下,您不会根据路径加载单个页面。使用像 React、Angular 或 Vue 这样的库,应用将计算出路线是如何工作的,而不使用服务器。
当在这样的环境中工作时,web 服务的路径对于前端应用变得非常重要。路由将决定应用如何发出请求以及如何检索信息。Web 服务通常位于与前端应用不同的服务器上。
在本节中,您将创建一个本地数据库,添加一些数据,并通过 Node 使其作为 web 服务可用。首先,您需要下载并安装一个数据库。
MAMP/WAMP (MacOS/Windows,Apache,MySQL 和 PHP)是一个点击式的开源软件栈。各个部分被放在一起,所以你可以有一个简单的安装和建立一个网站,而不需要配置一切。
栈的每个部分都可以是一本书,但是对于您的目的,您只需要查看 MySQL 数据库。
这个堆栈附带了一个名为 PHPMyAdmin 的 PHP 工具,可以让您编辑本地数据库。您还可以下载一些应用来帮助编辑和更新数据库,而无需了解大量 SQL(结构化查询语言)。其中一些工具包括 Mac 上的 Sequel Pro 和 Windows 上的 SQL Pro。
安装好软件包后,您就可以启动服务器了。这将打开您的默认浏览器窗口,显示欢迎使用 MAMP 页面。打开后,您现在可以访问 MySQL 数据库。
您需要创建一个数据库来使用。我不会涉及数据库如何工作的所有细节。您将创建一个简单的表,并使用 Node 创建一个简单的 API,允许您创建、检索、更新和删除数据。
为了理解什么是表格,可以把它想象成一个电子表格。使用电子表格时,所有信息都被分成行和列。数据库以类似的方式工作。在表格中,每一列用于特定类型的数据,每一行由您正在处理的数据组成。
使用任何可用的工具来创建数据库。Sequel Pro 有一个选项可以让你添加一个数据库。对于您的示例,将其命名为api
。一旦创建了数据库,接下来要做的就是创建一个表来保存数据。您的脚本将创建一个名为boroughs
的表。该脚本应该类似于清单 10-5 。
CREATE TABLE `boroughs` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(30) DEFAULT ",
`state` varchar(50) DEFAULT ",
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Listing 10-5A SQL Script to Create a Table Called boroughs
关于这个脚本,需要知道的最重要的事情是,它创建了一个名为boroughs
的表,这个表将包含名为id
、name
和state
的列。见图 10-2 。
图 10-2
运行 SQL 脚本来创建表
应该有一个可以添加查询的区域。添加之后,运行查询并创建表。
创建完表后,您需要再运行一个脚本,这样您就可以处理少量数据了。这个脚本将插入Brooklyn
作为name
和New York
作为state
。您没有为 ID 添加值,因为该字段设置为自动递增。有了这个设置,每次你添加一个新的条目到数据库中,它将获得一个唯一的编号。
下面是一个insert
脚本应该是什么样子:
INSERT INTO `boroughs` (`id`, `name`, `state`) VALUES (NULL, 'Brooklyn', 'New York');
现在您有了一个包含一些数据的表,您可以返回到 Node 并使用您的 API。首先,您需要将 MySQL 连接到您的节点应用。
您应该做的第一件事是将 MySQL 模块安装到您的项目中。这就像您在命令行中完成的所有其他工作一样。通过运行以下命令添加模块:
npm install mysql –save
安装后,它将使引用 MySQL 和访问新创建的数据库中的数据变得容易。
在第节中,您使用 W/MAMP 快速设置了一个数据库,并运行了几个 SQL 脚本来创建一个数据库和数据库中的一个表。现在,您可以使用 Node 从数据库中访问数据了。节点将对数据库运行查询,并将结果返回给浏览器。
弄清楚你将如何组织你的文件是很重要的。您确定了根据您想要返回特定类型数据的路径。
下一节将讨论如何用 Node 创建路由并从数据库中检索数据。
使用 NodeJS 从 MySQL 返回数据
在上一节中,您下载并设置了一个数据库。您还添加了一些数据,以便测试您的路线。路由是您要传递给节点的路径,告诉它您需要某种类型的数据。
您设置项目的方式将赋予您扩展和添加其他路线的能力,但是对于这个实例,您将专注于这一条路线。
你需要创建几个文件夹来存放你的文件。在当前项目中创建一个data
文件夹和一个controllers
文件夹。
在data
文件夹中,创建一个名为config.js
的文件来保存数据库的所有配置设置。
在controller
文件夹中,添加一个index.js
文件和一个boroughs.js
文件。有了这些文件,您就可以开始编写配置选项了,这样您就可以连接到 MySQL 数据库了。
在config.js
文件中,添加清单 10-6 中的代码。
const mysql = require('mysql');
const config ={
host:'localhost',
user:'root',
password:'root',
database:'api'
port:'8889'
}
const pool = mysql.createPool(config);
module.exports = pool;
Listing 10-6Configuration Settings for Node to Connect to the MySQL Database
本例中的代码类似于您创建节点服务器的方式。首先,mysql
库是必需的,并被赋给一个变量。然后创建一个包含所有配置设置的对象。在那之后,你创建一个所谓的pool
。这使您能够拥有到数据库的多个连接,并有助于管理这些连接。最后,导出用于代码其他部分的pool
对象。
现在,您可以连接到本地数据库了。现在您需要创建一个路由,它将成为您的 API 的一部分来检索数据。
原始的app.js
文件是节点应用的起点。在前面的例子中,您学习了如何监听请求并做出响应。
现在,您需要让 Node 捕获请求,并过滤出该请求需要到达的位置。这可以通过使用use
方法来实现。打开app.js
并添加清单 10-7 中的代码。
app.use(require('./controllers'));
Listing 10-7Having Node Send All Requests That Are Not to the Root of the Application to the Controllers File
这段代码从浏览器获取所有请求,并将其发送到所需的controllers
文件夹。
之前,您在controllers
文件夹中创建了一个名为index.js
的文件。默认情况下,Node 将引用这个文件并使用它来决定下一步做什么。
此示例创建了一条路线;但是,您可以随意添加。您可以在app.js
文件中添加所有内容。这本来是一个简单的解决方案;然而,它很难扩展和测试。
在这里,您可以清楚地知道哪些文件将处理每个请求。您的index.js
文件应该如下所示:
const router = require('express').Router();
router.use('/boroughs', require('./borough'));
module.exports = router;
第一行应该很熟悉,因为它是您在app.js
中用来让 Node 开始工作的。这里您添加了使用Router
对象的能力。该对象只执行路由功能,并允许您添加诸如身份验证之类的功能。
现在您已经可以访问router
对象,您可以使用相同的use
方法来指向 Node。您告诉 Node,对/boroughs
的每个请求都必须由borough
文件处理。
在这里,您已经能够缩小节点处理路由的方式。从一个非常高的层次来看,您将它定向到controllers
文件夹中的索引文件。然后你找出被请求的直接路线。最后,您处理请求的细节以及如何将响应发送回浏览器。
boroughs.js
文件检查请求是否就在这个路由的根上。然后,它向数据库查询关于这个主题的所有信息。另一种情况是它根据数据库中的索引号发出请求。如果是这种情况,它将发送一个结果。
在您的例子中,数据库中只有一个条目,但这是一个很好的介绍,便于您在下一章中添加和更新条目。
boroughs.js
文件需要更新。这个文件也应该使用 Node 提供的Route
对象。您还可以导入您在本节前面创建的pool
对象,并使用它来请求数据库。
您的文件应该有两个如下所示的方法:
const router = require('express').Router();
const pool = require('../data/config');
router.get('/', (req, res) => {
pool.query('SELECT * FROM boroughs', (error, result) => {
if(error) throw error;
res.send(result);
});
});
router.get('/:id', (req, res) => {
const id = req.params.id;
pool.query('SELECT name, state FROM boroughs WHERE id = ?', id, (error, result) => {
if(error) throw error;
res.send(result);
});
});
module.exports = router;
从顶部开始,创建Route
对象。然后您需要拥有所有数据库信息的pool
对象。
现在,您可以开始处理浏览器或应用对 Node 的请求。当应用发出请求localhost:3000/boroughs/
时,您的第一个方法将处理该请求并调用数据库。
这里你需要了解一点 SQL 通过使用pool
对象,您可以使用query
方法并将您的查询作为字符串传递。
写 SQL 时不需要大写;这更多的是一种形式。SQL 命令通常是大写的。
这里您想要选择名为boroughs
的表中的所有列。您知道您想要所有的列,因为您使用星号作为选择列的方式,而不是通过名称来要求每个列。
添加查询后,回调函数会查找错误或结果。该函数使用一个if
语句来检查错误。如果没有发现错误,它会将查询结果发送回请求它的应用。
第二个功能与第一个非常相似。重要的区别是它在调用中寻找一个参数。第一个方法只是想知道您是否正在请求/boroughs
路线,这里您不仅想知道您是否正在请求该路线,还想通过索引号知道路线。
第二个函数是寻找一个叫做id
的变量;您可以通过如何将请求设置为/:id
来了解这一点。通过在名称前使用冒号,您知道这是一个应用将传递给 Node 的变量。然后,您需要创建一个变量来捕获该值,并将其用作查询的一部分。
一旦有了值,就可以将它添加到查询中。在第一个例子中,您有查询和回调函数。在第二个示例中,在中间添加 id 的值,这样数据库就知道要查找什么 id。
如果服务器和数据库都在运行,您可以进入浏览器并查看结果。在每种情况下,都应该有一个 JSON 对象返回给您。见图 10-3 。
图 10-3
基于从节点请求的路由接收 JSON 对象
现在您有了一个节点服务器,可以用来从数据库中检索数据。您创建的 API 可以基于传递您感兴趣的项目的 id 为您提供单个结果,但是您也可以在单个调用中获得所有信息。
摘要
本章讲述了如何在服务器上使用 JavaScript。您了解了如何使用 Node 和 Express 框架来处理来自浏览器和其他应用的请求。
您安装了 MySQL 作为您的数据库。MySQL 是可以与 Node 一起使用的众多数据库之一。这些示例介绍了一些 SQL,它们创建了一个表并添加了一些数据供您使用。
最后,您利用了Routes
对象。您使用它来计算节点请求的路径。你只对/boroughs
路径感兴趣。如果是浏览器请求的,Node 会对数据库进行查询,并返回一个 JSON 对象和查询结果。
你也知道如何询问非常具体的信息。通过在 URL 中向服务器传递参数,您的脚本提取了该信息,并将其用作查询的一部分。
现在,您可以使用浏览器或应用直接从数据库中检索数据。如果您开发单页面应用,这将变得更加重要。这种性质的应用向服务器发出请求,并根据结果更新页面。
下一章将介绍 Angular 作为一种创建单页应用和从 API 中更新或检索数据的方法。
十一、JavaScript 和应用框架:Angular
为了使讨论简单,当我提到应用框架时,我指的是任何能帮助你快速开发完整 web 应用的库或框架。这可能包括但不限于 Angular、React、Vue 和 Polymer。
其中一些库被认为是完整的框架,而另一些只是库。本章的目标不是将两者对立起来,也不是描述各自的优缺点。这一章的目的是展示你如何利用前几章学到的知识来开发使用这些框架的应用。
在上一章中,您开发了一个 API,它发送一个描述行政区的 JSON 对象。您使用 MySQL 和 NodeJS 的组合让服务器查询数据库并返回查询结果。
本章将介绍如何使用 Angular 开发一个 web 应用,下一章将介绍 React。这两个应用都能够访问上一章开发的 API。
本章的前半部分将介绍如何使用命令行界面(CLI)快速开发 Angular 应用,以生成从 API 检索数据所需的所有文件。
一旦您能够检索和显示您的信息,您就可以创建一个可以在数据库中添加或更新信息的表单。
安装角形
如果你真的想从整体上对 Angular 框架有一个很好的了解,最好去的地方是项目主页( https://angular.io
)。
既然您将使用命令行界面来开发这个应用,那么您应该查看的下一个站点是 CLI 开发的主页( https://cli.angular.io
)。
使用 CLI 与直接使用 Node 时的最后几个示例非常相似。
如果您尚未打开终端窗口,请将其打开,并使用节点软件包管理器将 Angular 加载到您的计算机上。
在命令行中,键入
npm install -g @angular/cli
该命令在全局级别安装角度命令行工具。安装完成后,您可以在硬盘上的任何文件夹中创建 Angular 项目。
Angular 将自己描述为一个在 Web 上构建应用的平台。注意,我们说的不是 AngularJS,俗称 Angular 1。我们只打算讨论 Angular 的最新版本,俗称 Angular 2+。在撰写本文时,Angular 的最新版本是 7。
Angular 是一个开源的、基于类型脚本的平台,由 Google 的 Angular 团队领导。TypeScript 是微软维护的开源语言。
什么是 TypeScript?
TypeScript 是 JavaScript 的超集,这意味着除了将 JavaScript 用作强类型语言之外,您还可以使用 JavaScript 内置的更高级的功能。
TypeScript 和 JavaScript 的区别之一是 JavaScript 不强制数据类型,也就是说你不能强制一个变量只能处理字符串或数字。JavaScript 在处理不同类型的数据时非常灵活。
TypeScript 还具有以下功能:
-
接口
-
无商标消费品
-
名称空间
-
装饰者(实验性特征)
其他语言强调,一旦你将一个变量声明为某种类型,改变该类型将会产生错误。
如果您使用过 Java 或 Scala 之类的语言,语法看起来很相似。声明变量时,可以显式指示编译器将变量视为仅使用特定类型的数据:
let x: number = 42;
这段代码强制 TypeScript 编译器确保赋给 x 的任何变量都是数字。试图分配任何其他类型的数据将在编译时被捕获,并将产生错误。这里有一个例子:let x:number = 42;
x = "Fourty Two" //produces an error
TypeScript 语言有一个将 TypeScript 转换成 JavaScript 的编译器。新创建的 JavaScript 可以被多种浏览器理解。作为开发人员,您可以利用 JavaScript 中并非所有浏览器都能理解的高级特性。使用 TypeScript 的另一个好处是能够在编译时发现错误。TypeScript 为您提供了许多来自面向对象语言的人所期望的特性。
在下一节中,您将使用 CLI 创建一个 Angular 项目,并在浏览器中运行它。
开发角度应用
至此,您已经能够安装 Angular CLI 工具。使用这些工具将帮助您使用 Angular 框架开发应用。Angular 将下载构建应用所需的所有文件,并创建基本 Angular 应用所需的所有文件夹。
在命令行中,键入以下命令创建一个名为my-app
的应用:
ng new my-app
这一条命令会产生一些关于如何开发应用的问题。您可以随时在以后更新应用,但现在让我们来看一下 CLI 询问的一些问题。
第一个问题是,“您想添加角度路由吗?”用 Angular 路由就像用 Node 路由一样。这个想法是将一个路径传递到浏览器中,该路径将显示站点的某个部分。这里最大的不同是 Angular 将处理路由,而不是服务器。
这种区别非常重要,因为两个应用都会尝试解析路由。服务器将使用该路径查找包含基于该路径的文件的文件夹,然后尝试将结果发送回浏览器。因为服务器上不存在这些文件和文件夹,所以结果将是 404 或文件未找到错误。
对于您的示例,我们同意将其添加到您的应用中。在命令提示符下键入 Y,然后继续回答下一个问题。
下一个问题是,“您希望使用哪种样式表格式?”呈现的不同格式有
-
半铸钢ˌ钢性铸铁(Cast Semi-Steel)
-
半导体色敏传感器
-
厚颜无耻
-
较少的
-
唱针
第一种选择应该直截了当。接下来的四个在处理样式表的方式上都是相似的。
这个列表中的前两个是最相似的,在理解了前两个试图解决的问题之后,其他的就很容易理解了。
Sass 和 SCSS 都是预处理语言,这意味着它们会编译成你的浏览器能理解的 CSS。唯一真正的区别是语法。
Sass 使用缩进来表示选择器的嵌套。以下是 Sass 语法的一个示例:
//sass example
$backgroundColor: #C0C0C0; //silver
.container {
h1{
background-color: $backgroundColor;
; }
}
当编译这段代码时,Sass 知道根据文件顶部的变量将容器类中的h1
元素设置为背景色,
Sass 还通过添加嵌套规则扩展了该语言。这些规则被称为混合规则;有了 mixins,除了其他特性之外,您还可以进行数学运算。
扩展名为.scss
的文件具有更灵活的语法。这包括直接写 CSS。它还让您能够使用如下功能。清单 11-1 展示了一个 SCSS 的例子。
//scss example
$textColor: 'red';
p{
color: $textColor;
}
Listing 11-1Creating a Variable to Be Used Later in Your SCSS File
LESS 也是一个会转换成 CSS 的预处理程序。这里的区别在于,LESS 是用 JavaScript 编写的,需要 Node。
创建变量时,语法有细微的差别。萨斯和 SCSS 使用美元符号($)来创建变量,而 LESS 使用 at 符号(@)。
列表中的最后一个选项是手写笔。受 Sass 和 LESS 的启发,它与普通 CSS 的不同之处在于花括号和分号是可选的。变量不需要用美元符号($)或 at 符号(@)声明。
所有这些选项对于管理和缩放 CSS 都是有效的;对于你的项目,你可以选择你最喜欢的。在本例中,我们选择 Sass。
做出选择后,CLI 将创建 Angular 项目所需的所有文件和文件夹。您现在应该有一个名为my-app
的文件夹。进入该文件夹并启动应用类型:
cd my-app
ng serve
一旦进入应用文件夹,您就可以运行该应用。当应用完成编译时,您可以通过打开浏览器并键入localhost:4200
在浏览器中看到它。你的浏览器应该如图 11-1 所示。
图 11-1
在本地机器上运行的 Angular 应用
这是使用 CLI 创建的任何 Angular 应用的默认页面。现在,您可以开始为您认为合适的任何目的定制这个默认应用。
当启动这个应用时,使用命令ng serve
;这是特定于角度的。通过使用一系列以ng
开头的命令,您可以使用 CLI 来做一些事情,比如添加文件、运行您的测试套件,以及为生产构建最终版本。它甚至可以自己更新库和依赖项。要查看完整列表,请键入ng help
。
现在您的应用已经构建好了,让我们更好地理解它是如何组织的。
Angular 的建筑
像 Angular 和 React 这样的框架都有一个组件的概念。组件仅仅是放置应用不同部分的 HTML 和 CSS 的地方。
这方面的一个例子是标题。页眉可以有公司徽标和登录按钮。一旦一个人被认证,标题应该显示一个注销按钮。所有这些特性都应该存在于 header 组件中,应用逻辑应该决定什么应该可见以及何时可见。
Angular 还有一个概念叫做模块。Angular 中的模块与 JavaScript 模块不同。在这种情况下,Angular 中的模块为您提供了放置某个函数的所有相关代码的地方。你可以用页眉作为例子。头文件模块包括使头文件在应用中正常工作所需的所有文件。
所有角度应用都有一个根模块;这个模块启动应用。app.module.ts
文件位于src/app
文件夹中。它是启动应用的模块。
在上一章中,您使用 Node 创建了一个 API。您现在可以创建一个模块,它将使您能够从 Node 发出请求并显示结果。
在命令行中,您将创建一个模块,然后可以在其上进行构建。这个模块将很快让你有一个组件来保存你所有的显示信息。它还将利用表单和服务来连接到您的 API。
您的第一个练习是创建一个带有按钮的组件,该组件将向节点服务器发出请求,并在 Angular 应用中显示结果。
要创建一个模块,在命令行中键入ng g module boroughs
。这段代码创建了一个新的模块,您可以在其上进行构建。
一旦命令被执行,你需要得到更大的角度应用,以意识到这个模块现在是可用的。为此,打开app.module.ts
文件。在这里,您将模块导入到主应用模块中。
在app.module.ts
文件中是一个imports
数组。这个数组允许您将新模块添加到主应用中。如果你正在使用一个类似 Visual Studio 代码的应用,你可以开始直接将你的模块添加到imports
数组中。Visual Studio 和其他开发应用一样,会知道你想把这个添加到应用中,并自动在文档顶部添加import
语句。
app.module.ts
文件的导入部分现在应该看起来像清单 11-2 。
imports: [
BrowserModule,
AppRoutingModule,
BoroughsModule //this is the new module
],
Listing 11-2Adding a New Module to the app.module.ts File
既然主应用知道了您的定制模块,那么您需要向该模块添加一个组件。我提到过组件是应用的视图层。这是您与应用交互并查看来自数据库的结果的方式。
要创建一个将成为行政区模块一部分的组件,您需要确保文件是在您的boroughs
文件夹中生成的。在命令行中,键入
ng g component boroughs/list-boroughs
这将在boroughs
文件夹中创建创建该组件所需的所有文件。它还更新了boroughs.module.ts
文件,让该模块知道它现在可以访问组件了。
使用 CLI 创建组件时,declarations
数组会更新。declarations
数组跟踪这个模块中使用的所有组件。
为了让您的组件在浏览器中可见,您需要创建一个exports
数组并将该组件添加到其中。见清单 11-3 。
@NgModule({
declarations: [ListBoroughsComponent],
imports: [
CommonModule
],
exports: [
ListBoroughsComponent
]
})
Listing 11-3The Updated boroughs Module
现在组件可以导出了,您可以在应用中将它作为 HTML 元素使用。在list-boroughs.component.ts
文件中有一个叫做选择器的部分。选择器是你定制的 HTML 元素的名字。
您可以将该元素添加到您的 HTML 页面中,就像它是任何其他本机 HTML 元素一样。Angular 编译器将确保该元素在浏览器中正确呈现。
要查看这一过程,请打开app.component.html
。这里是您在主页上看到的所有信息。您将删除它并添加新的自定义 HTML 元素。最后,app.component.html
应该看起来像清单 11-4 。
<!--The content below is only a placeholder and can be replaced.-->
<div style="text-align:center">
<app-list-boroughs></app-list-boroughs>
</div>
<router-outlet></router-outlet>
Listing 11-4Adding the app-list-boroughs Component to the app.component.html File
一旦这个文件被更新,你应该看到应用编译所有的代码,并在浏览器中显示一个新的结果。页面应该如图 11-2 所示。
图 11-2
Chrome 渲染更新后的 app.component.html 文件
现在,您已经拥有了由 Angular 渲染的自定义组件。在下一节中,您将创建一个调用节点应用并在页面中显示结果的服务。
创建角度服务
至此,您已经使用 Angular CLI 创建了一个应用并开发了一个定制组件。所有这些都可以在本地机器的浏览器中看到,而不需要上传到远程服务器。
组件用于显示信息和处理用户交互。组件不应该被用来做像从数据库获取数据这样的事情。
在 Angular 中,您使用服务来执行像从数据库获取数据这样的操作。服务可以注入到任何组件中。这提供了为特定动作编写一个服务并在不同的组件中重用它的能力。您将使用 CLI 来创建服务。您的服务将首先连接到一个远程服务器作为测试。
在您知道它正在工作之后,您将连接到运行您在上一章中创建的节点应用的本地机器。
您将使用 CLI 来创建服务。该服务将位于一个名为service/borough
的文件夹中。文件本身将被称为borough.service
。
在命令行中,键入
ng generate service service/borough/borough
这将把service
文件夹添加到src/app
文件夹中。然后它添加了一个boroughs
文件夹;在该文件夹中,它创建了一个borough.service.ts
文件。
创建好服务后,您现在可以让您的定制组件访问它;之后,您可以配置您的组件,以便它可以调用远程服务。
我讨论了服务必须在多个组件中可用的能力。这是因为服务可以被添加或“注入”到多个组件中。让我们将您的服务添加到list-boroughts.component.ts
文件中。
打开文件并导入服务。导入服务后,您可以通过在组件内部使用constructor
方法来创建这个类的实例。JavaScript 和 TypeScript 都有类的概念。当创建一个类时,你定义了一个对象类型应该如何工作的“蓝图”。
使用该蓝图,您可以根据自己的需要创建尽可能多的对象。创建类时,首先声明类及其名称。在该声明中,您定义了constructor
函数。这个函数用于创建或初始化一个类中的对象。
在清单 11-5 中,您首先导入服务。然后使用constructor
函数创建一个对象。
import { Component, OnInit } from '@angular/core';
import { BoroughService } from '../../service/borough/borough.service';
@Component ({
selector:'app-list-boroughs;,
templateUrl:'./list-boroughs.component.html',
styleUrls: ['./list-boroughs.component.sass']
})
export class ListBoroughsComponent implements OnInit{
constructor (private boroughService: BoroughService){}
ngOnInit(){
this.boroughService.getBoroughs().subscribe((data) => {
console.log(data);
}):
}
}
Listing 11-5Importing the BoroughService into the ListBoroughsComponent
这段代码中有很多东西需要解开。我还没有讨论组件是如何工作的。您只是创建了它们,并添加了在屏幕上显示它们的功能。现在您需要添加一个服务,让我们详细了解一下组件是如何工作的,以及这个示例中发生了什么。
组件表示角度应用的可见部分。组件类有一种叫做生命周期挂钩的东西。这些钩子由 Angular 管理,它们让你知道在给定的时间内任何组件发生了什么。
比如你这里用的生命周期钩子是OnInit
。当在一个类中使用时,生命周期挂钩以ng
开始,所以您将使用的函数是ngOnInit
函数。
第一行导入使角度组件工作所需的两个项目。首先是组件装饰器。你可以在这个例子中看到。@Component
decorator 是第一个使它不同于常规 TypeScript 或 ES6 类的东西。它让 Angular 编译器知道这个类是 Angular 框架的一部分。
在这个装饰器中,您创建了一个对象,该对象描述了视觉细节以及它将如何在应用中实现。
从上到下,@Component
首先描述选择器。这是您在另一个示例中使用的自定义 HTML 标记,用于使该组件在文档中可见。
这个对象中的第二个元素是templateUrl
。它指向这个组件将要使用的 HTML 文件。
最后一节是styleUrls
。它指向一个样式表数组,其中一个文件设置为默认值。
使用@Component
装饰器,你现在可以添加所有的元数据(你将在文档中看到的一个术语),描述这个组件如何在更大的应用中工作。
回到文件的第一行,导入代码的第二件事是一个名为OnInt
的生命周期事件。您可以在类声明中看到这一点。你的类implements OnInit
作为类的一部分。
TypeScript 中的这个关键字implements
很重要。你告诉编译器这个类将在类内部使用方法OnInit
。如果你告诉这个类你将要实现某个东西,然后你没有在类中使用它,你会得到一个错误。
在你声明了你的类之后,你有了一个constructor
函数。这些函数在 JavaScript 和 TypeScript 中都存在。Constructor
函数在一个类中只能使用一次。在 TypeScript 中,你可以在constructor
函数中创建一个对象。在这个实例中,您创建了一个私有对象,它的类型是BoroughService
。
当你声明某个东西private
,时,意味着其他对象不能从外部访问这个对象的任何方法或属性。有几个关键字可用于控制其他对象对您正在使用的对象的方法和属性的访问级别;现在,你将和private
呆在一起。
使用完你的constructor
函数,就可以使用生命周期法ngOnInit
。当组件初始化时,constructor
创建对boroughService
对象的访问,并运行getBoroughs
方法。
您使用subscribe
方法告诉 Angular 您将检索从服务返回的任何结果。您将结果分配给一个名为data
的变量,并运行一个函数,将结果显示在浏览器的控制台窗口中。
现在,您引用了作为服务一部分的函数。getBoroughs
函数应该返回一个结果。此时,您尚未更新服务。根据您用来编写代码的环境,您可以使用 Control 键或 Command 键(如果您使用的是 MacOS)并单击boroughService
。
这将打开该文件,并允许您直接访问该服务。您可以在这里更新您的服务。下一节将展示如何添加调用外部资源并将数据传递回组件的能力。
更新您的角度服务
最后一节展示了如何使用 CLI 创建服务并在组件中使用它。本节将向您展示如何让这个服务访问外部资源,并将结果发送回组件进行显示。
如果您使用 CLI 创建文件,请确保该文件已打开。您只需在这个文件中添加一些项,就可以调用远程 API 了。当您完成时,服务应该看起来如清单 11-6 所示。
import { Injectiable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Injectable ({
providedIn:root
})
export class BoroughService {
constructor (private http: HttpClient){}
getBoroughs() `Observable<any>{
retun this.http.get('https://jsonplaceholder.typicode.com/todos/1')
}
}
Listing 11-6The BoroughService Class
在大多数情况下,您将要使用的格式与您创建组件时的格式相同。您将其他类导入到您的类中。有一个constructor
函数用于创建一个对象,您向该类添加一个公共方法,该方法将供组件将来使用。
服务中使用的类似于组件的东西之一是@Injectable
装饰器。这个装饰器使你的类能够被添加或被注入多个组件中。利用该功能,您可以编写一个服务并重用它。
装饰器里面是线providedIn: 'root'
。这优化了 Angular 使用所谓的依赖注入的能力。通过将服务注入到应用的根中,Angular 可以使用该服务的一个实例,并将该服务从不使用它的组件中删除。您还可以指定希望服务在哪个模块中可用。一旦加载了该模块,就可以使用该服务了。在每种情况下,您都不需要将服务添加到模块中的提供者阵列。
您需要添加到服务中的第二行是HttpClient
。这允许您对任何远程服务进行 HTTP 调用。因为您正在向服务器发出请求,所以您将使用 REST 动词GET
。这将让服务器知道您只想检索一个响应,而不想添加或更新数据库中的任何信息。
该方法内部是您想要从中获取信息的 URL。在这种情况下,您使用的是一个名为JSONPlaceHolder
的站点。这个站点将返回一些 JSON,让您知道一个请求检索到了一个结果。
getBoroughs
方法中的第一行告诉 TypeScript 它需要将这一行返回给调用该方法的类,在您的例子中是ListBoroughsComponent
类。
现在您已经设置了服务,您需要在应用中再添加一个,这样您就可以使用您的HttpClient
类了。在app.module.ts
文件中,你需要添加一行
import the HttpClientModule from '@angular/common/http'
此外,将HttpClientModule
添加到imports
数组中。
现在,您有了一个可以注入到组件中的服务。当组件初始化时,它将在服务上运行getBoroughs
方法,并在浏览器的控制台中打印结果。当调用boroughService
上的方法时,它将向JSONPlaceHolder
发出一个GET
请求并返回结果。
当您运行 Angular 应用时,浏览器窗口看起来应该是一样的;然而,当您查看控制台日志时,您应该会看到一个包含 REST 调用结果的对象。见图 11-3 。
图 11-3
来自市镇服务的 REST 调用的结果
既然您已经验证了有从 web 服务返回的结果,那么您可以将这些结果显示在屏幕上。
现在,您将对组件进行小的更新。通过这些更新,您可以在页面加载时在页面上显示 REST 调用的结果。
打开组件。在这里,您将为您的类创建一个属性。当在浏览器中显示值时,模板将读取该属性。
您将添加一个名为results
的属性。然后,您将把从服务返回的数据分配给这个属性。
代码应该如下所示:
private results: any;
constructor (private boroughService: BouroughService) {
}
ngOnInit() {
this.boroughService.getBroroughs().subscribe((data) => {
cosole.log(data);
this.results = data;
});
}
现在,您的函数能够将结果分配给类中的一个属性,您可以在屏幕上显示结果。
打开list-boroughs-components.html
。在这里,您可以添加组成组件的所有 HTML。默认情况下,有一个包含一些文本的段落。您将在其下创建三个新的div
元素来显示 REST 调用的结果。更新后的页面应该如下所示:
<p>
list-boroughs works!
</p>
<div> results.title {{results.title}} </div>
<div> results.Id {{results.userId}} </div>
<div> results.id {{results.id}} </div>
如果您的应用当前正在运行,您的浏览器窗口应该如图 11-4 所示。
图 11-4
在浏览器中显示 REST 调用的结果
你现在可以在屏幕上看到结果。这个例子让你使用一个占位符来获取结果并显示在屏幕上。在下一节中,您将利用您创建的节点 API。因为您的节点应用和角度应用都运行在同一台机器上,所以您需要开发一种方法让它们相互通信。
为本地角度应用创建代理
在上一章中,您创建了一个带有 Node 的 API。您的 API 从 MySQL 服务器发出请求,并将结果返回给浏览器。
本节将介绍如何让您的 Angular 应用向您的节点服务器发出相同的请求,即使它们运行在同一台计算机上。
从 Angular 应用的角度来看,它不知道它正在与一个单独的服务器进行对话。它将每个调用视为与本地服务器对话。
设置代理的好处之一是,在开发应用时,您可以连接到本地服务器(如本例所示)或远程服务器。代理文件将确保您指向正确的位置。
Angular 和 React 都依赖 Webpack 来完成诸如将 TypeScript 转换成 JavaScript 和运行本地开发服务器之类的事情。Webpack 还能够让您添加一个文件,这样您就可以解决这个问题。
在package.json
文件的同一层上,创建一个名为proxy.conf.json
的新文件。这个文件告诉 Angular 当它试图调用一个 REST 服务时应该去哪里查看。呼叫将被重新路由到您在此文件中指定的服务器。
一旦创建了这个文件,添加一个 JSON 对象,该对象指向您想要使用的端点、服务器的 URL,以及这是否是一个安全调用。您的代码应该类似于清单 11-7 。
{
"/boroughts/*":{
"target": http://localhost:3000/boroughs",
"secure": false,
"loglevel": "debug",
"changeOrigin": true,
"pathRewrite": {"^/boroughs": ""}
}
}
Listing 11-7The Body of the proxy.confg.json File
有了这个文件,您现在需要告诉 Angular 应用它需要在开发中使用这个文件。这样,当您在本地机器上测试您的 web 服务时,您可以在 Angular 应用中获得结果。
为此,您需要打开angular.json
来更新应用在开发模式下的运行方式。你可以搜索“服务”这个名字在options
对象中,可以在browserTarget
属性下添加一个产权。这一行应该是这样的:
"proxyConfig": "proxy.conf.json"
记住保存文件,如果需要,重新启动应用。
在高层次上,您已经使应用知道,当服务调用/boroughs
端点时,使用代理并将调用重新路由到具有相同端点的节点服务器。
重要的是要记住,即使你的 Angular 应用和 Node 应用运行在同一台机器上,它们也是两个独立的应用。
Angular 应用使用自己的服务器在开发人员的机器上显示站点。Node 应用也运行在自己的服务器上。这两个应用不知道对方正在运行。通过使用代理,您能够在这两个服务器之间架起一座桥梁,并将结果从一个应用返回到另一个应用。
此示例适用于本地开发,您可以将 Angular 应用指向本地服务器,甚至远程服务器来检索您的数据;当您将应用部署到生产环境中时,情况就不同了。有关如何将您的应用部署到生产环境中的更多信息,请查看位于 https://angular.io/guide/deployment
的 Angular 文档的部署部分。
现在,代理使您能够连接两个服务器,您可以更新组件中的结果,并显示从节点服务器返回的信息。
在服务中,将您的GET
方法的 URL 更改为/boroughs
。无需做任何更改,现在您可以在浏览器中查看控制台,并看到节点 API 发送回一个对象数组作为结果。在您的例子中,数组中只有一个对象。见图 11-5 。
图 11-5
浏览器控制台中显示的节点 API 的结果
来自节点服务器的结果为您提供了一组要处理的对象。在这种情况下,因为只有一个结果返回,所以很容易将第一个元素(元素 0)的值赋给变量,然后显示该对象的属性。
虽然这可行,但它不可扩展。如果要在数据库中添加更多的项目,该实例中的代码将不会考虑所有需要显示的新项目。您可以做的是分配一个循环来显示所有可用的结果。
在 HTML 文件中为你的组件(list-boroughs-component.html
) ,你需要更新内容。在这里,您将从角度查看您的数组,并遍历结果。
对于这个例子,您将使用*ngFor
指令来循环处理结果,并在屏幕上显示它们。当您更新文档时,页面应该类似于清单 11-8 。
<div *ngFor="let result of results">
Id = {{ result.id }}
</div>
<div *ngFor="let result of results">
name = {{ result.name }}
</div>
<div *ngFor="let result of results">
state = {{ result.state }}
</div>
Listing 11-8Updated list-boroughs-component.html Using the *ngFor Directive
这里有三个实例将为这个对象的每个属性生成div
标签。浏览器中的结果应该如图 11-6 所示。
图 11-6
使用*ngFor 指令时浏览器中的结果
现在您有了一个 Angular 应用,它可以使用服务从 web 服务请求信息。在开发应用时,您可以使用代理文件将请求重定向到能够满足请求的节点服务器。
当结果返回到组件时,您会收到一个对象数组。然后使用*ngFor
指令显示结果列表。
既然您已经能够连接到您的服务,那么您应该做一些事情。第一件事是制作一些视觉效果。您可以添加 Twitter Bootstrap 作为可视化框架,以帮助您的应用看起来更好。
您要做的最后一件事是创建一个允许您在数据库中创建新信息的表单。这将需要对节点应用和角度应用进行一些更新。
首先,让我们使用 Twitter Bootstrap 添加一些样式。下一节将展示如何将这个可视化框架添加到 Angular 应用中。
将 Twitter 引导添加到 Angular 应用中
Twitter 创建了一个开源框架,让你为你的网站创建一致的视觉效果。Bootstrap 使您能够开发一个具有一致的排版、表单和按钮的站点。
本节将展示如何将引导程序添加到您的应用中。您还将创建一个表单元素,该元素将使用 Bootstrap 来赋予它样式。为了更好地了解 Bootstrap 以及您可以用它做什么,请访问主页 https://getbootstrap.com/
。
为了让您的 Angular 应用具有 Bootstrap 所能提供的视觉一致性,您需要将它安装到应用中。
在命令行中,使用 NPM 安装 Bootstrap、jQuery 和 Popper。在应用的基础上,在命令行中键入以下内容:
npm install bootstrap jquery popper
安装完成后,打开angular.json
文件。样式和测试部分需要更新,以引用 Bootstrap 提供的 CSS 文件。这可以在node_modules
文件夹中找到。在脚本部分,添加引导节点模块的路径。样式和脚本部分应该如下所示:
"styles":{
"src/styles.scss",
"node_modules/bootstrap/dist/css/bootstrap.min.css"
}
"scripts":{
"node_modules/jquery/dist/jquery.min.js",
"node_modules/bootstrap/dist/js/bootstrap.min.js"
}
重新启动应用,你应该会看到字体的变化。为了确保这是可行的,将其中一个div
标签改为一个h1
标签。当应用重启时,你的页面应该如图 11-7 所示。
图 11-7
在组件中使用 Bootstrap 的 H1 标签
现在,Bootstrap 已经成为应用的一部分。您可以利用 Bootstrap 提供的布局和格式化功能。
在下一节中,您将创建一个表单。该表单将是您在数据库中添加新信息的方式。
在 Angular 中创建一个简单的表单,并用 Bootstrap 对其进行样式化
你在这一章中取得了很大的成就。您创建了自己的应用,开发了 UI 组件,并创建了一个调用节点服务器的服务。进行调用时,Node 会将结果发送回 Angular 应用,您可以在屏幕上显示结果。
现在,您将向该应用添加表单。Angular 有两种不同的表单处理方式:反应式表单和模板驱动表单。这个例子将使用模板驱动的表单。
当您赋予应用调用远程服务器的能力时,您必须将HttpClientModule
添加到app.module.ts
文件中。这增强了整个应用的功能。对于这个例子,您需要对表单做同样的事情。
重新打开app.module.ts
文件,从@angular/forms
导入FormsModule
。将imports
语句与其他语句一起添加到文档的顶部。
Import { FormsModule } from '@angular/forms';
还将模块添加到imports
数组中。一旦完成,在boroughs.module.ts
文件中做同样的事情。
现在,您可以返回组件,使用普通的 HTML 5 语法添加表单。你的表格会很简单。您希望有人在两个字段中输入文本,name
和state
。你将使用 Bootstrap 给这个表单添加一些形状。见清单 11-9 。
<div class="container-fluid">
<form #boroughForm="ngForm" (ngSubmit)="submitForm(boroughForm.form);">
<div class="row">
<div class="col">
<label for="boroughName">
Borough Name:
<input type="text" [(ngModel)] = "model.boroughName" id="boroughName" name="boroughName">
</label>
</div>
<div class="col">
<label for="state">
State:
<input types="text" [(ngModel)] = "model.state" id="state" name="state">
</label>
</div>
<div class="col">
<button class="btn btn-primary" type="submit">Submit</button>
</div>
</div>
</form>
</div>
Listing 11-9Updating list-borough-component.html with a HTML 5 Form and Using the ngModel Directive
这是您表单的模板。您正在使用 Bootstrap 创建行和列,就像网格一样。你也可以使用 Bootstrap 的 CSS 类来赋予你的 Submit 按钮一些风格。
在form
标签中,使用 Angular 语法创建一个名为boroughForm
的变量;在那之后,你给事件onSubmit
?? 分配一个函数。
当点击提交按钮时,该事件被触发并调用函数submitBorough
。当这个函数被调用时,你传递你的变量对象和form
属性。您的函数在组件内部被解析。
您还有一个使用角度指令ngModel
的表单。它将模板绑定到组件内部的一个对象。这个对象有两个属性,boroughName
和state
??。
现在创建一个具有这两种属性的类。然后,您可以将代码连接到模板。
在命令行中,创建一个类。这个类有两个与模板匹配的属性。类型
ng generate class boroughs/model/borough
这将进入boroughs
文件夹,创建一个名为models
的文件夹,并生成一个名为borough.ts
的文件。在这个类中,你需要添加两个公共属性。
打开文件。在构造函数之前,添加两个公共属性。它应该类似于清单 11-10 。
export class Borough {
public boroughName:string;
public state:string;
constructor (){
}
}
Listing 11-10Creating a borough Class
这个类为您提供了一个保存输入到表单中的值的地方。接下来,将这个类导入到list-boroughs.component.ts
文件中,就像其他类一样,然后创建一个名为model
的类实例来匹配模板。
下面是代码的一部分:
import { Borough } from ../model/borough;
private model: Borough = new Borough();
constructor (private boroughService: BoroughService) {
}
submitBorough(value) {
console,log(value);
}
您的类现在应该已经导入了新创建的borough
类。如果您单击 Submit 按钮,结果应该会显示在浏览器内的控制台中。通过使用ngModel
,您可以将表单的值直接绑定到对象,而无需编写任何额外的代码。然后结果变成控制台中的一个对象,该对象应该包含表单中的值。
有很多种函数可以添加到表单中。可以添加验证和用户反馈,以确保在提交给服务器之前,有人在字段中键入了某些内容,以及任何符合您需要的格式的内容。
在这种情况下,您只想确保从表单中捕获数据,并让 Angular 控制它。在下一节中,您将把数据传递回本地节点服务器并更新数据库。
将信息从角度传递到节点
你已经准备好了你的前端。Bootstrap 为你的页面结构和表单提供了一些风格。您无需编写任何自定义 CSS 就可以获得这一点。
使用 Angular 的一些指令,您可以将 HTML 模板中的数据绑定到 Typescript 组件代码,并检测表单的 Submit 按钮何时被单击。
在前面的例子中,您创建了一个从本地 MySQL 数据库中检索数据的服务。现在,您需要在服务中创建一个新的函数,将信息发送回节点。然后,服务器将提取数据并将其插入数据库。
您需要向您的 Express 应用添加一个库,以便它可以接受传入的数据。你所有的节点例子都是基于第十章所做的工作。如果这些看起来不熟悉,请参考那一章。如果您正在运行 Node,您可以停止服务器。在命令行中,添加该库:
npm install body-parser –save
当 Angular 发出 POST 请求时,这个库允许 Node 检索进入服务器的数据。
我已经介绍了 REST 动词的概念,比如使用GET
从数据库中检索信息,现在您可以使用POST
向数据库中添加新信息。
将这个库安装到您的节点应用中后,您现在可以更新您的app.js
文件来利用它。以下是更新的部分视图:
const bodyParser = require('body-parser');
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true });
您现在需要的是让服务器接受 Angular 应用发出的POST
调用。为此,从controllers
文件夹中打开boroughs.js
文件。
这里有您的原始代码,它响应了一个GET
请求。您现在可以添加响应POST
请求的能力。
在您的新函数中,您将提取发送过来的信息,并将其分配给局部变量。您还将创建一个对象来表示正在更新的列和数据。最后,您将运行 SQL 查询来更新数据库。
更新后的boroughs.js
文件应包含以下代码:
router.post('/' , (req, res) => {
let boroughName = req.body.boroughName;
let state = req.body.state;
let records = {name: boroughName, state: state};
pool.query('INSERT INTO boroughs SET ?', records, (error, results) => {
if (error) throw error;
});
});
您创建了一个允许您与节点服务器对话的代理。对于本地开发,您继续使用该代理连接到节点服务器,并传递来自 Angular 表单的数据。在节点端,提交到表单中的数据现在变成了body
对象的属性。
属性包含键入表单的值。现在,您可以提取这些值,并将它们保存为局部变量。
然后,您可以创建一个对象,该对象将把列与要插入到数据库中的值相匹配。最后一件事是运行 SQL 查询。
当您从数据库中获取数据时,其中一项是数据库中该项的 ID。通过在查询中使用INSERT
来添加新数据,数据库将自动为添加到数据库中的每个项目创建一个新的 ID 号。这很重要,因为这意味着您不需要跟踪每个项目的唯一 ID。
数据库准备好插入新数据,节点服务器准备好接收新数据,现在需要更新 Angular 应用来发送信息。打开borough.service.ts
,添加一个名为addBorough
的函数。它应该是这样的:
addBorough(value) {
return this.http.post('/boroughs', value,value);
}
就像GET
示例一样,您使用 Angular 提供的 HTTP 客户端来进行您的POST
调用。然后你把你的参数value
,它是一个对象(你可以把名字改成任何你喜欢的)。这个对象有一个名为value
的属性,这就是你发送给服务器的内容。
您需要更新您的组件,以便它可以向服务发送数据。打开list-boroughs.component.ts
。最近添加的功能是submitBorough
。您需要更新此功能,以便利用更新后的服务。该函数现在应该如下所示:
submitBorough(value) {
//console.log(value);
this,.boroughService.addBorough(value).subscribe((results) => {
console.log(results);
});
}
现在,当提交表单时,在文本字段中键入的信息将被发送到服务器。然后,服务器会在数据库中添加一条新记录。
当您测试应用时,您应该看到数据库已经更新。为了确保数据库已经更新,您可以查看 PHPMyAdmin 并查找之前创建的表。您还可以更新模板,以便获得数据库中的所有结果。
您要进行的最后一次更新是将新数据检索到同一页面中。这时,让我们把原来在ngOnInit
中的函数移到一个名为getBoroughs
的新函数中。你可以把这个电话作为一个整体,转移到你的新功能。它应该是这样的:
getBoroughs(){
this.boroughService.getBoroughs().subscribe((data) => {
this.results = data;
});
}
您有一个函数,当它被激活时,将进行相同的调用并检索数据库中的所有信息。最后,您需要更新 HTML 模板,以便在屏幕上显示所有结果。本次更新将利用上一课的内容。您将添加一个具有新按钮的新行,当该按钮被单击时,它将调用您的新函数并更新模板的后半部分。
新行应该如下所示:
<div class="row">
<div class="col-1">
<button class="btn btn-primary" (click) = 'getBoroughs()'>Get Boroughs</button>
<div>
<div class="col-11">
<div *ngFor = "let result of results">
id = {{ result.id }}
name = {{ result.name }}
state = {{ result.state }}
</div>
</div>
</div>
新行中有一些小的不同。Bootstrap 的工作原理是将屏幕分成 12 列网格。因此,您在第一列中添加按钮,结果在一列中横跨其他 11 列的宽度。
您使用与上一个示例相同的循环。当你的用户点击按钮时,Angular 会看到点击事件发生了。它将调用组件中定义的getBoroughs
函数。你应该在屏幕上看到结果,类似于图 11-8 。
图 11-8
角度插入和显示信息
摘要
这一章涵盖了很多内容。你被介绍到了角度框架。您了解了 Typescript 与 JavaScript 的细微差别,以及如何在 Angular 应用中使用它。
您使用命令行界面为您的应用创建了一个新的 Angular 应用、组件、类和服务。您还创建了一个代理,允许您的 Angular 应用与您的节点服务器对话,就好像它们运行在同一个端口上,而不是两个独立的实例。
使用 Express 框架,您找到了一种连接到 MySQL 数据库并运行查询的方法,该查询将结果发送回 Angular 应用。您还运行了一个查询,将数据从 Angular 应用插入到数据库中。
Angular 提供了一长串现成的函数,可以用来构建应用。
下一章将介绍如何使用 React 做同样的事情。React 没有 Angular 拥有的所有特性,但它是一个值得学习的技能。
十二、JavaScript 和应用框架:React
在上一章中,你能够创建一个 Angular 应用,并通过使用一个代理连接到一个可以访问 MySQL 数据库的节点服务器。通过这种设置,您能够检索数据并将其显示在屏幕上。您还更新了代码,以便可以使用 REST 谓词POST
将数据从表单发送到服务器,然后在数据库更新后查看结果。
这一章将涵盖一些相同的基础,但你将使用反应,而不是角度。这样做的目的是展示不同的框架如何以不同的方式解决问题。React 让开发人员可以自由选择他们认为能够解决他们所面临的挑战的库。
因为 React 有这个选项,所以很多开发人员喜欢自己把所有东西放在一起。
脸书创造了一种快速组装 React 应用的方法。它类似于 CLI 的角度使用。然而,重要的是要注意,这个工具只是启动一个应用,并不包含像 Angular 那样添加新文件的命令。
与 Angular 类似,如果你想在任何你喜欢的文件夹中创建一个 React 应用,你首先需要安装这个应用。
在命令行中,键入
npm install -g create-react-app
这使您能够在任何您喜欢的文件夹中创建 React 应用。
要创建一个全新的 React 应用,请在命令行中键入
npx create-react-app my-app
面试问题
npm 和 npx 有什么区别?答:npx 允许您执行 npm 注册表中的软件包,而不需要安装它们,而 npm 帮助您管理全局或本地安装在您机器上的软件包。
这个命令为一个基本的 React 应用创建所有的文件和文件夹。要启动该应用,您可以键入
cd my-app
npm start
这段代码以与 Angular 应用相同的方式启动应用。浏览器应该如图 12-1 所示。
图 12-1
使用 create-react-app 时的默认屏幕
现在,您已经在本地机器上运行了 React,您可以创建组件来调用 API 并在屏幕上显示数据。
React 将自己描述为一个高效的、声明性的 JavaScript 库,用于构建用户界面。记住这一点,您将使用 React 构建一个组件,该组件将从您的 web 服务中检索数据。
由于 React 不能在命令行创建文件,所以您必须手动创建您需要的文件。
所以创建一个名为components
的文件夹。在该文件夹中,创建另一个名为boroughs
的文件夹和一个名为boroughs.js
的文件。这是您创建 React 组件的地方。
为了确保您的新组件将由更大的应用呈现,您需要做一些更改。
在boroughs.js
文件中,您将导入确保这个 JavaScript 类将与 React 一起工作的代码。该组件看起来应该如清单 12-1 所示。
import { Component } from 'react';
export class Boroughs extends Component {
render() {
return "this is my component!"
}
}
Listing 12-1Basic React Component
React 组件的语法非常类似于 JavaScript 类。为了让这个类与 React 一起工作,首先要导入Component
类,并确保当前类扩展了它。
这个类还包含一个名为render
的方法。它返回所有需要在浏览器中显示的 HTML 代码的结果。
创建好组件后,您需要一种在屏幕上显示它的方式。要加载这个组件,您需要更新App.js
文件。
每个组件都被视为一个 HTML 标签。您可以导入您创建的类,并让 React 在应用中呈现它。
打开App.js
并导入新创建的组件。组件的名称是应用中使用的 HTML 标记。更新后的文件应该如清单 12-2 所示。
import React, { Component } from 'react';
import logo from './logo.svg';
import '.App.css';
import { Boroughs } from '../components/boroughs/boroughs';
export class App extends Component {
render() {
return(
<div className="App">
<header className="App-header">
<Boroughs/>
</header>
</div>
);
}
}
Listing 12-2
Updating App.js
既然文件已经更新,浏览器应该有更新的版本。你的浏览器应该如图 12-2 所示。
图 12-2
呈现基本组件的默认屏幕
React 不像 Angular 那样有服务的概念。但是,React 确实有生命周期方法的概念,就像 Angular 一样。
记住这一点,您将创建一个调用 API 的函数,并在浏览器控制台中显示结果。
添加代理和检索数据
就像上一章中的 Angular 应用一样,您需要一个代理将这个应用连接到单独的节点应用。
使用 Create React App 建立代理连接非常简单。在package.json
文件中,您需要添加一个代理部分。这将指向您的节点应用。
打开package.json
并添加这一行:
"proxy": "http://localhost:3001",
您可能会注意到,在其他示例中,节点应用指向端口 3000。默认情况下,React 和 Node 运行在同一个端口上,所以这将产生冲突。您必须更改节点的端口。这是对节点应用中的App.js
文件的简单更新。
第十章有一个设置 Express 服务器的例子,你可以设置节点监听的端口。如果需要如何更新节点端口的说明,请参考第十章。
打开节点应用的文件,将端口号更改为 3001。更新后的变量应该如下所示:
CONST port = 3001;
在这两种情况下,您都需要重新启动应用以确保更改生效。您可以更新组件,以便从数据库中检索结果。
React 拥有生命周期事件,可以让您了解组件的当前状态。
这里您将使用componentDidMount
功能。这将在组件呈现在屏幕上时执行。关于生命周期方法及其工作方式的更详细列表,请查看 React 官方网站上的文档( https://reactjs.org/docs/react-component.html
)。
在这个函数中,您将使用一个名为fetch
的函数。它不是 React 的一部分;它是 JavaScript 语言的一部分( https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
)。这将使您对服务器的 HTTP 调用通过您的代理路由到节点应用。它将返回所谓的 ?? 承诺。
承诺是表示某种事件完成的对象。在本例中,您正在发出一个 HTTP 请求,并期望得到一个回答。该承诺将解决要么完成该请求,在那里你得到一个结果,或失败的请求。
此示例从服务器返回数据。在then
方法中使用的函数获取结果,将它们转换成 JSON 对象,并将它们返回给fetch
。第二个then
方法获取 JSON 对象,并将其转换为要在浏览器控制台中显示的字符串。
现在您已经有了 API 调用的结果,并且知道您有查看结果的方法,本节的最后一部分将介绍如何在屏幕上显示结果。
React 有一个概念叫做状态。你可以把状态想象成应用此刻正在做的事情。您可以通过在constructor
函数中创建一个state
对象来设置组件的状态。
下一节将展示如何创建默认的state
,更新state
,并显示当前在state
对象中的值。
在 React 组件中创建、更新和显示状态
上一节展示了如何在本地机器上使用代理并检索数据。不属于 React 的fetch
方法用于向服务器发出GET
请求,并将结果返回给组件。
你现在面临的挑战是将结果显示在屏幕上。
React 中的每个组件都有一个状态概念。在constructor
函数中,你将创建一个state
对象。该对象将具有您将创建并赋值的属性。
这两个属性将被称为boroughs
和states
。每个属性将保存在一个数组中,你将在一会儿设置这个数组的值。
在这个例子中,您将获得 API 的结果,并使用内置的setState
方法来更新您创建的状态。您的组件应该看起来如清单 12-3 所示。
import React, { Component } from 'react';
export class Boroughs extends Component {
constructor(props){
super(props);
this.state = {
boroughs: [],
states:[]
}
}
componentDidMount(){
let boroughArray = [];
let stateArray = [];
fetch('/boroughs').then( (results) => {
return results.json();
}).then ( (resultJson) => {
resultJson.map( (value, index) => {
boroughArray.push(value.name);
stateArray.push(value.state);
});
this.setState({
boroughs: boroughsArray,
state: stateArray
});
});
}
createList(list){
if (list.length !== 0) {
const stateList = list.map( (value, index) => {
return (<div> {value} </div>)
});
return stateList
}
};
render() {
return(
<div>
<div>
<div>Boroughs:</div>
<div> { this.createList(this.state.boroughs)} </div>;
</div>
<div>
<div>States:</div>
<div> { this.createList(this.state.states)} </div>;
</div>
</div>
}
}
Listing 12-3boroughs.js Making an API Call and Displaying the Results on the Screen
您导入 React 和component
类的方式与使用 ES6 导入类的方式相同。React 组件具有与 ES6 类相同的格式。您扩展了component
类,为这个类提供了框架提供的额外能力。
在这个类中,您使用了constructor
函数。这个函数在组件被挂载之前被调用。您调用的第一个函数是super
方法。这样做的原因是,万一你想做的事情,如绑定一个事件处理方法到当前的类。
您还可以访问名为state
的属性。这是你让 React 跟踪变量或对象的地方。这个框架甚至可以让你知道当它被更新时以前的值是什么。
现在,您创建这些属性,并将它们分配给一个空数组,稍后您将对其进行更新。
接下来,你看看你的生命周期方法componentDidMount
。当组件被插入到 DOM 树中时,就会执行这个操作。这种方法是开始处理任何类型的外部数据的好地方。
您创建两个本地数组,然后使用fetch
方法。它会让你打电话到远程服务器。由于您的代理,对/boroughs
的调用将被重定向到您的节点服务器,就像在 Angular 应用中一样。
fetch
方法允许您链接一个then
方法,在这里您可以调用一个函数来处理 API 调用的结果。在该函数中,您获取结果并将其作为 JSON 对象返回。随着一个新对象的返回,你链接第二个then
方法并调用另一个函数。该函数用于使用map
方法循环数据。当调用map
中的一个函数时,你遍历每个值,并根据名字或状态将它们添加到本地数组中。
一旦循环结束,您就可以获取两个数组并更新应用的状态。使用内置的setState
方法,将数组分配给state
属性。
我们暂且跳过下一个函数,来说说render
函数。这里是 React 开始处理要在屏幕上显示的 HTML 的地方。花括号让 React 接受变量,并将它们显示为有效的 HTML 标记。从这里,您调用createList
方法并传递来自您的state
对象的每个属性。
这就把你带到了createList
方法。它接受一个名为list
的参数,这实际上是 React 应用中boroughs
或states
状态的替代。
该方法做的第一件事是确保传递的数组长度不为零,这意味着它不能是空数组。确定之后,使用map
遍历数组并返回一个包含数组中值的 HTML div
元素列表。这个列表被返回给render
方法以显示结果。
在render
方法中调用的函数现在将在浏览器中生成 HTML。见图 12-3 。
图 12-3
从节点 API 在屏幕上显示结果
现在 React 已经在屏幕上显示了您的结果,您可以使用 CSS 来显示它们。在下一节中,您将添加 Bootstrap 并更新应用的布局。
添加 Bootstrap 以进行反应
就像 Angular 一样,有几种方法可以将 Bootstrap 添加到 React 应用中。添加 Bootstrap 的方法是使用一个名为 reactstrap 的项目。
在 install reactstrap 中,您需要返回到应用底部的命令行,并使用 NPM 来安装库。在命令行中,键入
npm install bootstrap –save
npm install –save reactstrap react react-dom
这将在您的应用中安装 Twitter Bootstrap 和 reactstrap。现在,您可以使用相同的 CSS 类来布局应用。为了让您的应用利用作为应用一部分的 Bootstrap,将其导入到index.js
文件中,如下所示:
import 'bootstrap/dist/css/bootstrap.min.css'
现在,无需更改任何 JavaScript,您就可以更新 React 应用的 HTML 部分。参见清单 12-4 。
<div className='container-fluid'>
<div className="row">
<div className="col">Boroughs:</div>
<div className='col>States:</div>
</div>
<div className="row">
<div className="col"> { this.createList(this.state.boroughs)} </div>;
<div className="col"> { this.createList(this.state.states)} </div>;
</div>
</div>
Listing 12-4Using reactstrap to Lay Out the Content of Your Component
如果您再次启动应用,屏幕应该如图 12-4 所示。
图 12-4
使用 reactstrap 获得与 Bootstrap 相同的 CSS 类
您的应用现在拥有从数据库返回的信息,并使用 Bootstrap 进行格式化。本章的最后一节将介绍如何创建一个表单,并通过 web 服务将信息发送到数据库。
从 React 应用发布数据
您可以从数据库中检索数据,并使用 Bootstrap 将结果格式化为类似表格的布局。现在,您将通过添加一个 HTML 表单来更改这种布局,这样您就可以向数据库中提交新数据,并且在向数据库中添加新信息后,有一个单独的按钮来检索所有结果。
您在前面的示例中使用的一些函数在本练习中仍然有效。但是,该组件将负责调用服务器,然后在浏览器中呈现结果。
现在您已经将 reactstrap 添加到项目中,您可以使用它来使您的表单成形。通过从 reactstrap 库中导入组件,它们将使用 Bootstrap 提供的格式在浏览器中呈现。
你将从把组件作为一个整体来看开始,然后分解所有的单个部分。参见清单 12-5 。
import React, { Component } from 'react';
import { Container, Col, Form, FormGroup, Label, Input, Button } from 'reactstrap';
import './css/boroughs.css';
export class Boroughs extends Component {
constructor(props)} {
super(props);
this.state = {
boroughs: [],
states: [],
boroughInput: '',
stateInput: ''
}
};
componentDidMount(){}
createList = (list) => {
if(list.length !== 0) {
const itemList = list.map( (value, index ) => {
return (<div> {value} </div>)
});
return itemList;
}
}
onSubmitForm = (e) => {
e,prventDefault();
fetch( '/boroughs', {method: "POST",
headers:{'Accept': 'application/json', 'Content-type': 'application/json'},
body: JSON.stringify({boroughName: this.state.boroughInput, state: this.state.stateInput}) }).then((result) => {
console.log(result);
});
} )
}
onBoroughsUpdate = (event) => {
this.setState({boroughInput: event.target.value});
}
onStatusUpdate = (event) => {
this.setState({stateInput: event.target.valiue});
}
showResults = () => {
let boroughArray = [];
let stateArray = [];
fetch('/boroughs').then( (results) => {
return results.json();
}).then( (resultJson) => {
resultJson.map( (value, index ) => {
boroughArray.push(value.name);
stateArray.push(vaue.state);
});
this.setState({
boroughs: this.createList(broughsArray),
states: this.createList(stateArray)
});
});
}
render(){
return(
<Container className="borough-container">
<Form className="form" onSubmit={this.onSubmitform}>
<Col>
<FormGroup>
<Label>Add Name</Label>
<Input type="text" value={this.state.boroughInput} onChange={this.onBoroughUpdate) onBlur={this.onBoroughUpdate}/>
</FormGroup>
</Col>
<Col>
<FormGroup>
<Label>Add State</Label>
<Input type="text" value={this.state.stateInput} onChange={this.onStateUpdte}
onBlur={this.onStateUpdate}/>
</FormGroup>
</Col>
<Button>Submit</Button>
</Form>
<br/>
<div>
<div className="row">
<div className="col">
<button className="btn btn-primary" onClick={this.showResults}>Show Results</button>
</div>
</div>
<div className="row resultsPadding">
<div className="col">
Boroughs:
</div>
<div className="col">
{this.state.boroughs}
</div>
<div className="col">
States:
</div>
<div className="col">
{this.state.states}
</div>
</div>
</div>
</Container>
)
}
}
Listing 12-5boroughs.js Using reactstrap for Formatting Both Sending and Receiving Data from a Database
这个类做了很多工作。好好看看并理解它正在做的一切是有帮助的。让我们从顶部开始,进入所有的细节。
在最顶层,您导入帮助组成您的界面的组件。您以前遇到过 React 组件。其他成分来自反应堆。这些组件使您可以构建看起来相同的表单、列表和按钮,就像您手动为它们分配了引导 CSS 类一样。
第三行允许您导入将在该组件内部使用的 CSS。
在constructor
函数中,你为你的state
对象创建一些属性。前两个是数组,后两个是字符串。
您可能会注意到,在本例中没有使用生命周期事件。在这种情况下,请将它们留空。
createList
函数的工作方式与上一个例子完全相同。它根据传递给它的数组生成一个div
元素列表。有一点需要注意,因为您正在动态创建新的div
元素:React 有一个名为key
的属性。这就像给 React 渲染的元素一个唯一的 id。更多信息,看一下官方文档( https://reactjs.org/docs/lists-and-keys.html
)。
onSubmitForm
函数首先通过preventDefault
函数阻止浏览器刷新。然后,它将输入到表单域中的值发送到数据库。
使用fetch
方法,首先定义端点,然后创建一个对象,通知fetch
应该如何将数据传递给服务器。
该对象包含一些属性。第一个是您希望fetch
进行的 REST 跟注类型。在这种情况下,您正在创建一个POST
,以便更新数据库。第二个属性定义了将要发送到服务器的头。这让服务器知道您将向它发送一些 JSON 数据。
第三个属性是数据本身。它作为发送到服务器的消息正文的一部分发送出去。
数据将由服务器拾取,而不需要对角度示例进行任何更改,并将向数据库添加新记录。
功能onBoroughsUpdate
和onStatesUpdate
执行相同类型的动作。在这两种情况下,当有人在输入域中输入时,结果被保存在state
对象中。当用户点击文本字段之外的内容时,就会执行onBlur
事件。它将获取文本字段的当前值,并将其分配给state
对象。
showResults
函数的执行方式与之前的componentDidMount
函数相同。这里唯一的区别是,当使用setState
方法时,你分配createList
方法的结果。
一旦你使用了render
方法,当浏览器使你的 UI 可见时,你就进入了你看到的大多数 HTML 元素。例如,您可以看到按钮响应类似于onClick
的事件,并调用组件中定义的函数。
当运行应用和服务器时,如果您向数据库中添加新的内容,您的屏幕应该看起来如图 12-5 所示。
图 12-5
使用 reactstrap 构建表单并从数据库中检索结果
向 React 应用添加强类型
React 不像 Agular 那样使用 TypeScript 强制强类型。您可以开发一个 React 应用,并且永远不需要在 JavaScript 中强制类型。
然而,对于大型应用来说,类型安全和 JavaScript 强制数据类型可能会有所帮助。
在 React 应用中使用 Flow ( https://flow.org/en/
)是可选的,但是它使您能够让编译器检查数据类型,就像 TypeScript 或 Java 等其他语言强制类型一样。同样需要注意的是,如果你喜欢的话,可以使用 React with TypeScript(https://facebook.github.io/create-react-app/docs/adding-typescript
)。
在前面使用 React 的例子中,您使用了create-react-app
来构建您的应用。记住这一点,您可以快速地向您的应用添加流。在命令行中,键入
npm install –-save-dev flow-bin
如果您使用纱线,请键入
yarn add flow-bin
安装后,将这一行添加到package.json
文件的脚本部分:
"scripts":{
"flow":flow
}
现在在命令行中,通过键入以下命令创建一个名为.flowconfig
的文件
npm run flow init
如果您使用纱线,请键入
yarn flow init
安装好所有东西后,你现在可以添加//@flow
或/* @flow */
到你希望编译器开始检查类型的文件中。要检查您的文件并了解您需要在命令行添加类型的位置,您可以键入
npm run flow check
这将寻找每个添加了//@Flow
的文件。命令行将显示每个文件以及类型的预期位置。
您现在需要了解如何在代码中添加类型。在下一节中,您将看到如何做到这一点。
向 React 代码添加类型
每个 React 组件都有自己的内部状态。state
对象跟踪您希望组件一直知道的值。分配给state
对象的所有属性都可以被分配强类型。您可以在定义类之前定义这些类型。它应该是这样的:
type State = {
boroughs: Array<string>,
states: Array<string>,
boroughInput: string,
stateInput: string
}
此示例定义了数据类型。您学习了数组是如何工作的;在这里,您定义了数据类型将是一个数组,但是您也定义了在该数组中使用什么类型的数据。在这种情况下,boroughs
和states
都将保存一个数组。这里的主要区别是,它将是一个以字符串为值的数组。
如果您向该数组添加不同的数据类型,编译器将会抛出一个错误,因为这里您已经确切地告诉了它将会发生什么。
另外两个属性表示用于向数据库添加新数据的文本字段。在这种情况下,它们都被类型化为字符串。
React 还允许您以同样的方式设置属性的数据类型。一旦设置了类型,您就可以将它们应用到组件:
export class Boroughs extends Component<Props, State>{ }
现在如果你把一个数字赋给一个已经被赋了字符串的东西,编译器会抛出一个错误。
使用函数时,语法就像 TypeScript 一样。参数可以设置类型,因此当您将数据传递给函数时,它们必须是该类型。这里有一个例子:
function doMath(num1: number, num2: number){
return num1 + num2;
}
doMath(5,2);
您还可以为变量分配类型:
let username: string = "Hack One";
const accountNumbber: number = 11220;
var currentTime: string = "Time to make the donuts";
虽然这只是一个概述,但它让您深入了解了如何确保在处理数据时使用正确的类型。Flow 是可选的,但是很容易将它一次一个文件地添加到现有代码中,或者在开发新项目时对每个文件添加类型检查。
摘要
本章探讨了 React 库的一些基础知识。虽然您可以自己将所有部分组合在一起,但 React 有一个命令行界面,可以让您快速地将应用组合在一起。
React 和 Angular share 的一个想法是生命周期事件的概念。这些事件内置于框架中,让您知道组件当前处于哪个阶段。
第一个例子使用了componentDidMount
事件。当这个事件被触发时,它调用数据库。
另一个例子引入了 Bootstrap 进行布局,并能够设计按钮和表单等样式。
最后一个例子使用了一个向数据库发送POST
并添加新信息的表单。您还添加了一个按钮,该按钮发出一个GET
请求并显示来自数据库的所有更新信息。
虽然 Angular 和 React 之间有一些差异,但围绕生命周期事件等概念也有一些非常相似的想法。
Flow 类似于 TypeScript,因为您可以在 JavaScript 应用中实施类型安全。虽然 TypeScript 作为一种语言是 JavaScript 的超集,但 Flow 允许您使用当前的 JavaScript 并对其应用类型。
使用 Flow 的一个好处是可以在编译时发现错误。此外,您可以确保您的函数接收和返回正确类型的数据,以便它们能够正确执行。一个很好的例子就是当你想要更新一个字符串或者计算一个值的时候。
通过利用框架和 JavaScript 知识,您已经能够在本地计算机上开发应用。下一章将向您展示如何将一个应用部署到一个实时服务器上,供其他人查看。
十三、JavaScript 和静态部署
您已经在本地机器上进行了大量开发工作,直接在浏览器中使用 JavaScript,然后使用 Angular 和 React 等框架开发依赖于服务器数据的应用。既然您已经了解了使用大型应用是什么样子,那么您可以将这些应用从您的笔记本电脑移动到服务器上,让每个人都可以看到。
对于这个示例,您将使用 JSONPlaceholder 作为您的数据源,并专注于前端开发。
在本书的前几章中,我讨论了如何使用 Git 作为一种跟踪应用随时间推移而发生的所有变化的方式。虽然这很重要,但是 Git 并不能让其他人将网站视为成品。
您需要的是一种将您的代码自动部署到远程服务器的方法,这样访问者就可以看到您所有的努力工作。
本章将首先描述什么是静态站点,以及如何使用 Netlify 这样的服务将你的代码从 GitHub 这样的服务转移到一个动态服务器上。
开发 Angular 应用并将其连接到 GitHub
静态网站实际上是使前端工作的所有部分。在这种情况下,它是 HTML、JavaScript、CSS、图像和任何其他媒体,您需要将网站交付给浏览器。
与数据库的所有交互将由一个不同的服务器来处理,该服务器将响应所有的 API 调用。
对于此示例,您将进行基本的角度应用。这就像你的另一个例子。
确保在项目中添加角度路由器。在命令行中,键入
ng new static-app
这段代码把你的裸露的角度应用放在一起。创建好应用后,现在创建一个照片组件和一个相册组件:
cd static-app
ng generate module media
ng generate component media/photos
ng generate component media/albums
最后,创建一个将为这两个组件检索数据的服务:
ng generate service /media/services/service
一旦所有这些组件和服务都被添加到项目中,您就可以添加 Bootstrap 了,但是方式与过去不同。这里您将使用一个名为 ng-bootstrap ( https://ng-bootstrap.github.io
)的项目。它将为您提供 Bootstrap 的所有功能,而无需安装 jQuery 之类的库。
在命令行中,安装 ng-bootstrap:
npm install --save bootstrap
npm install -–save @ng-bootstrap/ng-bootstrap
添加 Bootstrap 后,您需要确保 Angular 应用可以利用它。有两种方法可以做到这一点。一种是将主模块安装到应用的根目录中。另一种方法是为每个模块只添加您需要的模块,以使该模块工作。
为了让您的应用简单,请打开app.module.ts
。在这里您可以将 NgbModule 导入到应用中,并将其添加到imports
数组中。
完成后,打开angular.json
文件。在 styles 部分中,添加以下代码行,以获取所有内置到 Bootstrap 中的 CSS,以便在您的应用中工作:
"styles": [
"src/styles.sass",
"node_modules/bootstrap/dist/css/bootstrap.min.css",
"node-modules/font-awesome/css/font-awesome.min.css"
]
您的应用现在正处于备份的良好阶段。如果你有一个像 GitHub 或 BitBucket 这样的网站的帐户,你可以为你的代码创建一个新的存储库。
以 GitHub 为例,开源项目可以免费托管。单击 Create repository 按钮,将项目命名为static-site
。见图 13-1 。
图 13-1
使用 GitHub 创建公共存储库
一旦创建了存储库,您将获得将您计算机上的文件连接到远程服务器的指令。
当您使用 CLI 创建站点时,Angular 在您的机器上创建了一个本地存储库。这对于跟踪代码中的变化非常有用,但是如果你想和其他人一起做这个项目,这就没什么帮助了。将代码放在远程服务器上的另一个好处是便于自动部署。你可以抓取网站的最新版本,将编译好的代码转移到一个实时服务器上。
当使用命令行连接 GitHub 时,您可能需要配置一个 SSH 密钥。这将允许您登录 GitHub 并控制您的远程存储库。有关如何设置的信息,请参考 GitHub 的 https://help.github.com/articles/adding-a-new-ssh-key-to-your-github-account/
。
在命令行中,添加连接到远程服务器的功能:
git add .
git commit -m "first commit"
git remote add origin git@github.com:USER-NAME/static-site.git
下一行将文件复制或“推”到服务器:
git push -u origin master
现在,您应该在远程服务器上有了站点的副本。这也将为您提供一个可视化的参考,让您了解随着时间的推移文件所发生的变化。
Git 作为工作流工具有一个branches
的概念。分支只是整个项目的副本。这允许您对项目进行单独的更改。这些更改可以合并到主分支中。
本章并不是 Git 工作流的完整教程;在 www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow
可以找到一个很好的带有视觉参考的解释。
现在您已经将应用和存储库放在一起了,下一节将让您使用 Angular router 和 Bootstrap 来创建站点的两个部分。
使用角度路由器
上一节让您创建一个添加了路由器的全新 Angular 应用。然后添加引导组件,并将整个项目连接到 GitHub 上的公共存储库。
这一节将向你展示如何使用角度路由器为网站创建一个菜单。当您进行更新时,您将使用 Git 跟踪它们。
这个例子将展示一个非常快速的使用角度路由器的方法。更详细的解释可以在 https://angular.io/guide/router
找到。
要创建简单的路由函数,向app.module.ts
添加一些代码。首先,从@ angular/router
路径导入两个项目。进口RouteModule
和Routes
。该行应该如下所示:
import { RouterModule, Routes } from '@angular/router';
您还需要导入您的两个可视化组件,PotosComponent
和AlbumsComponent:
import { PotosComponent } from '../media/photos/photos.component';
import { AlbumsComponent } from '../media/albums/albums.component';
记住获取这些新导入的组件,并将它们添加到app.module.ts
文件中的declarations
数组中:
@NgModule ({
declarations: [
AppComponent ,
PhotosComponent,
AlbumsComponent
]
})
之后,制作一个对象数组来代表你的路线。这个数组的数据类型为Routes
,并被分配给一个名为appRoutes
的变量。路由对象具有预定的属性。这些属性告诉 Angular 路线应该如何工作。
一些属性是
-
path
:匹配 URL 的路径 -
patchMatch
:匹配 URL 的正则表达式 -
component
:当路线被解析时,您希望看到的组件 -
redirectTo
:找到路径后需要重定向的地方 -
data
:需要分配给该路线的任何数据 -
children
:任何子程序,例如/borough/id
您的示例将利用这些属性中的两个,path
和component
。
您的路线将如下所示:
const appRoutes: Routes = [
{path: 'photos', components: PhotoComponent}.
{path: 'albums', components: AlbumsComponent}
]
制定这些路线很重要。你可以让他们变得更复杂,让他们处理一些问题,比如如果有人没有 Angular 应用可以识别的 URL。您可以设置默认路线。
设置好数组后,您可以将它传递给imports
数组中的RouterModule
:
imports: [
RouterModule.forRoot(appRoutes)
]
为了确保您的更新有效,您可以在应用中添加一些按钮,当单击这些按钮时,它们将更新路线。
打开app.component.html
文件。这里有您构建应用时生成的默认欢迎页面。您需要删除所有这些代码,并添加一些新的内容。
在前面的例子中,您使用 Bootstrap 来布局页面。这个例子将使用相同的技巧。从一个容器开始,然后定义组成页面的行和列。然后,您需要添加一个由 Angular 定义的定制元素来处理您的路线。
清单 13-1 显示了更新后的页面应该是什么样子。
<div class="container-fluid">
<div class="row">
<div class="col-12">
<h3>Static Site</h3>
</div>
</div>
<div class="row">
<div class="col-1">
<button class="btn btn-primary" routerLink='/photos'>Photos</button>
</div>
<div class="col-1">
<button class="btn btn-primary" routerLink='/albums'>Albums</button>
</div>
</div>
</div>
<router-outlet></router-outlet>
Listing 13-1The Updated Page
这与您在其他示例中使用的布局类型相同。一个很大的区别是你如何使用按钮。该按钮添加了一个名为routerLink
的属性。该值被传递给路由器,当发现匹配时,在页面上的router-outlet
元素所在的区域对组件进行角度加载。见图 13-2 。
图 13-2
Angular Router 允许您在单页应用中导航
保存文件后,浏览器应该更新,您的按钮应该处于活动状态,能够更改浏览器中的位置并加载适当的组件。
随着文件更新和您的站点提供新功能,现在是保存文件的当前状态并将其保存在存储库中的好时机。
要查看哪些文件已更改,请在命令行中键入
git status
你可以看到到目前为止你所处理的文件列表。下一步是告诉 Git 将这些文件添加到 Git 认为的临时区域中:
git add angular.json
git add package-lock.json
git add package.json
git add src/app/app.component.html
git add src/app/app.module.ts
git add src/app/media/media.module.ts
现在,您已经有了暂存的文件,是时候进行提交了。这是 Git 保存更改的地方,所以它现在有一种方法来比较文件过去的样子和现在的样子。除了提交之外,您还可以添加一条消息来描述此阶段的更改。
要提交,请键入
git commit -m "Angular routing added to the application"
这一行中的-m
告诉 Git 您想在提交中添加一条消息。它后面是用引号括起来的消息。
您已提交,但所有更改都保存在本地计算机上。您需要做的是将这些更改保存在远程服务器上。这是通过使用push
命令与 GitHub 上的远程存储库共享更新来实现的:
git push
可能会要求您输入 GitHub 密码。然后,更新将被发送到您的存储库中,现在在网站上可以看到这些更改。见图 13-3 。
图 13-3
随着时间的推移对应用的更改
您已经能够将导航添加到您的应用中,并随时跟踪您的更改。现在您可以在本地和 GitHub 存储库中看到这些变化。
下一节将向您展示如何使用角度服务提取每个部分的数据。
使用角度服务
Angular 中的服务允许您将相同的代码注入或使用到多个组件中。这为您提供了工作分离,组件可以接收并显示数据,服务可以发出远程服务的请求并解析结果。
为了让服务能够调用远程服务,主应用需要将HttpClientModule
导入并添加到imports
数组中。打开app.module.ts
文件,导入模块:
import { HttpClientModule } from '@angular/common/http';
现在,您可以指导您的服务进行远程调用。打开service.service.ts
。在这里,您可以导入HttpClient
库进行调用:
import _{ HttpClient } from '@angular/common/http';
使用constructor
函数,您创建了您的http
实例:
constructor ( private http: HttpClient ) { }
最后,添加两个方法,一个获取所有照片,另一个获取所有相册:
getAlbums() {
return this.http.get('https://jsonplaceholder.typicode.com/albums ');
}
getPhotos() {
return this.http.get('https://jsonplaceholder.typicode.com/photos ');
}
服务的一个好处是可以在多个组件中使用它们。在下一个示例中,您将服务注入到组件中。这两个组件的工作方式相同。每个组件都需要导入服务,并在构造函数中创建一个实例。
下一个例子展示了如何使用这个服务在PhotosComponent
中检索照片。该服务也可以以同样的方式在AlbumsComponent
内部使用。
在构造函数中已经创建了service
对象。当组件运行生命周期方法ngOnInit
时,服务将运行方法getPhotos
并将结果返回给组件。在AlbumsComponent
的情况下,它将运行方法getAlbums
。
部分PhotosComponent
代码应该是这样的:
constructor (private: service: ServiceService) {}
ngOnInit() {
this.service.getPhotos().subscribe( (results) => {
this.photoResults = results;
});
}
现在可以用 web 服务的结果更新 HTML 模板了。使用*ngFor
指令遍历结果数组,显示从服务返回的标题和缩略图。见清单 13-2 。
<div class="container-fluid">
<div *ngFor="let photo of photosResult" class="row">
<div class="col-1">
<p>Title</p>
</div>
<div class="class=col-6">
<p>{{photo.title}}</p>
</div>
<div class="col-2">
<img src="{{photo.thumnailURL}}">
</div>
</div>
</div>
Listing 13-2The HTML Template
这个例子展示了如何在结果中创建一个for
循环并获取细节。这些细节将生成创建所需的所有行和列所需的所有 HTML。需要指出的重要一点是图像的生成。因为 URL 是由 web 服务调用的结果生成的,所以您可以将该值应用于图像的来源,并且该图像将在浏览器中呈现。见图 13-4 。
图 13-4
调用 photos API 的渲染结果
您的站点现在有一些路线,您可以通过使用按钮或键入 URL 来直接访问。这些路由呈现使用相同服务类的组件,以调用您的数据源并在屏幕上呈现结果。
您对一些文件进行了更新以使其工作。现在是记录这些变化的好时机。回到命令行,使用相同的 git 命令保存应用的当前状态。
首先,检查已更改文件的状态:
git status
如果所有已更改的文件都在同一个文件夹中,例如src
文件夹,您可以使用快捷方式转移它们:
git add src
如果更新的文件在其他文件夹中,您必须添加这些文件的路径。如果你愿意,你可以再次检查状态,看看是否所有的文件现在都在隐藏中。
接下来,您提交:
git commit -m "Routing and Service added"
最后,您将更改推送到服务器:
git push
像前面的例子一样,这会将所有文件从您的本地机器复制到 GitHub 服务器,并记录对这些文件所做的所有更改。
现在,您既有了一个工作站点,又有了站点工作所需文件的备份。有了 GitHub 上的源代码,您现在可以将它连接到一个服务,该服务将部署您的应用,并使它对全世界可用。
下一节将讨论如何连接到 Netlify,这是一种旨在提供应用前端的服务,也称为静态站点。
将静态站点部署到网络
Netlify ( 对于在个人网站上工作的人来说,有一个免费层。一旦注册,开发者可以使用他们已经拥有的由该服务开发的域名,或者购买一个他们可以通过该服务自行管理的域名。
部署一个站点使你能够指向你自己的当前在 GitHub 中的源代码。让服务复制文件,构建项目,并将最终结果推送到一个具有公共地址的服务器上,在那里每个人都可以看到您的站点。
浏览器窗口的右上角是一个名为“来自 Git 的新站点”的按钮。点击它。下一个屏幕询问源代码的位置。这就是为什么把它从本地机器转移到像 GitHub 这样的服务上是很重要的。选择您正在使用的服务。Netlify 将请求您允许访问您在其他服务上的帐户。一旦授予了权限,您就可以指向您创建的用来保存静态站点的存储库。见图 13-5 。
图 13-5
创建新站点时,您需要指向代码存储库
当选择包含所有代码的存储库时,Netlify 会询问您想要部署哪个分支。在这种情况下,您只有一个分支,所以它是主。它还会询问应该运行什么命令来编译应用。在本例中,您使用的是 Angular,所以命令是ng build
。请记住,您需要构建网站,以便所有代码都可以转换为浏览器可以理解的 JavaScript。它需要知道的最后一件事是在哪里找到完成的站点。这个在dist/static-app
文件夹里。见图 13-6 。
图 13-6
将 Netlify 引导到构建站点,并指出在哪里可以找到完成的文件
给它几分钟,Netlify 会给你一个网址,你可以在那里看到你的生活网站。然后,您可以在浏览器中查看该 URL,甚至可以将该 URL 发送给朋友,他们可以在那里看到您网站的实时版本。
现在您的帐户和项目已经运行,您可以使用该服务提供的一些其他功能,如设置自定义域或为您的站点添加安全性的能力。
摘要
这一章在一个非常基础的层面上讲述了构建一个 web 应用的生产过程。开发人员在本地机器上工作,从远程数据源检索数据,并显示结果。组成项目的文件保存在一个存储库中,其中包含一段时间内的更改历史。最后,文件被转移到一个服务中进行编译,最终版本被发布到网站现在所在的服务器上。
您的 Angular 应用使用 routes 来更新浏览器中的 URL,并按需显示某些组件。您还创建了一个服务,它负责向 web 服务发出请求,并将结果发送回组件,以便组件可以在浏览器中呈现结果。
随着时间的推移,保存文件的当前状态是一个好主意。这是通过 Git 这样的版本控制软件完成的。使用 Git 这样的工具不仅可以让您了解文件是如何随时间变化的,而且当项目在公共存储库中时,您还可以与其他人共享这些文件。当你在你的项目中工作时,当你觉得你处于一个好的位置时,这是一个用 Git 保存文件当前状态的好时机。 www.atlassian.com/git/tutorials
有非常好的教程。
在开发项目并跟踪源代码之后,是时候部署应用了。在本章中,您使用了一个名为 Netlify 的服务。还有其他方法来部署你的站点,但是本章的重点是展示如何获取源代码并自动将完成的版本部署到服务器上。