按照惯例,我们将从 Hello World 应用开始,这是一个最简单的应用,使用了大部分 MERN 组件。任何 Hello World 的主要目的都是展示我们正在使用的技术或堆栈的基本特征,以及启动和运行它所需的工具。
在这个 Hello World 应用中,我们将使用 React 呈现一个简单的页面,并使用 Node.js 和 Express 从 web 服务器提供该页面。这将让你学习这些技术的基本原理。这也将让您对 nvm、npm 和 JSX 变换有一些基本的了解——一些我们将会经常用到的工具。
为了快速起步,让我们在一个 HTML 文件中编写一段简单的代码,使用 React 在浏览器上显示一个简单的页面。没有安装,下载,或服务器!你所需要的是一个现代的浏览器,可以运行我们编写的代码。
让我们开始创建这个 HTML 文件,并将其命名为index.html。您可以使用您最喜欢的编辑器,将这个文件保存在文件系统的任何地方。让我们从基本的 HTML 标签开始,比如<html>、<head>和<body>。然后,让我们包括 React 库。
毫不奇怪,React 库是一个 JavaScript 文件,我们可以使用<script>标签将它包含在 HTML 文件中。它由两部分组成:第一部分是 React 核心模块,负责处理 React 组件及其状态操作等。第二个是 ReactDOM 模块,它处理将 React 组件转换成浏览器可以理解的 DOM。这两个库可以在 unpkg 中找到,un pkg 是一个内容交付网络(CDN ),它使得所有开源 JavaScript 库都可以在线使用。让我们使用来自以下 URL 的库的开发(相对于生产)版本:
-
React 过来:
https://unpkg.com/react@16/umd/react.development.js -
反应式:“t1”【t0”
这两个脚本可以包含在<head>部分,使用如下的<script>标签:
...
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
...接下来,在主体中,让我们创建一个<div>,它将最终保存我们将创建的任何 React 元素。这可以是一个空的<div>,但是它需要一个 ID,比如说content,来识别和获取 JavaScript 代码中的句柄。
...
<div id="content"></div>
...要创建 React 元素,需要调用 React 模块的createElement()函数。这非常类似于 JavaScript document.createElement()函数,但是有一个额外的特性,允许嵌套元素。该函数最多接受三个参数,其原型如下:
React.createElement(type, [props], [...children])类型可以是任何 HTML 标签,比如字符串'div',或者 React 组件(我们将在下一章开始创建)。props是包含 HTML 属性或自定义组件属性的对象。最后一个参数是零个或多个子元素,也是使用createElement()函数本身创建的。
对于 Hello World 应用,让我们创建一个非常简单的嵌套元素——一个带有 title 属性的<div>(只是为了展示属性是如何工作的),它包含一个带有“Hello World!”字样的标题下面是用于创建我们的第一个 React 元素的 JavaScript 代码片段,该元素将放在主体的<script>标记中:
...
const element = React.createElement('div', {title: 'Outer div'},
React.createElement('h1', null, 'Hello World!')
);
...我们在本书中使用了 es 2015+JavaScript 特性,在这个片段中,我们使用了const关键字。这应该可以在所有现代浏览器中正常工作。如果你使用的是旧版浏览器,比如 Internet Explorer 10,你需要将const改为var。在本章的最后,我们将讨论如何支持旧的浏览器,但在此之前,请使用一种现代浏览器进行测试。
React 元素(React.createElement()调用的结果)是一个 JavaScript 对象,表示屏幕上显示的内容。因为它可以是其他元素的嵌套集合,并且可以描述整个屏幕上的一切,所以它也被称为虚拟 DOM 。请注意,这还不是真正的 DOM,它在浏览器的内存中,这就是它被称为虚拟 DOM 的原因。它作为一组嵌套很深的 React 元素驻留在 JavaScript 引擎的内存中,这些元素也是 JavaScript 对象。React 元素不仅包含需要创建哪些 DOM 元素的细节,还包含一些有助于优化的关于树的附加信息。
这些 React 元素中的每一个都需要被转移到真实的 DOM 中,以便在屏幕上构建用户界面。为此,需要对应于每个 React 元素进行一系列的document.createElement()调用。当调用ReactDOM.render()函数时,ReactDOM 会这样做。该函数将需要呈现的元素和需要放置的 DOM 元素作为参数。
我们已经使用React.createElement()构建了需要呈现的元素。至于包含元素,我们在主体中创建了一个<div>,它是新元素需要放置的目标。我们可以通过调用document.getElementByID()来获得父进程的句柄,就像我们使用普通的 JavaScript 一样。让我们这样做,并呈现 Hello World React 元素:
...
ReactDOM.render(element, document.getElementById('content'));
...让我们把这些都放在index.html里。该文件的内容如清单 2-1 所示。
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Pro MERN Stack</title>
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
</head>
<body>
<div id="contents"></div>
<script>
const element = React.createElement('div', {title: 'Outer div'},
React.createElement('h1', null, 'Hello World!')
);
ReactDOM.render(element, document.getElementById('content'));
</script>
</body>
</html>
Listing 2-1index.html: Server-less Hello World您可以通过在浏览器中打开该文件来测试它。加载 React 库可能需要几秒钟的时间,但是很快你就会看到浏览器显示标题,如图 2-1 所示。您还应该能够将鼠标悬停在文本上或外部 div 边界内文本右侧的任何地方,并且应该能够看到工具提示“外部 div”弹出。
图 2-1
用 React 写的 Hello World
-
尝试向
h1元素添加一个类(您还需要在<head>部分的<style>部分中定义该类,以测试它是否工作)。提示:在 stackoverflow.com 搜索“如何在 jsx 中指定类”。你能解释这个吗? -
检查开发人员控制台中的
element变量。你看到了什么?如果你给这棵树起个名字,你会给它起什么名字?
本章末尾有答案。
我们在上一节中创建的简单元素很容易使用React.createElement()调用来编写。但是想象一下,编写一个深度嵌套的元素和组件层次结构:它会变得非常复杂。此外,当使用函数调用时,实际的 DOM 不容易可视化,因为如果它是普通的 HTML,它是可以可视化的。
为了解决这个问题,React 有一种叫做 JSX 的标记语言,它代表了 JavaScript XML T3。JSX 看起来非常像 HTML,但也有一些不同之处。因此,代替React.createElement()调用,JSX 可以用来构建一个元素或元素层次结构,使它看起来非常像 HTML。对于我们创建的简单的 Hello World 元素,事实上,HTML 和 JSX 之间没有区别。所以,让我们把它写成 HTML 并把它赋给元素,替换掉React.CreateElement()调用:
...
const element = (
<div title="Outer div">
<h1>Hello World!</h1>
</div>
);
...注意,尽管它惊人地接近 HTML 语法,但它是而不是 HTML。还要注意,标记没有用引号括起来,所以它也不是一个可以用作innerHTML的字符串。它是 JSX,可以和 JavaScript 自由混合。
现在,考虑到与 HTML 相比的所有差异和复杂性,你为什么需要学习 JSX 呢?它增加了什么价值?为什么不直接编写 JavaScript 本身呢?我在导言一章中谈到的一件事是,MERN 自始至终只有一种语言;这不是与那相反吗?
随着我们进一步探索 React,你很快就会发现 HTML 和 JSX 之间的差异并不是翻天覆地的,它们非常符合逻辑。只要你理解并内化了其中的逻辑,你就不需要记很多东西,也不需要查资料。尽管直接编写 JavaScript 来创建虚拟 DOM 元素确实是一种选择,但我发现这非常繁琐,并且不能帮助我可视化 DOM。
此外,由于您可能已经知道基本的 HTML 语法,编写 JSX 可能会更好。当你阅读 JSX 时,很容易理解屏幕会是什么样子,因为它与 HTML 非常相似。因此,在本书的其余部分,我们使用 JSX。
但是浏览器的 JavaScript 引擎不理解 JSX。它必须被转换成常规的基于 JavaScript 的React.createElement()调用。为此,需要一个编译器。做这件事的编译器(事实上还可以做更多)是 Babel。理想情况下,我们应该预编译代码并将其注入到浏览器中,但出于原型设计的目的,Babel 提供了一个可以在浏览器中使用的独立编译器。像往常一样,这是一个 JavaScript 文件,可以在 unpkg 上获得。让我们将这个脚本包含在index.html的<head>部分中,如下所示:
...
<script src="https://unpkg.com/@babel/standalone@7/babel.min.js"></script>
...但是编译器也需要被告知哪些脚本必须被转换。它在所有脚本中查找属性type= " text/babel",并转换和运行任何具有该属性的脚本。因此,让我们将这个属性添加到主脚本中,让 Babel 完成它的工作。下面是实现这一点的代码片段:
...
<script type="text/babel">
...清单 2-2 显示了使用 JSX 的一整套更改。
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Pro MERN Stack</title>
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone@7/babel.min.js"></script>
</head>
<body>
<div id="contents"></div>
<script type="text/babel">
const element = React.createElement('div', {title: 'Outer div'},
React.createElement('h1', null, 'Hello World!')
);
const element = (
<div title="Outer div">
<h1>Hello World!</h1>
</div>
);
ReactDOM.render(element, document.getElementById('contents'));
</script>
</body>
</html>
Listing 2-2index.html: Changes for Using JSX虽然我们不遗余力地确保所有代码清单的准确性,但在本书付印之前,可能会有一些错别字甚至更正没有出现在书中。所以,总是依赖 GitHub 库( https://github.com/vasansr/pro-mern-stack-2 )作为所有代码清单的经过测试的和最新的源代码,尤其是当某些东西不能按预期工作时。
当您测试这组更改时,您会发现页面的外观没有什么不同,但是由于 Babel 进行了编译,它可能会稍微慢一点。但别担心。我们将很快切换到在构建时而不是运行时编译 JSX,以消除性能影响。请注意,该代码还不能在较旧的浏览器上运行;您可能会在脚本babel.min.js中得到错误。
文件index.html可以在 GitHub 存储库中的目录public下找到;这是文件最终的位置。
-
从脚本中删除
type=``text/babel。当你加载index.html时会发生什么?你能解释一下为什么吗?放回type=``text/babel但是去掉巴别塔 JavaScript 库。现在会发生什么? -
我们用的是缩小版的巴别塔,但不是 React 和 ReactDOM。你能猜到原因吗?切换到生产缩小版本,并在 React 中引入运行时错误(查看 unpkg.com 网站,了解这些库的生产版本的名称)。例如,在内容 Node 的 ID 中引入一个错别字,这样就没有地方安装组件了。会发生什么?
本章末尾有答案。
无服务器设置允许您熟悉 React,而无需任何安装或启动服务器。但是您可能已经注意到了,这对开发和生产都没有好处。在开发过程中,需要额外的时间从内容交付网络或 CDN 加载脚本。如果您使用浏览器开发人员控制台的 Network 选项卡查看每个脚本的大小,您会发现 babel 编译器(即使是缩小版)非常大。在生产中,尤其是在大型项目中,JSX 到 JavaScript 的运行时编译会降低页面加载速度并影响用户体验。
所以,让我们稍微组织一下,从 HTTP 服务器提供所有文件。当然,我们将使用 MERN 堆栈的一些其他组件来实现这一点。但是在我们做所有这些之前,让我们设置我们的项目和文件夹,我们将在其中保存文件和安装库。
我们将在 shell 中输入的命令已经被收集在 GitHub 库根目录下的一个名为commands.md的文件中。
如果您在键入命令时发现某些东西不能按预期工作,请在 GitHub 资源库( https://github.com/vasansr/pro-mern-stack-2 )中交叉检查这些命令。这是因为错别字可能是在书的制作过程中引入的,或者最后一刻的更正可能错过了这本书。另一方面,GitHub 库反映了最新的和经过测试的代码和命令。
首先,让我们安装 nvm。这代表 Node Version Manager,该工具使 Node.js 的多个版本之间的安装和切换变得容易。Node.js 可以在没有 nvm 的情况下直接安装,但我发现,当我不得不开始一个新项目,并且希望在那个时间点使用 Node.js 的最新和最棒的版本时,一开始安装 nvm 使我的生活变得更容易。与此同时,我不想为我的其他大型项目切换到最新版本,因为害怕破坏这些项目中的东西。
要安装 nvm,如果您使用的是 Mac OS 或任何基于 Linux 的发行版,请遵循 nvm 的 GitHub 页面上的说明。这可以在 https://github.com/creationix/nvm 找到。Windows 用户可以关注 nvm for Windows(在你喜欢的搜索引擎中搜索)或者直接安装 Node.js,不需要 nvm。一般来说,我建议 Windows 用户安装一个 Linux 虚拟机(VM),最好使用 vagger,并在 VM 内完成所有的服务器端编码。这通常效果最好,尤其是因为代码最终几乎总是部署在 Linux 服务器上,拥有相同的开发环境效果最好。
关于 nvm 的一件棘手的事情是知道它如何初始化你的路径。这在不同的操作系统上有不同的工作方式,所以一定要仔细阅读其中的细微差别。本质上,它向您的 shell 的初始化脚本添加了几行,以便您下次打开 shell 时,您的路径被初始化并执行 nvm 的初始化脚本。这让 nvm 知道所安装的 Node.js 的不同版本,以及默认可执行文件的路径。
因此,最好在安装 nvm 后立即启动一个新的 shell,而不是继续安装它。一旦你为你的 nvm 找到了正确的道路,事情就会进展顺利。
你可以选择直接安装 Node.js,不安装 nvm,这也很好。但是本章的其余部分假设您已经安装了 nvm。
现在我们已经安装了 nvm,让我们使用 nvm 安装 Node.js。有许多版本的 Node.js 可用(请查看网站, https://nodejs.org ),但出于本书的目的,我们将选择最新的长期支持(LTS),它恰好是 10:
$ nvm install 10LTS 版本肯定会比其他版本获得更长时间的支持。这意味着,尽管不能指望功能升级,但可以指望向后兼容的安全和性能修复。此外,新的次要版本可以安装,而不必担心破坏现有的代码。
现在我们已经安装了 Node.js,让我们将该版本作为未来的默认版本。
$ nvm alias default 10否则,下次进入 shell 时,node 将不在路径中,或者我们选择以前安装的默认版本。您可以通过在新的 shell 或终端中键入以下内容来确认默认安装的 node 版本:
$ node --version此外,一定要确保任何新外壳也显示最新版本。(注意,Windows 版本的 nvm 不支持alias命令。每次打开一个新的 shell 时,您可能都必须执行nvm use 10。)
通过 nvm 安装 Node.js 也会安装软件包管理器 npm。如果您直接安装 Node.js,请确保您也安装了兼容版本的 npm。您可以通过记下随 Node.js 一起安装的 npm 版本来确认这一点:
$ npm --version它应该显示版本 6 的一些内容。npm 可能会提示您有新版本的 npm,并要求您安装该版本。在任何情况下,让我们安装我们希望在本书中使用的 npm 版本,如下指定版本:
$ npm install –g npm@6请确保您不会错过–g标志。它告诉 npm 全局安装自己*,也就是说,对所有项目都可用。要再次检查,再次运行npm --version。*
*### 项目
在我们用 npm 安装任何第三方包之前,初始化项目是个好主意。有了 npm,甚至一个应用也被认为是一个包。包定义了应用的各种属性。一个重要的属性是应用所依赖的其他包的列表。随着时间的推移,这种情况将会改变,因为随着应用的发展,我们发现需要使用库。
首先,我们至少需要一个占位符来保存和初始化这些东西。让我们创建一个名为pro-mern-stack-2的目录来托管应用。让我们从这个目录中初始化项目,如下所示:
$ npm init这个命令问你的大多数问题应该很容易回答。默认设置也很好。从现在开始,对于所有 shell 命令,尤其是 npm 命令(我将在下面描述),您应该位于项目目录中。这将确保所有的更改和安装都本地化到项目目录中。
要使用 npm 安装任何东西,要使用的命令是npm install <package>。首先,因为我们需要一个 HTTP 服务器,所以让我们使用 npm 安装 Express。安装 Express 非常简单:
$ npm install express一旦完成,你会注意到它说安装了许多软件包。这是因为它还会安装 Express 依赖的所有其他软件包。现在,让我们卸载并重新安装一个特定的版本。在本书中,我们使用版本 4,所以让我们在安装时指定该版本。
$ npm uninstall express
$ npm install express@4安装软件包时,只指定主要版本(在本例中为 4)就足够了。这意味着你可以安装一个次要版本,这个版本与你写这本书时使用的版本不同。在极少数情况下,这会导致问题,请在 GitHub 存储库中的package.json中查找包的具体版本。然后,在安装软件包时指定确切名称,例如npm install express@4.16.4。
npm 是非常强大的,它的选择是巨大的。目前,我们只关心软件包的安装和一些其他有用的东西。项目目录下安装文件的位置是 npm 的制作者有意识的选择。这具有以下效果:
-
所有安装都位于项目目录的本地。这意味着不同的项目可以使用不同版本的任何已安装的软件包。乍一看,这似乎是不必要的,感觉像是大量的重复。但是,当您启动多个 Node.js 项目,并且不想处理一个不需要的包升级时,您将真正欣赏 npm 的这一特性。此外,您会注意到整个 Express 包(包括所有依赖项)只有 1.8MB。由于包非常小,所以磁盘使用量过大根本不是问题。
-
包的依赖项也在包内被隔离。因此,可以安装依赖于一个公共包的不同版本的两个包,并且它们都有自己的副本,因此可以完美地工作。
-
安装软件包不需要管理员(超级用户)权限。
当然,有一个全局安装包的选项,有时这样做很有用。一个用例是将命令行实用程序打包为 npm 包。在这种情况下,不管工作目录如何,让命令行可用是非常有用的。在这种情况下,npm install 的–g选项可用于全局安装包,并使其在任何地方都可用。
如果您已经通过 nvm 安装了 Node.js,全局安装将使用您自己的主目录,并使该包可用于您主目录中的所有项目。全局安装软件包不需要超级用户或管理员权限。另一方面,如果您直接安装了 Node.js,使其对您计算机上的所有用户可用,您将需要超级用户或管理员权限。
此时,最好再次查看 GitHub 存储库( https://github.com/vasansr/pro-mern-stack-2 ),尤其是查看与上一步的不同之处。在本节中,我们只添加了新文件,所以您将看到的唯一区别是新文件。
-
package.json是什么时候创作的?如果猜不出来,就考察内容;这应该给你一个提示。还是想不通?回去重新做你的步骤。从创建项目目录开始,在每个步骤中查看目录内容。 -
卸载 Express,但使用选项
--no-save。现在,只需输入npm install。会发生什么?这次手动添加另一个依赖项,比如 MongoDB 到package.json。使用版本作为“最新”。现在,输入npm install。会发生什么? -
安装任何新软件包时使用
--save-dev。你觉得package.json有什么不同?你认为会有什么不同? -
您认为软件包文件安装在哪里?键入
npm ls --depth=0检查所有当前安装的软件包。清理所有不需要的包。
试着安装和卸载 npm。这通常是有用的。从文档中了解关于 npm 版本语法的更多信息: https://docs.npmjs.com/files/package.json#dependencies 。
本章末尾有答案。
虽然签入package.json.lock文件是一个很好的做法,这样安装的确切版本可以在团队成员之间共享,但是我已经将它从存储库中排除了,以保持差异的简洁和可读性。当您使用 MERN 堆栈启动一个团队项目时,您应该在您的 Git 存储库中签入这个文件。
如果您还记得上一章的介绍,Express 是在 Node.js 环境中运行 HTTP 服务器的最佳方式。首先,我们将使用 Express 只服务静态文件。这是为了让我们习惯于 Express 所做的事情,而不用进入大量的服务器端编码。我们将通过 Express 提供我们在上一节中创建的index.html文件。
在上一步中,我们已经安装了 Express,但是为了确保它在那里,让我们执行 npm 命令来再次安装它。如果软件包已经安装,这个命令什么也不做,所以如果有疑问,我们可以再次运行它。
$ npm install express@4要开始使用 Express,让我们导入模块并使用模块导出的顶级函数,以便实例化一个应用。这可以使用以下代码来完成:
...
const express = require('express');
...require是 Node.js 特有的 JavaScript 关键字,用于导入其他模块。这个关键字不是浏览器端 JavaScript 的一部分,因为没有包含其他 JavaScript 文件的概念。所有需要的脚本都直接包含在 HTML 文件中。ES2015 规范想出了一种使用import关键字来包含其他文件的方法,但在规范出来之前,Node.js 不得不使用require发明自己的方法。它也被称为包含其他模块的常见方式。
在前一行中,我们加载了名为express的模块,并在名为express的常量中保存了该模块导出的顶层对象。Node.js 允许的东西是一个函数,一个对象,或者任何适合变量的东西。模块输出的类型和形式实际上取决于模块,模块的文档会告诉你如何使用它。
在 Express 的情况下,该模块导出一个可用于实例化应用的函数。我们只是把这个函数赋给了变量express。
我们使用 ES2015 const关键字来定义变量express。这使得变量在第一次声明后不可赋值。对于可能被赋予新值的变量,可以用关键字let代替const。
Express 应用是监听特定 IP 地址和端口的 web 服务器。可以创建多个应用来监听不同的端口,但是我们不会这样做,因为我们只需要一台服务器。让我们通过调用express()函数来实例化这个唯一的应用:
...
const app = express();
...现在我们已经有了应用的句柄,让我们来设置它。Express 是一个框架,它自己完成最少的工作;相反,它让名为中间件的功能来完成大部分工作。中间件是一个接受 HTTP 请求和响应对象的函数,加上链中的下一个中间件函数。该函数可以查看和修改请求和响应对象,响应请求,或者通过调用下一个中间件函数来决定继续使用中间件链。
此时,我们需要查看请求并根据请求 URL 的路径返回文件内容的东西。内置的express.static函数生成一个中间件函数来完成这个任务。它通过尝试将请求 URL 与生成器函数的参数指定的目录下的文件进行匹配来响应请求。如果文件存在,它返回文件的内容作为响应,如果不存在,它链接到下一个中间件函数。我们可以这样创建中间件:
...
const fileServerMiddleware = express.static('public');
...static()函数的参数是中间件应该查找文件的目录,相对于应用运行的位置。对于我们将作为本书的一部分构建的应用,我们将把所有静态文件存储在项目根目录下的public目录中。让我们在项目根目录下创建这个新目录public,并将我们在上一节中创建的index.html移动到这个新目录中。
现在,为了让应用使用静态中间件,我们需要在应用上安装。Express 应用中的中间件可以使用应用的use()方法来挂载。该方法的第一个参数是要匹配的任何 HTTP 请求的基本 URL。第二个论点是中间件功能本身。因此,要使用静态中间件,我们可以这样做:
...
app.use('/', fileServerMiddleware);
...第一个参数是可选的,如果没有指定,默认为'/',所以我们也可以跳过它。
最后,既然应用已经设置好了,我们需要启动服务器,让它为 HTTP 请求提供服务。应用的listen()方法启动服务器并永远等待请求。它将端口号作为第一个参数。让我们使用端口 3000,一个任意的端口。我们不会使用端口 80,通常的 HTTP 端口,因为要监听该端口,我们需要有管理(超级用户)权限。
listen()方法还接受另一个参数,这是一个可选的回调函数,当服务器成功启动时可以调用它。让我们提供一个匿名函数,它只打印服务器已经启动的消息,如下所示:
...
app.listen(3000, function () {
console.log('App started on port 3000');
});
...让我们将所有这些放在项目根目录下的一个名为server.js的文件中。清单 2-3 显示了最终的服务器代码,其中use()调用与中间件的创建合并在一行中,并跳过了可选的第一个参数,即挂载点。
const express = require('express');
const app = express();
app.use(express.static('public'));
app.listen(3000, function () {
console.log('App started on port 3000');
});
Listing 2-3server.js: Express Server现在,我们准备启动 web 服务器并为index.html提供服务。如果你在 GitHub 库中寻找代码,你会在一个名为server的目录下找到server.js。但是此时,该文件需要位于项目目录的根目录下。
要启动服务器,在项目的根目录下使用 Node.js 运行时运行它,如下所示:
$ node server.js您应该会看到一条消息,说明应用已经在端口 3000 上启动。现在,打开你的浏览器,在地址栏输入http://localhost:3000/index.html。您应该会看到我们在上一节中创建的 Hello World 页面。如果你看到一个 404 错误信息,可能你还没有将index.html移动到public目录中。
静态中间件函数服务于来自public目录的index.html文件的内容,因为它匹配请求 URL。但它也足够聪明,可以将请求翻译成/(网站的根目录),并通过在目录中查找index.html来做出响应。这类似于 Apache 等其他静态 web 服务器的做法。因此,只需输入http://localhost:3000/就足以进入 Hello World 页面。
要启动服务器,我们必须向 Node.js 提供入口点的名称(server.js),这可能不容易记住或者告诉项目的其他用户。如果我们的项目中有许多文件,那么人们如何知道哪个文件是启动服务器的呢?幸运的是,所有 Node.js 项目中都使用了一个约定:npm 脚本用于执行常见任务。以下命令行是启动服务器的另一种方法:
$ npm start执行这个命令时,npm 会查找文件server.js并使用 Node.js 运行它。因此,让我们停止服务器(在命令 shell 中使用 Ctrl+C)并使用npm start重新启动服务器。您应该会看到相同的消息,说明服务器已经启动。
但是如果我们有一个不同的服务器起点呢?事实上,我们希望所有与服务器相关的文件都放在一个名为server的目录中。因此,让我们创建该目录并将server.js移动到该目录中。
现在,如果您运行npm start,它将失败并出现错误。那是因为 npm 在根目录中寻找server.js,没有找到。为了让 npm 知道服务器的入口点是子目录server中的server.js,需要在package.json的scripts部分添加一个条目。清单 2-4 展示了这些变化。
...
"main": "index.js",
"scripts": {
"start": "node server/server.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
...
Listing 2-4package.json: Changes for Start Script因此,如果服务器起始点不是根目录中的server.js,那么完整的命令行必须在package.json的scripts部分指定。
注意在package.json中还有一个名为main的字段。当我们初始化这个文件时,这个字段的值被自动设置为index.js。该字段是而不是用于指示服务器的起点。相反,如果这个包是一个模块(相对于一个应用),那么当这个项目在其他项目中使用require()作为一个模块导入时,index.js将会是要加载的文件。由于这个项目不是可以导入到其他项目中的模块,所以这个字段我们没有任何兴趣,源代码中也没有index.js。
现在,我们可以使用npm start看到熟悉的应用启动消息,并最后一次测试它。在这个时候,最好检查一下 GitHub 库,看看这一部分的不同之处。特别是,看一下现有文件package.json中的更改,以熟悉如何在书中显示文件中的更改,以及如何在 GitHub 中将相同的更改视为差异。
-
将
index.html文件的名称改为其他名称,比如说hello.html。这对应用有什么影响? -
如果您希望所有静态文件都可以通过一个带前缀的 URL 来访问,例如
/public,您会做什么改变?提示:在https://expressjs.com/en/starter/static-files.html看一下静态文件的 Express 文档。
本章末尾有答案。
在前面的所有章节中,JSX 到 JavaScript 的转换发生在运行时。这是低效的,也是不必要的。相反,让我们将转换转移到开发中的构建阶段,这样我们就可以部署一个随时可用的应用发行版。
作为第一步,我们需要将 JSX 和 JavaScript 从一体化软件index.html中分离出来,并将其称为外部脚本。这样,我们可以将 HTML 保持为纯 HTML,并将所有需要编译的脚本保存在一个单独的文件中。让我们调用这个外部脚本App.jsx并将它放在public目录中,这样就可以从浏览器中引用它为/App.jsx。当然,新脚本文件的内容不会包含<script>标签。并且,在index.html中,让我们将内联脚本替换为对外部源的引用,如下所示:
...
<script type="text/babel" src="/App.jsx"></script>
...注意,仍然需要脚本类型text/babel,因为 JSX 编译是在浏览器中使用巴别塔库进行的。新修改的文件列在清单 2-5 和 2-6 中。
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Pro MERN Stack</title>
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone@7/babel.min.js"></script>
</head>
<body>
<div id="contents"></div>
<script type="text/babel" src="/App.jsx"></script>
</body>
</html>
Listing 2-5index.html: Separate HTML and JSXconst element = (
<div title="Outer div">
<h1>Hello World!</h1>
</div>
);
ReactDOM.render(element, document.getElementById('contents'));
Listing 2-6App.jsx: JSX Part Separated Out from the HTML此时,应用应该会像以前一样继续工作。如果您将浏览器指向http://localhost:3000,您应该会看到同样的 Hello World 消息。但是我们只分离了文件;我们没有把变形移到建造时间。JSX 继续通过在浏览器中执行的巴别塔库脚本进行转换。在下一节中,我们将把转换移到构建时间。
现在让我们创建一个新目录来保存所有的 JSX 文件,这些文件将被转换成普通的 JavaScript 并保存到public文件夹中。让我们称这个目录为src,并将App.jsx移动到这个目录中。
对于转换,我们需要安装一些巴别塔工具。我们需要核心的 Babel 库和命令行界面(CLI)来完成转换。让我们使用以下命令来安装它们:
$ npm install --save-dev @babel/core@7 @babel/cli@7为了确保 Babel 编译器作为命令行可执行文件可用,让我们尝试在命令行上执行命令babel,并使用--version选项检查安装的版本。由于这不是一个全球安装,巴别塔将不会出现在路径中。我们必须从它的安装位置专门调用它,如下所示:
$ node_modules/.bin/babel --version这应该会给出与此类似的输出,但是次要版本可能会有所不同,例如,7.2.5 而不是 7.2.3:
7.2.3 (@babel/core 7.2.2)我们可以使用 npm 的--global(或–g)选项在全球范围内安装@babel/cli。这样,我们就可以访问任何目录中的命令,而不必在路径前面加上前缀。但是正如前面所讨论的,将所有安装保持在项目的本地是一个好的做法。这是为了我们不必处理跨项目的包的版本差异。此外,npm 的最新版本给了我们一个方便的命令叫做npx,它可以解析任何可执行文件的正确的本地路径。该命令仅在 npm 版本 6 及更高版本中可用。让我们使用这个命令来检查 Babel 版本:
$ npx babel --version接下来,要将 JSX 语法转换成常规 JavaScript,我们需要一个预置(Babel 使用的一种插件)。这是因为 Babel 能够进行许多其他变换(我们将在下一节中介绍),并且许多不属于 Babel 核心库的预设作为不同的包提供。JSX 变换预置就是这样一个叫做preset-react的预置,所以让我们安装它。
$ npm install --save-dev @babel/preset-react@7现在我们准备将App.jsx转换成纯 JavaScript。babel命令行接受一个输入目录,其中包含适用的源文件和预置,并接受输出目录作为选项。对于 Hello World 应用,源文件在src目录中,所以我们希望转换的输出在public目录中,并且我们希望应用 JSX 转换预置@babel/react。下面是实现这一点的命令行:
$ npx babel src --presets @babel/react --out-dir public如果您查看输出目录public,您会看到那里有一个名为App.js的新文件。如果您在编辑器中打开该文件,您可以看到 JSX 元素已经被转换为React.createElement()调用。注意,Babel 编译自动为输出文件使用了扩展名.js,这表明它是纯 JavaScript。
现在,我们需要改变index.html中的引用来反映新的扩展,并删除脚本类型规范,因为它是纯 JavaScript。此外,我们不再需要在index.html中加载运行时转换器,因此我们可以摆脱babel-core脚本库规范。这些变化如清单 2-7 所示。
...
<script src="https://unpkg.com/@babel/standalone@7/babel.min.js"></script>
...
<body>
<div id="contents"></div
<script src="/App.jsx" type="text/babel"></script>
script src="/App.js"></script>
</body>
...
Listing 2-7index.html: Change in Script Name and Type如果您测试这一组更改,您应该看到事情像以前一样工作。为了更好地衡量,你可以使用浏览器的开发者控制台来确保获取的是App.js,而不是App.jsx。开发人员控制台可以在大多数浏览器上找到;您可能需要查看您的浏览器文档,以获得访问它的说明。
-
检查转换输出
App.js的内容。你会在public目录中找到它。你看到了什么? -
为什么我们在安装
babel-cli的时候用了--save-dev?提示:在https://docs.npmjs.com/cli/install阅读用于安装 CLI 命令的 npm 文档。
本章末尾有答案。
我之前提到过,JavaScript 代码将在所有支持 ES2015 的现代浏览器中工作。但是,如果我们需要支持旧的浏览器,例如,Internet Explorer,该怎么办呢?老版本的浏览器不支持箭头功能和Array.from()方法。事实上,在 IE 11 或更早版本中运行此时的代码应该会抛出一个控制台错误消息,说明Object.assign不是一个函数。
让我们对 JavaScript 进行一些更改,并使用这些高级 ES2015 功能。然后,让我们做一些改变,以便在旧的浏览器中也支持所有这些特性。要使用 ES2015 功能,我们不显示包含 Hello World 的消息,而是创建一个大陆数组,并构建一条包含每个大陆的消息。
...
const continents = ['Africa','America','Asia','Australia','Europe'];
...现在,让我们使用Array.from()方法构造一个新的数组,每个大洲的名称前面有一个 Hello,末尾有一个感叹号。为此,我们将使用数组的map()方法,接受一个箭头函数。我们将使用字符串插值,而不是连接字符串。Array.from()、箭头功能和字符串插值都是 ES2015 的功能。使用新的映射数组,让我们构造消息,它只是加入新数组。下面是代码片段:
...
const helloContinents = Array.from(continents, c => `Hello ${c}!`);
const message = helloContinents.join(' ');
...现在,让我们在 heading 元素中使用构造的message变量,而不是硬编码的 Hello World 消息。与使用反勾号的 ES2015 字符串插值类似,JSX 让我们通过将 JavaScript 表达式括在花括号中来使用它。这些将被表达式的值替换。这不仅适用于 HTML 文本 Node,也适用于属性。例如,元素的类名可以是 JavaScript 变量。让我们使用这个特性来设置要在标题中显示的消息。
...
<h1>{message}</h1>
...修改后的App.jsx的完整源代码如清单 2-8 所示。
const continents = ['Africa','America','Asia','Australia','Europe'];
const helloContinents = Array.from(continents, c => `Hello ${c}!`);
const message = helloContinents.join(' ');
const element = (
<div title="Outer div">
<h1>{message}</h1>
</div>
);
ReactDOM.render(element, document.getElementById('contents'));
Listing 2-8App.jsx: Changes to Show the World with ES2015 Features如果您使用 Babel 转换它,重启服务器并指向它的浏览器。你会发现它可以在大多数现代浏览器上运行。但是,如果您查看转换后的文件App.js,您会发现 JavaScript 本身并没有改变,只有 JSX 被替换为React.createElement()调用。这在既不识别箭头函数语法也不识别Array.from()方法的旧浏览器上肯定会失败。
巴贝尔再次前来救援。我谈到了 Babel 能够进行的其他转换,这包括将较新的 JavaScript 特性转换成较旧的 JavaScript,即 ES5。就像 JSX 变换的react预置一样,每个特性都有一个插件。比如有个插件叫plugin-transform-arrow-functions。我们可以安装这个插件,并在 React 预置之外使用它,如下所示:
$ npm install --no-save @babel/plugin-transform-arrow-functions@7我们使用了--no-save安装选项,因为这是一个临时安装,我们不希望package.json因为临时安装而改变。让我们使用这个插件并像这样转换源文件:
$ npx babel src --presets @babel/react
--plugins=@babel/plugin-transform-arrow-functions --out-dir public现在,如果您检查转换的输出,App.js,您将看到箭头函数已经被常规函数所取代。
这很好,但是如何知道哪些插件必须被使用呢?找出哪些浏览器支持什么语法以及我们必须为每种浏览器选择什么转换将是一件很乏味的事情。幸运的是,Babel 通过一个名为preset-env的预置来自动解决这个问题。这个预设让我们指定需要支持的目标浏览器,并自动应用支持这些浏览器所需的所有转换和插件。
所以,让我们卸载transform-arrow-function预置,安装包含所有其他插件的env预置。
$ npm uninstall @babel/plugin-transform-arrow-functions@7
$ npm install --save-dev @babel/preset-env@7我们不使用命令行(如果使用的话,会很长),而是指定需要在配置文件中使用的预置。Babel 在一个名为.babelrc的文件中寻找这个。事实上,在不同的目录中可以有一个.babelrc文件,该目录中文件的设置可以在每个目录中单独指定。因为我们在名为src的目录中有所有的客户端代码,所以让我们在那个目录中创建这个文件。
.babelrc文件是一个 JSON 文件,它可以包含预置和插件。预设被指定为一个数组。我们可以将这两个预设指定为数组中的字符串,如下所示:
...
{
"presets": ["@babel/preset-env", "@babel/preset-react"]
}预置preset-env需要进一步配置以指定目标浏览器及其版本。这可以通过使用第一个元素作为预设名称后跟其选项的数组来实现。对于preset-env,我们将使用的选项称为targets,它是一个对象,以键作为浏览器名称,以值作为其目标版本:
["@babel/preset-env", {
"targets": {
"safari": "10",
...
}
}]让我们包括对 IE 版本 11 和其他流行浏览器稍旧版本的支持。完整的配置文件如清单 2-9 所示。
{
"presets": [
["@babel/preset-env", {
"targets": {
"ie": "11",
"edge": "15",
"safari": "10",
"firefox": "50",
"chrome": "49"
}
}],
"@babel/preset-react"
]
}
Listing 2-9src/.babelrc: Presets Configured for JSX and ES5 Transform现在,babel命令可以在命令行上不指定任何预置的情况下运行:
$ npx babel src --out-dir public如果您运行这个命令,然后检查生成的App.js,您会发现箭头函数已经被一个常规函数所取代,字符串插值也已经被一个字符串连接所取代。如果您从配置文件中取出行ie: "11"并重新运行转换,您会发现这些转换不再存在于输出文件中,因为我们的目标浏览器已经本地支持这些特性。
但是,即使进行了这些转换,如果您在 Internet Explorer 版本 11 上测试,代码仍然无法工作。这是因为不仅仅是转变;有一些内置的东西,比如Array.find(),是浏览器中没有的。请注意,再多的编译或转换也不能像Array.find()实现那样添加一堆代码。我们真的需要这些实现作为函数库在运行时可用。
所有这些功能实现都被称为 polyfills ,以补充旧浏览器中缺失的实现。Babel 转换只能处理语法变化,但是需要这些 polyfills 来添加这些新函数的实现。Babel 也提供了这些聚合填充,只需将它们包含在 HTML 文件中就可以使用这些功能。巴别塔多填充可以在 unpkg 中找到,所以让我们把它包含在index.html中。清单 2-10 显示了index.html的变化,包括多孔填料。
...
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/polyfill@7/dist/polyfill.min.js"></script>
</head>
...
Listing 2-10index.html: Changes for Including Babel Polyfill现在,代码也可以在 Internet Explorer 上运行。图 2-2 显示了新的 Hello World 屏幕应该是什么样子。
图 2-2
新的 Hello World 屏幕
- 通过使用
<br />代替空格连接各个消息,尝试将消息格式化为每行一条。你能做到吗?为什么不呢?
本章末尾有答案。
除了能够使用npm start启动项目,npm 还能够定义其他定制命令。当该命令有许多命令行参数,并且在 shell 上输入这些参数变得很繁琐时,这一点尤其有用。(我不是说过 npm 很厉害吗?这是它做的事情之一,即使这不是真正的软件包管理器功能。)这些定制命令可以在package.json的脚本部分指定。然后可以从控制台使用npm run <script>运行这些程序。
让我们添加一个名为compile的脚本,它的命令行是 Babel 命令行来完成所有的转换。我们不需要前缀npx,因为 npm 会自动计算出命令的位置,这些命令是任何本地安装包的一部分。我们需要在package.json做的附加工作是:
...
"compile": "babel src --out-dir public",
...转换现在可以这样运行:
$ npm run compile在这之后,如果您再次运行npm start来启动服务器,您可以看到App.jsx的任何变化都反映在应用中。
避免使用也是 npm 第一级命令的 npm 子命令名,如build和rebuild,因为如果在 npm 命令中省略了run,会导致无声错误。
当我们处理客户端代码并频繁更改源文件时,我们必须为每次更改手动重新编译它。如果有人能为我们检测到这些变化,并将源代码重新编译成 JavaScript,那不是很好吗?嗯,Babel 通过--watch选项支持开箱即用。为了使用它,让我们在 Babel 命令行中添加另一个名为watch的脚本,并增加这个选项:
...
"watch": "babel src --out-dir public --watch --verbose"
...它本质上是与 compile 相同的命令,但是有两个额外的命令行选项,--watch和--verbose。第一个选项指示 Babel 监视源文件中的变化,第二个选项使它每当变化导致重新编译时就在控制台中打印出一行。这只是为了保证无论何时做出更改,编译都已经发生,只要您在运行该命令的控制台上保持警惕。
通过使用名为nodemon的包装器命令,可以对服务器代码的更改进行类似的重启。每当一组文件发生更改时,该命令都会使用指定的命令重新启动 Node.js。你也可能通过搜索互联网发现forever是另一个可以用来实现同样目标的包。通常,forever用于在崩溃时重启服务器,而不是监视文件的变化。最佳实践是在开发期间使用nodemon(真正需要观察变化的地方)和在生产中使用forever(崩溃时需要重启)。那么,现在让我们安装nodemon:
$ npm install nodemon@1现在,让我们使用nodemon来启动服务器,而不是package.json中start的脚本规范中的 Node.js。命令nodemon还需要一个选项来指示使用-w选项来监视哪个文件或目录的变化。因为所有的服务器文件都将放在名为server的目录中,所以当该目录中的任何文件发生变化时,我们可以使用-w server让nodemon重启 Node.js。因此,package.json中启动脚本的新命令现在将是:
...
"start": "nodemon -w server server/server.js"
...清单 2-11 显示了在package.json中添加或更改的最后一组脚本。
...
"scripts": {
"start": "node server/server.js",
"start": "nodemon -w server server/server.js",
"compile": "babel src --out-dir public",
"watch": "babel src --out-dir public --watch --verbose",
"test": "echo \"Error: no test specified\" && exit 1"
},
...
Listing 2-11Package.json: Adding Scripts for Transformation如果您现在使用npm run watch运行新命令,您会注意到它执行了一次转换,但是没有返回到 shell。它实际上是在一个永久的循环中等待,观察源文件的变化。所以,要运行服务器,需要另一个终端,在那里可以执行npm start。
如果你对App.jsx做了一个小的改变并保存文件,你会看到public目录中的App.js被重新生成。而且,当您刷新浏览器时,您可以看到这些更改,而不必手动重新编译。您还可以对server.js进行任何更改,并看到服务器启动,控制台上会显示一条消息,提示服务器正在重启。
在本章中,您学习了如何构建 React 应用的基础知识。我们从运行时编译的用 React JSX 编写的一段简单代码开始,然后我们将编译和文件提供给服务器。
我们用 nvm 安装 Node.js 您看到了 npm 不仅可以用来安装 Node.js 包,还可以用来在传统的或容易发现的脚本中保存命令行指令。然后,我们使用 Babel 来 transpile ,也就是说,将语言的一种规范转换或编译成另一种规范,以支持更老的浏览器。Babel 还帮助我们将 JSX 转化为纯 JavaScript。
您还对 Node.js with Express 的功能有所了解。我们没有使用 MongoDB,即 MERN 堆栈中的 M,但是我希望您能够很好地了解堆栈的其他组件。
到目前为止,您应该已经熟悉了这本书的 GitHub 库是如何组织的,以及书中使用的约定。对于每个部分,都有一组可测试的代码,您可以将自己的代码与它们进行比较。重要的是,每一步之间的差异对于理解每一步中发生的确切变化是有价值的。请再次注意,GitHub 资源库中的代码是值得依赖的,其中的最新更改无法在印刷书籍中体现出来。如果你发现你已经一字不差地遵循了这本书,但是事情并不像预期的那样工作,请参考 GitHub 库,看看印刷的书的错误是否已经在那里被纠正。
在接下来的两章中,我们将更深入地研究 React,然后在后面的章节中讨论 API、MongoDB 和 Express。
-
要在
React.createElement()中指定一个类,我们需要用{className: <name>}代替{class: <name>}。这是因为class是 JavaScript 中的保留字,我们不能把它作为对象中的字段名。 -
element变量包含一个嵌套的元素树,它反映了 DOM 应该包含的内容。我称之为虚拟世界,这也是人们通常所说的。
-
删除脚本类型将导致浏览器将其视为常规 JavaScript,我们将在控制台上看到语法错误,因为 JSX 不是有效的 JavaScript。相反,移除 Babel 编译器将导致该脚本被忽略,因为浏览器不识别类型为
text/babel的脚本,它将忽略该脚本。在这两种情况下,应用都不会工作。 -
缩小版的 React 隐藏或缩短了运行时错误。非精简版本给出了完整的错误和有用的警告。
-
package.json是在我们使用npm init创建项目时创建的。事实上,我们在运行npm init时对提示的所有 React 都记录在了package.json中。 -
当使用
--no-save时,npm 保持文件package.json不变。由此可见,package.json早就保留了明示的从属关系。不带任何选项或参数运行npm install会安装package.json中列出的所有依赖项。因此,您可以手动将依赖项添加到package.json中,只需使用npm install。 -
--save-dev选项在devDependencies而不是dependencies添加包。开发依赖列表将不会被安装到产品中,这由被设置为字符串production的环境变量NODE_ENV来指示。 -
包文件安装在项目下的目录
node_modules下。npm ls以树状方式列出所有已安装的软件包。--depth=0将树深度限制在顶层包。删除整个node_modules目录是确保您开始清理的一种方式。
-
静态文件中间件并没有像对待
index.html那样特别对待hello.html,所以你将不得不像这样使用文件名来访问应用:http://localhost:3000/hello.html。 -
为了通过不同的挂载点访问静态文件,在中间件生成的帮助函数中指定前缀作为第一个参数。比如
app.use('/public', express.static('/public'))。
-
App.js 现在包含纯 JavaScript,所有 JSX 元素都转换成了
React.createElement()调用。当转换发生在浏览器中时,我们之前看不到这种转换。 -
当我们部署代码时,我们将只部署应用的预构建版本。也就是说,我们将在构建服务器或我们的开发环境上转换 JSX,并将结果 JavaScript 推到我们的生产服务器上。因此,在生产服务器上,我们将不需要构建应用所需的工具。因此,我们使用了
--save-dev,这样,在生产服务器上,就不需要安装这个包了。
- React 故意这样做,以避免跨站点脚本漏洞。插入 HTML 标记并不容易,尽管有一种使用元素的
dangerouslySetInnerHTML属性的方法。正确的做法是组成一个组件数组。我们将在后面的章节中探讨如何做到这一点。*

