Web 应用开发已经今非昔比,甚至几年前也是如此。今天,有这么多的选择,门外汉往往不知道什么对他们有好处。有很多选择;不仅仅是广泛的栈(所使用的各种层或技术),还包括用于帮助开发的工具。这本书声称 MERN 堆栈对于开发一个完整的 web 应用是非常棒的,并带领读者完成所有必要的工作。
在这一章中,我将对 MERN 堆栈所包含的技术进行概述。在这一章中,我不会详细介绍细节或例子,相反,我将只介绍高层次的概念。我将关注这些概念如何影响对 MERN 是否是您下一个 web 应用项目的好选择的评估。
任何 web 应用都是使用多种技术构建的。这些技术的组合被称为“堆栈”,因 LAMP stack 而流行,LAMP stack 是 Linux、Apache、MySQL、PHP 的缩写,这些都是开源软件。随着 web 开发的成熟和交互性的出现,单页面应用变得越来越流行。SPA 是一种 web 应用范例,它避免了从服务器获取整个 web 页面的内容来显示新内容。相反,它使用对服务器的轻量级调用来获取一些数据或片段,并更改网页。与完全重新加载页面的旧方法相比,结果看起来相当漂亮。这带来了前端框架的兴起,因为很多工作都是在前端完成的。几乎在同一时间,虽然完全不相关,NoSQL 数据库也开始流行起来。
MEAN (MongoDB,Express,AngularJS,Node.js)栈是早期的开源栈之一,集中体现了向 SPAs 和 NoSQL 的转变。基于模型视图控制器(MVC)设计模式的前端框架 AngularJS 锚定了这个堆栈。MongoDB 是一个非常流行的 NoSQL 数据库,用于持久数据存储。Node.js 是一个服务器端 JavaScript 运行时环境,Express 是一个构建在 Node.js 之上的 web 服务器,它们构成了中间层,即 web 服务器。几年前,这种堆栈可以说是任何新的 web 应用最流行的堆栈。
不完全竞争,但 React 是脸书创造的替代前端技术,越来越受欢迎,并提供了 AngularJS 的替代方案。因此,它将 MEAN 中的“A”替换为“R ”,得到 MERN 堆栈。我说“不完全是”,因为 React 不是一个成熟的 MVC 框架。它是一个用于构建用户界面的 JavaScript 库,所以在某种意义上,它是 MVC 的视图部分。
尽管我们选择了一些定义技术来定义一个堆栈,但这些不足以构建一个完整的 web 应用。需要其他工具来帮助开发过程,并且需要许多库来补充 React。这本书是关于基于 MERN 堆栈和所有这些相关的工具和库来构建一个完整的 web 应用。
除了 MERN 堆栈之外,有任何 web 应用堆栈经验的开发人员和架构师会发现这本书对于了解这种现代堆栈很有用。要求事先了解 web 应用如何工作。还需要 JavaScript 知识。还假设读者了解 HTML 和 CSS 的基础知识。如果你也熟悉版本控制工具 git,那会有很大帮助;您可以通过克隆保存了本书中描述的所有源代码的 git 存储库,并通过检查一个分支来运行每个步骤,来试验代码。
书中的代码使用了 JavaScript (ES2015+)的最新特性,并且假设你对这些特性如类、胖箭头函数、const关键字等非常熟悉。每当我第一次使用这些现代 JavaScript 特性时,我会用注释指出来,这样您就知道这是一个新特性。如果你不熟悉某个特定的特性,你可以在遇到它的时候仔细阅读。
如果你已经决定你的新应用将使用 MERN 堆栈,那么这本书是一个完美的推动者,让你快速起步。即使你没有,读这本书也会让你对 MERN 感到兴奋,并让你有足够的知识为未来的项目做出选择。你将学到的最重要的东西是把多种技术放在一起,构建一个完整的、功能性的 web 应用,你可以被称为 MERN 的全栈开发者或架构师。
虽然这本书的重点是让您学习如何构建一个完整的 web 应用,但这本书的大部分内容都围绕 React 展开。这只是因为,就像大多数现代水疗中心一样,前端代码构成了主体。在这种情况下,React 用于前端。
这本书的基调是教程式的,是为边做边学而设计的。在本书的过程中,我们将构建一个 web 应用。我使用“我们”这个术语,因为您将需要编写代码,就像我向您展示将作为大量代码清单的一部分编写的代码一样。除非你和我一起自己写代码,并解决练习,否则你不会得到这本书的全部好处。我鼓励你而不是复制粘贴;相反,请键入代码。我发现这在学习过程中非常有价值。非常小的细微差别(例如,引号的类型)可能会造成很大的差异。当你输入代码时,你会比仅仅阅读代码时更能意识到这一点。
有时,你可能会遇到你输入的东西不工作的情况。在这种情况下,您可能希望复制粘贴以确保代码是正确的,并克服您可能犯下的任何打字错误。在这种情况下,不要从书的电子版本中复制粘贴而不是,因为排版可能不符合实际代码。我在 https://github.com/vasansr/pro-mern-stack-2 创建了一个 GitHub 库,供你比较,在不可避免的情况下,可以复制粘贴。
我还在每一个可以单独测试的更改后添加了一个检查点(实际上是一个 git 分支),这样您就可以在线查看两个检查点之间的确切差异。存储库的主页(自述文件)中列出了检查点和到 diffs 的链接。您可能会发现这比查看整个源代码,甚至是本书正文中的清单更有用,因为 GitHub 的差异远比我在印刷品中展示的更有表现力。
我采用了一种更实际、更能解决问题的方法,而不是每一节都涵盖一个主题或技术。到本书结束时,我们将已经开发出一个成熟的工作应用,但是我们将从一个 Hello World 示例开始。就像在一个真实的项目中一样,随着我们的进展,我们将为应用添加更多的功能。当我们这样做时,我们会遇到需要额外的概念或知识才能继续的任务。对于其中的每一个,我将介绍可以使用的概念或技术,我将详细讨论这一点。
因此,您可能不会发现每一章或每一节都专门针对一个主题或技术。一些章节可能集中在一项技术上,而其他章节可能针对我们希望在应用中实现的一系列目标。随着我们的进步,我们将在技术和工具之间切换。
我已经尽可能地包括练习,这使你要么思考,要么在互联网上查找各种资源。这是为了让您知道在哪里可以获得本书中没有涉及的其他信息,通常是非常高级的主题或 API。
我选择了一个问题跟踪应用作为我们将一起构建的应用。这是大多数开发人员都能理解的,同时具有任何企业应用都会有的许多属性和要求,通常称为“CRUD”应用(CRUD 代表数据库记录的创建、读取、更新和删除)。
书中使用的许多约定非常明显,所以我不会一一解释。我将只讨论一些关于如何构建各部分以及如何显示代码更改的约定,因为这不是很明显。
每章有多个部分,每个部分都致力于一组代码更改,这些代码更改会产生一个可以运行和测试的工作应用。一个部分可以有多个清单,但是它们中的每一个都不能单独测试。每个部分在 GitHub 存储库中都会有一个相应的条目,在那里您可以看到该部分结束时应用的完整源代码,以及前一部分和当前部分之间的差异。您会发现 difference 视图对于识别该部分中所做的更改非常有用。
所有代码更改将出现在该部分的列表中,但请不要依赖它们的准确性。可以在 GitHub 资源库中找到可靠且有效的代码,这些代码甚至可能在最后一刻发生了变化,无法及时印刷出版。所有列表都有一个列表标题,其中包括被更改或创建的文件的名称。
您可以使用 GitHub 资源库来报告印刷书籍中的问题。但是在你这样做之前,一定要检查现有的问题列表,看看是否有其他人报告了同样的问题。我会监控这些问题并发布解决方案,如果有必要,还会在 GitHub 库中更正代码。
如果一个列表包含一个文件、一个类、一个函数或一个完整的对象,那么它就是一个完整的列表。一个完整的列表也可以包含两个或更多的类、函数或对象,但不能包含多个文件。在这种情况下,如果实体不连续,我将使用省略号来表示未更改的代码块。
清单 1-1 是一个完整清单的例子,是整个文件的内容。
const express = require('express');
const app = express();
app.use(express.static('static'));
app.listen(3000, function () {
console.log('App started on port 3000');
});
Listing 1-1.server.js: Express Server另一方面,部分列表不会列出完整的文件、函数或对象。它将以省略号开始和结束,中间可能会有省略号以跳过未更改的代码块。添加的新代码将以粗体突出显示,未更改的代码将以正常字体显示。清单 1-2 是部分清单的一个例子,有一些小的增加。
...
"scripts": {
"compile": "babel src --presets react --out-dir static",
"watch": "babel src --presets react --out-dir static --watch",
"test": "echo \"Error: no test specified\" && exit 1"
},
...
Listing 1-2.package.json: Adding Scripts for Transformation删除的代码将使用删除线显示,如清单 1-3 所示。
...
"devDependencies": {
"babel-polyfill": "^6.13.0",
...
}
...
Listing 1-3.package.json: Changes for Removing Polyfill代码块在常规文本中用于提取代码中的变化以供讨论,通常是清单中代码的重复。这些不是清单,通常只有一两行。下面是一个例子,从清单中提取一行,突出显示一个单词:
...
const contentNode = ...
...所有需要在控制台上执行的命令都是以$开头的代码块形式。这里有一个例子:
$ npm install express书中使用的所有命令也可以在 GitHub 库的一个名为commands.md的文件中找到。这是为了在图书出版后纠正图书中的错误,同时也是一个更可靠的复制粘贴来源。同样,我们鼓励您不要复制粘贴这些命令,但是如果您因为发现某些东西不起作用而被迫这样做,那么请从 GitHub 库复制粘贴,而不是从书中的文本复制粘贴。
在本书后面的章节中,代码将被拆分到两个项目或目录中。为了区分应该在哪个目录下发出命令,命令块将以cd开始。例如,要在名为api的目录中执行一个命令,将使用以下命令:
$ cd api
$ npm install dotenv@6所有需要在 MongoDB shell 中执行的命令都是以>开头的代码块形式。例如:
> show collections这些命令也收集在一个文件中,在 GitHub 存储库中称为mongoCommands.md。
您将需要一台能够运行您的服务器并执行其他任务(如编译)的计算机。您还需要一个浏览器来测试您的应用。我推荐一台基于 Linux 的计算机,比如 Ubuntu,或者一台 Mac 作为你的开发服务器,但是稍加改动,你也可以使用 Windows PC。
直接在 Windows 上运行 Node.js 也可以,但是本书中的代码示例假设是基于 Linux 的 PC 或 Mac。如果您选择直接在 Windows PC 上运行,您可能需要进行适当的更改,特别是在 shell 中运行命令时,使用副本而不是使用软链接,在极少数情况下,还要处理路径分隔符中的\与/。
一种选择是尝试使用 vagger(https://www.vagrantup.com/)运行一个 Ubuntu 服务器虚拟机(VM)。这很有帮助,因为您最终将在基于 Linux 的服务器上部署您的代码,并且最好从一开始就习惯这种环境。但是你可能会发现编辑文件很难,因为在 Ubuntu 服务器中,你只有一个控制台。Ubuntu 桌面虚拟机可能更适合你,但是它需要更多的内存。
此外,为了保持本书的简洁,我没有包括软件包的安装说明,它们对于不同的操作系统是不同的。您需要遵循软件包提供商网站上的安装说明。在许多情况下,我没有包括网站的直接链接,我请你去看看。这是由于几个原因。首先是让你自己学习如何搜索这些。第二,由于在写这本书的时候 MERN 堆栈正在经历的快速变化,我提供的任何链接可能已经转移到另一个位置。
我将简要介绍构成 MERN 堆栈的主要组件,以及我们将用来构建 web 应用的其他一些库和工具。我将只谈一些突出的特点,而把细节留给更适合的其他章节。
React 锚定 MERN 堆栈。在某种意义上,这是 MERN 堆栈的定义组件。
React 是一个由脸书维护的开源 JavaScript 库,可用于创建以 HTML 呈现的视图。与 AngularJS 不同,React 不是一个框架。这是一个图书馆。因此,它本身并没有规定一个框架模式,比如 MVC 模式。您使用 React 来呈现视图(MVC 中的 V ),但是如何将应用的其余部分连接在一起完全取决于您。
不仅仅是脸书本身,还有许多其他公司在生产中使用 React,如 Airbnb、Atlassian、Bitbucket、Disqus、Walmart 等。GitHub 知识库上的 120,000 颗星星表明了它的受欢迎程度。
我将讨论 React 的几个突出特点。
脸书的人构建了 React 供自己使用,后来他们将其开源。现在,他们为什么要建立一个新的图书馆,而外面有成吨的图书馆呢?
React 并不是诞生于我们都看到的脸书应用,而是诞生于脸书的广告组织。最初,他们使用典型的客户端 MVC 模型,该模型具有所有常规的双向数据绑定和模板。视图会监听模型的变化,并通过更新自己来响应这些变化。
很快,随着应用变得越来越复杂,这变得非常棘手。将会发生的情况是,一个更改将导致一个更新,这将导致另一个更新(因为由于那个更新而发生了一些更改),这将导致另一个更新,以此类推。这种级联更新变得难以维护,因为根据更新的根本原因,更新视图的代码会有细微的差别。
然后他们想,当在视图中描述模型的所有代码都已经存在时,为什么我们还需要处理所有这些呢?我们不是通过添加越来越小的代码片段来管理转换来复制代码吗?为什么我们不能使用模板(也就是视图)本身来管理状态变化?
从那时起,他们开始考虑构建一些声明性的东西,而不是 T2 命令性的东西。
React 视图是声明性的。这实际上意味着,作为程序员,您不必担心管理视图状态或数据变化的影响。换句话说,您不必担心视图状态的变化导致的 DOM 转换或突变。声明性使得视图一致、可预测、更容易维护、更容易理解。处理过渡是别人的问题。
这是如何工作的?让我们比较一下 React 和传统方法(比如使用 jQuery)的工作原理。
给定数据,React 组件声明视图的外观。当数据发生变化时,如果您习惯了 jQuery 的工作方式,通常会进行一些 DOM 操作。例如,如果在一个表中插入了一个新行,您可以创建 DOM 元素并使用 jQuery 插入它。但不是在 React。你什么都不做!React 库计算出新视图的外观并呈现出来。
这样不会太慢吗?它不会导致每次数据更改时刷新整个屏幕吗?React 使用其虚拟 DOM 技术来处理这个问题。您声明视图的外观,React 用它构建一个虚拟表示,一个内存中的数据结构。我将在第二章中讨论更多,但是现在,只要把虚拟 DOM 看作一种中间表示,介于 HTML 和实际 DOM 之间。
当事情发生变化时,React 会基于新的事实(状态)构建一个新的虚拟 DOM,并将其与旧的(事情发生变化之前的)虚拟 DOM 进行比较。React 然后计算旧的和更改后的虚拟 DOM 之间的差异,然后将这些更改应用到实际的 DOM。
与用 jQuery 方式执行的手动更新相比,这只会增加很少的开销,因为计算虚拟 DOM 中差异的算法已经得到了最大程度的优化。因此,我们可以两全其美:不必担心实现转换,也不必担心最小变化的性能。
React 的基本构建块是一个维护自身状态并呈现自身的组件。
在 React 中,您所做的只是构建组件。然后,将组件放在一起,组成另一个组件来描述一个完整的视图或页面。组件封装了数据和视图的状态,或者它是如何呈现的。这使得整个应用的编写和推理变得更加容易,因为它被分割成多个组件,并且一次只关注一件事情。
组件通过以只读属性的形式将状态信息共享给它们的子组件,并通过回调它们的父组件来相互通信。我将在后面的章节中更深入地探讨这个概念,但是它的要点是 React 中的组件是非常内聚的,但是彼此之间的耦合是最小的。
许多 web 应用框架依赖模板来自动创建重复的 HTML 或 DOM 元素。这些框架中的模板语言是开发人员必须学习和练习的。不是在 React。
React 使用一种全功能编程语言来构造重复的或有条件的 DOM 元素。这种语言正是 JavaScript。例如,当你想构造一个表格时,你可以用 JavaScript 写一个for(...)循环或者使用Array的map()函数。
有一种中间语言来表示虚拟 DOM,那就是 JSX(JavaScript XML 的缩写),它非常像 HTML。这允许您用熟悉的语言创建嵌套的 DOM 元素,而不是使用 JavaScript 函数手工构造它们。注意,JSX 不是一种编程语言;它是一种像 HTML 一样的表示性标记。它也非常类似于 HTML,所以你不必学太多。稍后会详细介绍。
事实上,您不必使用 JSX——如果您愿意,您可以编写纯 JavaScript 来创建虚拟 DOM。但是如果你习惯于 HTML,用 JSX 会更简单。不过不用担心;这真的不是一门你需要学习的新语言。
React 也可以在服务器上运行。这就是同构的意思:相同的代码可以在服务器和浏览器上运行。这允许您在需要时在服务器上创建页面,例如,出于 SEO 目的。稍后我会在第 12 章中更详细地讨论这是如何工作的,这一章是关于服务器端渲染的。但是为了能够在服务器上运行 React 代码,我们确实需要能够运行 JavaScript 的东西,这就是我介绍 Node.js 的地方。
简单来说,Node.js 就是浏览器之外的 JavaScript。Node.js 的创建者只是采用了 Chrome 的 V8 JavaScript 引擎,并将其作为 JavaScript 运行时独立运行。如果您熟悉运行 Java 程序的 Java 运行时,您可以很容易地联想到 JavaScript 运行时:Node.js 运行时运行 JavaScript 程序。
虽然你可能会发现有人认为 Node.js 不适合生产使用,声称它是为浏览器设计的,但也有许多行业领导者选择了 Node.js。网飞、优步和 LinkedIn 是在生产中使用 Node.js 的几家公司,这应该可以作为一个健壮和可扩展的环境来运行任何应用的后端。
在浏览器中,您可以加载多个 JavaScript 文件,但是您需要一个 HTML 页面来完成所有这些工作。不能从一个 JavaScript 文件引用另一个 JavaScript 文件。但是对于 Node.js,没有 HTML 页面来启动这一切。在没有封装的 HTML 页面的情况下,Node.js 使用自己的基于 CommonJS 的模块系统将多个 JavaScript 文件放在一起。
模块就像图书馆。您可以通过使用require关键字(在浏览器的 JavaScript 中找不到)来包含另一个 JavaScript 文件的功能(假设它是按照模块的规范编写的)。因此,为了更好地组织,您可以将代码分成文件或模块,并使用require加载它们。我将在后面的章节中讨论确切的语法;在这一点上,需要注意的是,与浏览器上的 JavaScript 相比,使用 Node.js 有一种更简洁的模块化代码的方法。
Node.js 附带了许多编译成二进制文件的核心模块。这些模块提供对诸如文件系统、网络、输入/输出等操作系统元素的访问。它们还提供了一些大多数程序通常需要的实用函数。
除了您自己的文件和核心模块之外,您还可以找到大量第三方开源库,以便于安装。这就把我们带到了 npm。
npm 是 Node.js 的默认包管理器。您可以使用 npm 安装第三方库(包)并管理它们之间的依赖关系。npm 注册中心( www.npmjs.com )是人们出于共享目的发布的所有模块的公共存储库。
尽管 npm 最初是作为 Node.js 模块的存储库,但它很快转变为一个包管理器,用于交付其他基于 JavaScript 的模块,特别是那些可以在浏览器中使用的模块。jQuery 是目前最流行的客户端 JavaScript 库,它是一个 npm 模块。事实上,尽管 React 很大程度上是客户端代码,可以作为脚本文件直接包含在 HTML 中,但还是建议通过 npm 安装 React。但是一旦它作为一个包被安装,我们需要一些东西把所有的代码放在一起,这些代码可以包含在 HTML 中,这样浏览器就可以访问这些代码。为此,有一些构建工具,如 Browserify 或 Webpack,可以将您自己的模块以及第三方库放在一个包中,该包可以包含在 HTML 中。
在撰写本书时,npm 在模块或包存储库中名列前茅,拥有超过 450,000 个包(见图 1-1 )。Maven,两年前曾经是最大的,现在只有不到一半的数量。这表明 npm 不仅是最大的,而且是增长最快的存储库。人们常说 Node.js 的成功很大程度上归功于 npm 和围绕它涌现的模块生态系统。
图 1-1
各种语言的模块数量(来源: www.modulecounts.com )
npm 不仅易于创建和使用模块;它还有一个独特的冲突解决技术,允许一个模块的多个冲突版本并存,以满足依赖性。因此,在大多数情况下,npm 只是工作。
Node.js 有一个异步的、事件驱动的、非阻塞的输入/输出(I/O)模型,而不是使用线程来实现多任务。
大多数其他语言都依赖线程来同时处理事情。但事实上,当单个处理器运行您的代码时,并不存在并发这种情况。线程给人一种同时性的感觉,让其他代码段运行,而一个代码段等待(阻塞)某个事件完成。通常,这些是 I/O 事件,例如从文件中读取或通过网络进行通信。例如,在一行中,您调用打开一个文件,在下一行中,您已经准备好了文件句柄。真正发生的是,当文件被打开时,你的代码被阻塞(什么也不做)。如果有另一个线程正在运行,操作系统或语言将会切换出这段代码,并在阻塞期间开始运行其他代码。
另一方面,Node.js 没有线程。它依靠回调来让你知道一个挂起的任务已经完成。因此,如果您编写一行代码来打开一个文件,您可以为它提供一个回调函数来接收结果—文件句柄。在下一行,您继续做其他不需要文件句柄的事情。如果你习惯了异步 Ajax 调用,你会立刻明白我的意思。由于 JavaScript 的底层语言结构,如闭包,事件驱动编程对 Node.js 来说是很自然的。
Node.js 使用一个事件循环来实现多任务。这只是一个需要处理的事件队列,以及对这些事件运行的回调。在前面的例子中,准备读取的文件将是一个事件,它将触发您在打开它时提供的回调。如果你不完全明白这一点,也不用担心。本书其余部分的例子应该会让你对它真正的工作原理感到舒服。
一方面,基于事件的方法使 Node.js 应用变得更快,并让程序员能够愉快地忘记用于同步多线程事件的信号量和锁。另一方面,编写本质上异步的代码需要一些学习和实践。
Node.js 只是一个可以运行 JavaScript 的运行时环境。直接在 Node.js 上手工编写一个成熟的 web 服务器并不容易,也没有必要。Express 是一个简化编写服务器代码任务的框架。
Express 框架允许您定义路由,即当符合特定模式的 HTTP 请求到达时该做什么的规范。匹配规范是基于正则表达式(regex)的,非常灵活,就像大多数其他 web 应用框架一样。“做什么”部分只是一个函数,它被提供给解析后的 HTTP 请求。
Express 为您解析请求 URL、头和参数。在响应方面,正如预期的那样,它具有 web 应用所需的所有功能。这包括确定响应代码、设置 cookies、发送自定义标头等。此外,您可以编写 Express 中间件,即可以插入到任何请求/响应处理路径中的定制代码片段,以实现常见的功能,如日志记录、身份验证等。
Express 没有内置的模板引擎,但是它支持你选择的任何模板引擎,比如 pug、mustache 等。但是,对于 SPA,您不需要使用服务器端模板引擎。这是因为所有动态内容的生成都是在客户端完成的,而 web 服务器只通过 API 调用提供静态文件和数据。
总之,Express 是一个针对 Node.js 的 web 服务器框架。就您可以使用它实现的功能而言,它与许多其他 web 服务器框架没有太大的不同。
MongoDB 是 MERN 堆栈中使用的数据库。这是一个面向 NoSQL 文档的数据库,具有灵活的模式和基于 JSON 的查询语言。不仅许多现代公司(包括脸书和谷歌)在生产中使用 MongoDB,一些历史悠久的公司如 SAP 和苏格兰皇家银行也采用了 MongoDB。
我将在这里讨论 MongoDB 存在(和不存在)的几个问题。
NoSQL 代表“非关系型”,不管这个首字母缩略词扩展成什么。它本质上是而不是一个传统的数据库,在那里你有列和行的表,它们之间有严格的关系。我发现 NoSQL 数据库有两个区别于传统数据库的特征。
首先是它们通过将负载分布在多台服务器上进行水平扩展的能力。他们这样做是牺牲了传统数据库的一个重要方面:强一致性。也就是说,副本之间的数据不一定在很短的时间内保持一致。更多信息,请阅读“上限定理”( https://en.wikipedia.org/wiki/CAP_theorem )。但实际上,很少有应用需要 web 规模,NoSQL 数据库的这一方面也很少发挥作用。
第二,也是我认为更重要的一点,NoSQL 数据库不一定是关系数据库。你不必用表格的行和列来考虑你的对象。应用(对象)和磁盘(表格中的行)中的表示之间的差异有时被称为阻抗不匹配。这是一个从电气工程借来的术语,大概意思是,我们说的不是同一种语言。由于阻抗不匹配,我们必须使用一个层来转换或映射对象和关系。这些层被称为对象关系映射(ORM)层。
相反,在 MongoDB 中,您可以像在应用代码中一样看待持久化数据,即作为对象或文档。这有助于您避开 ORM 层,像在应用的内存中一样自然地考虑持久数据。
与数据以关系或表的形式存储的关系数据库相比,MongoDB 是一个面向文档的数据库。存储单位(相当于一行)是一个文档,或者一个对象,多个文档存储在集合(相当于一张表)。集合中的每个文档都有一个惟一的标识符,使用这个标识符可以访问它。标识符被自动索引。
想象一下发票的存储结构,包括客户姓名、地址等。以及发票中的项目(行)列表。如果您必须将它存储在一个关系数据库中,您将使用两个表,比如说,invoice和invoice_lines,其中的行或项目通过一个外键关系引用发票。在 MongoDB 中并非如此。您可以将整个发票存储为单个文档,获取它,并在原子操作中更新它。这不仅适用于发票中的行项目。文档可以是任何深度嵌套的对象。
现代关系数据库已经开始通过允许数组字段和 JSON 字段来支持一级嵌套,但它与真正的文档数据库并不相同。MongoDB 能够对深度嵌套的字段进行索引,这是关系数据库所不能做到的。
不利的一面是数据以非规范化的方式存储。这意味着数据有时会重复,需要更多的存储空间。此外,像重命名主(目录)条目名称这样的事情将意味着遍历数据库并更新所有重复数据。但话说回来,如今存储变得相对便宜,重命名主条目是罕见的操作。
在 MongoDB 数据库中存储对象并不一定要遵循规定的模式。集合中的所有文档不需要有相同的字段集。
这意味着,尤其是在开发的早期阶段,您不需要在模式中添加/重命名列。您可以在应用代码中快速添加字段,而不必担心数据库迁移脚本。乍一看,这似乎是一件好事,但实际上它所做的只是将数据完整性的责任从数据库转移到您的应用代码上。我发现在更大的团队和更稳定的产品中,最好有一个严格或半严格的模式。使用像 mongoose 这样的对象文档映射库(本书没有涉及)可以缓解这个问题。
MongoDB 的语言是 JavaScript。
对于关系数据库,我们有一种叫做 SQL 的查询语言。对于 MongoDB,查询语言是基于 JSON 的。通过在 JSON 对象中指定操作,可以创建、搜索、修改和删除文档。查询语言不像英语(你不用SELECT或说WHERE),因此更容易以编程方式构建。
数据也以 JSON 格式交换。事实上,为了有效地利用空间,数据被原生存储在一个叫做 BSON 的 JSON 变体中(其中 B 代表二进制)。当您从集合中检索文档时,它作为 JSON 对象返回。
MongoDB 附带了一个构建在 Node.js 等 JavaScript 运行时之上的 shell,这意味着您拥有了一个强大且熟悉的脚本语言(JavaScript)来通过命令行与数据库进行交互。您还可以用 JavaScript 编写代码片段,这些代码片段可以保存并在服务器上运行(相当于存储过程)。
如果不使用工具来帮助您,很难构建任何 web 应用。下面简单介绍一下除了 MERN 堆栈组件之外的其他工具,我们将使用它们来开发本书中的示例应用。
React 只为我们提供了视图呈现功能,并有助于管理单个组件中的交互。当涉及到在组件的不同视图之间转换并保持浏览器 URL 与视图的当前状态同步时,我们需要更多的东西。
这种管理 URL 和历史的能力被称为路由。它类似于 Express 所做的服务器端路由:解析一个 URL,并根据其组成部分,将一段代码与该 URL 相关联。React-Router 不仅可以做到这一点,还可以管理浏览器的Back按钮功能,这样我们就可以在看似页面的内容之间进行转换,而无需从服务器加载整个页面。我们可以自己构建这个,但是 React-Router 是一个非常易用的库,它为我们管理这个。
Bootstrap 是最流行的 CSS 框架,已经被改编为 React,该项目被称为 React-Bootstrap。这个库不仅为我们提供了大部分的 Bootstrap 功能,而且这个库提供的组件和部件也为我们提供了大量关于如何设计自己的部件和组件的信息。
还有其他为 React 构建的组件/CSS 库(如 Material-UI、MUI、Elemental UI 等。)和单个组件(如 react-select、react-treeview 和 react-date-picker)。所有这些都是很好的选择,取决于你想要达到的目标。但是我发现 React-Bootstrap 是最全面的单个库,并且熟悉 Bootstrap(我想大多数人已经知道了)。
当涉及到模块化代码时,这个工具是必不可少的。还有其他竞争工具,如 Bower 和 Browserify,它们也服务于模块化和捆绑所有客户端代码的目的,但是我发现 Webpack 更容易使用,并且不需要其他工具(如 gulp 或 grunt)来管理构建过程。
我们将使用 Webpack,不仅将客户端代码模块化并构建成一个包以交付给浏览器,还将“编译”一些代码。我们需要编译步骤来从用 JSX 编写的 React 代码生成纯 JavaScript。
很多时候,我们会觉得需要一个库来解决我们所有人都会面临的一个看似常见的问题。在本书中,我们将使用 body-parser(以 JSON 或表单数据的形式解析 POST 数据)和 ESLint(用于确保我们的代码遵循约定)等库,所有这些都在服务器端,还有一些类似 react-select 的库在客户端。
尽管我们不会将这些库作为本书的一部分使用,但是一些非常受欢迎的 MERN 堆栈的补充,并且一起使用的有:
-
这是一个状态管理库,也结合了 Flux 编程模式。它通常用在大型项目中,即使对于单个屏幕,管理状态也变得复杂。
-
mongose:如果你熟悉对象关系映射层,你可能会发现 mongose 有些类似。这个库在 MongoDB 数据库层上增加了一个抽象层次,让开发人员可以像这样查看对象。在处理 MongoDB 数据库时,该库还提供了其他有用的便利。
-
Jest :这是一个测试库,可以用来轻松测试 React 应用。
尽管 MERN 堆栈目前是一个健壮的、可用于生产的堆栈,但它的每个组件都在快速改进。在写这本书的时候,我已经使用了所有可用工具和库的最新版本。
但是毫无疑问,当你读到这本书的时候,很多东西都已经改变了,最新的版本将和我写这本书的时候用的不一样。因此,在安装任何软件包的说明中,我都包括了软件包的主要版本。
为了避免软件包变化带来的意外,请安装与书中提到的软件包相同的主要版本,而不是最新版本的软件包。
较小的版本变化应该是向后兼容的。例如,如果这本书使用了软件包的 4.1.0 版本,并且在安装时您获得了最新的 as 4.2.0,那么代码应该可以工作而无需任何更改。但是有很小的可能性,软件包的维护者在一个小版本升级中错误地引入了一个不兼容的变化。因此,如果您发现尽管从 GitHub 库复制粘贴了代码,但还是有问题,作为最后的手段,切换到书中使用的精确的版本,可以在库的根目录下的package.json文件中找到。
表 1-1 列出了作为本书一部分的重要工具和库的主要版本。
表 1-1
各种工具和库的版本
|成分
|
主要版本
|
评论
| | --- | --- | --- | | Node.js | Ten | 这是一个 LTS(长期支持)版本 | | 表达 | four | - | | MongoDB | Three point six | 社区版 | | React | Sixteen | - | | React 路由 | four | - | | ReactBootstrap | Zero point three two | 现在还没有 1.0 版本,1.0 可能会有问题 | | Bootstrap 程序 | three | 这与 React Bootstrap 程序 0(以及释放时的 1)兼容 | | 网页包 | four | - | | 斯洛文尼亚语 | five | - | | 巴比伦式的城市 | seven | - |
请注意,JavaScript 规范本身有许多版本,对各种版本和特性的支持因浏览器和 Node.js 而异。在本书中,我们将使用用于编译 JavaScript 的工具所支持的所有新特性,以达到最低的公分母:ES5。这组功能包括 ES2015 (ECMAScript 2015)、ES2016 和 ES2017 中的 JavaScript 功能。这些统称为 ES2015+。
现在,您对 MERN 堆栈及其组成有了一个大致的了解。但是它真的远远优于其他栈吗,比如说 LAMP,MEAN 等等。?无论如何,这些栈中的任何一个对于大多数现代 web 应用来说都足够好了。总而言之,熟悉是软件生产率的关键,所以我不建议一个 MERN 初学者盲目地在 MERN 上开始他们的新项目,尤其是如果他们有一个积极的截止日期。我建议他们选择他们已经熟悉的堆栈。
但是 MERN 确实有它特殊的地方。它非常适合前端内置大量交互性的 web 应用。回过头来再读一遍“为什么脸书建造 React”这一节,它会给你一些启发。你也许可以用其他的栈来达到同样的效果,但是你会发现用 MERN 来做是最方便的。所以,如果你有选择的余地,并且有时间熟悉一下,你会发现 MERN 是个不错的选择。我将谈谈我喜欢 MERN 的几件事,这些可能有助于你做出决定。
我最喜欢 MERN 的一点是,那里到处都使用单一的语言。我们对客户端代码和服务器端代码都使用 JavaScript。即使你有数据库脚本(在 MongoDB 中),你也是用 JavaScript 写的。所以,你唯一需要了解和熟悉的语言是 JavaScript。
后端的所有其他基于 MongoDB 和 Node.js 的栈都是如此,尤其是 MEAN 栈。但是让 MERN 技术栈脱颖而出的是,你甚至不需要知道一种生成页面的模板语言。在 React 方式中,以编程方式生成 HTML(实际上是 DOM 元素)的方式是使用 JavaScript。因此,您不仅避免了学习一门新语言,还获得了 JavaScript 的全部功能。这与模板语言不同,模板语言有其自身的局限性。当然,你需要了解 HTML 和 CSS,但是它们不是编程语言,你无法避免学习 HTML 和 CSS(不仅仅是标记,还有范例和结构)。
除了在编写客户端和服务器端代码时不必切换上下文的明显优势之外,跨层使用单一语言还可以让您在这些层之间共享代码。我能想到执行业务逻辑、进行验证等功能。可以分享的东西。它们需要在客户端运行,以便更好地响应用户输入,从而获得更好的用户体验。它们还需要在服务器端运行,以保护数据模型。
当使用 MERN 堆栈时,对象表示在任何地方都是 JSON (JavaScript 对象表示法)——在数据库中,在应用服务器上,在客户机上,甚至在网络上。
我发现这通常在转换方面为我节省了很多麻烦。没有对象关系映射(ORM),不必强制将对象模型放入行和列,没有特殊的序列化和反序列化代码。像 mongoose 这样的对象文档映射器(ODM)可能有助于实施一个模式,并使事情变得更加简单,但是底线是您可以节省大量的数据转换代码。
此外,它只是让我从本地对象的角度来考虑,甚至在使用 shell 直接检查数据库时,也将它们视为自然的自我。
由于其事件驱动的架构和非阻塞 I/O,Node.js 被认为是非常快速和有弹性的 web 服务器。
虽然需要一点时间来适应,但我毫不怀疑当您的应用开始扩展并接收大量流量时,这将在削减成本和节省花费在解决服务器 CPU 和 I/O 问题上的时间方面发挥重要作用。
我已经讨论了大量可供每个人免费使用的 npm 包。您面临的大多数问题都已经有了一个 npm 包作为解决方案。即使不完全符合你的需求,你也可以叉出来,自己做 npm 包。
npm 是在其他优秀的包管理人员的基础上发展起来的,因此它包含了许多最佳实践。我发现 npm 是迄今为止我用过的最容易使用和最快的包管理器。部分原因是由于 JavaScript 代码的紧凑性,大多数 npm 包都很小。
SPAs 曾经有过 SEO 不友好的问题,因为搜索引擎不会调用 Ajax 来获取数据或运行 JavaScript 代码来呈现页面。人们不得不使用变通方法,比如在服务器上运行 PhantomJS 来伪生成 HTML 页面,或者使用 Prerender.io 服务来为我们做同样的事情。这增加了复杂性。
有了 MERN 堆栈,在服务器之外提供完整的页面是很自然的事情,不需要事后才想到的工具。这是可能的,因为 React 运行在 JavaScript 上,这在客户端或服务器上都是一样的。当基于 React 的代码在浏览器上运行时,它从服务器获取数据,并在浏览器中构造页面(DOM)。这是呈现 UI 的 SPA 方式。如果我们想在服务器上为搜索引擎机器人生成相同的页面,可以使用相同的基于 React 的代码从 API 服务器获取数据,构建页面(这次是 HTML)并将其返回给客户端。这被称为服务器端渲染 (SSR)。
图 1-2 比较了这两种操作模式。使这成为可能的事实是,在服务器和浏览器中使用相同的语言来运行 UI 构造代码:JavaScript。这就是术语同构的含义:相同的代码可以在浏览器或服务器上运行。我们将在第 12 章中深入讨论 SSR 及其工作原理。在这一点上,足以理解的是,相同的代码可以在浏览器和客户端上运行,以实现两种不同的操作模式。
图 1-2
SPA 做事方式和使用 React 的服务器端渲染的比较
事实上,React Native 将它推向了另一个极端:它甚至可以生成移动应用的 UI。我没有在本书中介绍 React Native,但是这个事实应该会让您对 React 是如何构造的以及它将来能为您做什么有所了解。
没有多少人喜欢或欣赏这一点,但我真的很喜欢 React 是一个库,而不是一个框架。
一个框架是固执己见的;它有一套做事的方法。这个框架要求你填写它认为我们所有人都想完成的事情。另一方面,库为您提供了构建应用的工具。从短期来看,一个框架帮助很大,因为它去掉了大部分标准的东西。但是随着时间的推移,框架的变幻莫测,它对我们想要完成的事情的假设,以及学习曲线会让你希望你能对正在发生的事情有所控制,特别是当你有一些特殊的需求时。
有了函数库,有经验的架构师可以完全自由地设计自己的应用,从函数库的功能中挑选,并构建自己的框架,以满足应用的独特需求和变化。因此,对于有经验的架构师或非常独特的应用需求,库更好,尽管框架可以让您快速入门。
这本书让你体验使用 MERN 堆栈开发一个应用需要什么,是什么样子。
在这一章中,我们讨论了使 MERN 成为任何 web 应用的令人信服的选择的原因,其中包括单一编程语言在整个堆栈中的优势、NoSQL 数据库的特性以及 React 的同构。我希望这些理由能够说服您尝试 MERN 堆栈,如果不采用它的话。
这本书鼓励你去做,去思考,去实验,而不仅仅是阅读。因此,请记住以下提示,以便您能从本书中获得最大收益:
-
避免从书中或 GitHub 库中复制粘贴代码。相反,你可以自己输入代码。只有当你陷入困境,发现事情并不像预期的那样工作时,才求助于复制粘贴。
-
使用 GitHub 资源库(
https://github.com/vasansr/pro-mern-stack-2)查看代码列表和变更;因为 GitHub 显示差异的方式,所以更方便。 -
不要依赖书中代码列表的准确性,而是依赖 GitHub 库中的代码。如果你不得不复制粘贴,那么从 GitHub 库开始,而不是从书上。
-
使用本书中使用的相同版本的包和工具,而不是最新版本。最新版本和本书中的版本可能会有差异,这可能会导致问题的出现。
-
不要跳过练习:这些练习旨在让你思考并了解到哪里去寻找更多的资源。
最后,我希望您对了解 MERN 堆栈感到非常兴奋。因此,我们将在下一章直接进入代码,创建最基本的应用:Hello World 应用。

