写给-Python-开发者的-JavaScript-实用指南-全-

写给 Python 开发者的 JavaScript 实用指南(全)

原文:zh.annas-archive.org/md5/3cb5d18379244d57e9ec1c0b43934446

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

在学习 Python 时,您通过学习 Python 的基础知识、其优雅和编程原则,迈出了软件工程职业生涯的第一步。在您职业生涯的下一个阶段,让我们学习如何将您的编程知识转移到 JavaScript 上,以处理前端任务,包括 UX/UI 工作、表单验证、前端动画等。您可能熟悉使用 Flask 渲染前端,但 JavaScript 将使您能够实时创建用户界面并对用户输入做出反应。

我们将深入探讨两种语言之间的差异,不仅在语法层面上,还在语义层面上:为什么何时我们使用 JavaScript 而不是 Python,它们的关注点分离是什么,如何使用 Node.js 在前端和后端连接我们现有的 HTML 和 CSS,以创建引人入胜的用户体验,以及如何利用 Web 应用程序的所有层创建全栈应用程序。

本书的受众

在软件工程中,一刀切并不适用。Python 是一种适用、可扩展的语言,专为后端 Web 工作设计,但也可以引发对前端的好奇。本书是为具有 1-3 年 Python 经验的程序员编写的,他们希望扩展对前端编程世界的了解,该世界由 JavaScript 实现,并了解如何在前端和后端都使用 JavaScript(通过 Node.js)可以实现高效的编码和工作流程。

对数据类型、函数和作用域的扎实理解对于掌握本书中阐述的概念至关重要。熟悉 HTML、CSS、文档对象模型(DOM)以及 Flask 和/或 Django 将会很有帮助。

本书涵盖的内容

第一章,“JavaScript 进入主流编程”,我们将了解 JavaScript 的重要性。

第二章,“我们可以在服务器端使用 JavaScript 吗?当然可以!”,深入探讨了服务器端 JavaScript。JavaScript 的使用不仅限于浏览器端,还可以用于丰富、复杂的基于服务器的应用程序。

第三章,“细枝末节的语法”,是您将学习如何编写 JavaScript 以及它的语法与 Python 的不同之处的细节。

第四章,“数据及其朋友 JSON”,涵盖了数据。每个计算机程序都必须处理某种数据。您将学习如何在 JavaScript 中与数据交互。

第五章,“Hello World!及更多:您的第一个应用程序”,让您编写您的第一个 JavaScript 程序!

第六章,“文档对象模型(DOM)”,教会您如何使用网页的基础知识,以便将 JavaScript 与用户交互连接起来。

第七章,“事件、事件驱动设计和 API”,将带您超越基本交互,向您展示如何将动态数据纳入您的程序中。

第八章,“使用框架和库”,介绍了一些现代 JavaScript 程序的支架,以扩展您对行业标准应用的了解。

第九章,“解读错误消息和性能泄漏”,涵盖了错误。错误是难免的!我们应该了解一些如何处理它们并调试我们的程序的知识。

第十章,“JavaScript,前端的统治者”,更详细地介绍了 JavaScript 如何将前端整合在一起。

第十一章,“什么是 Node.js?”,深入探讨了 Node.js。由于已经研究了 JavaScript 在前端的使用,本章将探讨它在“JavaScript 无处不在”范式中使用 Node.js 的角色。

第十二章,Node.js 与 Python 对比,问,为什么开发人员选择 Node.js 而不是 Python?它们可以一起工作吗?我们如何安装我们需要创建和运行程序的软件包?

第十三章,使用 Express,介绍了 Express.js(或只是 Express),这是一个 Web 应用程序框架,被认为是 Node.js 的事实标准 Web 服务器。

第十四章,使用 Django 的 React,探索 Django。您可能已经将 Django 作为 Python 框架,让我们看看它与前端和后端的 JavaScript 框架有何不同。

第十五章,将 Node.js 与前端结合,将前端和后端连接在一起。我们将为(几乎)全栈功能构建两个小型应用程序。

第十六章,进入 Webpack,涉及部署工具,这对于高效的 JavaScript 至关重要。

第十七章,安全和密钥,深入探讨安全性。JavaScript 需要了解安全资源,那么我们该如何处理呢?

第十八章,Node.js 和 MongoDB,转向 MongoDB。MongoDB 是如何与 JavaScript 一起使用数据库的一个很好的例子。我们将使用它作为我们的示例 NoSQL 数据库,因为它与 JSON 数据很好地配合。

第十九章,将所有内容放在一起,让您使用完整的现代 JavaScript 堆栈创建最终项目。

要充分利用本书

由于我们将首先使用 JavaScript,因此您需要在计算机上安装代码编辑器,如 Visual Studio Code,Sublime Text 或其他通用编程环境。由于编码环境的限制,平板电脑等移动设备可能不是合适的环境,但较低配置的计算机可以使用。我们将使用命令行工具,因此熟悉 macOS 终端将会很有用;Windows 操作系统用户应下载并安装 Git Bash 或类似的终端程序,因为标准的 Windows 命令提示符将不够。

需要使用现代浏览器来使用我们的程序。推荐使用 Chrome。我们将在整个 JavaScript 工作中使用 ECMAScript 2015(也称为 ES6)。

我们将安装系统的各种其他组件,如 Node.js 和 Node Package Manager,Angular 和 React。每个必需组件的安装说明将在章节中提供。可能需要管理员权限才能完成所有安装步骤。

如果您使用的是本书的数字版本,我们建议您自己输入代码或通过 GitHub 存储库(链接在下一节中提供)访问代码。这样做将有助于避免与复制和粘贴代码相关的任何潜在错误。

我们的一些项目还需要访问网站,因此需要一个活跃的互联网连接。也建议具有一点幽默感。

下载示例代码文件

您可以从www.packt.com的帐户中下载本书的示例代码文件。如果您在其他地方购买了本书,可以访问www.packtpub.com/support注册,将文件直接发送到您的邮箱。

您可以按照以下步骤下载代码文件:

  1. www.packt.com上登录或注册。

  2. 选择“支持”选项卡。

  3. 点击“代码下载”。

  4. 在搜索框中输入书名,然后按照屏幕上的说明操作。

下载文件后,请确保使用以下最新版本解压或提取文件夹:

  • Windows 使用 WinRAR/7-Zip

  • Mac 使用 Zipeg/iZip/UnRarX

  • Linux 使用 7-Zip/PeaZip

该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers。如果代码有更新,将在现有的 GitHub 存储库上进行更新。

我们还有其他代码包,来自我们丰富的图书和视频目录,可在github.com/PacktPublishing/上找到。去看看吧!

下载彩色图片

我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图片。您可以在这里下载:static.packt-cdn.com/downloads/9781838648121_ColorImages.pdf

使用的约定

本书中使用了许多文本约定。

CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。这里有一个例子:“将下载的WebStorm-10*.dmg磁盘映像文件挂载为系统中的另一个磁盘。”

代码块设置如下:

html, body, #map {
 height: 100%; 
 margin: 0;
 padding: 0
}

当我们希望引起您对代码块的特别关注时,相关的行或项目会以粗体显示:

[default]
exten => s,1,Dial(Zap/1|30)
exten => s,2,Voicemail(u100)
exten => s,102,Voicemail(b100)
exten => i,1,Voicemail(s0)

任何命令行输入或输出都是这样写的:

$ mkdir css
$ cd css

粗体:表示一个新术语、一个重要词或屏幕上看到的词。例如,菜单或对话框中的单词会在文本中出现。这里有一个例子:“从管理面板中选择系统信息。”

警告或重要说明会出现在这样的地方。提示和技巧会出现在这样的地方。

第一部分 - JavaScript 是什么?它又不是什么?

啊,JavaScript,这个神秘的东西。让我们来解开它是什么,又不是什么,因为前端几乎离不开它,而后端也喜爱它。

在这一部分,我们将涵盖以下章节:

  • 第一章,JavaScript 进入主流编程

  • 第二章,我们可以在服务器端使用 JavaScript 吗?当然可以!

  • 第三章,细枝末节的语法

  • 第四章,数据和你的朋友,JSON

第一章:JavaScript 进入主流编程

JavaScript 可以在客户端和服务器端运行,这意味着使用 JavaScript 与 Python 的用例会有所不同。从不起眼的开始,JavaScript 以其怪癖、优势和局限性,现在成为我们所知的交互式网络的主要支柱之一,从丰富的前端交互到 Web 服务器。它是如何成为 Web 上最重要的普遍技术之一的?为了理解 JavaScript 在前端和后端都能添加功能的强大能力,我们首先需要了解前端是什么,以及它不是什么。了解 JavaScript 的起源有助于澄清 JavaScript 的“为什么”,所以让我们来看一下。

本章将涵盖以下主题:

  • 国家超级计算应用中心(NCSA)和互动的需求

  • 早期网络浏览器和 10 天的原型

  • 进入 Ecma 国际

  • HTML、CSS 和 JavaScript——前端的最好伙伴

  • JavaScript 如何适应前端生态系统

技术要求

您可以在 GitHub 上找到本章中的代码文件github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers

NCSA 和互动的需求

与 21 世纪现在拥有的丰富媒介相比,早期互联网是一个相当无聊的地方。没有图形浏览器,只有相当基本的(和神秘的)命令,早期采用者只能在一段时间内完成某些学术任务。从 ARPANET(高级研究计划局网络)开始,它旨在通过成为第一个分组交换网络之一来促进基本通信和文件传输。此外,它是第一个实现传输控制协议/互联网协议(TCP/IP)套件的网络,我们现在认为它是理所当然的,因为它在所有现代网络应用程序的幕后运行。

为什么这很重要?早期互联网是为基本和简单的目的而设计的,但自那时以来它已经发展壮大。作为 Python 开发人员,您已经了解现代网络的强大之处,因此不需要对网络的完整历史有所了解。让我们跳到我们现在所知的前端的起源。

1990 年,蒂姆·伯纳斯-李(Tim Berners-Lee)进入:发明了万维网。通过自己构建第一个网络浏览器,并与欧洲核子研究组织(CERN)创建第一个网站,闸门打开了,世界从此改变。从学术上的摆弄开始,现在已经成为全球必需品,全球数百万人依赖于互联网。不用说,在 21 世纪的今天,我们使用网络和多种形式的数字通信来进行日常生活。

伯纳斯-李创建的项目之一是 HTML——超文本标记语言。作为网站的支柱,这种基本标记语言在计算机社区中引发了重大的增长和发展。只用了几年的时间(确切地说是 1993 年),第一个我们现在称之为浏览器的迭代版本 Mosaic 发布了。它是由伊利诺伊大学厄巴纳-香槟分校的 NCSA 开发的,并且是网络发展的重要组成部分。

早期网络浏览器和 10 天的原型

那么,为什么是 JavaScript?显然,网络需要的不仅仅是静态数据,所以在 1995 年,Netscape Communications 的 Brendan Eich 出现了。最初的想法并不是创建一个全新的语言,而是将 Scheme 整合到 Netscape 中。这个想法被 Sun Microsystems 与 Java 的合作所取代。决定了 Eich 正在创建的这种语言会有些类似于 Java,而不是 Scheme。这个想法的起源来自 Netscape Communications 的创始人 Marc Andreessen。他觉得需要一种语言来将 HTML 与“粘合语言”结合起来,帮助处理图像、插件和——是的——交互性。

Eich 在 10 天内创建了 JavaScript 的原型(最初称为 Mocha,然后是 LiveScript)。很难相信一个 10 天的原型已经成为网络的如此重要的一部分,但这就是历史记录的事实。一旦 Netscape 开发出了一个可供生产使用的版本,JavaScript 就在 1995 年与 Netscape Navigator 一起发布了。JavaScript 发布后不久,微软创建了自己的 JavaScript 版本,称为(毫不起眼地)JScript。JScript 于 1996 年与微软的 Internet Explorer 3.0 一起发布。

现在,有两种技术在同一个领域竞争。JScript 是从 Netscape 的 JavaScript 中进行了逆向工程,但由于这两种语言的特点,浏览器之间的战争开始了,导致网站经常出现“最佳在 Netscape Navigator 中查看”或“最佳在 Internet Explorer 中查看”的标签,这是由于在一个网站上支持这两种技术涉及的技术复杂性。早期版本之间的差异只增加了。一些网站在一个浏览器中可以完美运行,在另一个浏览器中却会出现严重故障,更不用说其他竞争对手对 Netscape 和微软浏览器造成的复杂性了!早期开发人员还发现这两种技术之间的差异只加剧了武器竞赛。如果你经历过性能下降(或者更糟糕的是,你在早期像我一样使用 JavaScript),你肯定感受到了竞争版本的痛苦。每家公司以及其他第三方都在竞相创建下一个最好的 JavaScript 版本。JavaScript 的核心必须在客户端进行解释,而浏览器之间的差异导致了混乱。必须采取一些措施,而 Netscape 有一个解决方案,尽管它并不完美。

我们将在下一节中了解这个解决方案。

进入 Ecma International

欧洲计算机制造商协会ECMA)在 1994 年更名为 Ecma International,以反映其精炼的目的。作为一个标准组织,它的目的是促进各种技术的现代化和一致性。部分是为了应对微软的工作,Netscape 在 1996 年与 Ecma International 接触,以标准化这种语言。

JavaScript 在 ECMA-262 规范中有文档记录。你可能已经看到过ECMAScript或“基于 ECMAScript 的语言”这个术语。除了 JavaScript 之外,还有更多的 ECMAScript 语言!ActionScript 是另一种基于 ECMAScript 的语言,遵循与 JavaScript 类似的约定。随着 Flash 作为一种网络技术的衰落,我们不再在实践中看到 ActionScript,除了一些离散的用途,但事实仍然存在:Ecma International 创建了标准,并用于创建不同的技术,这有助于缓解浏览器之战——至少是一段时间。

关于 JavaScript,Ecma International 最有趣的部分也许是已经编码的各种版本。迄今为止,已经有九个版本,都有不同的差异。我们将在本书中使用 ECMAScript 2015(也称为 ES6),因为它是今天网页开发工作最稳定的基线。2016-2018 版本的功能可以被一些浏览器使用,并将被介绍。

HTML、CSS 和 JavaScript——前端的最好伙伴

每个现代网站或 Web 应用程序的核心至少包括三种技术:HTML、层叠样式表CSS)和 JavaScript。它们是前端的“最好的朋友”,并在以下截图中进行了说明:

图 1.1 - 最好的朋友:HTML、CSS 和 JavaScript

这三种技术的交汇处就是我们现代网站的所在。让我们在接下来的章节中来看看这些。

HTML,被忽视的英雄

当我们思考网络时,网站的基本结构——骨架,可以说是 HTML。然而,由于其(有意的)简单性,它经常被忽视为一种简单的技术。想象一下网站就像是一个身体:HTML 是骨架;CSS 是皮肤;我们的朋友 JavaScript 是肌肉。

HTML 的历史与网络本身的历史密不可分,因为它随着网络本身的发展不断演进,具有先进的规范、特性和语法。但 HTML 是什么?它不是一个完整的编程语言:它不能进行逻辑操作或数据操作。然而,作为一种标记语言,它对我们使用网络非常重要。我们不会花太多时间讨论 HTML,但一些基础知识会让我们走上正确的轨道。

HTML 规范由万维网联盟W3C)控制,其当前版本是 HTML5。HTML 的语法由称为标签的元素组成,这些标签具有特定的定义,并用尖括号括起来。在 JavaScript 中使用时,这些标签描述了 JavaScript 可以读取和操作的数据节点。

HTML 对我们在 JavaScript 中的重要性是什么?JavaScript 可以使用浏览器内部的应用程序编程接口API)即文档对象模型DOM)来操作 HTML。DOM 是页面上所有 HTML 的程序表示,并且它规定了 JavaScript 如何操作呈现页面上的元素。与 Python 不同,JavaScript 可以在前端对用户输入做出反应,而无需与服务器进行通信;它的执行逻辑可以在前端进行。想象一下当您在网站上的表单中输入信息时。有时,有必填字段,如果您尝试提交表单,JavaScript 可以阻止向服务器提交,并给出视觉提示——例如必填框上的红色轮廓和警告消息——并告知用户信息缺失。这是 JavaScript 使用 DOM 进行交互的一个例子。我们将在后面更深入地探讨这一点,在第七章中,事件、事件驱动设计和 API

这是一个简单的 HTML5 样板的例子:

<!doctype html>

<html lang="en">
<head>
  <meta charset="utf-8">

  <title>My Page</title>

</head>

<body>
  <h1>Welcome to my page!</h1>
  <p>Here’s where you can learn all about me</p>
</body>
</html>

它本身相当易读:在标题为title的标签中包含了一个包含页面简单标题的字符串。在meta标签中,除了标签的名称外,我们还有一个元素:charset 属性。HTML5 还引入了语义标签,它们不仅为页面提供了视觉结构,还描述了标签的目的。例如,navfooter用于表示页面上的导航和页脚部分。如果您想在我们进行的过程中尝试 HTML、CSS 和 JavaScript,您可以使用诸如 Codepen.io 或 JSFiddle.net 之类的工具。由于我们目前只使用客户端工作,您不需要在计算机上安装编译器或其他软件。您也可以使用您喜欢的文本编辑器在本地工作,然后在浏览器中加载您的 HTML。

对于我们在 JavaScript 中的需求来说,还有一组重要的属性是classid。这些属性为 JavaScript 访问 HTML 提供了一个高效的通道。让我们在下面的代码块中看一个更加详细的 HTML 示例:

<!doctype html>

<html lang="en">
<head>
  <meta charset="utf-8">

  <title>My Page</title>

</head>

<body>
  <h1 id="header">Welcome to my page!</h1>
  <label for="name">Please enter your name:</label>
  <form>
    <input type="text" placeholder="Name here" name="name" id="name" />
    <p class="error hidden" id="error">Please enter your name.</p>
    <button type="submit" id="submit">Submit</button>
  </form>
</body>
</html>

这将给我们一个非常简单的页面输出,如下所示:

图 1.2 - 一个简单的 HTML 页面

非常基础,对吧?为什么“请输入您的姓名”会重复显示?如果你注意到页面上的第二个p标签,它的一个类是hidden。然而,我们仍然可以看到它。我们需要 CSS 来帮助我们。

CSS

如果 HTML 是我们页面的骨架,那么 CSS 就是它的“皮肤”,赋予它外观和感觉。在前端使用 JavaScript 时,必然会考虑到 CSS。在我们网站表单的示例中,红色轮廓和警告消息通常是通过切换 CSS 类触发的。以下是 CSS 的简短示例:

.error {
  color: red;
  font-weight: bold;
}

在这个示例中,我们有一个 CSS 声明(error类,由其名称前面的句号表示为类),以及花括号内的两个 CSS 规则,用于字体颜色和字体粗细。现在完全掌握 CSS 结构和规则并不重要,但作为前端的 JavaScript 开发人员,你可能会与 CSS 互动。例如,切换我们的error类以使表单中的文本变红并加粗是 JavaScript 触发向用户发送消息的一种方式,告诉他们表单提交存在问题。

让我们将前面的 CSS 添加到我们之前的 HTML 工作中。我们可以看到这导致了以下变化:

图 1.3 - 添加一些 CSS

现在,我们可以看到红色和粗体的规则已经反映出来,但我们仍然可以看到段落。我们接下来的两个 CSS 规则是以下的:

.hidden {
  display: none;
}

.show {
  display: block;
}

这更接近我们期望看到的内容。但为什么要创建一个段落然后用 CSS 隐藏它呢?

JavaScript

现在,让我们来介绍 JavaScript。如果 JavaScript 是身体的肌肉,那么它就负责操纵骨骼(HTML)和皮肤(CSS)。我们的肌肉不能太多地改变我们的外貌,但它们肯定可以让我们处于不同的位置,扩展和收缩我们的弹性皮肤,并操纵我们的骨骼位置。通过 JavaScript,可以重新排列页面上的内容,更改颜色,创建动画等等。我们将深入探讨 JavaScript 如何与 HTML 和 CSS 交互,因为毕竟,JavaScript 就是我们现在阅读这本书的原因!

JavaScript 与 Python 相比最显著的一点是,为了对页面进行更改,Python 程序必须响应来自客户端的输入,然后浏览器会重新呈现 HTML。JavaScript 通过在浏览器中执行来避免这一点。

例如,在我们之前显示的页面中,如果用户尝试在不输入名称的情况下提交表单,JavaScript 可以移除hidden类并添加show类,此时错误消息就会显示。这是一个非常简单的例子,但它强调了 JavaScript 可以在浏览器中执行更改而无需回调服务器的想法。让我们把这些组合起来。

以下是 HTML 的示例:

<!doctype html>

<html lang="en">
<head>
  <meta charset="utf-8">

  <title>My Page</title>

</head>

<body>
  <h1 id="header">Welcome to my page!</h1>
  <form>
    <label for="name">Please enter your name:</label>
    <input type="text" placeholder="Name here" name="name" id="name" />
    <p class="error hidden" id="error">Please enter your name.</p>
    <button type="submit" id="submit">Submit</button>
 </form>
</body>
</html>

以下是 CSS 的示例:

.error {
  color: red;
  font-weight: bold;
}

.hidden {
  display: none;
}

.show {
  display: block;
}

现在,让我们写一些 JavaScript。目前可能还不太明白,但如果你在 JSFiddle 等编辑器中跟着做,尝试将以下 JavaScript 放入 JS 窗格中并点击运行:

document.getElementById('submit').onclick = e => {
  e.preventDefault()
  if (document.getElementById('name').value === '') {
    document.getElementById('error').classList.toggle('hidden')
    document.getElementById('error').classList.toggle('show')
  }
}

现在,如果你运行这个并在不输入任何数据的情况下点击提交,我们的错误消息将显示。到目前为止非常简单,但恭喜你!你刚刚写了一些 JavaScript!那么,我们如何用 Python 来做到这一点呢?我们需要将表单提交到后端,评估提供的输入,并重新呈现带有错误消息的页面。

相反,欢迎来到与前端一起工作。

JavaScript 如何适应前端生态系统

正如您所想象的那样,JavaScript 不仅仅是隐藏和显示元素。一个强大的应用程序不仅仅是一堆脚本标签——JavaScript 适应整个生命周期和生态系统,创造丰富的用户体验。我们将在第八章中使用 React 来深入探讨单页应用程序SPAs),所以现在,让我们先打下基础。

如果您对 SPA 这个术语不熟悉,不用担心——您可能已经使用了至少几个,而没有意识到它们是什么。也许您使用谷歌的 Gmail 服务。如果是这样,稍微浏览一下,注意到页面似乎并没有进行硬刷新来从服务器获取信息。相反,它与服务器异步通信,并动态呈现内容。在等待从服务器加载内容的过程中,通常会出现一个小的旋转图标。从服务器异步加载内容并发送数据的基本范式称为Ajax

Ajax,即异步 JavaScript 和 XML,只是一组用于客户端的技术和技巧,通过允许在后台获取和发送数据来简化用户体验。我们稍后将讨论使用 Ajax 从前端调用 API,但现在,让我们尝试一个小例子。

我们的第一个 Ajax 应用程序

首先,我们将使用 Flask 创建一个非常简单的 Python 脚本。如果您对 Flask 还不熟悉,不用担心——我们不会在这里详细介绍它。

这是一个app.py脚本的例子:

from flask import Flask
import os

app = Flask(__name__, static_folder=os.getcwd())

@app.route('/')
def root():
    return app.send_static_file('index.html')

@app.route('/data')
def query():
    return 'Todo...'

这是我们的 HTML 和 JavaScript(index.html):

<!doctype html>

<html lang="en">
<head>
  <meta charset="utf-8">

  <title>My Page</title>

</head>

<body>
 <h1 id="header">Welcome to my page!</h1>
 <form>
   <label for="name">Please enter your name:</label>
   <input type="text" placeholder="Name here" name="name" id="name" />
   <button type="submit" id="submit">Submit</button>
 </form>
 <script>
   document.getElementById('submit').onclick = event => {
     event.preventDefault()
     fetch('/data')
       .then(res => res.text())
       .then(response => alert(response))
       .catch(err => console.error(err))
   }
 </script>
</body>
</html>

在我们分解这个之前,让我们尝试运行它,通过执行以下代码:

$ pip install flask
$ export FLASK_APP=my_application
$ export FLASK_DEBUG=1
$ flask run

我们应该看到以下屏幕:

图 1.4 - 一个基本的 Flask 页面

让我们点击提交,然后应该出现以下屏幕:

图 1.5 - 将 Python 连接到 JavaScript!

我们成功地在 JavaScript 中显示了来自 Python 的文本 Todo…!让我们快速看一下我们是如何做到的。

我们的基本路由(/路由)将提供我们的静态index.html文件。太好了,现在我们可以看到我们的 HTML。但是第二个路由/data呢?它只是返回文本。到目前为止,它与任何基本的 Flask 应用程序并没有太大的不同。

现在,让我们看看我们的 JavaScript。首先要注意的一件事是:在我们的 HTML 文件中,我们可以用<script>标签包裹我们的 JavaScript。虽然将 JavaScript 存储在一个带有自己的脚本标签的单独文件中(我们会讨论到这一点),但在 HTML 中直接包含代码对于小型、快速和非生产调试目的非常方便。有时您会直接在 HTML 文件中插入代码,但这并不经常发生。现在,我们将打破最佳实践,玩一下以下片段:

document.getElementById('submit').onclick = event => {

嗯。这是什么神秘的一行?这是一个 ES6 箭头函数的开头。我们稍后会更深入地讨论函数,但现在,让我们看看我们可以从这行中得到什么,如下所示:

  • document.getElementById('submit'):通过查看我们的 HTML,我们可以看到有一个带有 ID 属性'submit'的元素:按钮。所以,首先,我们要找到我们的按钮。

  • .onclick:这是一个动作动词。如果您猜到这个函数是设计为在用户点击按钮时执行操作,那么您是正确的。

至于函数的其余内容,我们可以猜到我们正在处理一个事件——涉及获取数据然后对其进行某些操作。那么,这个操作是什么?

alert(response)就是我们对它的处理!alert只是你在浏览器中看到的那些烦人的弹出消息之一,而且,我们用来显示 Flask 的数据!虽然不太实用,但希望你能看到我们的方向:前端并不是独立存在的——我们可以在客户端和服务器端之间来回通信,只需在任一端写几行代码。

在讨论 API 时,我们将更详细地查看fetch函数,但现在,让我们花一分钟来看看我们到目前为止所做的练习,如下所示:

  1. 我们使用 Python 和 Flask 创建了一个小型的 Web 应用程序来提供一个简单的 HTML 页面。

  2. 这个应用程序还有一个端点,用来提供一个非常简单的消息作为输出:待办事项……。

  3. 使用 JavaScript,当用户点击提交按钮时我们采取了行动。

  4. 点击提交按钮后,JavaScript 与 Python 应用程序通信以请求数据。

  5. 返回的数据显示在警报窗口中向用户展示。

就是这样!我们成功发出了第一个 Ajax 调用。

实际中的 JavaScript

既然我们已经看到了 JavaScript 如何与 Python 一起使用的实际例子,让我们讨论一下它在前端领域的用途。剧透警告:我们将在下一章开始在服务器端使用 JavaScript。在我们的 Ajax 示例中遇到了一些神秘的命令,因此可能很容易忽视对 JavaScript 的使用和需求,但我们看到它是一种真正具有实际应用的语言。

JavaScript 之美的一部分在于它几乎被所有浏览器普遍采用。随着时间的推移,JavaScript 的语法和功能已经慢慢发展,但对于不同功能的支持,曾经在各个浏览器之间差异巨大,现在正在标准化。然而,仍然存在一些差异,但网上有一些有用的工具,可以及时更新浏览器可能支持或不支持的各种功能。其中一个网站是caniuse.com,如下截图所示:

图 1.6:caniuse.com 的屏幕截图,显示了元素滚动方法的选择。

这个网站将 JavaScript 的各种方法和属性按照各种流行的浏览器分解成矩阵,以显示每个浏览器支持(或不支持)的情况。然而,总的来说,除非你使用的是尖端功能,否则你不需要太担心你的代码是否能在特定的浏览器上运行。

现在,我们已经展示了 JavaScript 与 Python 交互的示例,作为我们的后端使用 Flask,但我们可以使用几乎任何后端系统,只要它准备好接受入站的 HTTP 流量。Python、PHP、Ruby、Java——所有的可能性都在那里,只要后端期望与前端一起工作。

关于 jQuery 等库的一点说明:我们在本书中不会使用 jQuery。虽然它对于某些方法的快捷方式和简化很有用,但它的一个主要吸引点(至少对于像我这样的许多开发人员来说)是它在浏览器之间的底层标准化。还记得我们发出的 Ajax fetch调用吗?过去,Ajax 调用必须以两种不同的方式编写,每种方式对应一个主要类型的 JavaScript 解释器。然而,浏览器的标准化已经缓解了大部分跨浏览器的噩梦。jQuery 仍然提供许多有用的工具,特别是对于用户界面UI)来说,比如可以使我们无需从头开始编写组件的插件。是否使用 jQuery 或类似的库取决于你或将由项目的需求决定。像 React 这样的库,我们将会讨论,旨在满足与 jQuery 等库非常不同的需求。

总结

JavaScript 在现代网络中占据着重要的地位。从 NCSA 的简单起步,它现在已经成为现代网络应用的一个组成部分,无论是用于 UI、Ajax 还是其他需求。它有官方规范,并不断发展,使得与 JavaScript 一起工作变得更加令人兴奋。与 HTML 和 CSS 协同工作,它可以做的远不止简单的交互,而且可以轻松地与(几乎)任何后端系统通信。它的目的是给我们带来不仅仅是静态页面,我们希望页面能够工作。如果你跟着编码,我们做了一个简单的 Ajax 应用,虽然现在这些命令对你来说可能毫无意义,但希望你能看到 JavaScript 是相当易读的。我们将在以后深入研究 JavaScript 的语法和结构。

我们还没有讨论过 JavaScript 后端的用途,但不用担心,下面就会讨论。

问题

试着回答以下问题来测试你的知识:

  1. 哪个国际组织维护 JavaScript 的官方规范?

  2. W3C

  3. Ecma 国际

  4. 网景

  5. Sun

  6. 哪些后端可以与 JavaScript 通信?

  7. PHP

  8. Python

  9. Java

  10. 以上所有

  11. 谁是 JavaScript 的原始作者?

  12. Tim Berners-Lee

  13. Brendan Eich

  14. Linus Torvalds

  15. 比尔·盖茨

  16. DOM 是什么?

  17. JavaScript 在内存中对 HTML 的表示

  18. 一个允许 JavaScript 修改页面的 API

  19. 以上两者

  20. 以上都不是

  21. Ajax 的主要用途是什么?

  22. 与 DOM 通信

  23. 操作 DOM

  24. 监听用户输入

  25. 与后端通信

进一步阅读

以下是一些资源供您参考:

第二章:我们可以在服务器端使用 JavaScript 吗?当然可以!

我们通常不会认为 JavaScript 存在于服务器端,因为它的大部分历史只存在于浏览器端。然而,归根结底,JavaScript 一种语言——而语言可以对其应用程序(在一定程度上)是不可知的。虽然从一开始就可以使用一些不同的工具在服务器端使用 JavaScript,但是Node.js的引入使得在服务器端使用 JavaScript 成为主流。在这里,Python 和 JavaScript 之间的相似之处比在前端更多,但在实践中两种技术的使用仍然存在显著差异。让我们来看一下 Node.js 以及我们如何利用它在服务器端的力量——以及为什么我们想要这样做!

本章将涵盖以下主题:

  • 为什么要在服务器端使用 JavaScript?

  • Node.js 生态系统

  • 线程和异步性

技术要求

您可以在 GitHub 上找到本章中的代码文件:github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers

为什么要在服务器端使用 JavaScript?

有许多服务器端语言:Java、PHP、Ruby、Go,还有我们的朋友 Python,只是举几个例子。那么,为什么我们要使用 JavaScript 作为服务器端语言呢?一个答案是为了减少上下文切换。理论上,同一个开发人员可以用最少的心理变化来编写 Web 应用程序的前端和后端。迄今为止,关于切换编程语言的成本的研究还很少,往往是高度个人化的,但一些研究表明,从一项任务切换到另一项任务,然后再切换回来,会降低生产力,增加完成任务所需时间。换句话说,从 JavaScript 切换到 Python 需要一些心理上的运动。当然,通过实践,这种心理负担变得不重要(想象一个可以实时听取一种语言并将其翻译成另一种语言的翻译员)。然而,随着技术变化的速度,达到那种流利程度更加困难。可以说,任务之间的一致性越大,切换任务所涉及的心理负担就越小。

让我们来看一下我们讨论过的编码语言在语法和风格方面的相似之处,还有一些历史。

语法相似之处

开发人员喜欢使用 Node.js 的原因之一是它在语法上几乎与前端 JavaScript 相同。

让我们来看一下我们已经写过的一些代码。

这里有一个 JavaScript 代码的例子:

document.getElementById('submit').onclick = event => {
  event.preventDefault()
  fetch('/data')
    .then(res => res.text())
    .then(response => alert(response))
    .catch(err => console.error(err))
}

现在,让我们来看一下一些完全不同的 Node.js 代码,但是具有类似的语法,点符号、花括号等。这是一个例子:

const http = require('http')

http.createServer((request, response) => {
  response.writeHead(200, {'Content-Type': 'text/plain'})
  response.end('Hello World!')
}).listen(8080)

乍一看,这两个代码片段可能看起来并不相似,所以让我们仔细看看。在我们的 JavaScript 示例中,看看event.preventDefault(),然后在我们的 Node.js 示例中,看看response.end('Hello World!')。它们都使用点语法来指定父对象的方法(或函数)。这两行完全做着不同的事情,但我们可以根据 JavaScript 的规则来阅读它们。点语法在 JavaScript 中是一个非常重要的概念,因为它本质上是一种面向对象的语言。就像在使用面向对象的 Python 处理对象时一样,我们可以访问 JavaScript 对象的类方法和属性。就像在 Python 中一样,JavaScript 中也有类、实例、方法和属性。

那么,这个 Node.js 示例到底在做什么呢?再次,我们可以看到 JavaScript 是一种相当易读的语言!即使不太了解 Node.js 的内部,我们也可以看到我们正在创建一个服务器,发送一些东西,并监听输入。如果我们再次与 Flask 示例进行比较,如下所示,我们正在做什么:

from flask import Flask, Response

app = Flask(__name__)

@app.route('/')
def main():
    content = {'Hello World!'}
    return Response(content, status=200, mimetype='text/plain')

$ flask run --port=8080

这两个片段的工作原理并没有本质上的不同;它们是用两种不同的语言实现相同目标的两种不同方式。

让我们看一个在客户端 JavaScript 和 Node.js 中执行相同工作的函数。我们还没有详细讨论语法,所以暂时不要让语法成为绊脚石。

这是一个 JavaScript 示例:

for (let i = 0; i < 100; i++) {
  console.log(i)
}

这是一个 Node.js 示例:

for (let i = 0; i < 100; i++) {
  console.log(i)
}

仔细看看这两个。这不是一个把戏:事实上,它们是相同的。将 JavaScript 版本与以下代码片段中的基本 Python 循环进行比较:

for x in range(100):
    print(x)

我们稍后将深入探讨 JavaScript 的语法,以及为什么它看起来比其 Pythonic 对应物更长,但现在,让我们承认 Python 代码与 JavaScript 有多么不同

更多历史

Node.js,由 Ryan Dahl 创建,最初于 2009 年发布,是 JavaScript 的开源运行时,可以在浏览器之外运行。它可能看起来很新,但在其时间内已经获得了很大的立足点,包括主要的公司。然而,大多数人不知道的一个事实是,Node.js 不是 服务器端 JavaScript 的第一个实现。这个区别再次属于 Netscape,几年前。然而,许多人认为这种语言发展不够,因此它在这方面的使用被限制到了不存在的程度。

Dahl 试图将服务器端和客户端更紧密地联系在一起。从历史上看,应用程序的两侧之间存在着相当大的关注点分离。JavaScript 可以与前端一起工作,但查询服务器是一个持续的过程。据说 Dahl 在创建 Node.js 时受到启发,因为他对文件上传进度条必须依赖与服务器的持续通信感到沮丧。Node.js 通过提供基于事件循环的架构来促进这种通信,呈现了一种更顺畅的执行方式。自从创建 Node.js 以来,Dahl 已经开始创建 Deno,这是一个类似于 Node.js 的 JavaScript 和 TypeScript 运行时。然而,对于我们的目的,我们将使用 Node.js。

我们稍后将深入探讨 Node.js 使用的回调范式,我们还将看到前端 JavaScript 也使用它。

让我们通过更仔细地观察它的谚语生命周期来看看 Node.js 是如何工作的。

Node.js 生态系统

大多数语言不是范式:只编写自包含的代码。称为的独立代码模块在软件工程和开发中被广泛使用。换个角度思考,即使是一个全新的 Web 服务器也没有软件来直接提供网站服务。您必须安装软件包,如 Apache 或 nginx,甚至才能到达网站的“Hello World!”步骤。Node.js 也不例外。它有许多工具可以使获取这些软件包的过程更简单。让我们从头开始看一个使用 Node.js 的基本“Hello World!”服务器示例。我们稍后将更详细地讨论这些概念,所以现在让我们只是进行基本设置。

Node.js

当然,我们首先需要访问语言本身。你可以通过几种方法在你的机器上获取 Node.js,包括包管理器,但最简单的方法就是从官方网站下载:nodejs.org。安装时,确保包括Node Package Managernpm)。根据你的环境,在安装完成后可能需要重新启动你的机器。

安装了 Node.js 之后,确保你可以访问它。打开你的终端并执行以下命令:

$ node -v

你应该会看到返回的版本号。如果是这样,你就准备好继续了!

npm

Node.js 的一个优势是其丰富的开源社区。当然,这并不是 Node.js 独有的,但这是一个吸引人的事实。就像 Python 有pip一样,Node.js 有npm。有数十万个软件包和数十亿次的下载,npm是世界上最大的软件包注册表。当然,随着软件包的增多,就会有一系列的相互依赖关系和保持它们更新的需求,因此 npm 提供了一个相当稳定的版本管理方法,以确保你使用的软件包在一起正常运行。

就像我们测试了 Node 版本一样,我们也会测试npm,就像这样:

$ npm -v

如果由于某种原因你没有安装npm,那么现在是时候研究如何安装它了,因为最初安装 Node 时并没有带有npm。有几种安装它的方法,比如使用 Homebrew,但最好重新查看一下你是如何安装 Node 的。

Express.js

Express 是一个快速、流行的 Web 应用程序框架。我们将把它作为我们 Node.js 工作的基础。我们稍后会详细讨论如何使用它,所以现在让我们快速搭建一个脚手架。我们将全局安装 Express 和一个脚手架工具,如下所示:

  1. 使用命令行安装 Express 生成器,通过运行以下命令:npm install -g express express-generator

  2. 使用生成器创建一个新目录并搭建应用程序,通过运行以下命令:express --view=hbs sample && cd sample

  3. 你的sample目录现在应该包含一个类似这样的骨架:

├── app.js
├── bin
│ └── www
├── package.json
├── public
│ ├── images
│ ├── javascripts
│ └── stylesheets
│ └── style.css
├── routes
│ ├── index.js
│ └── users.js
└── views
    ├── error.hbs
    ├── index.hbs
    └── layout.hbs
  1. 现在,我们将通过运行以下命令来安装应用程序的依赖项:npm install

  2. 它将下载必要的软件包,然后我们将准备启动服务器,通过运行以下命令:npm start

  3. 访问http://localhost:3000/,你应该会看到以下截图中显示的有史以来最激动人心的页面:

图 2.1 - Express 欢迎页面

恭喜!这是你的第一个 Node.js 应用程序!让我们来看看它的内部:

打开routes目录中的index.js文件,你应该会看到类似于这样的内容:

var express = require('express');
var router = express.Router();

/* GET home page. */
router.get('/', function(req, res, next) {
  res.render('index', { title: 'Express' });
});

module.exports = router;

值得注意的是,此时你可能会注意到一些 Node.js 示例和现代 JavaScript 之间的语法差异。如果你注意到了,这些行以分号结尾,而我们之前的示例没有。我们将在后面讨论不同版本的 JavaScript,但现在,如果这让你感到惊讶,就记住这个注释。

让我们来看一下router.get语句,在下面的代码块中有所说明:

router.get('/', function(req, res, next) {
 res.render('index', { title: 'Express' });
});

get指的是程序响应的 HTTP 动词。同样,如果我们处理 POST 数据,行的开头将是router.post。因此,本质上,这是在说:“嘿,服务器,当你收到对主页的请求时,用title变量等于Express来渲染 index 模板。”别担心,我们将在第十三章使用 Express中详细介绍这个问题,但现在,让我们玩一下:

  1. res.render行之前添加一行console.log('hello')

  2. Express改为My Site

在对 Node.js 代码进行更改时,您需要重新启动本地服务器。您可以返回到您的终端,使用Ctrl + C退出 Express,然后使用npm start重新启动它。当然,也有处理这个问题的进程管理器,但是现在,我们使用的是一个非常基本的实现。

再次导航到https://localhost:3000/。您应该会看到以下内容:

图 2.2 - 更改后的 Express 页面

现在,让我们回到您的终端。当您访问本地主机时,您还触发了一个console.log()语句-一个调试打印语句。您应该会看到hello与 Express 提供的请求和响应一起显示在屏幕上,如下截图所示:

图 2.3 - console.log

使用控制台对我们来说将是非常宝贵的,无论是在客户端还是服务器端。这只是它可以做的一小部分!继续使用 Ctrl + C 退出。

线程和异步性

与传统的 Web 架构一样,了解在后端使用 Node.js 的原因是很重要的。

我们已经看了 Node.js 的运行方式,现在,让我们看看 Node 的客户端-服务器架构与传统范式有何不同。

传统的客户端-服务器架构

为了了解 Node.js 与传统架构的不同之处,让我们看一下以下请求图表:

图 2.4 - 传统的客户端-服务器图表

在传统的设置中,每个对服务器的请求(或连接)都会在服务器的内存中产生一个新的线程,占用系统的随机存取内存RAM),直到达到可能的线程数量。之后,一些请求必须等待,直到有更多的内存可用。如果你不熟悉线程的概念,它们基本上是在计算机上运行的一小段命令。这种多线程范式意味着对服务器接收的每个新请求,都会在内存中创建一个新的唯一位置来处理该请求。

现在,请记住,一个请求不是一个完整的网页-一个页面可以有数十个请求,用于其他补充资产,如图像。在下面的截图中,看一下谷歌主页仅有 16 个请求:

图 2.5 - google.com 请求

为什么这很重要?简而言之:可伸缩性。每秒的请求越多,使用的内存就越多。我们都见过网站在负载下崩溃时会发生什么-一个令人讨厌的错误页面。这是我们都想要避免的事情。

Node.js 架构

与这种范式相反,Node.js 是单线程的,允许进行数千次非阻塞的输入输出调用,而无需额外的开销,如下图所示:

图 2.6 - Node.js 客户端-服务器图表

然而,有一件事情需要早早注意到:这种范式并不是管理服务器上的流量和负载的万能解决方案。目前确实没有一个完全解决大量流量问题的银弹。然而,这种结构确实有助于使服务器更加高效。

Node.js 与 JavaScript 配合得如此完美的原因之一是它已经在处理事件的概念。正如我们将看到的,事件是 JavaScript 前端的一个强大基石,因此可以推断,通过将这个过程延续到后端,我们将看到与其他架构有些不同的方法。

总结

尽管在服务器上运行 JavaScript 的概念并不新鲜,但随着 Node.js 的流行、稳定性和功能的大大扩展,它的受欢迎程度也大大提高。早期,服务器端 JavaScript 被抛弃,但在 2009 年随着 Node.js 的创建再次光芒万丈。

Node.js 通过在客户端和服务器端使用相同的基本语法,减少了开发人员的上下文切换心智负担。同一个开发人员可以相当无缝地处理整个堆栈,因为客户端工作和如何在服务器上操作 Node.js 之间存在相当大的相似之处。除了方法上的差异,还有一种不同的基本范式来处理对服务器的请求,与其他更传统的实现相比。

JavaScript:不仅仅是客户端!

在下一章中,我们将深入探讨 JavaScript 的语法、语义和最佳实践。

问题

尝试回答以下问题来测试你的知识:

  1. 真或假:Node.js 是单线程的。

  2. 真或假:Node.js 的架构使其不受分布式拒绝服务(DDoS)攻击的影响。

  3. 谁最初创建了 Node.js?

  4. Brendan Eich

  5. Linux Torvalds

  6. Ada Lovelace

  7. Ryan Dahl

  8. 真或假:服务器端的 JavaScript 本质上是不安全的,因为代码暴露在前端。

  9. 真或假:Node.js 本质上优于 Python。

进一步阅读

请参考以下链接以获取更多关于这个主题的信息:

第三章:细枝末节的语法

当比较两种编程语言时,必然会有结构和语法上的差异。好消息是,Python 和 JavaScript 都是非常易读的语言,所以从 Python 切换到 JavaScript 和 Node.js 的上下文转换不应该太费力。

风格是一个很好的问题:制表符还是空格?分号还是不用?在任何编程语言中写作时出现的许多风格问题都已经在 Python 的 PEP-8 风格指南中得到了回答。虽然 JavaScript 没有官方的风格指南,但不用担心——外面并不是西部荒野。

在我们能够编写 JavaScript 之前,我们必须知道它是什么,才能够阅读和理解它。所有编程语言都有所不同,利用你的 Python 知识来学习一门新语言将需要一些思维的重新构建。例如,当我们想要声明变量时,JavaScript 是什么样子的?它是如何构建的,以便计算机能够理解?在我们进展时,我们需要注意什么?

本章是解锁 JavaScript 能做什么以及如何做的关键。

本章将涵盖以下主题:

  • 风格的历史

  • 语法规则

  • 标点和可读性

  • 房间里的大象-空白

  • 现有标准-使用 linting 来拯救!

技术要求

要跟着本章的示例编码,你有几种选择:

  • 直接在浏览器的 JavaScript 控制台中编码

  • 在 Node 命令行中编码

  • 使用网络编辑器,如jsfiddle.netcodepen.io

使用网络编辑器可能更可取,因为你可以轻松保存你的进度。无论如何,你应该熟悉如何在浏览器中打开 JavaScript 控制台,因为我们将用它来调试输出。这通常在浏览器的“查看”菜单中;如果不是很明显,一些浏览器可能需要在“偏好设置”中打开开发者模式,所以请查阅你的浏览器文档以找到它。

你可以在 GitHub 上找到本章的代码文件,网址为github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-3/Linting

风格的历史

每种编程语言都有自己的风格,旨在简化每行代码的可读性和理解性。有些语言比其他语言更严格;JavaScript 在其原始形式中是较为宽松的语言之一。Brian W. Kernighan 和 P. J. Plauger 在 1974 年首次出版的《编程风格的要素》中有许多格言,这些格言不仅帮助塑造了编码标准,也塑造了编程语言本身。

你可能熟悉Python的 PEP-20 格言:

  • 美好胜过丑陋。

  • 明确比隐式更好。

  • 简单比复杂更好。

  • 复杂比复杂更好。

  • 平面比嵌套更好。

  • 稀疏比密集好。

  • 可读性很重要。

  • 特殊情况并不足以打破规则。

  • 尽管实用性胜过纯洁性。

  • 错误不应该悄悄地传递。

  • 除非明确被消除。

  • 面对模棱两可,拒绝猜测的诱惑。

  • 应该有一种——最好只有一种——明显的方法来做到这一点。

  • 尽管这种方式一开始可能不明显,除非你是荷兰人。

  • 现在总比永远好。

  • 尽管从来比现在好。

  • 如果实现难以解释,那是个坏主意。

  • 如果实现容易解释,那可能是个好主意。

  • 命名空间是一个很棒的想法——我们应该做更多这样的事情!

开玩笑的特质抛开不谈,这些格言中的许多都是在 Python 开发之前写下的原则和经验的启发。Python 于 1991 年首次发布,从一开始就强调代码的可读性,并制定了一些严格的指导方针,从 PEP-8 到 PEP-20。

让我们举个例子,比如两个格言:

编程风格的要素,1974 Python 之禅,1999
写得清晰——不要太聪明。 明确比含蓄更好。

这里表达了类似的观点。我认为大多数软件工程师都会同意这样一种说法,即清晰、明确和可读是你在开发程序时应该追求的良好品质。

然而,有一个观点需要牢记,随着你在 JavaScript 学习中不断进步:由于 JavaScript 的语法设计比其他一些语言更宽松,你可能会发现不同的公司对 JavaScript 代码有自己的内部风格。这并不是 JavaScript 独有的现象——许多语言在公司中也有风格指南,以确保员工之间的代码一致性。这也有助于语言的整体生态系统具有一致的可读性。然而,这会导致不同代码库在风格上存在差异。

与任何语言一样,我们需要了解语法,以知道我们将如何编写 JavaScript。与 Python 一样,机器在执行工作之前期望得到格式正确的代码,这是你的工作。接下来是语法。

语法规则

就像任何其他编程语言一样,JavaScript 有语法规则必须遵循,以便计算机理解我们的代码想要告诉它的内容。这些规则相当简单明了,从大写和标点符号到使用特定结构和避免混淆含义的常用词,都可以提高代码的可读性。JavaScript 语法规则包括以下内容:

  • 大写

  • 保留关键字

  • 变量语法

  • 数据类型

  • 逻辑结构

  • 函数

  • 标点符号

大小写很重要

与大多数编程语言一样,大小写有所不同。myNodemynode变量将被解释为完全不同的变量。也就是说,计算机会完全区分myNodemynode,因为它们的大小写不同。

保留关键字

JavaScript 中有许多保留的关键字不能用作变量名。以下是其中大部分的列表:

| abstract arguments

await

boolean

break

byte

case

catch

char

class

const

continue

debugger

default

delete

do | double else

enum

eval

export

extends

false

final

finally

float

for

function

goto

if

implements

import | in instanceof

int

interface

let

long

native

new

null

package

private

protected

public

return

short

static | super switch

synchronized

this

throw

throws

transient

true

try

typeof

var

void

volatile

while

with

yield |

这些关键字始终以小写形式存在,如果尝试将其中一个关键字用作变量名,程序将显示错误。

声明变量

在 JavaScript 中,最好在使用变量之前声明变量。这个声明可以在赋值时进行,也可以定义一个没有值的变量。

与其他一些语言不同,JavaScript 是弱类型的,因此不需要声明正在创建的变量的类型。按照惯例,JavaScript 中的变量以小写字母开头,采用驼峰命名法,而不是蛇形命名法。因此,myAgemy_ageMyAge更可取。变量不能以数字开头。

在 JavaScript 中有三个关键字用于声明变量:constletvar

const

const,即constant,是一个在程序运行过程中不会改变值的变量。它们对于强制执行不希望更改的值很有用。在 ECMAScript 的第六版 ES2015(通常称为 ES6)之前,任何变量的值都可以被改变,因此常见的错误,比如使用赋值运算符(=)而不是比较运算符(=====):

const firstName = "Jean-Luc"
const lastName = "Picard"

当然,皮卡德船长可能会改变他的名字,但这似乎不太可能。

有时,我们想将变量声明为硬常量,比如π或 API 密钥。这些用例通常是对命名标准的唯一例外,通常全部大写,有时有下划线:

const PI = 3.14159
const API_KEY = 'cnview8773jass'

到目前为止,我们已经有了两种数据类型的示例:字符串数字。JavaScript 没有floatintlong的概念;它们都是数字。如果你注意到了,我们也可以用单引号或双引号声明字符串。一些库和框架更喜欢其中一种,但对于标准的 JavaScript 来说,使用任何一种都可以。然而,最好保持一致。

let

使用let声明变量时,我们明确声明我们期望或允许变量的值在程序运行过程中发生变化:

let ship = "Stargazer"
ship = "Enterprise" // ship now equals "Enterprise"

皮卡德船长随时可以被转移到另一艘船上,所以我们希望我们的程序允许值的变化。

var

JavaScript 中定义变量的最古老的方法是使用var关键字。使用var声明不会对变量的值施加任何限制;它可以被更改。

var的使用仍然受支持,但被认为是遗留的,并在 ES6 中被弃用。然而,由于存在数十年的现有程序和示例,至少熟悉var是很重要的。

数据类型

尽管 JavaScript 是弱类型的,但了解可用的数据类型对我们很重要,因为我们需要了解它们以解决比较和重新赋值等问题。

以下是基本 Python 变量到基本 JavaScript 的粗略映射:

Python JavaScript
Number Number
String String
List Array
Dictionary Object
Set Set

这涵盖了你可能会使用的基本类型。让我们来看看其他更微妙的 JavaScript 数据类型。有些在 Python 中有对应的,有些没有:

Python JavaScript 半等效 差异原因
bool boolean 虽然在实践中数据类型是相同的,但 Python 的bool数据类型继承自int。虽然在 JavaScript 中可以使用10表示TrueFalse,但它们不会被识别为boolean类型。
None null 从技术上讲,None本身就是一个对象,而null是一个假值。
undefined 在 JavaScript 中,一个没有用值声明的变量仍然有一个伪值:undefined的单例值。
object Python 和 JavaScript 都是面向对象的语言,但它们对对象的使用有些不同。JavaScript 中对象的基本用法是键值存储。对象不是原始类型,可以存储多种类型的数据。
symbol 符号是 ES6 中的一种新数据类型。虽然使用方法有微妙之处,但值得一提。它们用于为对象创建唯一标识符。

现在,我们需要更多地了解一些类型,包括如何比较它们和处理它们。

typeof 和 equality

尽管变量类型是可变的,但了解变量在某一时刻是什么数据类型通常是有用的。typeof运算符帮助我们做到这一点:

typeof(1) // returns "number"
typeof("hello") // returns "string"

请注意返回值是字符串。

在比较变量时,有两种相等运算符:宽松相等和严格相等。让我们看一些例子:

let myAge = 38
const age = "38"
myAge == age

如果我们运行这个比较,将得到 true 的结果。然而,我们可以看到 myAge 是一个数字,而 age 是一个字符串。结果为 true 的原因是,当使用宽松相等运算符(双等号)时,JavaScript 使用类型强制转换来试图提供帮助。当比较不同类型的变量时,值会被宽松比较,因此虽然 38"38" 是不同类型,但由于它们的值,比较的结果是真值。

正如你可以想象的那样,这可能会产生一些意想不到的行为。要求 JavaScript 在比较中包含类型,使用严格相等运算符:三个等号。

通过前面的例子,我们可以尝试 myAge === age,将得到 false 的结果,因为它们是不同的数据类型。通常认为最佳实践是使用严格相等来避免类型强制转换,除非您有特定需要使用宽松相等。

数组和对象

数组和对象不是原始类型,可以包含混合类型。以下是一些示例:

const officers = ['Riker','Data','Worf']

const captain = {
  "name": "Jean-Luc Picard",
  "age": 62,
  "serialNumber": "SP 937-215",
  "command": "NCC 1701-D",
  "seniorStaff": officers
}

officers 是一个数组,我们可以通过方括号看到。关于数组的一个有趣的事实是,即使我们通常将它们声明为常量,数组中的值可以被更改。.push().pop() 是两个用于操作数组的有用方法:

officers.push('Troi') // officers now equals ['Riker','Data','Worf', 'Troi']

请注意,数组中的值没有以任何方式排序;我们可以通过使用方括号表示法来获取 Riker。然而,如果我们尝试完全重新分配数组,当重新分配已声明的常量时,我们仍然会得到一个错误。数组可以容纳任何组合的数据类型。

我们将使用的一个非常方便的数组属性是 .length。由于它是一个属性,它不使用括号:

officers.length // now equals 4

请注意,即使数组是从零开始索引的,length 属性却不是。数组中有四个元素,索引从 0 到 3。

我们将在本章中更详细地讨论方法和属性。

对象是 JavaScript 非常强大的基础组件。实际上,从技术上讲,JavaScript 中的几乎所有东西都是对象!我们可以通过点符号访问数组方法,因为数组从技术上讲是一种对象。但是,我们无法通过点符号访问数组的

如果我们看 captain,我们可以看到三种不同的数据类型:字符串、数字和数组。对象也可以有嵌套对象。作为键值存储的一部分,键应该是一个字符串。要访问一个值,我们使用点符号:

captain.command // equals "NCC 1701-D"

我们可以使用点符号访问对象的部分,这类似于 Python 中的dict,但不完全相同!随着我们使用对象,细微差别将变得更加清晰,因为它们是 JavaScript 独特之处的基础。

条件语句

让我们看看在 Python 和 JavaScript 中以两种方式编写的 if/else 语句:

Python JavaScript

|

if a < b:
  min = a
else:
  min = b

|

let min

if (a < b) {
  min = a
} else {
  min = b
}

|

|

min = a if a < b else b

|

let min = (a < b) ? a : b

|

在两列中,代码正在执行相同的操作:简单测试以查看 a 是否小于 b,然后将较小的值分配给 min 变量。第一行是完整的 if/else 语句,第二行使用三元结构。这些示例中有一些语法规则需要注意:

  • min 必须在使用之前声明,作为最佳实践。在严格模式下,这实际上会抛出错误。

  • 我们的 if 子句被括号包围。

  • 我们的 if/else 语句被大括号包围。

  • 三元运算符中的关键字和操作符与 Python 中的显着不同(并且有点更加神秘)。

如果我们想要使用我们现在了解的 typeof,我们可以使用严格相等来更好地理解我们的变量:

let myVar = 2

if (typeof(myVar) === "number") {
  myVar++; // myVar now equals 3
}

循环

JavaScript 中有四种主要类型的循环:forwhiledo/whilefor..in。(还有一些其他的循环结构方式,但这些是主要的。)它们的使用情况应该不会有太多意外。

for 循环

使用迭代器执行指定次数的代码:

Python JavaScript

|

names = ["Alice","Bob","Carol"]
for x in names:
    print(x)

|

const names = ["Alice","Bob","Carol"]

for (let i = 0; i < names.length; i++) {
  console.log(names[i])
}

|

现在,你可能会想,“如果 JavaScript 有for..in循环,为什么我们不使用它呢?”事实证明,Python 的for/in和 JavaScript 的for..in假朋友:它们的名字看起来很像,但在使用上却非常不同。我们将很快讨论 JavaScript 的for..in循环。另外,注意我们需要在for循环中有三个子句:

图 3.1 - for循环的声明、条件和执行阶段

声明将定义一个迭代器或使用现有的可变变量。注意它应该是一个可变的数字!

我们的条件是我们要测试的内容。我们希望我们的循环在i小于names.length时运行。由于name.length3,我们将运行我们的循环三次,或者直到i等于4,这不再满足我们的条件。

在我们的循环的每次迭代结束时,我们都会执行一些东西;通常是简单地递增我们的声明。现在注意一下我们的每个子句之间的分号…不像 JavaScript 的其他部分,这些是可选的。在执行部分之后没有分号。

while 循环

JavaScript 的while循环在使用上与 Python 的等效部分相同,只是语法上有一点不同:

Python JavaScript

|

i = 0
while i < 10:
    i += 1

|

let i = 0
while (i < 10) {
   i++
}

|

do/while 循环

正如名称所示,do/while循环在给定条件等于true时执行do代码。看一下 JavaScript:

let i = 0

do {
  i++
} while (i < 10)

for..in 循环

现在,我承诺要解释为什么 Python 的for..in与 JavaScript 的用法不同。不同之处在于 JavaScript 的for..in用于遍历对象中的键,而 Python 的for..in用作离散实体的循环。

让我们看一个例子:

const officers = ['Riker','Data','Worf']

const captain = {
  "name": "Jean-Luc Picard",
  "age": 62,
  "serialNumber": "SP 937-215",
  "command": "NCC 1701-D",
  "seniorStaff": officers
}

let myString = ''

for (let x in captain) {
  myString += captain[x] + ' '
}

你认为myString现在等于多少?由于JavaScriptfor..in的目的是遍历对象中的每个,它是Jean-Luc Picard 62 SP 937-215 NCC 1701-D Riker,Data,Worf

for..of 循环

还有一个for循环:for..of,它与for..in不同。for..of循环遍历任何可迭代的值,比如数组、字符串、集合等。如果我们想遍历officers并记录每个名字,我们可以这样做:

for (const officer of officers) {
  console.log(officer)
}

接下来,我们将讨论函数!

函数

啊,函数。我们喜欢它们,因为它们是模块化、不重复自己DRY)程序的关键。JavaScript 和 Python 中的用例是一样的:代码块打算被调用多次,通常是带有不同参数。参数是函数将接受的变量,以便在可变数据集上执行其代码。参数是我们在调用函数时传递的东西。本质上它们是一样的,但根据它们在何时何地使用,有不同的词:它们是抽象,还是实际数据?让我们来看一个并排比较:

Python JavaScript

|

def add_one(x):
  x += 1
  return x

print(add_one(5))
// output is 6

|

function addOne(val) {
  return ++val
}

console.log(addOne(5))
// output is 6

|

如果你还没有在浏览器中打开 JavaScript 控制台,现在应该打开来看看我们的输出6

你可以看到结构相当相似,我们的参数被传递在括号中。如前所述,我们在 JavaScript 中更喜欢驼峰命名,并用大括号封装。使用参数5调用函数是一样的。为了简洁起见,我们可以在return执行之前使用++运算符在左边递增val。这样的快捷方式在 JavaScript 中很常见,但记住要明智地使用它们:“写得清晰—不要太聪明。”

然而,JavaScript 实际上有两种不同的声明函数的方式,还有 ES6 中引入的新语法。

函数声明

在前面的代码中,addOne()函数声明的一个例子。它使用函数关键字来声明我们的功能。它的结构和看起来一样简单:

function functionName(optionalParameters, separatedByCommas) {
  // do work
  // optionally return a value
}

函数表达式

这是addOne()的一个函数表达式的例子:

const addOne = function(val) {
  return ++val
}

函数表达式应该在表达式中使用const,尽管使用varlet在语法上并不是错误的。

声明和表达式之间有什么区别?核心区别在于函数声明可以在程序中的任何地方使用,因为它被hoisted到顶部。由于 JavaScript 是自上而下解释的;这是对该范例的一个重大例外。因此,相反,使用表达式必须在表达式编写后发生。

箭头函数

ES6 引入了箭头语法来编写函数表达式:

const addOne = (val) => { return ++val }

为了进一步复杂化问题,我们可以省略val周围的括号,因为只有一个参数:

const addOne = val => { return ++val }

箭头函数和表达式之间的主要区别集中在词法作用域上。我们在hoisting中提到了作用域,并且我们将在下一章中更详细地讨论它。

注释

与任何语言一样,注释都很重要。在 JavaScript 中有两种声明注释的方法:

const addOne = (val) => { return ++val } // I am an inline, single line comment

// I am a single comment

/*
 I am a multiline comment
*/

因此,我们可以用//开始注释,并写到行尾。我们可以用//进行全行注释,还可以用/*进行多行注释,以*/结束。此外,您可能会在 JSDoc 风格的注释中遇到注释,用于内联文档:

/**
 * Returns the argument incremented by one
 * @example
 * // returns 6
 * addOne(5);
 * @returns {Number} Returns the value of the argument incremented by one.
 */    

有关 JSDoc 的更多信息包含在进一步阅读部分中。

方法和属性

到目前为止,我们已经看到.push().pop()作为数组实例的方法。在 JavaScript 中,方法只是一个固有于其数据类型的函数,它对变量的数据和属性进行操作。我之前提到过,几乎 JavaScript 中的一切都是对象,这并不夸张。从功能和语法到结构和用法,对象的原始数据类型与任何其他变量之间有许多相似之处。

我们对 JavaScript 语法的理解的下一部分是每个人都喜欢的:标点符号。虽然这可能看起来微不足道,但对于代码的解释,无论是人还是计算机,它都非常重要。

标点符号和可读性

与每种语言一样,JavaScript 对标点符号和空格如何影响可读性有一些约定。让我们看看一些想法:

  • Python
def add_one(x):
  x += 1
  return x
  • Java
int add_one(int val) {
  val += 1;
  return val;
}
  • C++
int add_one (int val)
{
  val += 1;
  return val;
}
  • JavaScript
function addOne(val) {
  return ++val
}

在 JavaScript 中,前面示例的约定如下:

  • 函数名称和括号之间没有空格。

  • 在左花括号之前有一个空格,它在同一行上。

  • 右花括号单独一行,与function的开头语句对齐。

在这里,关于 JavaScript 和我们将在本书中使用的示例与您可能在现场和在线示例中遇到的示例之间还有一个现代观点:分号

在现代 JavaScript 中,除了少数例外,语句末尾的分号是可选的。过去,始终用分号终止语句行是最佳实践,您会在现有代码中看到很多分号。这是一个从公司到公司、项目到项目和库到库的风格问题。有一些标准,我们将很快在 linting 中讨论,但在本书的目的上,我们将使用分号来终止语句,除非在语法上需要(例如我们在循环中看到的)。

重要的是要注意,嵌套行应该缩进两个空格。两个空格和四个空格是一个风格问题,但在这本书中,我们将使用两个空格。帮助保持一致性的一种方法是配置您的代码编辑器将制表符转换为两个空格(或四个,根据需要)。这样,您只需按一下Tab,而不用担心按了空格键多少次。我不会详细阐述正确缩进的重要性,但请记住:您的代码遵循的风格和最佳实践越多,对于维护您的代码的人员以及您未来的自己来说,它就会更易读!

大象在房间里——空白

好的,好的,我们知道 Python 是基于空格的:制表符很重要!然而,在大多数情况下,JavaScript 真的不在乎空格。正如我们之前看到的,缩进和空格是风格而不是语法的问题。

所以问题是:当我第一次学习 Python 时,依赖空格的语言的想法令人憎恶。我想:“一个依赖于不正确的 IDE 设置就会崩溃的语言怎么能生存?”。撇开我的观点不谈,好消息是 Python 中的缩进与 JavaScript 中的缩进加大括号是平行的。

这里有一个例子:

Python JavaScript

|

def hello_world(x):
 if x > 3:
   x += 1
 else:
   x += 2
 return x

|

function helloWorld(val) {
  if (val > 3) {
    return ++val
  } else {
    return val+2
  }
}

|

如果您注意到,我们 Python 函数中的if语句的缩进方式与此 JavaScript 示例的缩进方式相同,尽管没有大括号。所以耶!您对 Python 缩进规则的遵守实际上在 JavaScript 中非常有用!虽然不需要像 Python 那样包含空格,但它确实可以提高可读性。

归根结底,JavaScript 喜欢缩进就像 Python 一样,因为这样可以使代码更易读,尽管对于程序运行来说并不是必需的。

现有标准- linting 来拯救!

我们已经看过了 JavaScript 的约定和规范,但大多数规则都有一个“这可能会有所不同”的例外或“这在技术上并不是必需的”。那么,在一个可塑的、以意见为驱动的环境中,我们如何理解我们的代码呢?一个答案:linting

简而言之,linting指的是通过预定义的规则运行代码的过程,以确保它不仅在语法上正确,而且还遵循适当的风格规则。这不仅限于 JavaScript 的实践;您可能也对 Python 代码进行了 linting。在现代 JavaScript 中,linting 已经被视为确保代码一致的最佳实践。社区中的两个主要风格指南是 AirBnB (github.com/airbnb/javascript)和 Google (google.github.io/styleguide/jsguide.html)。您的代码编辑器可能支持使用 linter,但我们现在不会进入实际使用它们的细节,因为每个编辑器的设置都有所不同。以下是在 Atom 中的快速查看:

图 3.2 - Atom 中的 Linting 错误

对于我们的目的,要知道标准是存在的,尽管它们可能会因风格指南而有所不同。您可以从github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-3/Linting克隆一个演示 linting 的存储库。

有几种流行的 linting 工具可用,例如 ESLint 和 Prettier。您选择的工具可以根据您选择的风格指南进行自定义。

好了,这一章内容太多了!让我们结束吧。

总结

JavaScript 拥有丰富的语法和语法,经过多年的使用和完善。使用 ES6,我们有各种数据类型、声明函数的方法和代码规范。虽然编写 JavaScript 似乎是非常随意和快速的,但有最佳实践,而且语言的基本原理与其他语言一样强大。请记住,大小写是有影响的;不要将保留字用作变量名;使用constlet声明变量;尽管 JavaScript 是弱类型的,但数据类型很重要;条件、循环和函数都有助于构建代码的逻辑结构。

精通 JavaScript 的语法和语法对于理解如何使用这种强大的语言至关重要,所以花时间熟悉细节和复杂性。在向前迈进时,我们将假设您对 JavaScript 的风格流利,因为我们将涉及更困难的材料。

在下一章中,我们将亲自动手处理数据,并了解 JavaScript 如何处理和建模数据。

问题

尝试回答以下问题来测试你的知识:

  1. 以下哪个不是有效的 JavaScript 变量声明?

  2. var myVar = 'hello';

  3. const myVar = "hello"

  4. String myVar = "hello";

  5. let myVar = "hello"

  6. 以下哪个开始了函数声明?

  7. function

  8. const

  9. func

  10. def

  11. 以下哪个不是基本循环类型?

  12. for..in

  13. for

  14. while

  15. map

  16. 真或假 - JavaScript 需要使用分号进行行分隔。

  17. 真或假 - 空格在 JavaScript 中从不计数。

进一步阅读

第四章:数据和你的朋友,JSON

现在是时候学习 JavaScript 如何内部处理数据的具体细节了。这些结构大多数(几乎)与 Python 相同,但在语法和用法上有所不同。我们在第三章中提到过,Nitty-Gritty Grammar,但现在是时候深入了解如何处理数据、使用方法和属性了。了解如何处理数据是使用 JavaScript 的基础,特别是在进行高级工作,比如处理 API 和 Ajax 时。

本章将涵盖以下主题:

  • 数据类型 - JavaScript 和 Python 都是动态类型的!

  • 探索数据类型

  • 数组和集合

  • 对象和 JSON

  • HTTP 动词

  • 前端的 API 调用 - Ajax

技术要求

从 GitHub 上克隆或下载本书的存储库github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers,并查看Chapter-4的材料。

数据类型 - JavaScript 和 Python 都是动态类型的!

在第三章中,Nitty-Gritty Grammar,我们讨论了使用typeof()来确定变量的数据类型,并使用letconst来定义它们。关于 JavaScript 有一个有趣的事实,它与 Python 共享:它们都是动态类型的。与 Java 等静态类型语言相反,JavaScript 的变量类型可以在程序运行过程中改变。这就是为什么typeof()会有用的一个原因。

让我们看一个快速的例子,对比 JavaScript 和 Java:

Java JavaScript

|

int age;
age =  38;
age = "thirty-eight";

|

let age
age = 38
age = "thirty-eight"

|

如果我们尝试运行 Java 代码,我们会得到一个错误,指出类型不兼容。在 Java 中,变量有一个类型。然而,当我们运行 JavaScript 代码时,一切都很顺利。在 JavaScript 中,有一个类型。

还要知道 JavaScript 是弱类型的,这意味着在大多数情况下允许数据类型之间的隐式转换。如果我们回想一下第三章中的宽松和严格相等运算符,Nitty-Gritty Grammar,弱类型是为什么当前的最佳实践规定在尽可能使用严格相等检查。

如果我们看一下一些语言在强/弱和动态/静态方面的比较,我们可以将这些语言绘制在这样一个轴上:

图 4.1 - 类型的轴

JavaScript 风格通常倡导使用描述性名称而不是简写名称。这是可以接受的原因之一是,通常情况下,JavaScript 代码在进入生产之前会被缩小。这并不完全像编译,但它确实压缩空白并重命名变量以被压缩。当我们讨论 webpack 时,我们将在第十六章,进入 webpack中讨论一些这些构建过程。

好的,所以 JavaScript 是动态和弱类型的。这在实践中意味着什么?简短的答案是:要小心!在比较运算符中很容易混淆类型,甚至更糟糕的是,意外地将变量转换为不同的类型。在编写程序时,这给了我们更多的灵活性,但它也可能是一个诅咒。一些开发人员喜欢使用匈牙利命名法(frontstuff.io/write-more-understandable-code-with-hungarian-notation)来帮助区分变量类型,但这在 JavaScript 中并不常见。帮助自己和同事保持正确类型的最好方法可能是在变量名中明确表示类型。

探索数据类型

让我们深入研究原始数据类型,因为它们对我们在 JavaScript 中的工作至关重要。我们不仅需要知道我们正在使用的是什么,而且为什么也很重要。我们的原始数据类型是语言其余部分的构建块:布尔值、数字和字符串。JavaScript 的其余部分都是建立在这些原始数据类型之上的。我们将从布尔值开始。

布尔值

布尔值可能是最简单和最通用的数据类型,因为它与二进制逻辑的 1 和 0 紧密相关。在 JavaScript 中,布尔值简单地写为truefalse。不建议使用10作为布尔值,因为它们将被解释为数字,从而导致严格的相等失败。布尔值是一种特定的数据类型,与 Python 不同,在语言的核心部分,布尔值继承自数字。

还记得第三章中的Nitty-Gritty Grammar吗,我们在那里学到几乎所有 JavaScript 中的东西都是对象吗?布尔值也是如此。正如您在下面的屏幕截图中所看到的,如果您在浏览器中打开 JavaScript 控制台,很可能会自动完成,以便查看对于布尔值可用的方法:

图 4.2 - Chrome 中的布尔自动完成

现在,我怀疑这些方法中没有一个对您特别有用,但这是一个方便的方法,可以检查对于给定变量可用的方法。

布尔值只能带我们走这么远 - 是时候看看数字了。

数字

JavaScript 没有整数、浮点数或双精度等不同类型的数字概念 - 一切都只是一个数字。所有基本算术方法都是内置的,Math对象提供了您期望在编程语言中找到的其余功能。这是一个例子:

let myNumber = 2.14 myNumber = Math.floor(myNumber) // myNumber now equals 2

您还可以使用科学计数法,如下所示:

myNumber = 123e5  // myNumber is 12300000

JavaScript 中的数字不仅仅是任意的数字,而是固有的浮点数。从技术上讲,它们存储为遵循国际 IEEE 754 标准的双精度浮点数。然而,这确实导致了一些…有趣的…怪癖。如果您得到奇怪的结果,例如下面来自 JavaScript 控制台的屏幕截图中的结果,请记住这一点:

图 4.3 - 浮点精度错误

一个经验法则是考虑您希望进行计算的精度。您可以使用数字的toPrecision()方法指定您的精度,然后使用parseFloat()函数,如下所示:

let x = 0.2 + 0.1 // x = 0.30000000000000004
x = parseFloat(x.toPrecision(1)) // x = 0.3

toPrecision()返回一个字符串,这乍一看可能有些反直觉,但这有一个很好的理由。假设您需要您的数字有两位小数(例如,显示美元和美分)。如果您在数字上使用toPrecision()并返回一个数字,如果您对整数进行更多计算,它将仅呈现整数,除非您也操纵小数位。这其中是有一些方法的。

接下来是:字符串。我们需要向我们的程序添加一些内容!

字符串

啊,可敬的字符串数据类型。它具有一些您期望的基本功能,例如length属性和slice()split()方法,但总是让我困惑的两个是substr()substring()

"hello world".substr(3,5) // returns "lo wo" "hello world".substring(3,5) //  returns "lo"

这两种方法之间的区别在于第一个指定了(start, length),而第二个指定了(start, end index)。记住区别的一个方便方法是.substring()在名称中有一个"i",与索引相关 - 在字符串中停止的位置。

ES6 中的一个新添加使我们的生活更轻松,那就是模板文字。看看这个日志:

const name = "Bob"
let age = 50
console.log("My name is " + name + " and I am " + age + " years old.")

它可以工作,但有点笨重。让我们使用模板文字:

console.log(`My name is ${name} and I am ${age} years old.`)

在这个例子中有两个重要的事情需要注意:

  • 字符串以反引号开始和结束,而不是引号。

  • 要插入的变量被包含在${ }中。

模板文字很方便,但不是必需的。当您遇到问题时,在网上研究代码时,您肯定会看到以前的字符串连接方式的示例。但是,请记住,这对您来说也是一种选择。

让我们尝试一个练习!

练习-基本计算器

有了我们对布尔值、数字和字符串的了解,让我们构建一个基本的计算器。首先克隆存储库github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-4/calculator/starter-code

您可以大部分时间安全地忽略 HTML 和 CSS,但是阅读 HTML 将有所帮助。让我们来看看 JavaScript:

window.onload = (() => {
  const buttons = document.getElementsByTagName('button')
  const output = document.getElementsByTagName('input')[0]
  let operation = null
  let expression = firstNumber = secondNumber = 0

  output.value = expression

  const clickHandler = ((event) => {
    let value = event.target.value

    /** Write your calculator logic here.
        Use conditionals and math to modify the output variable.

        Example of how to use the operators object:
          operators'=' // returns 3

        Expected things to use:
          if/else
          switch() - https://developer.mozilla.org/en-
           US/docs/Web/JavaScript/Reference/Statements/switch
          parseFloat()
          String concatenation
          Assignment
    */

  })

  for (let i = 0; i < buttons.length; i++) {
    buttons[i].onclick = clickHandler
  }

  const operators = {
    '+': function(a, b) { return a + b },
    '-': function(a, b) { return a - b },
    '*': function(a, b) { return a * b },
    '/': function(a, b) { return a / b }
  };
})

这对于初学 JavaScript 的人来说并不是一个容易的练习,所以不要害怕查看解决方案代码并进行逆向工程:github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-4/calculator/solution-code

接下来,让我们探索数组和 ES6 的一个新添加:集合

数组和集合

任何编程语言都有一些关于数组或项目集合的概念,它们都共享一些共同的特征或用途。JavaScript 有一些这样的概念:数组集合。这两种结构都包含项目,并且在许多方面它们的使用方式也相似,因为它们可以被枚举、迭代和显示以进行逻辑构建。

让我们首先看看数组。

数组

数组可以包含不同的数据类型。这是一个完全可行的数组:

const myArray = ['hello',1,'goodbye',null,true,1,'hello',{ 0 : 1 }]

它包含字符串、数字、布尔值、null和一个对象。这没问题!虽然在实践中,您可能不会混合数据类型,但没有什么可以阻止您这样做。

使用typeof()对数组有一个怪癖:因为它们不是真正的原始值,typeof(myArray)将返回object。在编写 JavaScript 时,您应该记住这一点。

正如我们在第三章中看到的,Nitty-Gritty Grammar.push().pop()是最有用的数组方法之一,分别用于向数组添加和删除项目。还有很多其他可用的方法。让我们来看看其中的一些。

要创建一个数组,我们可以像以前的代码一样做,也可以简单地写成const myArray = []。现在,虽然我们可以修改数组中的值,但我们可以将其声明为const,因为在大多数情况下,我们不希望让程序完全重新定义它。我们仍然可以操作数组中的值;我们只是不想破坏和重新创建它。让我们继续使用前面示例中的数组项:

myArray.push('goodbye') // myArray is now ['hello',1,'goodbye',null,true,1,'hello',{ 0 : 1 }, 'goodbye']
myArray[3] // equals null

请记住,数组是从零开始索引的,所以我们的计数从0开始。

要从数组末尾删除一个元素,我们使用.pop(),如下所示:

let myValue = myArray.pop() // myValue = 'goodbye'

要从数组开头删除一个对象,使用.shift(),如下所示:

myValue = myArray.shift() // myValue now equals 'hello'

请注意,到目前为止介绍的所有这些方法都会直接改变原始数组。.pop().shift()返回被删除的值,而不是数组本身。这种区别很重要,因为并非所有的数组方法都是这样的。让我们来看看slicesplice

myValue =  myArray.slice(0,1) // myValue equals 1, and myArray is unchanged
myValue = myArray.splice(0,1,'oh no') // myValue = 1, and myArray equals ['oh no', 'goodbye', null, true, 1, 'hello',{ 0 : 1 }]

您可以在MDN Web Docs网站上查找这两种方法的参数。为了介绍这些方法,只需知道变量上的方法的行为可以从变异变为稳定。

集合与数组密切相关,但有一些细微的差别。让我们来看看。

集合

集合是 ES6 中引入的一种复合数据类型。集合是一个删除了重复项并禁止添加重复项的数组。尝试以下代码:

const myArray = ['oh no', 'goodbye', null, true, 1, 'hello',{ 0 : 1 }]
myArray.push('goodbye')
console.log(myArray)

const mySet = new Set(myArray)
console.log(mySet)

mySet.add('goodbye')
console.log(mySet)

myArray的长度为 8,而mySet的长度为 7——即使在尝试添加'goodbye'之后也是如此。JavaScript 的集合.add()方法首先会测试确保正在添加的是唯一值。请注意new关键字和数据类型的大写;这不是创建集合的唯一方式,但很重要。在 ES5 及之前,声明新变量的常见做法是这样的,但现在除了少数情况外,这种做法被认为是遗留的。

在面试中,有一个常见的初级 JavaScript 问题,要求你对数组进行去重。您可以使用set一次性完成这个操作,而不是遍历数组并检查每个值。

虽然有许多可能的解决方案可以在不使用集合的情况下对数组进行去重,但让我们看一个使用.sort()方法的相当基本的例子。正如您可以从名称中期望的那样,这个方法将按升序对数组进行排序。如果您知道数组将包含相同数据类型的字符串或数字,则最好使用这种方法。

考虑以下数组:

const myArray = ['oh no', 'goodbye', 'hello', 'hello', 'goodbye']

我们知道去重、排序后的数组应该如下所示:

['goodbye', 'hello', 'oh no']

我们可以这样测试:

const mySet = new Set(myArray.sort())

现在,让我们尝试不使用集合。这是一种使用去重函数的方法:

const myArray = ['oh no', 'goodbye', 'hello', 'hello', 'goodbye']

function unique(a) {
 return a.sort().filter(function(item, pos, ary) {
   return !pos || item != ary[pos - 1]
 })
}

console.log(unique(myArray))

继续看一下:github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/blob/master/chapter-4/deduplicate/index.html

输出是什么?我们应该得到一个长度为 3 的数组,如下所示:

["goodbye", "hello", "oh no"]

原始方法稍微复杂一些,对吧?集合是一种更加用户友好的数组去重方式。对象是 JavaScript 中的另一种集合类型。正如承诺的那样,这里有一个更深入的介绍。

对象和 JSON

对象!对象是 JavaScript 的核心。如前所述,在《第三章》Nitty-Gritty Grammar中,几乎 JavaScript 中的所有东西,从本质上讲,都是对象。对象可能一开始会让人望而生畏,但在理论上它们很容易理解:

这是一个对象的骨架:

const myObject = { key: value }

对象是一组键/值对。它们有很多用途,特别是用来包含和组织数据。让我们来看一下《第三章》中关于 Captain Picard 的例子,Nitty-Gritty Grammar

const captain = {
  "name": "Jean-Luc Picard",
  "age": 62,
  "serialNumber": "SP 937-215",
  "command": "NCC 1701-D",
  "seniorStaff": ['Riker','Data','Worf', 'Troi']
}

正如我们所见,我们可以使用点表示法来访问对象的属性,就像这样:

captain.command // equals "NCC 1701-D"

我们还可以将其他数据类型用作值,就像captain.seniorStaff一样。

与其他所有东西一样,对象也有自己的方法。其中最方便的之一是.hasOwnProperty()

console.log(captain.hasOwnProperty('command')) // logs true

现在,让我们再次尝试数组去重,但这次让我们利用对象来创建一个哈希映射:

const myArray = ['oh no', 'goodbye', 'hello', 'hello', 'goodbye']

function unique_fast(a) {
  const seen = {};
  const out = [];
  let len = a.length;
  let j = 0;
  for (let i = 0; i < len; i++) {
    const item = a[i];
    if (seen[item] !== 1) {
      seen[item] = 1;
      out[j++] = item;
    }
  }
  return out;
}

console.log(unique_fast(myArray))

让我们来看一下:github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/blob/master/chapter-4/deduplicate/hashmap.html。现在,这种方法几乎比我们之前探讨的去重方法快了近一倍,虽然这并不是立即显而易见的。为什么呢?简而言之,对象的值可以立即在 O(1)时间内访问,而不是在 O(n)时间内遍历整个数组。如果您对大 O 符号不熟悉,这是一种计算代码复杂性的模糊方式,这里有一个很好的入门:www.topcoder.com/blog/big-o-notation-primer/

让我们将两种方法与一个长度为 24,975 的数组进行对比。

第一个实现,github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/blob/master/chapter-4/deduplicate/large.html,将在 5 到 8 毫秒之间(结果可能有所不同)。

然而,通过使用带有对象的哈希映射,我们可以将运行时间减少至少几毫秒:github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/blob/master/chapter-4/deduplicate/large_hashmap.html

现在,几毫秒可能看起来微不足道(并且不可能用肉眼区分),但想想一个需要反复运行的操作,针对相似长度的数据集。节省的时间会累积起来。

你可以查看stackoverflow.com/a/9229821/2581282了解更多关于这个问题的想法和解释。

接下来,我们将研究一些使 JavaScript...嗯,JavaScript!它的继承和类的概念与其他语言有很大不同。让我们深入了解一下。

原型继承

JavaScript 中的继承确实是它的主要优势之一。JavaScript 使用原型继承,而不是经典的基于类的继承。(专业提示:它的发音是pro-to-TYPE-al而不是pro-to-TYPICAL。)这是因为它使用对象的原型作为模板。你还记得之前我们在控制台中使用字符串和数字的方法,并发现即使在简单的数据类型上,我们也有许多可用的方法吗?嗯,我们可以做得更多。

在 JavaScript 的原型继承概念中,原型链是基本的,它告诉我们在方法方面我们可以访问什么。让我们看一下图表:

图 4.4 - 原型链

那么,这意味着什么?考虑Alice:我们可以看到这个变量是一个字符串,因为它是从String原型继承而来的。因此,翻译成代码,我们可以这样说:

const Alice = new String()
Alice.name = "Alice"
console.log(Alice.name)

我们在控制台中会得到什么?只是Alice。我们给了我们的Alice字符串对象name属性。现在,让我们来看看原型中这个神秘的sayHello()方法。如果我们执行以下操作,你认为会发生什么?

Alice.sayHello()

如果你猜到我们在sayHello()函数上会得到一个未定义的错误,那么你是正确的。我们还没有定义它。

这是我们通过修改String原型来实现的:

String.prototype.sayHello = function() {
 console.log(`My name is ${this.name}.`)
}
const Alice = new String()
Alice.name = "Alice"
Alice.sayHello()

现在,在我们的控制台中,我们将得到My name is Alice。好的,发生了什么?

通过直接修改String原型并添加一个sayHello()方法,我们可以在任何字符串上使用这个方法并访问它的属性。就像我们之前使用点表示法一样,我们可以使用this关键字来引用我们正在工作的对象的属性。因此,在我们的原型中,this.name有效并等于Alice.name

现在,你可能会想这似乎有点危险。我们正在修改一个基本数据类型,如果我们尝试在没有name属性的字符串上调用.sayHello(),我们将得到一个很大的错误。你是对的!有一种更好的方法可以做到这一点,而且仍然利用了原型继承的概念。看一下这个:

function Person(name) {
  this.name = name

  this.sayHello = function() {
    console.log(`My name is ${this.name}.`)
  }
}

const Alice = new Person('Alice')
const Bob = new Person('Bob')

Alice.sayHello()
Bob.sayHello()

正如我们所期望的,我们得到了My name is Alice.My name is Bob.。我们不需要两次定义sayHello();相反,AliceBobPerson那里继承了这个方法。效率!

现在我们要谈谈杰森。杰森是谁?不,不,我们要检查的是基于对象的数据结构称为JSON

JSON

JSON(发音为jay-sohnjason)代表JavaScript 对象表示法。如果你以前在现场看到过它,你可能知道它经常被用作方便的 API 传输格式。我们稍后会更详细地讨论 API,但现在让我们了解一下 JSON 是什么,以及它为什么有用。

让我们看看它是什么样子的。我们将使用星球大战 APISWAPI)(swapi.dev)作为一个方便的只读 API。看一下这个例子的结果:swapi.dev/api/people/1/?format=json

图 4.5 - SWAPI 人物实例

JSON 的一个很棒的地方是它相当易读,因为它没有像 XML 那样有很多节点和格式。然而,在它的原始格式中,就像前面的截图一样,它仍然是一团糟。浏览器有很好的工具可以将 JSON 解析成易读的树形结构。花点时间找一个安装到你的浏览器上,然后访问之前的 API 调用。现在,你的响应应该格式化如下截图所示:

图 4.6 - SWAPI 格式化

现在更容易阅读了。向卢克·天行者问好!

这个 API 的作者之一的设计决定是,每个结果中只包含资源的唯一数据。例如,对于homeworld,它不会明确写出“塔图因”,而是提供了一个URIU****niform Resource Identifier)用于一个planet资源。我们可以看到homeworld及其数据是键值对,就像其他对象一样,films是一个字符串数组,整个数据集是一个用大括号括起来的对象。这就是 JSON 的全部内容:格式正确的 JavaScript 对象。

现在是时候深入了解一些关于互联网如何工作的信息,以便更好地使用 JavaScript、API 和整个网络。

HTTP 动词

让我们快速看一下允许我们与 API 来回通信的 HTTP 动词:

HTTP 动词 CRUD 等效
POST 创建
GET 读取
PUT 更新/替换
PATCH 更新/修改
DELETE 删除

虽然 API 中使用的实际动词取决于 API 的设计,但这些是许多 API 今天使用的标准 REST 术语。REST代表REpresentational State Transfer,是关于如何格式化 API 的标准描述。现在,REST 或 RESTful API 并不总是要使用 JSON 进行通信 - REST 对格式是不可知的。让我们看看实际中的 API 调用。

前端的 API 调用 - Ajax

Ajax(也拼写为 AJAX)代表异步 JavaScript 和 XML。然而,现在,你更可能使用 JSON 而不是 XML,所以这个名字有点误导。现在看代码:看一下github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/blob/master/chapter-4/ajax/swapi.html。在本地打开这个链接,在你的开发者工具中,你应该看到一个 JSON 对象,如下所示:

图 4.7 - SWAPI Ajax 结果

恭喜!你已经完成了你的第一个 Ajax 调用!让我们来分解一下下面的代码:

fetch('https://swapi.co/api/people/1/')
  .then((response) => {
    return response.json()
  })
  .then((json) => {
    console.log(json)
  })

fetch是 ES6 中一个相当新的 API,它基本上取代了使用XMLHttpRequest进行 Ajax 调用的旧方法,这种语法相当简洁。也许不太明显的是.then()函数的作用,甚至它们是什么。

.then()是 Promise 的一个例子。我们现在不会详细讨论 Promise,但基本前提是建立在 JavaScript 的异步部分。基本上,Promise 说:“执行这段代码,我保证以后会提供更多的数据给你。不要在这里阻塞代码执行。”

在浏览器中本地打开github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/blob/master/chapter-4/ajax/swapi-2.html。你应该会看到“加载数据…”一闪而过,然后显示 JSON。您可以使用浏览器的开发者工具来限制您的互联网连接,以查看它的运行情况。

以下是 JavaScript 代码:

fetch('https://swapi.co/api/people/1/')
  .then((response) => {
    return response.json()
  })
  .then((json) => {
    document.querySelector('#main').innerHTML = JSON.stringify(json)
  })
document.querySelector('#headline').innerHTML = "Luke Skywalker"

不要太担心document.querySelector行——我们将在第六章中详细介绍这些内容,文档对象模型(DOM)。现在,只需了解它们用于在 HTML 文档中放置信息。让我们使用开发者工具将连接节流到慢 3G 或类似的速度。当我们刷新时,我们应该看到“等待标题…”的闪烁,然后是“卢克·天行者”,接着是“加载数据…”,然后,几秒钟后,JSON 作为文本。

那么,这是如何工作的呢?将代码行从“等待标题…”更改为“卢克·天行者”是在 Ajax 调用之后。那么为什么标题在数据部分之前就改变了呢?答案是Promise

使用fetch,我们确定我们本质上使用的是异步数据,因此.then()语句告诉我们在承诺语句解析之后我们可以做什么。它使程序可以继续进行程序的其他部分。事实上,我们可以进行多次 fetch 调用,这些调用可能在不同的时间返回,但仍然不会阻止用户使用程序。异步性是使用现代 JavaScript 时的一个基本概念,所以请花时间理解它。

接下来,让我们通过实际使用API 来获得一些经验!现在是真正动手并与不仅是本地代码而且外部代码互动的时候了。

SWAPI 实验室

让我们通过这个 API 进行一些实践。我们现在要做的事情可能有些不够优雅,但它将向我们展示如何利用异步行为来获得优势。

您应该期望看到类似于这样的东西:

图 4.8 – SWAPI Promises result

请记住,由于我们使用了 Promise 并且必须迭代films数组,电影的顺序可能会有所不同。如果愿意,您可以选择按电影编号排序它们。

这个实验室将需要嵌套的 Promise 和一些我们尚未涵盖的语法,所以如果你想做这个实验,请给自己足够的时间来实验:

与任何实验室一样,请记住解决方案代码可能与您的代码不匹配,但它是作为思考过程的资源。

总结

数据是每个程序的核心,你的 JavaScript 程序也不例外:

  • JavaScript 是弱类型的,这意味着变量类型可以根据需要变化。

  • 布尔值是简单的真/假语句。

  • 数字之间没有区分整数、浮点数或其他类型的数字。

  • 数组和集合可以包含大量数据,并且使我们组织数据更容易。

  • 对象是高效存储数据的键值对。

  • API 调用实际上并不可怕!

我们仔细研究了数据类型、API 和 JSON。我们发现 JavaScript 中的数据非常灵活,甚至可以操作对象本身的原型。通过查看 JSON 和 API,我们成功地使用fetch()执行了我们的第一个 API 调用。

在下一章中,我们将进一步深入编写 JavaScript,制作一个更有趣的应用程序,并了解如何构建一个应用程序的细节!

问题

对于以下问题,请选择正确的选项:

  1. JavaScript 本质上是:

  2. 同步的

  3. 异步的

  4. 两者都是

  5. fetch() 调用返回:

  6. 然后

  7. 下一个

  8. 最后

  9. 承诺

  10. 通过原型继承,我们可以(全选):

  11. 向基本数据类型添加方法。

  12. 从基本数据类型中减去方法。

  13. 重命名我们的数据类型。

  14. 将我们的数据转换为另一种格式。

let x = !!1
console.log(x)
  1. 从上面的代码中,预期输出是什么?

  2. 1

  3. false

  4. 0

  5. true

const Officer = function(name, rank, posting) {
  this.name = name
  this.rank = rank
  this.posting = posting
  this.sayHello = () => {
    console.log(this.name)
  }
}

const Riker = new Officer("Will Riker", "Commander", "U.S.S. Enterprise")
  1. 在上面的代码中,输出 Will Riker 的最佳方法是什么?

  2. Riker.sayHello()

  3. console.log(Riker.name)

  4. console.log(Riker.this.name)

  5. Officer.Riker.name()

进一步阅读

有关静态与动态类型语言的更多信息,您可以参考 android.jlelse.eu/magic-lies-here-statically-typed-vs-dynamically-typed-languages-d151c7f95e2b

要了解更多关于匈牙利命名法的信息,请参考 frontstuff.io/write-more-understandable-code-with-hungarian-notation

第二部分 - 在前端使用 JavaScript

是时候编写代码了!让我们把我们对 JavaScript 的理论知识付诸实践,学习如何在页面上实际使用它。

在本节中,我们将涵盖以下章节:

  • 第五章,《你好,世界!以及更多:你的第一个应用程序》

  • 第六章,《文档对象模型(DOM)》

  • 第七章,《事件,事件驱动设计和 API》

  • 第八章,《使用框架和库》

  • 第九章,《解读错误消息和性能泄漏》

  • 第十章,《JavaScript,前端的统治者》

第五章:Hello World 以及更多:你的第一个应用

啊,那个古老的“Hello World!”脚本。虽然非常简单,但它是对任何语言的一个很好的第一次测试。不过,让我们做得更多一点,不仅仅是说 hello;让我们用几个小应用程序来动手。毕竟,编程不仅仅是理论。我们将看一下编码挑战中提出的一个常见问题,以及*我们的程序是如何工作的。

本章将涵盖以下主题:

  • 控制台和警报消息的 I/O

  • 在函数中处理输入

  • 使用对象作为数据存储

  • 理解作用域

技术要求

github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers克隆或下载本书的存储库,并准备查看Chapter-5的材料。

控制台和警报消息的 I/O

到目前为止,我们已经看到了 JavaScript 如何向用户输出信息。考虑以下代码:

const Officer = function(name, rank, posting) {
  this.name = name
  this.rank = rank
  this.posting = posting
  this.sayHello = () => {
    console.log(this.name)
  }
}

const Riker = new Officer("Will Riker", "Commander", "U.S.S. Enterprise")

现在,如果我们执行Riker.sayHello(),我们将在控制台中看到以下内容:

图 5.1 - 控制台输出

在存储库的chapter-5目录中自己看一看:github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/blob/master/chapter-5/alerts-and-prompts/console.html

好的,太好了。我们有一些控制台输出,但这不是一个很有效的输出方式,因为用户通常不会打开控制台。有一种方便的输出方法,虽然不适用于完整的网络应用程序,但对于测试和调试目的很有用:alert()。以下是一个例子:

const Officer = function(name, rank, posting) {
  this.name = name
  this.rank = rank
  this.posting = posting
  this.sayHello = () => {
    alert(this.name)
  }
}

const Riker = new Officer("Will Riker", "Commander", "U.S.S. Enterprise")

Riker.sayHello()

尝试从github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/blob/master/chapter-5/alerts-and-prompts/alert.html运行上述代码。你看到了什么?

图 5.2 - 警报消息

太棒了!我们有一个那种你可能在网上见过的烦人的小弹出框。当使用不当时,它们可能很烦人,但在适当的时候,它们可以非常有用。

让我们看看一个类似的东西,它将从用户那里得到输入(github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/blob/master/chapter-5/alerts-and-prompts/prompt.html):

const Officer = function(name, rank, posting) {
  this.name = name
  this.rank = rank
  this.posting = posting

  this.ask = () => {
    const values = ['name','rank','posting']

    let answer = prompt("What would you like to know about this officer?")
    answer = answer.toLowerCase()

    if (values.indexOf(answer) < 0) {
      alert('Value not found')
    } else {
      alert(this[answer])
    }
  }
}

const Riker = new Officer("Will Riker", "Commander", "U.S.S. Enterprise")

Riker.ask()

当你加载页面时,你会看到一个带有输入字段的弹出框。输入namerankposting,然后查看结果。如果刷新并输入除这些选项之外的内容,你应该会得到一个值未找到的响应。

啊!但让我们也看看以下一行:

answer = answer.toLowerCase()

由于这是前端 JavaScript,我们不知道用户会输入什么,所以我们应该考虑轻微的格式错误。数据净化是另一个话题,所以现在,让我们同意我们可以将整个字符串转换为小写以匹配预期的值。

到目前为止,一切都很好。现在,让我们看看answer是如何使用的。

在函数中处理输入

如果我们看一下前面的对象,我们会看到以下内容:

if (values.indexOf(answer) < 0) {
  alert('Value not found')
} else {
  alert(this[answer])
}
...

由于我们正在处理任意输入,我们首先要做的是检查我们的答案数组,看看所请求的属性是否存在。如果不存在,就会弹出一个简单的错误消息。如果找到了,那么我们可以弹出该值。如果你还记得第三章中的内容,Nitty-Gritty Grammar,对象属性可以通过点表示法括号表示法来访问。在这种情况下,我们正在使用一个变量作为键,所以我们不能这样做,因为它会被解释为键。因此,我们使用括号表示法来访问正确的对象值。

练习-斐波那契数列

对于这个练习,构建一个函数来接受一个数字。最终结果应该是斐波那契数列(en.wikipedia.org/wiki/Fibonacci_number)中到指定数字的数字之和。序列的前几个数字是[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]。每个数字都是前两个数字的和;例如,f[6] = 13,因为f[5] = 8f[4] = 5,因此f[6] = 8+5 = 13。你可以在github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-5/fibonacci/starter-code找到起始代码。不要太担心计算数字的最有效算法;只需确保不要硬编码值,而是依赖输入变量和公式。

斐波那契数列解决方案

让我们解剖一个可能的解决方案:

function fibonacci(num) {
  let a = 1, b = 0, temp

  while (num >= 0) {
    temp = a
    a = a + b
    b = temp
    num--
  }

  return b
}

let response = prompt("How many numbers?")
alert(`The Fibonacci number is ${fibonacci(response)}`)

让我们先看看函数外的行。我们所做的只是简单地询问用户想要计算到序列的哪个点。然后,response变量被传递到alert()语句作为fibonacci的参数,fibonacci接受num作为参数。从那时起,while()循环在num上执行,将num递减,而b的值则根据算法递增,最后返回到我们的警报消息中。

就是这样了!现在,让我们尝试一个变体,因为我们永远不知道我们的用户会输入什么。如果他们输入的是一个字符串而不是一个数字会发生什么?我们应该适应这一点,至少呈现一个错误消息。

让我们来看看这个解决方案:

function fibonacci(num) {
  let a = 1, b = 0, temp

  while (num >= 0) {
    temp = a
    a = a + b
    b = temp
    num--
  }

  return b
}

let response = prompt("How many numbers?")

while (typeof(parseInt(response)) !== "number" || !Number.isInteger(parseFloat(response))) {
  response = prompt("Please enter an integer:")
}

alert(`The Fibonacci number is ${fibonacci(response)}`)

你可以在 GitHub 上找到解决方案,网址是github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-5/fibonacci/solution-code-number-check

如果我们进入while()循环,我们会看到我们的类型匹配魔法。首先,由于response本质上是一个字符串,我们决定不要相信类型强制转换,这就是我们之前的解决方案在做的事情。我们使用parseInt()方法将response直接转换为一个数字。太好了!但这并不能确保我们的用户一开始输入的是一个整数。记住,JavaScript 没有intfloat的概念,所以我们必须进行一些操作,以确保我们的输入是一个整数,方法是使用Number.isInteger方法的否定。这确保了我们的输入是一个有效的整数。

在更深入地使用 JSON 之前,让我们看看如何将对象用作数据存储。

使用对象作为数据存储

这是一个我在编程面试中见过的有趣问题,以及解决它的最有效方法。它具有昂贵的输入时间,但具有 O(1)的检索时间,这通常被认为是算法复杂性成功的度量标准,当你可以预期读取的次数比写入的次数多时。

练习-乘法

考虑以下代码(github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-5/matrix/starter-code):

const a = [1, 3, 5, 7, 9]
const b = [2, 5, 7, 9, 14]

// compute the products of each permutation for efficient retrieval

const products = { }

// ...

const getProducts = function(a,b) {
  // make an efficient means of retrieval
  // ...
}

// bonus: get an arbitrary key/value pair. If nonexistent, compute it and store it.

那么,在使用对象的范例中,解决方案是什么?让我们来看看,分解一下,然后逆向工程我们使用对象作为数据存储的用法(剧透警告:你听说过 NoSQL 吗?)。

乘法解决方案

在我们开始之前,让我们将问题分解为两个步骤:给定两个数组,我们首先计算数组中每个项目的乘积,并将它们存储在一个对象中。然后,我们将编写一个函数来检索数组中给定两个数字的乘积。让我们来看看。

第一步 - 计算和存储

首先,我们的makeProducts函数将以两个数组作为参数。使用数组的.forEach()方法,我们将遍历第一个数组中的每个项目,将值命名为multiplicant

const makeProducts = function(array1, array2) {
  array1.forEach( (multiplicant) => {
    if (!products[multiplicant]) {
      products[multiplicant] = { }
    }
    array2.forEach( (multiplier) => {
      if (!products[multiplier]) {
        products[multiplier] = { }
      }
      products[multiplicant][multiplier] = multiplicant * multiplier
      products[multiplier][multiplicant] = products[multiplicant]
       [multiplier]
    })
  })
}

现在,我们的最终目标是有一个对象告诉我们“xy的乘积是z”。如果我们将这个抽象成使用对象作为数据存储,我们可以得到这样的结构:

{
  x: {
    y: z
  },
  y: {
    x: z
  }
}

在这个对象结构中,我们只需要指定x.y来检索我们的计算,它将是z。我们也不想假设一个顺序,所以我们也做相反的:y.z

那么,我们如何构建这个数据对象呢?记住,如果我们不是调用文字键,我们可以使用方括号表示法与对象;在这里,我们使用一个变量:

if (!products[multiplicant]) {
    products[multiplicant] = { }
}

我们的第一步是检查我们的对象中是否存在multiplicant键(在我们之前的理论讨论中是x)。如果不存在,将其设置为一个新对象。

现在,在我们的内部循环中,让我们对乘数做同样的事情:

if (!products[multiplier]) {
    products[multiplier] = { }
}

太好了!我们已经为xy都设置了键。现在,我们只需计算乘积并将其存储在两个位置,如下所示:

products[multiplicant][multiplier] = multiplicant * multiplier
products[multiplier][multiplicant] = products[multiplicant][multiplier]

注意决定将反向键值分配给正向键的值,而不是重新计算乘积。为什么我们要这样做?事实上,为什么我们要为一个简单的数学运算费这么大劲?原因是:如果我们不是做简单的乘法,而是做一个远远更复杂的计算呢?也许一个如此复杂以至于需要一秒或更长时间才能返回的计算?现在我们可以看到,我们希望减少我们的时间,这样我们只需要做一次计算,然后可以重复读取它以获得最佳性能。

构建了这个函数之后,我们将在我们的数组上执行它:

makeProducts(a,b)

这很容易调用!

第二步 - 检索

现在,让我们编写我们的检索函数:

const getProducts = function(a,b) {
  // make an efficient means of retrieval
  if (products[a]) {
    return products[a][b] || null
  }
  return null
}

如果我们看这个逻辑,首先我们确保第一个键存在。如果存在,我们返回x.y或者如果y不存在则返回null。对象很挑剔,如果你试图引用一个不存在的,你会得到一个错误。因此,我们首先需要存在性检查我们的键。如果键存在并且键/值对存在,返回计算出的值;否则,我们返回null。注意return products[a][b] || null的短路:这是一种有效的方式来表示“返回值或其他东西”。如果products[a][b]不存在,它将响应一个假值,然后OR操作将接管。高效!

看一下奖励问题的答案的解决方案代码。存在检查和计算的相同原则适用。

理解范围

在我们构建一个更大的应用程序之前,让我们讨论一下范围。简而言之,范围定义了我们何时何地可以使用变量或函数。JavaScript 中的范围被分为两个离散的类别:局部和全局。如果我们看看我们之前的乘法程序,我们可以看到有三个变量在任何函数之外;它们挂在我们程序的根级别:

01: const a = [1, 3, 5, 7, 9]
02: const b = [2, 5, 7, 9, 14]
03: 
04: // compute the products of each permutation for efficient retrieval
05: 
06: const products = { }
07: 
08: const makeProducts = function(array1, array2) {
09:     array1.forEach( (multiplicant) => {
10:         if (!products[multiplicant]) {
11:             products[multiplicant] = { }
12:         }
13:         array2.forEach( (multiplier) => {
14:             if (!products[multiplier]) {
15:                 products[multiplier] = { }
16:             }
17:             products[multiplicant][multiplier] = multiplicant * 
                 multiplier
18:             products[multiplier][multiplicant] = products[multiplicant]
                 [multiplier]
19:         })
20:     })
21: }
22: 
23: const getProducts = function(a,b) {
24:     // make an efficient means of retrieval
25:     if (products[a]) {
26:         return products[a][b] || null
27:     }
28:     return null
29: }
30: 
31: makeProducts(a,b)

问题中的变量分别在第 1、2 和 6 行:abproducts。太好了!这意味着我们可以在任何地方使用它们,比如第 10、11、14、15 行,以及更多地方,只要我们在它们被定义之后使用它们。现在,如果我们仔细看,我们还会看到我们在全局作用域中有一些函数:makeProductsgetProducts。同样,只要它们已经被定义,我们可以在任何地方使用它们。

好的,太好了——这是有道理的,因为 JavaScript 是从上到下读取的。但等等!如果你还记得第三章中的内容,Nitty-Gritty Grammar,函数声明被提升到顶部,因此可以在任何地方使用。

让我们重构我们的程序,利用提升和抽象我们的数学来成为理论上的长时间运行的过程。我们还将使用Promises作为一个很好的概念介绍。在我们深入研究之前,阅读使用Promises可能会有所帮助:developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises

github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-5/matrix-refactored中查看index.js。我们将一步一步地分解这个过程。

首先,在浏览器中打开index.html。确保你的控制台是打开的。2 秒后,你会在控制台中看到一个简单的消息:9 x 2 = 18。如果你看一下index.js中的第 44 行,你会看到它在使用getProducts来计算a[4]b[0]的乘积,它们分别是92。太棒了!到目前为止,我们的功能与添加了一个感知延迟是一样的。

让我们从头开始:

1: const a = [1, 3, 5, 7, 9]
2: const b = [2, 5, 7, 9, 14]
3: 
4: // compute the products of each permutation for efficient retrieval
5: 
6: const products = {}
7: 

到目前为止,我们有相同的代码。那么我们的makeProducts函数呢?

08: const makeProducts = async function(array1, array2) {
09:     const promises = []
10:     array1.forEach((multiplicant) => {
11:         if (!products[multiplicant]) {
12:             products[multiplicant] = {}
13:         }
14:         array2.forEach(async (multiplier) => {
15:             if (!products[multiplier]) {
16:                 products[multiplier] = {}
17:             }
18: 
19:             promises.push(new Promise(resolve => 
                 resolve(calculation(multiplicant, multiplier))))
20:             promises[promises.length - 1].then((val) => {
21:                 products[multiplicant][multiplier] = products[
                      multiplier][multiplicant] = val
22:             })
23:         })
24:     })
25:     return promises
26: }

嗯。好的,我们有一些相同的部分,但也有一些新的部分。首先,让我们考虑async。当与一个函数一起使用时,这个关键字意味着这个函数的使用者应该期望异步行为,而不是 JavaScript 通常的自上而下的行为。在我们深入研究新的 19-21 行之前,让我们看一下我们的calculation函数为什么是异步的:

37: async function calculation(value1, value2) {
38:     await new Promise(resolve => setTimeout(resolve, 2000))
39:     return value1 * value2
40: }

这里又是第 37 行的async,现在我们在第 38 行看到一个新的关键字:awaitasyncawait是指定我们可以异步工作的一种方式:在第 38 行,我们指定我们在继续之前正在等待这个promise解析。我们的promise在做什么?嗯,事实证明,并不多!它只是使用setTimeout延迟 2,000 毫秒。这个延迟旨在模拟一个长时间运行的过程,比如一个 Ajax 调用或者一个需要 2 秒才能完成的复杂过程(甚至是一个不确定的时间量)。

好的,太好了。到目前为止,我们基本上是在欺骗程序,让它期望在继续之前有 2 秒的延迟。让我们看看第 9 行:一个名为promises的新数组。现在,回到我们关于作用域的想法,你可以注意到我们的数组是在makeProducts内部定义的。这意味着这个变量只存在于函数的局部作用域内。与 products 相反,我们无法从这个函数的外部访问 promises。没关系——我们真的不需要。事实上,最好的做法是尽量减少在全局作用域中定义的变量数量。

现在,让我们看一下第 19 行,看起来更加微妙:

promises.push(new Promise(resolve => resolve(calculation(multiplicant, multiplier))))

如果我们分解一下,首先我们看到了一些熟悉的东西:我们正在将一些东西推到我们的promises数组中。我们正在推送的是一个新的Promise,类似于第 38 行,但在这种情况下,我们不是在行内等待它,而是只是说“用calculation()的值解析这个promise——无论何时发生”。到目前为止,一切都很好。下一部分呢?

20: promises[promises.length - 1].then((val) => {
21:     products[multiplicant][multiplier] = products[multiplier]
         [multiplicant] = val
22: })

现在,一些语法糖就出现了:现在我们在promises数组中有了我们的promise,我们可以使用[promises.length - 1]来访问它,因为length返回的是从1开始的完整长度。.then()子句是我们的魔法:它表示一旦promise完成,就对结果进行处理。在这种情况下,我们的处理是将val分配给产品的两个变体。最后,在第 25 行,我们返回promises数组。

我们的getProducts函数一点都没有改变!我们的检索函数的复杂性保持不变:高效。

这个怎么样?

42: makeProducts(a,b).then((arrOfPromises) => {
43:     Promise.all(arrOfPromises).then(() => {
44:         console.log(`${a[4]} x ${b[0]} = ${getProducts(a[4], b[0])}`)
             // 18
45:     })
46: })

我们之前见过.then,所以它的参数是makeProducts的返回值,即promises数组。然后,我们可以在.then之前使用.all()来有效地表示“当arrOfPromises中的所有promises都已解决时,然后执行下一个函数”。下一个函数是记录我们的答案。你可以在第 44 行之后添加额外的产品检查;它们将与第 44 行同时返回,因为我们的“计算”中的延迟已经发生。

作用域链和作用域树

进一步深入作用域,我们有作用域链作用域树的概念。让我们考虑以下例子:

function someFunc() {
  let outerVar = 1;
  function zip() {
    let innerVar = 2;
  }
}

someFunc有哪些变量可以访问?zip有哪些变量可以访问?如果你猜到someFunc可以访问outerVar,但zip可以访问innerVarouterVar,那么你是正确的。这是因为这两个变量存在于zip的作用域链中,但只有outerVar存在于someFunc的作用域中。清楚了吗?太好了。让我们看一些图表。

看一下以下代码:

function someFunc() {
  function zip() {
    function foo() {
    }
  }
  function quux() {
  }
}

我们可以从上到下构建一个函数的作用域树的图表:

图 5.3 - 作用域树

这告诉我们什么?quux似乎独立存在于someFunc内部的小世界中。它可以访问someFunc的变量,但不能访问zipfoo。我们也可以通过作用域链从下到上来理解它:

图 5.4 - 作用域链

在这个例子中,我们看一下foo可以访问什么。从下到上,我们可以看到它与代码其他部分的关系。

闭包

现在,我们将进入闭包,这显然是 JavaScript 中一个可怕的话题。然而,基本概念是可以理解的:一个闭包就是一个函数,它在另一个函数内部,可以访问其父函数的作用域链。在这种情况下,它有三个作用域链:自己的作用域链,其中定义了自己的变量;全局的作用域链,其中可以访问全局作用域中的所有变量;以及父函数的作用域。

这是一个我们将解剖的例子:

function someFunc() {
  let bar = 1;

  function zip() {
    alert(bar); // 1
    let beep = 2;

    function foo() {
      alert(bar); // 1
      alert(beep); // 2
    }
  }
}

哪些变量可以被哪些函数访问?这里有一个图表:

图 5.5 - 闭包

从下到上,foo可以访问beepbar,而zip只能访问bar。到目前为止,一切都好,对吧?闭包只是一种描述每个嵌套函数可用作用域的方式。它们本身并不可怕。

一个闭包在实践中的基本例子

看一下以下函数:

  function sayHello(name) {
    const sayAlert = function() {
      alert(greeting)
    }

    let greeting = `Hello ${name}`
    return sayAlert
  }

  sayHello('Alice')()
  alert(greeting)

首先,让我们看看这个有趣的构造:sayHello('Alice')()。由于我们的sayAlert()函数是sayHello的返回值,我们首先用一个括号对调用sayHello,并带上我们的参数,然后用第二对括号调用它的返回值(sayAlert函数)。注意greetingsayHello的作用域内,当我们调用我们的函数时,我们会得到一个 Hello Alice 的警报。然而,如果我们尝试单独警报greeting,我们会得到一个错误。只有sayAlert可以访问greeting。同样,如果我们试图从函数外部访问name,我们会得到一个错误。

摘要

为了使我们的程序有用,它们通常依赖于用户或其他函数的输入。通过搭建我们的程序以使其灵活,我们还需要牢记作用域的概念:何时何地可以使用函数或变量。我们还看了一下对象如何用于有效存储数据以便检索。

让我们不要忘记闭包,这个看似复杂的概念实际上只是一种描述作用域的方式。

在下一章中,随着我们开始使用文档对象模型DOM)并操纵页面上的信息,而不仅仅是与警报和控制台交互,我们将更多地探索前端。

问题

考虑以下代码:

function someFunc() {
  let bar = 1;

  function zip() {
    alert(bar); // 1
    let beep = 2;

    function foo() {
      alert(bar); // 1
      alert(beep); // 2
    }
  }

  return zip
}

function sayHello(name) {
  const sayAlert = function() {
    alert(greeting)
  }

  const sayZip = function() {
    someFunc.zip()
  }

  let greeting = `Hello ${name}`
  return sayAlert
}
  1. 如何获得警报 Hello Bob?

  2. sayHello()('Bob')

  3. sayHello('Bob')()

  4. sayHello('Bob')

  5. someFunc()(sayHello('Bob'))

  6. 在前面的代码中,alert(greeting)会做什么?

  7. 警报问候语。

  8. 警报 你好 Alice。

  9. 抛出错误。

  10. 以上都不是。

  11. 我们如何获得警报消息 1?

  12. someFunc()()

  13. sayHello().sayZip()

  14. alert(someFunc.bar)

  15. sayZip()

  16. 我们如何获得警报消息 2?

  17. someFunc().foo().

  18. someFunc()().beep

  19. 我们不能,因为它不在作用域内。

  20. 我们不能,因为它没有定义。

  21. 我们如何将someFunc更改为警报 1 1 2?

  22. 我们不能。

  23. return zip后添加return foo

  24. return zip更改为return foo

  25. foo声明后添加return foo

  26. 给定前面问题的正确解决方案,我们如何实际获得三个警报 1、1、2?

  27. someFunc()()()

  28. someFunc()().foo()

  29. someFunc.foo()

  30. alert(someFunc)

进一步阅读

第六章:文档对象模型(DOM)

文档对象模型DOM)是浏览器暴露给 JavaScript 的 API,允许 JavaScript 与 HTML 和间接与 CSS 进行通信。由于 JavaScript 的主要能力之一是动态更改页面上的内容,我们应该知道如何做到这一点。这就是 DOM 的作用。

在本章中,我们将学习如何使用这个强大的 API 来读取和更改页面上的内容。我相信你已经看到过不需要重新加载页面就可以更改内容的网站。这些程序使用DOM 操作,我们将学习如何使用它。

本章将涵盖以下主题:

  • 选择器

  • 属性

  • 操作

技术要求

确保在Chapter-6目录中有github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers 存储库方便使用。

使用选择器

到目前为止,我们只使用了console.log和警报和提示来输入和输出信息。虽然这些方法对于测试很有用,但并不是你在日常生活中会使用的。我们使用的大多数 Web 应用程序,从搜索到电子邮件,都使用 DOM 与用户交互以获取输入和显示信息。让我们看一个小例子:github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-6/hello

如果你在浏览器中打开 HTML,我们会看到一个非常简单的页面:

图 6.1 我们的基本页面

如果我们点击按钮,我们不会得到警报或控制台消息,而是会看到这个:

图 6.2 我们点击后页面的响应!

耶!这是我们第一个DOM 操作的实例。

解释 DOM 操作

让我们看看支持这个惊人示例的 JavaScript:

document.querySelector('button').addEventListener('click', (e) => {
 document.querySelector('p').innerHTML = `Hello! It is currently ${
  new Date()}.`
})

首先要注意的是,我们正在操作document对象。document是 JavaScript 对浏览器页面的概念。记得我提到过 DOM 是浏览器暴露的 API 吗?这是你访问 DOM 的方式:document

在我们分析 JavaScript 之前,让我们看看 DOM 和 HTML 有什么不同。这是我们页面的 HTML:

<!DOCTYPE html>
<html lang="en" dir="ltr">

<head>
  <meta charset="utf-8">
  <title>Example</title>
</head>

<body>
  <p></p>
  <button>Click me!</button>
  <script src="index.js"></script>
</body>

</html>

如果我们现在使用控制台来检查元素而不是控制台,我们会看到这个:

图 6.3 我们页面的 DOM

如果你仔细观察并将这个截图与前面的 HTML 进行比较,你不会真的找到任何区别。然而,现在点击按钮,看看<p>标签会发生什么:

图 6.4 点击按钮后

啊!现在我们看到 HTML 和 DOM 之间的区别:在段落标签内添加了文本。如果我们重新加载页面,文本就会消失,我们又回到了起点。所以,我们看到的是什么都没有在磁盘上改变,只是在内存中改变。DOM 只存在于内存中。你可以在元素视图中通过更改值甚至删除整个节点来进行实验。节点是 DOM 对 HTML 标签的反映。你可能会听到节点标签互换使用,但在使用 JavaScript 时,使用节点是一个好习惯,以保持与 JavaScript 的命名一致,我们稍后会看到。

回到我们的 JavaScript。到目前为止,我们已经讨论了document,它是 DOM 对 HTML 的内存解释。我们正在使用的document方法是一个强大的方法:.querySelector()。这个方法返回与我们传递给方法的参数的第一个匹配项。在这种情况下,我们要求button。由于页面上只有一个按钮,我们可以简单地使用标签名称。但是,querySelector比这更强大,因为我们也可以基于 CSS 选择器进行选择。例如,假设我们的按钮上有一个类,就像这样:

<button class="clickme">Click me!</button>

然后我们可以这样访问按钮:

document.querySelector('.clickme')

注意clickme前面的“.”,就像 CSS 选择器一样。同样,当访问具有 ID 的元素时,您将使用“#”。

现在我们已经可以访问我们的按钮,我们想对它做一些事情。在这种情况下,一些是指在点击按钮时采取行动。我们通过添加事件监听器来实现这一点。我们将在第七章中更深入地了解事件监听器,所以现在让我们只是浅尝辄止。

这是事件监听器的结构:

图 6.5 事件监听器结构

首先,我们的事件目标是我们要监听的节点;在这种情况下,我们的目标是按钮。然后我们使用.addEventListener()方法,并将click事件分配为我们要监听的事件。我们事件监听器的第二个参数是一个称为事件处理程序的函数。我们可以将实际的事件对象传递给我们的处理程序。事件处理程序通常不必是匿名的,但这是常见的做法,除非您需要为多个事件类型重复使用功能。我们的处理程序再次使用querySelector来定位p节点,并将其innerHTML属性设置为包含我们日期的字符串。

关于节点属性:节点的属性是 HTML 元素属性在 DOM 中的内存表示。这意味着有很多属性:classNameidinnerHTML,只是举几个例子;当我们到达属性部分时,我们将更深入地了解它们。因此,这些代码行告诉浏览器:“嘿,当点击这个按钮时,将p标签的内容更改为这个字符串。”

现在我们已经俯视了这个问题,让我们深入研究涉及 DOM 操作的每个部分。

使用选择器

让我们考虑一个更复杂的页面。我们将打开一个示例页面,并使用为您提供的一些元素:

  1. 在浏览器中打开github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-6/animals中的index.html

图 6.6 动物页面

  1. 如果您悬停在橙色按钮上,它将变为青绿色,并且当您单击它时,页面顶部的黑色框将显示动物:

图 6.7 选择的动物

  1. 花一分钟玩一下页面,观察它的行为。还要尝试悬停在照片上;会发生什么?

现在让我们来看看 JavaScript。再次,它非常简单,但是我们的故事中有一些新的字符:

01: const images = {
02:   'path': 'images/',
03:   'dog': 'dog.jpg',
04:   'cat': 'cat.jpg',
05:   'elephant': 'elephant.jpg',
06:   'horse': 'horse.jpg',
07:   'panda': 'panda.jpg',
08:   'rabbit': 'rabbit.jpg'
09: }
10: 
11: const buttons = document.querySelectorAll('.flex-item');
12: 
13: buttons.forEach((button) => {
14:   button.addEventListener('click', (e) => {
15:     document.querySelector('img').src = 
         `${images.path}${images[e.target.id]}`
16:   })
17: })
18: 
19: document.querySelector('#image').addEventListener('mouseover', (e) => {
20:   alert(`My favorite picture is ${e.target.src}`)
21: })

第 1-9 行包含一个数据存储对象。太棒了!我们在第五章中已经介绍了这种用法,你的第一个应用程序:你好,世界!以及更多

第 11 行介绍了使用选择器的一种新方法:.querySelectorAll()。如前所述,当我们使用.querySelector()时,我们会得到与我们查询匹配的第一个项目。这种方法将返回所有匹配节点的数组。然后,我们可以在第 13 行对它们进行迭代,为每个节点添加一个点击处理程序。在第 15 行,我们定义了我们事件处理程序中的发生了什么:将唯一img节点的源设置为来自我们数据对象的路径和图像源的连接。

但等等!e.target是什么?我们将在第七章 事件、事件驱动设计和 API中深入探讨事件,但现在只需要知道e.target事件目标的 DOM 节点。因此,在这个例子中,我们正在遍历所有.flex-item类的 DOM 节点。在每个节点上,我们正在分配一个事件处理程序,因此e.target等于 DOM 节点,e.target.id等于其id的 HTML 属性。

太棒了。让我们看看第 19 行,我们正在做类似的事情,但这次使用 CSS 选择器id——image。看一下 HTML:

 <div class="flex-header"><img id="image"/></div>

我们看到标签上有一个image的 ID,这意味着我们的 DOM 节点也会有这个 ID。现在,当我们移动(或悬停)在图像上时,我们将收到一个警报消息,说明图像文件的本地路径。

如果你对 CSS 不太熟悉,现在你可能会问自己:但是用 JavaScript 把橙色框变成蓝绿色的代码在哪里?哈!这是个陷阱问题!让我们看一下style.css文件中的 45-48 行:

.flex-item:hover {
  cursor: pointer;
  background-color: turquoise;
}

如果你注意到了项目上的:hover伪类,我们可以看到改变光标从箭头到手的 CSS 规则(在大多数用户界面中表示可点击性),以及背景颜色的改变。惊喜!

这不是一本关于 CSS 的书;相反,我们将尽量避免过多的样式依赖。然而,重要的是要注意,通常 CSS 允许我们对 HTML 元素的一些表现方面进行更改。但是我们为什么要在意呢?毕竟,我们正在写JavaScript。答案很简单:计算开销。通过 JavaScript 修改元素比通过 CSS 更昂贵(也就是说,需要更多的处理能力)。如果你正在操作不需要逻辑的 CSS 属性,请尽可能使用 CSS。但是,如果你需要逻辑(比如在我们的例子中将变量拼接到显示图像中),那么 JavaScript 是正确的选择。

使用其他选择器

重要的是要注意,在 ES6 和 HTML5 的一部分之前,querySelectorquerySelectorAll被标准化之前,有其他更常见的选择器,你肯定会在实际中遇到它们。其中一些包括getElementByIdgetElementsByClassNamegetElementsByTagName。现在使用querySelector的变体被认为是标准做法,但是和所有 JavaScript 一样,有一个警告:从技术上讲,querySelector方法比getElement风格的方法稍微昂贵一点。通常情况下,与querySelector方法的强大和灵活性相比,这种开销是可以忽略的,但在处理大页面时,这是需要记在心里的事情。

现在,让我们看一看在选择了元素之后我们可以改变什么。这些是元素的属性

属性

我们已经处理了一些属性:节点的innerHTML,图像的src和节点的id。我们有大量可用的属性,所以让我们来看看 CSS 是如何与 JavaScript 结合的。

光是为了论证,让我们把我们的动物程序改成使用 JavaScript 来改变目标的背景颜色,而不是 CSS(github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-6/animals-2):

const images = {
  'path': 'images/',
  'dog': 'dog.jpg',
  'cat': 'cat.jpg',
  'elephant': 'elephant.jpg',
  'horse': 'horse.jpg',
  'panda': 'panda.jpg',
  'rabbit': 'rabbit.jpg'
}

const buttons = document.querySelectorAll('.flex-item');

buttons.forEach((button) => {
  button.addEventListener('mouseover', (e) => {
    e.target.style.backgroundColor = 'turquoise'
  })
  button.addEventListener('click', (e) => {
    document.querySelector('img').src = 
     `${images.path}${images[e.target.id]}`
  })
})

document.querySelector('#image').addEventListener('mouseover', (e) => {
  alert(`My favorite picture is ${e.target.src}`)
})

如果我们检查我们的 mouseover 处理程序,我们可以注意到两件事:

  • 事件的名称是mouseover,而不是hover。稍后再详细讨论。

  • 我们正在修改目标的样式属性,但名称是backgroundColor,而不是 CSS 中的background-color

CSS 中属性的驼峰命名规则在 JavaScript 中也是标准的。在实践中,这可能看起来有点违反直觉,因为你不必使用括号表示法和引号来处理属性名称中的连字符(这将被解释为无效的减法语句)。

然而,现在让我们运行程序并悬停在所有框上。你看到颜色从一种颜色变成另一种颜色了吗,就像这样吗?

图 6.8 所有的框都改变了!

是的,如果你猜到我们没有包括一个“重置”处理程序,你是对的。我们可以用mouseout事件来做到这一点。然而,你看到当你可以使用 CSS 时使用 CSS 是有道理的吗?

当然,没有必要记住 DOM 节点上可用的各种属性,但idclassNamestyledataset可能是最有用的。

你问的这个dataset属性是什么?你可能不熟悉 HTML 中的数据属性,但它们非常方便。考虑 MDN 中的这个例子:

<article id="electric-cars" data-columns="3" data-index-number="12314" data-parent="cars"> ... </article>

当你的后端可以将标记插入到 HTML 中,但与 JavaScript 分离时(几乎总是如此,并且可以说是你的结构应该被架构化的方式),data-属性就非常方便。要访问articledata-index-number,我们使用这个:

article.dataset.indexNumber // "12314"

再次注意我们的驼峰命名法和.dataset.的新用法,而不是data-

我们现在知道足够多的知识来对我们的元素进行更多的激动人心的工作。我们可以用选择器来定位元素并读取元素的属性。接下来,让我们看看操作

操作

在使用 JavaScript 通过 DOM 工作时,我们不仅可以读取,还可以操作这些属性。让我们通过制作一个小程序来练习操作属性:便利贴创建者。

便利贴创建者

我们将制作一个便利贴创建者,它接受颜色和消息,并将带有序号的彩色框添加到 DOM 中。我们的最终产品可能看起来像这样:

图 6.9 最终产品

看一下起始代码:github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-6/stickies/starter-code

你的目标是重新创建这个功能。这里有两种我们还没有涵盖的方法供你研究:

  • document.createElement()

  • container.appendChild()

解决方案代码

你做得怎么样?让我们看看解决方案代码:

const container = document.querySelector('.container') // set .container to a variable so we don't need to find it every time we click
let noteCount = 1 // inital value

// access our button and assign a click handler
document.querySelector('.box-creator-button').addEventListener('click', () => {
  // create our DOM element
  const stickyNote = document.createElement('div')

  // set our class name
  stickyNote.className = 'box'

  // get our other DOM elements
  const stickyMessage = document.querySelector('.box-color-note')
  const stickyColor = document.querySelector('.box-color-input')

  // get our variables
  const message = stickyMessage.value
  const color = stickyColor.value

  // blank out the input fields
  stickyMessage.value = stickyColor.value = ''

  // define the attributes
  stickyNote.innerHTML = `${noteCount++}. ${message}`
  stickyNote.style.backgroundColor = color

  // add the sticky
  container.appendChild(stickyNote)
})

好了!其中一些行不应该是一个谜,但最有趣的是第 7 行(const stickyNote = document.createElement('div'))和第 28 行(container.appendChild(stickyNote))。正如之前提到的,这是你需要研究的两种方法,以完成这个程序。第 7 行正在创建一个 DOM 节点——在内存中!我们可以对它进行操作,比如添加内容和样式,然后在第 28 行将其添加到 DOM 中。

总结

耶,我们终于进入了 DOM 并对其进行了操作!恭喜你迄今为止的成就!

现在,我们可以通过 JavaScript 动态地改变页面上的内容,而不仅仅是使用警报和控制台消息。以下是我们学到的内容的概述:

  • querySelectorquerySelectorAll是我们进入 DOM 的神奇领域的门户。

  • DOM 只存在于内存中,作为 HTML 在页面加载时的动态表示。

  • 这些方法的选择器将使用 CSS 选择器;旧方法不会。

  • 节点的属性可以更改,但术语不同。

在下一章中,我们将更多地使用events。事件是 JavaScript 程序的核心,让我们学习它们的结构和用法。

问题

考虑以下代码:

  <button>Click me!</button>

回答以下问题:

  1. 选择按钮的正确语法是什么?

  2. document.querySelector('点击我!')

  3. document.querySelector('.button')

  4. document.querySelector('#button')

  5. document.querySelector('button')

看看这段代码:

<button>Click me!</button>
<button>Click me two!</button>
<button>Click me three!</button>
<button>Click me four!</button>

回答以下问题:

  1. 真或假:document.querySelector('button') 将满足我们对每个按钮放置点击处理程序的需求。

  2. 正确

  3. 错误

  4. 要将按钮的文本从“点击我!”更改为“先点击我!”,我们应该使用什么?

  5. document.querySelectorAll('button')[0].innerHTML = "先点击我!"

  6. document.querySelector('button')[0].innerHTML = "先点击我!"

  7. document.querySelector('button').innerHTML = "先点击我!"

  8. document.querySelectorAll('#button')[0].innerHTML = "先点击我!"

  9. 我们可以使用什么方法来添加另一个按钮?

  10. document.appendChild('button')

  11. document.appendChild('<button>')

  12. document.appendChild(document.createElement('button'))

  13. document.appendChild(document.querySelector('button'))

  14. 我们如何将第三个按钮的类更改为third

  15. document.querySelector('button')[3].className = 'third'

  16. document.querySelectorAll('button')[2].className = 'third'

  17. document.querySelector('button[2]').className = 'third'

  18. document.querySelectorAll('button')[3].className = 'third'

进一步阅读

有关更多信息,您可以参考以下链接:

第七章:事件,事件驱动设计和 API

在前端应用的核心是事件。JavaScript 允许我们监听并对用户和浏览器事件做出反应,以直观地改变用户内容,从而创建优雅的用户界面和体验。我们需要知道如何使用这些被抛出的数据包。浏览器事件是我们的基础 - 它们使我们不仅仅拥有静态应用,而是动态的!通过理解事件,您将成为一个完整的 JavaScript 开发人员。

本章将涵盖以下主题:

  • 事件生命周期

  • 捕获事件并读取其属性

  • 使用 Ajax 和事件来填充 API 数据

  • 处理异步性

技术要求

准备好使用存储库的Chapter-7目录中提供的代码:github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-7

事件生命周期

当 JavaScript 中发生事件时,它不仅仅发生并消失 - 它经历了一个生命周期。这个生命周期有三个阶段:

  • 捕获阶段

  • 目标阶段

  • 冒泡阶段

考虑以下 HTML:

<!doctype html>
<html>

<head>
  <title>My great page</title>
</head>

<body>
  <button>Click here</button>
</body>

</html>

我们可以将其可视化如下:

图 7.1 - 事件生命周期

现在,还有一件重要的事情需要考虑,那就是事件发生时不仅仅影响到确切的目标,而是整个对象堆栈。在描述捕获、目标和冒泡之前,看一下我们代码的以下表示:

图 7.2 - 事件分层

如果我们把我们的页面想象成一个分层蛋糕,我们可以看到这个事件(由箭头表示)必须通过我们 DOM 的所有层才能到达按钮。这是我们的捕获阶段。当按钮被点击时,事件被派发到事件流中。首先,事件查看文档对象。然后它穿过 DOM 的各层直到到达预定目的地:按钮。

现在事件已经到达按钮,我们开始目标阶段。事件应该从按钮中捕获的任何信息都将被收集,比如事件类型(比如点击或鼠标悬停)和其他细节,比如光标的X/Y坐标。

最后,事件在冒泡阶段返回到文档的各层。冒泡阶段允许我们通过其父元素在任何元素上处理事件。

让我们在实践中看看并稍微玩一下我们的事件。找到以下目录并在浏览器中打开index.html - github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-7/events

图 7.3 - 事件游乐场

如果我们看一下这个页面并玩几分钟,我们会看到一些东西:

  • 右侧的X/Y坐标将随着我们在页面上移动鼠标而改变。

  • 当我们打开控制台时,它将显示有关我们的点击事件以及发生在哪个阶段的消息。

让我们看看index.js中的代码,网址是github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/blob/master/chapter-7/events/index.js

从 1 到 5 行,我们只是设置了一个数据对象,将数字代码映射到一个字符串。到目前为止,一切都很顺利。现在,让我们看看第 32 行,那里写着document.querySelector('html').addEventListener('click', logClick, true)。这个可选的布尔参数对我们来说是新的;当它放入事件监听器中时,它只是表示“让我在捕获阶段监听”。因此,当我们在页面的任何地方点击时,我们将得到一个点击事件,其中包含信息点击事件在 HTML 上的捕获阶段触发。这个事件之前在未定义处被处理,因为这是对这个事件的第一次遭遇。它还没有冒泡或被定位。

让我们在下一节继续剖析这个例子,了解代码中这些神秘的部分。

捕获事件并读取其属性

我们将继续使用我们的events游乐场代码:github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/blob/master/chapter-7/events/index.js

在 32-34 行,我们注册了三个点击事件监听器,如下所示:

document.querySelector('html').addEventListener('click', logClick, true)
document.querySelector('body').addEventListener('click', logClick)
document.querySelector('button').addEventListener('click', logClick)

正如我们讨论过的,第一个事件监听在捕获阶段,因为我们包括了最后的布尔参数。

我们还有三个mousemove事件在 16-29 行。让我们看看其中一个:

document.querySelector('button').addEventListener('mousemove', (e) => {
  document.querySelector('#x').value = e.x
  document.querySelector('#y').value = e.y
})

我希望大部分都是有意义的-我们正在使用一个新的事件类型mousemove,所以这个事件表示“当用户的鼠标移过按钮时,执行这段代码。”就是这么简单。我们要执行的代码是将 ID 为xy的输入的值设置为事件的 x 和 y 值。这就是事件对象的魔力所在:它携带了很多信息。继续在这个函数内添加一行console.log(e),看看记录了什么,如下面的截图所示:

图 7.4-记录事件

正如预期的那样,每当你的鼠标移动到“点击这里”上时,事件就会触发,并且鼠标事件被记录下来。打开其中一个事件。你会看到类似于以下内容:

图 7.5-鼠标事件

在这里,我们看到了关于事件的大量信息,包括(如预期的那样)我们鼠标在那个时候的XY坐标。这些属性中的许多将会很有用,但特别要注意的是target。事件的目标是我们放置事件监听器的节点。从target属性中,我们可以得到它的 ID,如果我们有一个事件处理程序用于多个节点,这将会很有用。

你还记得我们在第六章中的便利贴程序,文档对象模型(DOM)吗?现在让我们来增强它。

重新审视便利贴

让我们从第六章中的便利贴程序文档对象模型(DOM)中更仔细地看一下:github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-7/stickies/starter-code,并包括创建模态窗口的能力,当点击时显示有关便利贴的信息,并能够删除该便利贴,如下面的截图所示:

图 7.6-新的和改进的便利贴创建者

要成功编写这段代码,你需要使用一个新的 DOM 操作方法:.remove()。查看developer.mozilla.org/en-US/docs/Web/API/ChildNode/remove获取文档。你可能还想看一下visibility的 CSS 属性来显示和隐藏模态窗口。

只是为了好玩,我还包括了一个小的 JavaScript 库,用于将颜色选择器用于便利贴颜色字段,作为包含第三方代码的简单示例。您不需要对jscolor.js脚本做任何操作;它将自动工作。

便利贴 - 解决方案 1

您是否得到了类似以下代码的东西?

01: const container = document.querySelector('.container') // set 
    .container to a variable so we don't need to find it every time 
     we click
02: let noteCount = 1 // inital value
03: const messageBox = document.querySelector('#messageBox')
04: 
05: // access our button and assign a click handler
06: document.querySelector('.box-creator-button').addEventListener(
    'click', () => {
07:   // create our DOM element
08:   const stickyNote = document.createElement('div')
09: 
10:   // set our class name
11:   stickyNote.className = 'box'
12: 
13:   // get our other DOM elements
14:   const stickyMessage = document.querySelector('.box-color-note')
15:   const stickyColor = document.querySelector('.box-color-input')
16: 
17:   // get our variables
18:   const message = stickyMessage.value
19:   const color = stickyColor.style.backgroundColor
20: 
21:   // blank out the input fields
22:   stickyMessage.value = stickyColor.value = ''
23:   stickyColor.style.backgroundColor = '#fff'
24: 
25:   // define the attributes
26:   stickyNote.innerHTML = `${noteCount++}. ${message}`
27:   stickyNote.style.backgroundColor = color
28: 
29:   stickyNote.addEventListener('click', (e) => {
30:     document.querySelector('#color').innerHTML = 
        e.target.style.backgroundColor
31:     document.querySelector('#message').innerHTML = e.target.innerHTML
32: 
33:     messageBox.style.visibility = 'visible'
34: 
35:     document.querySelector('#delete').addEventListener('click', (event) => {
36:       messageBox.style.visibility = 'hidden'
37:       e.target.remove()
38:     })
39:   })
40: 
41:   // add the sticky
42:   container.appendChild(stickyNote)
43: })
44: 
45: document.querySelector('#close').addEventListener('click', (e) => {
46:   messageBox.style.visibility = 'hidden'
47: })

您可以在 GitHub 上找到这个代码文件:github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-7/stickies/solution-code-1

这里有一些有趣的部分,比如我们的便利贴单击处理程序从第 29 行开始。大部分内容应该看起来很熟悉,只是增加了一些新的内容。首先,单击处理程序使用事件的目标属性来使用目标的属性设置消息框中的文本。我们不必在 DOM 中搜索以查找我们的属性。事实上,当事件对象已经将信息传递给我们时,这样做将是昂贵和浪费的操作。第 33 行修改了模态窗口的 CSS 以显示它,第 37 行在模态的删除按钮被单击时删除了便利贴。

这个效果相当不错!但是,由于事件生命周期的特性,我们可以使用另一个事件的特性来使我们的代码更加高效:事件委托

便利贴 - 解决方案 2 - 事件委托

事件委托的原则是在父事件上注册一个事件监听器,让事件传播告诉我们哪个元素被点击了。还记得我们的事件生命周期图和事件传播的层次吗?我们可以利用这一点。看一下第 37 行,如下所示:

container.addEventListener('click', (e) => {
 if (e.target.className === 'box') {
   document.querySelector('#color').innerHTML = 
    e.target.style.backgroundColor
   document.querySelector('#message').innerHTML = e.target.innerHTML
   messageBox.style.visibility = 'visible'
   document.querySelector('#delete').addEventListener('click', (event) => {
     messageBox.style.visibility = 'hidden'
     e.target.remove()
   })
 }
})

您可以在 GitHub 上找到这段代码:github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/blob/master/chapter-7/stickies/solution-code-2/script.js#L37

在这段代码中,我们已经将点击监听器的附加从便利贴创建逻辑中移除,并将其抽象为附加到整个容器。当单击container时,我们检查目标是否具有box作为其类。如果是,我们执行我们的逻辑!这是事件监听器更有效的使用,特别是在动态创建的元素上使用时。有些情况下,事件委托将是您的最佳选择,有时任何一种都可以。

但现在我们有另一个问题:每次单击便利贴时,都会向删除按钮添加一个新的单击处理程序。这并不是很高效。看看是否可以重构代码以消除这个问题。

便利贴 - 解决方案 3

这是一个可能的解决方案:

let target = {}

...

container.addEventListener('click', (e) => {
  if (e.target.className === 'box') {
    document.querySelector('#color').innerHTML = 
     e.target.style.backgroundColor
    document.querySelector('#message').innerHTML = e.target.innerHTML
    messageBox.style.visibility = 'visible'
    target = e.target
  }
})

document.querySelector('#delete').addEventListener('click', (event) => {
  messageBox.style.visibility = 'hidden'
  target.remove()
})

您可以在 GitHub 上找到这个解决方案:github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/blob/master/chapter-7/stickies/solution-code-3/script.js

虽然这使用了一个全局变量,但它仍然更高效。通过将整个程序封装在一个函数或类中,我们可以消除全局变量,但这对于这个概念来说并不重要。

现在是时候看一下 Ajax 以及事件如何与程序的生命周期联系起来了。让我们做一个实验!

使用 Ajax 和事件来填充 API 数据

让我们把所有东西都放在一起。在这个实验中,我们将使用 PokéAPI 创建一个简化的宝可梦游戏:pokeapi.co/

这就是我们的游戏最终的样子:sleepy-anchorage-53323.herokuapp.com/。请打开网站并尝试一下功能。

请抵制诱惑,暂时不要查看已完成的 JavaScript 文件。

这是当您访问上述 URL 并开始玩游戏时会看到的屏幕截图:

图 7.7 – 宝可梦游戏

所有的 HTML 和 CSS 都已经为您提供。您将在main.js文件中工作:github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-7/pokeapi/starter-code

如果您不熟悉宝可梦,不用担心!这个游戏的逻辑很基本。(如果您熟悉这些游戏,请原谅这种简化的方法。)

这是我们将要做的事情:

  1. 查询 PokéAPI 以获取所有可用的宝可梦。

  2. 使用 API 提供的宝可梦名称和 API URL 的值填充选择列表。

  3. 完成后,切换 CSS 属性以显示玩家的选择。

  4. 允许每个玩家选择他们的宝可梦。

  5. 为每个玩家创建功能,让他们使用自己宝可梦的招式对抗对方。

  6. 根据从最大可能力量生成的随机数减少另一个玩家的宝可梦生命值。

  7. 显示叠加文本,指出它是有效的。

  8. 如果招式没有力量属性,显示叠加,表示它不起作用。

  9. 当一个宝可梦的生命值为0或更低时,显示对手已经晕倒的叠加。

让我们逐步分解起始代码。

起始代码

让我们逐步看一下起始代码,因为它引入了我们的 JavaScript 的一个新的构造:类!如果您熟悉 Python 或其他语言中的类,这个 ES6 的介绍将是对 JavaScript 使用的一个受欢迎的提醒。让我们开始:

class Poke {
  ...
}

首先,在 JavaScript ES6 中声明一个类时,我们只是创建一个对象!现在,对象的细节与我们习惯的有些不同,但许多原则是相同的。要创建类的实例,我们可以在完成类代码后说const p = new Poke()

之后,有一些类的语法糖,比如构造函数、getter 和 setter。随意研究 JavaScript 中的类,因为它将帮助您实现整体目标。

我已经为您提供了构造函数的起始部分,当您创建一个类的实例时,它将被执行:

constructor() {
    /**
      * Use the constructor as you would in other languages: Set up your 
        instance variables and globals
      */
  }

您的构造函数可能需要什么?也许您想要对经常使用的 DOM 元素或事件处理程序进行引用?然后,当然,问题就出现了:我们如何引用我们创建的变量?

答案是this。当使用一个全局变量到类时,您可以在this.<variableName>之前加上它,它将对所有方法可用。这里的好处是:它不是我们整个页面的纯全局变量,而只是我们类的全局变量!如果您回忆一下之前的一些代码示例,我们没有处理那一部分;这是一种处理的方法:

choosePokemon(url, parent) {
…
const moves = data.moves.filter((move) => {
  const mymoves = move.version_group_details.filter((level) => {
    return level.level_learned_at === 1
  })
  return mymoves.length > 0
 })
}

由于每个宝可梦在游戏的不同阶段学习多个招式,这是在游戏开始时找到可用招式的逻辑。您不必修改它,但是看一下数组的.filter()方法。我们之前没有涉及它,但这是一个有用的方法。MDN 是一个很好的资源:developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter

我们感兴趣的代码的下一部分是setter

set hp(event) {
  ...
  if (event.hp) {
    this[event.player].hp = event.hp
  }

  if (event.damage) {
    this[event.player].hp -= event.damage
  }
  const e = new CustomEvent("hp", {
    detail: {
      player: event.player,
      hp: this[event.player].hp
    }
  })
  document.dispatchEvent(e)
}

setter是一个处理设置或更改成员变量的类方法。通常与getter一起使用,这个概念允许我们在更改(或检索)变量时抽象出所需的操作逻辑。在这种情况下,我们使用一些游戏逻辑来看待生命值。但是然后我们进入了一个新的、美妙的想法:自定义事件。

自定义事件

使用new CustomEvent()指令,我们可以创建一个新的命名事件在我们的程序中使用。有时,用户交互或页面行为并不能完全满足我们的需求。自定义事件可以帮助满足这种需求。请注意在前面的代码中,detail对象包含要传递的事件数据,我们使用document.dispatchEvent()将其发送到事件流中。创建自定义事件的事件监听器与使用内置事件一样:使用.addEventListener()。我们将要使用doMove()函数。

解决方案代码

您尝试得怎么样?您可以在这里看到解决实验室的一种可能方式:github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-7/pokeapi/solution-code

记住,解决编程问题有多种方法,所以如果您的解决方案与提供的方法不匹配,也没关系!主要目的是解决问题。

处理异步性

正如我们在使用 API 时所看到的,Ajax 调用的异步性需要一些创造性的方法。在我们的宝可梦游戏中,我们在调用完成时使用了加载旋转器;这是您在现代网络上到处都能看到的方法。让我们看一个游戏中的例子:

toggleLoader() {
  /**
    * As this is visual logic, here's the complete code for this function
    */
  if (this.loader.style.visibility === 'visible' || 
  this.loader.style.visibility === '') {
    this.loader.style.visibility = 'hidden'
  } else {
    this.loader.style.visibility = 'visible'
  }
}

部分代码所做的只是切换包含旋转图像的图层的可见性。这都是在 CSS 中(因为它不是技术上的图像,而是 CSS 动画)。让我们看看它是如何使用的:

getPokemon() {
    fetch('https://pokeapi.co/api/v2/pokemon?limit=1000')
      .then((response) => {
        return response.json()
      })
      .then((data) => {
        const pokeSelector = document.querySelector('.pokeSelector.main')

        data.results.forEach((poke) => {
          const option = document.createElement('option')
          option.value = poke.url
          option.innerHTML = poke.name
          pokeSelector.appendChild(option)
        })

        const selector = pokeSelector.cloneNode(true)
        document.querySelector('.pokeSelector.clone').replaceWith(selector)

        this.toggleLoader()

        document.querySelector('#Player1').style.visibility = 'visible'
        document.querySelector('#Player2').style.visibility = 'visible'
      })
  }

在这里,我们看到在我们的异步 Promise 调用中使用.then()时,当一切都完成时切换加载程序!这是一个很好的小捆绑。如果您想复习如何使用fetch和一般的 Ajax 调用,请回顾一下第四章,数据和您的朋友,JSON,在来自前端的 API 调用 - Ajax部分。

在处理 Ajax 调用固有的异步特性时,重要的是要记住我们不知道调用何时会返回其数据,甚至是否会返回!我们可以通过错误处理使我们的代码更好。

错误处理

看一下这段代码:

fetch('/profile')
  .then(data => {
    if (data.status === 200) {
      return data.json()
    }
    throw new Error("Unable to get Profile.")
  })
  .then(json => {
    console.log(json)
  })
  .catch(error => {
    alert(error)
  })

我们在这里有一些常见的嫌疑人:一个fetch调用和.then()处理我们的结果。现在,看一下new Error().catch()。就像大多数语言一样,JavaScript 有一种明确抛出错误的方法,我们fetch链的末尾的.catch()将在警报框中向用户呈现错误。在您的 Ajax 调用中包含错误处理总是最佳实践,以防您调用的服务没有响应,没有及时响应或发送错误。我们将在第九章中更多地讨论错误,解密错误消息和性能泄漏

星球大战 API 探索实验室

让我们通过一些 Ajax 调用来动手。我们将使用流行的星球大战 APISWAPI):swapi.dev/。花几分钟时间熟悉文档和 API 的工作原理。

这是我们将要构建的内容:

图 7.8 - 星球大战探索

您可以在packtpublishing.github.io/Hands-on-JavaScript-for-Python-Developers/chapter-7/swapi/solution-code/上尝试该功能的功能。在尝试重新创建功能之后,试着抵制浏览解决方案代码的诱惑。

我们的代码应该做到以下几点:

  1. 在页面加载时显示加载程序。这个加载程序作为 CSS 动画为您提供。

  2. 调用/people SWAPI 端点来检索 API 中的所有人。提示:您需要多次调用 SWAPI 才能获取所有人。

  3. 用人们的名字填充选择列表并隐藏加载器。

  4. 当点击 Go 时,再次调用 SWAPI 以检索有关所选人员的详细信息并显示它们(至少是姓名)。

我们的方法将首先填充列表,然后准备用户操作,以便探索同步链接事件和异步动作依赖于用户输入的情况。

起始 HTML 和 CSS 不应该需要更改,我们的起始 JavaScript 文件几乎是空的!您准备好挑战了吗?祝你好运!

一个解决方案

如果您查看解决方案代码,您会发现创建此功能的一种方法。让我们来分解一下。

就像在我们的宝可梦游戏中一样,我们将使用一个类。它的构造函数将存储一些各种信息,并添加一个事件侦听器到 Go 按钮:

class SWAPI {
  constructor() {
    this.loader = document.querySelector('#loader')
    this.people = []

    document.querySelector('.go').addEventListener('click', (e) => {
      this.getPerson(document.querySelector('#peopleSelector').value)
    })
  }

接下来,我们知道我们将多次调用 SWAPI,我们可以创建一个帮助函数来简化这项工作。它可能需要四个参数:SWAPI API URL,先前结果的数组(如果我们正在分页的话很有用!),以及类似 Promise 的resolvereject参数:

  fetchThis(url, arr, resolve, reject) {
    fetch(url)
      .then((response) => {
        return response.json()
      })
      .then((data) => {
        arr = [...arr, ...data.results]

最后一行可能是新的。是扩展运算符,它将数组展开为其各个部分。有了这个 ES6 功能,我们就不需要迭代数组来将其连接到另一个数组或进行任何其他重新分配的操作。我们可以简单地展开结果并将它们与现有结果连接起来:

        if (data.next !== null) {
          this.fetchThis(data.next, arr, resolve, reject)
        } else {
          resolve(arr)
        }

在许多 API 中,如果数据集很大,只会返回有限的结果,并提供下一页和上一页数据的链接。 SWAPI 的命名规范指定.next是要查找的属性,如果有另一页的话。否则,我们可以在我们的resolve函数中返回我们的结果:

      })
      .catch((err) => {
        console.log(err)
      })

不要忘记错误处理!

  }

  getPeople() {
    new Promise((resolve, reject) => {
        this.fetchThis('https://swapi.dev/api/people', this.people, 
        resolve, reject)
      })
      .then((response) => {
        this.people = response
        const peopleSelector = document.querySelector('#peopleSelector')

        this.people.forEach((person) => {
          const option = document.createElement('option')
          option.value = person.url
          option.innerHTML = person.name
          peopleSelector.appendChild(option)
        })
        this.toggleLoader()
        document.querySelector('#people').style.visibility = 'visible'
      })
      .catch((err) => {
        console.log(err)
      })
  }

尝试完整阅读getPeople(),以了解它的功能。其中一些是简单的操作,但new Promise()是这个函数的核心。我们不是在我们的 API 人员列表上硬编码页数来迭代,而是创建一个使用我们的fetchThis函数的新 Promise:

  getPerson(url) {
    this.toggleLoader()
    fetch(url)
      .then((response) => {
        return response.json()
      })
      .then((json) => {
        document.querySelector('#person').style.visibility = 'visible'
        document.querySelector('#person h2').innerHTML = json.name
        this.toggleLoader()
      })
      .catch((err) => {
        console.log(err)
      })
  }

理论上,一旦点击按钮,我们可以使用相同的fetchThis函数来获取单个人,但仅仅为了我们的示例,这个解决方案将所有内容都处理在一个地方:

  toggleLoader() {
    if (this.loader.style.visibility === 'visible' ||
    this.loader.style.visibility === '') {
      this.loader.style.visibility = 'hidden'
    } else {
      this.loader.style.visibility = 'visible'
    }
  }
}

然后,我们只需要实例化我们的类!

const s = new SWAPI().getPeople()

此时,我们的程序已经完成并且可以运行!访问页面,您将看到我们的完全运行的页面。帝国皇帝感谢您帮助消灭叛军。我们已经看到了类、基于事件的编程以及我们利用事件的能力。

摘要

我们已经了解了事件、它们的生命周期以及事件驱动设计的工作原理。事件是由用户的动作(或基于程序逻辑的程序化触发)而触发的,并进入其生命周期。在事件生命周期中,我们的程序可以捕获事件对象本身携带的许多信息,例如鼠标位置或目标 DOM 节点。

通过了解 Ajax 如何与事件配合工作,您已经在成为一个完全成熟的 JavaScript 开发人员的道路上迈出了重要的一步。Ajax非常重要,因为它是 JavaScript 和外部 API 之间的通道。由于 JavaScript 是无状态的,客户端 JavaScript 没有会话的概念,因此 Ajax 调用在性质上需要是异步的;因此引入了诸如fetch之类的工具。

恭喜!我们已经涵盖了很多非常密集的材料。接下来是 JavaScript 中的框架和库。

问题

回答以下问题以评估您对事件的理解:

  1. 这些中哪一个是事件生命周期的第二阶段?

  2. 捕获

  3. 目标

  4. 冒泡

  5. 事件对象为我们提供了以下哪些内容?- 选择所有适用的:

  6. 触发的事件类型

  7. 目标 DOM 节点,如果适用的话

  8. 鼠标坐标,如果适用的话

  9. 父 DOM 节点,如果适用的话

看看这段代码:

container.addEventListener('click', (e) => {
  if (e.target.className === 'box') {
    document.querySelector('#color').innerHTML = 
     e.target.style.backgroundColor
    document.querySelector('#message').innerHTML = e.target.innerHTML
    messageBox.style.visibility = 'visible'
    document.querySelector('#delete').addEventListener('click', (event) => {
      messageBox.style.visibility = 'hidden'
      e.target.remove()
    })
  }
})
  1. 在上述代码中使用了哪些 JavaScript 特性?选择所有适用的:

  2. DOM 操作

  3. 事件委托

  4. 事件注册

  5. 样式更改

  6. 当容器被点击时会发生什么?

  7. box 将可见。

  8. #color 将是红色。

  9. 1 和 2 都是。

  10. 没有足够的上下文。

  11. 在事件生命周期的哪个阶段我们通常采取行动?

  12. 目标

  13. 捕获

  14. 冒泡

进一步阅读

第八章:与框架和库一起工作

很少有语言存在于一个自包含的、整体的象牙塔中。几乎总是,特别是对于任何现代语言,程序中都会使用第三方代码来增加功能。使用第三方代码,比如库和框架,也是使用 JavaScript 的一个重要部分。让我们来看看我们工具包中一些更受欢迎的开源工具。

本章将涵盖以下主题:

  • jQuery

  • Angular

  • React 和 React Native

  • Vue.js

技术要求

准备好使用存储库的Chapter-8目录中提供的代码:github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-8。由于我们将使用命令行工具,还要准备好你的终端或命令行 shell。我们需要一个现代浏览器和一个本地代码编辑器。

jQuery

创建或使用 JavaScript 库的主要原因之一是为了简化重复或复杂的任务。毕竟,你不能通过插件或库从根本上改变一种语言——你所能做的只是增加或改变现有的功能。

正如我们在第一章中讨论的那样,JavaScript 进入主流编程,JavaScript 的早期历史有点像是一个荒野西部的情景。浏览器之间的战争正在全面展开,功能没有标准化,甚至发起一个 Ajax 调用都需要两套不同的代码:一套是为了 Internet Explorer,另一套是为了其他浏览器。

2006 年,由 John Resign 创建了 jQuery。

浏览器之间的标准化不足是创建 jQuery 的动力。从 DOM 操作到 Ajax 调用,jQuery 的语法和结构是一种“一次编写,所有浏览器使用”的范式。随着 ES6 及更高版本的开发,JavaScript正在变得更加标准化。然而,有超过十年的 jQuery 代码存在,大多数 JavaScript 重的网站都在使用。由于这些传统应用程序,它仍然非常受欢迎,因此对我们的讨论很重要。它也是开源的,因此使用它不需要许可费。

jQuery 的优势

考虑以下例子,它们做了同样的事情:

  • JavaScript ES6document.querySelector("#main").classList.add

("red")

  • jQuery$("#main").addClass("red");

正如你所看到的,jQuery 的构造要简短得多。太好了!简洁的代码通常是一件好事。所以,让我们来分解这个例子:

图 8.1 - jQuery 语法

  1. 我们几乎所有的 jQuery 语句都是以$开头的。这是许多库中使用的一个惯例,实际上,你可以覆盖美元符号并使用任何你喜欢的东西,所以你可能会看到以jQuery开头的例子。

  2. 我们的选择器是 CSS 选择器,就像我们在document.querySelector()中使用的一样。一个惯例是,如果你要存储通过 jQuery 选择的 DOM 节点以供以后使用,就用美元符号表示。所以,如果我们要将#main存储为一个变量,它可能看起来像这样:const $main = $("#main")

  3. jQuery 有自己的一系列函数,通常是内部功能的可读性缩写。

关于 jQuery 的一个有趣的事实:你可以将 jQuery 与原生 JavaScript(即不使用任何框架或库)混合使用。事实上,“原生 JavaScript”这个术语是指非 jQuery 代码的一种常用方式。

此外,一些前端库,如 Bootstrap,在 Bootstrap 5 之前,是使用 jQuery 构建的,因此了解其用法可以帮助你了解其他库和框架。这并不是一个事,但在你探索前端开发的新世界时要注意这一点。

jQuery 的缺点

使用 jQuery,就像使用任何库一样,需要在客户端上进行额外的下载。截至撰写本文时,jQuery 3.4.1 的压缩版本大小为 88 KB。尽管这在很大程度上可以忽略不计,并且将被浏览器缓存,但请记住,这必须在每个页面上执行和加载,因此不仅要考虑下载大小,还要考虑执行时间。Wes Bos 还有一些关于 ES6 和 jQuery 中作用域的很好的信息:wesbos.com/javascript-arrow-functions/

另外,虽然并非所有情况都是如此,但大部分 jQuery 的用法存在是为了标准化 ES5,所以你在网上和示例中看到的大部分代码都是 ES5。

jQuery 的例子

让我们比较一下我们原始的星球大战探索第七章,“事件、事件驱动设计和 API”(github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-8/swapi)与 jQuery 版本(github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-8/swapi-jQuery)。

现在,我承认这并不是最优雅的 jQuery 代码,但这样做是有原因的。让我们来分析一下。

首先是 HTML:

ES6 jQuery
无变化 添加<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>

正如我们讨论过的,添加 JavaScript 库或框架本质上需要从本地文件下载另一个文件,并/或者需要额外的处理时间。通常,大小是可以忽略不计的,所以在这种情况下,唯一相关的因素是我们需要添加一行 HTML 来从全局内容传递网络加载 jQuery 文件。

CSS 不会有变化,这是预期的。所以让我们深入 JavaScript:

ES6 jQuery

|

class SWAPI {
  constructor() {
    …
  }
}

|

var swapi;

$(document).ready(function() {
  swapi = new SWAPI;
});

|

好了,现在我们看到了一些主要的区别。正如前面提到的,这并不一定是最理想的 jQuery 程序,但我认为它能传达出要点。首先,虽然 jQuery 和 ES6 是兼容的,但大多数情况下,jQuery 是在 ES6 不可用的地方使用的,或者代码尚未升级到 ES6。你会注意到大多数 jQuery 代码的第一件事是,在行尾使用分号,并使用var而不是letconst。这并不是 jQuery 独有的,而是 ES5 的约定。

ES5 通常使用对象原型的操作,而不是使用类,如下所示:

SWAPI.prototype.constructor = function() {
  this.$loader = $('#loader');
  this.people = [];
};

类可以说是更干净的工作方式,因为它们在方法和用法上更加自包含和明确。然而,当 jQuery 流行时,这种约定还不存在,所以我们将使用 ES5 原型继承。

现在让我们一起看看使用 ES6 和 jQuery 进行 Ajax 调用的不同之处:

ES6 jQuery

|

fetch(url)
  .then((response) => {
     return response.json()
  })
  .then((json) => {
    … 
  })

|

$.get(url)
  .done(function(data) {
     …
  };

|

这是一个很好的例子,说明了为什么要使用 jQuery 以及它的创建如何促进了 ES6 的一些简化。在 ES5 中,进行 Ajax 请求需要两种不同的方法——一种是针对 Internet Explorer,另一种是针对其他浏览器——因为请求方法并没有标准化。jQuery 通过在幕后进行浏览器检测和代码切换来帮助开发人员,这样开发人员只需要编写一条语句。然而,使用fetch就不再需要这样做了。不过,我们可以看到 jQuery 代码稍微短一些,因为我们没有第一个.then函数来返回请求的 JSON。这是设计缺陷还是特性?实际上是后者,因为 API 可能返回许多不同类型的响应。fetch方法在幕后为您进行了一些转换,而 jQuery 则希望您基本上知道您的数据是什么以及如何处理它。

W3Schools 在 jQuery 上有很好的示例和参考资料:www.w3schools.com/jquery/

如果您查看 jQuery 版本的其余代码,您会发现许多其他有趣的差异示例,但现在——从 jQuery 继续前进!让我们来看看一个完整的web 框架:Angular。

Angular

Angular 由 Google 创建为AngularJS。在 2016 年,它被重写为版本 2,使其与 AngularJS 分离。它是开源的框架,而不是库,现在引发了一个问题:框架之间有什么区别?

是一个工具包,用于更轻松地编写您的代码,用于不同的目的。使用建筑类比,库就像一套可以用来组装房子的砖头。相反,框架更类似于设计房子所使用的蓝图。它可能使用一些相同的砖头,也可能不使用!主要区别之一是,一般来说,库允许您按照自己想要的方式编写代码,而不会让库对如何构建代码的结构发表意见。另一方面,框架更具有意见,并要求您按照框架的最佳实践来构建代码。这是一个模糊的(有时是过载的)术语,因此对于什么是库和什么是框架存在可以理解的争论。只需搜索Stack Overflow,您就会找到竞争性的定义。一个很好的简化陈述是,框架可以是一组具有指定使用模式的技术,而更有可能是一种帮助操作数据的技术。

让我们考虑这个图表:

图 8.2 - 框架组成

正如我们所看到的,框架实际上可以由多个库组成。框架的设计模式通常决定了这些库的使用方式和时间。

Angular 使用TypeScript,这是一种开源的编程语言。最初由微软开发,它是 JavaScript 的一个超集,具有一些额外的功能,对一些开发人员来说是吸引人的。尽管 TypeScript 被归类为自己的语言,但它是 JavaScript 的超集,因此可以转换为普通 JavaScript,因此在浏览器中运行时不需要额外的工作,除了执行 Angular 构建过程。

Angular 的优势

Angular,像大多数框架一样,对您的文件结构和代码语法有自己的看法(特别是在混合使用 TypeScript 时)。这可能听起来像一个缺点,但实际上在团队合作中非常重要:您已经有了关于如何处理代码的现有文件结构,这是一件事情。

Angular 也不是独立存在的。它是技术栈的一部分,这意味着它是一个从前端到数据库的一揽子解决方案。您可能已经遇到过MEAN技术栈这个术语:MongoDB, Express, Angular, 和 Node.js。虽然您可以在这个技术栈之外使用 Angular,但它提供了一个易于设置的开发生态系统,被他人广泛理解。

如果您对Model-View-ControllerMVC)范例不熟悉,现在是熟悉它的好时机。许多技术堆栈跨越多种语言利用这种范例来分离代码库中的关注点。例如,程序中的模型与数据源(如数据库和/或 API)的数据获取和操作进行交互,而控制器管理模型、数据源和视图层之间的交互。视图主要控制全栈环境中信息的视觉显示。在全栈 MVC 社区内存在争议,就方法而言,所谓的“模型臃肿,控制器瘦身”方法和相反的方法之间存在争论。现在不重要去讨论这种区别,但您会在社区中看到这种争论。

谈到社区,事实上 Angular 开发人员已经形成了一个临时网络,相互帮助。单单讨论就很有价值,可以帮助您在这个领域中导航。

Angular 还有一些其他优点,比如双向数据绑定(确保模型和视图相互通信)和绑定到 HTML 元素的专门指令,但这些都是现在不重要讨论的细微差别。

Angular 的缺点

Angular 的主要缺点是其陡峭的学习曲线。除了原始的 AngularJS 和更现代的 Angular 迭代之间的差异之外,Angular 不幸地在开发人员中的流行度正在下降。此外,它相当冗长和复杂。根据一些 Angular 开发人员的说法,诸如使用第三方库之类的任务可能会重复。

使用 TypeScript 而不是标准的 ES6 也是一个值得关注的问题。虽然 TypeScript 很有用,但它增加了使用 Angular 的学习曲线。也就是说,Angular 确实非常灵活。

Angular 的例子

让我们用 Angular 构建一个小的“Hello World”应用程序。我们需要一些工具来开始我们的工作,比如npm。参考第二章,我们可以在服务器端使用 JavaScript 吗?当然可以!,来安装npm及其相关工具。如果您愿意,您也可以按照提供的代码在github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-8/angular-example进行操作。

以下是我们的步骤:

  1. 首先安装 Angular CLI:npm install -g @angular-cli

  2. 使用ng new example创建一个新的示例项目。按照提示接受此安装的默认设置。

  3. 进入刚刚创建的目录:cd example

  4. 启动服务器:ng serve --open

此时,您的网络浏览器应该在http://localhost:4200/打开此页面:

图 8.3 - 示例起始页面

好的。这看起来是一个足够简单的页面供我们使用。这是我们的 CLI 创建的文件结构:

.
├── README.md
├── angular-cli.json
├── e2e
│   ├── app.e2e-spec.ts
│   ├── app.po.ts
│   └── tsconfig.json
├── karma.conf.js
├── package-lock.json
├── package.json
├── protractor.conf.js
├── src
│   ├── app
│   │   ├── app.component.css
│   │   ├── app.component.html
│   │   ├── app.component.spec.ts
│   │   ├── app.component.ts
│   │   └── app.module.ts
│   ├── assets
│   ├── environments
│   │   ├── environment.prod.ts
│   │   └── environment.ts
│   ├── favicon.ico
│   ├── index.html
│   ├── main.ts
│   ├── polyfills.ts
│   ├── styles.css
│   ├── test.ts
│   └── tsconfig.json
└── tslint.json

让我们看一下生成的代码。打开src/index.html。您会看到:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Example</title>
  <base href="/">

  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
  <app-root></app-root>
</body>
</html>

就是这样!您看,这只是 Angular 创建我们刚刚查看的页面的模板,然后 JavaScript 完成其余工作。如果您在浏览器中查看页面的源代码,您会看到非常相似的内容,只是有一些脚本调用。所有 JavaScript 都是一次性下载或可能被分块成用于协同使用的块。

单页应用程序

值得讨论的是什么是 SPA。我们之前已经提到过这个话题,但现在让我们来看看为什么 Angular(以及我们即将介绍的 React 和 Vue)如此受欢迎和引人注目。想象一个标准的基于 HTML 的网站。它可能有一个一致的页眉、页脚和样式。然而,一个标准的网站需要在每次导航到不同页面时下载(或从本地缓存中提供)这些资产(更不用说检索 HTML 并重新呈现它了)。SPA 通过将所有相关数据打包到一个统一的包中,然后传输到浏览器中来消除这种冗余。浏览器然后解析 JavaScript 并呈现它。结果是一个快速、流畅的体验,基本上消除了页面加载时间的延迟。你已经使用过这些了。如果你使用 Gmail 或大多数现代在线电子邮件系统,你可能已经注意到页面加载时间是可以忽略的,或者最坏的情况下有一个小的加载图标。页面加载时间和表面上浪费的资源和内容重新下载是 SPA 旨在处理的一个问题。

既然我们已经讨论了 SPA 如何帮助提高我们的效率,让我们来看看我们的 Angular 示例背后的 JavaScript。

JavaScript

首先,让我们打开src/app/app.component.html,看看第 2 行:{{ title }}!

嗯,这些花括号是什么?如果你熟悉其他模板语言,你可能会认出这是一个模板标记,旨在在呈现之前被我们的呈现语言替换。那么,替换它的方法是什么?

现在让我们看看src/app/app.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'app works!';
}

我们可以看到模板引用了app.component.html,而我们的AppComponent类将title指定为app works!。这正是我们在浏览器中看到的。欢迎来到模板系统的强大之处!

现在,我们不会深入讨论 Angular 的 SPA 特性,但是请查看angular.io/tutorial上的 Angular 教程以获取更多详细信息。

现在,让我们继续我们的 React 之旅。

React 和 React Native

React 最初是由 Facebook 的 Jordan Walke 于 2013 年创建的,迅速发展成为目前使用最广泛的用户界面库之一。与 Angular 相比,React 并不试图成为一个完整的框架,而是专注于 Web 工作流的特定部分。由于 Web 页面本质上是无状态的(也就是说,没有真正的信息从页面传递到页面),SPA 旨在将某些状态的片段存储在 JavaScript 内存中,从而使后续视图能够填充数据。React 是这种类型架构如何工作的一个典型例子,同时又不包含整个框架范式。在 MVC 术语中,React 处理视图层。

React 的优势

由于 React 本身只处理视图,它依赖于其他库来补充其功能集,比如 React Router 和 Hooks。也就是说,React 的基本架构被设计为模块化,并且有附加组件用于执行工作流的其他部分。目前,了解 React Router、Hooks 或 Redux 并不重要,但要知道 React 只是完整网站中的一个部分。

那么,为什么这是一个优势呢?与一些其他 JavaScript 工具(如 Angular)不同,React 并不试图用自己的规则、法规或语言结构重新发明轮子。它感觉就像你在基本的 JavaScript 中编码,因为在大多数情况下,你确实是!

React 的另一个优势是它如何处理组件和模板。组件只是可重用的代码片段,可以在程序中的多个位置使用不同的数据来填充视图。React 还在reactjs.org/tutorial/tutorial.html上有一个很好的逐步教程。我们将在React 示例部分对此进行分析。现在,当然,我们需要讨论一下缺点。

React 的缺点

坦率地说,React 的学习曲线(尤其是它的新姐妹技术,如 Redux 和 Hooks,简化了基于状态的管理)是陡峭的。然而,对于社区来说,这甚至不被认为是一个主要的缺点,因为几乎所有的库和框架都是如此。然而,一个主要的缺点是它的快速发展速度。现在,你可能会想:“但是一个不断发展的技术是好事”!这是一个好想法,但在实践中,这可能有点令人生畏,特别是在处理重大变化时。

一些开发人员的另一个不喜欢的地方是在 JavaScript 中混合 HTML 和 JavaScript。它使用一种语法扩展,允许在 JavaScript 中添加 HTML,称为 JSX。对于纯粹主义者来说,将表示层代码混合到逻辑结构中可能会显得陌生和构架反模式。再次强调,JSX 有一个学习曲线。

现在是时候看一个经典的 React 示例应用程序了:井字棋。

React 示例

您可以按照逐步教程构建此应用程序,网址为reactjs.org/tutorial/tutorial.html,为了方便使用,您可以使用这个 GitHub 目录 - github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-8/react-tic-tac-toe - 完整的示例:

  1. 克隆存储库并cd进入react-tic-tac-toe目录。

  2. 执行yarn start

不要对新的yarn命令感到惊讶。这是一个类似于npm的不同的包管理器。

  1. yarn start完成后,它会为您提供一个类似于http://localhost:3000/的 URL。在浏览器中打开它。你应该看到这个:

图 8.4 - React 井字棋,开始

如果你不熟悉井字棋游戏,逻辑很简单。两名玩家轮流在 3x3 的网格中标记 X 或 O,直到一名玩家在横向、纵向或对角线上有三个标记。

让我们玩吧!如果你点击方框,你可能会得到以下结果:

图 8.5 - React 井字棋,可能的结束状态

请注意,示例还在屏幕右侧的按钮上保持状态历史。您可以通过单击按钮将播放倒带到这些状态中的任何一个。这是 React 如何使用状态来保持应用程序各部分的连续性的一个例子。

组件

为了说明可重用组件的概念,考虑一下井字棋网格的顶行代码。看一下src/index.js

你应该在第 27 行看到这个:

<div className="board-row">
  {this.renderSquare(0)}
  {this.renderSquare(1)}
  {this.renderSquare(2)}
</div>

renderSquare是一个相当简单的函数,它呈现 JavaScript XML,或JSX。如前所述,JSX 是 JavaScript 的扩展。它在标准 JavaScript 文件中引入了类似 XML 的功能,将 JavaScript 语法与一组 HTML 和 XML 结合起来构建我们一直在谈论的组件。它并不是自己的完全成熟的模板语言,但在某些方面,它实际上可能更强大。

这是renderSquare

renderSquare(i) {
  return (
    <Square
      value={this.props.squares[i]}
      onClick={() => this.props.onClick(i)}
    />
  );
}

到目前为止,一切都很好...看起来相当标准...除了一件事。什么是Square?那不是 HTML 标签!这就是 JSX 的威力:我们可以定义自己的可重用标签,就像我们一直在谈论的这些精彩的组件一样。把它们想象成我们可以用来组装自己应用程序的 LEGO®积木。从基本的构建块中,我们可以构建一个非常复杂的 SPA。

因此,Square只是一个返回标准 HTML 按钮的函数,具有一些属性,例如它的onClick处理程序。您可以在代码后面看到这个处理程序的作用:

function Square(props) {
  return (
    <button className="square" onClick={props.onClick}>
      {props.value}
    </button>
  );
}

我们只是初步了解了 React,但我希望你已经感受到了它的强大。事实上,它有望成为生态系统中主导的前端框架。在撰写本文时,React 在技术世界的工作机会数量上远远超过了 Angular。

React Native

谈论 React 而不提及 React Native 是不完整的。原生移动应用程序开发的一个困难之处在于,嗯,原生语言。Android 平台使用 Java,而 iOS 依赖 Swift 作为编程语言。我们不会在这里深入讨论移动开发(或 React Native),但重要的是要注意 React 和 React Native 之间存在重大差异。当我开始尝试 React 时,我以为组件在 React 和 React Native 之间是可重用的。在某种程度上,这是轻微正确的,但两者之间的差异超过了相似之处。

Native 的主要优势在于你不需要使用另一种语言;相反,你仍然在使用 JavaScript。话虽如此,Native 还存在额外的复杂性,特别是在处理移动设备的原生功能(如相机)时。因此,我建议您在项目生命周期中慎重考虑使用 React Native,并不要假设所有知识都可以从一个项目转移到另一个项目。

接下来,让我们讨论一下 JavaScript 世界的新成员:Vue.js。

Vue.js

JavaScript 框架生态系统中的另一个新成员是 Vue.js(通常简称为 Vue)。由 Evan You 于 2014 年开发,这是另一个旨在为单页应用程序和用户界面提供高级功能的开源框架。Evan You 认为 Angular 中有值得保留的部分,但还有改进的空间。这是一个值得赞赏的目标!有人可能会说该项目成功做到了这一点,而其他人则认为其他项目更优秀。然而,本章的目标不是对任何技术进行评判,而是让您了解 JavaScript 的各种扩展,以使您的工作更轻松,并更符合现代标准。

与 React 不同,Vue 包含路由状态构建工具。它也有一个学习曲线,就像许多类似的技术一样,所以如果您选择探索 Vue,请确保给自己足够的空间和时间来学习。

我们将在官方指南的基本示例中研究 Vue 的基本示例vuejs.org/v2/guide/。如果你查看声明性渲染部分的课程,你会发现一个 Scrimba 课程。随意观看教程或从github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-8/vue-tutorial访问代码,但以下是基础知识。

Vue 的 HTML 看起来与使用花括号标记进行内容替换的任何其他框架非常相似:

<html>
   <head>
       <link rel="stylesheet" href="index.css">
       <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
   </head>
   <body>

       <div id="app">
           {{ message }}
       </div>

       <script src="index.js"></script>
   </body>
</html>

值得注意的是,花括号语法可能会与其他模板系统(如 Mustache)发生冲突,但我们暂时将继续使用内置的 Vue 技术。

由于你有{{ message }}标记,让我们看看它的功能。

如果你查看index.js文件,你会发现它非常简单:

var app = new Vue({ 
    el: '#app',
    data: {
        message: 'Hello Vue!'
    }
});

这种基本结构应该看起来很熟悉:它是一个带有对象作为参数的类的实例化。请注意,数据元素包含一个带有值Hello Vue的消息键。这是传递给视图层的{{ message }},因此我们的应用程序呈现我们的消息:

图 8.6 - Vue 的“Hello World”示例

到目前为止,它的能力似乎与我们探索过的其他工具类似,所以让我们深入探讨其优缺点。

Vue.js 的特点

由于 Vue 在实践中唯一的竞争对手是 React,也许将这个比较留给你来决定就足够了:vuejs.org/v2/guide/comparison.html。然而,让我们以更客观的眼光来分析比较的一些要点,因为即使是比较的作者也承认它对 Vue 有偏见(这是可以预料的):

  • 性能:理想情况下,任何框架或库对应用程序的加载时间或实例化时间只会增加可忽略的时间,但实际情况却有所不同。我相信我们都记得多秒级的 Ajax 或 Flash(甚至是 Java servlet!)加载器的日子,但总的来说,这些延迟已经被异步、分步加载模式所缓解。现代 Web 技术的一个标志性细节应该是对用户体验的不显眼和渐进式增强。在这一点上,Vue 在增强用户体验方面做得非常出色。

  • HTML + JavaScript + CSS:Vue 允许技术的混合和匹配,它可以使用标准的 HTML、CSS 和 JavaScript 与 JSX 和 Vue 特定的语法相结合来构建应用程序。这是一个利弊参半的问题,但这是技术的事实。

  • Angular 的思想:与 React 拒绝几乎所有 Angular 约定不同,Vue 从 Angular 中借鉴了一些学习要点。这可能使它成为一个值得考虑的框架,适合想要离开 Angular 的人,尽管对这种方法的价值/效果尚未定论。

现在,让我们来看一个 Vue 的例子。

Vue.js 示例

让我们使用 Vue CLI 创建一个示例项目:

  1. 使用npm install -g @vue/cli安装 CLI。

  2. 在新目录中执行vue create vue-example。对于我们的目的,你可以在每个提示处按Enter使用默认选项。

  3. 进入目录:cd vue-example

  4. 使用yarn serve启动程序:

图 8.7 - Vue 生成器主页

Vue 的 CLI 生成器在vue-example目录中为我们创建了许多文件:

.
├── README.md
├── babel.config.js
├── package.json
├── public
│ ├── favicon.ico
│ └── index.html
├── src
│ ├── App.vue
│ ├── assets
│ │ └── logo.png
│ ├── components
│ │ └── HelloWorld.vue
│ └── main.js
└── yarn.lock

让我们来看看它为我们创建的部分:

  1. 打开src/App.vue。我们将在脚本块中看到这个:
import HelloWorld from './components/HelloWorld.vue'

export default {
 name: 'app',
 components: {
   HelloWorld
 }
}

我们在浏览器中看不到任何链接,但import行告诉我们内容在哪里。

  1. 打开src/components/HelloWorld.vue。现在,我们在<template>节点中看到了页面的内容。随意更改一些标记并尝试不同的变量。

这就是 Vue 的要点!你会发现在学习了 Angular 和 React 之后,Vue 中的概念是一个逻辑的进步,不难掌握。

总结

前端框架是强大的工具,但它们并不是可以互换的。每种框架都有其优缺点,你使用它们不仅应该受到当下流行的影响,还应该考虑到社区支持、性能考虑和项目的长期性。选择一个框架是一个需要仔细思考和规划的复杂过程。目前,React 在采用率上有相当大的增长,但随着时间的推移,所有的框架都会受到青睐和抛弃。我们在这里所涵盖的只是每个框架的冰山一角,所以在承诺之前一定要做好你的研究。

在下一章中,我们将探讨调试 JavaScript,因为让我们面对现实吧:我们会犯错误,我们需要知道如何修复它们。

进一步阅读

第九章:解读错误消息和性能泄漏

当然,没有一个好的语言是完整的,没有一种方法可以检测和诊断代码中的问题。JavaScript 提供了非常强大和直观的丰富错误消息,但在处理错误时有一些注意事项和技巧。

你可能知道,在自己的代码中找到问题(“bug”)是开发人员最沮丧的事件之一。我们以代码能够完成任务为傲,但有时我们没有考虑到边缘和特殊情况。此外,错误消息通过提供重要的诊断信息,给我们在编码过程中提供了重要的信息。幸运的是,有一些工具可以帮助我们理解 JavaScript 中发生的情况。

让我们来探索一下。

本章将涵盖以下主题:

  • 错误对象

  • 使用调试器和其他工具

  • 适应 JavaScript 的性能限制

技术要求

准备好在 GitHub 上的Chapter-9示例中进行工作,网址为github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-9/

我们将在浏览器中使用开发者工具,为了说明的目的,指南和截图将来自 Google Chrome。但如果你熟悉其他浏览器中的工具,概念是相似的。如果你还没有这样做,你可能还想在 Chrome 中添加一个 JSON 解析扩展。

本章没有特定的硬件要求。

错误对象

让我们看一下github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-9/error-object。打开index.html文件并检查 JavaScript 控制台。第一个函数typoError被调用并抛出了一个精彩的错误。

它应该看起来像这样:

图 9.1 - 错误控制台

现在,让我们看看index.js中我们函数的代码:

const typoError = () => {
  cnosole.error('my fault')
}

好了!这只是一个简单的拼写错误,我们都犯过:应该是console.error而不是cnosole.error。如果你在代码中从未犯过拼写错误……你是一个独角兽。我们在控制台中看到的错误消息使我们很容易看到错误是什么,以及它存在于代码的哪一行:第 2 行。现在,有趣的是,在文件末尾调用typoError()之后,我们还调用了另一个函数但它没有触发。我们知道这是因为(剧透警告)它也抛出了错误,但我们没有看到它们。未捕获的引用错误是一个阻塞错误

在 JavaScript 中,一些错误称为阻塞错误,将停止代码的执行。其他一些称为非阻塞错误,以这样的方式进行缓解,即使问题没有解决,代码仍然可以继续执行。处理错误的方法有几种,当面临潜在的错误向量时,你应该这样做。你还记得第七章吗,事件、事件驱动设计和 API,我们在fetch()调用中使用了.catch()块来优雅地处理 Ajax 错误?同样的原则也适用于这里。这显然是一个非常牵强的例子,但让我们继续缓解我们的错误,就像这样:

const typoError = () => {
  try {
    cnosole.error('my fault')
  } catch(e) {
    console.error(e)
  }
}

对于拼写错误使用try/catch块是杀鸡用牛刀,但让我们假装它是更严重的问题,比如异步调用或来自另一个库的依赖。如果我们现在查看控制台输出,我们会看到我们的第二个函数fetchAttempt已经触发,并且也产生了错误。打开index-mitigated.js文件和相应的index-mitigated.html文件。

你应该在index-mitigated.html的控制台中看到这个:

图 9.2 - 非阻塞错误

在这里,我们看到我们的代码并没有在拼写错误处停止;我们已经通过 try/catch 将其变成了一个非阻塞错误。我们看到我们的fetchAttempt函数正在触发并给我们一个不同类型的错误:404 Not Found。由于我们输入了一个不存在的 URL(故意以undefined结尾),之后我们收到了另一个错误:来自我们的 promise 的SyntaxError

乍一看,这个错误可能很难理解,因为它明确地谈到了 JSON 中的意外字符。在第七章中,事件、事件驱动设计和 API,我们使用了星球大战 API:https://swapi.dev/

  1. 让我们来看一下从https://swapi.dev/api/people/1/获取的示例响应的 JSON。这可能是一个很好的时机,确保你的浏览器中有一个 JSON 解析扩展:

图 9.3 - 来自 https://swapi.dev/api/people/1/的 JSON

  1. 它是格式良好的 JSON,所以即使我们的错误指定了语法错误,实际上问题并不在于响应数据的语法。我们需要更深入地了解一下。让我们看看我们在 Chrome JavaScript 调试器中从fetchAttempt调用中得到的内容。让我们点击这里我们代码中的第二个错误的链接:

图 9.4 - 跟踪 404 的路径...

然后我们看到这个面板,有红色的波浪线和红色的标记表示错误:

图 9.5 - 调试器中的错误

  1. 到目前为止,一切都很好。如果你在第 20 行上悬停在红色 X 上,工具提示会告诉我们有 404 错误。

  2. 导航到网络选项卡。这个工具跟踪传入和传出的 HTTP 请求。

  3. 点击名为 undefined 的调用,然后进入头部面板,就像这样:

图 9.6 - 头部选项卡

啊哈!现在我们知道问题所在了:JSON 错误是有帮助的,但是让我们走错了方向。错误不在于 JSON 本身,而是错误意味着响应根本就不是 JSON!这是一个 HTML 404 错误,所以没有 JSON 数据。我们的问题被确认为在获取一个不存在的地址的 URL 中,因此会呈现一个错误页面,这对于fetch的 JSON 解析器来说是没有意义的。

让我们花更多的时间来使用调试工具。

使用调试器和其他工具

许多 Web 开发人员选择使用 Google Chrome 作为他们的首选浏览器,因为它提供了丰富的开发工具。如果 Chrome 不是你的首选浏览器,这里有一些具有类似开发工具的浏览器。

Safari

Safari 默认情况下不带开发者模式,所以如果你使用 Safari,切换到首选项的高级面板中的开发菜单:

图 9.7 - 在 Safari 中添加开发菜单

现在,你将拥有一个带有工具的开发菜单,这些工具可能会以与 Chrome 略有不同的方式呈现错误消息,但仍然可以访问。

Internet Explorer 和 Microsoft Edge

真诚地并且只有一点点偏见,我建议不要在 JavaScript 开发中使用 Internet Explorer 或 Microsoft Edge。跨浏览器测试很重要,但我发现 IE 和 Edge 提供的开发工具有所欠缺。例如,让我们在 Edge 的开发工具中看一下完全相同的页面:

图 9.8 - Edge JavaScript 控制台

尽管我们用 try/catch 块减轻了错误,Edge 仍然将拼写错误视为阻塞错误。微软浏览器还有其他特殊之处,这些特殊之处可以追溯到我们之前学到的浏览器战争,所以一个好的经验法则是在 Chrome 中开发,然后在微软浏览器中测试,以确保跨浏览器兼容性。

虽然所有主要浏览器都有开发工具,但这里使用的示例将来自 Chrome。让我们更仔细地看看 JavaScript 控制台本身。

JavaScript 控制台

控制台不仅是查看错误的地方,还可以用来执行代码。这对于快速调试特别有用,特别是在页面上可能包含另一个代码库的情况下。只要从顶层window对象访问,控制台就可以访问页面上加载的所有 JavaScript 的作用域。我们不希望访问函数的内部变量,但如果浏览器可以访问数据,我们就可以在控制台中访问它。

debugger文件夹中打开fetch.htmlfetch.js文件并查看。这是fetch.js文件:

const fetchAttempt = (url) => {
  fetch(url)
    .then((response) => {
        return response
    }).then((data) => {
      if (data.status === 500) {
        console.log("We got a 500 error")
      }
      console.log(data)
      }).catch((error) => {
        throw new Error(error)
    })
}

这是一个简单的fetch请求,其中 URL 作为参数传递给我们的函数。在我们的 HTML 页面的控制台中,我们实际上可以执行这个函数,就像这样:

图 9.9 - 在控制台中执行代码

当你输入fetchAttempt('http://httpstat.us/500')时,你是否注意到控制台给出了自动完成的代码提示?这是另一个有用的工具,用于确定你是否可以访问你正在工作的级别的函数和变量。现在我们看到我们可以在控制台中执行代码,而不必修改我们的 JavaScript 文件。我们从控制台学到了什么?我们的data.status确实是500,所以我们从第 7 行抛出了控制台错误。从第 9 行,我们得到了我们的响应数据,它明确说明了500。可能不用说,但console.logconsole.errorconsole.info函数在调试 JavaScript 时可能非常有价值。经常使用它们,但记得在将代码推送到生产级环境之前将它们删除,因为如果记录大对象或记录太频繁,它们可能会降低站点性能。

JavaScript 的一个棘手之处在于,你可能要处理数百行代码,有时还是来自第三方。幸运的是,大多数浏览器的工具允许在代码中设置断点,这会在指定的点中断代码的执行。让我们在控制台中看看我们之前的文件,并设置一些断点。如果我们点击第 7 行的错误,源面板将显示。如果你点击行号,你将设置一个断点,就像这样:

图 9.10 - 注意第 6 行上的箭头标记

在浏览器报错的那一行之前设置断点通常很有用,以便更彻底地跟踪传递给我们代码的变量。让我们再次运行我们的代码,刷新页面,看看会发生什么:

  1. 在第 6 行和第 7 行设置断点。

  2. 刷新页面。

  3. 导航到控制台并执行我们之前的命令:fetchAttempt('http://httpstat.us/500')

浏览器将再次拉起源选项卡,我们应该看到类似于这样的东西:

图 9.11 - 断点的结果

我们可以看到在作用域选项卡中,我们得到了在执行代码的上下文中定义的变量列表。然后,我们可以使用步骤按钮,如截图所示,继续移动到我们的断点并执行后续的代码行:

图 9.12 - 步骤按钮

当我们通过断点时,作用域面板将更新以显示我们当前的上下文,这比显式的console.log函数给我们更多的信息。

现在让我们看看如何改进 JavaScript 代码以提高性能的一些想法。

适应 JavaScript 的性能限制

与任何语言一样,有写 JavaScript 的方法,也有更好的写法。然而,在其他语言中不那么明显的是,您的代码对网站用户体验的直接影响。复杂、低效的代码可能会使浏览器变慢,消耗 CPU 周期,并且在某些情况下甚至会导致浏览器崩溃。

看一下 Talon Bragg 在hackernoon.com/crashing-the-browser-7d540beb0478上的这个简单的四行代码片段:

txt = "a";
while (1) {
    txt = txt += "a"; // add as much as the browser can handle
}

警告不要在浏览器中尝试运行这个代码!如果您对此感到好奇,它最终会在浏览器中创建一个内存不足的异常,导致标签被关闭,并显示页面已经无响应的消息。为什么会这样?我们的while循环的条件是一个简单的真值,因此它将继续向字符串文本添加"a",直到分配给该浏览器进程的内存耗尽。根据您的浏览器行为,它可能会崩溃标签、整个浏览器,或者更糟。我们都有不稳定程序的经验(Windows 蓝屏,有人吗?),但通常可以避免浏览器崩溃。除了编码最佳实践,如最小化循环和避免重新分配变量之外,还有一些特定于 JavaScript 的想法需要指出。W3Schools 有一些很有用的例子,可以在www.w3schools.com/js/js_performance.asp找到,我想特别强调其中的一个。

在标准 JavaScript 应用程序中,最占用内存的操作之一是 DOM 访问。像document.getElementById("helloWorld")这样简单的一行代码实际上是一个相当昂贵的操作。作为最佳实践,如果您在代码中要多次使用 DOM 元素,您应该将其保存到一个变量中,并对该变量进行操作,而不是返回到 DOM 遍历。如果回想一下第六章:文档对象模型(DOM),我们将便利贴 DOM 元素存储为一个变量:github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/blob/master/chapter-6/stickies/solution-code/script.js#L13

内存面板

不要深入讨论计算机如何分配内存的细节,可以说,编写不当的程序可能会导致内存泄漏,因为它没有正确释放和回收内存,这可能导致程序崩溃。与一些低级语言相反,JavaScript 应该自动进行垃圾回收:自动内存管理的实践,通过销毁不需要的数据片段来释放内存。然而,有些情况下,编写不当的代码可能会导致垃圾回收无法处理的内存泄漏。

由于 JavaScript 在客户端运行,很难准确解释程序中到底发生了什么。幸运的是,有一些工具可以帮助。让我们通过一个将分配大量内存的程序示例来进行演示。看一下这个例子:github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/blob/master/chapter-9/memory-leak/index.html.

如果您查看包含的 JavaScript 文件,您会发现它非常简单,但非常强大:

// Based on https://developers.google.com/web/tools/chrome-devtools/memory-problems

let x = []
const grow = (log = false) => {
  x.push(new Array(1000000).join('x'))
  if (log) {
    console.log(x)
  }
}

document.getElementById('grow').addEventListener('click', () => grow())
document.getElementById('log').addEventListener('click', () => grow(true))

让我们检查我们的代码,并看看当我们使用这个简单的脚本时会发生什么。请注意,这些说明可能会有所不同,具体取决于您的浏览器和操作系统版本:

  1. 在 Chrome 中打开index.html页面。

  2. 打开开发者工具。

  3. 从“更多工具”菜单中,选择性能监视器:

图 9.13 - 调查性能监视器

您将看到一个带有移动时间线的面板:github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/blob/master/chapter-9/memory-leak/memory-leak.gif.

  1. 现在,点击几次 Grow 按钮。您应该看到 JavaScript 堆大小增加,可能达到 13MB 范围。但是,随着您不断点击,堆大小不应该超过已经存在的范围。

为什么会这样?在现代浏览器中,意外创建内存泄漏实际上变得有点困难。在这种情况下,Chrome 足够聪明,可以对内存进行一些技巧处理,不会因我们重复操作而导致内存大幅增加。

  1. 然而,现在开始点击 Log 按钮几次。您将在控制台中看到输出以及堆大小的增加:

图 9.14 - 内存堆调查

注意图表的增长。然而,随着时间的推移,如果停止点击 Log,内存分配实际上会下降。这是 Chrome 智能垃圾回收的一个例子。

摘要

我们在编码时都会犯错误,知道如何找到、诊断和调试这些问题是任何语言中的关键技能。在本章中,我们已经看到了 Error 对象和控制台如何为我们提供丰富的诊断信息,包括错误发生的位置、对象上附加的详细信息以及如何阅读它们。不要忘记,有时错误可能在表面上看起来是一种方式(我们在错误对象部分的 JSON 错误),不要害怕尝试使用控制台语句和断点来跟踪代码。

由于 JavaScript 在客户端运行,因此重要的是要牢记用户的性能容量。在编写 JavaScript 时有许多最佳实践,例如重用变量(特别是与 DOM 相关的变量),因此请务必确保使您的代码 DRY(不要重复自己)。

在下一章中,我们将结束前端的工作,并了解 JavaScript 真正是前端的统治者。

问题

  1. 内存问题的根本原因是什么?

  2. 您程序中的变量是全局的。

  3. 低效的代码。

  4. JavaScript 的性能限制。

  5. 硬件不足。

  6. 在使用 DOM 元素时,应将对它们的引用存储在本地,而不是始终访问 DOM。

  7. 正确

  8. 错误

  9. 在多次使用时为真

  10. JavaScript 在服务器端进行预处理,因此比 Python 更高效。

  11. 正确

  12. 错误

  13. 设置断点无法找到内存泄漏。

  14. 正确

  15. 错误

  16. 将所有变量存储在全局命名空间中是个好主意,因为它们更有效地引用。

  17. 正确

  18. 错误

进一步阅读

有关更多信息,您可以使用以下链接:

第十章:JavaScript,前端的统治者

如果您开始领会 JavaScript 对现代网站和 Web 应用程序功能的重要性,那么您正在走上正确的道路。没有 JavaScript,我们在网页上理所当然的大多数用户界面都不会存在。让我们更仔细地看看 JavaScript 如何将前端整合在一起。我们将使用一些 React 应用程序,并比较和对比 Python 应用程序,以进一步了解 JavaScript 在前端的重要性的原因和方式。

本章将涵盖以下主题:

  • 构建交互

  • 使用动态数据

  • 了解现代应用程序

技术要求

准备好使用存储库中Chapter-10目录中提供的代码进行工作,网址是github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-10。由于我们将使用命令行工具,因此请确保您的终端或命令行 shell 可用。我们需要一个现代浏览器和一个本地代码编辑器。

构建交互

让我们看一个简单的单页应用程序SPA):

  1. 导航到chapter-10中的simple-reactjs-app目录(cd simple-reactjs-app)。

  2. 使用npm install安装依赖项。

  3. 使用npm start运行应用程序。

  4. http://localhost:3000访问应用程序。您会看到以下内容:

图 10.1 - 简单的 React 应用程序

当您单击详细按钮并检查网络选项卡时,您会发现页面不会重新加载,它只会从服务器加载 JSON 数据。这是单页应用程序功能的一个非常基本的示例:使用最少的服务器使用,用户体验的交互被简化,有助于高效、低开销的工作流程。您可能熟悉其他单页应用程序,如 Gmail、Google 地图和 Facebook,尽管底层技术有所不同。

在互联网技术时代,JavaScript 可能被视为理所当然,但它是这些应用程序工作的基础。没有 JavaScript,我们将有大量的页面重新加载和长时间等待,即使使用 Ajax 也是如此。

让我们通过比较和对比一个基本的 Python 示例和一个现代的 React 应用程序来看看如何使用动态数据。

使用动态数据

让我们首先看一个 Python Flask 示例:

  1. 导航到chapter-10中的flask目录(cd flask)。

  2. 您需要安装一些组件以进行设置。以下说明适用于 Python:

  3. 使用python3 -m venv env创建虚拟环境。

  4. 使用. env/bin/activate激活它。

  5. 安装要求:pip3 install -r requirements.txt

  6. 现在您可以启动应用程序:python3 app.py

  7. http://localhost:5000访问页面。您会看到这个:

图 10.2 - 基本 Flask 应用程序

尝试输入和不输入您的姓名,并观察页面在这样做时重新加载的事实(我添加了时间戳,以便更容易看到页面重新加载可能发生得太快而看不到)。这是一个非常基本的 Flask 应用程序,有更有效的方法可以使用 Python 和 JavaScript 的组合进行验证工作,但在基本水平上,即使使用一些基于 Flask 的表单验证工具,我们所看到的验证和交互也是在后端进行的。每次我们点击提交时,服务器都会被访问。以下截图显示了如果您不输入字符串的服务器端验证:

图 10.3 - 基本 Flask 验证

请注意时间戳的更改,表示服务器重新渲染。

通过修改我们简单的 React 应用程序,让我们为我们的表单验证交互做得更好:

  1. 导航到reactjs-app-form目录:cd reactjs-app-form

  2. 安装依赖项:npm install

  3. 启动服务器:npm start

  4. http://localhost:5000访问页面。这是我们简单应用的更新版本:

图 10.4 - 具有动态数据的简单应用

现在尝试使用它,并注意如果您更改一个主要字段,左侧的字段也会更改。此外,它会在您编辑时保存 JSON,因此如果您刷新页面,您的更改将保留。这要归功于 JavaScript 的强大功能:React 前端正在处理您在应用程序各个部分进行的所有更改,然后 Express 后端正在提供和保存 JSON 文件。在这种情况下,页面上标记的更新是实时发生的。当然,每次编辑时我们都会与服务器进行保存和读取操作,但这是因为应用程序的设计方式。要保持更改,创建一个保存按钮而不是在字段更改时进行保存将是微不足道的。

如果您想使用这个示例,您需要做一些事情:

  1. 首先,在新的 shell 窗口中导航到目录(保留之前的实例运行):cd client

  2. 执行npm install

  3. 开始程序:npm start

然后,Express 服务器将收集由 React 运行过程创建的构建文件,与已经存在于目录中的预构建文件进行比较。

输入验证和错误处理

关于动态数据的一个重要部分是输入验证错误处理。请注意,在我们的应用程序中,如果电子邮件字段为空或者我们没有输入有效的电子邮件,它将有一个红色轮廓。否则,它将有一个绿色轮廓。当您输入有效的电子邮件地址并选择下一个字段时,您会发现红色轮廓会在不与服务器交互的情况下(除了保存数据,正如我们之前讨论的那样)变为绿色。这是客户端验证,当创建流畅的用户体验时非常强大:用户不必点击保存并等待服务器响应,以查看他们是否输入了不正确的数据。

在处理电话字段时,您可能已经注意到一个细节:它被限制为数字。如果您查看client/src/CustomerDetails.js,我们在这里将类型限制为数字:

<Input name="phone" type="number" value={this.state.customerDetails.data.phone || ''} onChange={this.handleChange} />

这里还有一些其他的 React 部分。让我们来看一下handleChange函数:

handleChange(event) {
   const details = this.state.customerDetails
   details.data[event.target.name] = event.target.value
   this.validate(event.target)

   this.setState({ customerDetails: details })
   console.log(this.state.customerDetails)

   axios.post(`${CONSTANTS.API_ROOT}/api/save/` + 
   this.state.customerDetails.data.id, details)
     .then(() => {
       this.props.handler();
     })
 }

Axios 是一个简化 Ajax 调用的库,我在这里使用它而不是fetch只是为了演示。您可能会在 React 工作中看到 Axios 被使用,尽管您始终可以选择使用原始的fetch。但是,让我们专注于this.validate(event.target)这一行。

这是函数的内容:

validate(el) {
   const properties = (el.name) ? el : el.props

   if (properties.name === 'email') {
     if (validateEmail(properties.value)) {
       this.setState({ validate: { email: true }});
     } else {
       this.setState({ validate: { email: false }});
     }
   }
 }

validateEmail()是一个神奇的函数!您可以在client/src/validation.js中找到它,它使用正则表达式来模式匹配输入字符串,以查看它是否看起来像一个正确格式的电子邮件地址。然后,根据函数返回truefalse,我们设置一个验证状态,React 将使用它来设置电子邮件字段的边框颜色。

前端验证和错误处理对于流畅的用户体验非常重要,但这只是故事的一部分。另一部分是安全性。

安全和数据

正如您从浏览器中的开发者工具中了解的那样,如果您努力尝试,几乎可以规避任何前端限制。例如,对于我们的电话字段,尽管我们在前端限制了它,但我们总是可以检查 HTML 并输入任何我们想要的值。一个快速的提示是,也很重要在后端验证您的数据,以确保它格式正确。

企业数据泄露和黑客攻击的一个共同点是攻击者利用了系统中的弱点。很少是密码泄露的情况;更常见的是弱加密或甚至是前端问题。我们将在第十七章中进一步讨论安全性和密钥。您可以在OWASP.org了解更多信息。

让我们继续回顾我们所学到的东西。

理解现代应用程序

在这一点上,毫不奇怪的是,所有现代 Web 应用程序都与 JavaScript 紧密联系在一起。没有它,交互就无法实时发生。服务器端有其位置和重要性,但用户看到和交互的关键是由 JavaScript 控制的。

就像 CSS 是 HTML 的补充一样,JavaScript 是这个组合中的第三个朋友,通过一系列标记和样式创建有意义的体验。作为 Web 应用程序的肌肉,它为我们提供丰富的交互和逻辑,并且是所有单页应用程序的基础。它真的是一个神奇而美丽的工具。

总结

通过 JavaScript,我们可以超越“网页”,创建完整的 Web 应用程序。从电子邮件系统到银行,再到电子表格,几乎任何您使用计算机的东西,JavaScript 都可以帮助您。

在下一章中,我们将使用 Node.js 在服务器端使用 JavaScript。我们不会完全抛弃前端,而是会看到它们如何联系在一起。

第三部分 - 后端:Node.js vs. Python

现在我们已经看到了 JavaScript 在前端的用法(一个新的,可能令人恐惧的地方),让我们换个角度,转向后端。当在后端使用时,Node.js 与 Python 有更多的共同之处,但也有显著的区别。让我们构建一个应用程序来探索 Node.js 的工作原理。

在本节中,我们将涵盖以下章节:

  • 第十一章,什么是 Node.js?

  • 第十二章,Node.js vs. Python

  • 第十三章,使用 Express

  • 第十四章,React 与 Django

  • 第十五章,将 Node.js 与前端结合使用

  • 第十六章,进入 Webpack

第十一章:什么是 Node.js?

现在我们已经研究了 JavaScript 在前端的使用,让我们深入探讨它在“JavaScript 无处不在”范式中的作用,使用 Node.js。我们在第二章,我们可以在服务器端使用 JavaScript 吗?当然可以!中讨论了 Node.js,现在是时候更深入地了解我们如何使用它来创建丰富的服务器端应用程序了。

本章将涵盖以下主题:

  • 历史和用法

  • 安装和用法

  • 语法和结构

  • Hello, World!

技术要求

准备好在存储库的 Chapter-11 目录中使用提供的代码:github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-11。由于我们将使用命令行工具,还需要准备好终端或命令行 shell。我们需要一个现代浏览器和一个本地代码编辑器。

历史和用法

Node.js 首次发布于 2009 年,已被行业中的大公司和小公司广泛采用。在 Node.js 中有成千上万的可用包,为用户和开发者社区创造了丰富的生态系统。与任何开源项目一样,社区支持对于技术的采用和长期性至关重要。

从技术角度来看,Node.js 是一个单线程事件循环的运行时环境。在实践中,这意味着它可以处理成千上万个并发连接,而无需在上下文之间切换时产生额外开销。对于那些更熟悉其他架构模式的人来说,单线程可能看起来有些违反直觉,过去它曾被视为 Node.js 感知到的断点的一个例子。然而,可以说 Node.js 系统的稳定性和可靠性已经证明了这种范式是可持续的。有办法增加服务器处理请求的能力,但应该注意的是,这比简单地向问题投入更多硬件资源要复杂一些。如何扩展 Node.js 超出了本书的范围,但涉及到底层库 libuv 的技术。

在撰写本文时,Node.js 最大的优势可能是推动 Twitter。根据 SimilarTech 的数据,其每月 43 亿次访问证明了其强大的力量。现在,我相信 Twitter 团队多年来在推动平台方面做了一些令人难以置信的架构工作,我们很少再看到著名的 Twitter “fail whale”;我认为依赖 Node.js 是一个有助于提供可持续性和可靠性的好事情。

继续使用它!

安装和用法

安装 Node.js 的最简单方法是使用 nodejs.org 提供的安装程序。这些包将指导您在系统上安装 Node.js。确保还安装了 npm,Node 的包管理器。您可以参考第三章,细枝末节的语法,了解更多安装细节。

让我们试一试:

  1. 打开一个终端窗口。

  2. 输入 node。您将看到一个简单的 >,表示 Node.js 正在运行。

  3. 输入 console.log("Hi!") 并按 Enter

就是这么简单!通过两次按 Ctrl + C 或输入 .exit 来退出命令提示符。

所以,这相当基础。让我们做一些更有趣的事情。这是 chapter-11/guessing-game/guessing-game.js 的内容:

const readline = require('readline')
const randomNumber = Math.ceil(Math.random() * 10)

const rl = readline.createInterface({
 input: process.stdin,
 output: process.stdout
});

askQuestion()

function askQuestion() {
 rl.question('Enter a number from 1 to 10:\n', (answer) => {
   evaluateAnswer(answer)
 })
}

function evaluateAnswer(guess) {
 if (parseInt(guess) === randomNumber) {
   console.log("Correct!\n")
   rl.close()
   process.exit(1)
 } else {
   console.log("Incorrect!")
   askQuestion()
 }
}

使用 node guessing-game.js 运行程序。从代码中您可能能够看出,程序将在 1 到 10 之间选择一个随机数,然后要求您猜测它。您可以在命令提示符中输入数字来猜测这个数字。

让我们在下一节中分解这个示例。

语法和结构

Node.js 的伟大之处在于您已经知道如何编写它!举个例子:

JavaScript Node.js
console.log("Hello!") console.log("Hello!")

这不是一个技巧:它是相同的。Node.js 在语法上几乎与基于浏览器的 JavaScript 相同,甚至包括 ES5 和 ES6 之间的区别,正如我们之前讨论过的。根据我的经验,Node.js 中仍然存在大量使用 ES5 风格的代码,因此您会看到使用var而不是letconst的代码,以及大量使用分号。您可以查看第三章,细枝末节的语法,了解更多关于这些区别的信息。

在我们的猜数字游戏示例中,我们看到了一个对我们来说是新的东西 - 第一行:

const readline = require('readline')

Node.js 是一个模块化系统,这意味着并非所有语言的部分都会一次性引入。相反,当发出require()语句时,将包含模块。其中一些模块将内置到 Node.js 中,如readline,而另一些将通过 npm 安装(更多内容将在后面介绍)。我们使用readline.createInterface()方法创建一种使用我们的输入和输出的方式,然后我们猜数字游戏程序的其余代码应该是有些意义的。它只是会一遍又一遍地问问题,直到输入的数字等于程序生成的随机数:

function evaluateAnswer(guess) {
 if (parseInt(guess) === randomNumber) {
   console.log("Correct!\n")
   rl.close()
   process.exit(1)
 } else {
   console.log("Incorrect!")
   askQuestion()
 }
}

让我们看一个例子,从文件系统中读取文件,这是我们无法从普通的客户端 Web 应用程序中做到的。

客户查找

查看customer-lookup目录,github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-11/customer-lookup,并使用node index.js运行脚本。这很简单:

const fs = require('fs')
const readline = require('readline')

const rl = readline.createInterface({
 input: process.stdin,
 output: process.stdout
});

const customers = []

getCustomers()
ask()

function getCustomers() {
 const files = fs.readdirSync('data')

 for (let i = 0; i < files.length; i++) {
   const data = fs.readFileSync(`data/${files[i]}`)
   customers.push(JSON.parse(data))
 }
}

function ask() {
 rl.question(`There are ${customers.length} customers. Enter a number to 
 see details:\n`, (customer) => {
   if (customer > customers.length || customer < 1) {
     console.log("Customer not found. Please try again")
   } else {
     console.log(customers[customer - 1])
   }
   ask()
 })
}

其中一些看起来很熟悉,比如readline接口。不过,我们正在使用一些新的东西:const fs = require('fs')。这是引入文件系统模块,以便我们可以处理存储在文件系统上的文件。如果您查看数据目录,您会发现四个基本的 JSON 文件。

getCustomers()函数中,我们要做三件事:

  1. 使用readdirSync获取数据目录中文件的列表。在处理文件系统时,您可以以同步或异步方式与系统进行交互,类似于与 API 和 Ajax 进行交互。为了方便起见,在本例中,我们将使用同步文件系统调用。

  2. 现在files将是数据目录中文件的列表。循环遍历文件并将内容存储在data变量中。

  3. 将解析后的 JSON 推送到customers数组中。

到目前为止一切顺利。ask()函数也应该很容易理解,因为我们只是查看用户输入的数字是否存在于数组中,然后返回相关文件中的数据。

现在让我们看看如何在 Node.js 中使用开源项目来实现一个(相当愚蠢的)目标:创建照片的文本艺术表示。

ASCII 艺术和包

我们将使用 GitHub 存储库中的指令www.npmjs.com/package/asciify-image

图 11.1 - 我的 ASCII 艺术表示!

以下是逐步安装步骤:

  1. 创建一个名为ascii-art的新目录。

  2. cd ascii-art

  3. npm init。您可以接受 npm 提供的默认值。

  4. npm install asciify-image

现在,让我们来玩一些游戏:

  1. ascii-art目录中放置一张图片,比如一个大小不超过 200 x 200 像素的 JPEG。命名为image.jpg

  2. 在目录中创建index.js并打开它。

  3. 输入此代码:

const asciify = require('asciify-image')

asciify(__dirname + '/image.jpg', { fit: 'box', width: 25, height: 25}, (err, converted) => {
 console.log(err || converted)
})
  1. 使用node index.js执行程序,并查看你美妙的艺术作品!根据你的终端颜色,你可能需要使用一些选项来改变颜色以在浅色背景上显示。这些选项在之前链接的 GitHub 存储库中有文档记录。

我们在这里展示了什么?首先,我们使用 npm 初始化了一个项目,然后安装了一个依赖项。如果你注意到了,运行这些命令为你创建了一些文件和目录。你的目录结构应该看起来接近这样:

.
├── image.jpg
├── index.js
├── node_modules

├── package-lock.json
└── package.json

node_modules目录里会有更多的文件。如果你熟悉 Git 等源代码控制,你会知道node_modules目录应该始终被忽略,不要提交到源代码控制。

让我们来看看package.json,它看起来会类似于这样:

{
 "name": "ascii-art",
 "version": "1.0.0",
 "description": "",
 "main": "index.js",
 "dependencies": {
   "asciify-image": "⁰.1.5"
 },
 "devDependencies": {},
 "scripts": {
   "test": "echo \"Error: no test specified\" && exit 1"
 },
 "author": "",
 "license": "ISC"
}

如果我们稍微分析一下,我们会发现这个 npm 入口点到我们的程序实际上相当简单。有关项目的一些元数据,一个带有版本的依赖对象,以及一些我们可以用来控制我们的项目的脚本。

如果你熟悉 npm,你可能已经使用npm start命令来运行一个项目,而不是手动输入node。然而,在我们的package.json中,我们没有一个启动脚本。让我们添加一个。

修改scripts对象看起来像这样:

"scripts": {
   "test": "echo \"Error: no test specified\" && exit 1",
   "start": "node index.js"
 },

不要忘记注意你的逗号,因为这是有效的 JSON,如果逗号使用不当,它将会中断。现在,要启动我们的程序,我们只需要输入npm start

这是 npm 脚本的一个非常基本的例子。在 Node.js 中,习惯使用package.json来控制所有构建和测试的脚本。你可以按自己的喜好命名你的命令,并像这样执行它们:npm run my-fun-command

对于我们接下来的技巧,我们将从头开始创建一个“Hello, World!”应用程序。然而,它将做的不仅仅是打招呼。

你好,世界!

创建一个名为hello-world的新目录,并使用npm init初始化一个 node 项目,类似于我们之前的做法。在第十三章,使用 Express中,我们将使用 Express,一个流行的 Node.js 网络服务器。然而,现在,我们将使用一种非常简单的方法来创建一个页面。

开始你的index.js脚本如下:

const http = require('http')

http.createServer((req, res) => {
 res.writeHead(200, {'Content-Type': 'text/plain'})
 res.end("Hello, World!")
}).listen(8080)

fsreadline一样,http内置在 Node 中,所以我们不必使用npm install来获取它。相反,这将直接使用。在你的package.json文件中添加一个启动脚本:

"scripts": {
   "test": "echo \"Error: no test specified\" && exit 1",
   "start": "node index.js"
 },

然后启动它!

图 11.2 - 执行 npm start

好的,我们的输出并不是特别有用,但是如果我们阅读我们的代码,我们可以看到我们已经做到了这一点:“创建一个监听端口8080的 HTTP 服务器。发送一个 200 OK 消息并输出'Hello, World!'”。现在让我们打开浏览器并转到localhost:8080。我们应该看到一个简单的页面向我们问候。

太好了!到目前为止很容易。用Ctrl + C停止你的服务器,然后让我们继续编码。

如果我们能够使用我们在上一个例子中使用的 ASCII 艺术生成器来要求用户输入,然后在浏览器中显示图像,那该多好啊?让我们试试看。

首先,我们需要运行npm install asciify-image,然后让我们尝试这段代码:

const http = require('http')
const asciify = require('asciify-image')

http.createServer((req, res) => {
 res.writeHead(200, {'Content-Type': 'text/html'})
 asciify(__dirname + '/img/image.jpg', { fit: 'box', width: 25, height: 25
  }, (err, converted) => {
   res.end(err || converted)
 })
}).listen(8080)

这与我们之前输出到命令行的方式类似,但是我们使用http服务器res对象来发送一个回复。用npm start启动你的服务器,让我们看看我们得到了什么:

图 11.3 - 原始输出

好吧,这与我们想要看到的完全不一样。这就是问题所在:我们发送给浏览器的是ANSI 编码的文本,而不是实际的 HTML。我们需要做一些工作来转换它。再次退出服务器然后…

等一下。为什么我们必须不断地启动和停止服务器?事实证明,我们 真的必须这样做。有一些工具可以在文件更改时重新加载我们的服务器。让我们安装一个叫做supervisor的工具:

  1. npm install supervisor

  2. 修改你的package.json启动脚本以读取supervisor index.js

现在使用npm start启动服务器,当你编码时,服务器将在保存后重新启动,使开发速度更快。

回到代码。我们需要一个将 ANSI 转换为 HTML 的包。使用npm install安装ansi-to-html,然后让我们开始:

const http = require('http')
const asciify = require('asciify-image')
const Convert = require('ansi-to-html')
const convert = new Convert()

http.createServer((req, res) => {
 res.writeHead(200, {'Content-Type': 'text/html'})
 asciify(__dirname + '/img/image.jpg', { fit: 'box', width: 25, height: 25 
  }, (err, converted) => {
   res.end(convert.toHtml(err || converted))
 })
}).listen(8080)

如果刷新浏览器,你会看到我们离成功更近了!

图 11.4 - 这是 HTML!

现在我们只需要一点 CSS:

const css = `
<style>
body {
 background-color: #000;
}
* {
 font-family: "Courier New";
 white-space: pre-wrap;
}
</style>
`

将其添加到我们的index.js中,并连接到输出,如下所示:

asciify(__dirname + '/img/image.jpg', { fit: 'box', width: 25, height: 25 }, (err, converted) => {
   res.write(css)
   res.end(convert.toHtml(err || converted))
 })

现在刷新,我们应该能看到我们的图片!

图 11.5 - ANSI 转 HTML

太棒了!比只打印“Hello, World!”要令人兴奋多了,你不觉得吗?

让我们通过重新访问我们在第七章中的宝可梦游戏来增强我们的 Node.js 技能,事件,事件驱动设计和 API,但这次是在 Node.js 中。

Pokéapi,重访

我们将使用 Pokéapi (pokeapi.co) 制作一个小型终端命令行界面CLI)游戏。由于我们在github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-7/pokeapi/solution-code中有游戏的基本逻辑,我们只需要开始并将游戏逻辑从前端移植到后端的 Node.js 中完成游戏。

从头开始一个新项目,如下所示:

  1. mkdir pokecli

  2. npm init

  3. npm install asciify-image axios terminal-kit

  4. pokeapi.co复制 Pokéapi 标志到一个新的img目录,使用浏览器中的 Save Image。

  5. 创建一个新的index.js文件。

  6. 修改package.json,添加一个启动脚本,如下所示:"start": "node index.js"

你的文件结构应该是这样的,减去node_modules目录:

.
├── img
│   └── pokeapi_256.png
├── index.js
├── package-lock.json
└── package.json

让我们开始在我们的index.js上工作。首先,我们需要包括我们正在使用的包:

const axios = require('axios')
const asciify = require('asciify-image')
const term = require('terminal-kit').terminal

接下来,由于我们将使用 API 来检索和存储我们的宝可梦,让我们创建一个新对象将它们存储在顶层,这样我们就可以访问它们:

const pokes = {}

现在我们将使用 Terminal Kit (www.npmjs.com/package/terminal-kit) 来创建一个比标准的console.log输出和readline输入更好的 CLI 体验:

function terminate() {
 term.grabInput(false);
 setTimeout(function () { process.exit() }, 100);
}
term.on('key', (name, matches, data) => {
 if (name === 'CTRL_C') {
   terminate();
 }
})
term.grabInput({ mouse: 'button' });

我们在这里做的第一件事是创建一个终止函数,它将在停止term捕获输入后退出我们的 Node.js 程序,以进行清理。下一个方法指定当我们按下Ctrl + C时,程序将调用terminate()函数退出。这是我们程序的一个重要部分,因为term默认情况下不会在按下 Ctrl + C 时退出。最后,我们告诉term捕获输入。

开始我们的游戏,从 Pokéapi 标志的闪屏开始:

term.drawImage(__dirname + '/img/pokeapi_256.png', {
 shrink: {
   width: term.width,
   height: term.height * 2
 }
})

我们可以直接使用term而不是asciify-image库(不用担心,我们以后会用到):

图 11.6 - Pokéapi 闪屏

接下来,编写一个函数,使用 Axios Ajax 库从 API 中检索信息:

async function getPokemon() {
 const pokes = await axios({
   url: 'https://pokeapi.co/api/v2/pokemon?limit=50'
 })

 return pokes.data.results
}

Axios (www.npmjs.com/package/axios) 是一个使请求比fetch更容易的包,通过减少所需的 promise 数量。正如我们在之前的章节中看到的,fetch很强大,但确实需要一些 promise 解析的链接。这次,让我们使用 Axios。请注意,该函数是一个async函数,因为它将返回一个 promise。

start()函数开始我们的游戏:

async function start() {
 const pokemon = await getPokemon()
}

我们将保持简单。请注意,此函数还使用了 async/await 模式,并调用我们的函数,该函数使用 API 检索宝可梦列表。此时,通过使用console.log()输出pokemon的值来测试我们的程序是一个好主意。您需要在程序中调用start()函数。您应该看到 50 只宝可梦的漂亮 JSON 数据。

在我们的start()函数中,我们将要求玩家选择他们的宝可梦并显示消息:

term.bold.cyan('Choose your Pokémon!\n')

现在我们将使用我们的pokemon变量使用term创建一个网格菜单,询问我们的玩家他们想要哪个宝可梦,如下所示:

term.gridMenu(pokemon.map(mon => mon.name), {}, async (error, response) => {
   pokes['player'] = pokemon[response.selectedIndex]
   pokes['computer'] = pokemon[(Math.floor(Math.random() *
    pokemon.length))]
})

您可以阅读term的文档,了解有关网格菜单的选项更多的信息。现在我们应该运行我们的代码,为了做到这一点,在程序的末尾添加对start()函数的调用:

start()

如果我们用npm start运行我们的代码,我们将看到这个新的添加:

图 11.7 - 菜单

通过箭头键,我们可以在网格周围导航,并通过按Enter来选择我们的宝可梦。在我们的代码中,我们正在为我们的pokes对象的两个条目分配值:playercomputer。现在,computer将是从我们的pokemon变量中随机选择的条目。

我们需要更多的信息来玩我们的宝可梦,所以我们将创建一个辅助函数。将其添加到我们的start函数中:

await createPokemon('player')
await createPokemon('computer')

现在我们将编写createPokemon函数如下:

async function createPokemon(person) {
 let poke = pokes[person]

 const myPoke = await axios({
   url: poke.url,
   method: 'get'
 })
 poke = myPoke.data
 const moves = poke.moves.filter((move) => {
   const mymoves = move.version_group_details.filter((level) => {
     return level.level_learned_at === 1
   })
   return mymoves.length > 0
 })
 const move1 = await axios({
   url: moves[0].move.url
 })
 const move2 = await axios({
   url: moves[1].move.url
 })
 pokes[person] = {
   name: poke.name,
   hp: poke.stats[5].base_stat,
   img: await createImg(poke.sprites.front_default),
   moves: {
     [moves[0].move.name]: {
       name: moves[0].move.name,
       url: moves[0].move.url,
       power: move1.data.power
     },
     [moves[1].move.name]: {
       name: moves[1].move.name,
       url: moves[1].move.url,
       power: move2.data.power
     }
   }
 }
}

让我们解释一下这个函数在做什么。首先,我们将从 API 中获取有关我们的宝可梦的信息(一次为玩家,一次为计算机)。由于游戏玩法复杂,宝可梦的移动部分有点复杂。对于我们的目的,我们将简单地为我们的宝可梦在pokes对象中分配前两个可能的移动。

对于图像,我们使用了一个小的辅助函数:

async function createImg(url) {
 return asciify(url, { fit: 'box', width: 25 })
   .then((ascii) => {
     return ascii
   }).catch((err) => {
     console.error(err);
   });
}

我们几乎完成了我们游戏的开始部分!我们需要在start中的gridMenu方法中添加几行:

term.gridMenu(pokemon.map(mon => mon.name), {}, async (error, response) => {
   pokes['player'] = pokemon[response.selectedIndex]
   pokes['computer'] = pokemon[(Math.floor(Math.random() * 
    pokemon.length))]
   await createPokemon('player')
   await createPokemon('computer')
   term(`Your ${pokes['player'].name} is so 
    cute!\n${pokes['player'].img}\n`)
   term.singleLineMenu( ['Continue'], (error, response) => {
     term(`\nWould you like to continue against the computer's scary
     ${pokes['computer'].name}? \n ${pokes['computer'].img}\n`)
     term.singleLineMenu( ['Yes', 'No'], (error, response) => {
       term(`${pokes['computer'].name} is already attacking! No time to 
       decide!`)
     })
   })
 })

现在我们可以玩了!

图 11.8 - 介绍你的宝可梦!

程序继续进行计算机选择宝可梦:

图 11.9 - 可怕的敌人宝可梦

目前,我们还没有包括使用移动和生命值进行实际游戏。这可以成为您根据第七章事件、事件驱动设计和 API**s的逻辑来完成play()函数的挑战。

完整的代码在这里:github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-11/pokecli

恭喜!我们做得比“Hello, World!”要多得多。

总结

我们在本章中学到,Node.js 是一种完整的编程语言,能够做几乎所有与后端相关的事情。我们将在第十八章,Node.js 和 MongoDB中使用 Node.js 进行数据库操作,但是目前,我们可以放心地说它可以做现代编程语言所期望的事情。

Node.js 的好处在于它的语法和结构普通的 JavaScript!一些术语不同,但总的来说,如果你能读写 JavaScript,你就能读写 Node.js。就像每种语言一样,术语和用法上有差异,但事实是 Node.js 和 JavaScript 是同一种语言!

在下一章中,我们将讨论 Node.js 和 Python 以及在何种情况下使用其中之一是有意义的。

进一步阅读

有关更多信息,您可以参考以下内容:

第十二章:Node.js 与 Python

为什么开发人员会选择 Node.js 而不是 Python?它们可以一起工作吗?我们的程序是什么样子的?这些问题等等都是 Python 和 Node.js 之间一些差异的核心,了解何时以及在何处使用特定的语言非常重要。例如,有些任务更适合某种语言,而不适合其他语言,技术人员有责任为适当的语言进行倡导。让我们调查在选择 Node.js 与 Python 时的用例和不同的考虑因素。

本章将涵盖以下主题:

  • Node.js 和 Python 之间的哲学差异

  • 性能影响

技术要求

准备好使用存储库中Chapter-12目录中提供的代码,网址为github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-12。由于我们将使用命令行工具,请确保你的终端或命令行 shell 可用。我们需要一个现代浏览器和一个本地代码编辑器。

Node.js 和 Python 之间的哲学差异

通常会有一个你熟悉、使用并且感到舒适的主要语言。然而,重要的是要意识到并非所有编程语言都是为相同的目的而创建的。这就是为什么使用合适的工具非常重要。就像你不会用小刀来建造房子一样,你可能也不会用台锯来把树枝削成棍子,用于篝火做棉花糖。

如果你在这个行业呆了一段时间,你可能听说过“堆栈”这个术语。在技术上,堆栈是用于创建程序或多个程序的生态系统的技术的架构组合。过去,应用程序往往是大规模的单体应用,以“一款应用程序统治它们所有”为思维方式构建的。在今天的世界中,单体应用的使用正在减少,而更多地采用多个更小的应用程序和微服务。通过这种方式,工作流程的不同部分可以分布到完全独立的进程中,大大提高了整个系统的稳定性。

让我们以办公软件为例。你肯定不会试图在 Microsoft Excel 中写下你的下一部畅销小说,你可能也不想在 Microsoft Word 中做税务。这些程序之间存在着“关注点分离”。它们在工作流程中很好地协同工作并形成一个统一的整体,但每个程序都有自己的作用。

同样,Web 应用程序中的不同技术部分都有自己的用途和关注点。用于 Web 应用程序的更传统的堆栈之一称为LAMPLinux, Apache, MySQL 和 PHP)。

图 12.1 - LAMP 堆栈

你可以看到,当讨论具体的 Web 应用程序时,我们将 Web 浏览器和客户端堆栈视为已知但未列在 LAMP 缩写中。在这种情况下,LAMP 只是服务器端组件。

随着 Web 的发展,支持它的基础技术及其堆栈也在发展。现在你可能会听到的两种更常见的堆栈是MEANMongoDB, Express, Angular 和 Node.js)和MERNMongoDB, Express, React 和 Node.js)。这两者之间唯一的区别是 Angular 与 React。它们在一个稳定的系统中实质上扮演着相同的角色。我们将在第十三章中探讨 Express,这是 Node.js 的普遍 Web 服务器框架,以及在第十八章中探讨 MongoDB,现在让我们专注于“为什么选择 Node.js?”。

在选择项目的语言时,有许多因素需要考虑。其中一些如下:

  • 项目类型

  • 预算

  • 上市时间

  • 性能

这些可能听起来是非常基本的因素,但我确实见过选择的技术不适合项目类型的情况。

对于那些沉浸在软件网络端的人来说,选择在后端使用 JavaScript 还是其他语言似乎是一个不言而喻的选择。JavaScript 是现代网络使用的基础,因此听起来,顺理成章地,应该在客户端和服务器端都使用它。

然而,Python 已经存在更长时间,而且在开发社区中肯定已经牢固地占据了一席之地,特别是在数据科学和机器学习的兴起中,Python 占据着主导地位。Flask 和 Django 是出色的 Web 框架,功能强大。那么,为什么我们要使用 Node.js 呢?

决定使用什么技术栈的第一步是了解项目类型。在我们今天的讨论范围内,让我们将我们的项目类型限制在合理的用例范围内。我们不会打开物联网/连接设备的潘多拉魔盒,因为这些大多数是用 Java 编写的。让我们也排除机器学习和数据科学作为可能的用例,因为在该领域已经确定 Python 更适合这些用例。然而,实际上有一个关于用 JavaScript 开发桌面和移动应用程序的论点。

首先,让我们考虑一下我们的项目是否是一个 Web 应用程序。在大多数情况下,Node.js 会是一个比 Python 更合理的选择,原因有很多,我们已经探讨过:它的异步性质、上下文切换较少、性能等等。我很难想象出一个使用 Python 后端的 Web 应用程序的充分用例,它会比 Node.js 更优越。我相信一些情况确实存在,但总的来说,即使在处理更大、更复杂的系统时,今天的偏好也不是拥有一个单一的后端应用程序,而是拥有一组微服务相互交互,并进行数据交接。

让我们来看一个可能的高级架构HLA)图表。如果你正在处理复杂的应用程序,了解系统的 HLA 是非常有用的。即使你只是在应用程序的一部分上积极工作,了解其他系统的需求和结构也是非常宝贵的。在这个例子中,我们有一个可能的电子商务网站架构,还有一个移动应用程序:

图 12.2 – 高级架构

我们可以看到可能有多个微服务,包括一些不是Node.js 或 JavaScript 的。Python 更适合作为一个微服务,为整体应用程序提供推荐,因为这需要数据分析,而 Python 和 R 在这方面做得比 Node.js 更好。此外,你可以看到在应用程序中,可以有多个不同的数据源,从第三方到不同的数据库类型。

那么,我们的项目呢?我们是在构建一个庞大的生态系统还是其中的一个特定部分?在这个例子中,Web 应用程序、支付服务、账户服务和库存服务都是 Node.js,因为使用设计用于异步通信的技术是有意义的。然而,推荐引擎可以是一个完全独立的堆栈,没有任何问题,因为它包含在微服务的整体生态系统中。只要应用程序的各个部分适当地相互通信,每个服务几乎可以是独立的。

为什么这很重要?简单地说,这是使更小、更灵活的团队能够并行工作,创建比单一应用程序更快、更稳定的软件的好方法。让我们来看一个例子:你打开一个大型零售商的网站来购物,但是你没有看到主页,而是看到了以下内容:

图 12.3 – 500!错误,错误,危险,危险!

任何 Web 应用程序开发人员的梦魇:由于代码问题导致的全面中断。相反,如果网站在大部分时间内正常运行,但是在结账时可能会显示“抱歉,我们的支付处理系统目前离线。我们已保存您的购物车以便以后使用。”或者说推荐引擎的 Python 部分崩溃了——我们可以改为提供静态的物品集合。为了创造一个大型微服务生态系统的真实用户体验,重要的是考虑最终用户的立场以及业务目标。在我们的电子商务商店的情况下,我们不希望整个应用程序因为一个小错误而崩溃。相反,如果出现问题,我们可以智能地降级体验。这是一个常被称为容错计算的原则的例子,在设计大型应用程序时,将单体应用程序分解为微服务以提高容错性是非常有力的。

在我们讨论预算考虑之前,我想向你展示一些 JavaScript 在桌面领域的强大示例。让我们运行一个示例代码片段,该代码片段在 GitHub 存储库github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-12/electron中为您提供:

  1. 使用npm install安装依赖项。

  2. 使用npm start运行应用程序。

您应该看到一个本机应用程序启动——我们在第七章中创建的 Pokémon 游戏,事件、事件驱动设计和 API

图 12.4 – 这是一个桌面应用程序!

这是如何发生的?我们利用了一个很棒的工具:Electron。您可以在electronjs.org/了解更多关于 Electron 的信息,但要点是它是一个容器工具,用于将 HTML、CSS 和 JavaScript 呈现为桌面应用程序。您可能已经在不知不觉中使用了 Electron:Spotify、Slack 和其他流行的桌面应用程序都是使用 Electron 构建的。

让我们快速看一下内部结构:

.
├── fonts
│   ├── pokemon_solid-webfont.woff
│   └── pokemon_solid-webfont.woff2
├── images
│   └── pokemon-2048x1152.jpg
├── index.html
├── main.js
├── package-lock.json
├── package.json
├── poke.js
├── preload.js
├── renderer.js
└── style.css

如果我们将其与第七章中的 PokéAPI 项目进行比较,事件、事件驱动设计和 APIgithub.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-7/pokeapi/solution-code),我们会发现有很多相似之处。

等等。

不仅相似……这与我们用于浏览器的代码完全相同main.js已重命名为poke.js以避免命名冲突,但这只是一个小细节。是的:您刚刚成功地使用现有代码创建了一个桌面应用程序。

所以,回到预算问题:如果你需要一个 Web 应用程序一个桌面应用程序呢?你现在应该已经明白,使用 JavaScript,你可以同时拥有现代 Web 应用程序一个桌面应用程序,只需进行最小的更改。细微之处比我们在这里做的要多一些,但是 Electron 的强大应该是显而易见的。一次编写,多次使用——这不就是 DRY 编码的口头禅吗?

然而,这个论点也有反面。由于 Python 的成熟程度比 Node.js 更长,Python 开发人员的小时费用可能会更具成本效益。然而,我认为这是一个次要的问题。

同样,作为次要关注点,上市时间确实是在选择技术时出现的一个问题。不幸的是,这里的数字并不确定。因为 Node.js 是 JavaScript,理论上可以快速迭代开发。然而,Python 的明确和简单的语法有时会更快地编写。这是一个非常难解决的问题,因此最好考虑时间方面的另一个部分:技术债务。技术债务是工程团队的大敌,它简单地意味着以牺牲最佳解决方案为代价,实施了更快的解决方案。此外,技术的淘汰也会导致技术债务。您还记得 Y2K 吗?当发现世界上许多主要应用程序依赖于两位数年份时,人们担心从 1999 年到 2000 年的变化会对计算机系统造成严重破坏。幸运的是,只发生了一些小故障,但技术债务的问题出现了:许多这些系统是用已经变得陈旧的语言编写的。找到程序员来开发这些修复程序是困难且昂贵的。同样,如果您选择一种技术是因为它更快,您可能会发现自己在预算和时间方面付出两三倍于最初投资的代价来重构应用程序以满足持续的需求。

让我们把注意力转向性能。这里有很多要考虑的,所以让我们继续到下一节,讨论为什么在讨论 Node.js 时性能总是需要考虑的。

性能影响

当 Node.js 首次开始流行时,人们对其单线程性质表示担忧。单线程意味着一个 CPU,一个 CPU 可能会被大量的流量压倒。然而,大部分情况下,所有这些线程问题都已经被服务器技术、托管和 DevOps 工具的进步所缓解。话虽如此,单线程性质本身也不应该成为阻碍:我们将在稍后讨论为什么Node 事件循环在任何关于 Node.js 性能的讨论中扮演着重要角色。

简而言之,要真正在性能上有所区别,我们应该专注于感知性能。Python 是一种易于理解、强大、面向对象的编程语言;这是毋庸置疑的。然而,它不会在浏览器中运行。这个位置被 JavaScript 占据了。

为什么这很重要,它与性能有什么关系?简而言之:Python 无法对浏览器中的更改做出反应。每次页面 UI 更改时执行 Ajax 请求是可能的,但这对浏览器和服务器在计算上都非常昂贵。此外,您必须使浏览器在每次更改时等待来自服务器的响应,导致非常卡顿的体验。因此,在浏览器中我们能做的越多,越好。在需要与服务器通信之前,使用浏览器中的 JavaScript 来处理逻辑是目标。

在使用 Node.js 的讨论中隐含着您可能从上一节中得出的一个想法:Node.js 也不在浏览器中运行!这是真的!然而,Node.js 基于 Chrome 解释器,因此其设计中隐含的是异步性的概念。Node.js 的事件循环是为事件设计的,而事件的内在特性是它们是异步的。

让我们回顾一下第七章中的以下图表,事件、事件驱动设计和 API

图 12.5 - 事件生命周期

如果您还记得,这个图表代表了浏览器事件的三个阶段:捕获、目标和冒泡。DOM 事件特指在浏览器中由用户或程序本身引起的操作、交互或触发器。

同样,Node.js 的事件循环有一个生命周期:

图 12.6 - Node.js 事件循环

让我们来解释一下。单线程事件循环在 Node 应用程序的生命周期内运行,并接受来自浏览器、其他 API 或其他来源的传入请求,并执行其工作。如果是一个简单的请求或被指定为同步的,它可以立即返回。对于更密集的操作,Node 将注册一个回调。记住,这是一个传递给另一个函数以在其完成工作时执行的函数的术语。到目前为止,我们已经在 JavaScript 中广泛使用这些作为事件处理程序。Node.js 事件循环提供了一种有效的方式来访问和为我们的应用程序提供数据。

如果你对线程和进程的概念不太熟悉,没关系,因为我们不会在这里深入讨论。然而,指出一些关于 Node 使用进程和线程的事实是很重要的。一些计算机科学家指出,Node 的单线程特性在本质上是不可扩展的,无法承受成熟的 Web 应用程序所需的流量。然而,正如我之前提到的,我们的应用程序并不孤立存在。任何需要设计成可扩展性的应用程序都不会只独自在服务器上运行。随着云技术的出现,比如亚马逊 AWS,很容易整合多个虚拟机、负载均衡器和其他虚拟工具,以适当地分配应用程序的负载。是的,Python 可能更适合作为一个单一盒子应用程序来接收成千上万的传入请求,但是这种性能基准已经过时,不符合当今技术的状态。

买方自负

现在我们已经爱上了 Node,让我们回到手头任务的正确工具的想法。Node 并不是解决世界所有计算问题的灵丹妙药。事实上,它特意设计成瑞士军刀。它有它的用途和位置,但它并不试图成为所有人的一切。Java 的“做任何事情”的特性可能被认为是它的弱点,因为虽然你可以编写一次 Java 代码并为几乎任何架构编译它,但为了适应这一点,已经做出了限制、考虑和权衡。Node.js 和 JavaScript 本质上试图留在自己的领域。

那么,有什么问题吗?我们知道 JavaScript 快速、强大、有效和易懂。像任何技术一样,总会有细微差别,JavaScript 和 Node 的一个细微差别就是在一些 Linux 系统中,当你首次以超级用户身份登录时,会出现这样的座右铭:“伴随着伟大的力量而来的是伟大的责任。”尽管这句话的出处模糊不清,但在执行任何对他人有影响的事情时,思考这一点是很重要的。(不要用催眠术做坏事!)

开玩笑的话,异步环境可能会出现非常真实的问题。我们知道,我们可以轻松地通过我们自己的客户端 JavaScript 代码将用户的浏览器崩溃,只需将其放入一个无限循环中。考虑以下代码:

let text = ''

while (1) {
  text += '1'
}

很好。如果你在浏览器中运行这个代码,最好的情况是浏览器会识别出一个无限循环,并提示你退出脚本,因为页面无响应。第二种情况是浏览器崩溃,最坏的情况是用户的整个机器可能因为内存不足而崩溃。伴随着伟大的力量……

同样,通过不正确地处理状态和事件,我们可以严重影响用户在 Node 中的体验。例如,如果您的前端代码依赖于一个 Node 进程,而该进程从未返回会怎么样?幸运的是,在大多数情况下,内置了 Ajax 保障措施,以防止这种情况发生,即 HTTP 请求将在一定时间后默认关闭并在必要时报错。话虽如此,有许多方法可以强制连接保持打开状态,从而对用户的浏览器造成绝对混乱。有很多正当的理由来做这件事,比如长轮询实时数据,这就是它们存在的原因。另一方面,也有可能意外地给用户造成重大问题。像超时请求这样的故障保护措施存在是为了保护您,但任何优秀的工程师都会告诉您:不要依赖故障保护措施——避免在设计过程中出现错误。

总结

Python 很棒。Node 也很棒。两者都很棒。那么为什么我们要进行这次对话呢?虽然这两种技术都很强大和成熟,但每种技术在技术生态系统中都有其作用。并非所有语言都是平等的,也并非所有语言以相同的方式处理问题。

总之,我们已经学到了以下内容:

  • Node.js 是异步的,并且与基于事件的思想很好地配合,比如浏览器中的 JavaScript 对页面事件的反应。

  • Python 已经确立了自己作为数据分析和机器学习领域的领导者,因为它能够快速处理大型数据集。

  • 对于 Web 工作,这些技术可能是可以互换的,但是复杂的架构可能会涉及两者(甚至更多)。

在下一章中,我们将开始使用 Express,这是 Node.js 的基础 Web 服务器。我们将创建自己的网站并与它们一起工作。

进一步阅读

以下是一些关于这些主题的更多阅读:

第十三章:使用 Express

正如我们讨论过的,后端的 JavaScript 对于创建 Web 应用程序和利用 JavaScript 在前端和后端都非常有用。与前端交互的服务器端应用程序最基本的工具之一是基本的 Web 服务器。为了提供 API、数据库访问和其他不适合由浏览器处理的功能,我们首先需要设置一个软件来处理这些交互。

Express.js(或者只是 Express)是一个 Web 应用程序框架,被认为是 Node.js 的事实标准Web 服务器。它享有很高的流行度和易用性。让我们使用它来构建一个完整的 Web 应用程序。

本章将涵盖以下主题:

  • 搭建脚手架:使用express-generator

  • 路由和视图

  • 在 Express 中使用控制器和数据

  • 使用 Express 创建 API

技术要求

准备好在github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-13的 GitHub 存储库中使用代码编辑器和浏览器。在路由和视图部分,我们将讨论一些使用代码编辑器的最佳实践。

命令行示例以 macOS/Linux 风格呈现。Windows 用户可能需要查阅文档以了解 Windows 命令行的一些细微差别。

搭建脚手架:使用 express-generator

要开始,我们需要再次使用我们的命令行界面CLI)。如果你还记得第二章中的内容,我们可以在服务器端使用 JavaScript 吗?当然可以!,我们曾经在命令行上查看了 Node 和npm。让我们再次检查我们的版本,以便我们可以对我们的应用程序做出一些决定。在你的命令行上运行node -v。如果你的版本是 v8.2.0 或更高,你可以选择使用npx来安装某些只在项目生命周期中运行一次的包,比如 express-generator。然而,如果你的版本较低,你也可以使用npm来安装一次性使用的包以及在你的项目中使用的包。

在本章中,我们将继续使用npx,所以如果你需要快速查看npmnpx的文档,请确保给自己一些时间来做。实质上,要使用npm安装一次性包,这些包不应该存在于你的代码库中,例如 Express 生成器或 React 应用程序创建器,你可以在系统上全局安装该包,如下所示:npm install -g express-generator。然后,你将使用 Express 运行该程序。然而,这被认为是npm的传统用法,因为在今天的环境中,npx更受青睐。

让我们从头开始创建我们的 Express 应用程序,以建立肌肉记忆,而不是继续第二章中的内容,我们可以在服务器端使用 JavaScript 吗?当然可以!。按照以下步骤开始使用 Express web 服务器:

  1. 在一个方便的位置,使用mkdir my-webapp创建一个新目录。

  2. 使用cd my-webapp进入其中。

  3. npx express-generator --view=hbs express 生成器将创建多个文件和目录:

图 13.1 - 创建我们的 Express 脚手架

我们将设置我们的应用程序使用 Handlebars 作为我们的模板,而不是默认选项 Jade。Express 支持多种模板语言(并且可以扩展使用任何模板语言),但为了方便起见,我们将使用类似于我们在第八章中使用的 React 和 Vue 前端框架的 Handlebars,它使用基本的花括号标记。

  1. 使用npm install来安装我们的依赖项。(请注意,即使之前使用过npx,在这里你也要使用npm。)这将需要几秒钟的时间,并将下载许多包和其他依赖项。另一个需要注意的是,你需要互联网连接,因为npm会从互联网上检索包。

  2. 现在,我们准备使用npm start来启动我们的应用程序:

图 13.2 - 我们的应用程序开始

  1. 好了!现在,让我们在 Web 浏览器中访问我们的 Express 网站:

图 13.3 - Express 欢迎页面

太棒了!现在我们到了这一步,让我们比在第二章中所做的更进一步,我们可以在服务器端使用 JavaScript 吗?当然可以!

RESTful 架构

许多 Web 应用程序的核心是一个 REST(或 RESTful)应用程序。RESTREpresentational State Transfer的缩写,它是一种处理大多数 Web 技术固有的无状态的设计模式。想象一下一个不需要登录或太多数据的标准网站——只是静态的 HTML 和 CSS,就像我们在之前的章节中创建的那样,但更简单:没有 JavaScript。如果我们从状态的角度来看待这样的网站,我们会发现一堆 HTML 并不知道我们的用户旅程,不知道我们是谁,而且,坦率地说,它也不关心。这样的网站就像印刷材料:你通过观看、阅读和翻页来与它交互。你不会改变它的任何内容。一般来说,你真正修改书的状态的唯一方式就是用书签保存你的位置。老实说,这比基本的 HTML 和 CSS 更具交互性。

为了处理用户和数据,REST 被用作一种功能范式。在处理 API 时,我们已经使用了两个主要的 HTTP 动词:GET 和 POST。这是我们将要使用的两个主要动词,但我们将再看看另外两个:PUT 和 DELETE。

如果你熟悉创建、读取、更新和删除CRUD)的概念,这就是标准的 HTTP REST 动词的翻译方式:

概念 HTTP 动词
创建 创建
读取 获取
更新 PUT 或 PATCH
删除 删除

更多信息,你可以查看 Packt REST 教程:hub.packtpub.com/what-are-rest-verbs-and-status-codes-tutorial/

现在,可能只使用 GET,或者只使用 GET 和 POST 来创建一个完整的应用程序是可能的,但出于安全和架构的原因,你不会想这样做。现在,让我们同意遵循最佳实践,并在这个已建立的范式内工作。

现在,我们将创建一个 RESTful 应用程序。

路由和视图

路由和视图是 RESTful 应用程序的 URL 的基础,它们作为逻辑的路径,以及向用户呈现内容的方式。路由将决定代码的哪些部分对应于应用程序界面的 URL。视图确定显示什么,无论是向浏览器、另一个 API 还是其他编程访问。

为了进一步了解 Express 应用程序的结构,我们可以检查它的路由和视图:

  1. 首先,让我们在你喜欢的 IDE 中打开 Express 应用程序。我将使用 VS Code 进行工作。如果你使用 VS Code、Atom、Sublime 或其他具有命令行工具的 IDE,我强烈建议安装它们。例如,使用 Atom,你可以在命令提示符中输入atom .来启动多面板 Atom 编辑界面,并在 Atom 中打开该目录。

  2. 同样,VS Code 会用code .来做到这一点。这是它的样子:

图 13.4 - VS Code

我已经展开了左侧的目录,这样我们就可以看到层次结构的第一层。

  1. 打开app.js

你会注意到这段代码的语法是 express-generator 为我们创建的ES5,而不是 ES6。暂时,我们不要担心将其转换为 ES6;我们稍后会做。当我们在第一个 Node.js REST 应用程序上工作时,请记住有几种不同的方法可以实现我们的目标,我们将首先采用更冗长的路径来使功能正常工作,然后对其进行迭代,使其更灵活和更 DRY。

  1. 现在,你不需要对app.js做任何更改,但是花点时间熟悉它的结构。它可能比较陌生的一个方面是文件开头的require()语句。类似于前端框架中使用的importrequire()是 Node 的一种方式,用于从其他文件中引入这些部分。在这种情况下,前几行是通过npm安装的模块,如下所示:
var express = require('express');

请注意,('express')前面没有路径。它只是简单地陈述。这表明所引用的模块不是我们代码的本地模块。然而,如果你看一下indexRouterrequire语句,我们会看到它一个路径:'./routes/index'。它没有.js扩展名,但对于我们的模块使用来说,路径是正确的。

现在,让我们检查一下我们的routes/index.js文件。

路由

如果你打开routes/index.js,你会看到为我们生成的以下几行代码:

var express = require('express');
var router = express.Router();

/* GET home page. */
router.get('/', function(req, res, next) {
    res.render('index', { title: 'Express' });
});

module.exports = router;

这里没有太多令人惊讶的地方:正如我们开始了解的那样,Express 文件以require语句开头,特别是对于express本身。在下一个代码块中,我们开始看到我们的 REST 服务的开端:GET home page。看一下注释后面的router.get()方法。它明确地告诉路由器,当收到 URL 为/的 GET 请求时,执行此代码。

我们可以通过在这里添加一些 GET 路径来验证这一事实,只是为了好玩。让我们尝试修改我们的代码如下。在router.get()块之后,但在module.exports之前,让我们在路由器上注册更多的路由:

/* GET sub page. */
 router.get('/hello', function(req, res, next) {
     res.render('index', { title: 'Hello! This is a route!' });
 });

现在,我们必须用Ctrl + C停止我们的 Express 服务器,用npm start重新启动它,并在http://localhost:3000/hello上访问我们的新页面:

图 13.5 - 一个新的路由,打开了网络选项卡,显示我们正在进行 GET 请求

到目前为止,这应该看起来相当基本。现在,让我们做点不一样的事情。让我们使用这个视图并为 Ajax POST 请求创建一个表单:

  1. 创建一个名为public/javascripts/index.js的新文件。

  2. 编写一个基本的fetch请求到端点/hello,POST JSON 为{ message: "This is from Ajax" },如下所示:

fetch('/hello', {
 method: 'POST',
 body: JSON.stringify({ message: "This is from AJAX" }),
 headers: {
   'Content-Type': 'application/json'
 },
});
  1. 像这样在views/index.hbs中包含这个文件:
<h1>{{title}}</h1>

<p>Welcome to {{title}}</p>

<p id="data">{{ data }}</p>

<script src="/javascripts/index.js"></script>

请注意,我们不需要在路径中包含public。这是因为 Express 已经理解到public中的文件应该静态提供,而不需要 Express 的干预或解析,与必须运行的 Node 文件相反。

  1. 如果现在重新加载页面,你不会看到任何令人兴奋的事情发生,因为我们还没有编写处理 POST 请求的路由。编写如下:
/* POST to sub page. */
router.post('/hello', function(req, res, next) {
  res.send(req.body);
});
  1. 重新加载页面,你会看到... 什么也没有。在网络选项卡中没有 POST,当然也没有渲染。发生了什么?

Node 有几个工具用于在代码更改时重新启动 Express 服务器,以便引擎会自动刷新,而无需我们杀死并重新启动它,就像我们以前做的那样,但这次没有。这些工具随时间而变化,但我喜欢的是 Supervisor:www.npmjs.com/package/supervisor。只需在项目目录中执行npm install supervisor即可在项目中安装它。

  1. 现在,打开项目根目录中的package.json文件。你应该看到一个类似于这样的文件,但可能有一些版本差异:
{
 "name": "my-webapp",
 "version": "0.0.0",
 "private": true,
 "scripts": {
 "start": "node ./bin/www"
 },
 "dependencies": {
 "cookie-parser": "~1.4.4",
 "debug": "~2.6.9",
 "express": "~4.16.1",
 "hbs": "~4.0.4",
 "http-errors": "~1.6.3",
 "morgan": "~1.9.1",
 "supervisor": "⁰.12.0"
 }
}

这是运行npm install时安装的核心内容。运行时,您会看到一个node_modules目录被创建,并且有许多文件写入其中。

如果您正在使用诸如 Git 之类的版本控制,您将想提交node_modules目录。使用 Git,您会在.gitignore文件中包含node_modules

  1. 我们接下来要做的事情是修改我们的启动脚本,现在使用 Supervisor:
"scripts": {
     "start": "supervisor ./bin/www"
 },

要使用它,我们仍然使用npm start,要退出它,只需按下Ctrl + C。值得注意的是,Supervisor 最适合本地开发工作,而不是生产工作;还有其他工具,比如 Forever,可以用于这个目的。

  1. 现在,让我们运行npm start,看看会发生什么。您应该看到一些以按下 rs 重新启动进程结束的控制台消息。在大多数情况下,不需要发出rs,但如果需要,可以使用它:

图 13.6 - 来自 Ajax 的响应!

  1. 由于我们从前端 JavaScript 发送了这是来自 AJAX,我们在响应 HTML 中看到了它的反映!现在,如果我们想要在我们的页面中看到它,我们会在我们的前端 JavaScript 中这样做:
fetch('/hello', {
 method: 'POST',
 body: JSON.stringify({ message: "This is from AJAX" }),
 headers: {
   'Content-Type': 'application/json'
 },
}).then((res) => {
 return res.json();
}).then((data) => {
 document.querySelector('#data').innerHTML = data.message
});

我们将看到以下内容:

图 13.7 - 来自 Ajax 的消息!

接下来,让我们了解如何保存数据。

保存数据

对于我们的下一步,我们将在本地数据存储中持久化数据,这将是一个简单的本地 JSON 文件:

  1. 继续并使用Ctrl + C退出 Express。让我们安装一个简单的模块,它可以在本地存储中保存数据:npm install data-store

  2. 让我们修改我们的路由以使用它,就像这样:

var express = require('express');
var router = express.Router();

const store = require('data-store')({ path: process.cwd() + '/data.json' });

/* GET home page. */
router.get('/', function(req, res, next) {
 res.render('index', { title: 'Express', data: 
 JSON.stringify(store.get()) });
});

/* GET sub page. */
router.get('/hello', function(req, res, next) {
 res.render('index', { title: 'Hello! This is a route!' });
});

/* POST to sub page. */
router.post('/hello', function(req, res) {
 store.set('message', { message: `${req.body.message} at ${Date.now()}` })

 res.set('Content-Type', 'application/json');
 res.send(req.body);
});

module.exports = router;
  1. 注意store的包含以及在hello/路由中的使用。让我们还修改我们的index.hbs文件,就像这样:
<h1>{{title}}</h1>
<p>Welcome to {{title}}</p>

<button id="add">Add Data</button>
<button id="delete">Delete Data</button>

<p id="data">{{ data }}</p>
<script src="/javascripts/index.js"></script>

  1. 我们稍后会使用删除数据按钮,但现在我们将使用添加数据按钮。在public/javascripts/index.js中添加一些保存逻辑,就像这样:
const addData = () => {
 fetch('/hello', {
   method: 'POST',
   headers: {
     'Content-Type': 'application/json'
   },
   body: JSON.stringify({ message: "This is from Ajax" })
 }).then((res) => {
   return res.json()
 }).then((data) => {
     document.querySelector('#data').innerHTML = data.message
 })
}
  1. 现在我们将添加我们的点击处理程序:
document.querySelector('#add').addEventListener('click', () => {
 addData()
 window.location = "/"
})
  1. 如果您刷新/页面并点击添加数据按钮,您应该会看到类似这样的东西:

图 13.8 - 添加数据

  1. 现在,再次刷新该页面。注意消息是持久的。在您的文件系统中,您还应该注意到一个包含数据的data.json文件。

现在我们准备使用删除方法更多地工作一下。

删除

我们已经探讨了 GET 和 POST,现在是时候处理另一个基础 REST 动词了:DELETE

顾名思义,它的目标是从数据存储中删除数据。我们已经有了我们的按钮来这样做,所以让我们把它连接起来:

  1. 在我们的前端 JavaScript 中,我们将添加以下内容:
const deleteData = () => {
 fetch('/', {
   method: 'DELETE',
   headers: {
     'Content-Type': 'application/json'
   },
   body: JSON.stringify({ id: 'message' })
 })
}
document.querySelector('#delete').addEventListener('click', () => {
 deleteData()
 window.location = "/"
})
  1. 现在,在路由中添加这个路由:
/* DELETE from json and return to home page */
router.delete('/', function(req, res) {
 store.del(req.body.id);

 res.sendStatus(200);
});

那应该是我们需要的全部。刷新您的索引页面,并尝试使用添加和删除按钮。相当容易,对吧?在第十八章中,Node.js 和 MongoDB,我们将讨论在一个完整的数据库中持久化和操作我们的数据,但现在,我们可以使用 GET、POST 和 DELETE 的知识。我们将使用 PUT 来处理实际数据库。

视图

我们在Routers部分涉及了视图的操作,现在让我们深入了解一下。应用程序的视图层是表示层,这就是为什么它包含我们的前端 JavaScript。虽然并非所有的后端 Node 应用程序都会提供前端,但了解如何使用它是很方便的。每当我设置一个简单的 Web 服务器时,我都会使用 Express 及其对前端和后端功能的功能。

由于我们有多种前端模板语言可供选择,让我们以 Handlebars 作为逻辑和结构的示例。

如果我们愿意,我们可以在我们的前端代码中提供一些条件逻辑。请注意,这个逻辑是由后端渲染的,所以这是一个很好的例子,说明何时在后端渲染数据(对于前端来说更高效),何时通过 JavaScript 来做(这在表面上更灵活)。

让我们修改我们的views/index.hbs文件,就像这样:

{{#if data }}
 <p id="data">{{ data }}</p>
{{/if}}

让我们还修改routes/index.js

/* GET home page. */
router.get('/', function(req, res, next) {
 res.render('index', { title: 'Express', data: 
 JSON.stringify(Object.entries(store.get()).length > 0 ? store.get() :
  null) });
});

现在,我们使用三元运算符来简化我们的显示逻辑。由于我们从存储中获取的数据是 JSON,我们不能简单地测试它的长度:我们必须使用Object.entries方法。如果你认为我们可以将store.get()保存到一个变量中而不是写两次,你是对的。然而,在这种情况下,我们不需要占用额外的内存空间,因为我们立即返回它而不是对它进行操作。在这种情况下,性能影响是可以忽略不计的,但这是需要记住的一点。

现在,如果我们删除我们的数据,我们会看到这个:

图 13.9 - 删除数据后

看起来比看到一个空对象的花括号要少混乱一些。当然,我们可以通过编写更复杂的条件在前端进行条件工作,但为什么在后端可以发送适当的数据时要做这项工作呢?当然,对于这两种情况都有情况,但在这种情况下,最好让每个部分都做自己的工作。

你可以在这里找到我们完成的工作:github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-13/my-webapp

现在让我们把注意力转向如何使用控制器将数据实际传入 Express。

控制器和数据:在 Express 中使用 API

正如你可能在网络上听到的那样,Express 很棒,因为它对你如何使用它没有太多的意见,同时,人们说 Express 很难使用,因为它的意见不够明确!虽然 Express 通常不设置为传统的模型-视图-控制器设置,但将功能拆分出路由并放入单独的控制器中可能是有益的,特别是如果你可能在路由之间有类似的功能,并且想要保持代码的 DRY。

如果你对模型-视图-控制器MVC)范式不太熟悉,不用担心——我们不会详细讨论它,因为这是一个非常沉重的话题,有着自己的争论和惯例。现在,我们只是定义一些术语:

  • 模型是应用程序的一部分,处理数据操作,特别是与数据库之间的通信。

  • 控制器处理来自路由的逻辑(即用户的 HTTP 请求路径)。

  • 视图是向最终客户端提供标记的表示层,由控制器路由。

这就是 MVC 范式的样子:

图 13.10 - MVC 范式

让我们来看一个示例应用程序。在github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-13/controllers是一个使用 Express 的应用程序。

这是一个使用控制器和模型的 API。正如我们将看到的,这种结构将简化我们的工作流程。这仍然是一个相当简单的例子,但这会让你了解为什么控制器和模型会派上用场。让我们来调查一下:

  1. 继续运行npm install,然后运行npm start来运行应用程序。它应该可以在你的浏览器中访问http://localhost:3000,但如果你有其他东西在运行,Node 会警告你并指定一个不同的端口。你会看到以下内容:

图 13.11 - 我们的示例 Express 应用程序

  1. 到目前为止非常简单。继续点击添加用户几次,然后尝试一下功能。这使用后端的随机用户 API 来创建用户并将它们持久化到文件系统数据存储中。

  2. 查看public/javascripts目录中的客户端 JavaScript。这应该看起来很熟悉。如果我们记得fetch()调用的结构,它返回一个 promise,所以我们可以使用.then()范式来对我们的事件做出反应。

  3. public/javascripts/index.js中,我们可以看到当我们点击添加用户时创建用户的机制:

document.querySelector('.add-user').addEventListener('click', (e) => {
  fetch('/user', {
    method: 'POST'
  }).then( (data) => {
    window.location.reload()
  })
})

这不应该有什么意外:我们在事件处理程序中使用 JavaScript 的fetch来调用带有 POST 的/user路由。路由基本上是 Express(或其他)应用程序中的一个端点:它包含一些逻辑来对事件做出反应。那么,这个逻辑是什么?

  1. 打开routes/user.js
var express = require('express');
var router = express.Router();

const UsersController = require('../controllers/users');

/* GET all users. */
router.get('/', async (req, res, next) => {
  res.send(await UsersController.getUsers());
});

/* GET user. */
router.get('/:user', async (req, res, next) => {
  const user = await UsersController.getUser(req.params.user);
  res.render('user', { user: user });
});

/* POST to create user. */
router.post('/', async (req, res, next) => {
  await UsersController.createUser();
  res.send(await UsersController.getUsers());
});

/* DELETE user. */
router.delete('/:user', async (req, res, next) => {
  await UsersController.deleteUser(req.params.user);
  res.sendStatus(200);
});

module.exports = router;

首先,让我们将其结构与其他示例进行比较。首先,我们将看到用户控制器的require()语句。这里有一个router.post()方法语句,它使用async/await进行对控制器的异步调用。然后,我们的控制器将调用我们的模型来进行数据库工作。

到目前为止,有许多文件和路径需要执行。在我们在代码中迷失之前,让我们看一下前端方法(例如添加用户点击处理程序)如何与我们的 Express 后端通信的图表:

图 13.14 - 端到端通信

从左到右,从上到下阅读,我们可以看到每个步骤在过程中扮演的角色。对于从 API 检索信息这样基本的事情,它可能看起来有点复杂,但这种架构模式的一部分力量在于每个层可以由不同的一方编写和控制。例如,模型层通常由数据库专家掌握,而不是其他类型的后端开发人员。

当您跟踪控制器和模型的代码时,请考虑代码每一层的关注点分离如何使设计更加模块化。例如,我们使用一个 LocalStorage 数据库来存储我们的用户。如果我们想要将 LocalStorage 替换为更强大的系统,比如 MongoDB,我们实际上只需要编辑一个文件:模型。事实上,甚至模型也可以被抽象化为具有统一数据处理程序,然后使用适配器进行特定数据库方法的调用。

这对我们来说可能有点太多了,但接下来让我们把目光转向使用我们刚学到的原则来创建一个星际飞船游戏。我们将使用这个 Node.js 后端来制作 JavaScript 游戏的前端最终项目。

在下一节中,我们将开始创建我们游戏的 API。

使用 Express 创建 API

谁不喜欢像《星球大战》或《星际迷航》中的美丽星舰战斗呢?我碰巧是科幻小说的忠实粉丝,所以让我们一起来构建一个使用存储、路由、控制器和模型来跟踪我们游戏过程的 RESTful API。虽然我们将专注于应用程序的后端,但我们将建立一个简单的前端来填充数据和测试。

您可以在github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-13/starship-app找到一个正在进行中的示例应用程序。让我们从那里开始,您可以使用以下步骤完成它:

  1. 如果您还没有克隆存储库,请克隆它。

  2. 进入cd starship-app目录并运行npm install

  3. 使用npm start启动项目。

  4. 在浏览器中打开http://localhost:3000。如果您已经在端口 3000 上运行任何项目,start命令可能会提示您使用其他端口。这是我们的基本前端:

图 13.15 - 星舰舰队

  1. 随意添加和销毁飞船,无论是随机还是手动。这将是我们游戏的设置。

  2. 现在,让我们解开代码在做什么。这是我们的文件结构:

.
├── README.md
├── app.js
├── bin
│ └── www
├── controllers
│ └── ships.js
├── data
│ └── starship-names.json
├── models
│ └── ships.js
├── package-lock.json
├── package.json
├── public
│ ├── images
│ │ └── bg.jpg
│ ├── javascripts
│ │ └── index.js
│ └── stylesheets
│ └── style.css
├── routes
│ ├── index.js
│ ├── ships.js
│ └── users.js
└── views
 ├── error.hbs
 ├── index.hbs
 └── layout.hbs
  1. 打开public/javascripts/index.js。让我们首先检查随机飞船创建的事件处理程序:
document.querySelector('.random').addEventListener('click', () => {
 fetch('/ships/random', {
   method: 'POST'
 }).then( () => {
   window.location.reload();
 })
})

到目前为止一切都很顺利。这应该看起来很熟悉。

  1. 让我们来看看这条路线:/ships/random。打开routes/ships.js(我们可以猜测/ships/的路由将在ships.js文件中,但我们可以通过阅读app.js文件中的路由来确认这一点,因为我们已经学过了)。阅读/random路线:
router.post('/random', async (req, res, next) => {
 await ShipsController.createRandom();
 res.sendStatus(200);
});

我们首先注意到的是这是一个async/await结构,因为我们将在前端使用fetch,(剧透)后端使用数据库。

  1. 让我们接下来看一下控制器方法:
exports.createRandom = async () => {
 return await ShipsModel.createRandom();
}
  1. 很容易。现在是模型方法:
exports.createRandom = async () => {
 const shipNames = require('../data/starship-names');
 const randomSeed = Math.ceil(Math.random() * 
  shipNames.names.length);

 const shipData = {
   name: shipNames.names[randomSeed],
   registry: `NCC-${Math.round(Math.random()*10000)}`,
   shields: 100,
   torpedoes: Math.round(Math.random()*255+1),
   hull: 0,
   speed: (Math.random()*9+1).toPrecision(2),
   phasers: Math.round(Math.random()*100+1),
   x: 0,
   y: 0,
   z: 0
 };

 if (storage.getItem(shipData.registry) || storage.values('name') 
 == shipData.name) {
   shipData.registry = `NCC-${Math.round(Math.random()*10000)}`;
   shipData.name = shipNames.names[Math.round(Math.random()*
    shipNames.names.length)];
 }
  await storage.setItem(shipData.registry, shipData);
 return;
}

好的,这有点复杂,所以让我们来解开这个。前几行只是从一个为你提供的种子文件中选择一个随机名称。我们的shipData对象由几个键/值对构成,每个对应于我们新创建的船只的特定属性。之后,我们检查我们的数据库,看看是否已经有一个同名或注册号的船只。如果有,我们将再次随机化。

然而,与每个应用程序一样,都有改进的空间。这里有一个挑战给你。

挑战

你的第一个任务是:你能想出如何改进代码,使得在重新随机化时,优雅地检查随机化是否也存在于我们的数据库中吗?提示:你可能想创建一个单独的辅助函数或两个。

也许你得到了类似于这样的东西(github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-13/starship-app-solution1):

const eliminateExistingShips = async () => {
 const shipNames = require('../data/starship-names');
 const ships = await storage.values();

 const names = Object.values(ships).map((value, index, arr) => {
   return value.name;
 });

 const availableNames = shipNames.names.filter((val) => {
   return !names.includes(val);
 });

 const unavailableRegistryNumbers = Object.values(ships).map((value, index, 
 arr) => {
   return value.registry;
 });

 return { names: availableNames, unavailableRegistries: 
 unavailableRegistryNumbers };
}

并使用它,执行以下命令:

exports.createRandom = async () => {
 const { names, unavailableRegistries } = await eliminateExistingShips();

 const randomSeed = Math.ceil(Math.random() * names.length);

 const shipData = {
   name: names[randomSeed],
   registry: `NCC-${Math.round(Math.random() * 10000)}`,
   shields: 100,
   torpedoes: Math.round(Math.random() * 255 + 1),
   hull: 0,
   speed: (Math.random() * 9 + 1).toPrecision(2),
   phasers: Math.round(Math.random() * 100 + 1),
   x: 0,
   y: 0,
   z: 0
 };

 while (unavailableRegistries.includes(shipData.registry)) {
   shipData.registry = `NCC-${Math.round(Math.random() * 10000)}`;
 }
 await storage.setItem(shipData.registry, shipData);
 return;
}

那么,我们在这里做什么呢?首先,让我们看一下Objects.map的用法:

const names = Object.values(ships).map((value, index, arr) => {
   return value.name;
});

在这里,我们正在使用ships对象的.map()方法来创建一个只包含现有船只名称的新数组。基本上,我们所做的就是将对象的每个名称返回到我们的数组中,所以现在我们有了一个可枚举的数据类型。

接下来,我们想要消除已使用的名称,所以我们将使用数组的.filter()函数,只有在它不包含在我们之前创建的数组中时才返回该值:

const availableNames = shipNames.names.filter((val) => {
   return !names.includes(val);
});

我们与我们的名称一样处理我们的注册号,并返回一个对象。

现在,这里有一个新技巧:解构一个对象。看看这个:

 const { names, unavailableRegistries } = await eliminateExistingShips();

我们在这里做的是一举两得地分配两个变量!由于我们的eliminateExistingShips()方法返回一个对象,我们可以使用解构将其分解为单独的变量。这并不是完全必要的,但它通过减少我们使用点符号的次数来简化我们的代码。

继续。

船只属性

这是我们为游戏定义的船只属性及其描述。这个属性表对我们将构建的所有船只都是相同的,无论是随机还是手动:

name 一个字符串值。
registry 一个字符串值。
shields 一个护盾强度的数字,初始化为 100。随着船只受到损害,这个数字会减少。
torpedos 一个数字,表示船只拥有的鱼雷数量。在我们的游戏中,每次发射鱼雷时,这个数字会减少 1。
hull 从 0 开始,一个数字,表示护盾耗尽后船只所承受的船体损伤。当这个数字达到 100 时,船只被摧毁。希望每个人都能到达逃生舱!
speed 从 warp 1 到 9.99,我们的船只有一个可变速度。
phasers 没有战斗相位器的船只是不完整的!定义一个从 1 到 100 的随机数字,以指定船只的相位器造成的伤害。
x, y, and z 我们船只在三维空间中的坐标,从[0,0,0]开始。对于我们的游戏玩法,我们将坐标上限设定为[100,100,100]。我们不希望我们的船只在太空中迷失!

对于我们的数据库,我们并没有做任何复杂的事情;我们使用了一个名为node-persist的 Node 包。它使用文件系统上的一个目录来存储值。它很基础,但能完成任务。我们将在第十八章 Node.js 和 MongoDB 中介绍真正的数据库。请注意,这些方法也是async/await函数,因为我们期望代码与数据库交互时会有轻微的延迟(在这种情况下,是我们的文件系统)。

好了!由于我们的函数只返回空值,它将触发我们控制器方法的完成,然后返回一个200 OK消息到前端。根据我们的前端代码,页面将重新加载,显示我们的新飞船。

这里有第二个改进的空间:你能否使用 DOM 操作在不刷新页面的情况下将你的飞船添加到页面上?你将需要修改整个堆栈的所有级别来实现你的目标,通过将随机值返回到前端。

在你开始之前,让我们问自己一个重要的问题:在我们当前的结构下这样做是否有意义?如果你的思维过程导致了一个过于复杂的解决方案,就像我的一样,答案是否定的。很明显,处理 DOM 更新的最佳方式是利用我们拥有的另一个工具:一个框架。我们现在暂且不管它,但在我们的最终项目中第十九章 将所有内容整合在一起 中,我们将重新讨论它。

接下来,让我们看看星舰战斗将如何进行。如果我们回到我们的飞船路由,我们会看到这个路由:

router.get('/:ship1/attack/:ship2', async (req, res, next) => {
 const damage = await ShipsController.fire(req.params.ship1, 
 req.params.ship2);
 res.sendStatus(200);
});

如果你能从路由的构造中猜出来,路由将以第一艘飞船的名称作为参数(ship1),然后是attack字符串,然后是第二艘飞船的名称。这是一个 RESTful 路由的例子,以及 Express 如何处理路径参数。在我们的控制器调用中,我们使用这些参数和控制器的.fire()方法。在控制器中,我们看到这样的内容:

exports.fire = async (ship1, ship2, weapon) => {
 const target = await ShipsModel.getShip(ship2);
 const source = await ShipsModel.getShip(ship1);
 let damage = calculateDamage(source, target, weapon);

 if (weapon == 'torpedo' && source.torpedoes > 0) {
   ShipsModel.fireTorpedo(ship1);
 } else {
   damage = 0
 }

 return damage;
}

现在我们玩得很开心。你可以追踪不同的模型部分,但我想指出使用calculateDamage辅助函数。你会在文件的顶部找到它。

对于伤害计算,我们将使用以下内容:

或者,用英语说,“计算目标被源命中的几率是通过从三维空间中两艘飞船之间的距离中减去 100 来计算的,得到 0%到 100%之间的几率。为了计算这个值,将 100 减去xyz坐标增量的平方和的平方根四舍五入。”(是的,我不得不查找三维空间距离的计算。如果这对你来说很陌生,不用担心。)

然后,让R[1]成为 0 到 100 之间的伪随机值,四舍五入。在 JavaScript 中,就像所有编程语言一样,随机数在技术上只是一个伪随机数:

或者,“源头的相位炮可能造成的伤害是通过将源头的相位功率乘以一个Math.random()数四舍五入得到的。”

然而,如果源头发射了鱼雷(并且还有鱼雷剩余),那么possibledamage = 125

R[2]成为 0 到 100 之间的伪随机数,四舍五入:

如果chance减去随机数大于 0,伤害将发生为possibledamage。否则,不会发生伤害。

好了,现在我们有了计算。你能想出用 JavaScript 代码来实现这个吗?

就是这样:

const calculateDamage = (ship1, ship2, weapon) => {
 const distanceBetweenShips = Math.sqrt(Math.pow(ship2.x - ship1.x, 2) + 
 Math.pow(ship2.y - ship1.y, 2) + Math.pow(ship2.z - ship1.z, 2));
 const chanceToStrike = Math.floor(100-distanceBetweenShips);
 const didStrike = (Math.ceil(Math.random()*100) - chanceToStrike) ? true : 
 false;
 const damage = (didStrike) ? ((weapon == 'phasers') ? 
 Math.ceil(Math.random()*ship1.phasers) : TORPEDO_DAMAGE) : 0;
 return damage;
}

为了完成我们的游戏,我们需要创建一个机制来实际发射并在前端注册伤害。

总结

本章我们涵盖了很多内容,从路由到控制器再到模型。请记住,并非每个应用都遵循这种范式,但这是一个很好的基准,可以帮助你开始处理后端服务与前端的关系。

我们应该记住,使用express-generator可以帮助搭建应用程序,使用npmnpx。路由和视图是我们应用程序的前线,决定代码的路由和最终客户端所看到的内容(无论是 JSON 还是 HTML)。我们使用 API 来探索 API 的固有异步行为,并创建了自己的API!

在下一章中,我们将讨论 Express 与 Django 或 Flask 不同类型的框架。我们还将研究如何连接我们的前端和后端框架。

进一步阅读

第十四章:React 与 Django

到目前为止,我们已经使用了相当多的 Express,但 Django 提供了标准 Express 应用程序所没有的功能。它具有内置的脚手架、数据库集成和模板工具,提供了一种诱人的后端解决方案。然而,正如我们所学到的,JavaScript 在前端解决方案方面具有更强大的功能。那么,我们如何将这两者结合起来呢?

我们要做的是创建一个 Django 后端,为了将两种伟大的技术联系在一起,为 React 应用提供服务。

本章将涵盖以下主题:

  • Django 设置

  • 创建 React 前端

  • 将所有内容整合在一起

技术要求

准备好使用存储库中chapter-14目录中提供的代码,该存储库位于github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-14。由于我们将使用命令行工具,还需要准备好终端或命令行 shell。我们需要一个现代浏览器和本地代码编辑器。

Django 设置

有几种不同的方法可以结合 React 和 Django,复杂程度和集成级别各不相同。我们将采取的方法是将 React 编写为 Django 应用程序的前端,加载一个模板,让 React 处理前端。然后,我们将使用标准的 Ajax 调用与 Django 路由和数据存储逻辑进行交互。这是一种将这两种技术结合在一起的中间方法,略微保持它们完全分开,但也不为每个路由创建一个 React 应用程序。我们将保持简单。

请告诉我们我们将要劳作在什么上?说!

我们的应用将是一个聊天机器人,将使用大师剧作家莎士比亚的话语来回应输入!首先,我们将使用一个简单的 Django 实例的数据库加载莎士比亚的完整文本;接下来,我们将编写我们的路由来搜索匹配的文本;最后,我们将创建我们的 React 应用程序,成为用户和 Django 后端之间的桥梁。我们不会在我们的 Python 中使用复杂的机器学习或语言处理,尽管如果你愿意,你可以随时将我们的机器人推向更高一步!

请注意,我们将使用 Python 3。有关安装和设置 Django 的更详细信息,包括使用虚拟环境,请访问官方文档docs.djangoproject.com/en/3.0/topics/install/

首先,让我们使用以下步骤设置 Django:

  1. 创建一个新的虚拟环境:python -m venv shakespeare

  2. 启动venvsource shakespeare/bin/activate

  3. 安装 Django:python -m pip install Django

  4. 使用django-admin startproject shakespearebot开始一个新项目。

  5. 测试我们的 Django 设置:cd shakespearebot ; python manage.py runserver

  6. 如果我们访问127.0.0.1:8000/,我们应该看到默认的 Django 欢迎页面。

  7. 我们需要一个应用程序来使用:python manage.py startapp bot

  8. settings.py中将 bot 应用添加到INSTALLED_APPS'bot.apps.BotConfig'

接下来,我们将需要我们的莎士比亚数据集:

  1. 在书的 GitHub 存储库的chapter-14目录中包含一个名为Shakespeare_data.csv.zip的文件。解压缩此文件,你就可以随时查阅莎士比亚的所有作品。我们将使用一个基本模型将这个 CSV 导入 Django。

  2. bot目录中编辑models.py如下:

from django.db import models

class Text(models.Model):
  PlayerLine = models.CharField(max_length=1000)

   def __str__(self):
       return self.PlayerLine

我们将保持数据库简单,只摄取文本行,而不是行周围的任何其他数据。毕竟,我们只会对语料库进行简单的文本搜索,没有比这更复杂的操作。在导入数据的下一步之前,让我们包含一个 Django 模块,以使我们的生活更轻松:pip install django-import-export。这个模块将允许我们通过几次点击而不是命令行过程轻松导入我们的文本。

现在我们有一个模型,我们需要在admin.py中注册它:

from import_export.admin import ImportExportModelAdmin
from django.contrib import admin
from .models import Text

@admin.register(Text)
class TextAdmin(ImportExportModelAdmin):
   pass

让我们登录到 Django 的管理部分,确保一切正常运行。我们首先必须运行我们的数据库命令:

  1. 准备数据库命令:python manage.py makemigrations

  2. 接下来,使用python manage.py migrate执行更改。

  3. 使用python manage.py createsuperuser创建一个管理用户,并按照提示操作。请注意,当您创建密码时,您将看不到输入,尽管它正在使用您的输入。

  4. 重新启动 Django:python manage.py runserver

  5. 访问127.0.0.1/admin,并使用刚刚创建的凭据登录。

我们将在我们的管理面板中看到我们的机器人应用程序:

图 14.1 - Django 的站点管理面板

太好了,那只是一个检查点。我们还有更多的工作要做!因为我们有django-import-export,让我们把它连接起来:

settings.py文件中进行以下操作:

  1. import_export添加到INSTALLED_APPS

  2. 在设置部分的末尾加上这行代码,正确地设置我们的静态文件路径:STATIC_ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static')

  3. 运行python manage.py collectstatic

现在,您可以继续在管理面板中点击“文本”,您将看到可用的“导入”和“导出”按钮:

图 14.2 - 是时候导入我们的文本了!

点击“导入”按钮,并按照步骤导入包含莎士比亚文本的 CSV 文件:

图 14.3 - 导入完成注意:导入会花一些时间,但不会像威尔一开始写作那样长!请务必在预览后确认导入。

路由我们的请求

在我们开始 React 之前,我们需要构建的下一个部分是我们的 API,它将为我们的前端提供内容。让我们看看步骤:

  1. bot/views.py中,设置我们将用于测试的索引路由,以及我们将用于提供信息的 API 路由:
from django.http import HttpResponse
from django.template import Context, loader
from bot.models import Text
import random
import json

def index(request):
   template = loader.get_template("bot/index.html")
   return HttpResponse(template.render())

def api(request):
   if request.method == 'POST':
       data = json.loads(request.body.decode("utf8"))
       query = data['chattext']
       responses = Text.objects.filter(PlayerLine__contains=" %s " 
       % (query))

   if len(responses) > 0:
       return HttpResponse(responses[random.randint(0,
       len(responses))])

   else:
       return HttpResponse("Get thee to a nunnery!")

所有这些都应该是简单的 Python,所以我们不会详细介绍。基本上,当我们向 API 发送 POST 请求时,Django 将在数据库中搜索包含通过 Ajax 发送的单词的文本行。如果找到一个或多个,它将随机返回一个给前端。如果没有,我们总是希望处理我们的错误情况,因此它将以哈姆雷特著名的一句话作为回应:“去修道院吧!”

  1. 创建一个文件bot/urls.py,并插入以下代码:
from django.urls import path

from . import views

urlpatterns = [
 path('', views.index, name='index'),
 path('api', views.api, name='api'),
]
  1. 编辑shakespearebot/urls.py如下:
from django.contrib import admin
from django.urls import path, include
import bot

urlpatterns = [
   path('admin/', admin.site.urls),
   path('api/', include('bot.urls')),
   path('', include('bot.urls')),
]
  1. 还有一件事:在shakespearebot/settings.py中,按照以下方式移除 CSRF 中间件:
'django.middleware.csrf.CsrfViewMiddleware',
  1. 现在是有趣的部分:我们用于测试的前端。创建一个名为bot/的文件

templates/bot/index.html并添加以下 HTML 设置:

<!DOCTYPE html>

<html>

<head>
 <style>
 textarea {
 height: 500px;
 width: 300px;
 }
 </style>
</head>

<body>
 <form method="POST" type="" id="chat">
 <input type="text" id="chattext"></textarea>
 <button id="submit">Chat</button>
 <textarea id="chatresponse"></textarea>
 </form>

</body>

</html>

在这里,我们可以看到一些基本的表单和一些样式 - 没有太多内容,因为这只是一个用来测试我们对 API 理解是否正确的页面。

  1. 在表单之后插入这个脚本:
<script>
   document.getElementById('submit').addEventListener('click', (e) 
   => {
     e.preventDefault()

     let term = document.getElementById('chattext').value.split('
      ')
     term = term[term.length - 2] || term[0]

     fetch("/api", {
       method: "POST",
       headers: {
         'Content-Type': 'application/json'
       },
       body: JSON.stringify({ chattext: term })
     })
       .then(response => response.text())
       .then(data => document.querySelector('#chatresponse').value
        += `\n${data}\n`)
   })
 </script>

到目前为止,fetch 调用的结构应该很熟悉,所以让我们快速浏览一下:当点击按钮时,将文本按空格分割,选择倒数第二个单词(最后一个“单词”可能是标点符号),或者如果是一个单词条目,则是单词本身。将这个术语发送到 API,并等待响应。

如果一切正常工作,我们应该会看到一个非常激动人心的页面:

图 14.4 - 这是一个开始!

虽然不多,但这应该足以测试我们的后端。尝试在聊天框中输入几个单词,单击聊天,然后看看会发生什么。希望您能听到很久以前在阿文长听到的一些话。

创建 React 前端

如前所述,有几种不同的方法可以使用 Django 和 React。我们将分别设置我们的前端,并让 React 做自己的事情,让 Django 做自己的事情,并让它们在中间握手。正如我们将看到的,这种方法确实有其局限性,但这是一个基本介绍。我们以后会变得更加复杂。

让我们开始吧,首先创建一个新的 React 应用程序:

  1. 切换到shakespearebot目录(而不是bot)并执行npx create-react-app react-frontend

  2. 继续执行cd react-frontend && yarn start并在http://localhost:3000访问开发服务器,以确保一切正常。您应该在前述 URL 收到 React 演示页面。使用Ctrl + C停止服务器。

  3. 执行yarn build

现在,这里的事情有点受限制。我们现在所做的是执行创建站点的生产优化构建。这是设计为发布代码,而不是开发代码,因此限制在于您无法编辑代码并在不再次运行构建的情况下反映出来。考虑到这一点,让我们构建并继续我们的设置。

在我们的shakespearebot目录中,我们将对settings.pyurls.py进行一些编辑:

  1. settings.pyTEMPLATES数组中,将DIRS更改为'DIRS': [os.path.join(BASE_DIR, 'react-frontend')],

  2. 同样在settings.py中,修改STATIC_URLSTATICFILES_DIRS变量如下:

STATIC_URL = '/static/'
STATICFILES_DIRS = (
 os.path.join(BASE_DIR, 'react-frontend', 'build', 'static'),

)
  1. urls.py中添加一行,以便urlpatterns数组读取如下:
urlpatterns = [
   path('admin/', admin.site.urls),
   path('api/', include('bot.urls')),
   path('', include('bot.urls')),
]
  1. bot目录中,是时候将我们的前端指向我们的静态目录了。首先,编辑urls.py,创建一个urlpatterns部分如下:
urlpatterns = [
    path('api', views.api, name='api'),
    path('', views.index, name='index'),
]
  1. 接下来,我们的视图将需要我们静态目录的路径。bot/views.py需要更改index路由以使用我们的 React 前端:
def index(request):
    return render(request, "../react-frontend/build/index.html")

那应该是我们需要的。继续通过运行python manage.py runserver在根级别启动服务器,然后访问http://127.0.0.1:8000并祈祷吧!您应该看到 React 欢迎页面!如果是这样的话,恭喜;我们已经准备好继续了。如果您遇到任何问题,请随时查阅 GitHub 存储库上的第二个航点目录。

完成我们的脚手架后,让我们看一个 React 与 Django 完整交互的示例。

将所有内容整合在一起

我们将使用一个完整的带有前端和后端的莎士比亚机器人。继续导航到shakespearebot-complete目录。在接下来的步骤中,我们将设置我们的应用程序,导入我们的数据,并与前端交互:

  1. 首先,使用python manage.py migrate运行 Django 迁移并使用python manage.py createsuperuser创建用户。

  2. 使用python manage.py runserver启动服务器。

  3. http://localhost:8000/admin登录。

  4. 转到http://localhost:8000/admin/bot/text/并导入Shakespeare_text.csv文件(这将需要一些时间)。

  5. 在导入过程中,我们可以继续使用cd react-frontend命令检查我们的前端。

  6. 使用yarn install安装我们的依赖项。

  7. 使用yarn start启动服务器。

  8. 现在,如果您导航到http://localhost:3000,我们应该看到我们的前端:

图 14.5 - 我们完整的 Shakespearebot

  1. 使用Ctrl + C停止开发服务器。

  2. 执行yarn build

  3. 导入完成后,我们可以访问我们的前端,然后我们应该能够通过在框中输入文本并单击“立即说话”按钮与莎士比亚互动。在localhost:8000/尝试一下。

有趣!它有点粗糙,肯定可以从前端的一些 CSS 工作和后端的智能方面通过自然语言处理中受益,但这并不是我们目前的目标。我们取得了什么成就?我们利用了我们的 Python 知识,并将其与 React 结合起来创建了一个完整的应用程序。在下一节中,我们将更仔细地研究应用程序的 React 部分。

调查 React 前端

我们的 React 前端目录结构非常简单:

.
├── App.css
├── App.js
├── App.test.js
├── components
│   ├── bot
│   │ └── bot.jsx
│   ├── chatpanel
│   │ ├── chatpanel.css
│   │ └── chatpanel.jsx
│   └── talkinghead
│       ├── shakespeare.png
│       ├── talkinghead.css
│       └── talkinghead.jsx
├── css
│   ├── parchment.jpg
│   └── styles.css
├── index.css
├── index.js
├── logo.svg
├── serviceWorker.js
└── setupTests.js

就像任何其他 React 应用程序一样,我们将从我们的根组件开始,这种情况下是App.js

import React from 'react';
import Bot from './components/bot/bot';
import './App.css';
import './css/styles.css'

function App() {
 return (
   <>
     <h1>Banter with the Bard</h1>
     <Bot />
   </>
 );
}

export default App;

到目前为止很简单:一个组件。让我们看看components/bot/bot.jsx

import React from 'react'
import TalkingHeadLayout from '../talkinghead/talkinghead'
import ChatPanel from '../chatpanel/chatpanel'
import { Col, Row, Container } from 'reactstrap'

export default class Bot extends React.Component {
 constructor() {
   super()

   this.state = {
     text: [
       "Away, you starvelling, you elf-skin, you dried neat's-tongue, 
        bull's-pizzle, you stock-fish!",
       "Thou art a boil, a plague sore.",
       "Speak, knave!",
       "Away, you three-inch fool!",
       "I scorn you, scurvy companion.",
       "Thou sodden-witted lord! Thou hast no more brain than I have in 
        mine elbows",
       "I am sick when I do look on thee",
       "Methink'st thou art a general offence and every man should beat 
        thee."
     ]
   }

   this.captureInput = this.captureInput.bind(this)
 }

到目前为止,除了常规设置外,没有什么特别令人兴奋的事情:我们导入了reactstrap,我们将用它来进行一些布局帮助,并在状态中定义了一个包含一些莎士比亚式的侮辱的文本数组。我们的最后一行涉及captureInput方法。这是什么:

captureInput(e) {
   const question = document.querySelector('#question').value
   fetch(`/api?chattext="${question}"`)
     .then((response) => response.text())
     .then((data) => {
       this.setState({
         text: `${data}`
       })
     })
 }

很棒!我们知道这在做什么:这是对同一服务器的标准 Ajax 调用,其中包含一个带有我们问题的 GET 请求。这与我们在 Python 中所做的有点不同,因为我们使用 GET 而不是 POST 来简化设置,但这只是一个微不足道的区别。

接下来的部分只是我们的渲染:

render() {
   const { text } = this.state

   return (
     <div className="App">
       <Container>
         <Row>
           <Col>
             <ChatPanel speak={this.captureInput} />
           </Col>
           <Col>
             <TalkingHeadLayout response={text} />
           </Col>
         </Row>
       </Container>
     </div>
   )
 }
}

我们的说话头有一点动画效果,我们是通过components/talkinghead/talkinghead.jsx中的一个 Node.js 模块来实现的:

import React from 'react'
import ReactTypingEffect from 'react-typing-effect';

import './talkinghead.css'
import TalkingHead from './shakespeare.png'

export default class TalkingHeadLayout extends React.Component {
 render() {
   return (
     <div id="talkinghead">
       <div className="text">
         <ReactTypingEffect text={this.props.response} speed="50" 
          typingDelay="0" />
       </div>
       <img src={TalkingHead} alt="Speak, knave!" />
     </div>
   )
 }
}

这基本上就是我们应用程序的全部内容了!

在本章中,我们玩得有点开心,让我们回顾一下我们学到了什么。

摘要

虽然我们的重点大多是通过选择 Node.js 和 Express 而不是 Python 和 Django 来摆脱 Python,但将它们整合起来是可行的。我们在这里使用了一个特定的范例:一个 React 应用程序作为静态构建的应用程序嵌入到 Django 应用程序中。Django 应用程序将 HTTP 请求路由到 APIbot应用程序(如果 URL 中包含/api),或者对于其他所有内容,路由到 Reactreact-frontend应用程序。

将 Django 与 React 整合起来并不是世界上最容易的事情,这只是如何将它们耦合在一起的一种可能的范例,我称之为紧密耦合的脚手架。如果我们的 React 和 Django 应用程序完全分开,并且只通过 Ajax 进行 XHR 调用进行交互,那可能是一个更贴近实际情况的场景。然而,这将涉及为两个部分分别设置,而今天我们构建的是一个整个应用程序的单一服务器。

在下一章中,我们将在一个更直接的互补技术应用中使用 Express 和 React。

第十五章:将 Node.js 与前端结合

现在我们知道了前端框架和 Node.js,让我们将两端连接起来。我们将构建三个小应用程序,以演示我们的知识几乎实现全栈功能。毕竟,前端和后端都想要彼此了解!这将是我们首次尝试同时使用这些技术,所以一定要给自己足够的空间和时间来学习,因为这些是重要但非常重的话题。

本章将涵盖以下主题:

  • 理解架构握手

  • 前端和 Node.js:React 和图像上传

  • 使用 API 和 JSON 创建食谱书

  • 使用 Yelp 和 Firebase 创建餐厅数据库

技术要求

准备好使用存储库的Chapter-15目录中提供的代码:github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-15。由于我们将使用命令行工具,还要准备好您的终端或命令行 shell。我们需要一个现代浏览器和一个本地代码编辑器。

理解架构握手

既然我们在前端和后端都有了 JavaScript 的经验,让我们讨论一下将这两个部分绑在一起到底意味着什么。我们知道前端的 JavaScript 非常适合用户交互、视觉、数据验证和其他与用户体验相关的部分。后端的 Node.js 是一个强大的服务器端语言,可以帮助我们做几乎任何其他服务器端语言需要做的事情。那么,理论上将这两端结合起来是什么样子呢?

也许你会想知道为什么一个应用程序会有两端。我们知道 Python、Node.js 和 JavaScript 都执行不同的任务,并且在前端或后端执行,但背后的理论是什么?答案是:软件工程中有一个被称为关注分离*的原则,基本上是指程序的每个部分应该做一项或几项任务,并且做得很好。与其使用单片应用程序,实际上,一个对规模有良好反应的模块化系统的概念更高效。在本章中,我们将创建三个应用程序来使用这个原则。

前端和 Node.js - React 和图像上传

让我们从将 React 和 Node 绑定在一起开始。准备好跟随解决方案代码一起进行,网址是github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-15/photo-album。我们将构建一个类似于这样的相册应用程序:

图 15.1 - 我们的相册

我们将首先探索架构布局,然后我们将审查 React 代码,最后我们将检查 Express 后端。

架构

这个应用程序将使用后端的 Node.js 来存储我们上传的文件,并在前端使用 React。但是我们该如何做呢?从概念上讲,我们需要告诉 React 使用 Express 应用程序来提供 React 信息并消耗我们发送的文件。为了实现这一点,我们在package.json文件中使用了一个代理。它基本上看起来像这样:

图 15.2 - 代理

如果您对代理的概念不熟悉,基本上它在计算中的意思与英语中的意思相同:一个代理人代表另一个代理人执行操作。它本质上是一个中间人,正如这个图表所示,它可以被认为是我们目的的中间人。由于 React 和前端 JavaScript 无法与文件系统交互或执行我们在第十二章中学到的其他重要操作,Node.js vs Python,以及第十三章 使用 Express,我们需要使用我们的能力将前端和后端连接在一起。因此,代理的概念。

让我们看一下package.json中的一行:

"proxy": "http://localhost:3001",

这告诉 React 将某些请求路由到我们的 Express 应用程序。如果您正在从 GitHub 上跟随代码,这意味着我们实际上需要执行一些不同的npm命令:

  1. 首先,在photo-album目录中安装 Express 的包:npm install

  2. 启动 Express 服务器:npm start

  3. 在另一个终端窗口中,cd进入client目录并运行npm install

  4. 现在,使用npm start启动 React 应用程序。

当我们访问http://localhost:3000时,我们的相册应用程序已经准备好使用。尝试通过选择文件并点击上传来上传照片。UI 也会刷新并显示您刚刚上传的照片。恭喜!这是一个端到端的应用程序!

那么这段代码在做什么呢?让我们来分析一下。

首先,我们来看一下 JavaScript。

调查 React JSX

打开client/src/components/upload/Upload.jsx。我们将首先检查render()方法的内容:

<p><button id="upload" onClick={this.upload}>Upload Photo</button></p>
<div id="uploadForm" className="w3-modal">   <form method="post"
 encType="multipart/form-data">
     <p><input type="file" name="filetoupload" /></p>
     <p><button type="submit" onClick={this.uploadForm}>Upload</button></p>
   </form>
</div>

太好了,这是一个基本的 HTML 表单。这个表单中唯一与 React 相关的部分是点击处理程序。让我们看一下表单的onClick方法:this.uploadForm。如果我们查看该方法,我们将看到我们上传表单的真正功能:

 uploadForm(e) {
 e.preventDefault();
 const formData = new FormData()

 formData.append('file', document.querySelector('input').files[0]);

 fetch("http://localhost:3000/upload", {
   method: 'POST',
   body: formData
 })
   .then(() => {
     this.props.reload()
   })
}

您准备好查看 Node.js Express 路由了吗?

解密 Express 应用程序

打开routes/upload.js。它非常简单:

const express = require('express');
const formidable = require('formidable');
const router = express.Router();
const fs = require('fs');

router.post('/', (req, res, next) => {
  const form = new formidable.IncomingForm().parse(req)
    .on('fileBegin', (name, file) => {
      file.path = __dirname + '/../public/images/' + file.name
    })
    .on('file', () => {
      res.sendStatus(200)
    })
});

module.exports = router;

为了让我们的生活变得更轻松,我们使用了一个名为 Formidable 的表单处理程序包。当通过 Ajax 收到 POST 请求到/upload端点时,它将运行此代码。当通过 Ajax 接收到表单时,我们的承诺会监听文件并触发fileBeginfile事件,这将把文件写入磁盘,然后发出成功信号。这是我们在Upload.jsx中使用的上传表单的方法,以及我们的应用程序的两个方面如何联系在一起,以执行前端 JavaScript 无法单独执行的操作——访问服务器的文件系统。

使用前端上传几张图片。您会注意到它们将存储在public/images中,就像我们在代码中读到的那样。请注意,这个系统非常简单:它不会检查是否是图像文件,而是盲目地接受我们发送的内容并将其存储在文件系统中。在实践中,这是危险的。在处理用户输入时,始终需要预防攻击和可能的恶意文件。虽然保护您的 Web 应用程序的方法有些超出了本书的范围,但需要牢记的一个基本原则是:不要相信用户。我们已经研究了在前端验证输入的方法,虽然这很有用,但在后端也检查它同样重要。一些可能的威胁减少方法包括列出某些文件扩展名,黑名单其他文件扩展名,并使用沙盒环境来运行上传文件的分析代码,以确定它是否是无害的图像文件。

现在我们已经上传了我们的图片,让我们继续进行应用程序的检索方面。打开routes/gallery.js

var express = require('express');
const fs = require('fs');

var router = express.Router();

router.get('/', (req, res, next) => {
 fs.readdir(`${__dirname}/../public/images`, (err, files) => {
     if (err) {
       res.json({
         path: '',
         files: []
       });
       return;
     }

     const data = {
       path: 'images/',
       files: files.splice(1,files.length) // remove the .gitignore
     };
     res.json(data);
 });
});

router.delete('/:name', (req, res) => {
 fs.unlink(`${__dirname}/../public/images/${req.params.name}`, (err) => {
   res.json(1)
 });
});

module.exports = router;

希望这不会太难解释。在我们的 GET 路由中,我们首先检查文件系统,看看我们是否可以访问文件。如果出现某种原因的错误,比如权限不正确,我们将向前端发送错误并中止。否则,我们将格式化我们的返回数据并发送!非常简单。

我们的下一个方法定义了 DELETE 功能,它是一个简单的文件系统 unlink 方法。这个功能的前端并不是很复杂:如果你点击我们画廊中的一张图片,它将删除这张照片。当然,在实践中,你可能希望有一些更好的用户界面和确认消息,但对于我们的目的来说,这已经足够了。

欢迎来到你的第一个端到端应用程序!

继续进行我们的下一个应用程序!

使用 API 和 JSON 创建食谱

使用后端的美妙之一是促进应用程序、文件系统和 API 之间的通信。以前,我们所做的所有工作都局限于前端,没有持久性。现在我们将制作一个食谱应用程序,以 JSON 格式保存我们的信息。别担心,我们将在第十八章中使用数据库,Node.js 和 MongoDB。现在,我们将使用本地文件。这是我们要构建的内容:

图 15.3 - 我们的食谱册

首先,我们将使用第三方 API 设置凭据,然后继续编写代码。

设置应用程序

github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-15/recipe-book/上克隆起始代码。确保在该目录和client内执行npm install。我们还需要做一些设置来访问我们的 API。要访问 Edamam API,请在developer.edamam.com/注册免费 API 密钥以获取食谱搜索 API。

在我们项目的根目录,创建一个.env文件,并填写如下内容:

APPLICATION_ID=<your id>
APPLICATION_KEY=<your key>

请注意,这些都是构造为环境变量,没有分号或空格。

我们接下来要做的一步是确保我们的应用程序可以读取这些变量。在app.js的末尾附近,你会看到这个:

console.log(process.env.APPLICATION_ID, process.env.APPLICATION_KEY);

process.env.<variable name>的构造方式是我们如何访问.env中的环境变量的。提供这种访问的机制是dotenv包;你可以看到它包含在package.json中;文件中的环境变量默认情况下不包括在内。

为什么我们要使用环境文件?正如我们将在第十七章中学到的那样,安全和密钥,我们不希望在我们可能提交到 GitHub 或类似平台的代码中暴露我们的 API 密钥,因为那样会允许任何人使用(和滥用)我们的密钥。我们必须保持它们的安全性,如果你注意到.gitignore文件中,我已经列出了.env不要在 Git 中提交,这就是为什么你必须自己创建这个文件。这是敏感信息的最佳实践。虽然这可能会使开发人员之间共享代码变得有点棘手,但最好还是将敏感信息与我们的代码分开。

让我们测试我们的 API。

测试 API

如果你阅读routes/tests.js,你可以看到我们到底在做什么:

const https = require('https');

require('dotenv').config();

https.get(`https://api.edamam.com/search?app_id=${process.env.APPLICATION_ID}&app_key=${process.env.APPLICATION_KEY}&q=cheesecake`, (res) => {
 console.log("Got response: " + res.statusCode)

 res.setEncoding('utf8')
  res.on("data", (chunk) => {
   console.log(chunk)
 })
}).on('error', (e) => {
 console.log("Got error: " + e.message);
})

我们的fetch调用是硬编码为搜索cheesecake(我最喜欢的甜点...问我食谱),如果我们用node routes/tests.js运行它,我们将在控制台中看到一堆 JSON 返回。如果你遇到任何问题,请确保检查你的 API 密钥。

深入代码

既然我们知道我们的 API 调用是有效的,让我们切换到我们的前端。看一下client/src/components/search/Search.jsx及其render函数:

render() {
 return (
   <h2>Search for: <input type="text" id="searchTerm" />
     <button onClick={this.submitSearch}>Search!</button></h2>
 )
}

到目前为止,这是一个简单的表单。接下来,让我们看看submitSearch方法:

 submitSearch(e) {
 e.preventDefault()

 fetch(`http://localhost:3000/search?q=${document.querySelector('#searchTerm').value}`)
   .then(data => data.json())
   .then((json) => {
     this.props.handleSearchResults(json)
   })
}

我们再次使用代理来从表单提交我们的搜索。在获得结果后,我们将 JSON 传递给来自父组件RecipeBookpropshandleSearchResults方法。我们稍后会看一下,但现在让我们切换回 Express 应用程序,看看我们的搜索路由在做什么。看一下routes/search.js

GET 路由实际上非常简单:

router.get('/', (req, res, next) => {
 https.get(`https://api.edamam.com/search?app_id=${process.env.APPLICATION_ID}&app_key=${process.env.APPLICATION_KEY}&q=${req.query.q}`, (data) => {

   let chunks = '';

   data.on("data", (chunk) => {
     chunks += chunk
   })

   data.on("end", () => {
     res.send(JSON.parse(chunks))
   })

   data.on('error', (e) => {
     console.log("Got error: " + e.message);
   })
 })
});

这应该看起来有点类似于我们的测试文件。我们再次使用我们的.env文件来进行搜索查询,但这次我们传递了查询字符串参数进行搜索并处理错误。我们的data.on("end")处理程序将我们的结果传递回 React,以便在RecipeBook.jsx中使用handleSearchResults方法:

handleSearchResults(data) {
 const recipes = []

 data.hits.forEach( (item) => {
   const recipe = item.recipe

   recipes.push({
     "title": recipe.label,
     "url": recipe.url,
     "image": recipe.image
   })
 })

 this.setState({
   recipes: recipes
 })
}

我们正在解析出我们应用程序所需的数据,并将其分配给组件的状态。到目前为止一切顺利!

接下来是食谱书的render方法,用于显示我们的搜索结果:

<Search handleSearchResults={this.handleSearchResults} />

{
 recipes.length > 0 ? (
   <>
     <p>Search Results</p>
     <div className="card-columns">
       {
         recipes.map((recipe, i) => (
           <Recipe recipe={recipe} key={i} search="true" 
            refresh={this.refresh} />
         ))
       }
     </div>
   </>
 ) : <p></p>

我们使用另一个三元运算符来有条件地呈现我们的结果,如果有的话,作为<Recipe>组件。我们的 key 属性只是 React 希望项目具有的唯一标识符,但refresh属性是一个有趣的属性。让我们看看它在Recipe组件中是如何使用的。

我们的Recipe组件的render方法相当标准:它使用一些 Bootstrap 组件来呈现我们漂亮的小卡片,但除此之外并不引人注目。save方法才是我们真正想要调查的内容:

save(e) {
   e.preventDefault()

   const recipe = { [this.props.recipe.title]: this.props.recipe }

   fetch('http://localhost:3000/recipes', {
     method: 'POST',
     headers: {
       'Accept': 'application/json',
       'Content-Type': 'application/json'
     },
     body: JSON.stringify(recipe)
   })
   .then(json => json.json())
   .then( (data) => {
     this.props.refresh(data)
   })
 }

const recipe声明可能看起来有点奇怪,让我们来解开它。这是创建一个对象键/值对,对于键,我们使用了食谱的标题。因为它是一个变量,我们希望使用方括号来表示它应该被解释。我们不能使用点属性作为键,所以我们的标题将是一个字符串。

这是一个构造中食谱的示例可能是这样的:

{"Strawberry Cheesecake Parfaits": {"title":"Strawberry Cheesecake Parfaits", "image":"https://www.edamam.com/web-img/d4c/d4c3a4f1db4e8c413301ae1f324cf32a.jpg", "url":"http://honestcooking.com/strawberry-cheesecake-parfaits/"}}

它包含了我们之前在RecipeBook.jsx中映射对象时指定的所有信息。我们过程的下一步是使用另一个fetch请求将食谱保存到文件系统中。

回到 Express,这次是到routes/recipes.js

让我们逐部分查看文件。在我们的 Express 方法之外,我们有一个readData方法,它检查我们的recipes.json文件是否存在:

const readData = () => {
 if (!fs.existsSync(__dirname + "/../data/recipes.json")) {
   fs.writeFileSync(__dirname + "/../data/recipes.json", '[]')
 }

 return JSON.parse(fs.readFileSync(__dirname + "/../data/recipes.json"))
}

如果没有,它将创建一个包含空数组的文件。然后将文件的内容(无论是空的还是非空的)返回给调用函数。

我们的 GET 方法从readData中消耗数据,并将其发送到响应中,这次是到RecipeBook.jsx

router.get('/', (req, res, next) => {
 const recipes = readData()
 res.json(recipes)
})

RecipeBook.render方法的第二部分(我们没有看到)类似于搜索结果的 JSX,并且消耗了这个 JSON。

我们的save方法与我们的readData方法有些相似:

router.post('/', (req, res) => {
 let recipes = readData()
 const data = req.body
 recipes.push(data)
 fs.writeFileSync(__dirname + "/../data/recipes.json",JSON.stringify(recipes))
 res.json(recipes)
})

请注意,它还将 JSON 发送到响应,因此当项目保存时,它还会在RecipeBook.jsx中填充保存的食谱。可能不用说,但请注意我们再次使用readData方法,而不是重写相同的逻辑,使我们的代码保持 DRY。

这就是我们应用程序的逻辑!我们成功地将 API、Node.js、Express 和 React 组合成了一个端到端的应用程序。接下来,我们将创建一个更符合实际的应用程序:我们将创建一个餐厅搜索应用程序,将其保存到一个云数据库中,并通过 JavaScript 访问。

使用 Yelp 和 Firebase 创建餐厅数据库

到目前为止,我们的应用程序相当简单,只是在文件系统上存储信息。然而,在大多数情况下,您可能希望它是某种数据库,而不是静态文件。我们将使用 Firebase,这是一个与 JavaScript 兼容良好的基于云的 NoSQL 数据库,但首先让我们设置 React 脚手架。

开始行 - 创建一个 React 应用程序

我们之前已经进行了几次这样的设置,所以这应该不足为奇:

  1. 使用npx create-react-app restaurant-finder创建一个新的 React 应用程序,我们准备好了!

  2. 使用npm start测试您的设置,并访问http://localhost:3000

使用 Firebase 进行设置

我们要做的第一件事是设置我们的 Firebase 帐户。

请记住,Firebase 的用户界面(与大多数网站一样)会定期更改,因此我不会为注册过程向您展示截图。如果您在设置过程中遇到任何问题,可以查阅文档。以下是步骤:

  1. 转到firebase.google.com

  2. 如果您还没有 Google 帐户,您需要创建一个,然后访问控制台。

  3. 创建一个名为restaurant-database的新项目。

  4. 您可以选择为项目启用 Google Analytics;这取决于您。

  5. 在项目概述页面上,我们将使用</>按钮访问网页应用程序的设置说明。

  6. 在下一个屏幕上,创建一个应用程序昵称(您可以再次使用restaurant-database),您不需要设置 Firebase Hosting。

  7. 下一个屏幕将向您显示包含您的 Firebase 配置的代码,但我们不会完全按照说明进行,因为我们可以使用 Node 模块来帮助我们!不过,请复制firebaseConfig变量中的信息:我们以后会用到它。

  8. 当您的数据库创建好后,转到 UI 中的数据库选项卡,选择实时数据库,并在测试模式下启动它。

然后您应该看到类似于这样的屏幕:

图 15.4 - Firebase 的基本测试模式视图

接下来,我们将返回到命令行,并准备好使用 Firebase。安装 Firebase 工具包:npm install firebase

安装就是这样!接下来,在我们项目的根目录创建一个.env文件,并输入您之前从firebaseConfig中复制的凭据,类似于这样:

REACT_APP_apiKey=<key>
REACT_APP_authDomain=restaurant-database-<id>.firebaseapp.com
REACT_APP_databaseURL=https://restaurant-database-<id>.firebaseio.com
REACT_APP_projectId=restaurant-database-<id>
REACT_APP_storageBucket=restaurant-database-<id>.appspot.com
REACT_APP_messagingSenderId=<id>
REACT_APP_appId=<id>

请注意REACT_APP_的前缀,等号,引号和缺少尾随逗号。填写您的配置类似。

在我们进一步之前,让我们测试我们的数据库。

测试我们的数据库

现在我们将创建一些 React 组件。在src中创建一个components目录,在其中创建两个名为databasefinder的目录。我们将首先创建我们的数据库引用:

  1. 在数据库目录中,创建一个database.js文件。请注意,它是js,而不是jsx,因为我们实际上不会渲染任何数据。相反,我们将返回一个变量给jsx组件。您的文件应该如下所示:
import * as firebase from 'firebase'

const app = firebase.initializeApp({
 apiKey: process.env.REACT_APP_apiKey,
 authDomain: process.env.REACT_APP_authDomain,
 databaseURL: process.env.REACT_APP_databaseURL,
 projectId: process.env.REACT_APP_projectId,
 storageBucket: process.env.REACT_APP_storageBucket,
 messagingSenderId: process.env.REACT_APP_messagingSenderId,
 appId: process.env.REACT_APP_appId
})

const Database = app.database()

export default Database

请注意每个变量上的process.env前缀以及尾随逗号。process.env指定应用程序应查看dotenv提供的环境变量。

  1. 接下来,我们有Finder.jsx。在finder目录中创建此文件:
import React from 'react'
import Database from '../database/database'

export default class Finder extends React.Component {
 constructor() {
   super()

   Database.ref('/test').set({
     helloworld: 'Hello, World'
   })
 }

 render() {
   return <h1>Let's find some restaurants!</h1>
 }
}

我们的App.js文件将如下所示:

import React from 'react'
import Finder from './components/finder/Finder'
import './App.css'

function App() {
 return (
   <div className="App">
     <Finder />     
   </div>
 );
}

export default App;
  1. 现在,由于我们刚刚创建了我们的环境变量,我们需要停止并重新启动我们的 React 应用程序。这对于我们大部分的 React 工作来说并不是必需的,但在这里是必需的。

  2. 继续访问http://localhost:3000上的应用程序。我们应该只在页面上看到“让我们找一些餐馆”,但是如果我们转到 Firebase,我们会看到这个:

图 15.5 - 我们在 Firebase 中有数据!

数据似乎被截断了,但您可以单击它并查看整个语句。

万岁!我们的 Firebase 正在运行。现在是我们应用程序的其余部分。

创建我们的应用程序

我们可以从Finder.jsx中删除测试插入。这就是我们要做的事情:

图 15.6 - 餐厅查找器

为了实现这一点,我们将使用 Yelp API。首先,您需要转到www.yelp.com/developers并注册 Yelp Fusion API 密钥。一旦您拥有它,我们将把它存储在一个新的.env文件中的新api目录中。

Yelp Fusion API 并不在所有国家/地区都可用,所以如果你无法访问它,请在 GitHub 的Chapter-15文件夹中寻找替代 API 使用示例。

Yelp API 是一个 REST API,不允许来自前端 JavaScript 的连接,以保护你的密钥。因此,就像我们的食谱书一样,我们将创建一个小的 API 层来处理我们的请求。不同于我们的食谱书,这将会相当简单,所以我们不会使用 Express。让我们看看步骤:

  1. 在项目的根目录,我们将安装一些工具供我们使用:npm install yelp-fusion dotenv react-bootstrap

  2. 在项目的根目录创建一个名为api的目录,并在其中创建一个api.js文件。

  3. 我们也将在我们的api目录中有一个.env文件:

Yelp_Client_ID=<your client id>
YELP_API_Key=<your api key>
  1. 如果你使用 Git,不要忘记将这些添加到.gitignore条目

我们的api.js文件将会相当简单:

const yelp = require('yelp-fusion');
const http = require('http');
const url = require('url');
require('dotenv').config();

const hostname = 'localhost';
const port = 3001;

const client = yelp.client(process.env.YELP_API_Key);

const server = http.createServer((req, res) => {
 const { lat, lng, value } = url.parse(req.url, true).query

 client.search({
   term: value,
   latitude: lat,
   longitude: lng,
   categories: 'Restaurants'
 }).then(response => {
   res.statusCode = 200;
   res.setHeader('Content-Type', 'application/json');

   res.write(response.body);
   res.end();
 })
   .catch(e => {
     console.error('error',e)
   })
 });

 server.listen(port, hostname, () => {
   console.log(`Server running at http://${hostname}:${port}/`);
 });

到目前为止,很多内容应该都很熟悉:我们将包括一些包,比如之前使用过的 Yelp API,我们将定义一些变量来帮助我们。接下来,我们将使用httpcreateServer方法创建一个非常简单的服务器来响应我们的 API 请求。在其中,我们将使用urlparse方法来获取我们的查询字符串参数,然后将其传递给我们的 API。

接下来的部分,client.search,可能会让人感到陌生。这是从 Yelp 文档中提取的,专门设计以符合他们 API 的要求。一旦我们有了异步响应,我们就将其发送回我们的请求应用程序。不要忘记处理错误!然后我们在端口3001上启动服务器。你可以使用node api.js启动这个服务器,然后你会看到关于它运行的控制台错误消息。

现在让我们把注意力转向我们应用程序的 React 部分:

  1. 在我们的src目录中,当我们完成时,将会有这样的文件结构:
.
├── App.css
├── App.js
├── App.test.js
├── components
│   ├── database
│   │ └── database.js
│   ├── finder
│   │ └── Finder.jsx
│   ├── restaurant
│   │ ├── Restaurant.css
│   │ └── Restaurant.jsx
│   └── search
│       └── Search.jsx
├── index.css
├── index.js
├── logo.svg
├── serviceWorker.js
└── setupTests.js

许多这些文件在我们之前搭建应用程序时已经创建好了,但components目录的一些部分是新的。

  1. 创建这些文件,我们将从探索Restaurant.jsx开始:
import React from 'react'
import { Button, Card } from 'react-bootstrap'
import Database from '../database/database'

import './Restaurant.css'

export default class Restaurant extends React.Component {
 constructor() {
   super();

   this.saveRestaurant = this.saveRestaurant.bind(this)
 }

 saveRestaurant(e) {
   const { restaurant } = this.props

   Database.ref(`/restaurants/${restaurant.id}`).set({
     ...restaurant
   })
 }

 render() {
   const { restaurant } = this.props

   return (
     <Card>
       <Card.Img variant="top" src={restaurant.image_url} 
        alt={restaurant.name} />
       <Card.Body>
         <Card.Title>{restaurant.name}</Card.Title>
         {!this.props.saved && <Button variant="primary" 
         onClick={this.saveRestaurant}>Save Restaurant</Button>}
      </Card.Body>
     </Card>
   )
 }
}

大部分内容都不是新的,我们的食谱书的结构可以帮助我们理清思路。不过,我们应该拆分saveRestaurant方法,因为它使用了一些有趣的部分:

saveRestaurant(e) {
   const { restaurant } = this.props

   Database.ref(`/restaurants/${restaurant.id}`).set({
     ...restaurant
   })
 }

首先,我们可以推断出我们将从组件的props中获取餐厅的数据。这将直接来自我们的搜索结果。因此,我们需要稍微处理一下我们的数据。

这是我们从props中得到的搜索结果的样子:

{id: "CO3lm5309asRY7XG5eXNgg", alias: "rahi-new-york", name: "Rahi", image_url: "https://s3-media1.fl.yelpcdn.com/bphoto/rPh_LboeIOiTVeXCuas5jA/o.jpg", is_closed: false, ...}
id: "CO3lm5309asRY7XG5eXNgg"
alias: "rahi-new-york"
name: "Rahi"
image_url: "https://s3-media1.fl.yelpcdn.com/bphoto/rPh_LboeIOiTVeXCuas5jA/o.jpg"
is_closed: false
url: "https://www.yelp.com/biz/rahi-new-york?adjust_creative=-YEyXjz9iO0W5ymAnPt6kA&utm_campaign=yelp_api_v3&utm_medium=api_v3_business_search&utm_source=-YEyXjz9iO0W5ymAnPt6kA"
review_count: 448
categories: (3) [{...}, {...}, {...}]
rating: 4.5
coordinates: {latitude: 40.7360271, longitude: -74.0005436}
transactions: (2) ["delivery", "pickup"]
price: "$$$"
location: {address1: "60 Greenwich Ave", address2: "", address3: null, city: "New York", zip_code: "10011", ...}
phone: "+12123738900"
display_phone: "(212) 373-8900"
distance: 1305.5181202902097
  1. 我们将其保存到 Firebase 中:
Database.ref(`/restaurants/${restaurant.id}`).set({
  ...restaurant
})

我们使用展开运算符(三个点)来将对象扩展为其组成的键/值对,以避免在我们的数据库中出现嵌套对象。我们还有一点点 CSS 来格式化我们的卡片。

让我们把注意力转向Search组件:

import React from 'react'
import { Button } from 'react-bootstrap'
import Restaurant from '../restaurant/Restaurant'

export default class Search extends React.Component {
 constructor() {
   super()

   this.state = {
     businesses: []
   }

在我们的构造函数中,我们做了一些有趣的事情:浏览器地理定位

你是否见过某些网站询问你的位置时弹出的小警告窗口?这就是这些网站的做法。如果浏览器支持地理定位,我们将使用它并从浏览器中设置我们的纬度和经度。否则,我们将简单地将其设置为null

   if (navigator.geolocation) {
     navigator.geolocation.getCurrentPosition((position) => {
       this.setState({
         lng: position.coords.longitude,
         lat: position.coords.latitude
       })
     })

   } else {
     this.setState({
       lng: null,
       lat: null
     })
   }

   this.search = this.search.bind(this)
   this.handleChange = this.handleChange.bind(this)
 }

 handleChange(e) {
   this.setState({
     val: e.target.value
   })
 }

搜索端点的构建应该看起来很熟悉:

 search(event) {
   const { lng, lat, val } = this.state

   fetch(`http://localhost:3000/businesses/search?
   value=${val}&lat=${lat}&lng=${lng}`)
     .then(data => data.json())
     .then(data => this.handleSearchResults(data))
 }

 handleSearchResults(data) {
   this.setState({
     businesses: data.businesses
   })
 }

 render() {
   const { businesses } = this.state

   return (
     <>
       <h2>Enter a type of cuisine: <input type="text" onChange=
       {this.handleChange} /> <Button id="search" onClick={this.search}>
       Search!</Button></h2>
       <div className="card-columns">
         {
           businesses.length > 0 ? (
             businesses.map((restaurant, i) => (
               <Restaurant restaurant={restaurant} key={i} />
             ))
           ) : <p>No results</p>
         }
       </div>
     </>
   )
 }
}

当你在我们的代码中前进时,如果你得到纬度或经度的空值,你可能需要完全退出 React 应用程序并重新启动它。

类似于我们的食谱书通过代理调用我们的 Express 应用程序,不要忘记将这行添加到你的package.json文件中:"proxy": "http://localhost:3001"。这样我们就可以使用fetch。这些是我们传递给api.js的值,用于向 Yelp API 发送请求。

我们的应用程序快要完成了!接下来是我们开始的Finder组件:

  1. 首先,我们有我们的导入:
import React from 'react'
import Database from '../database/database'
import { Tabs, Tab } from 'react-bootstrap'
import Search from '../search/Search'
import Restaurant from '../restaurant/Restaurant'

  1. 接下来,我们有一些非常标准的部分:
export default class Finder extends React.Component {
 constructor() {
   super()

   this.state = {
     restaurants: []
   }

   this.getRestaurants = this.getRestaurants.bind(this)
 }

 componentDidMount() {
   this.getRestaurants()
 }
  1. 作为一个新的部分,让我们来看看我们如何从 Firebase 中检索信息:
 getRestaurants() {

   Database.ref('/restaurants').on('value', (snapshot) => {
     const restaurants = []

     const data = snapshot.val()

     for(let restaurant in data) {
       restaurants.push(data[restaurant])
     }
     this.setState({
       restaurants: restaurants
     })
   })
 }

关于 Firebase 的一个有趣之处是它是一个实时数据库;你不总是需要执行查询来检索最新的数据。在这个构造中,我们告诉数据库不断更新我们组件的状态,以反映/restaurants的值的变化。当我们保存一个新的餐馆并转到我们的“已保存!”选项卡时,我们将看到我们的新条目。

  1. 我们在这里使用了其他组件,将其完整地呈现出来:
 render() {

   const { restaurants } = this.state
   return (
     <>
       <h1>Let's find some restaurants!</h1>

       <Tabs defaultActiveKey="search" id="restaurantsearch">
         <Tab eventKey="search" title="Search!">
           <Search handleSearchResults={this.handleSearchResults} 
        />
         </Tab>
         <Tab eventKey="saved" title="Saved!">
           <div className="card-columns">
             {
               restaurants.length > 0 ? (
                 restaurants.map((restaurant, i) => (
                   <Restaurant restaurant={restaurant} saved={true} 
                    key={i} />
                 ))
               ) : <p>No saved restaurants</p>
             }
           </div>
         </Tab>
       </Tabs>
     </>
   )
 }
}

当一切都完成时,我们将保持我们的api.js文件运行,并使用npm start启动我们的 React 应用程序,我们的应用程序就完成了!

是时候结束本章了。

总结

在本章中,我们涵盖了很多内容。JavaScript 在前端和后端的强大功能向我们展示,我们确实可以用它来满足许多应用程序需求,不仅仅是 Python。我们使用了很多 React,但请记住,任何前端都可以在这里替代:Vue、Angular,甚至是无框架的 HTML、CSS 和 JavaScript 都可以用来创建强大的 Web 应用程序。

在使用 JavaScript 和 API 时需要注意的一点是,有些情况下我们需要一个中间件层,例如在保存文件或使用密钥访问 REST API 时。将 Express 与基本的 Node.js 脚本结合起来与 API 进行交互,这只是 JavaScript 和 Node.js 结合所能实现的开始。

在下一章中,我们将探讨 webpack,这是一个工具,允许我们将 JavaScript 应用逻辑地组合和打包以进行部署。

第十六章:进入 Webpack

所以,现在你有了漂亮的前端和后端代码。太棒了!它看起来在你的笔记本上如此漂亮……那么下一步是什么?将它发布到世界上!听起来很容易,但当我们有像 React 这样的高级 JavaScript 使用时,我们可能还想采取一些额外步骤,以确保我们的代码以最高效率运行,所有依赖项都得到解决,并且一切都与现代技术兼容。此外,下载大小是一个重要考虑因素,所以让我们探讨一下 webpack,这是一个帮助解决这些问题的工具。

在本章中,我们将涵盖以下几点:

  • 捆绑和模块的需求

  • 使用 webpack

  • 部署

技术要求

准备好使用存储库的Chapter-16目录中提供的代码:github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-16。因为我们将使用命令行工具,还要准备好你的终端或命令行 shell。我们需要一个现代浏览器和一个本地代码编辑器。

捆绑和模块的需求

理想情况下,一切都会在网站上无缝运行,无需采取任何额外步骤。你拿起你的源文件,放在一个 web 服务器上,然后:一个网站。然而,情况并非总是如此。例如,对于 React,我们需要运行npm run build来为我们的项目生成一个输出分发目录。我们可能还有其他类型的非源文件,比如 SASS 或 TypeScript,需要转换成浏览器可以理解的原生文件格式。

那么,模块是什么?有模块化编程的概念,它将大型程序按照关注点和封装(作用域)分离成更小、更独立的模块。模块化编程背后的思想有很多:作用域、抽象、逻辑设计、测试和调试。同样,一个捆绑是浏览器可以轻松使用的一块代码,通常由一个或多个模块构成。

现在是有趣的部分:我们已经使用过模块!让我们来看看我们在第十一章中编写的一些 Node.js 代码,什么是 Node.js?:

const readline = require('readline')
const randomNumber = Math.ceil(Math.random() * 10)

const rl = readline.createInterface({
 input: process.stdin,
 output: process.stdout
});

askQuestion()

function askQuestion() {
 rl.question('Enter a number from 1 to 10:\n', (answer) => {
   evaluateAnswer(answer)
 })
}

function evaluateAnswer(guess) {
 if (parseInt(guess) === randomNumber) {
   console.log("Correct!\n")
   rl.close()
   process.exit(1)
 } else {
   console.log("Incorrect!")
   askQuestion()
 }
}

在第一行,我们使用了一个叫做readline的模块,如果你还记得我们的程序,它将被用来从命令行接收用户输入。我们在 React 中也使用了它们——每当我们需要使用npm install时,我们都在使用模块的概念。那么这为什么重要呢?让我们考虑从头开始标准的create-react-app安装:

  1. 使用npx创建一个新的 React 项目:npx create-react-app sample-project

  2. 进入目录并安装依赖项:cd sample-project ; npm install

  3. 使用npm start启动项目。

如果你还记得,这给我们一个非常有趣的起始页面:

图 16.1 – React 起始页面

当我们运行npm install时,我们到底得到了什么?让我们看看我们的文件结构:

.
├── README.md
├── package-lock.json
├── package.json
├── public
│   ├── favicon.ico
│   ├── index.html
│   ├── logo192.png
│   ├── logo512.png
│   ├── manifest.json
│   └── robots.txt
├── src
│   ├── App.css
│   ├── App.js
│   ├── App.test.js
│   ├── index.css
│   ├── index.js
│   ├── logo.svg
│   ├── serviceWorker.js
│   └── setupTests.js
└── yarn.lock

到目前为止还算简单。然而,在这个清单中,我故意排除了node_modules目录。这个目录有 18 个文件。尝试在我们项目的根目录运行这个命令,不排除那个目录:tree。享受观看繁忙的行数——32,418 个文件!这些都是从哪里来的?是我们的朋友npm install

package.json

我们的项目结构在一定程度上由我们的package.json文件控制以管理依赖项。大多数捆绑工具,比如 webpack,将利用这个文件中的信息来创建我们的依赖图和一小块一小块的模块。让我们来看看它:

package.json

{
 "name": "sample-project",
 "version": "0.1.0",
 "private": true,
 "dependencies": {
   "@testing-library/jest-dom": "⁴.2.4",
   "@testing-library/react": "⁹.3.2",
   "@testing-library/user-event": "⁷.1.2",
   "react": "¹⁶.13.1",
   "react-dom": "¹⁶.13.1",
   "react-scripts": "3.4.1"
 },
 "scripts": {
   "start": "react-scripts start",
   "build": "react-scripts build",
   "test": "react-scripts test",
   "eject": "react-scripts eject"
 },
 "eslintConfig": {
   "extends": "react-app"
 },
 "browserslist": {
   "production": [
     ">0.2%",
     "not dead",
     "not op_mini all"
   ],
   "development": [
     "last 1 chrome version",
     "last 1 firefox version",
     "last 1 safari version"
   ]
 }
}

这是一个标准的基本包文件;它只包含六个依赖项:一半用于测试,一半用于 React。现在,有趣的部分是:每个依赖项又有自己的依赖项,这就是为什么我们在node_modules目录中单独有 32,400 个文件。通过使用模块,我们不必手动构建或管理依赖项;我们可以遵循 DRY 原则,并利用其他人(或我们自己)以模块形式编写的现有代码。正如我们在比较 Python 和 Node.js 时讨论的那样,npm install类似于 Python 中的pip install,我们在 Python 程序中使用import关键字来使用包,而在 Node.js 中我们使用require

当我们使用npm install将一个新的包安装到我们的项目中时,它会在package.json中添加一个条目。这是一个文件,如果你进行任何编辑,你需要非常小心。一般来说,你不应该需要做太多更改,尤其是应该避免对依赖项进行实质性的更改。利用install命令来完成这些。

构建流水线

让我们看看当我们准备将 React 项目部署时会发生什么。运行npm run build并观察输出。你应该会看到类似以下的输出:

Creating an optimized production build...
Compiled successfully.

File sizes after gzip:

  39.39 KB  build/static/js/2.deae54a5.chunk.js
  776 B     build/static/js/runtime-main.70500df8.js
  650 B     build/static/js/main.0fefaef6.chunk.js
  547 B     build/static/css/main.5f361e03.chunk.css

The project was built assuming it is hosted at /.
You can control this with the homepage field in your package.json.

The build folder is ready to be deployed.
You may serve it with a static server:

  yarn global add serve
  serve -s build

Find out more about deployment here:

  bit.ly/CRA-deploy

如果你查看构建目录,你会看到精简的 JavaScript 文件,打包好以便高效部署。有趣的部分在于:create-react-app 使用 webpack 进行构建create-react-app设置处理了这些部分。修改create-react-app的内部 webpack 设置有点棘手,所以现在让我们来看看如何在 React 的用例之外直接使用 webpack。

使用 webpack

现在,webpack 是许多模块化工具之一,可以在你的程序中使用。此外,与 React 脚本不同,它在 React 之外也有用途:它可以用作许多不同类型应用的打包工具。让我们动手创建一个小的、无用的示例项目:

  1. 创建一个新的目录并进入其中:mkdir webpack-example ; cd webpack-example

  2. 我们将使用 NPM,所以我们需要初始化它。我们也会接受默认值:npm init -y

  3. 然后我们需要安装 webpack:npm install webpack webpack-cli --save-dev

请注意,我们在这里使用--save-dev,因为我们不需要将 webpack 构建到我们的生产级文件中。通过使用开发依赖,我们可以帮助减少我们的捆绑大小,这是一个可能会拖慢应用程序的因素。

如果你在这里的node_modules目录中查看,你会看到我们已经从依赖中安装了超过 3.5 千个文件。我们的项目目前相当无聊:没有任何内容!让我们修复这个问题,创建一些文件,如下所示:

src/index.html

<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <title>Webpack Example</title>
</head>
<body>
 <h1>Welcome to Webpack!</h1>
 <script src="index.js"></script>
</body>
</html>

src/index.js

console.log('hello')

到目前为止,非常令人兴奋和有用,对吧?如果你在浏览器中打开我们的首页,你会看到控制台中的预期内容。现在,让我们将 webpack 引入其中:

  1. package.jsonscripts节点更改为以下内容:
"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "webpack --mode development",
    "build": "webpack --mode production"
  },
  1. 运行npm run dev。你应该会看到类似这样的输出:
> webpack --mode development

Hash: 21e0ae2cc4ae17d2754f
Version: webpack 4.43.0
Time: 53ms
Built at: 06/14/2020 1:37:27 PM
  Asset      Size  Chunks             Chunk Names
main.js  3.79 KiB    main  [emitted]  main
Entrypoint main = main.js
[./src/index.js] 20 bytes {main} [built]

现在去查看你新创建的dist目录:

dist
└── main.js

如果你打开main.js,你会发现它看起来与我们的index.js非常不同!这是 webpack 在幕后做一些模块化的工作。

等等。我们从一行代码变成了 100 行。为什么这样做更好呢?对于这样简单的例子来说可能并不是,但请再给我一点时间。让我们尝试npm run build并比较输出:main.js现在是一行,被精简了。

查看我们的package.json文件,除了我们操作的脚本节点之外,还有一些值得注意的部分:

{
 "name": "webpack-example",
 "version": "1.0.0",
 "description": "",
 "main": "index.js",
 "scripts": {
   "test": "echo \"Error: no test specified\" && exit 1",
   "dev": "webpack --mode development",
   "build": "webpack --mode production"
 },
 "keywords": [],
 "author": "",
 "license": "ISC",
 "devDependencies": {
   "webpack": "⁴.43.0",
   "webpack-cli": "³.3.11"
 }
}

我们看到一个"main"节点指定了一个index.js作为我们的主入口点,或者说 webpack 开始查找其依赖的地方。

在使用 webpack 时,有三个重要的概念需要理解:

  • 入口:webpack 开始工作的地方。

  • 输出:webpack 将输出其完成的产品的地方。如果我们查看前面测试的输出,我们会看到main.js 3.79 KiB main [emitted] main。webpack 更加优雅地将其定义为“emitting”其捆绑包,而不是“spits out”这个短语。

  • 加载器:如前所述,webpack 可以用于各种不同的目的;然而,默认情况下,webpack 只处理 JavaScript 和 JSON 文件。因此,我们使用加载器来做更多的工作。我们将在一分钟内使用一个加载器来操作index.html文件。

模式和插件的概念也很重要,尽管有点更容易理解:模式,正如我们在package.json中添加脚本时所看到的,定义了我们是否希望对我们的环境进行开发、生产或“无”优化。模式比这更复杂,但现在我们不会变得疯狂——webpack 相当复杂,因此对其有一个表面理解是一个很好的开始。插件基本上做着加载器无法做的事情。尽管我们会保持简单,现在我们将添加一个能够理解 HTML 文件的加载器。准备好……输出并不是你所想象的那样:

  1. 运行npm install html-loader --save-dev

  2. 现在我们已经到了需要一个配置文件的地步,所以创建webpack.config.js

  3. webpack.config.js中输入以下内容:

module.exports = {
  module: {
    rules: [
      {
        test: /\.html$/i,
        loader: 'html-loader',
      },
    ],
  },
};
  1. 修改index.js如下:
import html from './index.html'

console.log(html)
  1. 通过修改index.html,将脚本标签更改为以下内容:

  2. 重新运行npm run dev,然后在浏览器中打开该页面。

如果我们查看控制台,我们会看到我们的 HTML!哇!几乎所有的东西都在那里,除了我们的<script>标签在src中显示为"[Object object]"。现在你应该问自己:“我们刚刚完成了什么?”。

事实证明,加载器不是我们想要的!当你想要插件时,却使用加载器,反之亦然,这是一个常见的错误。现在让我们撤销我们所做的,并安装一个做我们期望的 HTML 插件:将index.html插入dist目录,并优化main.js文件:

  1. 实际上,我们并不想要或需要 HTML 加载器来完成这个任务:npm uninstall html-loader

  2. 安装正确的插件:npm install html-webpack-plugin --save-dev

  3. 完全用这个配置替换webpack.config.js的内容:

var HtmlWebpackPlugin = require('html-webpack-plugin');
var path = require('path');

module.exports = {
 entry: './src/index.js',
 output: {
   path: path.resolve(__dirname, './dist'),
   filename: 'index_bundle.js'
 },
 plugins: [new HtmlWebpackPlugin({
   template: './src/index.html'
 })]
};
  1. index.js修改回原始的一行:console.log('hello')

  2. src/index.html中删除<script>标签。它将为我们构建。

  3. 执行npm run dev

  4. 最后,在浏览器中打开dist/index.html

这应该更符合你的喜好,也是你使用 webpack 所期望的。然而,这只是一个非常基本的例子,所以让我们看看是否可以做一些更花哨的事情。编辑文件如下:

src/index.html

<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <title>Webpack Example</title>
</head>
<body>
 <h1>Welcome to Webpack!</h1>
<div id="container"></div>
</body>
</html>

src/index.js

import Highcharts from 'highcharts'

// Create the chart
Highcharts.chart('container', {
 chart: {
   type: 'bar'
 },
 title: {
   text: 'Fruit Consumption'
 },
 xAxis: {
   categories: ['Apples', 'Bananas', 'Oranges']
 },
 yAxis: {
   title: {
     text: 'Fruit eaten'
   }
 },
 series: [{
   name: 'Jane',
   data: [1, 0, 4]
 }, {
   name: 'John',
   data: [5, 7, 3]
 }]
});

在这个例子中,我们使用了 Highcharts,一个图表库。这是他们的样板例子,直接从他们的网站上取出;我没有对它做任何花哨的事情,除了修改第一行为import Highcharts from 'highcharts'。这意味着我们将使用一个模块,所以让我们安装它——npm install highcharts

  1. 将此脚本添加到你的package.jsonscripts节点中:"watch": "webpack --watch -- mode development"

  2. 运行npm run watch

  3. 在浏览器中加载dist/index.html

图 16.2 - 使用 Highcharts 的 Webpack

更有趣,不是吗?还有,花点时间看看index_bundle.js文件,并注意更大的文件和缩小的代码。如果你用watch编辑src中的文件,webpack 会即时为你重新打包文件。如果你使用支持热重载的实时服务器,比如 Visual Studio Code,它也会为你刷新页面——对于快速开发很方便!

现在是时候尝试我们一直在构建的东西了。让我们尝试为部署构建我们的项目。

部署我们的项目

到目前为止,我们已经做了很多开发工作,现在是时候尝试对我们的项目进行生产构建了。运行npm run build,嗯,它并不是那么开心,是吧?你应该会收到一些像这样的警告:

WARNING in asset size limit: The following asset(s) exceed the recommended size limit (244 KiB).
This can impact web performance.
Assets: 
  index_bundle.js (263 KiB)

WARNING in entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit (244 KiB). This can impact web performance.
Entrypoints:
  main (263 KiB)
      index_bundle.js

WARNING in webpack performance recommendations: 
You can limit the size of your bundles by using import() or require.ensure to lazy load some parts of your application.
For more info visit https://webpack.js.org/guides/code-splitting/
Child HtmlWebpackCompiler:
     1 asset
    Entrypoint HtmlWebpackPlugin_0 = __child-HtmlWebpackPlugin_0
    [0] ./node_modules/html-webpack-plugin/lib/loader.js!./src/index.html 522 bytes {0} [built]

那么,这是在告诉我们什么?还记得我说过捆绑大小会影响性能吗?让我们尝试优化一下,这样我们就不会再收到这些消息了。我们将研究一些开发技术来做到这一点。

简而言之,块是将大文件拆分成较小块的方法。我们可以通过在我们的webpack.config.js文件的插件节点之后添加这个部分来轻松完成这一部分:

optimization: {
   splitChunks: {
     chunks: 'all',
   }
 }

现在,继续构建;它会稍微开心一点:

Built at: 06/14/2020 3:46:38 PM
                 Asset       Size  Chunks                    Chunk Names
            index.html  321 bytes          [emitted]         
        main.bundle.js   1.74 KiB       0  [emitted]         main
vendors~main.bundle.js    262 KiB       1  [emitted]  [big]  vendors~main
Entrypoint main [big] = vendors~main.bundle.js main.bundle.js

不幸的是,它仍然会抱怨。我们将 1.74 KB 削减到一个单独的文件中,但我们仍然有一个 262 KB 的vendors捆绑包。如果你在dist中查看,现在你会看到两个js文件以及 HTML 中的两个<script>标签。

它之所以不进一步拆分是因为供应商(Highcharts)捆绑包已经相当自包含,所以我们需要探索其他方法来实现我们的需求。然而,如果我们有很多自己的代码,可能会进一步将其拆分为多个块。

那么,我们的下一个选择是什么?我们调整优化!

试试这个:

optimization: {
   splitChunks: {
     chunks: 'async',
     minSize: 30000,
     maxSize: 244000,
     minChunks: 2,
     maxAsyncRequests: 6,
     maxInitialRequests: 4,
     automaticNameDelimiter: '~',
     cacheGroups: {
       defaultVendors: {
         test: /[\\/]node_modules[\\/]/,
         priority: -10
       },
       default: {
         minChunks: 2,
         priority: -20,
         reuseExistingChunk: true
       }
     }
   }
 }

如果你注意到,这里的选项更加明确,包括块的最大大小,重用现有的供应商块,以及最小数量的块。让我们试试看。

没有变化,对吧?

让我们尝试一些不同的东西:修改index.js以使用 promises 和webpack 提示来将 Highcharts 依赖项拆分为自己的捆绑包:

import( /* webpackChunkName: "highcharts" */ 'highcharts').then(({ default: Highcharts }) => {
 // Create the chart
 Highcharts.chart('container', {
   chart: {
     type: 'bar'
   },
   title: {
     text: 'Fruit Consumption'
   },
   xAxis: {
     categories: ['Apples', 'Bananas', 'Oranges']
   },
   yAxis: {
     title: {
       text: 'Fruit eaten'
     }
   },
   series: [{
     name: 'Jane',
     data: [1, 0, 4]
   }, {
     name: 'John',
     data: [5, 7, 3]
   }
   ]
 });
})

我们从npm run build的输出现在应该更像这样:

Version: webpack 4.43.0
Time: 610ms
Built at: 06/14/2020 4:38:41 PM
                        Asset       Size  Chunks                    Chunk Names
highcharts~c19dcf7a.bundle.js    262 KiB       0  [emitted]  [big]  highcharts~c19dcf7a
                   index.html  284 bytes          [emitted]         
      main~d1c01171.bundle.js   2.33 KiB       1  [emitted]         main~d1c01171
Entrypoint main = main~d1c01171.bundle.js

嗯...这仍然没有达到我们想要的效果!虽然我们为 Highcharts 有一个单独的块,但它仍然是一个庞大的、单一的文件。那么,我们该怎么办?

投降

举起白旗。承认失败。

几乎。

在这里,每个供应商包可能不同,每个导入都将是独特的;我们想做的是尝试找到适合我们需求的最小块的供应商库。在这种情况下,导入所有 Highcharts 正在创建一个庞大的文件。然而,让我们看看node_modules/highcharts。在es-modules目录中,有一个有趣的文件:highcharts.src.js。这是我们想要的更模块化的文件,所以让我们尝试导入它而不是一次导入整个库:

import( /* webpackChunkName: "highcharts" */ 'highcharts/es-modules/highcharts.src.js').then(({ default: Highcharts }) => {

...

现在看看如果我们使用npm run build会发生什么:

Version: webpack 4.43.0
Time: 411ms
Built at: 06/14/2020 4:48:43 PM
                        Asset       Size  Chunks             Chunk Names
highcharts~47c7b5d6.bundle.js    170 KiB       0  [emitted]  highcharts~47c7b5d6
                   index.html  284 bytes          [emitted]  
      main~d1c01171.bundle.js   2.33 KiB       1  [emitted]  main~d1c01171
Entrypoint main = main~d1c01171.bundle.js

啊哈!好多了!所以,在这种情况下,答案是晦涩的。Highcharts 捆绑可以被解开,以便只添加代码的特定部分。这在所有情况下都不会起作用,特别是在源代码未包含的情况下;然而,这是我们目前的一种方法:将包含的包裁减到最小需要的集合。还记得我们在 React 中有选择地包含库的部分吗?这里的想法也是一样的。

部署,完成

现在我们该怎么办?你真正需要做的就是将你的dist目录的内容放在一个 Web 服务器上供全世界查看!让你的辛勤工作展现出来。

总结

Webpack 是我们的朋友。它模块化,最小化,分块,并使我们的代码更有效,同时在某些部分没有得到适当优化时警告我们。有方法可以消除这些警报,但总的来说,倾听它们并至少尝试解决它们是一个好主意。

然而,一个仍然没有答案的燃烧问题是:增加下载文件的数量会增加加载时间吗?这是一个常见的误解,它始于互联网早期:更多的文件==更长的加载时间。然而,事实是,多个浏览器可以同时打开许多非阻塞流,从而比一个巨大的文件实现更高效的下载。这是所有多个文件的解决方案吗?不是:例如,CSS 图像精灵仍然是更有效地使用图像资源。为了性能,我们必须在提供最佳用户体验的同时,与最佳开发者体验相结合。整本书都是关于这个主题的,所以我不会试图给你所有的答案。我只会留下这个:

优化,优化,优化。

在下一章中,我们将处理编程的所有部分都非常重要的一个主题:安全性。

第四部分 - 与数据库通信

我们 JavaScript 全栈体验的最后部分是数据库层。我们将使用 NoSQL 数据存储,因为它们使用类似 JSON 的文档。

在本节中,我们将涵盖以下章节:

  • 第十七章,安全和密钥

  • 第十八章,Node.js 和 MongoDB

  • 第十九章,将所有内容整合在一起

第十七章:安全和密钥

安全性并不是一件简单的事情。在设计应用程序时,从一开始就牢记安全性是很重要的。例如,如果您意外地将您的密钥提交到存储库中,您将不得不进行一些技巧,要么从存储库的历史记录中删除它,要么更有可能的是,您将不得不撤销这些凭据并生成新的凭据。

我们不能让我们的数据库凭据在前端 JavaScript 中对世界可见,但是前端可以与数据库进行交互的方法。第一步是实施适当的安全措施,并了解我们可以将凭据放在哪里,无论是前端还是后端。

本章将涵盖以下主题:

  • 身份验证与授权

  • 使用 Firebase

  • .gitignore和凭据的环境变量

技术要求

准备好使用存储库的Chapter-17目录中提供的代码:github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-17。由于我们将使用命令行工具,还需要准备您的终端或命令行 shell。我们需要一个现代浏览器和一个本地代码编辑器。

身份验证与授权

在我们开始探讨 JavaScript 安全性时,了解身份验证授权之间的重要区别至关重要。简而言之,身份验证是一个系统确认和承认您是您所说的人的过程。想象一下去商店买一瓶葡萄酒。您可能会被要求提供证明您达到或超过当地法定饮酒年龄的身份证明。店员通过您的身份证对您进行了身份验证,以证明是的,您就是,因为我,店员,已经将您的面孔与身份证中的照片相匹配。第二种情况是当您乘坐航空公司的飞机时。当您通过安检时,他们也会出于同样的原因检查您的身份证:您是否是您所说的人?

然而,这两种用例最终都与授权有关。授权表示:我知道你就是你所说的那个人。现在,你是否被允许做你想做的事情?在我们的葡萄酒例子中,如果你在美国年满 21 岁,或者在世界上大多数其他地方年满 18 岁,你就被授权消费酒精饮料。现在,机场的安全人员并不真正关心你的年龄有任何真正的原因;他们只关心你是否是你所说的那个人,以及你是否有一张有效的登机牌。然后你就被授权进入机场的安全区并登机。

让我们进一步延伸我们的航空公司例子。在当今旅行安全加强的时代,身份验证和授权过程既不是开始也不是结束于安全人员。如果您在线预订商业航班机票,该过程看起来更像是这样:

图 17.1 - 航空公司网站的身份验证和授权

在使用航空公司的网站时,您可能拥有一个帐户并被授权继续登录,或者您可能已经登录并被授权搜索航班。如果您已经登出,您必须验证才能搜索航班。要预订航班,您可能需要一些特定的细节,比如签证,以便被授权预订该航班。您可能也被列入旅行到某个国家的观察名单或黑名单,因此您的旅程可能会在开始之前就结束。有很多步骤,但其中许多是在幕后发生的;例如,当您输入您的姓名预订机票时,您可能不知道您的姓名已被搜索对全球记录,以查看您是否被授权飞行。您的签证号码可能已被交叉引用,以查看您是否被授权飞往该国家。

就像你需要经过身份验证和授权才能飞行一样,你的网络应用程序也应该被设计成允许身份验证和授权。考虑一下我们在第十五章中的餐厅查找应用,将 Node.js 与前端结合使用,它允许我们在 Firebase 中搜索并保存不同的餐厅:

图 17.2 - 我们的餐厅应用

如果你还记得,我们在实时数据库部分以开放权限启动了我们的 Firebase 应用:

图 17.3 - 我们的 Firebase 安全规则

显然,这对于生产网站来说不是一个好主意。因此,为了缓解这个问题,让我们返回 Firebase 并设置一些身份验证和授权!

使用 Firebase

为了方便起见,我在 GitHub 存储库的Chapter-17目录中复制了我们的餐厅查找应用。不要忘记在第十五章中的餐厅查找应用中的.env文件中包含你自己的环境变量。在我们继续之前,花点时间来设置和运行它。

我们需要做的下一件事是去 Firebase 配置它以使用身份验证。在 Firebase 控制台中,访问身份验证部分并设置一个登录方法;例如,你可以设置 Google 身份验证。这里有一系列你可以使用的方法,所以继续添加一个或多个。

接下来,我们将在实时数据库部分设置我们的规则,如下所示:

{
  "rules": {
    "restaurants": {
      "$uid": {
        ".write": "auth != null && auth.uid == $uid",
        ".read": "auth != null && auth.uid == $uid"
      }
    }
  }
}

我们在这里说的是,如果经过身份验证的数据不是null,并且用户 ID 与你尝试写入和读取的数据库位置的用户 ID 匹配,那么用户就被允许从你的数据库的restaurants/<user id>部分读取和写入。

现在我们的规则已经设置好了,让我们尝试保存一个餐厅:

  1. 通过在根目录执行npm start来启动应用,并访问http://localhost:3000

  2. 搜索餐厅。

  3. 尝试保存这个餐厅。

  4. 见证一个史诗般的失败。

你应该看到一个类似以下的错误页面:

图 17.4 - 错误,错误!

另外,如果我们进入开发者工具并检查网络选项卡的 WS 选项卡(WS代表WebSockets,这是 Firebase 通信的方式),我们可能会看到类似以下的内容:

图 17.5 - WebSockets 通信检查器

太棒了!我们现在证明了我们的 Firebase 规则起作用,并且不允许保存到/restaurants/<user_id>,因为我们没有经过身份验证。是时候设置这个了。

我们要做的第一件事是稍微改变我们的App.js脚本。在编写 React 时有一些不同的约定,我们将继续使用基于类的方法。我们的App.js脚本将如下所示:

import React from 'react'
import cookie from "react-cookies"

import Finder from './components/finder/Finder'
import SignIn from './components/signIn/SignIn'

import './App.css'

export default class App extends React.Component {
 constructor() {
   super()

   this.state = {
     user: cookie.load("username")
   }

   this.setUser = this.setUser.bind(this)
 }

 setUser(user) {
   this.setState({
     user: user
   })

   cookie.save("username", user)
 }

 render() {
   const { user } = this.state
   return (
     <div className="App">
       { (user) ? <Finder user={user} /> : <SignIn setUser={this.setUser}
     /> }
     </div>
   )
 }
}

首先要注意的是,我们包含了一个新的npm模块:react-cookies。虽然从浏览器中读取 cookie 很容易,但有一些模块可以让它变得更容易一点。当我们检索用户的 ID 时,我们将把它存储在一个 cookie 中,这样浏览器就记住了用户已经经过身份验证。

为什么我们需要使用 cookie?如果你还记得,网络本质上是无状态的,所以 cookie 是一种从应用程序的一个部分传递信息到另一个部分,从一个会话到另一个会话的手段。这是一个基本的例子,但重要的是要记住不要在 cookie 中存储任何敏感信息;在身份验证工作流程中,令牌或用户名可能是你想要放入其中的最多的信息。

我们还引入了一个新组件SignIn,如果用户变量不存在,也就是说,如果用户没有登录,它会有条件地渲染。让我们来看看这个组件:

import React from 'react'
import { Button } from 'react-bootstrap'
import * as firebase from 'firebase'

const provider = new firebase.auth.GoogleAuthProvider()

export default class SignIn extends React.Component {
 constructor() {
   super()

   this.login = this.login.bind(this)
 }

 login() {
   const self = this

   firebase.auth().signInWithPopup(provider).then(function (result) {
     // This gives you a Google Access Token. You can use it to access the
     // Google API.
     var token = result.credential.accessToken;
     // The signed-in user info.
     self.props.setUser(result.user);
     // ...
   }).catch(function (error) {
     // Handle Errors here.
     var errorCode = error.code;
     var errorMessage = error.message;
     // The email of the user's account used.
     var email = error.email;
     // The firebase.auth.AuthCredential type that was used.
     var credential = error.credential;
     // ...
   });
 }
 render() {
   return <Button onClick={this.login}>Sign In</Button>
 }
}

这里有两件事需要注意:

  • 我们正在使用GoogleAuthProvider来进行我们的SignIn机制。如果你在设置 Firebase 时选择了不同的认证方法,这个提供者可能会有所不同,但代码的其余部分应该是相同或相似的。

  • signInWithPopup方法几乎直接从 Firebase 文档中复制过来。这里唯一的改变是创建self变量,这样我们就可以在另一个方法中保持对this的作用域。

当这个被渲染时,如果用户还没有登录,它将是一个简单的按钮,上面写着登录。它将激活一个弹出窗口,用你的 Google 账号登录,然后像以前一样继续。不是很可怕,对吧?

接下来,我们需要处理我们的用户。你是否注意到在App.js中,我们将user传递给了 Finder?这将使在我们的基本应用程序中轻松地传递一个对我们用户的引用,就像在Finder.jsx中一样:

getRestaurants() {
   const { user } = this.props

   Database.ref(`/restaurants/${user.uid}`).on('value', (snapshot) => {
     const restaurants = []

     const data = snapshot.val()

     for(let restaurant in data) {
       restaurants.push(data[restaurant])
     }
     this.setState({
       restaurants: restaurants
     })
   })
 }

这是在这种情况下唯一改变的方法,如果你仔细看,改变是从this.props中解构user并在我们的数据库引用中使用它。如果你记得我们的安全规则,我们不得不稍微改变我们的数据库结构,以适应我们认证用户的简单授权

{
  "rules": {
    "restaurants": {
      "$uid": {
        ".write": "auth != null && auth.uid == $uid",
        ".read": "auth != null && auth.uid == $uid"
      }
    }
  }
}

我们在安全规则中所说的是,格式为restaurants.$uid的节点是我们将存储每个单独用户的餐厅的地方。我们的 Firebase 结构现在看起来像这样:

图 17.6 - 我们的 Firebase 结构可能是这样的一个例子

在这个结构中,我们看到restaurants内部的TT8PYnjX6FP1YikssoHnINIpukZ2节点。那是认证用户的uid用户 ID),在那个节点内,我们找到用户保存的餐厅。

这个数据库结构很简单,但提供了简单的授权。我们的规则规定“给用户 TT8 权限查看和修改他们自己节点内的数据,仅此而已。”

我们之前已经讨论了我们的.env变量,所以让我们更深入地看一下它们。我们将把我们的应用部署到 Heroku,创建一个公开可见的网站。

.gitignore 和凭据的环境变量

由于我们一直在使用.env文件,我特意指出这些文件绝对不应该提交到仓库中。事实上,一个好的做法是在创建任何敏感文件之前向你的.gitignore文件添加一个条目,以确保你永远不会意外提交你的凭据。即使你后来从仓库中删除它,文件历史仍然保留,你将不得不使这些密钥失效(或循环使用),以便它们不会在历史中暴露出来。

虽然 Git 的完整部分超出了我们在这里的工作范围,但让我们看一个.gitignore文件的例子:

# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# production
/build

# misc
.DS_Store
.env*

npm-debug.log*
yarn-debug.log*
yarn-error.log*

其中有几个是由create-react-app脚手架创建的条目。特别注意.env*。星号(或星号,或通配符)是一个正则表达式通配符,指定任何以.env开头的文件都被忽略。你可以有.env.prod,它也会被忽略。一定要忽略你的凭据文件!

我还喜欢将/node_modules改为*node_modules*,以防你有自己的子目录和它们自己的 node 模块。

.env文件中存储变量很方便,但也可以创建内存中的环境变量。为了演示这个功能,我们将把项目部署到 Heroku,一个云应用平台。让我们开始吧:

  1. heroku.com创建一个新账户。

  2. 根据提供的文档安装 Heroku 命令行界面CLI)。一定要遵循登录说明。

  3. 在餐厅查找器目录中初始化一个新的仓库:git init

  4. 执行heroku create --ssh-git。它会提供你的 Heroku 端点的 Git URL,以及https:// URL。继续访问 HTTPS URL。你应该会看到一个欢迎消息:

图 17.7 - 哦耶!我们有一个空白的 Heroku 应用程序!

我们现在可以继续组织我们应用的逻辑。

重新组织我们的应用

接下来,我们要做的与第十五章中的将 Node.js 与前端结合不同的事情,就是稍微重新组织我们的文件。这并不是完全必要的,但在部署生产级别的代码时,它提供了前端和后端之间的一个很好的逻辑区分。我们之前的应用和我们要在这里创建的应用之间还有一个语义上的区别:我们不会提供一个正在运行的开发 React 应用,而是一个静态的生产版本。

如果你还记得,我们之前的餐厅结构是这样的:

图 17.8 – 代理与应用的区别,解释。

我们之前实际上是使用 React 应用作为 Web 服务器,并通过它代理到 Express 后端,以便使用 Yelp API。然而,现在我们将使用 Express 作为主要的 Web 服务器,并提供一个 React 应用的生产级别构建。

我们之前的应用逻辑如下:

IF NOT a React page,
 Serve from proxy
ELSE
 Serve React

我们要颠倒这个逻辑,并声明以下内容:

IF NOT an Express route,
 Serve from static build
ELSE
 Serve API

下面是要做的事情:

  1. 创建一个新的client目录。

  2. 如果你还有yarn.lock文件,请删除它。我们将专注于使用 NPM 而不是yarn

  3. 将所有文件移动到 client 目录中,除了 API 目录。

  4. 接下来,我们要在根目录下创建一个新的package.jsonnpm install dotenv express yelp-fusion

如果你注意到了,我们还安装了 Express,这是之前没有做的。我们将使用它来更轻松地路由我们的请求。

在我们的package.json中,在级别,添加这些脚本:

"postinstall": "cd client && npm install && npm run build",
"start": "node api/api.js"

由于我们正在处理 Heroku,我们还可以从package.json中删除proxy行,因为一切都将在同一服务器上运行,不需要代理。现在,我们的package.json中的postinstall行怎么样?我们要做的是创建我们应用的生产就绪版本。create-react-app通过npm run build脚本免费为我们提供了这个功能。当我们部署到 Heroku 时,它将运行npm install,然后运行postinstall,以创建我们的 React 应用的生产版本。

现在我们准备向我们的项目添加一个新的元数据,以便 Heroku 可以提供我们的应用:Procfile

Procfile 会告诉 Heroku 如何处理我们的代码。你的 Procfile 会是这样的:

web: npm start

实质上,它所做的就是告诉 Heroku 从哪里开始运行程序:运行npm start

我们的目录结构现在应该是这样的:

.
├── Procfile
├── api
│   └── api.js
├── client
│   ├── README.md
│   ├── package-lock.json
│   ├── package.json
│   ├── public
│   └── src
├── package-lock.json
└── package.json

我们接下来的重要步骤是修改我们的api.js文件,如下所示:

const yelp = require('yelp-fusion');
const express = require('express');
const path = require('path');

const app = express();

require('dotenv').config();

const PORT = process.env.PORT || 3000;

const client = yelp.client(process.env.YELP_API_Key);

到目前为止,这看起来与之前相似,只是增加了 Express。但是看看接下来的一行:

app.use(express.static(path.join(__dirname, '../client/build')));

啊哈!这是我们的秘密酱:这行表示使用client/build目录作为静态资源,而不是 Node.js 代码。

继续,我们正在定义我们的 Express 路由来处理格式为/search的请求:

app.get('/search', (req, res) => {
 const { lat, lng, value } = req.query

 client.search({
   term: value,
   latitude: lat,
   longitude: lng,
   categories: 'Restaurants'
 }).then(response => {
   res.statusCode = 200;
   res.setHeader('Content-Type', 'application/json');
   res.setHeader('Access-Control-Allow-Origin', '*');

   res.write(response.body);
   res.end();
 })
   .catch(e => {
     console.error('error', e)
   })
});

对于我们秘密酱的下一部分,如果路由匹配/search,将其发送到静态的 React 构建:

app.get('*', (req, res) => {
 res.sendFile(path.join(__dirname + '../client/build/index.html'));
});

app.listen(PORT, () => console.log(`Server listening on port ${PORT}`));

将所有内容添加到你的 Git 仓库:git add。现在你可以执行git status来确保你的.env文件没有被包含

接下来,提交你的代码:git commit -m "Initial commit。如果你需要关于 Git 的帮助,Heroku 文档提供了参考资料。接下来,部署到 Heroku:git push heroku master。这会花一些时间,因为 Heroku 不仅会使用 Git 部署你的代码,还会创建你的代码的生产版本。

访问构建脚本提供的 URL,希望你会看到一个很棒的错误消息:

图 17.9 – 哦不!一个错误!实际上这不是坏事!

太好了!这告诉我们的是应用程序正在运行,但我们缺少一些重要的部分:我们的环境变量。对您.env文件中的每个条目执行heroku config:set <entry>(根目录和client中)。

当您刷新页面时,您将看到“登录”按钮。但是,如果您单击它,将不会发生任何事情。它可能会在一秒钟内弹出一个弹出窗口,但不会弹出身份验证窗口。我们需要返回到 Firebase 控制台,将我们的 Firebase URL 添加为已授权URL。

在 Firebase 控制台中,转到身份验证部分,并将您的 Heroku URL 输入到已授权域部分。返回到您的 Heroku 应用程序,刷新,然后瞧!身份验证面板可以正常工作。如果您转到 Saved!,甚至会看到您保存的餐馆。

这并不难!Heroku 存储环境变量的方法与我们的.env文件并没有太大的不同,但它可以在不需要太多工作的情况下为我们处理。但是,我们还需要配置最后一个部分:我们的搜索不起作用。如果您查看控制台错误消息,您应该会看到一条说明拒绝连接到localhost:3000的提示。我们需要采取最后一步来将我们的代码从使用localhost抽象出来。

src/components/search/Search.jsx中,您可能会认出这种方法:

search(event) {
   const { lng, lat, val } = this.state

   fetch(`http://localhost:3000/businesses/search?value=${val}&lat=${lat}&lng=${lng}`)
     .then(data => data.json())
     .then(data => this.handleSearchResults(data))
 }

好了!我们已经将我们的fetch调用硬编码为localhost和我们的代理路径。让我们将其更改为以下内容:

fetch(`/search?value=${val}&lat=${lat}&lng=${lng}`)

提交您的更改并再次推送到 Heroku。在开发过程中,您还可以使用heroku local web来生成一个浏览器并测试您的更改,而无需提交和部署。

幸运的话,您应该拥有一个完全功能的前后端应用程序,并且凭据已经安全存储在 Heroku 环境变量中!恭喜!

总结

在本章中,我们学习了身份验证、授权以及两者之间的区别。请记住,通常仅执行其中一个是不够的:大多数需要凭据的应用程序需要两者的组合。

Firebase 是一个有用的云存储数据库,您可以将其与现有的登录系统一起使用,不仅可以作为开发资源,还可以扩展到生产级别的使用。最后,请记住这些要点:因为 JavaScript 是客户端的,我们必须以不同的方式保护敏感信息,而不是纯粹的后端应用程序:

  1. 进行身份验证和授权以确定谁可以使用哪些资源。

  2. 将我们的敏感数据与我们的公共数据分开。

  3. 永远不要将密钥和敏感数据提交到存储库中!

我们每个人都有责任成为良好的数字公民,但也存在不良行为者。保护自己和您的代码!

在下一章中,我们将把 Node.js 和 MongoDB 联系在一起,以持久化我们的数据。我们将重新审视我们的星际飞船游戏,但这次将使用持久存储。

第十八章:Node.js 和 MongoDB

您可能已经听说过MEAN堆栈:MongoDB、Express、Angular 和 Node.js,或者MERN堆栈:MongoDB、Express、React 和 Node.js。我们尚未讨论的缺失部分是 MongoDB。让我们探讨一下这个 NoSQL 数据库如何可以直接从 Express 中使用。我们将构建我们在第十三章中开始的星际飞船游戏的下一个迭代,使用 Express,只是这次使用 MongoDB 并且加入了一些测试!

我们将在本章中涵盖以下主题:

  • 使用 MongoDB

  • 使用 Jest 进行测试

  • 存储和检索数据

  • 将 API 连接在一起

技术要求

准备好使用存储库的chapter-18目录中提供的代码:github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-18。由于我们将使用命令行工具,还要确保您的终端或命令行 shell 可用。我们需要一个现代浏览器和一个本地代码编辑器。

使用 MongoDB

MongoDB 的基本前提是,它与其他类型的结构化键/值对数据库不同的地方在于它是无模式的:您可以插入无结构数据的任意文档,而不必担心数据库中的另一个条目是什么样子。在 NoSQL 术语中,文档对我们来说已经很熟悉了:一个 JavaScript 对象!

这是一个文件:

{
 "first_name": "Sonyl",
 "last_name": "Nagale",
 "role": "author",
 "mood": "accomplished"
}

我们可以看到它是一个基本的 JavaScript 对象;更具体地说,它是 JSON,这意味着它也可以支持嵌套数据。这是一个例子:

{
 "first_name": "Sonyl",
 "last_name": "Nagale",
 "role": "author",
 "mood": "accomplished",
 "tasks": {
  "write": {
   "status": "incomplete"
  },
  "cook": {
   "meal": "carne asada"
  },
  "read": {
   "book": "Le Petit Prince"
  },
  "sleep": {
   "time": "8"
  }
 },
 "favorite_foods": {
  "mexican": ["enchiladas", "burritos", "quesadillas"],
  "indian": ["saag paneer", "murgh makhani", "kulfi"]
 }
}

那么这与 MySQL 有什么不同呢?考虑一下这个 MySQL 模式:

图 18.1 - 一个 MySQL 数据库表结构的示例

如果您熟悉 SQL 数据库,您会知道数据库表中的每个字段类型必须是特定类型的。在从 SQL 类型数据库检索时,我们使用结构化查询语言SQL)。正如我们的表结构化一样,我们的查询也是结构化的。

在使用数据库表之前,我们需要创建数据库表,在 SQL 中,建议不要在创建后更改其结构,而不进行一些额外的清理工作。以下是我们将创建我们之前的表的方法:

CREATE TABLE `admins` (
 `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
 `admin_role_id` int(11) DEFAULT NULL,
 `first_name` varchar(50) COLLATE utf8_unicode_ci DEFAULT NULL,
 `last_name` varchar(50) COLLATE utf8_unicode_ci DEFAULT NULL,
 `username` varchar(50) COLLATE utf8_unicode_ci DEFAULT NULL,
 `email` varchar(100) COLLATE utf8_unicode_ci DEFAULT NULL,
 `phone` varchar(100) COLLATE utf8_unicode_ci DEFAULT NULL,
 `password` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
 `avatar` varchar(100) COLLATE utf8_unicode_ci DEFAULT NULL,
 `admin_role` enum('admin','sub_admin') COLLATE utf8_unicode_ci DEFAULT
  NULL,
 `status` enum('active','inactive','deleted') COLLATE utf8_unicode_ci 
  DEFAULT NULL,
 `last_login` datetime DEFAULT NULL,
 `secret_key` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
 `last_login_ip` varchar(50) COLLATE utf8_unicode_ci DEFAULT NULL,
 `sidebar_status` enum('open','close') COLLATE utf8_unicode_ci DEFAULT
  'open',
 `created` datetime DEFAULT NULL,
 `modified` datetime DEFAULT NULL,
 PRIMARY KEY (`id`),
 KEY `email` (`email`),
 KEY `password` (`password`),
 KEY `admin_role` (`admin_role`),
 KEY `status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

现在,对于 MongoDB,我们不会构建具有预定义数据类型和长度的表。相反,我们将将 JSON 块插入到我们的数据库中作为文档。MongoDB 的理念与我们在第十七章中使用 Firebase 时非常相似,安全和密钥,插入 JSON 并对其进行查询,即使有多个嵌套的 JSON 对象,而不是存储、交叉连接和查询多个表。

假设我们有以下两个文档:

{
  "first_name": "Sonyl",
  "last_name": "Nagale",
  "admin_role": "admin",
  "status": "active"
},
{
  "first_name": "Jean-Luc",
  "last_name": "Picard",
  "admin_role": "admin",
  "status": "inactive"
}

我们如何将它们插入到我们的数据库中?这将使用 MySQL:

INSERT INTO
    admins(first_name, last_name, admin_role, status)
  VALUES
    ('Sonyl', 'Nagale', 'admin', 'active'),
    ('Jean-Luc', 'Picard', 'admin', 'inactive')

使用 MongoDB 的答案实际上比 SQL 要容易得多,因为我们可以轻松地放置数组,而不必担心数据类型或数据排序!我们可以只是把文档塞进去,而不必担心其他任何事情,这更有可能是我们从前端接收到的方式:

db.admins.insertMany([
{
  "first_name": "Sonyl",
  "last_name": "Nagale",
  "admin_role": "admin",
  "status": "active"
},
{
  "first_name": "Jean-Luc",
  "last_name": "Picard",
  "admin_role": "admin",
  "status": "inactive"
}]
)

例如,要从前述的admins表中获取所有活动管理员,我们在 MySQL 中会写出类似于这样的内容:

SELECT
  first_name, last_name 
FROM 
  admins 
WHERE 
  admin_role = "admin" 
AND 
  status = "active"

first_namelast_name字段被预定义为VARCHAR类型(可变字符),最大长度为 50 个字符。admin_rolestatusENUM(枚举类型),具有预定义的可能值(就像站点上的下拉选择列表)。然而,这是我们如何在 MongoDB 中构造我们的查询:

db.admins.find({ status: 'active', admin_role: 'admin'}, { first_name: 1, last_name: 1})

我们在这里不会深入研究 MongoDB 的语法,因为这有点超出了本书的范围,我们只会使用简单的查询。话虽如此,在我们开始之前,我们应该了解更多。

以下是我们在制作游戏时将使用的 mongo 命令列表:

  • find

  • 查找一个

  • insertOne

  • updateOne

  • updateMany

相当容易管理,对吧?我们可以将许多 MongoDB 命令分解为以下一般的句法结构:

<dbHandle>.<collectionName>.<method>(query, projection)

在这里,queryprojection 是指导我们使用 MongoDB 的对象。例如,在我们前面的语句中,{ status: 'active', admin_role: 'admin' } 是我们的查询,指定我们希望这些字段等于这些值。这个例子中的 projection 指定了我们想要返回的内容。

让我们深入我们的项目。

入门

我们可以做的第一件事是从 MongoDBdb.com 下载 MongoDB Community Server。当你安装好后,从我们的 GitHub 仓库中导航到 chapter-18/starships 目录,让我们尝试启动它:

npm install
mkdir -p data/MongoDB
mongod --dbpath data/MongoDB

如果一切安装正确,你应该会看到一大堆通知消息,最后一条消息类似于 [initandlisten] waiting for connections on port 27017。如果一切不如预期,花些时间确保你的安装工作正常。一个有用的工具是 MongoDB Compass,一个连接到 MongoDB 的 GUI 工具。确保检查权限,并且适当的端口是打开的,因为我们将使用端口 27017(MongoDB 的默认端口)进行连接。

本章将是一个实验,将我们的星际飞船游戏提升到一个新的水平。这是我们将要构建的内容:

图 18.2 – 创建我们的舰队

然后,我们将把它连接到 MongoDB,并在这个界面上实际执行游戏:

图 18.3 – 攻击敌人!

我们将使用简化版本的 MERN,使用原生 JavaScript 而不是 React,依赖于 Express 以一种比 React 更少受控的方式呈现我们的 HTML。也许 JEMN stack 是一个好的名字?

在我们开始编写实际代码之前,让我们检查项目的设置并开始测试!

使用 Jest 进行测试

starships 目录中,你会找到完成的游戏。让我们来剖析一下。

这是目录列表:

.
├── README.md
├── app.js
├── bin
│   └── www
├── controllers
│   └── ships.js
├── jest-MongoDBdb-config.js
├── jest.config.js
├── models
│   ├── MongoDB.js
│   ├── setup.js
│   └── ships.js
├── package-lock.json
├── package.json
├── public
│   ├── images
│   │   └── bg.jpg
│   ├── javascripts
│   │   ├── index.js
│   │   └── play.js
│   └── stylesheets
│       ├── micromodal.css
│       └── style.css
├── routes
│   ├── enemy.js
│   ├── index.js
│   ├── play.js
│   ├── ships.js
│   └── users.js
├── tests
│   ├── setup.model.test.js
│   ├── ships.controller.test.js
│   └── ships.model.test.js
└── views
    ├── enemy.hbs
    ├── error.hbs
    ├── index.hbs
    ├── layout.hbs
    └── play.hbs

我们将采取一种与我们其他项目有些不同的方法,在这里实现一个非常轻量级的测试驱动开发TDD)循环。TDD 是在编写能够工作的代码之前编写失败的测试的实践。虽然我们没有实现真正的 TDD,但使用测试来引导我们的思维过程是我们将要做的事情。

我们将使用 Jest 作为我们的测试框架。让我们来看一下步骤:

  1. tests 目录中,创建一个名为 test.test.js 的新文件。第一个 test 是我们测试套件的名称,以 .test.js 结尾的约定表示这是一个要执行的测试套件。在文件中,创建这个测试脚本:
describe('test', () => {
 it('should return true', () => {
   expect(1).toEqual(1)
 });
});
  1. 使用 node_modules/.bin/jest test.test.js 运行测试(确保你已经运行了 npm install!)。你将会得到类似以下的测试套件输出:
$ node_modules/.bin/jest test.test.js
 PASS  tests/test.test.js
  test
    ✓ should return true (2ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.711s, estimated 1s
Ran all test suites matching /test.test.js/i.

我们刚刚编写了我们的第一个测试套件!它简单地说“我期望 1 等于 1。如果是,通过测试。如果不是,测试失败。”对于五行代码来说,相当强大,对吧?好吧,也许不是,但这将为我们的所有其他测试提供支架。

  1. 让我们来看一下 MongoDB 模型:models/mongo.js:
const MongoClient = require('mongodb').MongoClient;
const client = new MongoClient("mongodb://127.0.0.1:27017", { useNewUrlParser: true, useUnifiedTopology: true });

let db;
  1. 到目前为止,我们只是在设置我们的 MongoDB 连接。确保你现在仍然有你的 MongoDB 连接运行着:
const connectDB = async (test = '') => {
 if (db) {
   return db;
 }

 try {
   await client.connect();
   db = client.db(`starships${test}`);
 } catch (err) {
   console.error(err);
 }

 return db;
}
  1. 与所有良好的数据库连接代码一样,我们在一个 try/catch 块中执行我们的代码,以确保我们的连接正确建立:
const getDB = () => db

const disconnectDB = () => client.close()

module.exports = { connectDB, getDB, disconnectDB }

预览:我们将在测试和模型中使用这个 MongoDB.js 文件。module.exports 行指定了从这个文件导出并暴露给我们程序的其他部分的函数。我们将在整个程序中一贯使用这个导出指令:当我们想要暴露一个方法时,我们会使用一个导出。

  1. 返回到 test.test.js 并在文件开头包含我们的 MongoDB 模型:
const MongoDB = require('../models/mongo')
  1. 现在,让我们在我们的测试套件中变得更加花哨一点。在我们的describe方法增加以下代码:
let db

beforeAll(async () => {
   db = await MongoDB.connectDB('test')
})

afterAll(async (done) => {
   await db.collection('names').deleteMany({})
   await MongoDB.disconnectDB()
   done()
})

并在我们简单的测试之后添加以下情况:

it('should find names and return true', async () => {
   const names = await db.collection("names").find().toArray()
   expect(names.length).toBeGreaterThan(0)
})

然后使用与之前相同的命令运行它:node_modules/.bin/jest test.test.js

这里发生了什么?首先,在我们的测试套件中的每个单独的测试之前,我们正在指定按照我们在 MongoDB 模型中编写的方法连接到数据库。在一切都完成之后,拆除数据库并断开连接。

当我们运行它时会发生什么?一个史诗般的失败!

$ node_modules/.bin/jest test.test.js
 FAIL  tests/test.test.js
  test
    ✓ should return true (2ms)
    ✕ should find names and return true (9ms)

  ● test > should find names and return true

    expect(received).toBeGreaterThan(expected)

    Expected: > 0
    Received:   0

      20 |   it('should find names and return true', async () => {
      21 |     const names = await db.collection("names"
                ).find().toArray()
    > 22 |     expect(names.length).toBeGreaterThan(0)
         |                          ^
      23 |   })
      24 | });

      at Object.<anonymous> (tests/test.test.js:22:26)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 passed, 2 total
Snapshots:   0 total
Time:        1.622s, estimated 2s
Ran all test suites matching /test.test.js/i.

我们应该期望出现错误,因为我们还没有向名为names(或任何其他数据)的集合中插入任何信息!欢迎来到 TDD:我们编写了一个在编写代码之前就失败的测试。

显然,我们在这个过程中的下一步是实际插入一些数据!让我们这样做。

存储和检索数据

让我们使用我编写的一个测试套件来确保我们的 MongoDB 连接更加健壮,并包括将数据插入数据库,然后测试以确保它存在:

  1. 检查test/setup.model.test.js
const MongoDB = require('../models/mongo')
const insertRandomNames = require('../models/setup')

describe('insert', () => {
 let db

 beforeAll(async () => {
   db = await MongoDB.connectDB('test')
 })

 afterAll(async (done) => {
   await db.collection('names').deleteMany({})
   await MongoDB.disconnectDB()
   done()
 })

 it('should insert the random names', async () => {
   await insertRandomNames()

   const names = await db.collection("names").find().toArray()
   expect(names.length).toBeGreaterThan(0)
 })

})
  1. 如果我们运行node_modules/.bin/jest setup,我们会看到成功,因为我们的设置模型中存在insertRandomNames()方法。所以让我们来看看我们的设置模型(models/setups.js)并看看它是如何填充数据库的:
const fs = require('fs')
const MongoDB = require('./mongo')

let db

const setup = async () => {
 db = await MongoDB.connectDB()
}

const insertRandomNames = async () => {
 await setup()

 const names = JSON.parse(fs.readFileSync(`${__dirname}/../
  data/starship-names.json`)).names

 const result = await db.collection("names").updateOne({ key: 
  "names" }, { $set: { names: names } }, { upsert: true })

 return result
}

module.exports = insertRandomNames
  1. 还不错!我们有一个导出的方法,根据我提供的“随机”星际飞船名称的 JSON 文件将名称插入到数据库中。文件被读取,然后按以下方式放入数据库中:
db.collection("names").updateOne({ key: "names" }, { $set: { names: names } }, { upsert: true })

由于我们并没有深入了解 MongoDB 本身的细节,可以说这行代码的意思是“在names集合中(即使它还不存在),将names键设置为相等的 JSON。根据需要更新或插入”。

现在,我们可以用我提供的“随机”星际飞船名称的 JSON 文件来填充我们的数据库。执行npm run install-data

到目前为止,一切都很好!在这个项目中有很多文件,所以我们不会遍历所有文件;让我们检查一个代表性的样本。

模型,视图和控制器

模型-视图-控制器MVC)范式是我们在 Express 中使用的。虽然在 Express 中并不是真正必要的,但我发现逻辑上的关注点分离比单一类型的不可区分的文件更有用且更容易使用。在我们走得太远之前,我会提到 MVC 可能被认为是一种过时的模式,因为它确实在层之间创建了一些额外的依赖关系。话虽如此,将逻辑分离为离散的角色的架构范式背后的思想在 MVC 中是合理的。你可能会听到MV的使用,这基本上应该被理解为“模型,视图和将它们绑定在一起的任何东西”。在某些框架中,这些天 MV更受欢迎。

MVC 结构将程序的逻辑分为三个部分:

  1. 模型处理数据交互。

  2. 视图处理表示层。

  3. 控制器处理数据操作,并充当模型和视图之间的粘合剂。

这是设计模式的一个可视化表示:

图 18.4 - MVC 范例的生命周期

关于这种关注点分离的更重要的部分之一是,视图层和控制器层永远不应直接与数据存储交互;这一荣誉是为模型保留的。

现在让我们来看一个视图:

views/index.hbs

<h1>Starship Fleet</h1>

<hr />

<h2>Fleet Status</h2>
{{#if ships.length}}
 <table class="table">
   <tr>
     <th>Name</th>
     <th>Registry</th>
     <th>Top Speed</th>
     <th>Shield Strength</th>
     <th>Phaser Power</th>
     <th>Hull Damage</th>
     <th>Torpedo Complement</th>
     <th></th>
   </tr>
 {{#each ships}}
   <tr data-ship="{{this.registry}}">
     <td>{{this.name}}</td>
     <td>{{this.registry}}</td>
     <td>{{this.speed}}</td>
     <td>{{this.shields}}</td>
     <td>{{this.phasers}}</td>
     <td>{{this.hull}}</td>
     <td>{{this.torpedoes}}</td>
     <td><a class="btn btn-primary scuttle">Scuttle Ship</a></td>
   </tr>
 {{/each}}
 </table>
{{else}}
 <p>The fleet is empty. Create some ships below.</p>
{{/if}}

Express 控制我们的视图,我们使用 Handlebars 来处理我们的模板逻辑和循环。虽然语法简单,但 Handlebars 功能强大,可以极大地简化我们的生活。在这种情况下,我们正在测试并循环遍历ships变量,以创建我们拥有的ships的表格,或者发送一条消息说舰队是空的。我们的视图如何获得ships?它是通过我们的控制器通过我们的路由提供给视图的。对于这部分,它看起来是这样的:

routes/index.js

var express = require('express');
var router = express.Router();
const ShipsController = require('../controllers/ships');

/* GET home page. */
router.get('/', async (req, res, next) => {
 res.render('index', { ships: await ShipsController.getFleet() });
});

module.exports = router;

为什么我们在这里使用var而不是constlet?为什么要使用分号?答案是:在撰写本文时,Express 脚手架工具仍然使用var和分号。标准化始终是最佳实践,但在这个例子中,我想引起注意。随时根据新的语法进行标准化。

现在是getFleet方法:

controllers/ships.js

exports.getFleet = async (enemy = false) => {
 return await ShipsModel.getFleet(enemy)
}

因为这是一个简单的例子,我们的控制器除了从模型获取信息外并没有做太多事情,模型查询 MongoDB。让我们来看看:

models/ships.js

exports.getFleet = async (enemy) => {
 await setup()

 const fleet = await db.collection((!enemy) ? "fleet" :
 "enemy").find().toArray();
 return fleet.sort((a, b) => (a.name > b.name) ? 1 : -1)
}

设置函数规定了与 MongoDB 的连接(注意异步/等待设置!),我们的舰队要么来自敌人,要么来自我们的舰队集合。return行包含了一个方便的方法,按字母顺序对舰队进行排序。

在这个例子中,我们将保持控制器相当简单,并依靠模型来完成大部分工作。这是一个风格上的决定,尽管选择应用程序的一边来完成大部分工作是很好的。

现在是时候从头到尾查看程序了。

将 API 连接在一起

为了进一步了解游戏玩法,我们将逐步介绍从船只发射鱼雷的步骤:

  1. public/javascripts/play.js中找到前端 JavaScript:
document.querySelectorAll('.fire').forEach((el) => {
 el.addEventListener('click', (e) => {
   const weapon = (e.target.classList.value.indexOf('fire-torpedo') 
   > 0) ? "torpedo" : "phasers"
   const target = e.target.parentNode.getElementsByTagName
   ('select')[0].value
  1. 在我们的界面上为fire按钮创建了一个点击处理程序,并确定了我们的武器和目标船只:
fetch(
`/play/fire?  attacker=${e.target.closest('td').dataset.attacker}&target=${target}&weapon=${weapon}`)
.then(response => response.json())
.then(data => {

这一行可能需要一些解释。我们正在从我们的 JavaScript 向我们的 Node 应用程序进行 AJAX 调用,带有特定的查询字符串参数:attackertargetweapon。我们也期望从我们的应用程序返回 JSON。

  1. 记住,我们的反引号允许我们组合一个带有${ }中变量的字符串:
const { registry, name, shields, torpedoes, hull, scuttled } = data.target
  1. 我们使用对象解构data.target中提取每个信息片段。这比逐个定义它们或甚至使用循环更有效,对吧?
if (scuttled) {
       document.querySelector(`[data-ship=${registry}]`).remove()
       document.querySelectorAll(`option[value=${registry}]`).
        forEach(el => el.remove())

       const titleNode = document.querySelector("#modal-1-title")

       if (data.fleet.length === 0) {
         titleNode.innerHTML = "Your fleet has been destroyed!"
       } else if (data.enemyFleet.length === 0) {
         titleNode.innerHTML = "You've destroyed the Borg!"
       } else {
         titleNode.innerHTML = `${name} destroyed!`
       }

       MicroModal.show('modal-1')
       return
     }
  1. 如果scuttledtrue,我们的目标船只已被摧毁,因此让我们向用户传达这一点。无论哪种情况,我们都将编辑我们船只的值:
     const targetShip = document.querySelector(`[data-
      ship=${registry}]`)

     targetShip.querySelector('.shields').innerHTML = shields
     targetShip.querySelector('.torpedoes').innerHTML = torpedoes
     targetShip.querySelector('.hull').innerHTML = hull

   })
 })
})

这就是前端代码。如果我们查看我们的app.js文件,我们可以看到我们对/play的 AJAX 调用转到playRouter,从app.use语句。因此,我们的下一站是路由器:

routes/play.js

const express = require('express');
const router = express.Router();
const ShipsController = require('../controllers/ships');

router.get('/', async (req, res, next) => {
 res.render('play', { fleet: await ShipsController.getFleet(), enemyFleet:
  await ShipsController.getFleet(true) });
});

router.get('/fire', async (req, res, next) => {
 res.json(await ShipsController.fire(req.query.attacker, req.query.target, 
  req.query.weapon));
});

module.exports = router;

由于我们的 URL 是从/play/fire构建的,我们知道第二个router.get语句处理我们的请求。继续到控制器及其fire方法:

controllers/ships.js

exports.fire = async (ship1, ship2, weapon) => {
 let target = await ShipsModel.getShip(ship2)
 const source = await ShipsModel.getShip(ship1)
 let damage = calculateDamage(source, target, weapon)
  target = await ShipsModel.registerDamage(target, damage)

 return { target: target, fleet: await this.getFleet(false), enemyFleet: 
  await this.getFleet(true) }
}

在前面的代码中,我们看到了控制器和模型之间的粘合剂。首先,我们获取目标和源船只。你为什么认为我决定在目标上使用let,在源上使用const?如果你认为目标需要是可变的,你是对的:当我们在目标上使用registerDamage方法时,重写变量会比创建新变量更有效。

在查看我们的模型的registerDamage方法之前,请注意到迄今为止的返回路径是控制器将返回到返回到我们前端脚本的路由。

继续前进!

models/ships.js

exports.registerDamage = async (ship, damage) => {
 const enemy = (!ship.registry.indexOf('NCC')) ? "fleet" : "enemy"
  const target = await db.collection(enemy).findOne({ registry:
   ship.registry })

 if (target.shields > damage) {
   target.shields -= damage
 } else {
   target.shields -= damage
   target.hull += Math.abs(target.shields)
   target.shields = 0
 }

 await db.collection(enemy).updateOne({ registry: ship.registry }, { $set: { shields: target.shields, hull: target.hull } })
  if (target.hull >= 100) {
   await this.scuttle(target.registry)
   target.scuttled = true
 }

 return target
}

现在这里是我们实际与我们的数据库通信的地方。我们可以看到我们正在检索我们的目标,注册对其护盾和可能对其船体的损坏,将这些值设置在 MongoDB 中,并最终通过控制器将目标船的信息返回到我们的前端 JavaScript。

让我们来看看这一行:

await db.collection(enemy).updateOne({ registry: ship.registry }, { $set: { shields: target.shields, hull: target.hull } })

我们将更新集合中的一个项目,以说明它是敌船还是我们的舰队,并设置护盾强度和船体损坏。

导出函数

到目前为止,您可能已经注意到一些模型方法,比如registerDamage,是以exports为前缀的,而其他一些方法,比如eliminateExistingShips,则没有。在复杂的 JavaScript 应用程序中,良好的设计方面之一是封装那些不打算在特定上下文之外使用的函数。当以exports为前缀时,可以从不同的上下文中调用函数,比如从我们的控制器中。如果它不打算暴露给应用程序的其他部分;本质上,它是一个私有函数。导出变量的概念类似于作用域的概念,我们确保保持应用程序的整洁,并且只公开程序的有用部分。

如果我们看一下eliminateExistingShips,我们可以看到它只是一个辅助函数,由createRandom使用,以确保我们不会将相同的船只注册编号或名称分配给两艘不同的船只。我们可以在createRandom中看到这种用法:

const randomSeed = Math.ceil(Math.random() * names.length);

const shipData = {
  name: (!enemy) ? names[randomSeed] : "Borg Cube",

更多代码...然后:

while (unavailableRegistries.includes(shipData.registry)) {
  shipData.registry = `NCC-${Math.round(Math.random() * 10000)}`;
}

为了确保我们船只的注册编号在我们的舰队中是唯一的,我们将使用while循环来不断更新船只的注册编号,直到它不是已经存在的编号。使用eliminateExistingShips辅助函数,我们返回并解构已经存在于我们舰队中的名称和注册,以便我们不会创建重复的注册。

我们并不经常使用while循环,因为它们经常是程序中的阻塞点,并且很容易被滥用。话虽如此,这是while循环的一个很好的用例:它确保我们的程序在船只注册是唯一的情况下才能继续。通过一个随机化乘数为 10,000,很少会出现连续两次或更多次生成重复的随机注册,因此while循环是合适的。

因此,导出还是不导出,这是个问题。答案取决于我们是否需要在其直接范围之外使用该函数。如果在程序的其他部分中没有使用该函数,则不应该导出它。在这种情况下,我们需要确定船只的详细信息是否已经存在于舰队中,这在我们的ships模型中确实只有用,因此我们将不导出它。

改进我们的程序

当您阅读ships模型和控制器时,我相信您可以找到改进的地方。例如,我为了了解船只是在我们的舰队还是敌方舰队而编写的开关方式有点死板:它无法容纳在一场战斗中有三个单独的舰队。每个程序员都会创造技术债务,或者代码中的小错误或低效。这就需要重构,即改变代码使其更好。不要被愚弄以为您曾经写过完美的程序——这样的东西是不存在的。改进和持续迭代是编程过程的一部分。

然而,重构有一个重要的警告,那就是通常所谓的合同。当设计一个由前端使用的后端,并且不同的团体正在编写系统的不同部分时,重要的是要与彼此和整个程序的前提和需求保持同步。

让我们以前端 JavaScript 代码为例。如果我们枚举它正在使用的端点,我们将看到正在使用四个端点:

  • /ships

  • /ships/${e.currentTarget.closest('tr').dataset.ship}

  • /ships/random

  • `/play/fire?attacker=\({e.target.closest('td').dataset.attacker}&target=\){target}&weapon=${weapon}``

至少,在重构后端代码时,我们应该假定有一个合同义务,即不更改这些端点的路径,也不更改要接收的数据类型的期望。

我们可以帮助我们的代码更具未来性,使用一种名为 JSDoc 的松散标准进行内联文档。从代码注释创建文档是一种长期以来的做法,为了促进标准,许多语言都存在注释结构。在 API 等情况下,通常会运行一个辅助程序来针对源代码生成独立的文档,通常作为一个小型的 HTML/CSS 微型网站。您可能已经遇到了与类似风格的在线文档无关的程序。有很大的可能性,这些无关的文档站点是通过相同的机制从代码生成的。

为什么在关于 MongoDB 的章节中这很重要?嗯,文档不仅仅是数据库使用的需要;相反,当创建任何具有多个移动部分的程序时,它是重要的。考虑前面列表中的最后一个端点:/play/fire?attacker=${e.target.closest('td').dataset.attacker}&target=${target}&weapon=${weapon}

fire 端点接受三个参数:attackertargetweapon。但这些参数是什么?它们是什么样子的——是对象?字符串?布尔值?数组?此外,如果我们要接受用户生成的数据,我们需要比以前更加小心,因为GIGO垃圾进,垃圾出。如果我们用坏数据填充我们的数据库,我们最好能期望的是一个破碎的程序。事实上,我们最坏的期望是安全妥协:数据库或服务器凭据泄露或恶意代码执行。让我们谈谈安全。

安全

如果您熟悉 SQL,您可能熟悉一种称为SQL 注入的安全漏洞。关于 Web 应用程序安全最佳实践的良好信息可以在owasp.org找到。开放 Web 应用程序安全项目OWASP)是一个社区驱动的倡议,旨在记录和教育用户有关 Web 应用程序中存在的安全漏洞,以便我们可以更有效地对抗恶意黑客。如果您的电子邮件、社交帐户或网站曾被黑客入侵,您就会知道随之而来的痛苦——数字身份盗窃。OWASP 关于 SQL 注入的列表在这里:owasp.org/www-community/attacks/SQL_Injection

那么,如果我们使用的是 MongoDB 这种 NoSQL 数据库,为什么要谈论 SQL 呢?因为MongoDB 中不存在 SQL 注入。"太好了!"你可能会说,"我的安全问题解决了!"不幸的是,情况并非如此。重构以提高应用程序效率的想法,重构以减轻安全入侵向量是负责任地管理 Web 应用程序的重要部分。我曾在一家公司工作,那家公司被黑客入侵了——原因是因为在 URL 中插入了不到五个字符。这使得黑客能够破坏 Web 应用程序的操作并执行任意的 SQL 命令。对所有用户生成的内容进行消毒和重构是 Web 安全的重要部分。现在,我们还没有为这个应用程序做到这一点,因为我相信你不会黑自己的机器。

等等。我刚刚不是说 MongoDB 中不存在 SQL 注入吗?是的,NoSQL 数据库有它们等效的攻击方法:代码和命令注入。因为我们没有对用户输入进行消毒或验证完整性,所以我们的应用程序可能会存储和使用已提交并存储在我们的数据库中的任意代码。虽然本书不涵盖 JavaScript 安全的完整介绍,但请记住这一点。长话短说就是要消毒或验证您的用户生成的输入的有效性。

就这样,让我们结束这一章。只要记住,在野外编写 MongoDB 应用程序时要注意安全!

总结

JavaScript 并不孤立存在!MongoDB 是 JavaScript 的绝佳伴侣,因为它设计为面向对象,并依赖于友好的 JavaScript 查询语法。我们已经学习了 TDD 的原则,使用了 MVC 范式,并且扩展了我们的游戏。

在进行编码练习时,一定要考虑使用诸如 MongoDB 这样的数据库时的用例:虽然 MongoDB 的语法不容易受到 SQL 注入的影响,但仍然容易受到其他类型的注入攻击,这可能会危及您的应用程序。

希望我们的星际飞船游戏足够有趣,让您继续开发它。我们的下一个(也是最后一个)章节将汇集 JavaScript 开发原则,并完善我们的游戏。

第十九章:将所有内容整合在一起

终于!我们现在可以构建网站的前端和后端,并在两侧使用 JavaScript!为了将所有内容整合在一起,让我们构建一个小型 Web 应用程序,该应用程序使用带有 React 前端和 MongoDB 的 Express API。

对于我们的最终项目,我们将利用我们的技能创建一个基于数据库的旅行日志或旅行日志,包括照片和故事。我们的方法是从最初的视觉布局一直到前端和后端代码。如果您的 HTML/CSS 技能不太好,不用担心:代码已经为您提供了多个实例,因此您可以从任何地方开始处理项目。

本章将涵盖以下主题:

  • 项目简介

  • 脚手架 - React

  • 后端 - 设置我们的 API

  • 数据库 - 所有 CRUD 操作

技术要求

准备好使用存储库的chapter-19目录中提供的代码,网址为github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-19。由于我们将使用命令行工具,还需要准备终端或命令行 shell。我们需要一个现代浏览器和一个本地代码编辑器。

项目简介

从头到尾开始一个真实的 Web 项目时,重要的是要提前收集要求。这可以以许多形式呈现:口头描述,功能的项目列表,视觉线框图,完整的设计文档,或者这些的任何组合。在审查要求时,重要的是要尽可能明确,以减少误传、冗余或被放弃的工作,以及简化的工作流程。对于这个项目,我们将从视觉 comp 开始。

如果您曾经与平面设计师合作过,您可能熟悉术语 comp。视觉 comp,简称全面布局,是设计工件,是所需项目最终状态的高保真视觉表示。例如,印刷项目的 comp 将是一个数字文件,其中包含所有所需的资产,可立即发送给打印机使用。对于数字作品,您可能会收到 Adobe Photoshop、XD 或 Sketch 文件,或者许多其他类型的设计文档格式。

让我们先看一下视觉效果,以便随后确定我们的要求:

图 19.1 - 主页

我们的应用程序将具有已登录已注销状态。注销时,用户将看到封面页面,并可以使用导航按钮浏览旅行日志的条目。作为挑战,在页面加载时显示一个随机条目。

左上角的登录按钮将引导到下一个屏幕,即登录屏幕:

图 19.2 - 登录

登录页面可以简单也可以复杂。也许输入任何用户名和密码组合都可以工作,或者为了增加挑战,您可以整合 Google 或 Facebook 身份验证。您甚至可以编写自己的身份验证,使用您的数据库存储凭据。

一旦经过身份验证,我们在左侧栏有一个新按钮:仪表板按钮。这是带我们到应用程序的各个部分的地方:

图 19.3 - 仪表板

当单击“访问过的国家”按钮时,我们将显示由 D3.js 图形库提供支持的矢量地图:

图 19.4 - 旅行地图

突出显示的国家由数据库提供的 JSON 清单控制。

最后但同样重要的是,用户需要能够撰写条目并插入照片:

图 19.5 - 新条目/编辑条目屏幕

我们将使用一个名为 Quill 的 JavaScript 所见即所得(WYSIWYG)编辑器。

在构建应用程序时,可以随意对其外观和感觉进行一些自定义-使其成为您自己的!您可能还想添加一些其他功能,例如媒体库来管理上传的照片,或者搜索功能。

现在我们已经有了关于我们的视觉布局的想法,让我们开始着手项目的前端。

脚手架 - React

我们的项目非常适合使用 React 来进行前端开发,因此让我们为前端制定我们的要求:一个单一的 React 应用程序,具有可重用的组件和Hooks 和 context用于状态保存。与我们以前使用 React 的方式相比,Hooks 是一个新概念。在 React 16.8 中添加的 Hooks 是允许您在函数组件中操作状态和上下文以进行状态管理的函数。

除了我们手工制作的 React 应用程序,我们还将整合一些额外的预构建库,以简化我们的项目并利用现成的工具。D3.js 是一个强大的图形和数据可视化库,我们将利用它来制作我们的地图。Quill 是一个富文本编辑器,它将允许您使用文本格式编写条目,并上传和放置照片。

由您决定是要从npx create-react-app开始,还是使用 GitHub 存储库的chapter-19目录中的Step 1文件夹中提供的脚手架代码。

我将对要使用的其他包提出一些建议;在项目进行过程中,可以随意添加或删除包。我将使用以下内容:

  • 引导(用于布局)

  • d3d3-queuetopojson-client(用于我们的地图)

  • node-sass(使用 Sass 创建更高效的样式表)

  • quillreact-quilljs(一个所见即所得的编辑器)

  • react-router-dom(一个使 URL 路径设置变得容易的 React 扩展)

  • react-cookie(一个方便使用 cookie 的包)

如果您从头开始,现在可以使用create-react-app脚手架进行设置,或者开始使用Step 1目录。在本章的其余部分,将为您提供逐步跟随的说明。

Step 1目录中,您将找到以下内容:

.
├── README.md
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ ├── robots.txt
│ └── uploads
├── src
│ ├── App.css
│ ├── App.js
│ ├── App.test.js
│ ├── components
│ │ ├── Dashboard
│ │ │ └── Dashboard.js
│ │ ├── Editor
│ │ │ └── Editor.js
│ │ ├── Header
│ │ │ └── Header.js
│ │ ├── Login
│ │ │ └── Login.js
│ │ ├── Main
│ │ │ └── Main.js
│ │ ├── Map
│ │ │ └── Map.js
│ │ └── Toolbar
│ │ ├── Toolbar.js
│ │ ├── dashboard.svg
│ │ └── login.svg
│ ├── index.css
│ ├── index.js
│ ├── logo.svg
│ ├── serviceWorker.js
│ ├── setupTests.js
│ └── styles
│ └── _App.scss
└── yarn.lock

这是一个标准的create-react-app脚手架,与我们以前所做的有一些不同。让我们来看一个组件:标题。

函数组件

这是我们的Header.js文件的代码:

import React from 'react'

function Header() {
 return (
   <>
     <h2>Chris Newman's</h2>
     <h1>Travelogue</h1>
   </>
 )
}
export default Header

您应该注意到一些事情:首先,文件名以js结尾,而不是jsx。其次,我们的组件是一个返回 HTML 的函数,而不是扩展React.Component的类。虽然在 React 中,基于类和函数的组件都是有效的,但在使用 React 时,特别是使用最新的方法来利用状态和上下文时,函数组件被认为更现代。我们现在不会深入讨论函数和面向对象编程之间的区别,但可以说有一些需要注意的区别。您可以在本章末找到有关这些区别的有用资源。

下一步

要将应用程序推进到下一个阶段,考虑我们制定的功能要求。一个很好的下一步可能是实现一个登录系统。在这一点上,您可能既不想也不需要实际验证凭据,因此一个虚拟的登录页面就足够了。您可以在Login/Login.js中找到标记。

我们要采取的方法是使用Hookscontext。由于这是一个相当复杂的主题,我们在这里不会详细介绍所有细节,但有很多文章解释了这些概念。这是其中一个:www.digitalocean.com/community/tutorials/react-crud-context-hooks

我们将通过一个上下文示例和一些 Hooks 示例来帮助您入门:

  1. 首先,我们需要创建一个UserContext.js文件,它将帮助我们在用户交互的整个生命周期中跟踪我们的登录状态。代码本身非常简单:
import React from 'react'

export const loggedIn = false

const UserContext = React.createContext(loggedIn)

export default UserContext
  1. React 的Context API是一种向多个组件提供有状态信息的方法。注意我说的“提供”?这正是我们接下来需要做的:提供我们的App.js上下文。我们将组件包装如下:
import React, { useState } from 'react'
import './styles/_App.scss'
import Main from './components/Main/Main';
import UserContext, { loggedIn } from './components/UserContext'

function App() {

 const loginHook = useState(loggedIn)

 return (
   <UserContext.Provider value={loginHook}>
     <div className="App">
       <Main />
     </div>
   </UserContext.Provider>
 )
}

export default App

注意我们如何导入UserContext并在UserContext.Provider标签中包装我们的App组件,并向其提供loginHook有状态值,从而传递给其子组件。

  1. 我们的Main.js文件也需要一些更改。看一下这段代码:
function Main() {
 const [loggedIn, setLoggedIn] = useContext(UserContext)
 const [cookies, setCookie] = useCookies(['logged-in'])
...

我们需要从 React 和react-cookies中分别导入useContextuseCookies,然后我们可以使用这些Hooks来处理我们的登录状态。除了内部上下文之外,我们还将在 cookie 中存储我们的登录状态,以便返回会话时保持登录状态。我们还需要从 React 中导入useEffect作为下一步:

const setOrCheckLoggedIn = (status) => {
   if (cookies['logged-in'] === 'true' || status) {
     setLoggedIn(true)
   }

   if (status && cookies['logged-in'] !== 'true') {
     setCookie('logged-in', true)
   }
 }

 useEffect(() => {
 setOrCheckLoggedIn()
 })

您是否还记得在以前的章节中,我们是如何直接使用componentDidMount()来对 React 组件的挂载状态做出反应的?使用 React Hooks,我们可以使用useEffect Hook 来处理我们组件的状态。在这里,我们将确保我们的用户上下文(loggedIn)和logged-in cookie 被适当设置。

  1. 我们的setOrCheckLoggedIn函数还需要传递给其他组件,即ToolbarLogin。将其设置为doLogin属性。

从这一点开始,当我们包括UserContext的上下文时,我们可以依赖loggedIn状态变量来确定我们的用户是否已登录。例如,我们简单的Login组件的逻辑可以利用这些 Hooks 如下:

import React, { useContext } from 'react'
import UserContext from '../UserContext'

const Login = (props) => {

 let [loggedIn, setLoggedIn] = useContext(UserContext)

 const logMeIn = () => {
   loggedIn = !loggedIn
   props.doLogin(loggedIn)
 }

 return (
   <>
     <div className="Login">
       <h1>Log In</h1>

       <p><input type="text" name="username" id="username" /></p>
       <p><input type="password" name="password" id="password"
       /></p>
       <p><button type="submit" onClick={logMeIn}>Go</button></p>
     </div>
   </>
 )
}

export default Login

相当简单!首先,我们获取我们的上下文,并在点击Go按钮时,翻转上下文。您应该在Toolbar.js文件中也加入类似的逻辑,以便登录图标也能处理登出。

现在,我们需要一个后端与我们的前端进行交互,并与 MongoDB 数据库进行交易,该数据库将存储我们的故事条目和可能的用户身份验证数据。还需要创建一个端点来上传图像,因为仅有前端代码是无法写入服务器文件系统的。

后端 - 设置我们的 API

让我们列出我们需要使我们的旅行日志工作的端点:

  • 读取(GET):像大多数 API 一样,我们需要一个端点来读取条目。对于这一点,我们不会强制进行身份验证或登录。

  • 写入(POST):此端点将用于创建新的旅行和编辑现有的旅行。

  • 上传(POST):我们需要一个端点从我们的前端调用以上传照片。

  • 登录(POST)(可选):如果您想自己处理身份验证,创建一个登录端点,可以使用数据库或社交媒体登录端点的凭据。

  • 媒体(GET)(可选):有一个列出所有上传到服务器的媒体文件的 API 将是有用的。

  • 国家(GET)(可选):为列出您访问过的国家提供一个特定的端点也是一个好主意,以支持您的世界地图。

在工作过程中,您可能会发现自己创建更多的端点,这很正常!从头到尾规划您的 API 总是一个好主意,但如果您需要在途中进行更改以便通过辅助端点或其他部分更轻松地完成工作,那也是可以的。

我们准备好进入我们存储库中的Step 3目录了。

API 作为代理 - 第 3 步

因为我们正在使用 React 前端,我们将重新考虑使用 Express 作为后端,React 代理我们的 API 请求,如下所示:

  1. 我们需要做的第一件事是告诉我们的系统通过在我们的package.json中添加这一行来使用代理:"proxy": "http://localhost:5000"

  2. 添加后,重新启动 React(您会注意到我们的前端主页已经改变;我们马上就会解决这个问题),然后在api目录中,执行npm install,然后在api目录中执行npm start

  3. 我们应该测试我们的后端,确保我们的 API 有响应。将这作为一个测试添加到App.js文件的导入后:

fetch('/api')
 .then(res => res.text())
 .then(text => console.log(text))

这个非常基本的fetch调用应该调用我们 API 中的routes/index.js组件的get方法:

router.get('/', (req, res) => {
 res.sendStatus(200)
})

此时,我们的控制台应该显示OK。如果你在这个阶段遇到任何问题,最好现在调试它们。

  1. 我们知道我们将设置一个数据库来处理我们的数据,但目前,我们可以搭建我们的 API 方法,就像你在routes/index.js中看到的那样:
router.get('/article', (req, res) => {
 res.send({ story: "A story from the database" })
})

router.post('/article/edit', (req, res) => {
 res.sendStatus(200)
})

router.post('/media/upload', (req, res) => {
 res.sendStatus(200)
})

router.get('/media', (req, res) => {
 res.send({ media: "A list of media" })
})

router.post('/login', (req, res) => {
 res.sendStatus(200)
})

router.get('/countries', (req, res) => {
 res.send({ countries: "A list of countries" })
})

现在我们已经在步骤 2中搭建了我们的登录系统,我对步骤 3目录进行了一些修改。如前所述,我们的主页有点不同,因为它是旅行日志的首页,用于在用户注销时显示故事。

  1. 接下来检查Story/Story.js组件:
import React from 'react'

function Story() {

 fetch('/api/article')
   .then(res => res.json())
   .then(json => console.log(json))

 return (
   <div className="Story">
     <h1>Headline</h1>
...

是的,另一个虚拟 API 调用到我们的后端!这个调用也是一个简单的 GET 请求,所以让我们做一些更复杂的事情。

  1. 继续登录到网站,你会在你的仪表板上看到一些不同的东西:

图 19.6 - 我们的仪表板正在成形...

  1. 很好,现在我们有一个完整的仪表板。点击“添加行程”按钮,你将看到一个编辑器,如下所示:

图 19.7 - 我们的文本编辑器

如果你在编辑器中输入富文本并保存它,你会在控制台中看到来自 API 的提交数据的响应。从那里,我们需要与我们的 API 一起工作,将数据保存到我们的数据库中。所以...最后,但并非最不重要的,我们需要设置我们的数据库。

数据库 - 所有 CRUD 操作

当然,我们需要一个数据存储库来进行创建、读取、更新和删除功能,所以让我们返回到 MongoDB 来存储这些文档。如果需要刷新你在设置方面的记忆,你可以参考第十八章,Node.js 和 MongoDB

要从头开始设置数据库,有助于考虑你打算使用的数据库结构。虽然 MongoDB 不需要模式,但计划你的 MongoDB 文档仍然是一个好主意,这样你就不会在各个部分之间的功能或命名上随意。

以下是每个集合可能看起来像的一个想法:

settings:  {
  user
    firstname
    lastname
    username
    password
  title
  URL
  media directory
}

entry: {
  title
  location
  date
    month
    day
    year
  body
}

location: {
  city
  region
  country
  latitude
  longitude
  entries
}

保持数据库简单是好的,但记住你总是可以扩展它。

总结

当然,我不能只是交给你一个最终项目,对吧?在这一章中,我们搭建了我们的旅行日志 - 其余的就看你了。还有一些功能尚未完成,以便拥有一个完全功能的项目。毕竟,我们还没有完全遵守我们的视觉设计,对吧?以下是一些关于要实现哪些功能的想法,以完成项目:

  • 将信息持久化到数据库。

  • 工作在图像上传和保存上。

  • 编辑现有文章。

  • 创建countries端点以填充 D3.js 地图。

  • 启用真正的登录。

  • 简化用户旅程。

完成后,这个项目将成为你的作品集中的一个作品,展示了,一个 Python 开发者,如何掌握 JavaScript。从数据类型、语法、循环和 Node.js 的开始,到最终创建一个完全功能的项目,你已经走了很长的路。

我由衷地感谢你陪伴我走过这段旅程!继续学习,长寿繁荣

进一步阅读

关于函数式编程和面向对象编程之间的区别的有用资源可以在www.geeksforgeeks.org/difference-between-functional-programming-and-object-oriented-programming/找到。

第二十章:评估

第一章

  1. 哪个国际组织维护 JavaScript 的官方规范?

  2. W3C

  3. Ecma International

  4. 网景

  5. 太阳

  6. 哪些后端可以与 JavaScript 通信?

  7. PHP

  8. Python

  9. Java

  10. 以上所有

  11. 谁是 JavaScript 的原始作者?

  12. 蒂姆·伯纳斯-李

  13. 布兰登·艾奇

  14. Linus Torvalds

  15. 比尔·盖茨

  16. DOM 是什么?

  17. JavaScript 在内存中对 HTML 的表示

  18. 允许 JavaScript 修改页面的 API

  19. 以上两者

  20. 以上都不是

  21. Ajax 的主要用途是什么?

  22. 与 DOM 通信

  23. DOM 的操作

  24. 监听用户输入

  25. 与后端通信

第二章

  1. 或假:Node.js 是单线程的。

  2. 真或:Node.js 的架构使其不受分布式拒绝服务DDoS)攻击的影响。

  3. 谁最初创建了 Node.js?

  4. 布兰登·艾奇

  5. Linux Torvalds

  6. 阿达·洛夫莱斯

  7. Ryan Dahl

  8. 真或:服务器端的 JavaScript 本质上是不安全的,因为代码在前端暴露。

  9. 真或:Node.js 本质上优于 Python。

第三章

  1. 以下哪个不是有效的 JavaScript 变量声明?

  2. var myVar = 'hello';

  3. const myVar = "hello"

  4. String myVar = "hello";

  5. let myVar = "hello"

  6. 以下哪个开始了函数声明?

  7. 功能

  8. const

  9. 功能

  10. def

  11. 以下哪个不是基本循环类型?

  12. for..in

  13. 映射

  14. JavaScript 需要使用分号进行行分隔:

  15. 在 JavaScript 中,空格永远不计算:

第四章

  1. JavaScript 本质上是:

  2. 同步

  3. 异步

  4. 两者

  5. 一个fetch()调用返回:

  6. then

  7. next

  8. 最后

  9. Promise

  10. 使用原型继承,我们可以(选择所有适用的选项):

  11. 在基本数据类型中添加方法。

  12. 从基本数据类型中减去方法。

  13. 重命名我们的数据类型。

  14. 将我们的数据转换为另一种格式。

let x = !!1
console.log(x)
  1. 在给定的代码中,预期的输出是什么?

  2. 1

  3. 0

const Officer = function(name, rank, posting) {
 this.name = name
 this.rank = rank
 this.posting = posting
 this.sayHello = () => {
 console.log(this.name)
 }
}

const Riker = new Officer("Will Riker", "Commander", "U.S.S. Enterprise")
  1. 在这段代码中,输出“威尔·莱克”最好的方法是什么?

  2. Riker.sayHello() *

  3. console.log(Riker.name)

  4. console.log(Riker.this.name)

  5. Officer.Riker.name()

第五章

考虑以下代码:

function someFunc() {
  let bar = 1;

  function zip() {
    alert(bar); // 1
    let beep = 2;

    function foo() {
      alert(bar); // 1
      alert(beep); // 2
    }
  }

  return zip
}

function sayHello(name) {
  const sayAlert = function() {
    alert(greeting)
  }

  const sayZip = function() {
    someFunc.zip()
  }

  let greeting = `Hello ${name}`
  return sayAlert
}
  1. 如何获得警报'你好,鲍勃'

  2. sayHello()('Bob')

  3. sayHello('Bob')()*****

  4. sayHello('Bob')

  5. someFunc()(sayHello('Bob'))

  6. 在上述代码中,alert(greeting)会做什么?

  7. 警报'问候'

  8. 警报'你好,爱丽丝'

  9. 抛出错误

  10. 以上都不是

  11. 我们如何获得警报消息1

  12. someFunc()()*****

  13. sayHello().sayZip()

  14. alert(someFunc.bar)

  15. sayZip()

  16. 我们如何获得警报消息2

  17. someFunc().foo()

  18. someFunc()().beep

  19. 我们不能,因为它不在范围内

  20. 我们不能,因为它没有定义

  21. 我们如何将someFunc更改为警报 1 1 2?

  22. 我们不能。

  23. return zip之后添加return foo

  24. return zip更改为return foo

  25. foo声明之后添加return foo

  26. 在给定上一个问题的正确解决方案的情况下,我们如何实际获得三个警报,即 1、1、2?

  27. someFunc()()()*****

  28. someFunc()().foo()

  29. someFunc.foo()

  30. alert(someFunc)

第六章

考虑以下代码:

  <button>Click me!</button>
  1. 选择按钮的正确语法是什么?

  2. document.querySelector('点击我!')

  3. document.querySelector('.button')

  4. document.querySelector('#button')

  5. document.querySelector('button')

看看这段代码:

<button>Click me!</button>
<button>Click me two!</button>
<button>Click me three!</button>
<button>Click me four!</button>
  1. 真或:document.querySelector('button')将满足我们对每个按钮放置点击处理程序的需求。

  2. 要将按钮的文本从“点击我!”更改为“先点我!”,我们应该使用什么?

  3. document.querySelectorAll('button')[0].innerHTML = "先点我!"

  4. document.querySelector('button')[0].innerHTML = "先点我!"

  5. document.querySelector('button').innerHTML = "先点我!"

  6. document.querySelectorAll('#button')[0].innerHTML = "先点我!"

  7. 我们可以使用哪种方法添加另一个按钮?

  8. document.appendChild('button')

  9. document.appendChild('

  10. document.appendChild(document.createElement('button'))

  11. document.appendChild(document.querySelector('button'))

  12. 如何将第三个按钮的类更改为“third”?

  13. document.querySelector('button')[3].className = 'third'

  14. document.querySelectorAll('button')[2].className = 'third'

  15. document.querySelector('button[2]').className = 'third'

  16. document.querySelectorAll('button')[3].className = 'third'

第七章

回答以下问题以衡量您对事件的理解:

  1. 以下哪个是事件生命周期的第二阶段?

  2. 捕获

  3. 定位

  4. 冒泡

  5. (选择所有正确答案)事件对象为我们提供了什么?

  6. 触发的事件类型

  7. 目标 DOM 节点(如果适用)

  8. 鼠标坐标(如果适用)

  9. 父 DOM 节点(如果适用)

看看这段代码:

container.addEventListener('click', (e) => {
  if (e.target.className === 'box') {
    document.querySelector('#color').innerHTML = e.target.style.backgroundColor
    document.querySelector('#message').innerHTML = e.target.innerHTML
    messageBox.style.visibility = 'visible'
    document.querySelector('#delete').addEventListener('click', (event) => {
      messageBox.style.visibility = 'hidden'
      e.target.remove()
    })
  }
})
  1. 它使用了哪些 JavaScript 特性?选择所有适用的答案:

  2. DOM 操作

  3. 事件委托

  4. 事件注册

  5. 样式更改

  6. 当容器被点击时会发生什么?

  7. box 将可见。

  8. #color 将是红色的。

  9. 选项 1 和 2 都是。

  10. 没有足够的上下文来说。

  11. 在事件生命周期的哪个阶段我们通常采取行动?

  12. 定位

  13. 捕获

  14. 冒泡

第九章

  1. 内存问题的根本原因是什么?

  2. 程序中的变量是全局的。

  3. 低效的代码。

  4. JavaScript 的性能限制。

  5. 硬件不足。

  6. 在使用 DOM 元素时,应该将对它们的引用存储在本地,而不是总是访问 DOM。

  7. 当多次使用它们时为真

  8. JavaScript 在服务器端进行预处理,因此比 Python 更有效。

  9. 设置断点无法找到内存泄漏。

  10. 将所有变量存储在全局命名空间中是一个好主意,因为它们更有效地引用。

posted @ 2024-05-22 12:09  绝不原创的飞龙  阅读(32)  评论(0编辑  收藏  举报