Vue-和-Node-全栈-Web-开发-全-

Vue 和 Node 全栈 Web 开发(全)

原文:zh.annas-archive.org/md5/1432c32f9d6dcbddfc63da998dd364ec

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

JavaScript 已经成为当今和未来最重要的语言之一。

在过去几年中,JavaScript 的崛起非常迅速,它已成为现代网络应用开发中的强大语言。

MEVN 是除了 MEAN 和 MERN 之外,用于开发现代网络应用的一种技术栈。本书提供了一种逐步构建全栈网络应用程序的方法,使用 MEVN 技术栈,即 MongoDB、Express.js、Vue.js 和 Node.js。

本书将提供 Node.js 和 MongoDB 的基本概念,接着是构建 Express.js 应用程序并实现 Vue.js。

在这本书中,我们将涵盖以下内容:

  • 了解技术栈——MongoDB、Node.js、Express.js 和 Vue.js

  • 构建 Express.js 应用程序

  • 学习什么是 REST API 以及如何实现它们

  • 学习如何在 Express.js 应用程序中将 Vue.js 作为前端层使用

  • 在应用程序中添加一个认证层

  • 添加自动化脚本和测试

这本书面向的对象

这本书是为对学习如何使用 JavaScript 作为唯一编程语言构建全栈应用程序感兴趣的网页开发者而设计的,使用的技术栈是:MongoDB、Express.js、Vue.js 和 Node.js。

这本书适合对 HTML、CSS 和 JavaScript 有基本知识的初学者和中级开发者。如果你是网页或全栈 JavaScript 开发者,并且已经尝试过传统的技术栈,如 LAMP、MEAN 或 MERN,并希望探索一个使用现代网络技术的全新技术栈,那么这本书适合你。

这本书涵盖的内容

第一章,MEVN 简介,介绍了 MEVN 栈以及构建应用程序基础所需的不同工具的安装。

第二章,构建 Express 应用程序,介绍了 Express.js,解释了 模型视图控制器MVC)结构的概念,并展示了如何使用 Express.js 和 MVC 结构设置应用程序。

第三章,MongoDB 简介,重点介绍了 Mongo 及其查询,介绍了 Mongoose 以及使用 Mongoose 进行 创建读取更新删除CRUD)操作的性能。

第四章,REST API,介绍了 REST 架构以及 RESTful API 的概念。本章还介绍了不同的 HTTP 动词以及开发 REST API。

第五章,构建真实应用程序,介绍了 Vue.js 并展示了如何使用 MEVN 中的所有技术构建一个完全工作的动态应用程序。

第六章,使用 Passport.js 进行身份验证,讨论了 Passport.js,并描述了如何实现 JWT 和本地策略以在应用程序中添加身份验证层。

第七章,Passport.js OAuth 策略,介绍了 OAuth 策略,并指导您实现 Facebook、Twitter、Google 和 LinkedIn 的 Passport.js 策略。

第八章,Vuex 简介,介绍了 Vuex 的核心概念——状态、获取器、突变和动作。它还描述了您如何在应用程序中实现它们。

第九章,测试 MEVN 应用程序,解释了单元测试和端到端测试是什么,并指导您为应用程序的不同方面编写单元测试和自动化测试。

第十章,Go Live,解释了什么是持续集成,并指导您如何设置与应用程序的持续集成服务,并在 Heroku 上部署应用程序。

要充分利用本书

如果您具备以下技能,本书将更有益:

  • 了解 HTML、CSS 和 JavaScript

  • 了解 Vue.js 和 Node.js 是一个加分项

  • 了解如何使用 MEAN 和 MERN 堆栈构建 Web 应用程序是一个加分项

下载示例代码文件

您可以从 www.packtpub.com 的账户下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问 www.packtpub.com/support 并注册,以便将文件直接通过电子邮件发送给您。

您可以通过以下步骤下载代码文件:

  1. www.packtpub.com 登录或注册。

  2. 选择支持选项卡。

  3. 点击代码下载与勘误表。

  4. 在搜索框中输入本书的名称,并遵循屏幕上的说明。

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

  • Windows 上的 WinRAR/7-Zip

  • Mac 上的 Zipeg/iZip/UnRarX

  • 7-Zip/PeaZip for Linux

本书代码包也托管在 GitHub 上,网址为 github.com/PacktPublishing/Full-Stack-Web-Development-with-Vue.js-and-Node。如果代码有更新,它将在现有的 GitHub 仓库中更新。

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

使用的约定

本书使用了多种文本约定。

CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“一个模块是可以由 Node.js 使用require命令加载的,并且有一个命名空间。模块与其关联的package.json文件。”

代码块设置如下:

extends layout

block content
  h1= title
  p Welcome to #{title}

当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:

var index = require('./routes/index');
var users = require('./routes/users');

var app = express();

// Require file system module
var fs = require('file-system');

任何命令行输入或输出都应如下编写:

$ mkdir css
$ cd css

粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。以下是一个示例:“只需点击“继续”,直到安装完成。”

警告或重要注意事项看起来像这样。

小贴士和技巧看起来像这样。

联系我们

我们始终欢迎读者的反馈。

一般反馈:请发送电子邮件至 feedback@packtpub.com,并在邮件主题中提及书籍标题。如果您对本书的任何方面有疑问,请发送电子邮件至 questions@packtpub.com

勘误:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将不胜感激,如果您能向我们报告,我们将不胜感激。请访问 www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。

盗版:如果您在互联网上发现我们作品的任何非法副本,我们将不胜感激,如果您能提供位置地址或网站名称,我们将不胜感激。请通过链接至材料的方式与我们联系 copyright@packtpub.com

如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问 authors.packtpub.com

评论

请留下评论。一旦您阅读并使用过这本书,为什么不在您购买它的网站上留下评论呢?潜在读者可以查看并使用您的客观意见来做出购买决定,Packt 可以了解您对我们产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!

如需更多关于 Packt 的信息,请访问 packtpub.com

第一章:MEVN 简介

Mongo, Express, Vue.js, and Node.jsMEVN)是一组 JavaScript 技术,就像MongoDBExpressAngularNode.jsMEAN)一样,以及像MongoDBExpressReactNode.jsMERN)一样。它是一个全栈解决方案,用于构建使用 MongoDB 作为数据存储、Express.js 作为后端框架(该框架建立在 Node.js 之上)、Vue.js 作为前端 JavaScript 框架以及 Node.js 作为后端主要引擎的基于 Web 的应用程序。

本书面向对使用 MongoDB、Express.js、Vue.js 和 Node.js 构建全栈 JavaScript 应用程序感兴趣的 Web 开发者。它适合对 HTML、CSS 和 JavaScript 有基本知识的初学者和中级开发者。

MEVN 这个术语可能很新,但其中使用的技术并不新。这里介绍的唯一新技术是 Vue.js。Vue.js 是一个开源的 JavaScript 框架,其受欢迎程度正在迅速增长。Vue.js 的学习曲线并不陡峭,它也是 AngularJS 和 ReactJS 等其他 JavaScript 框架的激烈竞争对手。

现代 Web 应用程序需要快速且易于扩展。在过去,JavaScript 仅在需要添加一些正常 HTML 和 CSS 无法实现的视觉效果或动画时才用于 Web 应用程序。但今天,JavaScript 已经改变了。今天,JavaScript 几乎用于所有基于 Web 的应用程序,从小型到大型应用程序。当应用程序需要更快和更交互式时,会选择 JavaScript。

使用 JavaScript 作为唯一编程语言构建全栈应用程序有其自身的好处:

  • 如果你刚开始学习编程,你只需要掌握一种语言:JavaScript。

  • 全栈工程师需求量大。成为一名全栈开发者意味着你了解数据库的工作原理,你知道如何构建后端和前端,并且你还具备 UI/UX 的技能。

在本书中,我们将使用这些技术栈来构建应用程序。

本章我们将涵盖以下主题:

  • MEVN 技术栈简介

  • Node.js 及其在 Windows、Linux 和 macOS 上的安装简介

  • npm及其安装概述

  • MongoDB 及其安装简介以及 MongoDB 中的一些基本命令

  • GitHub 版本控制简介及其如何帮助软件工程师轻松访问代码历史和协作

JavaScript 技术栈的演变

JavaScript 是当今最重要的编程语言之一。由 Brendan Eich 于 1995 年创立,它做得非常出色,不仅保持了其地位,而且还在所有其他编程语言之上脱颖而出。

JavaScript 的流行度一直在增长,而且似乎没有尽头。以 JavaScript 作为唯一编程语言构建 Web 应用程序一直很受欢迎。随着这种快速的增长速度,软件工程师对 JavaScript 的知识需求也在不断增加。无论你选择哪种编程语言来精通,JavaScript 总是以某种方式渗透进来,与其他编程语言一起参与,无论方式如何。

在开发应用程序时,前端和后端有很多技术可供选择。虽然这本书使用 Express.js 作为后端框架,但还有其他框架可供学习,如果你愿意的话。

其他可用的后端框架包括 Meteor.jsSails.jsHapi.jsMojitoKoa.js 以及更多。

类似地,对于前端,技术包括 Vue.jsReactAngularBackbone 以及更多。

对于数据库,除了 MongoDB 之外,还有 MySQLPostgreSQLCassandra 以及其他选项。

介绍 MEVN

JavaScript 框架正日益增多,无论是数量还是使用频率都在上升。JavaScript 最初仅用于客户端逻辑实现,但经过多年的发展,现在它既用于前端也用于后端。

在 MEVN 堆栈中,Express.js 用于管理所有与后端相关的内容,Vue.js 处理所有与视图相关的内容。使用 MEVN 堆栈的优势如下:

  • 整个应用程序中只使用一种语言,这意味着你只需要了解 JavaScript。

  • 使用一种语言理解客户端和服务器端非常简单。

  • 这是一个非常快速且可靠的应用程序,具有 Node.js 的非阻塞 I/O。

  • 这是一种很好的方式,可以保持对 JavaScript 不断增长的生态系统的了解。

安装 Node.js

要开始,我们需要添加 MEVN 堆栈应用程序所需的全部依赖项。我们还可以参考官方网站(nodejs.org/)上的文档,了解如何在任何操作系统上安装 Node.js 的详细信息。

在 macOS 上安装 Node.js

在 macOS 上安装 Node.js 有两种方式:使用安装程序或使用 bash。

使用安装程序安装 Node.js

要使用安装程序安装 Node.js,请执行以下步骤:

  1. 安装安装程序:我们可以从官方网站的下载页面(nodejs.org/en/#download)下载 macOS 的安装程序。我们将安装最新的 node 版本,即 10.0.0。你可以安装任何你想要的 node 版本,但我们在本书中构建的应用程序将需要 node 版本 >= 6.0.0。运行安装程序并遵循给出的说明。当我们下载并运行安装程序时,我们将看到一个如下对话框:

图片

  1. 点击继续,直到安装完成。一旦安装完成,我们将能够看到以下对话框:

图片

只需点击关闭,我们就会完成。

使用 bash 安装 Node.js

Node.js 可以很容易地使用 macOS 中的 Homebrew 安装。Homebrew 是一个免费的开源软件包管理器,用于在 macOS 上安装软件。我个人更喜欢 Homebrew,因为它使得在 Mac 上安装不同的软件变得非常容易:

  1. 要安装Homebrew,请输入以下命令:
$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
  1. 现在,使用以下命令通过Homebrew安装 Node.js:
$ brew install node

在 Linux 上安装 Node.js

对于 Linux,我们可以安装 Node.js 的默认发行版,或者我们可以从 NodeSource 下载它以使用最新版本。

从默认发行版安装 Node.js

要从默认发行版安装,我们可以使用以下命令在 Linux 上安装 Node.js:

$ sudo apt-get install -y nodejs

从 NodeSource 安装 Node.js

要从 NodeSource 安装 Node.js,请执行以下步骤:

  1. 首先从 NodeSource 下载最新版本的 Node.js:
$ curl -sL https://deb.nodesource.com/setup_9.x | sudo -E bash 
  1. 然后,使用以下命令安装 Node.js:
$ sudo apt-get install -y nodejs

apt是高级包工具的简称,用于在 Debian 和 Linux 发行版上安装软件。基本上,这相当于 macOS 中的 Homebrew 命令。

在 Windows 上安装 Node.js

我们可以通过以下步骤在 Windows 上安装 Node.js:

  1. 从官方网站下载 Node.js 安装程序(nodejs.org/en/download/)。

  2. 运行安装程序并按照给定的说明操作。

  3. 点击关闭/完成按钮。

通过安装程序在 Windows 上安装 Node.js 几乎与在 macOS 上相同。一旦我们下载并运行安装程序,我们将看到一个对话框。只需点击继续,直到安装完成。当我们最终看到一个带有确认的对话框时,我们点击关闭。Node.js 将被安装!

介绍 NVM

NVM代表Node 版本管理器。NVM 跟踪我们安装的所有node版本,并允许我们在不同版本之间切换。当我们为 Node.js 的一个版本构建的应用程序与另一个版本不兼容时,我们需要那个特定的node版本来使事情正常工作,这非常有用。NVM 使我们能够轻松管理这些版本。当我们需要升级或降级node版本时,这也非常有帮助。

从 NVM 安装 Node.js

  1. 要下载 NVM,请使用以下命令:
$ curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.0/install.sh | bash
  1. 我们也可以使用以下命令:
$ wget -qO- https://raw.githubusercontent.com/creationix/nvm/v0.33.6/install.sh | bash
  1. 使用以下命令检查nvm是否已成功安装:
$ nvm --version 
  1. 现在,要使用nvm安装node,请使用以下命令:
$ nvm install node

介绍 npm

npm 是 Node Package Manager 的缩写。基本上,它是一个负责我们为 Node.js 安装的所有包的工具。我们可以在官方网站 (www.npmjs.com/) 上找到所有现有的包。npm 使开发者能够轻松地保持他们的代码更新,并重用许多其他开发者共享的代码。

开发者经常被包和模块这两个术语所困惑。然而,这两个术语之间有一个清晰的区分。

模块

模块是可以通过 Node.js 的 require 命令加载并具有命名空间的东西。一个模块会关联一个 package.json 文件。

一个 package 只是一个文件,或者一组文件,它能够独立运行。每个包也都有一个 package.json 文件,其中包含描述该包的所有相关元数据信息。模块的组合构成了一个 node 包。

安装 npm

当我们从安装程序本身安装 Node.js 时,npm 作为 node 的一部分被安装。我们可以通过使用以下命令来检查 npm 是否已安装:

$ npm --version

如果 npm 未安装,命令会显示错误,而如果已安装,它只会打印出已安装的 npm 版本。

使用 npm

npm 用于在我们的应用程序中安装不同的包。安装包有两种方式:本地和全局。当我们想要安装针对我们应用程序的特定包时,我们希望将其本地安装。然而,如果我们想要将某个包用作命令行工具或能够在我们应用程序外部访问它,我们则希望将其作为全局包安装。

本地安装 npm 包

要仅安装针对我们应用程序的特定包,我们可以使用以下命令:

$ npm install <package_name> --save

全局安装 npm 包

要全局安装包,我们可以使用以下命令:

 $ npm install -g <package_name>

介绍 package.json

所有的 node 包和模块都包含一个名为 package.json 的文件。这个文件的主要功能是携带与该包或模块相关的所有元信息。一个 package.json 文件要求内容是一个 JSON 对象。

至少,一个 package.json 文件包含以下内容:

  • name:包的名称。这是 package.json 文件的一个重要部分,因为它主要是区分其他包的东西,因此是一个必填字段。

  • version:包的版本。这也是一个必填字段。为了能够安装我们的包,需要提供 nameversion 字段。

  • description:包的简要总结。

  • main:这是用于查找包的主要入口点。基本上,它是一个文件路径,因此当用户安装此包时,它会知道从哪里开始查找模块。

  • 脚本:此字段包含可以在应用程序的各种状态下运行的命令。它由键值对组成。key 是命令应该运行的触发事件,而 value 是实际的命令。

  • 作者/贡献者:作者和贡献者是人们。它包含一个个人标识符。作者是一个单独的人,而贡献者可以是一组人。

  • 许可协议:当提供许可字段时,使用户能够轻松使用我们的包。这有助于在使用包时识别权限和限制。

创建一个 package.json 文件

我们可以手动创建一个 package.json 文件并自行指定选项,或者我们可以使用命令从命令提示符交互式地创建它。

让我们继续使用 npm 初始化一个带有 package.json 的示例应用程序。

首先,使用以下命令在项目目录中创建一个文件夹:

$ mkdir testproject

要创建一个 package.json 文件,请在我们创建的应用程序中运行以下命令:

$ npm init

运行此命令将向我们提出一系列问题,我们可以通过命令行交互式地回答这些问题:

图片

最后,它将创建一个 package.json 文件,其中将包含以下内容:

图片

安装 MongoDB

MongoDB 是 MEVN 技术栈中的第一个部分。MongoDB 是一个在 GNU 许可下发布的免费和开源文档型数据库。它是一个 NoSQL 数据库,这意味着它是一个非关系型数据库。与使用表和行来表示数据的关系型数据库不同,MongoDB 使用集合和文档。MongoDB 将数据表示为 JSON 文档的集合。它为我们提供了以任何我们想要的方式添加字段的灵活性。单个集合中的每个文档都可以有完全不同的结构。除了添加字段外,它还提供了以任何我们想要的方式从文档到文档更改字段的灵活性,这在关系型数据库中是一个繁琐的任务。

与关系型数据库管理系统(RDBMS)相比,MongoDB 的优势

与关系型数据库管理系统(RDBMS)相比,MongoDB 提供了许多好处:

  • 无模式架构:MongoDB 不要求我们为其集合设计特定的模式。一个文档的模式可以不同,另一个文档则可能完全不同。

  • 每个文档都存储在 JSON 结构化格式中。

  • 查询和索引 MongoDB 非常容易。

  • MongoDB 是一个免费和开源的程序。

在 macOS 上安装 MongoDB

安装 MongoDB 有两种方式。我们可以从官方 MongoDB 网站下载它(www.mongodb.org/downloads#production),或者我们可以使用 Homebrew 来安装它。

通过下载安装 MongoDB

  1. www.mongodb.com/download-center#production 下载您想要的 MongoDB 版本。

  2. 将下载的 gzipped 文件复制到根目录。将其添加到根目录将允许我们全局使用它:

 $ cd Downloads $ mv mongodb-osx-x86_64-3.0.7.tgz ~/
  1. 解压 gzipped 文件:
 $ tar -zxvf mongodb-osx-x86_64-3.0.7.tgz
  1. 创建一个目录,该目录将由 Mongo 用于保存数据:
 $ mkdir -p /data/db
  1. 现在,为了检查安装是否成功,启动 Mongo 服务器:
 $ ~/mongodb/bin/mongod

在这里,我们已经成功安装并启动了 mongo 服务器。

通过 Homebrew 安装 MongoDB

要在 macOS 上使用 Homebrew 安装 MongoDB,请按照以下步骤操作:

  1. 使用 Homebrew,我们只需要一个命令来安装 MongoDB:
$ brew install mongodb
  1. 创建一个目录,该目录将由 Mongo 用于保存数据:
 $ sudo mkdir -p /data/db
  1. 启动 Mongo 服务器:
 $ ~/mongodb/bin/mongod 

因此,MongoDB 最终已安装。

在 Linux 上安装 MongoDB

在 Linux 上安装 MongoDB 也有两种方式:我们可以使用 apt-get 命令,或者我们可以下载 tarball 并解压它。

使用 apt-get 安装 MongoDB

要使用 apt-get 安装 MongoDB,请执行以下步骤:

  1. 运行以下命令以安装 MongoDB 的最新版本:
 $ sudo apt-get install -y mongodb-org
  1. 通过运行以下命令来验证 mongod 是否已成功安装:
 $ cd /var/log/mongodb/mongod.log
  1. 要启动 mongod 进程,请在终端中执行以下命令:
 $ sudo service mongod start
  1. 查看日志文件是否有表示 MongoDB 连接成功建立的行:
 $ [initandlisten] waiting for connections on port<port>
  1. 要停止 mongod 进程:
 $ sudo service mongod stop
  1. 要重新启动 mongod 进程:
 $ sudo service mongod restart

使用 tarball 安装 MongoDB

  1. www.mongodb.com/download-center?_ga=2.230171226.752000573.1511359743-2029118384.1508567417 下载二进制文件。使用以下命令:
 $ curl -O https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-
 3.4.10.tgz
  1. 解压下载的文件:
 $ tar -zxvf mongodb-linux-x86_64-3.4.10.tgz
  1. 复制并解压到目标目录:
 $ mkdir -p mongodb $ cp -R -n mongodb-linux-x86_64-3.4.10/ mongodb
  1. 在 PATH 变量中设置二进制文件的位置:
 $ export PATH=<mongodb-install-directory>/bin:$PATH
  1. 创建一个目录,供 Mongo 用于存储所有数据库相关数据:
 $ mkdir -p /data/db
  1. 要启动 mongod 进程:
 $ mongod

在 Windows 上安装 MongoDB

从安装程序安装 MongoDB 与在 Windows 上安装任何其他软件一样简单。就像我们为 Node.js 所做的那样,我们可以从官方网站 (www.mongodb.com/download-center#atlas) 下载 MongoDB 安装程序。这将下载一个可执行文件。

一旦可执行文件下载完成,运行安装程序并按照说明操作。只需浏览对话框,仔细阅读说明。安装完成后,只需点击“关闭”按钮即可完成。

使用 MongoDB

让我们更深入地了解 MongoDB。正如之前提到的,Mongo 包含一个数据库,其中包含集合(表格/数据组)和文档(行/条目/记录)。我们将使用 MongoDB 提供的一些命令来创建、更新和删除文档:

首先,使用以下命令启动 Mongo 服务器:

$ mongod

然后,使用以下命令打开 Mongo shell:

$ mongo

创建或使用 MongoDB 数据库

这是我们可以看到所有数据库、集合和文档的地方。

要显示我们拥有的数据库列表,我们可以使用以下命令:

> show dbs

现在,此命令应该列出所有现有数据库。要使用我们想要的数据库,我们可以简单地运行此命令:

> use <database_name>

但是,如果没有列出数据库,请不要担心。MongoDB 为我们提供了一个功能,当我们运行前面的命令时,即使该数据库不存在,它也会自动为我们创建一个具有给定名称的数据库。

因此,如果我们已经有一个想要使用的数据库,我们只需运行该命令,如果没有数据库,我们将使用此命令创建一个:

> use posts

当我们运行此命令时,将创建一个名为posts的数据库。

创建文档

现在,让我们快速回顾一下在 MongoDB 中使用的命令。insert命令用于在 MongoDB 中的集合中创建新文档。让我们向刚刚创建的名为posts的数据库中添加一条新记录。

在这里也是如此,为了向集合中添加文档,我们首先需要一个集合,我们还没有。但是 MongoDB 允许我们通过运行insert命令轻松创建集合。同样,如果集合存在,它将文档添加到指定的集合中,如果集合不存在,它将简单地创建一个新的集合。

现在,在 Mongo shell 中,运行以下命令:

> db.posts.insertOne({
 title: 'MEVN',
 description: 'Yet another Javascript full stack technology'
});

此命令将在posts数据库中创建一个名为posts的新集合。此命令的输出是:

它将返回一个 JSON 对象,其中包含我们在insertedId键中创建的文档的 ID,以及事件被接收为acknowledged的标志。

获取文档

此命令用于当我们想要从一个集合中获取记录时。我们可以通过传递参数来获取所有记录或特定文档。我们可以向posts数据库添加一些更多文档,以便更好地学习该命令

获取所有文档

要获取posts集合中的所有记录,请运行以下命令:

> db.posts.find()

这将返回posts集合中我们拥有的所有文档:

获取特定文档

让我们找到一个标题为MEVN的帖子。为此,我们可以运行:

> db.posts.find({ 'title': 'MEVN' }) 

此命令将只返回标题为MEVN的文档:

更新文档

此命令用于当我们想要更新集合的某个部分时。比如说,我们想要更新标题为Vue.js的帖子的描述;我们可以运行以下命令:

> db.posts.updateOne(
 { "title" : "MEVN" },
 { $set: { "description" : "A frontend framework for Javascript programming language" } }
 )

此命令的输出将是:

我们可以看到这里matchedCount1,这意味着在posts集合中有一个文档与我们要更新的标题MEVN的查询参数相匹配。

另一个关键值modifiedCount给出了更新文档的数量。

删除文档

delete命令用于从集合中删除文档。从 MongoDB 中删除文档有几种方法。

删除符合给定条件的文档

要删除具有特定条件的所有文档,我们可以运行:

> db.posts.remove({ title: 'MEVN' })

此命令将从posts集合中删除所有标题为MEVN的文档。

删除符合给定条件的单个文档

要删除仅满足给定条件的第一个记录,我们只需使用:

> db.posts.deleteOne({ title: 'Expressjs' })

删除所有记录

要删除集合中的所有记录,我们可以使用:

> db.posts.remove({})

介绍 Git

Git 是我们应用程序中跟踪代码变更的版本控制系统。它是一个免费的开源软件,用于在构建应用程序时跟踪和协调多个用户。

要开始使用此软件,我们首先需要安装它。在每种操作系统上安装它都非常简单。

在 Windows 上安装 Git

我们可以在gitforwindows.org/找到 Git for Windows 的安装程序。

下载 Windows 的可执行安装文件并按照相应的步骤进行操作。

在 Mac 上安装 Git

我们可以轻松通过 Homebrew 在 Mac 上安装 Git。只需在命令行中输入以下命令即可在 Mac 上安装 Git:

$ brew install git 

在 Linux 上安装 Git

在 Linux 上安装 Git 与在 macOS 上安装 Git 一样简单。只需在命令行中输入以下命令并按回车键即可在 Linux 上安装 Git:

$ sudo apt-get install git

介绍 GitHub

GitHub 是一个版本控制系统。它是一个专门设计用来跟踪代码变更的源代码管理工具。GitHub 还提供了社交网络功能,例如添加评论和显示动态,这使得它更加强大,因为多个开发者可以在单个应用程序中同时协作。

为什么选择 GitHub?

GitHub 是软件工程师的救星。GitHub 提供了许多优势,使其值得使用。以下是 GitHub 提供的一些好处:

  • 跟踪代码变更:GitHub 帮助跟踪代码变更,这意味着它维护了我们代码的历史记录。这使得我们能够查看在任何时间段内对代码库进行的修订。

  • 文档:GitHub 为我们提供了添加文档、维基等功能,这些可以使用简单的 Markdown 语言编写。

  • 图表和报告:GitHub 提供了对各种指标的洞察,包括对代码进行了多少次添加和删除,谁是主要贡献者,以及谁提交最多。

  • 错误跟踪:由于 GitHub 跟踪每个时间点的所有活动,当出现问题时,我们可以轻松回溯到导致代码中断的点。我们还可以集成第三方工具,如 Travis 进行持续集成,这有助于我们轻松跟踪和识别错误。

  • 协作简单:GitHub 跟踪项目中每个协作者的所有活动,并为此发送电子邮件通知。它还提供社交媒体功能,如动态、评论、表情符号和提及。

  • 托管自己的网站:我们还可以使用 GitHub 的 GitHub Pages 功能托管自己的网站。我们只需要为我们的项目创建一个仓库,并使用 GitHub Pages 托管它,这样网站就会适用于 URL:https://<username>.github.io

使用 GitHub

GitHub 非常易于使用。然而,要开始使用 GitHub,我们至少需要了解一些在 GitHub 中使用的术语:

  • 仓库/Repo:仓库是我们所有代码库存储的地方。仓库可以是私有的或公共的。

  • ssh-key:ssh-key 是 GitHub 中授权的一种方式。它存储我们的身份信息。

  • 分支:分支可以被定义为仓库的多个状态。任何仓库的主要分支是master分支。多个用户可以在不同的分支上并行工作。

  • 提交:提交使得在给定时间区分文件的不同状态变得容易。当我们提交时,会为该提交分配一个唯一的标识符,这样我们就可以轻松检查在该提交中做了哪些更改。提交以消息作为参数,描述正在进行的更改类型。

  • 推送:推送将我们做出的提交发送回我们的仓库。

  • 拉取:与推送相反,拉取是从远程仓库将提交拉取到我们的本地项目。

  • 合并:合并基本上是在多个分支之间进行的。它用于将一个分支的更改应用到另一个分支。

  • 拉取请求:创建一个pull request基本上是将我们对我们代码库所做的更改发送给其他开发者的审批。我们可以在pull request上开始讨论,以检查代码质量并确保更改不会破坏任何东西。

要了解更多关于 GitHub 中使用的词汇,请访问help.github.com/articles/github-glossary/

设置 GitHub 仓库

现在我们已经了解了 GitHub 的基础知识,让我们开始创建我们想要构建的项目 GitHub 仓库:

  1. 首先,在根目录下为应用程序创建一个文件夹。让我们把这个应用程序命名为blog
 $ mkdir blog
  1. github.com/上创建一个 GitHub 账户。

  2. 前往您的个人资料。在“仓库”选项卡下,点击“新建”,如下所示:

图片

  1. 将此仓库命名为blog

  2. 现在,在终端中,转到此应用程序的位置,并使用以下命令初始化一个空的仓库:

 $ cd blog $ git init
  1. 现在,让我们创建一个名为README.md的文件,为应用程序编写描述,然后保存它:
 $ echo 'Blog' > README.md 
  1. 将此文件添加到 GitHub:
 $ git add README.md
  1. 添加一个提交,以便我们有这个代码更改的历史记录:
 $ git commit -m 'Initial Commit'
  1. 现在,为了将本地应用程序与 GitHub 中的远程仓库链接起来,请使用以下命令:
$ git remote add origin https://github.com/{github_username}/blog.git
  1. 最后,我们需要将这个提交``push到 GitHub:
 $ git push -u origin master

完成后,访问 GitHub 仓库,在那里你可以找到对我们仓库所做的提交历史,如下所示:

图片

就这样。现在,当我们想要编写更改时,我们首先会创建一个分支,然后将更改push到该分支。

摘要

在本章中,我们学习了 MEVN 栈是什么。我们还了解了 Node.js、npm 和 MongoDB 是什么,以及 GitHub 的简要概述,以及它是如何帮助软件工程师方便地访问代码历史和协作的。

在下一章中,我们将学习更多关于 Node.js 和 Node.js 模块的知识。我们将了解 MVC 架构以及如何通过使用 Express.js 构建应用程序来实现它。

第二章:构建 Express 应用

Express.js 是一个 Node.js 网络应用框架。Express.js 使得使用 Node.js 更加容易,并利用了其强大功能。在本章中,我们将仅使用 Express.js 创建一个应用。Express.js 也是一个node包。我们可以使用应用程序生成器工具,它让我们可以轻松地创建 Express 应用的骨架,或者我们可以从头开始自己创建。

在上一章中,我们学习了npm是什么,包是什么,以及如何安装一个包。在本章中,我们将涵盖以下元素:

  • Node.js 是什么以及它能做什么

  • 它带来的好处

  • Node.js 的基本编程

  • Node.js 核心和自定义模块

  • Express.js 简介

  • 使用 Express.js 创建应用

  • Express.js 中的路由

  • MVC 架构:它是什么以及它在应用中实现时增加了什么价值

  • 应用的文件命名规范

  • 对文件夹进行重组以融入 MVC 模式

  • 为 Express.js 应用创建视图

有很多npm包可以帮助我们创建 Express.js 应用的骨架。其中一个这样的包是express-generator。它让我们能在几秒钟内搭建整个应用。它将以模块化结构创建所有必要的文件和文件夹。它生成的文件结构非常易于理解。我们唯一需要做的是定义模板视图和路由。

我们也可以根据我们的需求和需求修改这个结构。当我们面临紧迫的截止日期,想在一天或几天内构建一个应用时,这非常方便。这个过程极其简单。

express-generator只是众多可用于创建 Express 应用骨架或模块化结构的工具之一。每个生成器工具都有其自己的文件结构构建方式,这可以根据其标准轻松定制。

如果你是一个初学者并且想了解文件夹结构是如何工作的,我建议你从头开始构建应用。我们将在本章中进一步讨论这一点。

要开始,我们首先需要了解 Node.js,然后再深入研究 Express.js。

Node.js 简介

Node.js 是一个基于 JavaScript 引擎构建的 JavaScript 运行时。它是一个开源框架,用于服务器端管理。Node.js 轻量级且高效,可在 Windows、Linux 和 macOS 等平台上运行。

Node.js 由 Ryan Dahl 于 2009 年创建。JavaScript 过去主要用于客户端脚本,但 Node.js 使得 JavaScript 也可以在服务器端使用。Node.js 的发明引入了在 Web 应用中使用单一编程语言的做法。Node.js 带来了很多好处,其中一些如下:

  • 事件驱动编程:这意味着改变一个对象的状态从一个变为另一个。Node.js 使用事件驱动编程,这意味着它使用用户的交互动作,如鼠标点击和按键,来改变对象的状态。

  • 非阻塞 I/O:非阻塞 I/O 或非同步 I/O 意味着异步 I/O。同步进程会等待当前运行进程完成,因此会阻塞进程。另一方面,异步进程不需要等待该进程完成,这使得它既快速又可靠。

  • 单线程:单线程意味着 JavaScript 只在单个事件循环中运行。由于异步进程允许我们同时拥有多个进程,这可能会让人感觉所有这些进程都在它们自己的特定线程中运行。但 Node.js 处理异步的方式略有不同。Node.js 中的事件循环在相应事件发生后触发下一个已安排执行的回调函数。

理解 Node.js

在深入研究 Node.js 编程之前,让我们先了解一下 Node.js 的一些基础知识。Node.js 运行在 JavaScript V8 引擎上。JavaScript V8 引擎是由Chromium 项目为 Google Chrome 和 Chromium 网络浏览器构建的。这是一个用 C++编写的开源项目。这个引擎用于客户端和服务器端 Web 应用程序的 JavaScript。

Node.js 编程

让我们从运行一个node进程开始。打开终端并输入以下命令:

$ node

这将启动一个新的node进程。我们在这里可以编写正常的 JavaScript 代码。

例如,我们可以在新的 Node shell 中编写以下 JavaScript 命令:

> var a = 1;

当我们输入a并按回车键时,它返回1

我们也可以在node进程中运行扩展名为.js的文件。让我们在根目录下使用命令mkdir tutorial创建一个名为tutorial的文件夹,并在其中创建一个名为tutorial.js的文件。

现在,在终端中,让我们使用以下命令进入该目录:

$ cd tutorial $ node tutorial.js

我们应该看到以下类似的内容:

图片

由于我们还没有为tutorial.js编写任何内容,所以它不会返回任何内容。

现在,让我们向tutorial.js文件中添加一些代码:

console.log('Hello World');

现在,使用以下命令运行文件:

$ node tutorial.js

我们将看到一个输出显示Hello World。这就是我们在 Node.js 中执行文件的方式。

除了在 V8 引擎上运行并在浏览器中执行 JavaScript 代码之外,Node.js 还提供了一个服务器运行环境。这是 Node.js 最强大的功能。Node.js 提供了一个自带的 HTTP 模块,它实现了一个非阻塞的 HTTP 实现。让我们构建一个简单的 Web 服务器来理解这一点。

在同一文件tutorial.js中,用以下代码覆盖文件:

const http = require('http');

http.createServer(function (req, res) {
 res.writeHead(200, { 'Content-Type': 'text/plain' });
 res.end('Hello World\n');
}).listen(8080, '127.0.0.1');

console.log('Server running at http://127.0.0.1:8080/');

在这里,var http = require('http');这段代码将 HTTP 模块引入到我们的应用程序中。这意味着现在我们可以通过http变量访问 HTTP 库中定义的函数。现在我们需要创建一个 Web 服务器。前面的代码告诉 Node.js 在 8080 端口运行 Web 服务器。createServer方法中的function参数接受两个参数,分别是reqres,它们分别是请求和响应的简称。在这个函数内部,我们首先需要做的是设置 HTTP 头。这基本上是定义我们希望从该请求中获取哪种类型的响应。然后,我们使用res.send定义我们希望在响应中获取的内容。最后,我们要求 Web 服务器监听 8080 端口。

当我们使用$ node tutorial.js运行此代码时,输出看起来像这样:

图片

当我们在浏览器中输入该 URL 时,我们应该能够看到以下内容:

图片

这就是 Node.js 作为服务器程序的工作方式。

要退出node控制台,请按Ctrl + C两次。

Node.js 模块

Node.js 模块只是一个普通的 JavaScript 文件,它包含可重用的代码。每个模块都有自己的特定功能。我们可以将其视为一个库。

例如,如果我们想在应用程序中隔离所有与用户相关的活动,我们可以为它创建一个模块,该模块将处理所有关于用户的数据库。

在 Node.js 中使用模块的方式是通过require。我们刚才向您展示的创建 Web 服务器的例子也是一个 Node.js 模块。

Node.js 核心模块

在 Node.js 中有两种类型的模块。核心模块是在 Node.js 中构建的模块。在我们安装 Node.js 时它们就存在了。这些也被称为内置模块。Node.js 中有许多核心模块:

  • 调试器

  • 文件系统

  • HTTP

  • 路径

  • 处理

  • 事件

如果你想详细了解每个核心模块的更多细节,你可以访问以下文档:

nodejs.org/api/.

自定义模块

这些是我们自己基于 Node.js 创建的模块。由于 Node.js 有一个非常大的生态系统,因此有大量的不同模块可供我们根据需要免费获取。我们可以自己构建一个,或者直接使用别人的模块。这也是 Node.js 强大之处的一个方面。它为我们提供了使用社区模块或自己构建模块的灵活性。

我们可以在www.npmjs.com/browse/depended查看所有现有可用模块的列表:

图片

介绍 Express.js

Express.js 是一个用于 Node.js 的极简服务器端网络框架。它建立在 Node.js 之上,以便更容易地管理 Node.js 服务器。Express.js 最重要的优势是它使路由变得非常简单。它提供的强大 API 非常容易配置。从前端接收请求和连接到数据库都很简单。Express.js 也是 Node.js 最受欢迎的网络框架。它使用 模型-视图-控制器MVC)设计模式,我们将在本章后面讨论。

安装 Express.js

我们已经介绍了如何通过 npm 安装 node 模块。同样,我们可以使用此命令通过 NPM 安装 Express.js:

$ npm install express

这是一种安装 node 模块的简单方法。但是,在构建应用程序时,我们需要很多不同种类的模块。我们还想在多个应用程序之间共享这些模块。因此,为了使模块全局可用,我们必须全局安装它。为此,npm 提供了在安装 node 模块时添加 -g 选项的功能。所以,现在我们可以使用:

$ npm install -g express

这将全局安装 Express.js,这允许我们在多个应用程序中使用 express 命令。

创建 Express.js 应用程序

现在我们已经安装了 Express.js,让我们开始使用 Express.js 创建应用程序。

我们将命名我们的应用程序为 express_app。使用 express 命令构建 express 应用的轮廓非常简单。我们可以简单地使用:

$ express express_app

输出如下所示:

此命令会在我们的应用程序中创建大量的文件和文件夹。让我们快速看一下这些:

  • package.json:此文件包含我们在应用程序中安装的所有 node 包的列表以及应用程序的简介。

  • app.js:此文件是 express 应用的主入口页面。网络服务器代码位于此文件中。

  • public:我们可以使用此文件夹来插入我们的资产,例如图片、样式表或自定义 JavaScript 代码。

  • views:此文件夹包含所有将在浏览器中渲染的视图文件。它包含主布局文件(其中包含视图文件的 HTML 模板),一个 index.jade 文件(它扩展了布局文件,只包含可变或动态的内容),以及一个 error.jade 文件(当我们需要向前端显示某种错误消息时显示)。

  • routes:此文件夹包含我们将构建的所有路由,以便访问应用程序的不同页面。我们将在后续章节中进一步讨论。

  • bin:此文件夹包含 Node.js 的可执行文件。

因此,这些都是我们需要了解的基本知识。现在,使用您喜欢的文本编辑器来处理应用程序,让我们开始吧。现在,如果我们查看 package.json,会发现某些我们没有安装但列在依赖项中的包:

图片

这是因为这些是 Express.js 任何应用程序的依赖项。这意味着,当我们使用 express 命令创建应用程序时,它将自动安装它需要的所有依赖项。例如,前面 package.json 文件中列出的依赖项执行以下操作:

  • body-parser:用于解析我们在发起 HTTP 请求时提供的请求体参数

  • debug:这是一个提供 console.log 返回值的格式化工具的 JavaScript 实用程序包

    我们可以通过 package.json 文件安装或删除包。只需在 package.json 文件中添加或删除包的名称,然后运行 $ npm install

  • express:这是一个 Node.js JavaScript 框架,用于在 Node.js 上构建可扩展的 Web 应用程序。

  • jade:如前所述,这是 Node.js 的默认模板引擎。我们应该在用 express 命令创建应用程序时看到一条警告信息,说明在未来的版本中默认视图引擎将不再是 jade。这是因为 jade 被一家公司拥有版权,后来将其名称更改为 pug

Express 生成器使用过时的 jade 模板引擎。要更改模板引擎,请按照以下步骤操作:

  1. package.json 文件中,删除 "jade": "~1.11.0" 行并运行:
$ cd express_app
$ npm install
  1. 现在,要安装新的 pug 模板引擎,请运行:
$ npm install pug --save
  1. 如果我们查看 package.json 文件,我们应该看到一条类似以下内容的行:

    "pug": "².0.0-rc.4"

  2. 重命名 views 文件夹中的文件:

    • error.jade 重命名为 error.pug

    • index.jade 重命名为 index.pug

    • layout.jade 重命名为 layout.pug

  3. 最后,在 app.js 中,删除显示以下内容的行:

app.set('view engine', 'jade');
  1. 添加以下行以使用 pug 作为视图引擎:
app.set('view engine', 'pug');
  • morgan:这是一个用于记录 HTTP 请求的中间件

  • serve-favicon:这是在浏览器中显示 favicon 以识别我们的应用程序

对于我们的应用程序来说,没有必要拥有所有这些依赖项。它们来自安装 Express.js。只需查找你想要的,然后根据应用程序的需求添加或删除包。

目前,我们将保持原样。express 命令只是将依赖项添加到我们的 package.json 文件中,并为我们的应用程序创建一个骨架。为了实际安装 package.json 文件中列出的这些模块和包,我们需要运行:

$ npm install

这个命令实际上会安装所有依赖项。现在,如果我们查看文件夹结构,我们可以看到一个名为 node_modules 的新文件夹被添加。这就是我们在这个应用程序中安装的所有包的存放地。

现在,我们首先想要做的是设置一个 Web 服务器。为此,在 app.js 文件中添加以下行:

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

app.listen(3000, function() { console.log('listening on 3000') })

module.exports = app;

现在,运行以下命令:

$ node app.js

这将启动我们的应用程序服务器。现在,当我们访问 http://localhost:3000/ URL 时,我们应该能够得到以下内容:

图片

就这些。我们已经成功创建了一个 Express 应用程序。

Express 路由器

让我们继续到 Express 路由器。如本章前面所述,Express.js 最重要的一点是它为应用程序提供了简单的路由。路由是应用程序 URL 的定义。如果我们查看 app.js,我们会看到一个类似以下的部分:

...
app.use('/', index);
app.use('/users', users);
...

这意味着当我们访问一个网页,并且当对主页发起请求时,Express 路由器将其重定向到名为 index 的路由器。现在,看看 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;

这意味着当我们访问主页时,它会渲染一个名为 index 的页面,该页面位于 views/index.pug 内,并将要显示在页面上的 title 参数传递过去。现在,看看位于视图文件夹中的 index.pug 文件,它包含以下代码:

extends layout

block content
  h1= title
  p Welcome to #{title}

这意味着它使用了 layout.pug 文件中的布局,并显示了一个 h1 标题以及一个渲染我们从前端文件传递过来的标题的段落。因此,输出如下:

图片

非常简单直接,对吧?

请求对象

请求对象是一个包含 HTTP 请求信息的对象。请求的属性包括:

  • query: 这包含有关解析查询字符串的信息。通过 req.query 访问。

  • params: 这包含有关解析路由参数的信息。通过 req.params 访问。

  • body: 这包含有关解析请求体的信息。通过 req.body 访问。

响应对象

req 变量上接收到 request 之后,res 对象是我们想要发送回的 response

响应对象的属性包括:

  • send: 这用于向视图发送响应。通过 res.send 访问。它接受两个参数,状态码和响应体。

  • status: 如果我们想要发送应用程序的成功或失败,使用 res.status。这是 HTTP 状态码。

  • redirect: 当我们想要重定向到某个页面而不是以其他格式发送响应时,使用 res.redirect

MVC 简介

MVC 模型在构建应用程序时至关重要,无论使用哪种编程语言。MVC 架构使得组织我们应用程序的结构和分离逻辑部分和视图部分变得容易。我们可以在任何时间点整合这个 MVC 结构,即使我们已经完成了应用程序的一半。最佳实施时间是任何应用程序的开始。

如其名所示,它有三个部分:

  • Model: 所有应用程序的业务逻辑都位于这些 models 之下。它们处理数据库。它们处理应用程序的所有逻辑部分。

  • 视图:浏览器渲染的任何内容——用户看到的内容——都由这些视图文件处理。它处理我们发送给客户端的任何内容。

  • 控制器:Controllers基本上连接这些models和视图。它负责将models中完成的逻辑计算传递到views部分:

图片

在我们构建的应用程序中实现 MVC 平台并不是必需的。JavaScript 是无模式的,这意味着我们可以创建自己的文件夹结构。与其他编程语言不同,我们可以选择对我们来说最容易的结构。

为什么使用 MVC?

当我们将 MVC 架构实现到我们的应用程序中时,会添加很多好处:

  • 商业逻辑和视图的清晰分离。这种分离使我们能够在整个应用程序中重用业务逻辑。

  • 开发过程变得更快。这一点很明显,因为各个部分已经清晰地分离出来。我们只需将视图添加到视图文件夹中,并在models文件夹中添加逻辑。

  • 修改现有代码很容易。当多个开发者共同参与同一项目时,这一点非常方便。任何人都可以从任何地方开始修改应用程序。

将文件夹结构修改为包含 MVC

既然我们已经对 MVC 有了足够的了解,让我们修改一下我们创建的应用程序的文件夹结构,该应用程序名为express_app。首先,我们需要在根目录中创建这三个文件夹。已经有一个views文件夹,所以我们可以跳过它。让我们继续创建modelscontrollers文件夹。

之后,在我们的app.js中,我们需要包含我们的控制器文件。为此,我们首先需要引入一个新的包,称为 filesystem。这个模块使得执行与文件相关的操作变得容易,例如读取/写入文件。

因此,要将这个包添加到我们的应用程序中,请运行:

$ npm install file-system --save 

这个--save参数用于当我们想要一个node模块只在我们应用程序中安装时。此外,安装后,这个包将自动包含在我们的package.json中。

图片

现在,我们需要引入这个模块并使用它来包含所有位于控制器中的文件。为此,在app.js中添加以下代码行。确保在运行我们的网络服务器代码之前添加这些行:

var index = require('./routes/index');
var users = require('./routes/users');

var app = express();

// Require file system module
var fs = require('file-system');

// Include controllers
fs.readdirSync('controllers').forEach(function (file) {
 if(file.substr(-3) == '.js') {
 const route = require('./controllers/' + file)
 route.controller(app)
 }
})

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');

让我们继续添加一个路由到我们的控制器。让我们在应用程序的根目录中创建一个名为controllers的文件夹,并在controllers文件夹中添加一个index.js文件,并将以下代码粘贴进去:

module.exports.controller = (app) => {
 // get homepage
 app.get('/', (req, res) => {
 res.render('index', { title: 'Express' });
 })
}

现在,所有路由都将由控制器文件处理,这意味着我们不需要在app.js中控制路由的代码。因此,我们可以从文件中删除这些行:

var index = require('./routes/index');
var users = require('./routes/users');

app.use('/', index);
app.use('/users', users);

实际上,我们不再需要那个routes文件夹。让我们也删除routes文件夹。

同样,让我们添加一个新的路由来控制所有与用户相关的操作。为此,在 controllers 文件夹中添加一个名为 users.js 的新文件,并将以下代码粘贴到其中:

module.exports.controller = (app) => {
 // get users page
 app.get('/users', (req, res) => {
 res.render('index', { title: 'Users' });
 })
}

现在,让我们使用以下命令重新启动我们的应用节点服务器:

$ node app.js

通过这种方式,当我们访问 http://localhost:3000/users 时,我们将能够看到以下内容:

图片

我们已经成功设置了 MVC 架构中的 controllersviews 部分。我们将在后续章节中进一步介绍 models 部分。

在上一章中,我们讨论了 GitHub 以及如何通过进行小提交来使用它来记录代码历史。不要忘记设置仓库并持续将代码推送到 GitHub。

npm 包存储在 node_modules 目录中,我们不应将其推送到 GitHub。为了忽略此类文件,我们可以添加一个名为 .gitignore 的文件,并指定我们不想推送到 GitHub 的文件。

让我们在我们的应用程序中创建一个名为 .gitignore 的文件,并添加以下内容:

node_modules/

这样,当我们安装任何包时,它不会在向 GitHub 提交代码时显示为代码差异。

每当我们对代码进行一些更改时,我们都需要重新启动我们的 node 服务器,这非常耗时。为了简化这个过程,node 提供了一个名为 nodemon 的包,它会在我们更改代码时自动重新启动服务器。

要安装该包,请运行:

$ npm install nodemon --save

要运行服务器,请使用以下命令:

$ nodemon app.js

文件命名规范

在开发应用程序时,我们需要遵循一定的命名约定来命名文件。随着我们继续构建应用程序,我们将拥有大量的文件,这可能会变得混乱。MVC 允许在不同的文件夹之间有并行的命名规范,这可能导致不同文件夹中存在相同的文件名。

如果我们认为这样做既简单又易于维护,我们也可以处理这样的文件名。否则,我们只需将文件类型追加到每个文件中,例如,在以下示例中;对于处理用户相关活动的控制器文件,我们可以将其保留为 controllers/users.js,或者我们可以将其重命名为 controllers/users_controller.js。我们将为我们的应用程序使用 controllers/users

对于 modelsservices 或任何需要在应用程序的不同区域之间共享的文件夹也是如此。对于这个应用程序,我们将使用以下命名约定:

图片

记住,Node.js 中没有官方的命名规范。我们绝对可以自定义我们找到的更简单的方式。我们将在后续章节中进一步讨论创建 models 的问题。这需要我们与 Mongo 建立连接,我们将在后续章节中描述。

为 Express.js 应用程序创建视图文件

在上一节中,我们学习了如何创建 controllers。在本节中,我们将讨论如何添加和自定义视图文件。如果您还记得,我们在 controllers/users.js 中有这段代码:

module.exports.controller = (app) => {
  // get users page
  app.get('/users', (req, res) => {
    res.render('index', { title: 'Users' });
  })
}

让我们更改渲染 index 文件的行,改为以下内容:

module.exports.controller = (app) => {
  // get users page
  app.get('/users', (req, res) => {
    res.render('users', { title: 'Users' });
  })
}

这意味着控制器想要加载 views 文件夹中的 users 文件。让我们继续在 views 文件夹中创建一个 users.pug 文件。

创建文件后,粘贴以下代码;这与我们在 views 文件夹中的 index.pug 文件中的代码相同:

extends layout

block content
 h1= title
 p Welcome to #{title}

现在,如果我们使用了 nodemon,我们不需要重新启动服务器;只需用位置 http://localhost:3000/users 重新加载浏览器。这应该会渲染以下内容:

图片

现在我们已经知道了如何连接 controllersviews 以及如何创建视图文件,让我们来获取一些关于文件代码的更多信息。

第一行表示:

extends layout

这意味着它要求扩展 layout.pug 文件中已经存在的视图。现在,看看 layout.pug

doctype html
html
  head
    title= title
    link(rel='stylesheet', href='/stylesheets/style.css')
  body
    block content

这是一个简单的 HTML 文件,包含 doctypeHTMLheadbody 标签。在 body 标签内,它指示阻塞内容,这意味着它从任何其他文件中产生内容,这些文件都写在 block content 语句之下。如果我们查看 users.jade,我们可以看到内容是写在 block content 语句之下的。现在,这非常实用,因为我们不需要在创建的每个视图文件中重复整个 HTML 标签。

此外,如果我们查看控制器中的 users.js,有一行写着:

res.render('users', { title: 'Users' });

渲染方法有两个参数:它想要加载的视图以及想要传递给该视图的变量。在这个例子中,Users 被传递给标题变量。在 views 文件夹中的 users.jade 文件里,我们有:

block content
  h1= title
  p Welcome to #{title}

这会在 h1 标签和 p 标签内渲染该变量。这样,我们可以从 controllers 向视图传递任何我们想要的内容。让我们在 users.js 控制器的 render 方法中添加一个新的变量 description

module.exports.controller = (app) => {
  // get homepage
  app.get('/users', (req, res) => {
    res.render('users', { title: 'Users', description: 'This is the description of all the users' });
  })
}

此外,让我们在 users.pug 中创建一个渲染该内容的区域:

extends layout

block content
  h1= title
  p Welcome to #{title}
  p #{description}

如果我们重新加载浏览器,我们会得到:

图片

这就是我们在 Express 应用程序中创建视图的方法。现在,继续添加您希望为我们的应用程序添加的视图。

总是要确保将更改提交并推送到 GitHub。提交越小,代码的可维护性就越高。

摘要

在这一章中,我们学习了 Node.js 是什么以及 Express.js 是什么。我们学习了如何使用 Express.js 创建应用程序,并了解了 MVC 架构。

在下一章中,我们将讨论 MongoDB 及其查询。我们还将讨论使用 Mongoose 进行快速开发和 Mongoose 查询和验证。

第三章:介绍 MongoDB

MongoDB 的名称来源于短语 huMONGOus data,意味着它可以处理大量数据。MongoDB 是一种面向文档的数据库架构。它使我们能够更快地开发并更好地扩展。在关系型数据库设计中,我们通过创建表和行来存储数据,但与 MongoDB 相比,我们可以将数据建模为 JSON 文档,这比那些关系型数据库要简单得多。如果我们是敏捷的,并且我们的需求经常变化,并且如果我们需要持续部署,那么 MongoDB 是我们的选择。作为一个基于文档的数据模型,MongoDB 也非常灵活。

使用 MongoDB 的最大优势是数据非结构化。我们可以以任何我们喜欢的格式自定义我们的数据。在关系型数据库管理系统RDBMS)中,我们必须精确地定义一个表可以有多少字段,但与 MongoDB 不同,每个文档都可以有自己的字段数。我们可以添加新数据,甚至无需担心更改模式,这就是为什么 Mongo 数据库采用了无模式设计模型

如果我们的业务增长迅速,我们需要更快地扩展,以更灵活的方式访问数据,如果我们需要更改数据而无需担心更新应用程序的数据库模式,那么 MongoDB 是我们最佳的选择。在 RDBMS 的表中添加新列也会引起一些性能问题。但是,由于 MongoDB 是无模式的,添加新字段可以瞬间完成,而不会影响我们应用程序的性能。

在关系型数据库中,我们使用的术语是数据库,而在 MongoDB 中,我们分别使用数据库集合文档

本章将简要概述我们将要涵盖的内容:

  • 介绍 MongoDB 及其使用 MongoDB 的好处

  • 理解 MongoDB 数据库、集合和文档

  • 介绍 Mongoose,使用 Mongoose 建立连接,理解 Mongoose,以及使用 Mongoose 进行 CRUD 操作

  • 使用 Mongoose 添加默认和自定义验证

为什么选择 MongoDB?

MongoDB 提供了许多优势,其中一些包括:

  • 灵活的文档:MongoDB 的集合包含多个文档。一个集合下的每个文档可以有不同的字段名,并且可以有不同的尺寸,这意味着我们不需要定义模式。

  • 无复杂关系:MongoDB 中的文档以 JSON 文档的形式存储,这意味着我们不再需要费心学习应用程序各个组件之间的关系。

  • 易于扩展:MongoDB 易于扩展,因为它通过使用称为分片的方法来最小化数据库大小。分片是一种数据库分区方法,允许我们将大型数据库分割成更小的部分。

MongoDB 查询

我们在 第一章,“MEVN 简介”中简要回顾了 Mongo 查询的外观。在这里,我们将深入探讨这些查询。

我们需要做的第一件事是启动 MongoDB 服务器。我们可以使用以下命令来完成:

$ mongod

现在,让我们通过在终端中键入 mongo 来打开 mongo shell。当我们进入 mongo shell 时,要显示数据库列表,我们键入 show dbs

如果你看到数据库在列表中,键入 use {database_name} 以开始使用此数据库。如果我们还没有创建我们的数据库,只需使用 use {database_name} 就会为我们创建一个数据库。就这么简单。对于这个练习,让我们创建一个名为 mongo_test_queries 的数据库。为此,我们需要使用:

> use mongo_test_queries

这应该在终端中输出以下内容:

# switched to db mongo_test_queries

现在,一旦我们进入数据库,我们首先需要的是一个集合。我们有一个数据库,但没有集合。在 MongoDB 中创建集合的最佳方式是通过插入一个文档。这不仅初始化了一个集合,还将文档添加到该集合中。就这么简单。现在,让我们继续学习 Mongo 查询。

创建文档

在 MongoDB 中创建文档有不同的查询,例如 insertOne()insertMany()insert()

insertOne()

insertOne() 命令将单个文档添加到我们的集合中。例如:

> db.users.insertOne(
 {
 name: "Brooke",
 email: "brooke@app.com",
 address: 'Kathmandu'
 }
)

此命令仅接受一个参数,即一个对象,我们可以传递我们想要为 users 集合指定的字段名称和值。当我们运行上述代码时,我们应该在 Mongo shell 的终端中得到以下输出:

它返回刚刚创建的文档的 _id。我们已经成功在 users 集合中创建了一个集合和文档。

insertOne()insertMany() 命令仅适用于 Mongo 版本 3.2 或更高版本。

insertMany()

此命令用于将多个文档插入到集合中。在先前的示例中,我们看到了 insertOne() 命令接受一个参数,该参数是一个对象。insertMany() 命令接受一个数组作为参数,这样我们就可以在它内部传递多个对象,并将多个文档插入到集合中。让我们看一个例子:

> db.users.insertMany(
 [
 { name: "Jack", email: "jack@mongo.com" },
 { name: "John", email: "john@mongo.com" },
 { name: "Peter", email: "peter@mongo.com" }
 ]
)

此代码片段在 users 集合中创建了三个文档。当我们运行此命令时,输出应该是:

insert()

此命令将单个文档以及多个文档插入到集合中。它同时完成了 insertOne()insertMany() 命令的工作。要插入单个文档,我们可以使用:

> db.users.insert(
    { name: "Mike", email: "mike@mongo.com" }
)

如果命令执行成功,我们应该看到以下输出:

现在,如果我们想插入多个文档,我们可以简单地使用:

> db.users.insert(
  [
    { name: "Josh", email: "josh@mongo.com" },
    { name: "Ross", email: "ross@mongo.com" },
  ]
)

输出应该是以下内容:

检索文档

使用 find() 命令从 MongoDB 的集合中检索文档。有多种使用此命令的方法。

查找所有文档

要从集合中检索所有文档,我们可以使用:

> db.users.find()

我们也可以使用以下:

> db.users.find({})

这会输出以下内容:

图片

通过过滤器查找文档

我们也可以在 find() 命令中添加过滤器。让我们检索名为 Mike 的文档。为此,我们可以使用:

> db.users.find({ name: 'Mike' })

它应该返回以下文档:

图片

我们也可以使用 ANDOR 查询指定多个条件。

要查找名为 Mike 且电子邮件为 mike@mongo.com 的集合,我们可以简单地使用:

> db.users.find({ name: 'Mike', email: 'mike@mongo.com' })

逗号运算符表示 AND 运算符。我们可以使用逗号分隔的值指定任意数量的条件。前面的命令应该输出:

图片

现在指定 AND 或逗号运算符的条件变得简单。如果我们想使用 OR 运算符,那么我们应该使用:

> db.users.find(
 {
 $or: [ { email: "josh@mongo.com" }, { name: "Mike" } ]
 }
)

这里,我们说的是:检索那些名为 Mike 的用户文档,电子邮件可以是 josh@mongo.com。输出如下:

图片

更新文档

就像 insert() 一样,在 MongoDB 中使用 update() 命令有三种方法:updateOne()updateMany()update()

updateOne()

此命令只更新集合中的一个文档。在这里,我们插入了一些用户条目,电子邮件不正确。对于名为 Peter 的用户,电子邮件是 jack@mongo.com。让我们使用 updateOne() 更新此文档:

> db.users.updateOne(
 { "name": "Peter" },
 {
 $set: { "email": "peter@mongo.com" }
 }
 )

此命令将更新 Peter 的电子邮件为 peter@mongo.com。输出如下:

图片

正如输出所示,modifiedCount1matchedCount 也是 1,这意味着找到了符合给定条件的文档并已更新。

updateMany()

此命令用于更新集合中的多个文档。updateOne()updateMany() 更新文档的命令相同。要更新多个记录,我们指定条件并设置所需的值:

> db.users.updateOne(
 { "name": "Peter" },
 {
 $set: { "email": "peter@mongo.com" }
 }
 )

updateOne()updateMany() 之间的唯一区别是 updateOne() 只更新第一个匹配的文档,而 updateMany() 更新所有匹配的文档。

update()

就像插入一样,update() 命令为 updateOne()updateMany() 做了同样的事情。为了消除混淆,我们可以直接使用 update() 命令而不是 updateOne()updateMany()

> db.users.update(
 { "name": "John" },
 {
 $set: { "email": "john@mongo.com" }
 }
 )

输出如下:

图片

删除文档

MongoDB 提供了多个命令用于从集合中删除和移除文档。

deleteOne()

deleteOne() 只从集合中删除一个文档:

> db.users.deleteOne( { name: "John" } )

这会删除名为 John 的用户条目。输出如下:

图片

如您在输出中看到的,deletedCount1,这意味着记录已被删除。

deleteMany()

deleteMany() 命令与 deleteOne() 相同。唯一的区别是 deleteOne() 只删除与匹配过滤器匹配的单个条目,而 deleteMany() 则删除所有符合给定条件的文档:

> db.users.deleteMany( { name: "Jack" } )

输出如下:

remove()

remove() 命令用于从集合中删除单个条目以及多个条目。如果我们只想删除符合某些条件的单个文档,则可以传递我们希望删除的条目数。例如,让我们首先创建一个条目:

> db.users.insertOne({ name: 'Mike', email: 'mike@mike.com' })

使用此方法,现在我们有两个 Mike 的条目。现在,如果我们想使用 remove() 删除单个条目,我们可以这样做:

> db.users.remove({ name: 'Mike' }, 1)

输出如下:

如您所见,我们有两个名为 Mike 的条目,但它只删除了一个。同样,如果我们想删除所有文档,我们只需使用:

> db.users.remove({})

所有文档都将被删除。

我们讨论了如何在 Mongo 中查询文档的基本思路。要了解更多详细信息,请访问 docs.mongodb.com/v3.2/tutorial/query-documents/

介绍 Mongoose

Mongoose 是一个优雅的 MongoDB 对象建模库,适用于 Node.js。正如我之前提到的,MongoDB 是一种无模式的数据库设计。虽然这有其自身的优势,但有时我们还需要添加某些验证,这意味着我们需要为我们的文档定义模式。Mongoose 提供了一种简单的方法来添加此类验证以及将文档中的字段类型化。

例如,要将数据插入 MongoDB 文档中,我们可以使用:

> db.posts.insert({ title : 'test title', description : 'test description'})

现在,如果我们想添加另一个文档并且想在其中添加一个额外的字段,我们可以使用:

> db.posts.insert({ title : 'test title', description : 'test description', category: 'News'})

这在 MongoDB 中是可能的,因为没有定义模式。这些类型的文档在构建应用程序时也是必需的。MongoDB 会默默地接受任何类型的文档。然而,有时我们需要文档看起来相似,以便在特定的验证中表现良好或具有特定的数据类型。在这种情况下,Mongoose 就派上用场了。我们也可以利用这些功能与原始 MongoDB 一起使用,但在 MongoDB 中编写验证是一个极其痛苦的任务。这就是为什么创建 Mongoose 的原因。

Mongoose 是用 Node.js 编写的 Mongo 数据建模技术。Mongoose 集合中的每个文档都需要固定数量的字段。我们必须显式定义一个 Schema 并遵守它。一个 Mongoose 模式的例子是:

const UserSchema = new Schema({
 name: String,
 bio: String,
 extras: {}
})

这意味着名称和描述字段必须是字符串,而额外字段可以是一个完整的 JSON 对象,在其中我们可以存储嵌套值。

安装 Mongoose

就像任何其他包一样,Mongoose 可以使用 NPM 在我们的项目中安装。在上一章中创建的 express_app 文件夹内的终端中运行以下命令来安装 Mongoose:

$ npm install mongoose --save

如果安装成功,我们应该在我们的 package.json 文件中添加一行:

将 Mongoose 连接到 MongoDB

一旦安装了 Mongoose,我们必须将其连接到 MongoDB 才能开始使用它。使用 Mongoose 来做这件事非常直接;我们只需在 app.js 文件中添加一段代码来 require Mongoose,并使用 mongoose.connect 方法将其连接到数据库。让我们继续这样做。在 app.js 文件中,添加以下代码:

var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var mongoose = require('mongoose');

这将把 Mongoose 模块导入到我们的代码库中。

现在,为了连接到 MongoDB 数据库,请在我们的 app.js 文件中添加以下代码行:

var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var mongoose = require('mongoose');

var app = express();

//connect to mongodb
mongoose.connect('mongodb://localhost:27017/express_app', function() {
 console.log('Connection has been made');
})
.catch(err => {
 console.error('App starting error:', err.stack);
 process.exit(1);
});

// Require file system module
var fs = require('file-system');

这与我们的 Mongoose 数据库建立了一个连接。现在,让我们使用以下命令运行应用程序:

$ nodemon app.js

并在我们的终端中显示一条消息,如果成功或失败:

就这样!我们已经成功连接到我们的 MongoDB 数据库。这里的 URL 是本地托管的数据库名称。

在 Mongoose 中创建记录

让我们从在应用程序的 express_app 中创建一个新的模型开始。在项目的根目录下创建一个名为 models 的文件夹,并将其命名为 User.js

我们使用文件名的首字母大写。同样,我们使用 models 的单数形式。与此相反,对于 controllers,我们使用复数形式和小写字母,例如 users.js

一旦我们创建了文件,将以下代码粘贴到其中:

const mongoose = require('mongoose');

const Schema = mongoose.Schema;

const UserSchema = new Schema({
 name: String,
 email: String
})

const User = mongoose.model("User", UserSchema)
module.exports = User

这里的第一行只是导入 Mongoose 模块。这个 Mongoose 包为我们提供了几个属性,其中之一是定义 Schema。现在,这里的原始 Schema 定义是以下高亮部分:

const mongoose = require('mongoose');

const Schema = mongoose.Schema;

const UserSchema = new Schema({
 name: String,
 email: String
})

const User = mongoose.model("User", UserSchema)
module.exports = User

这所做的就是在我们的 User 数据模型中添加了一个验证,其中说明总共必须有两个字段。在为 Mongoose 集合创建文档时,它不会接受一个或多个数据字段。此外,它还向这个 Schema 添加了一个验证层,说明两个字段,即 nameemail,应该是一个有效的字符串。它不会接受整数、布尔值或这两个字段之外的任何其他类型。这就是我们定义 Schema 的方式:

const mongoose = require("mongoose")
const Schema = mongoose.Schema

const UserSchema = new Schema({
  name: String,
  email: String
})

const User = mongoose.model("User", UserSchema)
module.exports = User

这段代码的高亮部分表示创建模型的方式。该方法的第一参数是我们的模型名称,它映射到集合名称的相应复数形式。因此,当我们创建一个 User 模型时,这会自动映射到我们数据库中的 user 集合。

现在,要创建一个用户,首先要创建一个资源:

const user_resource = new User({
  name: 'John Doe',
  email: 'john@doe.com'
})

现在,最后,真正创建 user 的部分是:

user_resource.save((error) => {
  if(error)
 console.log(error);

  res.send({
    success: true,
    code: 200,
    msg: "User added!"
  })
})

之前的代码使用了一个名为save的 Mongoose 函数。save方法有一个用于错误处理的回调函数。当我们将资源保存到数据库时遇到错误,我们可以做任何我们想做的事情:

user_resource.save((error) => {
  if(error)
    console.log(error);

  res.send({
 success: true,
 code: 200,
 msg: "User added!"
 })
})

res.send方法允许我们在资源成功保存到数据库时向客户端发送我们想要的内容。对象中的第一个元素是success: true,表示执行是否成功。第二个元素是状态码或响应码。一个200响应码表示成功执行。我们将在后续章节中进一步讨论这一点。最后一个元素是发送给客户端的消息;用户在前端会看到这个消息。

这就是我们在 Mongoose 中创建资源的方式。

从 Mongoose 获取记录

现在我们已经成功创建了一个用户,我们在数据库的users集合中有一个记录。在我们的客户端中,有两种方式可以获取这个记录:获取所有用户的记录或者获取一个特定的用户。

获取所有记录

Mongoose 模型有很多开箱即用的方法,可以使我们的工作更轻松。其中两种方法是find()findById()。在 MongoDB 中,我们看到了如何通过原始 MongoDB 查询检索集合的记录数据。这与此类似,唯一的区别是 Mongoose 有一个非常简单的方式来做到这一点。我建议你先学习 MongoDB 而不是 Mongoose,因为 MongoDB 可以给你一个数据库的整体概念,你将学习数据库的基础知识和其查询。Mongoose 只是在 MongoDB 之上添加了一层,使其看起来更容易,以便更快地开发。

这样,让我们来看看这里的代码片段:

User.find({}, 'name email', function (error, users) {
  if (error) { console.error(error); }
  res.send({
    users: users
  })
})

Mongoose 模型User调用一个名为find()的方法。第一个参数是我们的查询字符串,它是空的:前一个查询中的{}。因此,如果我们想检索所有同名用户,比如 Peter,那么我们可以将那个空{}替换为{ name: 'Peter'}

第二个参数表示我们想从数据库中检索哪些字段。如果我们想检索所有字段,可以将其留空,或者我们也可以在这里指定。对于这个例子,我们只是检索用户名和电子邮件。

第三个参数附加了一个回调函数。这个函数有两个参数,与create方法不同。第一个参数处理错误。如果执行未成功完成,它返回一个错误,我们可以按我们想要的方式自定义它。第二个参数是这里的一个重要参数;它返回执行成功时的响应。在这种情况下,users参数是从users集合中检索到的对象的数组。这个调用的输出将是:

users: [
  {
    name: 'John Doe',
    email: 'john@doe.com'
  }
]

现在我们已经从users集合中获取了所有记录。

获取特定记录

这与从集合中检索所有记录一样简单。我们在上一节中讨论了使用 find()。要检索单个记录,我们必须使用 findById()findOne(),或者我们也可以使用 where 查询。where 查询与我们之前讨论的相同,当时我们需要传递一个参数来检索属于同一类别的记录。

让我们继续使用以下查询:

User.findById(1, 'name email', function (error, user) {
  if (error) { console.error(error); }
  res.send(user)
}) 

如您所见,find()findById() 的语法相似。它们接受相同数量的参数,表现相同。这两个方法之间的唯一区别是,前面的 find() 方法返回了一个记录数组作为响应,而 findById() 返回一个单个对象。因此,前面查询的响应将是:

{
    name: 'John Doe',
    email 'john@doe.com'
}

就这么简单 - 简单!

在 Mongoose 中更新记录

让我们继续更新集合中的记录。更新集合记录的方法也很多,就像从集合中检索数据一样。在 Mongoose 中更新文档是 read(读取)和 create(保存)方法的组合。要更新文档,我们首先需要使用 Mongoose 的读取查询找到该文档,然后更改该文档,最后保存更改。

findById() 和 save()

让我们看看以下示例:

User.findById(1, 'name email', function (error, user) {
  if (error) { console.error(error); }

  user.name = 'Peter'
  user.email = 'peter@gmail.com'
  user.save(function (error) {
    if (error) {
      console.log(error)
    }
    res.send({
      success: true
    })
  })
})

因此,我们首先需要找到用户文档,我们通过 findById() 来做这件事。此方法返回具有给定 ID 的用户。现在我们有了这个用户,我们可以更改我们想要的任何内容。在前面的例子中,我们更改了那个人的姓名和电子邮件。

现在重要的部分来了。更新此用户文档的工作是由这里的 save() 方法完成的。我们已经通过以下方式更改了用户的姓名和电子邮件:

user.name = 'Peter'
user.email = 'peter@gmail.com'

我们正在直接更改最初通过 findById() 返回的对象。现在,当我们使用 user.save() 时,此方法会使用新的姓名和电子邮件覆盖此用户之前的所有值。

我们还可以使用其他方法来更新 Mongoose 中的文档。

findOneAndUpdate()

当我们想要更新单个条目时,可以使用此方法。例如:

User.findOneAndUpdate({name: 'Peter'}, { $set: { name: "Sara" } },   function(err){
  if(err){
    console.log(err);
  }
});

如您所见,第一个参数定义了描述我们想要更新的记录的准则,在这种情况下,是名为 Peter 的用户。第二个参数是我们定义要更新的 user 的哪些属性的对象,它由 { $set: { name: "Sara" } 定义。这会将 Peter 的 name 设置为 Sara。

现在,让我们对前面的代码进行一些小的修改:

User.findOneAndUpdate({name: 'Peter'}, { $set: { name: "Sara" } },   function(err, user){
  if(err){
    console.log(err);
  }
  res.send(user);
});

在这里,请注意,我向回调函数 user 添加了一个第二个参数。这样做的作用是,当 Mongoose 完成在数据库中更新该文档后,它会返回该对象。当我们更新记录后想要做出一些决定,并想要操作新更新的文档时,这非常有用。

findByIdAndUpdate()

这与findOneAndUpdate()有些相似。此方法接受一个 ID 作为参数,与findOneAndUpdate()不同,后者我们可以添加自己的条件,并更新该文档:

User.findByIdAndUpdate(1, { $set: { name: "Sara" } },   function(err){
  if(err){
    console.log(err);
  }
});

这里的唯一区别在于,第一个参数接受一个单独的整数值,即文档的 ID,而不是一个对象。此方法还会返回正在更新的对象。因此,我们可以使用:

User.findByIdAndUpdate(1, { $set: { name: "Sara" } }, function(err){
  if(err, user){
    console.log(err);
  }
 res.send(user);
});

在 Mongoose 中删除记录

正如 Mongoose 中有许多创建、获取和更新记录的方法一样,它也提供了几种从集合中删除记录的方法,例如remove()findOneAndRemove()findByIdAndRemove()。我们可以使用remove()来删除一个或多个文档。我们也可以先找到我们想要删除的文档,然后使用remove()命令仅删除那些文档。如果我们想根据某些条件找到特定的文档,我们可以使用findOneAndRemove()。当我们知道要删除的文档的 ID 时,我们可以使用findByIdAndRemove()

remove()

让我们看看使用此方法的示例:

User.remove({
  _id: 1
}, function(err){
  if (err)
    res.send(err)
  res.send({
    success: true
  })
})

remove()方法的第一个参数接受过滤条件,指定我们想要删除哪个用户。它接受一个 ID 作为参数。它找到具有给定 ID 的用户,并从集合中删除该文档。第二个参数是之前提到的回调函数。如果上述操作出现错误,它将返回一个错误,我们可以使用它来更好地处理应用程序中发生的异常或错误。在成功的情况下,我们可以定义自己的逻辑来决定返回什么。在前面的例子中,我们返回了{ success: true }

findOneAndRemove

findOneAndRemove()的行为与remove()相同,并且接受相同数量的参数:

User.findOneAndRemove({
  _id: 1
}, function(err){
  if (err)
    res.send(err)
  res.send({
    success: true
  })
})

我们只需要定义我们想要删除的文档的筛选条件。

现在,我们也可以修改前面的代码:

User.findOneAndRemove({
  _id: 1
}, function(err, user){
  if (err)
    res.send(err)
  res.send({
    success: true,
    user: user
  })
})

在这里,我已经突出显示了新增的代码片段。我们还可以向回调函数传递第二个参数,该参数返回被删除的user对象。现在,如果我们想在前端显示某些消息并添加一些用户属性,如usernameemail,这将非常有用。例如,如果我们想在前端显示一条消息,说明名为{x}的用户已被删除,那么我们可以在这里传递user或其他user属性;在这种情况下,是名称,将在前端显示。

remove()findOneAndRemove()之间的主要区别在于,remove()不会返回被删除的文档,而findOneAndRemove()会。现在我们知道何时使用这两种方法。

findByIdAndRemove()

这与findOneAndRemove()相同,但总是需要传递一个id作为参数:

User.findByIdAndRemove(1, function(err){
  if (err)
    res.send(err)
  res.send({
    success: true
  })
})

你在findOneAndRemove()和前面的findByIdAndRemove()代码之间发现了任何区别吗?如果我们查看这个方法的第一参数,它只接受一个简单的整数值,即文档 ID。现在,如果我们查看前面的findOneAndRemove()代码,我们会注意到我们在第一个参数中传递了一个对象。这是因为对于findOneAndRemove(),我们除了 ID 之外还可以传递其他参数。例如,我们也可以在findOneAndRemove()的参数中传递{ name: 'Anita' }。但是,对于findByIdAndRemove(),正如方法名所暗示的,我们不需要传递对象,只需要传递表示文档 ID 的整数。

它在参数中找到指定 ID 的文档,并将其从集合中删除。就像findOneAndRemove()一样,这个操作也会返回被删除的文档。

使用 Mongoose 添加验证

Mongoose 中的验证是在模式级别定义的。验证可以设置在字符串和数字中。Mongoose 为我们提供了字符串和数字的内置验证技术。此外,我们也可以根据我们的需求自定义这些验证。由于验证是在模式中定义的,因此当我们在任何文档上调用save()方法时,它们会被触发。如果我们只想测试这些验证,我们也可以通过仅通过{doc}.validate()执行验证方法来做到这一点。

validate()也是中间件,这意味着它在以异步方式执行某些方法时具有控制权。

默认验证

让我们谈谈 Mongoose 为我们提供的默认验证。这些也被称为内置验证器。

required()

required()验证器检查我们添加验证的字段是否有值。在之前的User模型中,我们有以下代码:

var mongoose = require("mongoose");
var Schema = mongoose.Schema;

var UserSchema = new Schema({
  name: String,
  email: String
});

var User = mongoose.model("User", UserSchema);
module.exports = User;

这段代码也与用户的字段相关联的验证有关。它要求用户的名字和电子邮件必须是字符串,而不是数字、布尔值或其他任何东西。但这段代码并没有确保用户的名字和电子邮件字段已被设置。

因此,如果我们想添加一个required()验证,代码应该这样修改:

var mongoose = require("mongoose");
var Schema = mongoose.Schema;

var UserSchema = new Schema({
  name: {
 required: true
 },
  email: {
 required: true
 }
});

var User = mongoose.model("User", UserSchema);
module.exports = User;

如您所见,我们已经将 name 键的值从字符串更改为对象。在这里,我们可以添加我们想要的任何验证。因此,添加的验证required: true检查在将文档保存到集合之前,用户的名字和电子邮件是否已设置某个值。如果验证未通过,它将返回一个错误。

我们还可以在验证返回错误时传递一个消息。例如:

var mongoose = require("mongoose");
var Schema = mongoose.Schema;

var UserSchema = new Schema({
  name: {
 required: [true, 'Let us know you by adding your name!']
 },
  email: {
 required: [true, 'Please add your email as well.']
 }
});

var User = mongoose.model("User", UserSchema);
module.exports = User;

这样,我们也可以根据我们的需求自定义消息。非常酷,对吧?

类型验证

类型验证方法定义了文档中字段的类型。类型的不同变体可以是Stringbooleannumber

String

字符串本身下面有几个验证器,例如enummatchmaxlengthminlength

maxlengthminlength 定义了字符串的长度。

数字

数字有两个验证器:minmaxminmax 的值定义了集合中字段值的范围。

自定义验证

如果默认的内置验证不够用,我们还可以添加自定义验证。我们可以传递一个 validate 函数并将我们的自定义代码写入该函数。让我们看一个例子:

var userSchema = new Schema({
  phone: {
    type: String,
    validate: {
 validator: function(v) {
 return /\d{3}-\d{3}-\d{4}/.test(v);
 },
 message: '{VALUE} is not a valid phone number!'
 }
  }
});

这里,我们向 Schema 传递了一个 validate 方法。它接受一个验证器函数,我们可以在其中添加自己的验证代码。前面的方法检查用户的电话号码字段是否处于正确的格式。如果它未通过验证,则显示消息 {value} 不是一个有效的电话号码}

我们还可以在 Mongoose 中添加嵌套验证:例如,如果我们的用户集合中的名称保存为 { name: { first_name: 'Anita', last_name: 'Sharma' } },我们需要为 first_namelast_name 添加验证。为此,我们可以使用:

var nameSchema = new Schema({
  first_name: String,
  last_name: String
});

userSchema = new Schema({
  name: {
    type: nameSchema,
    required: true
  }
});

首先,我们定义一个低级对象的 Schema,它包括 first_namelast_name 字段。然后,对于 userSchema,我们将 nameSchema 传递给名称字段。

记住,我们无法在这个 Schema 中添加嵌套验证,如下所示:

var nameSchema = new Schema({
  first_name: String,
  last_name: String
});

personSchema = new Schema({
  name: {
    type: {
      first_name: String,
      last_name: String
    },
    required: true
  }
});

你可以在这里查看 Mongoose 验证的相关信息:mongoosejs.com/docs/validation.html

摘要

在本章中,我们介绍了 MongoDB 的基本信息和它的好处,如何在 MongoDB 中进行 CRUD 操作和查询,以及使用 Mongoose 的基本验证。

在下一章中,我们将进一步讨论我们的应用程序中的 REST API 和 RESTful 架构设计。

第四章:介绍 REST API

应用程序编程接口API)通常用于从一个应用程序获取数据到另一个应用程序。不同领域有不同的 API,例如硬件和编程,但我们将只讨论 Web API。Web API 是一种提供多个应用程序之间通信接口的 Web 服务。使用这些 API,一个应用程序的数据通过 HTTP 协议发送到另一个应用程序。

在本章中,我们将讨论:

  • REST 架构和 RESTful API

  • HTTP 动词和状态码

  • 使用 Postman 开发和测试 API

Web API 的工作方式与浏览器如何与我们的应用程序服务器交互相似。客户端从服务器请求一些数据,服务器以格式化的数据对客户端做出响应;API 做的是类似的事情。例如,多个应用程序之间事先有一个合同。所以,如果有两个应用程序需要共享数据,那么一个应用程序将向另一个应用程序提交一个请求,说明它需要以这种格式的数据。当另一个应用程序收到请求时,它会从其服务器获取数据,并以结构化和格式化的数据对客户端或请求者做出响应。

Web API 被分为简单对象访问协议SOAP)、远程过程调用RPC)或表示性状态转移REST)类别。这些 API 的响应格式可以是各种形式,如 XML、JSON、HTML、图片和视频。

API 也有不同的模型,例如公共 API 和私有 API:

  • 私有 API:私有或内部 API 仅用于组织内部的内部应用程序

  • 公共 API:公共或外部 API 设计的方式使得它们可以被组织外部的公众共享

什么是 REST?

REST 是一种通过 HTTP 协议在多个应用程序之间交换数据的 Web 服务。RESTful Web 服务是可扩展的且易于维护的。

这里有一个简单的图解,说明了 REST Web Service 是如何工作的:

图片

如图中所示,客户端通过调用 Rest Web Service Server 来请求一些数据。在这里,当我们发送一个 HTTP 请求时,我们也会提供一些头部信息,例如我们希望作为响应的数据类型。这些响应可以是 JSON、XML、HTML 或其他任何形式。当服务器接收到请求并从存储中提取数据时,它不会简单地以数据库资源的形式返回响应。它发送这些资源的表示。这就是为什么它被称为表示性。当服务器以这种格式化的数据对客户端做出响应时,我们应用程序的状态发生了变化。这就是为什么它被称为状态转移

介绍 REST API

REST API 是基于 RESTful 架构设计的。遵循 RESTful 架构原则构建的 API 被称为 RESTful API。RESTful 架构也被称为 无状态架构,因为客户端和服务器之间的连接不会被保留。在客户端和服务器之间每次事务之后,连接都会被重置。

由于存在多个网络服务,我们必须能够选择我们的需求和需求,以便为我们的应用程序构建完美的 API。SOAP 和 REST 协议都有一些优点和局限性。

SOAP 协议是在 1998 年由 Dave Winer 设计的。它使用 可扩展标记语言 (XML) 进行数据交换。是否使用 SOAP 或 REST 取决于我们在开发时选择的编程语言以及应用程序的需求。

REST API 允许我们在 JSON/XML 数据格式之间进行应用程序间的通信。JSON/XML 是一种易于格式化和易于人类阅读的数据表示。通过 RESTful API,我们可以从一个应用程序到另一个应用程序执行 创建读取更新删除CRUD) 操作。

REST API 的好处

REST API 提供了许多好处。以下是我们通过使用 REST API 可以获得的一些优势:

  • 从一个应用程序向另一个应用程序发送请求和获取响应非常容易。

  • 响应可以以 JSON 或 XML 的人类可读格式检索。

  • 所有的操作都是以 URI 的形式进行的,这意味着每个请求都通过 URI 来标识。

  • 客户端和服务器之间的分离使得在需要时可以轻松迁移到不同的服务器,并且更改最小。客户端和服务器之间的隔离也使得扩展变得容易。

  • 它与任何编程语言无关。无论我们使用 PHP、JAVA、Rails、Node.js 等何种编程语言,都可以实现 REST 架构。

  • 开始使用非常容易,学习曲线也很短。

HTTP 动词

HTTP 动词是用来定义我们对资源想要执行的操作的不同方法。最常用的 HTTP 动词是 GET、POST、PUT、PATCH 和 DELETE。HTTP 动词是使多个应用程序之间通信成为可能请求方法。这些 HTTP 动词使得在不需要完全更改 URL 的情况下对资源执行多个操作成为可能。让我们更详细地看看这些。

GET

GET 请求是无效请求。这在我们想要获取资源信息时使用。这不会修改或删除资源。GET 请求的等效 CRUD 操作是 READ,这意味着它只获取信息,仅此而已。一个 GET 请求的示例 URL 是:

  • 要获取所有记录:
GET http://www.example.com/users
  • 要获取单个用户的信息:
GET http://www.example.com/users/{user_id}

POST

对于POST请求的等效 CRUD 操作是CREATE。这用于向集合中添加新记录。由于这会改变服务器状态,因此这不是一个幂等请求。如果我们两次以相同的参数请求POST方法,那么将在数据库中创建两个相同的新资源。以下是一个POST请求的示例 URL:

POST http://www.example.com/users/

PUT

PUT请求用于创建或更新记录。如果资源尚不存在,则创建新记录;如果资源已存在,则更新现有记录。等效的 CRUD 操作是update()。它替换了资源的现有表示。以下是一个PUT请求的示例 URL:

PUT http://www.example.com/users/

DELETE

这用于从集合中删除资源。等效的 CRUD 操作是delete()

以下是一个DELETE请求的示例 URL:

DELETE http://www.example.com/users/{user_id}

HTTP 状态码

状态码是服务器对请求做出的响应的一部分。它表示请求的状态,无论它是否成功执行。状态码有三个数字。第一个数字表示该响应的类别或类别。HTTP 状态码的范围是100-500。在本节中,我们将介绍一些主要的状态码。

2XX 代码

200 范围内的状态码是 API 中任何请求的成功范围。在 200 范围内,有许多代码表示不同的成功形式。以下是可用的许多状态码中的一些解释:

  • 200 OK:这是一个标准响应。它只是表示请求成功。此状态码还返回请求执行的资源。

  • 201 Created:这表示资源的成功创建。

  • 204 No Content:此状态码成功执行请求,但不返回任何内容。

4XX 代码

当客户端发生错误时,会出现 400 范围内的状态码:

  • 400 Bad Request:当请求参数格式不正确或语法错误时,服务器会返回 400 状态码。

  • 401 Unauthorized:当未经授权的方尝试发送 API 请求时,会返回此状态码。这基本上检查了认证部分。

  • 403 Forbidden:这与 401 有些相似。这检查执行 API 请求的方的授权。这通常在执行 API 的不同用户有不同的权限设置时进行。

  • 404 Not Found:当服务器在数据库中找不到我们尝试执行某些操作的资源时,会返回此响应。

5XX 代码

状态码 500 表示在给定资源执行的动作执行过程中存在问题:

  • 500 Internal Server Error:当动作未成功执行时,会显示此状态码。与 200 状态码一样,这是服务器在出现问题时返回的通用代码。

  • 503 服务不可用:当我们的服务器没有运行时,会显示这个状态码。

  • 504 网关超时:这表示请求已发送到服务器,但在给定时间内没有收到任何响应。

介绍 Postman

Postman 是一个让我们更快地开发和测试我们的 API 的工具。这个工具提供了一个 GUI,使得调整我们的 API 变得容易,从而减少了我们 API 的开发时间。我们还可以通过创建所有已开发的 API 的集合来维护历史记录。

Postman 也有不同的替代方案,例如 Runscope 和 Paw。我们将在这本书中使用 Postman。

安装 Postman

使用 Postman 有不同的方式:

  1. 我们可以通过以下方式获取 Chrome 扩展程序:如果您访问 chrome.google.com/webstore/detail/postman/fhbjgbiflinjbdggehcddcbncdddomop?hl=en,我们会看到以下内容:

点击“添加到 Chrome”按钮,扩展程序将被安装。

  1. 我们可以通过以下方式下载适合我们操作系统的桌面应用程序:

    www.getpostman.com/.

我们在这本书中使用了桌面应用程序。

使用 Postman 测试 API

首先,让我们快速回顾一下到目前为止我们已经做了什么。在我们正在构建的应用程序中,app.js 文件应该包含以下代码:

var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var fs = require('file-system');
var mongoose = require('mongoose');

var app = express();
var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/tutorial2', {
  useMongoClient: true
});
var db = mongoose.connection;
db.on("error", console.error.bind(console, "connection error"));
db.once("open", function(callback){
  console.log("Connection Succeeded");
});

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');

// uncomment after placing our favicon in /public
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

// Include controllers
fs.readdirSync("controllers").forEach(function (file) {
  if(file.substr(-3) == ".js") {
    const route = require("./controllers/" + file)
    route.controller(app)
  }
})

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  var err = new Error('Not Found');
  err.status = 404;
  next(err);
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

app.listen(3000, function() {
  console.log('listening on 3000')
})

由于此文件是在我们通过命令行构建应用程序时自动生成的,它使用 TypeScript 语法。如果我们想使用 ES 6 语法,我们可以将 var 替换为 const

在我们的 models/User.js 文件中,我们有以下内容:

const mongoose = require("mongoose")
const Schema = mongoose.Schema
const UserSchema = new Schema({
 name: String,
 email: String
})

const User = mongoose.model("User", UserSchema)
module.exports = User

此外,在 controllers/users.js 文件中,我们有以下内容:

module.exports.controller = (app) => {
  // get homepage
  app.get('/users', (req, res) => {
    res.render('index', { title: 'Users' });
  })
}

在用户控制器中添加 GET 端点

让我们在 controllers/users.js 中添加一个路由,该路由将从数据库中获取所有用户的记录。

目前,根据我们 users 控制器中的代码,当我们访问 http://localhost:3000/users 时,它只返回一个标题,Users。让我们修改这段代码以包含一个 GET 请求来获取所有用户请求。

获取所有用户

首先,使用 $ nodemon app.js 启动服务器。现在,在 controllers/users.js 中:

var User = require("../models/User");

module.exports.controller = (app) => {
  // get all users
  app.get('/users', (req, res) => {
    User.find({}, 'name email', function (error, users) {
      if (error) { console.log(error); }
      res.send(users);
    })
  })
}

现在我们已经将代码设置好了,让我们使用 Postman 应用程序测试这个端点。在 Postman 应用程序中,在 URL 中添加必要的详细信息。当我们点击发送按钮时,我们应该看到以下响应:

_id 是用户的 MongoDB ID,这是 Mongoose 查询默认发送的,我们正在获取用户的名字和电子邮件。如果我们只想获取名字,我们可以在 users 控制器中更改我们的查询以只获取名字。

Postman 允许我们编辑端点,并且请求的开发变得容易。如果我们想使用自己的本地浏览器进行测试,我们也可以这样做。

我使用了一个名为 JSONview 的 Chrome 插件来格式化 JSON 响应。您可以从这里获取插件:

chrome.google.com/webstore/detail/jsonview/chklaanhfefbnpoihckbnefhakgolnmc.

如我之前所述,如果我们访问http://localhost:3000/users,我们应该能够看到类似以下的内容:

我们可以使用 Postman 提供的save查询功能在将来运行这些查询。只需点击右上角的保存按钮,然后在我们继续前进的过程中创建新的查询。

获取单个用户

如 HTTP 动词部分所述,要从集合中获取单个记录,我们必须在参数中传递用户的id以获取用户详细信息。从先前的 Postman 响应示例中,让我们选择一个id并使用它来获取用户的记录。首先,让我们在我们的控制器中添加端点。在controllers/users.js中添加以下代码行:

var User = require("../models/User");

module.exports.controller = (app) => {
  // get all users
  app.get('/users', (req, res) => {
    User.find({}, 'name email', function (error, users) {
      if (error) { console.log(error); }
       res.send({
        users: users
      })
    })
  })

  //get a single user details
 app.get('/users/:id', (req, res) => {
 User.findById(req.params.id, 'name email', function (error, user) {
 if (error) { console.log(error); }
 res.send(user)
 })
 })
}

现在,在 Postman 中创建一个新的查询,以下参数。我们将创建一个带有 URL http://localhost:3000/users/:user_idGET请求,其中user_id是您在数据库中创建的任何用户的id。使用此设置,我们应该能够看到如下内容:

查询应返回 URL 中给定 ID 的用户详细信息。

在用户控制器中添加 POST 端点

让我们看看一个例子。让我们创建一个 API,它将使用 MongoDB 的insert()命令将用户资源保存到数据库中。在用户控制器中添加一个新的端点:

// add a new user
  app.post('/users', (req, res) => {
    const user = new User({
      name: req.body.name,
      email: req.body.email
    })

    user.save(function (error, user) {
      if (error) { console.log(error); }
      res.send(user)
    })
  })

在 Postman 中,将方法设置为POST,URL 设置为http://localhost:3000/users,将参数设置为原始 JSON,并输入以下内容:

{
 "name": "Dave",
 "email": "dave@mongo.com"
}

GET请求不同,我们必须在body参数中传递我们想要添加的用户的名字和电子邮件。现在,如果我们运行一个GET all users查询,我们应该能够看到这个新用户。如果我们用相同的参数运行两次POST请求,那么它将创建两个不同的资源。

在用户控制器中添加 PUT 端点

让我们更新一个 ID 为5a3153d7ba3a827ecb241779的用户(将此 ID 更改为您的文档 ID),这是我们刚刚创建的。让我们重命名电子邮件:为此,首先让我们在我们的用户控制器中添加端点,换句话说,在controllers/user.js中:

// update a user
  app.put('/users/:id', (req, res) => {
    User.findById(req.params.id, 'name email', function (error, user) {
      if (error) { console.error(error); }

      user.name = req.body.name
      user.email = req.body.email
      user.save(function (error, user) {
        if (error) { console.log(error); }
        res.send(user)
      })
    })
  })

我们在这里所做的是,我们添加了一个用于PUT请求的端点,它接受名称和电子邮件作为参数并将其保存到数据库中。相应的 Postman 看起来如下所示:

在这里,我们可以看到用户的名字已经被更新。而且,如果我们查看请求参数,我们还添加了一个age参数。但由于我们在定义用户模型时没有添加age,它丢弃了年龄值但更新了其余部分。

我们还可以使用 PATCH 方法来更新资源。PUTPATCH 方法的区别是:PUT 方法更新整个资源,而 PATCH 用于对资源进行部分更新。

在用户控制器中添加 DELETE 端点

同样地,对于删除操作,让我们在 controllers/users.js 中添加一个端点:

// delete a user
  app.delete('/users/:id', (req, res) => {
    User.remove({
      _id: req.params.id
    }, function(error, user){
      if (error) { console.error(error); }
      res.send({ success: true })
    })
  })

以下代码获取用户的 ID,并从数据库中删除具有给定 ID 的用户。在 Postman 中,端点将如下所示:

摘要

在本章中,我们学习了什么是 RESTful API,不同的 HTTP 动词和状态码,以及如何使用 Postman 开发和测试 RESTful API。

在下一章中,我们将进入 Vue.js 的介绍,并使用 Vue.js 构建一个应用程序。

第五章:构建真实的应用程序

我们已经涵盖了构建全栈 JavaScript 应用程序所需了解的基本组件。从现在开始,我们将使用所有这些技术来构建一个完整的 Web 应用程序。

在这本书中,我们将构建一个具有以下功能的电影评分应用程序:

  • 一个列出所有电影及其其他属性的首页

  • 将会有一个管理员部分,管理员将能够添加电影

  • 用户将能够登录和注册

  • 用户将能够对电影进行评分

  • 将会有一个电影简介部分,登录用户可以对该电影进行评分

那么,让我们开始吧。

介绍 Vue.js

Vue.js 是一个用于构建用户界面的开源、渐进式 JavaScript 框架。新 JavaScript 框架的兴起是巨大的。随着这种增长,你可能会感到困惑,不知道从哪里开始以及如何开始。今天有数百个 JavaScript 框架;其中,有数十个框架脱颖而出。但仍然,从这些数十个中选择可能是一项艰巨的任务。

今天有很多流行的框架,例如 React、Ember 和 Angular。虽然这些框架有其自身的优点,但也存在一些局限性。虽然使用 React 或 Angular 构建应用程序本身是好的,但 Vue.js 有助于消除与这些框架相关的一些局限性。

Vue.js 是渐进式的。使用 Vue.js,你可以从小型应用开始,然后逐步构建更大的应用程序。这意味着如果你是初学者,你可能想从一个非常小的应用程序开始,并逐步扩展。Vue.js 非常适合这样的应用程序。它轻量级且灵活。学习曲线也非常容易,并且非常容易上手。

Vue.js 是由 Evan You 发明的。它首次发布于 2014 年 2 月,并在 2016 年左右获得了巨大的流行度。他曾经为谷歌工作,并在 Angular 项目中工作。发明这个技术的动机主要是因为他不想在小型项目中使用 Angular,因为 Angular 提供了大量的内置包,因此不够轻量级,不适合小型应用程序。话虽如此,Vue.js 并不仅针对小型应用程序。它确实不提供所有的包,但你可以随着应用程序的进展逐步添加它们。这就是 Vue.js 的美丽之处。

安装 Vue.js

让我们从 Vue.js 的安装开始。安装和使用 Vue.js 有三种方式。

<script>标签中包含它

使用 Vue.js 最简单的方法是下载它并将其包含在<script>标签中。你可以从cdn.jsdelivr.net/npm/vue下载:

<script type="text/javascript" src="img/vue.js"></script>

使用内容分发网络(CDN)直接链接

CDN 是一个分布式服务器网络。它在不同的地理位置存储内容的缓存版本,以便在获取内容时加载更快。我们可以在 script 标签中直接使用 CDN 链接:

<script type="text/javascript" src="img/vue.js"></script>

将 Vue.js 作为 npm 包使用

npm 也为 vue 提供了一个包,可以按照以下步骤进行安装:

$ npm install vue

介绍 vue-cli

CLI 代表命令行界面。一个 cli 在命令行界面中连续运行一个或多个命令。Vue.js 也有一个 cli,当安装后,可以非常容易地启动项目。我们将在这本书中使用 vue-cli 来创建 Vue.js 应用程序。让我们使用以下命令安装 vue-cli。您可以在根目录中执行此命令:

$ npm install -g vue-cli

使用 vue-cli 初始化项目

让我们继续创建一个新的项目文件夹,用于我们的电影评分应用。我们将称之为 movie_rating_app。在终端中转到您想要创建应用程序的目录,并运行以下命令:

$ vue init webpack movie_rating_app

上述命令初始化了一个包含 Vue.js 项目所需所有依赖的应用程序。它将询问您一些有关项目设置的问题,您可以选择回答 y(表示是)或 n(表示否):

  • Vue 构建选项:您将找到两个构建 Vue.js 应用的选项:运行时 + 编译器,或仅运行时。这与模板编译器有关:

    • 仅运行时:运行时选项用于创建 vue 实例。此选项不包括模板编译器。

    • 运行时 + 编译器:此选项包括模板编译器,这意味着 vue 模板将被编译成普通的 JavaScript 渲染函数。

  • Vue-router:Vue-router 是 Vue.js 应用的官方路由器。此选项特别用于我们想要将我们的应用程序制作成 单页应用SPA)的情况。当使用此选项时,应用程序在页面首次加载时一次性发出所有必要的请求,并在需要新数据时向服务器发送请求。我们将在未来的章节中更多地讨论单页和多页应用。现在,我们将使用 Vue-router。

  • ESLint:ESLint 是一个 JavaScript 代码检查工具。它是一个静态代码分析工具,用于查找代码中的错误或错误。它基本上确保代码遵循标准指南。选择 ESLint 也有两种选项:标准检查或 Airbnb 检查。我们将在这个项目中使用 Airbnb。

  • 设置测试:通过设置测试,项目为我们将为应用程序编写的测试创建了一个包装器。它创建了测试代码所需的必要结构和配置,以便能够运行。我们也将使用此选项。对于测试运行器,我们将使用 Mocha 和 Karma,对于端到端测试,我们将使用 Nightwatch,我们将在后续章节中学习它。

  • 依赖管理:最后,为了管理包和依赖项,这里有两种选项:npmYarn。我们在前面的章节中主要讨论了 npmYarn 也是一个与 npm 类似的依赖管理工具。Yarn 和 npm 都有自己的优点,但在这个应用程序中,我们将使用 npm。你可以在这里了解更多关于 Yarn 的信息 (yarnpkg.com/en/)。

这将花费一些时间,因为它将安装所有依赖项。以下是我们在应用程序中选择的选项:

图片

当命令成功执行后,你应该能在你的终端上看到进一步的步骤:

图片

如果构建成功,我们将能够看到前面的输出。现在,让我们按照终端上的指示操作:

$ cd movie_rating_app
$ npm run dev

这将启动你的应用程序。Vue.js 应用程序的默认端口是 8080。正如你在终端中看到的那样,它应该显示:

图片

打开浏览器并访问 URL http://localhost:8080/#/,我们应该能够看到我们的应用程序:

图片

干得好!这很简单。你已经成功创建并运行了一个 Vue.js 应用程序。

项目文件夹结构

现在,如果你已经注意到,vue-cli 命令为你的应用程序添加了一堆依赖项,这些依赖项列在 package.json 文件中。cli 命令还设置了一个你可以根据需要定制的文件夹结构。让我们回顾并理解 cli 为我们创建的结构:

图片

  • build 文件夹:这个文件夹包含不同环境(开发、测试和生产)的 webpack 配置文件

  • config 文件夹:所有应用程序的配置都会放在这里

  • node_modules: 我们安装的所有 npm 包都位于这个文件夹中

  • src: 这个文件夹包含所有与在浏览器中渲染组件相关的文件:

    • assets: 你可以在该文件夹内添加你的应用程序的 CSS 和图片。

    • components: 这个文件夹将存放所有具有 .vue 扩展名的前端渲染文件。

    • router: 这个文件夹将负责应用程序中不同页面的所有 URL 路由。

    • App.vue: 你可以将 App.vue 视为渲染视图文件的主组件。其他文件将扩展此文件上定义的布局以创建不同的视图。

    • main.js: 这是任何 Vue.js 应用程序的主入口点。

  • Static: 你也可以使用这个文件夹来保存你的静态文件,例如 CSS 和图片。

  • Test: 这个文件夹将用于处理我们应用程序编写的所有测试。

使用 Vue.js 构建静态应用程序

现在我们已经初始化了一个项目,让我们继续创建一个静态的 Web 应用程序。别忘了在 GitHub 上创建一个仓库,并定期提交和推送更改。

当你访问 URL http://localhost:8080/#/ 时,你将看到一个默认页面被渲染。这段代码是写在 src/components/HelloWorld.vue 中的。

如果你查看 build/webpack.base.conf.js,你将在 module.exports 部分看到以下代码行:

module.exports = {
  context: path.resolve(__dirname, '../'),
  entry: {
    app: './src/main.js'
  },
  output: {

这意味着,当你运行应用时,这个 main.js 将是应用的入口点。一切都将从这里开始。让我们快速查看 src 中的 main.js 文件:

// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue';
import App from './App';
import router from './router';

Vue.config.productionTip = false;

/* eslint-disable no-new */
new Vue({
 el: '#app',
 router,
 template: '<App/>',
 components: { App },
});

前三条语句导入了此应用运行所需的必要包。App.vue 是此应用的主模板布局。所有其他 .vue 文件都将扩展此布局。

底部块定义了运行应用时渲染哪个组件。在这种情况下,这是告诉我们的应用将模板 <App> 渲染在 #app 元素内。现在,如果我们查看 App.vue

<template>
 <div id="app">
 <img src="img/logo.png">
 <router-view/>
 </div>
</template>

<script>
export default {
 name: 'app',
};
</script>

<style>
#app {
 font-family: 'Avenir', Helvetica, Arial, sans-serif;
 -webkit-font-smoothing: antialiased;
 -moz-osx-font-smoothing: grayscale;
 text-align: center;
 color: #2c3e50;
 margin-top: 60px;
}
</style>

这里有一个包含 ID #appdiv 元素的模板。这意味着我们创建的 vue 模板将被渲染在这里。

重新定义主页

让我们为主页创建自己的视图页面。为此,我们只需修改 HelloWorld.vue 组件。.vue 文件应该始终以模板开始。因此,此文件的基本模板如下:

<template>
 <div>
 </div>
</template>

你也可以在这个页面上包含你的样式表和 JavaScript 代码定义,但如果我们将这些代码分离到其他地方,页面会显得更整洁。

让我们从 HelloWorld.vue 中删除所有内容,并添加以下代码行:

<template>
 <div>
 Hello World
 </div>
</template>

我们也不需要 Vue.js 的标志,所以让我们也从 src/assets 中删除它,并在 App.vue 中的代码行:

<img src="img/logo.png">

现在,如果你重新访问 URL http://localhost:8080/#/,你将看到 Hello World 被渲染:

分离 CSS

是时候分离 CSS 了。让我们在 src/assets 文件夹内创建一个名为 stylesheets 的文件夹,并添加一个 main.css 文件。在 main.css 中添加以下行:

@import './home.css';

main.css 将是我们的主要 CSS 文件,它包含所有其他 CSS 组件。我们也可以直接在这里添加所有样式代码。但为了保持可读性,我们将为应用中的不同部分创建单独的样式表并将它们导入此处。

由于我们将在这里导入所有样式表,现在我们只需要在主应用中包含 main.css 文件,以便它被加载。为此,让我们在 src/App.vue 中添加以下代码行:

<template>
  <div id="app">
    <router-view/>
  </div>
</template>

<script>
import './assets/stylesheets/main.css'; 
export default {
  name: 'App',
};
</script>

我们在 main.css 中导入了名为 home.css 的样式表,但该样式表尚未存在。因此,让我们在同一个目录 src/assets 中创建它。此外,让我们从 App.vue 中删除以下代码段并将其粘贴到 home.css 文件中,以便我们的组件更简洁:

#app {
 font-family: 'Avenir', Helvetica, Arial, sans-serif;
 -webkit-font-smoothing: antialiased;
 -moz-osx-font-smoothing: grayscale;
 text-align: center;
 color: #2c3e50;
 margin-top: 60px;
 width: 100%;
}

Vuetify 简介

Vuetify 是一个可以用于为 Vue.js 应用构建物质化网页设计的模块。它提供了一些可以作为我们应用构建块使用的功能。它是一个类似于 Bootstrap 的 UI 框架,但它主要包含材料组件。更多详情,您可以访问这个链接vuetifyjs.com

在构建应用时,我们将同时使用 Vuetify 和 Bootstrap。第一步是安装包:

$ npm install bootstrap bootstrap-vue vuetify --save

安装完这些之后,接下来我们需要在我们的主文件中引入这些包。所以,在src/main.js文件中,添加以下代码行:

// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import 'bootstrap/dist/css/bootstrap.min.css';
import 'bootstrap-vue/dist/bootstrap-vue.css';
import BootstrapVue from 'bootstrap-vue'; 
import Vue from 'vue';
import Vuetify from 'vuetify';
import App from './App';
import router from './router';

Vue.use(BootstrapVue);
Vue.use(Vuetify);

Vue.config.productionTip = false;

/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  components: { App },
  template: '<App/>',
});

我们还需要使用vuetify.css,它包含了所有与其设计相关的样式表。我们也将需要它。我们可以简单地链接一个样式表来实现这一点。在index.html文件中,在你的head部分添加以下代码行:

...
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link href="https://unpkg.com/vuetify/dist/vuetify.min.css" rel="stylesheet">
    <title>movie_rating_app</title>
  </head>
...

Vuetify 很好地使用了材料图标,因此也要导入字体。在index.html中同样添加以下代码行:

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link href="https://unpkg.com/vuetify/dist/vuetify.min.css" rel="stylesheet">
    <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons" rel="stylesheet">
    <title>movie_rating_app</title>
  </head>

使用 Vuetify 重新设计页面

现在我们有了 Vuetify,让我们继续创建应用页面。它还提供了一些预定义的主题。我们将为应用使用一个非常简单和极简主义的主题。当然,我们也可以根据我们的需求进行自定义。

本节的结果将如下所示:

重新设计主页

在我们的App.vue中,将文件内容替换为以下代码:

<template>
 <v-app id="inspire">
 <v-navigation-drawer
 fixed
 v-model="drawer"
 app
 >
 <v-list dense>
 <router-link v-bind:to="{ name: 'Home' }" class="side_bar_link">
 <v-list-tile>
 <v-list-tile-action>
 <v-icon>home</v-icon>
 </v-list-tile-action>
 <v-list-tile-content>Home</v-list-tile-content>
 </v-list-tile>
 </router-link>
 <router-link v-bind:to="{ name: 'Contact' }" class="side_bar_link">
 <v-list-tile>
 <v-list-tile-action>
 <v-icon>contact_mail</v-icon>
 </v-list-tile-action>
 <v-list-tile-content>Contact</v-list-tile-content>
 </v-list-tile>
 </router-link>
 </v-list>
 </v-navigation-drawer>
 <v-toolbar color="indigo" dark fixed app>
 <v-toolbar-side-icon @click.stop="drawer = !drawer"></v-toolbar-side-icon>
 <v-toolbar-title>Home</v-toolbar-title>
 </v-toolbar>
 <v-content>
 <v-container fluid>
 <div id="app">
 <router-view/>
 </div>
 </v-container>
 </v-content>
 <v-footer color="indigo" app>
 <span class="white--text">&copy; 2018</span>
 </v-footer>
 </v-app>
</template>

<script>
import './assets/stylesheets/main.css';

export default {
 data: () => ({
 drawer: null,
 }),
 props: {
 source: String,
 },
};
</script>

这包含了一些主要以v-开头的标签。这些是 Vuetify 提供的标签,用于定义我们的 UI 块。我们与前面的文件一起附加了一个名为main.cssstylesheet文件。让我们给我们的App.vue页面添加一些样式。

将以下代码添加到src/assets/stylesheets/home.css中:

#app {
 font-family: 'Avenir', Helvetica, Arial, sans-serif;
 -webkit-font-smoothing: antialiased;
 -moz-osx-font-smoothing: grayscale;
 text-align: center;
 color: #2c3e50;
}

#inspire {
 font-family: 'Avenir', Helvetica, Arial, sans-serif;
}

.container.fill-height {
 align-items: normal;
}

a.side_bar_link {
 text-decoration: none;
}

我们仍然有一个包含 ID 为 app 的div部分。这是所有其他.vue文件将被渲染的部分。

现在,在HelloWorld.vue中,将内容替换为以下内容:

<template>
 <v-layout>
 this is home
 </v-layout>
</template>

现在,如果你访问http://localhost:8080/#/,你应该能够查看主页。

重新设计联系页面

让我们继续添加一个新的联系页面。首先要做的是在我们的路由文件中添加一个路由。在router/index.js中添加以下代码:

import Vue from 'vue';
import Router from 'vue-router';
import HelloWorld from '@/components/HelloWorld';
import Contact from '@/components/Contact';

Vue.use(Router);

export default new Router({
  routes: [
    {
      path: '/',
      name: 'HelloWorld',
      component: HelloWorld,
    },
 {
 path: '/contact',
 name: 'Contact',
 component: Contact,
 },
  ],
});

我们在这里所做的是为联系页面添加了一个路径,组件的名称,这是我们之前在.vue文件中的导出模块中做的,以及组件的实际名称。现在我们需要构建一个视图文件。所以,让我们在src/components/中创建一个Contact.vue文件,并添加以下内容:

<template>
 <v-layout>
 this is contact
 </v-layout>
</template>

现在,访问http://localhost:8080/#/contact,你应该能够查看这两个页面。

为了使它对我们应用来说既可用又易于阅读,让我们将HelloWorld组件重命名为Home组件。将文件HelloWorld.vue重命名为Home.vue

同时,在App.vue中将绑定路由从HelloWorld更改为Home

<template>
  <v-app id="inspire">
    <v-navigation-drawer
      fixed
      v-model="drawer"
      app
    >
      <v-list dense>
 <router-link v-bind:to="{ name: 'Home' }" class="side_bar_link">
          <v-list-tile @click="">
            <v-list-tile-action>
              <v-icon>home</v-icon>

routes/index.js中,同样将组件名称和路由从HelloWorld更改为Home

import Vue from 'vue';
import Router from 'vue-router';
import Home from '@/components/Home';
import Contact from '@/components/Contact';

Vue.use(Router);

export default new Router({
  routes: [
    {
      path: '/',
      name: 'Home',
      component: Home,
    },
    {
      path: '/contact',
      name: 'Contact',
      component: Contact,
    },
  ],
});

当我们访问 URL http://localhost:8080/#/ 时,我们应该能看到类似以下内容:

图片

就这样。你已经成功创建了一个基本的静态两页网页应用!

理解 Vue.js 组件

vue 组件与你在应用中编写的 HTML 文件等效。你可以在 .vue 文件中编写纯 HTML 语法。唯一需要注意的事情是,需要将所有内容包裹在 <template></template> 中。

Vue.js 指令

指令与标记语言一起使用,在 DOM 元素上执行一些功能。例如,在 HTML 标记语言中,当我们写入:

<div class='app'></div>

这里使用的 class 是 HTML 语言的指令。同样,Vue.js 也提供了许多这样的指令来简化应用开发,例如:

  • v-text

  • v-on

  • v-ref

  • v-show

  • v-pre

  • v-transition

  • v-for

v-text

当你需要动态定义一些变量来显示时,可以使用 v-text。让我们通过一个例子来看一下。在 src/components/Home.vue 中,让我们添加以下内容:

<template>
  <v-layout>
    <div v-text="message"></div>
  </v-layout>
</template>
<script type="text/javascript">
export default {
 data() {
 return {
 message: 'Hello there, how are you this morning?',
 };
 },
};
</script>

script 标签内的代码是一个数据变量,它将定义在其中的数据绑定到该组件。当你改变该变量的值 message 时,带有该指令的 div 元素也会更新。

如果我们访问 URL (http://localhost:8080/#/),我们可以看到以下内容:

图片

v-on

这个指令用于事件处理。我们可以用它来触发应用中的某些逻辑。例如,假设我们想要回复上一个例子中的一个问题,为此我们可以这样做。将 src/components/Home.vue 中的代码更改为以下内容:

<template>
  <v-layout row wrap>
 <v-flex xs12>
 <div v-text="message"></div>
 </v-flex>
 <v-flex xs12>
 <v-btn color="primary" v-on:click="reply">Reply</v-btn>
 </v-flex>
 </v-layout>
</template>
<script type="text/javascript">
export default {
  data() {
    return {
      message: 'Hello there, how are you this morning?',
    };
  },
  methods: {
 reply() {
 this.message = "I'm doing great. Thank You!";
 },
 },
};
</script>

第一屏将如下所示:

图片

当你点击回复时,你会看到以下内容:

图片

这些是我们将在应用中主要使用的指令。还有很多其他的指令,我们将在学习过程中探索。如果你想了解更多关于每个指令的信息,你可以访问 https://012.vuejs.org/api/directives.html

数据绑定

数据绑定是同步数据的过程。例如,对于我们在 v-text 中所做的相同示例,我们可以使用数据绑定与模板符号,换句话说,使用 {{}} 操作符。

例如,我们可以使用 {{message}} 而不是使用 Vue.js 指令来显示消息。让我们将 src/components/Home.vue 中的代码更改为以下内容:

<template>
  <v-layout row wrap>
    <v-flex xs12>
      <div>{{message}}</div>
    </v-flex>
    <v-flex xs12>
      <v-btn color="primary" v-on:click="reply">Reply</v-btn>
    </v-flex>
  </v-layout>
</template>
<script type="text/javascript">
  export default {
    data () {
      return {
        message: 'Hello there, how are you?',
      }
    },
    methods: {
      reply () {
        this.message = "I'm doing great. Thank You!"
      }
    }
  }
</script>

这将表现得与我们在 v-text 中所做的一样。

使用 Vue.js 处理表单

现在我们对 Vue.js 的工作原理有了基本的了解,让我们继续我们的第一个表单,我们将添加电影的详细信息并在主页上显示这些电影,以便用户可以查看。

创建电影列表页面

首先,让我们从为我们的主页创建静态电影卡片开始,我们将在下一步使这些数据动态化。在Home.vue中,将template内的内容替换为以下代码:

<template>
 <v-layout row wrap>
 <v-flex xs4>
 <v-card>
 <v-card-title primary-title>
 <div>
 <div class="headline">Batman vs Superman</div>
 <span class="grey--text">2016 ‧ Science fiction film/Action fiction ‧ 3h 3m</span>
 </div>
 </v-card-title>
 <v-card-text>
 It's been nearly two years since Superman's (Henry Cavill) colossal battle with Zod (Michael Shannon) devastated the city of Metropolis. The loss of life and collateral damage left many feeling angry and helpless, including crime-fighting billionaire Bruce Wayne (Ben Affleck). Convinced that Superman is now a threat to humanity, Batman embarks on a personal vendetta to end his reign on Earth, while the conniving Lex Luthor (Jesse Eisenberg) launches his own crusade against the Man of Steel.
 </v-card-text>
 <v-card-actions>
 <v-btn flat color="purple">Rate this movie</v-btn>
 <v-spacer></v-spacer>
 </v-card-actions>
 </v-card>
 </v-flex>
 <v-flex xs4>
 <v-card>
 <v-card-title primary-title>
 <div>
 <div class="headline">Logan</div>
 <span class="grey--text">2017 ‧ Drama/Science fiction film ‧ 2h 21m</span>
 </div>
 </v-card-title>
 <v-card-text>
 In the near future, a weary Logan (Hugh Jackman) cares for an ailing Professor X (Patrick Stewart) at a remote outpost on the Mexican border. His plan to hide from the outside world gets upended when he meets a young mutant (Dafne Keen) who is very much like him. Logan must now protect the girl and battle the dark forces that want to capture her.
 </v-card-text>
 <v-card-actions>
 <v-btn flat color="purple">Rate this movie</v-btn>
 <v-spacer></v-spacer>
 </v-card-actions>
 </v-card>
 </v-flex>
 <v-flex xs4>
 <v-card>
 <v-card-title primary-title>
 <div>
 <div class="headline">Star Wars: The Last Jedi</div>
 <span class="grey--text">2017 ‧ Fantasy/Science fiction film ‧ 2h 35m</span>
 </div>
 </v-card-title>
 <v-card-text>
 Luke Skywalker's peaceful and solitary existence gets upended when he encounters Rey, a young woman who shows strong signs of the Force. Her desire to learn the ways of the Jedi forces Luke to make a decision that changes their lives forever. Meanwhile, Kylo Ren and General Hux lead the First Order in an all-out assault against Leia and the Resistance for supremacy of the galaxy.
 </v-card-text>
 <v-card-actions>
 <v-btn flat color="purple">Rate this movie</v-btn>
 <v-spacer></v-spacer>
 </v-card-actions>
 </v-card>
 </v-flex>
 <v-flex xs4>
 <v-card>
 <v-card-title primary-title>
 <div>
 <div class="headline">Wonder Woman</div>
 <span class="grey--text">2017 ‧ Fantasy/Science fiction film ‧ 2h 21m</span>
 </div>
 </v-card-title>
 <v-card-text>
 Before she was Wonder Woman (Gal Gadot), she was Diana, princess of the Amazons, trained to be an unconquerable warrior. Raised on a sheltered island paradise, Diana meets an American pilot (Chris Pine) who tells her about the massive conflict that's raging in the outside world. Convinced that she can stop the threat, Diana leaves her home for the first time. Fighting alongside men in a war to end all wars, she finally discovers her full powers and true destiny.
 </v-card-text>
 <v-card-actions>
 <v-btn flat color="purple">Rate this movie</v-btn>
 <v-spacer></v-spacer>
 </v-card-actions>
 </v-card>
 </v-flex>
 <v-flex xs4>
 <v-card>
 <v-card-title primary-title>
 <div>
 <div class="headline">Dunkirk</div>
 <span class="grey--text">2017 ‧ Drama/Thriller ‧ 2 hours</span>
 </div>
 </v-card-title>
 <v-card-text>
 In May 1940, Germany advanced into France, trapping Allied troops on the beaches of Dunkirk. Under air and ground cover from British and French forces, troops were slowly and methodically evacuated from the beach using every serviceable naval and civilian vessel that could be found. At the end of this heroic mission, 330,000 French, British, Belgian and Dutch soldiers were safely evacuated.
 </v-card-text>
 <v-card-actions>
 <v-btn flat color="purple">Rate this movie</v-btn>
 <v-spacer></v-spacer>
 </v-card-actions>
 </v-card>
 </v-flex>
 <v-flex xs4>
 <v-card>
 <v-card-title primary-title>
 <div>
 <div class="headline">The Revenant</div>
 <span class="grey--text">2015 ‧ Drama/Thriller ‧ 2h 36m</span>
 </div>
 </v-card-title>
 <v-card-text>
 While exploring the uncharted wilderness in 1823, frontiersman Hugh Glass (Leonardo DiCaprio) sustains life-threatening injuries from a brutal bear attack. When a member (Tom Hardy) of his hunting team kills his young son (Forrest Goodluck) and leaves him for dead, Glass must utilize his survival skills to find a way back to civilization. Grief-stricken and fueled by vengeance, the legendary fur trapper treks through the snowy terrain to track down the man who betrayed him.
 </v-card-text>
 <v-card-actions>
 <v-btn flat color="purple">Rate this movie</v-btn>
 <v-spacer></v-spacer>
 </v-card-actions>
 </v-card>
 </v-flex>
 </v-layout>
</template>

此外,按照以下内容替换home.css中的内容:

#app {
 font-family: 'Avenir', Helvetica, Arial, sans-serif;
 -webkit-font-smoothing: antialiased;
 -moz-osx-font-smoothing: grayscale;
 text-align: center;
 color: #2c3e50;
 width: 100%;
}

#inspire {
 font-family: 'Avenir', Helvetica, Arial, sans-serif;
}

.container.fill-height {
 align-items: normal;
}

a.side_bar_link {
 text-decoration: none;
}

.card__title--primary, .card__text {
 text-align: left;
}

.card {
 height: 100% !important;
}

此外,在App.vue中,将内容替换为以下内容:

<template>
 <v-app id="inspire">
 <v-navigation-drawer
 fixed
 v-model="drawer"
 app
 >
 <v-list dense>
 <router-link v-bind:to="{ name: 'Home' }" class="side_bar_link">
 <v-list-tile>
 <v-list-tile-action>
 <v-icon>home</v-icon>
 </v-list-tile-action>
 <v-list-tile-content>Home</v-list-tile-content>
 </v-list-tile>
 </router-link>
 <router-link v-bind:to="{ name: 'Contact' }" class="side_bar_link">
 <v-list-tile>
 <v-list-tile-action>
 <v-icon>contact_mail</v-icon>
 </v-list-tile-action>
 <v-list-tile-content>Contact</v-list-tile-content>
 </v-list-tile>
 </router-link>
 </v-list>
 </v-navigation-drawer>
 <v-toolbar color="indigo" dark fixed app>
 <v-toolbar-side-icon @click.stop="drawer = !drawer"></v-toolbar-side-icon>
 <v-toolbar-title>Home</v-toolbar-title>
 <v-spacer></v-spacer>
 <v-toolbar-items class="hidden-sm-and-down">
 <v-btn flat v-bind:to="{ name: 'AddMovie' }">Add Movie</v-btn>
 </v-toolbar-items>
 </v-toolbar>
 <v-content>
 <v-container fluid>
 <div id="app">
 <router-view/>
 </div>
 </v-container>
 </v-content>
 <v-footer color="indigo" app>
 <span class="white--text">&copy; 2018</span>
 </v-footer>
 </v-app>
</template>

<script>
import './assets/stylesheets/main.css';

export default {
 data: () => ({
 drawer: null,
 }),
 props: {
 source: String,
 },
};
</script>

最后,替换src/main.js中的内容:

// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import 'bootstrap/dist/css/bootstrap.min.css';
import 'bootstrap-vue/dist/bootstrap-vue.css'; 
import BootstrapVue from 'bootstrap-vue';
import Vue from 'vue';
import Vuetify from 'vuetify';
import App from './App';
import router from './router';

Vue.use(BootstrapVue);
Vue.use(Vuetify);

Vue.config.productionTip = false;

/* eslint-disable no-new */
new Vue({
 el: '#app',
 router,
 components: { App },
 template: '<App/>',
});

这样,我们应该在主页上有一个这样的页面:

我们将在进行过程中使这些页面动态化。

创建添加电影表单

首先,我们需要添加一个链接,带我们到一个添加电影的表单。为此,我们需要更改App.vue中的工具栏。所以,让我们在App.vue的工具栏中添加一个链接:

<v-toolbar color="indigo" dark fixed app>
 <v-toolbar-side-icon @click.stop="drawer = !drawer"></v-toolbar-side-icon>
 <v-toolbar-title>Home</v-toolbar-title>
 <v-spacer></v-spacer>
 <v-toolbar-items class="hidden-sm-and-down">
 <v-btn flat v-bind:to="{ name: 'AddMovie' }">Add Movie</v-btn>
 </v-toolbar-items>
</v-toolbar>

现在我们有了链接,我们需要添加一个路由来将其链接到页面。就像我们为我们的Contact页面所做的那样,让我们添加一个用于添加电影到我们应用程序的路由。所以,在routes/index.js中:

import Vue from 'vue';
import Router from 'vue-router';
import Home from '@/components/Home';
import Contact from '@/components/Contact';
import AddMovie from '@/components/AddMovie';

Vue.use(Router);

export default new Router({
  routes: [
    {
      path: '/',
      name: 'Home',
      component: Home,
    },
    {
      path: '/contact',
      name: 'Contact',
      component: Contact,
    },
    {
 path: '/movies/add',
 name: 'AddMovie',
 component: AddMovie,
 },
  ],
});

在这里,我们为AddMovie添加了一个路由,这意味着我们现在可以访问http://localhost:8080/#/movies/add的添加电影页面。

现在我们需要做的是创建一个vue组件文件。为此,让我们在src/components中添加一个新的AddMovie.vue文件。Vuetify 提供了一个非常简单的方式来创建表单并添加验证。你可以在vuetifyjs.com/components/forms查找更多信息。

让我们在src/components/AddMovie.vue中添加以下内容:

<template>
 <v-form v-model="valid" ref="form" lazy-validation>
 <v-text-field
 label="Movie Name"
 v-model="name"
 :rules="nameRules"
 required
 ></v-text-field>
 <v-text-field
 name="input-7-1"
 label="Movie Description"
 v-model="description"
 multi-line
 ></v-text-field>
 <v-select
 label="Movie Release Year"
 v-model="release_year"
 :items="years"
 ></v-select>
 <v-text-field
 label="Movie Genre"
 v-model="genre"
 ></v-text-field>
 <v-btn
 @click="submit"
 :disabled="!valid"
 >
 submit
 </v-btn>
 <v-btn @click="clear">clear</v-btn>
 </v-form>
</template>

Vuetify 还为表单提供了一些基本的验证。让我们也添加一些验证。

AddMovie.vue中的script标签内添加以下代码:

<template>
...
</template>
<script>
export default {
 data: () => ({
 valid: true,
 name: '',
 description: '',
 genre: '',
 release_year: '',
 nameRules: [
 v => !!v || 'Movie name is required',
 ],
 select: null,
 years: [
 '2018',
 '2017',
 '2016',
 '2015',
 ],
 }),
 methods: {
 submit() {
 if (this.$refs.form.validate()) {
 // Perform next action
 }
 },
 clear() {
 this.$refs.form.reset();
 },
 },
};
</script>

如果我们查看AddMovie.vue中的表单元素,说“v-model="year"”的行:

 <v-form v-model="valid" ref="form" lazy-validation> 

这里v-model="valid"部分的作用是,确保表单在为真之前不会提交,这再次与我们在底部添加的脚本相关联。此外,让我们看看我们添加到表单中的验证。

第一个基本验证是required验证:

<v-text-field
  label="Movie Name"
  v-model="name"
  :rules="nameRules"
  required
></v-text-field>

这在name字段中添加了一个required验证。

此外,对于release_year字段,我们希望它是一个年份的下拉列表,因此,为此,我们添加了以下内容:

<script>
export default {
  data: () => ({
    valid: true,
    name: '',
    description: '',
    genre: '',
    release_year: '',
    nameRules: [
      v => !!v || 'Movie name is required',
    ],
    select: null,
    years: [
 '2018',
 '2017',
 '2016',
 '2015',
 ],
  }),
  methods: {
    submit() {
      if (this.$refs.form.validate()) {
        // Perform next action
      }
    },
    clear() {
      this.$refs.form.reset();
    },
  },
};
</script>

这通过脚本动态地向选择列表中添加项目。

关于最后一部分,我们有两个按钮SubmitClear,分别调用submit()clear()方法。

现在,当你访问 URL(http://localhost:8080/#/movies/add)时,你应该有一个这样的表单:

Movie Name中的*表示这是一个必填字段。

如果你注意到了,我们一直在向所有添加的路由中添加#。这是因为这是 Vue.js 路由器的默认设置。我们可以通过在routes/index.js中添加mode: 'history'来移除它:

import Vue from 'vue';
import Router from 'vue-router';
import Home from '@/components/Home';
import Contact from '@/components/Contact';
import AddMovie from '@/components/AddMovie';

Vue.use(Router);

export default new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      name: 'Home',
      component: Home,
    },
    {
      path: '/contact',
      name: 'Contact',
      component: Contact,
    },
    {
      path: '/movies/add',
      name: 'AddMovie',
      component: AddMovie,
    },
  ],
});

现在,我们都可以在 URL 中不添加#来路由,如下所示:

  • http://localhost:8080/

  • http://localhost:8080/contact

  • http://localhost:8080/movies/add

与服务器通信

现在我们已经有了电影列表页面,也有了一个添加电影的页面,所以接下来我们要做的是在提交表单时将数据保存到 MongoDB 中。

将 express 添加到我们的应用程序

现在我们已经准备好了所有组件,是时候将服务器层添加到我们的应用程序中。

让我们从添加以下 express 包开始:

npm install express --save

下一个部分是创建必要的端点和模型,以便我们可以将电影添加到数据库中。

要做到这一点,我们首先需要安装所需的包:

  • body-parser: 解析传入的请求

  • cors: 用于处理前端和后端之间的跨域请求

  • morgan: HTTP 请求记录器

  • mongoose: 用于 MongoDB 的对象建模

让我们在终端中运行以下命令来安装所有这些包:

$ npm install morgan body-parser cors mongoose --save

添加服务器文件

现在,我们需要为我们的应用程序设置服务器。让我们在应用程序的根目录下添加一个名为 server.js 的文件,并添加以下内容:

const express = require('express');
const bodyParser = require('body-parser');
const mongoose = require('mongoose');
const cors = require('cors');
const morgan = require('morgan');
const fs = require('fs');

const app = express();
const router = express.Router();
app.use(morgan('combined'));
app.use(bodyParser.json());
app.use(cors());

//connect to mongodb
mongoose.connect('mongodb://localhost/movie_rating_app', function() {
 console.log('Connection has been made');
})
.catch(err => {
 console.error('App starting error:', err.stack);
 process.exit(1);
});

router.get('/', function(req, res) {
 res.json({ message: 'API Initialized!'});
});

const port = process.env.API_PORT || 8081;
app.use('/', router);
app.listen(port, function() {
 console.log(`api running on port ${port}`);
});

在这里,我们已经设置了一个服务器,告诉 express 服务器在 8081 端口上运行。我们将使用这个服务器通过 express 处理所有 API 请求。

此外,我们在 server.js 文件中已导入并使用了所有需要的包。

此外,对于 mongoose 连接,我们已经添加了一个连接到我们本地数据库 movie_rating_app 的连接,以下为代码块:

//connect to mongodb
mongoose.connect('mongodb://localhost/movie_rating_app', function() {
  console.log('Connection has been made');
})
.catch(err => {
  console.error('App starting error:', err.stack);
  process.exit(1);
});

如我之前提到的,如果数据库尚不存在,当我们向 DB 添加第一个 Mongoose 文档时,它将自动创建。

下一步是运行我们的 MongoDB 服务器。让我们在终端中运行以下命令来完成:

$ mongod

一旦 Mongo 服务器启动,让我们使用以下命令启动这个应用程序的 node 服务器:

$ node server.js

现在,当我们打开 http://localhost:8081/ 时,你应该能看到以下信息:

到目前为止,我们的前端服务器已经在端口 8080 上启动并运行,以下为配置信息:

$ npm run dev

后端服务器运行在端口 8081,以下为配置信息:

$ node server.js 

需要记住的一个重要事项是,每次我们更改 server.js 中的代码时,都必须通过运行以下命令来重新启动服务器:

$ node server.js

这是一个非常繁琐的任务。然而,有一个很好的方法可以摆脱它。有一个名为 nodemon 的包,当安装后,每当代码更新时,它会自动重新启动服务器,我们不需要每次都手动操作。所以,让我们继续安装这个包:

$ npm install nodemon --save 

安装了包后,现在我们可以使用以下命令启动我们的服务器:

$ nodemon server.js

添加电影模型

下一步是在提交表单时将电影添加到数据库中。让我们先在根目录下创建一个名为 models 的文件夹,并在 models 文件夹中添加一个 Movie.js 文件:

我们将为模型使用单数大写名称,并为 Controllers 文件使用所有小写复数名称。

将以下代码块放入 Movie.js

const mongoose = require('mongoose');

const Schema = mongoose.Schema;
const MovieSchema = new Schema({
 name: String,
 description: String,
 release_year: Number,
 genre: String,
});

const Movie = mongoose.model('Movie', MovieSchema)
module.exports = Movie;

这里,我们创建了一个 Movie 模型,它将接受我们之前添加到 AddMovie.vue 表单中的所有四个属性。

添加电影控制器

现在,我们需要设置的最后一件事是保存电影到数据库的端点。让我们在根目录中创建一个名为 controllers 的文件夹,并在该目录中添加一个名为 movies.js 的文件,并添加以下代码:

const MovieSchema = require('../models/Movie.js');

module.exports.controller = (app) => {
 // add a new movie
 app.post('/movies', (req, res) => {
 const newMovie = new MovieSchema({
 name: req.body.name,
 description: req.body.description,
 release_year: req.body.release_year,
 genre: req.body.genre,
 });

 newMovie.save((error, movie) => {
 if (error) { console.log(error); }
 res.send(movie);
 });
 });
};

在这里,我们添加了一个端点,它接受带有给定参数的 post 请求,并在我们配置的数据库中创建一个 Mongoose 文档。

由于这些控制器有路由,我们需要将这些文件包含在我们的主入口点中。对于我们的后端,主入口文件是 server.js。所以,让我们在 server.js 中添加以下高亮代码块:

...
//connect to mongodb
mongoose.connect('mongodb://localhost/movie_rating_app', function() {
  console.log('Connection has been made');
})
.catch(err => {
  console.error('App starting error:', err.stack);
  process.exit(1);
});

// Include controllers
fs.readdirSync("controllers").forEach(function (file) {
 if(file.substr(-3) == ".js") {
 const route = require("./controllers/" + file)
 route.controller(app)
 }
})

router.get('/', function(req, res) {
  res.json({ message: 'API Initialized!'});
});
...

这个代码块将包括所有控制器的文件,我们不需要手动添加每一个。

连接前端和后端

现在,我们有了模型和端点。接下来要做的事情是在我们点击 AddMovie.vue 中的 提交 按钮时调用这个端点。

这是我们需要与前端和后端通信的部分。为此,我们需要使用一个名为 axios 的单独包。

axios 包帮助我们从 Node.js 中进行 HTTP 请求。它还帮助我们从前端进行 Ajax 调用。axios 也有几个替代方案,例如 fetch 和 superagent。但 axios 已经足够成功,成为了这些中最受欢迎的一个。因此,我们也将使用它。

安装 axios

现在,为了在客户端和服务器之间进行通信,我们将使用 axios 库。所以,让我们首先安装这个库:

npm install axios --save

连接所有部分

现在,我们已经有了所有东西(电影模型、电影控制器和 axios)来在客户端和服务器之间进行通信。现在要做的最后一件事是在我们点击电影添加表单中的提交按钮时连接这些部分。如果你记得,我们在 AddMovie.vue 中提交按钮时添加了一个占位符:

<v-select
      label="Movie Release Year"
      v-model="select"
      :items="years"
    ></v-select>
    <v-text-field
      label="Movie Genre"
      v-model="genre"
    ></v-text-field>
    <v-btn
 @click="submit"
 :disabled="!valid"
 >
      submit
    </v-btn>
    <v-btn @click="clear">clear</v-btn>

这段代码告诉我们,当按钮被点击时,执行 submit() 方法。我们也在 script 部分有这个操作:

...
methods: {
    submit() {
 if (this.$refs.form.validate()) {
 // Perform next action
 }
 },
    clear() {
      this.$refs.form.reset();
    },
  },
...

我们将在本节中添加所有方法。现在我们已经有了 submit 的占位符,让我们修改这段代码以包含电影添加表单:

<script>
import axios from 'axios';

export default {
  data: () => ({
    valid: true,
    name: '',
    description: '',
    genre: '',
    release_year: '',
    nameRules: [
      v => !!v || 'Movie name is required',
    ],
    select: null,
    years: [
      '2018',
      '2017',
      '2016',
      '2015',
    ],
  }),
  methods: {
    submit() {
 if (this.$refs.form.validate()) {
 return axios({
 method: 'post',
 data: {
 name: this.name,
 description: this.description,
 release_year: this.release_year,
 genre: this.genre,
 },
 url: 'http://localhost:8081/movies',
 headers: {
 'Content-Type': 'application/json',
 },
 })
 .then(() => {
 this.$router.push({ name: 'Home' });
 this.$refs.form.reset();
 })
 .catch(() => {
 });
 }
 return true;
 },
    clear() {
      this.$refs.form.reset();
    },
  },
};
</script>

这应该足够了。现在,让我们从 UI 本身添加一个电影到 http://localhost:8080/movies/add 端点。我们应该能够将电影的记录保存到 MongoDB 中。让我简单解释一下我们在这里做了什么。

当我们点击 提交 按钮时,我们通过 axios 发送一个 AJAX 请求,以击中电影控制器中的 post 端点。电影控制器中的 post 方法反过来,根据我们为电影设计的模型模式保存记录。并且,当这个过程完成后,将页面重定向回主页。

为了检查记录是否实际上已创建,让我们看看 MongoDB:

$ mongo
$ use movie_rating_app
$ db.movies.find()

我们可以看到正在创建的记录,其参数是我们提供的表单中的参数:

添加表单验证

我们在上一节中介绍了如何添加验证。让我们继续添加一些验证到我们的电影添加表单中。我们将添加以下验证:

  • 电影名称不能为空

  • 电影描述是可选的

  • 电影发行年份不能为空

  • 电影类型是必需的,并且最大长度为 80 个字符

AddMovie.vue中,让我们在输入字段中添加规则,并将规则从脚本中绑定:

<template>
  <v-form v-model="valid" ref="form" lazy-validation>
    <v-text-field
      label="Movie Name"
      v-model="name"
      :rules="nameRules"
      required
    ></v-text-field>
    <v-text-field
      name="input-7-1"
      label="Movie Description"
      v-model="description"
      multi-line
    ></v-text-field>
    <v-select
      label="Movie Release Year"
      v-model="release_year"
      required
 :rules="releaseRules"
      :items="years"
    ></v-select>
    <v-text-field
      label="Movie Genre"
      v-model="genre"
      required
 :rules="genreRules"
    ></v-text-field>
    <v-btn
      @click="submit"
      :disabled="!valid"
    >
      submit
    </v-btn>
    <v-btn @click="clear">clear</v-btn>
  </v-form>
</template>
<script>
  import axios from 'axios';

  export default {
    data: () => ({
      valid: true,
      name: '',
      description: '',
      genre: '',
      release_year: '',
      nameRules: [
        (v) => !!v || 'Movie name is required'
      ],
      genreRules: [
 v => !!v || 'Movie genre year is required',
 v => (v && v.length <= 80) || 'Genre must be less than equal to 80 characters.',
 ],
 releaseRules: [
 v => !!v || 'Movie release year is required',
 ],
      select: null,
      years: [
        '2018',
        '2017',
        '2016',
        '2015'
      ],
      checkbox: false
    }),
    methods: {
      submit () {
        if (this.$refs.form.validate()) {
          return axios({
            method: 'post',
            data: {
              name: this.name,
              description: this.description,
              release_year: this.release_year,
              genre: this.genre
            },
            url: 'http://localhost:8081/movies',
            headers: {
              'Content-Type': 'application/json'
            }
          })
          .then((response) => {
            this.$router.push({ name: 'Home' });
            this.$refs.form.reset();
          })
          .catch((error) => {
          });
        }
      },
      clear () {
        this.$refs.form.reset()
      }
    }
  }
</script>

现在,如果我们尝试提交所有字段都为空的表单,以及电影类型字段超过 80 个字符,我们就不应该能够提交表单。表单将显示以下错误信息:

添加闪存消息

我们已经介绍了应用构建的基础知识。现在,既然我们可以添加一部电影,那么当电影成功保存在数据库中时,显示一条消息或者通知如果出现错误就非常好了。有几个npm包可以做到这一点。我们也可以自己构建。对于这个应用,我们将使用一个名为:vue-swal(https://www.npmjs.com/package/vue-swal)的包。让我们首先添加这个包:

$ npm install vue-swal --save

现在,让我们将包包含到我们的main.js文件中:

// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import 'bootstrap/dist/css/bootstrap.min.css';
import 'bootstrap-vue/dist/bootstrap-vue.css';

import BootstrapVue from 'bootstrap-vue';
import Vue from 'vue';
import Vuetify from 'vuetify';
import VueSwal from 'vue-swal';
import App from './App';
import router from './router';

Vue.use(BootstrapVue);
Vue.use(Vuetify);
Vue.use(VueSwal);

Vue.config.productionTip = false;

/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  components: { App },
  template: '<App/>',
});

现在,让我们修改我们的AddMovie.vue,以便在操作成功执行或失败时显示闪存消息:

...
methods: {
  submit() {
    if (this.$refs.form.validate()) {
      return axios({
        method: 'post',
        data: {
          name: this.name,
          description: this.description,
          release_year: this.release_year,
          genre: this.genre,
        },
        url: 'http://localhost:8081/movies',
        headers: {
          'Content-Type': 'application/json',
        },
      })
        .then(() => {
          this.$swal(
 'Great!',
 'Movie added successfully!',
 'success',
 );
          this.$router.push({ name: 'Home' });
          this.$refs.form.reset();
        })
        .catch(() => {
          this.$swal(
 'Oh oo!',
 'Could not add the movie!',
 'error',
 );
        });
    }
    return true;
  },
  clear() {
    this.$refs.form.reset();
  },
},
...

现在,有了这个,当我们提交电影时,我们应该能够在重定向到主页之前看到一条成功消息:

还有其他几个用于消息警报的包,例如vue-flashvuex-flashsweet-alert

在主页上加载动态内容

目前,我们的主页内容中包含所有静态电影。让我们用我们添加到数据库中的电影数据来填充数据。为此,首先要做的事情是添加一些电影到数据库中,我们可以通过 UI 中的http://localhost:8080/movies/add端点来完成:

获取所有电影的 API 端点

首先,我们需要添加一个端点来从 Mongo 数据库中获取所有电影。所以,让我们首先在controllers/movies.js中添加一个获取所有电影的端点:

const MovieSchema = require('../models/Movie.js');

module.exports.controller = (app) => {
  // fetch all movies
 app.get('/movies', (req, res) => {
 MovieSchema.find({}, 'name description release_year genre', (error, movies) => {
 if (error) { console.log(error); }
 res.send({
 movies,
 });
 });
 });

  // add a new movie
  app.post('/movies', (req, res) => {
    const newMovie = new MovieSchema({
      name: req.body.name,
      description: req.body.description,
      release_year: req.body.release_year,
      genre: req.body.genre,
    });

    newMovie.save((error, movie) => {
      if (error) { console.log(error); }
      res.send(movie);
    });
  });
};

现在,如果你点击 URL http://localhost:8081/movies,我们应该能够看到通过 UI 或 mongo shell 本身添加的整个电影列表。以下是我所看到的:

修改Home.vue以显示动态内容

现在,让我们更新我们的Home.vue,它将获取我们的 Mongo 数据库中的电影并显示动态内容。用以下内容替换Home.vue中的代码:

<template>
  <v-layout row wrap>
    <v-flex xs4>
      <v-card>
        <v-card-title primary-title>
          <div>
            <div class="headline">Batman vs Superman</div>
            <span class="grey--text">2016 ‧ Science fiction film/Action 
            fiction ‧ 3h 3m</span>
          </div>
        </v-card-title>
        <v-card-text>
          It's been nearly two years since Superman's (Henry Cavill) colossal battle with Zod (Michael Shannon) devastated the city of Metropolis. The loss of life and collateral damage left many feeling angry and helpless, including crime-fighting billionaire Bruce Wayne (Ben Affleck). Convinced that Superman is now a threat to humanity, Batman embarks on a personal vendetta to end his reign on Earth, while the conniving Lex Luthor (Jesse Eisenberg) launches his own crusade against the Man of Steel.
        </v-card-text>
        <v-card-actions>
          <v-btn flat color="purple">Rate this movie</v-btn>
          <v-spacer></v-spacer>
        </v-card-actions>
      </v-card>
    </v-flex>
    <v-flex xs4>
      <v-card>
        <v-card-title primary-title>
          <div>
            <div class="headline">Logan</div>
            <span class="grey--text">2017 ‧ Drama/Science fiction film ‧ 
            2h 21m</span>
          </div>
        </v-card-title>
        <v-card-text>
          In the near future, a weary Logan (Hugh Jackman) cares for an ailing Professor X (Patrick Stewart) at a remote outpost on the Mexican border. His plan to hide from the outside world gets upended when he meets a young mutant (Dafne Keen) who is very much like him. Logan must now protect the girl and battle the dark forces that want to capture her.
        </v-card-text>
        <v-card-actions>
          <v-btn flat color="purple">Rate this movie</v-btn>
          <v-spacer></v-spacer>
        </v-card-actions>
      </v-card>
    </v-flex>
    <v-flex xs4>
      <v-card>
        <v-card-title primary-title>
          <div>
            <div class="headline">Star Wars: The Last Jedi</div>
            <span class="grey--text">2017 ‧ Fantasy/Science fiction film 
            ‧ 2h 35m</span>
          </div>
        </v-card-title>
        <v-card-text>
          Luke Skywalker's peaceful and solitary existence gets upended when he encounters Rey, a young woman who shows strong signs of the Force. Her desire to learn the ways of the Jedi forces Luke to make a decision that changes their lives forever. Meanwhile, Kylo Ren and General Hux lead the First Order in an all-out assault against Leia and the Resistance for supremacy of the galaxy.
        </v-card-text>
        <v-card-actions>
          <v-btn flat color="purple">Rate this movie</v-btn>
          <v-spacer></v-spacer>
        </v-card-actions>
      </v-card>
    </v-flex>
    <v-flex xs4>
      <v-card>
        <v-card-title primary-title>
          <div>
            <div class="headline">Wonder Woman</div>
            <span class="grey--text">2017 ‧ Fantasy/Science fiction film 
            ‧ 2h 21m</span>
          </div>
        </v-card-title>
        <v-card-text>
          Before she was Wonder Woman (Gal Gadot), she was Diana, princess of the Amazons, trained to be an unconquerable warrior. Raised on a sheltered island paradise, Diana meets an American pilot (Chris Pine) who tells her about the massive conflict that's raging in the outside world. Convinced that she can stop the threat, Diana leaves her home for the first time. Fighting alongside men in a war to end all wars, she finally discovers her full powers and true destiny.
        </v-card-text>
        <v-card-actions>
          <v-btn flat color="purple">Rate this movie</v-btn>
          <v-spacer></v-spacer>
        </v-card-actions>
      </v-card>
    </v-flex>
    <v-flex xs4>
      <v-card>
        <v-card-title primary-title>
          <div>
            <div class="headline">Dunkirk</div>
            <span class="grey--text">2017 ‧ Drama/Thriller ‧ 2 
            hours</span>
          </div>
        </v-card-title>
        <v-card-text>
          In May 1940, Germany advanced into France, trapping Allied troops on the beaches of Dunkirk. Under air and ground cover from British and French forces, troops were slowly and methodically evacuated from the beach using every serviceable naval and civilian vessel that could be found. At the end of this heroic mission, 330,000 French, British, Belgian and Dutch soldiers were safely evacuated.
        </v-card-text>
        <v-card-actions>
          <v-btn flat color="purple">Rate this movie</v-btn>
          <v-spacer></v-spacer>
        </v-card-actions>
      </v-card>
    </v-flex>
    <v-flex xs4>
      <v-card>
        <v-card-title primary-title>
          <div>
            <div class="headline">The Revenant</div>
            <span class="grey--text">2015 ‧ Drama/Thriller ‧ 2h 
            36m</span>
          </div>
        </v-card-title>
        <v-card-text>
          While exploring the uncharted wilderness in 1823, frontiersman Hugh Glass (Leonardo DiCaprio) sustains life-threatening injuries from a brutal bear attack. When a member (Tom Hardy) of his hunting team kills his young son (Forrest Goodluck) and leaves him for dead, Glass must utilize his survival skills to find a way back to civilization. Grief-stricken and fueled by vengeance, the legendary fur trapper treks through the snowy terrain to track down the man who betrayed him.
        </v-card-text>
        <v-card-actions>
          <v-btn flat color="purple">Rate this movie</v-btn>
          <v-spacer></v-spacer>
        </v-card-actions>
      </v-card>
    </v-flex>
  </v-layout>
</template>
<script>
import axios from 'axios';

export default {
  name: 'Movies',
  data() {
    return {
      movies: [],
    };
  },
  mounted() {
    this.fetchMovies();
  },
  methods: {
    async fetchMovies() {
      return axios({
        method: 'get',
        url: 'http://localhost:8081/movies',
      })
        .then((response) => {
          this.movies = response.data.movies;
        })
        .catch(() => {
        });
    },
  },
};
</script>

此代码在页面加载时调用一个方法,该方法在 mounted 方法中定义。该方法使用 axios 请求获取电影。现在,我们已经从服务器将数据拉到客户端。现在,我们将使用 vue 指令遍历这些电影并在主页上渲染。在 Home.vue 中,将 <template> 标签的内容替换为以下代码:

<template>
  <v-layout row wrap>
    <v-flex xs4 v-for="movie in movies" :key="movie._id">
      <v-card>
        <v-card-title primary-title>
          <div>
            <div class="headline">{{ movie.name }}</div>
            <span class="grey--text">{{ movie.release_year }} ‧ {{ movie.genre }}</span>
          </div>
        </v-card-title>
        <v-card-text>
          {{ movie.description }}
        </v-card-text>
      </v-card>
    </v-flex>
  </v-layout>
</template>
...

如您所见,我们使用了 vue 指令 for。键用于为每个记录分配一个唯一的标识符。现在,当您访问 http://localhost:8080/ 时,您将看到以下内容:

我们已经成功构建了一个应用程序,可以添加电影到 MongoDB,并在主页上显示数据库记录。

添加电影简介页面

现在,我们需要一个页面,让登录用户可以前往并评分电影。为此,让我们在主页上添加一个链接到电影的标题。在 Home.vue 中,将模板部分替换为以下内容:

<template>
  <v-layout row wrap>
    <v-flex xs4 v-for="movie in movies" :key="movie._id">
      <v-card>
        <v-card-title primary-title>
          <div>
            <div class="headline">
 <v-btn flat v-bind:to="`/movies/${movie._id}`">
 {{ movie.name }}
 </v-btn>
 </div>
            <span class="grey--text">{{ movie.release_year }} ‧ {{ movie.genre }}</span>
          </div>
        </v-card-title>
        <v-card-text>
          {{ movie.description }}
        </v-card-text>
      </v-card>
    </v-flex>
  </v-layout>
</template>

在这里,我们添加了一个链接到标题,该链接将用户带到相应的详情页面。

让我们添加一个页面,用于显示电影的详细视图,登录用户可以前往并评分电影。在 src/components 目录下创建一个名为 Movie.vue 的文件,并添加以下内容:

<template>
 <v-layout row wrap>
 <v-flex xs4>
 <v-card>
 <v-card-title primary-title>
 <div>
 <div class="headline">{{ movie.name }}</div>
 <span class="grey--text">{{ movie.release_year }} ‧ {{ movie.genre }}</span>
 </div>
 </v-card-title>
 <h6 class="card-title">Rate this movie</h6>
 <v-card-text>
 {{ movie.description }}
 </v-card-text>
 </v-card>
 </v-flex>
 </v-layout>
</template>
<script>
import axios from 'axios';

export default {
 name: 'Movie',
 data() {
 return {
 movie: [],
 };
 },
 mounted() {
 this.fetchMovie();
 },
 methods: {
 async fetchMovie() {
 return axios({
 method: 'get',
 url: `http://localhost:8081/api/movies/${this.$route.params.id}`,
 })
 .then((response) => {
 this.movie = response.data;
 })
 .catch(() => {
 });
 },
 },
};
</script>

我们在这里添加了一个 axios 请求,用于在用户点击电影标题时获取电影。

现在,我们还需要定义到该页面的路由。所以,在 routes/index.js 中,将内容替换为以下:

import Vue from 'vue';
import Router from 'vue-router';
import Home from '@/components/Home';
import Contact from '@/components/Contact';
import AddMovie from '@/components/AddMovie';
import Register from '@/components/Register';
import Login from '@/components/Login';
import Movie from '@/components/Movie';

Vue.use(Router);

export default new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      name: 'Home',
      component: Home,
    },
    {
      path: '/contact',
      name: 'Contact',
      component: Contact,
    },
    {
      path: '/movies/add',
      name: 'AddMovie',
      component: AddMovie,
    },
    {
 path: '/movies/:id',
 name: 'Movie',
 component: Movie,
 },
  ],
});

现在,我们需要添加一个用于获取指定 ID 电影的 GET 请求端点。

controllers/movies.js 中的内容替换为以下:

const MovieSchema = require('../models/Movie.js');

module.exports.controller = (app) => {
  // fetch all movies
  app.get('/movies', (req, res) => {
    MovieSchema.find({}, 'name description release_year genre', (error, movies) => {
      if (error) { console.log(error); }
      res.send({
        movies,
      });
    });
  });

  // fetch a single movie
 app.get('/api/movies/:id', (req, res) => {
 MovieSchema.findById(req.params.id, 'name description release_year genre', (error, movie) => {
 if (error) { console.error(error); }
 res.send(movie);
 });
 });

  // add a new movie
  app.post('/movies', (req, res) => {
    const newMovie = new MovieSchema({
      name: req.body.name,
      description: req.body.description,
      release_year: req.body.release_year,
      genre: req.body.genre,
    });

    newMovie.save((error, movie) => {
      if (error) { console.log(error); }
      res.send(movie);
    });
  });
};

现在,当我们点击电影标题上的链接时,我们应该能够看到以下页面:

在这里,我们还添加了一个用户可以点击的区域来评分电影。让我们继续添加评分电影的功能。为此,我们将使用一个名为 vue-star-rating 的包,它使得添加评分组件变得容易。您可以在以下链接中找到这个示例:https://jsfiddle.net/anteriovieira/8nawdjs7/

让我们首先添加这个包:

$ npm install vue-star-rating --save

Movie.vue 中,将内容替换为以下:

<template>
  <v-layout row wrap>
    <v-flex xs4>
      <v-card>
        <v-card-title primary-title>
          <div>
            <div class="headline">{{ movie.name }}</div>
            <span class="grey--text">{{ movie.release_year }} ‧ {{ movie.genre }}</span>
          </div>
        </v-card-title>
 <h6 class="card-title" v-if="current_user">Rate this movie</h6>
        <v-card-text>
          {{ movie.description }}
        </v-card-text>
      </v-card>
    </v-flex>
  </v-layout>
</template>
<script>
import axios from 'axios';
import Vue from 'vue';
import StarRating from 'vue-star-rating';

const wrapper = document.createElement('div');
// shared state
const state = {
 note: 0,
};
// crate component to content
const RatingComponent = Vue.extend({
 data() {
 return { rating: 0 };
 },
 watch: {
 rating(newVal) { state.note = newVal; },
 },
 template: `
 <div class="rating">
 How was your experience getting help with this issues?
 <star-rating v-model="rating" :show-rating="false"></star-rating>
 </div>`,
 components: { 'star-rating': StarRating },
});

const component = new RatingComponent().$mount(wrapper);

export default {
  name: 'Movie',
  data() {
    return {
      movie: [],
    };
  },
  mounted() {
    this.fetchMovie();
  },
  methods: {
    async rate() {
 this.$swal({
 content: component.$el,
 buttons: {
 confirm: {
 value: 0,
 },
 },
 }).then(() => {
 const movieId = this.$route.params.id;
 return axios({
 method: 'post',
 data: {
 rate: state.note,
 },
 url: `http://localhost:8081/movies/rate/${movieId}`,
 headers: {
 'Content-Type': 'application/json',
 },
 })
 .then(() => {
 this.$swal(`Thank you for rating! ${state.note}`, 'success');
 })
 .catch((error) => {
 const message = error.response.data.message;
 this.$swal('Oh oo!', `${message}`, 'error');
 });
 });
 },
    async fetchMovie() {
      return axios({
        method: 'get',
        url: `http://localhost:8081/api/movies/${this.$route.params.id}`,
      })
        .then((response) => {
          this.movie = response.data;
        })
        .catch(() => {
        });
    },
  },
};
</script>

让我们更新代码,以便在点击“评分这部电影”时调用 rate 方法。在 Movie.vue 中更新以下代码行:

...
<h6 class="card-title" v-if="current_user" @click="rate">Rate this movie</h6>
...

现在我们需要做的最后一件事是在 movies.js 中添加 rate 端点:

var Movie = require("../models/Movie");

module.exports.controller = (app) => {
  // fetch all movies
  app.get("/movies", function(req, res) {
    Movie.find({}, 'name description release_year genre', function (error, movies) {
      if (error) { console.log(error); }
       res.send({
        movies: movies
      })
    })
  })

  // fetch a single movie
  app.get("/api/movies/:id", function(req, res) {
    Movie.findById(req.params.id, 'name description release_year 
    genre', function (error, movie) {
      if (error) { console.error(error); }
      res.send(movie)
    })
  })

  // rate a movie
 app.post('/movies/rate/:id', (req, res) => {
 const rating = new Rating({
 movie_id: req.params.id,
 user_id: req.body.user_id,
 rate: req.body.rate,
 })

 rating.save(function (error, rating) {
 if (error) { console.log(error); }
 res.send({
 movie_id: rating.movie_id,
 user_id: rating.user_id,
 rate: rating.rate
 })
 })
 })

  // add a new movie
  app.post('/movies', (req, res) => {
    const movie = new Movie({
      name: req.body.name,
      description: req.body.description,
      release_year: req.body.release_year,
      genre: req.body.genre
    })

    movie.save(function (error, movie) {
      if (error) { console.log(error); }
      res.send(movie)
    })
  })
}

该端点将用户评分保存到一个名为 Rating 的单独集合中,我们尚未创建。让我们继续创建该文件。在 models 目录下创建一个名为 Rating.js 的文件,并添加以下内容:

const mongoose = require('mongoose')
const Schema = mongoose.Schema
const RatingSchema = new Schema({
 movie_id: String,
 user_id: String,
 rate: Number
})

const Rating = mongoose.model("Rating", RatingSchema)
module.exports = Rating

同样在 movies.js 中包含相同的模型:

const Movie = require("../models/Movie");
const Rating = require("../models/Rating");

就这样!现在,用户在登录后应该能够评分电影。当点击“评分这部电影”时,用户应该会弹出一个窗口,并在成功评分后显示评分分数以及感谢信息:

图片

摘要

在本章中,我们介绍了 Vue.js 是什么!我们构建了一个静态应用程序,列出了电影,随后通过一个表单添加了动态功能,该表单将电影存储在 MongoDB 中。我们还学习了 Vue.js 组件、数据绑定和 Vue.js 指令。

我们还增加了用户对电影进行评分的功能。

在下一章中,我们将在同一应用程序中添加用户和登录/注册功能。

第六章:使用passport.js构建认证

认证是任何应用程序的重要组成部分。认证是保护我们构建的应用程序的一种方式。每个应用程序都需要某种认证机制。它帮助我们识别向应用程序服务器发送请求的用户。

在本章中,我们将讨论以下主题:

  • 创建登录和注册页面

  • 安装和配置passport.js

  • 了解更多关于passport.js策略,即JSON Web Token(JWT)策略

  • 了解更多关于passport.js本地策略

  • 在应用程序服务器中创建必要的端点以处理注册和登录请求

我们可以自己构建用户认证。然而,这需要大量的配置和很多麻烦。passport.js是一个允许我们高效配置认证的包,只需花费很少的时间。如果您想自己学习和开发,我鼓励您这样做。这将使您更深入地了解一切是如何工作的。然而,对于这本书,我们将使用这个叫做passport.js的强大工具,它非常容易集成和学习。

到目前为止,我们已经创建了一个动态的 Web 应用程序,该程序在主页上显示我们通过电影添加表单和 API 添加的所有电影。我们也有一种方法通过前端将这些电影添加到数据库中。现在,由于这将是一个公开的 Web 应用程序,我们不能允许每个人在未登录的情况下自行添加电影。只有登录的用户才能访问并添加电影。此外,为了对电影进行评分,用户应先登录,然后对电影进行评分。

passport.js简介

passport.js是 Node.js 提供的用于认证的中间件。passport.js的功能是认证发送到服务器的请求。它提供了多种认证策略。passport.js提供了诸如本地策略、Facebook 策略、Google 策略、Twitter 策略和 JWT 策略等策略。在本章中,我们将专注于使用 JWT 策略。

JWT

JWT 是一种使用基于令牌的方法进行请求认证的方式。有两种认证请求的方法:基于 cookie 的认证和基于令牌的认证。基于 cookie 的认证机制将用户的会话 ID 保存在浏览器的 cookie 中,而基于令牌的机制使用一个签名的令牌,其外观如下:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVhNjhhNDMzMDJkMWNlZDU5YjExNDg3MCIsImlhdCI6MTUxNzI0MjM1M30.5xY59iTIjpt9ukDmxseNAGbOdz6weWL1drJkeQzoO3M

然后,这个令牌将在我们向controllers发出的每个请求上进行验证。

对于我们的应用程序,我们将使用两种方法的组合。当用户请求登录到应用程序时,我们将为他们创建一个签名的令牌,然后将该令牌添加到浏览器的 cookie 中。下次用户登录时,我们将从 cookie 中读取该令牌,并使用服务器中的passport-jwt模块验证该令牌,然后决定是否让该用户登录。

如果你仔细查看前面的令牌,你会看到令牌由三个部分组成,由句点(.)分隔;每个部分都有自己的含义:

  • 第一部分代表头部

  • 第二部分代表有效载荷

  • 第三部分代表签名

为了能够使用这个 JWT,我们需要添加一个包。为此,我们可以运行以下命令:

$ npm install jsonwebtoken --save

要开始使用这个包,让我们在server.js中也定义它:

...
const morgan = require('morgan')
const fs = require('fs')
const jwt = require('jsonwebtoken');
...

安装 passport.js

就像任何其他的npm包一样,我们可以通过运行以下命令来安装passport.js

$ npm install passport --save

在成功安装后,你应该在你的package.json文件中列出以下这些包:

...
"nodemon": "¹.14.10",
"passport": "⁰.4.0",
"sass-loader": "⁶.0.6",
...

你也可以通过首先将包添加到你的package.json文件中,然后运行以下命令来完成此操作:

$ npm install

配置 passport

就像任何其他的node包一样,我们需要为passport.js配置包。在我们的server.js文件中,添加以下几行代码:

...
const mongoose = require('mongoose');
const cors = require('cors');
const morgan = require('morgan');
const fs = require('fs');
const jwt = require('jsonwebtoken');
const passport = require('passport');

const app = express();
const router = express.Router();
app.use(morgan('combined'));
app.use(bodyParser.json());
app.use(cors());
app.use(passport.initialize());
...

上述代码只是在我们应用程序中初始化了passport.js。我们仍然需要配置一些东西才能开始使用 JWT 认证机制。

passport.js 策略

如前所述,passport.js提供了许多策略以实现轻松集成。我们将要使用的一个策略是 JWT 策略。我们已经添加了passport.js并初始化了它。现在,让我们也添加这个策略。

安装 passport-jwt 策略

仅安装 passport 模块并不足以满足我们的需求。passport.js提供了其策略在单独的npm包中。对于jwt认证,我们必须安装passport-jwt模块,如下所示:

$ npm install passport-jwt --save

在成功安装后,你应该在应用程序的package.json文件中列出以下这些包:

...
"nodemon": "¹.14.10",
"passport": "⁰.4.0", "passport-jwt": "³.0.1",
"sass-loader": "⁶.0.6",
...

配置 passport-jwt 策略

现在我们已经拥有了所有需要的东西,让我们跳转到 JWT 策略的配置设置。在server.js中添加以下几行代码:

...
const morgan = require('morgan');
const fs = require('fs');
const jwt = require('jsonwebtoken');
const passport = require('passport');
const passportJWT = require('passport-jwt');
const ExtractJwt = passportJWT.ExtractJwt;
const JwtStrategy = passportJWT.Strategy;
const jwtOptions = {}
jwtOptions.jwtFromRequest = ExtractJwt.fromAuthHeaderWithScheme('jwt');
jwtOptions.secretOrKey = 'movieratingapplicationsecretkey';

const app = express();
const router = express.Router();
...

上述代码足以让我们开始。我们需要从passport.js中获取JwtStrategy,并且ExtractJwT将用于从jwt令牌中提取有效载荷数据。

我们还定义了一个变量来设置 JWT auth设置,其中配置了一个密钥。这个密钥将用于签名任何请求的有效载荷。

你也可以创建一个单独的文件来存储你的重要密钥。

使用 JWT 策略

现在我们已经准备好使用passport.js提供的服务了。让我们快速回顾一下到目前为止我们已经做了什么:

  1. 安装了 passport、passport-jwtjsonwebtoken

  2. 为这三个包配置了所有设置

下一步如下:

  1. 创建我们的用户模型

  2. 为用户实体创建 API 端点,即登录和注册

  3. 构建我们的认证视图,即登录页面和注册页面

  4. 使用 JWT 策略最终验证请求

设置用户注册

让我们从添加用户注册功能开始。

创建用户模型

我们还没有一个集合来管理用户。在我们的 User 模型中将有三个参数:nameemailpassword。让我们继续在 models 目录中创建我们的 User 模型,名为 User.js

const mongoose = require('mongoose');

const Schema = mongoose.Schema;
const UserSchema = new Schema({
 name: String,
 email: String,
 password: String,
});

const User = mongoose.model('User', UserSchema);
module.exports = User;

如您所见,以下是为用户定义的三个属性:nameemailpassword

安装 bcryptjs

现在,我们不能再以明文形式保存这些用户的密码,因此我们需要一个机制来加密它们。幸运的是,我们已经有了一个专门用于加密密码的包,即 bcryptjs。让我们首先将这个包添加到我们的应用程序中:

$ npm install bcryptjs --save

当包安装完成后,让我们在 User.js 模型中添加初始化块:

const mongoose = require('mongoose');
const bcryptjs = require('bcryptjs');

const Schema = mongoose.Schema;
const UserSchema = new Schema({
  name: String,
  email: String,
  password: String,
});

const User = mongoose.model('User', UserSchema);
module.exports = User;

现在,当我们保存一个用户时,我们应该创建我们自己的方法来将用户添加到数据库中,因为我们想加密他们的密码。所以,让我们将以下代码添加到 models/User.js 中:

...
const User = mongoose.model('User', UserSchema);
module.exports = User;

module.exports.createUser = (newUser, callback) => {
 bcryptjs.genSalt(10, (err, salt) => {
 bcryptjs.hash(newUser.password, salt, (error, hash) => {
 // store the hashed password
 const newUserResource = newUser;
 newUserResource.password = hash;
 newUserResource.save(callback);
 });
 });
};
...

在前面的代码中,我们使用了 bcrypt 库,该库使用 genSalt 机制将密码转换为加密字符串。在 User 模型中的 preceding 方法 createUser 接收 user 对象,将用户提供的密码转换为加密密码,并将其保存到数据库中。

为注册用户添加 API 端点

现在我们已经有了我们的模型,让我们继续创建一个用于创建用户的端点。为此,让我们首先在 controllers 文件夹中创建一个名为 users.js 的控制器,以管理所有与用户相关的请求。由于我们在 server.js 中添加了一个代码块来初始化 controllers 目录中的所有文件,所以我们在这里不需要引入这些文件。

users.js 中,将文件内容替换为以下代码:

const User = require('../models/User.js');

module.exports.controller = (app) => {
 // register a user
 app.post('/users/register', (req, res) => {
 const name = req.body.name;
 const email = req.body.email;
 const password = req.body.password;
 const newUser = new User({
 name,
 email,
 password,
 });
 User.createUser(newUser, (error, user) => {
 if (error) { console.log(error); }
 res.send({ user });
 });
 });
};

在前面的代码中,我们添加了一个端点,该端点向 http://localhost:8081/users/register URL 发送 POST 请求,获取用户的 nameemailpassword,并将它们保存到我们的数据库中。在响应中,它返回刚刚创建的用户。这很简单。

现在,让我们在 Postman 中测试这个端点。您应该能够在响应中看到返回的用户:

创建注册视图页面

让我们为用户添加一个注册视图页面。为此,我们需要创建一个表单,该表单接受 nameemailpassword 参数。在 src/components 中创建一个名为 Register.vue 的文件:

<template>
 <v-form v-model="valid" ref="form" lazy-validation>
 <v-text-field
 label="Name"
 v-model="name"
 required
 ></v-text-field>
 <v-text-field
 label="Email"
 v-model="email"
 :rules="emailRules"
 required
 ></v-text-field>
 <v-text-field
 label="Password"
 v-model="password"
 required
 ></v-text-field>
 <v-text-field
 name="input-7-1"
 label="Confirm Password"
 v-model="confirm_password"
 ></v-text-field>
 <v-btn
 @click="submit"
 :disabled="!valid"
 >
 submit
 </v-btn>
 <v-btn @click="clear">clear</v-btn>
 </v-form>
</template>

vue 文件是一个简单的模板文件,其中包含表单组件。下一步是为该文件添加路由。

src/router/index.js 中,添加以下代码行:

import Vue from 'vue';
import Router from 'vue-router';
import Home from '@/components/Home';
import Contact from '@/components/Contact';
import AddMovie from '@/components/AddMovie';
import Movie from '@/components/Movie';
import Register from '@/components/Register';

Vue.use(Router);

export default new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      name: 'Home',
      component: Home,
    },
    {
      path: '/contact',
      name: 'Contact',
      component: Contact,
    },
    {
      path: '/movies/add',
      name: 'AddMovie',
      component: AddMovie,
    },{ path: '/movies/:id',name: 'Movie',component: Movie,},
 {
 path: '/users/register',
 name: 'Register',
 component: Register,
 },
  ],
});

就这样!现在,让我们导航到 http://localhost.com:8080/users/register

在注册表单中添加提交和清除方法

下一步是为 submitclear 方法添加功能。让我们在 Register.vue 中添加一些方法:

...
    <v-btn @click="clear">clear</v-btn>
  </v-form>
</template>
<script>
export default {
 data: () => ({
 valid: true,
 name: '',
 email: '',
 password: '',
 confirm_password: '',
 emailRules: [
 v => !!v || 'E-mail is required',
 v => /\S+@\S+\.\S+/.test(v) || 'E-mail must be valid',
 ],
 }),
 methods: {
 async submit() {
 if (this.$refs.form.validate()) {
 // add process here
 }
 },
 clear() {
 this.$refs.form.reset();
 },
 },
};
</script>

我们还在此处添加了一些注册表单的验证。它根据给定的正则表达式验证用户提供的电子邮件。

我们添加了两个方法,submitclearclear方法重置表单值;相当直接,对吧?现在,当我们点击submit按钮时,首先运行验证。如果所有验证都通过,那么只有submit方法内的逻辑会被处理。在这里,我们需要向服务器发送带有用户参数的请求,这时axios就派上用场了。

介绍 axios

Axios 是一种将请求数据发送到服务器的机制。你可以将其视为 JavaScript 中的 AJAX 请求。使用axios,我们可以有效地处理来自服务器的成功和错误响应。

要安装axios,请运行以下命令:

$ npm install axios --save

使用 axios

现在,让我们修改Register.vue文件以实现axios——按照以下方式替换script标签内的内容:

...
</v-form>
</template>
<script>
import axios from 'axios';

export default {
  data: () => ({
    valid: true,
    name: '',
    email: '',
    password: '',
    confirm_password: '',
    emailRules: [
      v => !!v || 'E-mail is required',
      v => /\S+@\S+\.\S+/.test(v) || 'E-mail must be valid',
    ],
  }),
  methods: {
    async submit() {
 if (this.$refs.form.validate()) {
 return axios({
 method: 'post',
 data: {
 name: this.name,
 email: this.email,
 password: this.password,
 },
 url: 'http://localhost:8081/users/register',
 headers: {
 'Content-Type': 'application/json',
 },
 })
 .then(() => {
 this.$swal(
 'Great!',
 'You have been successfully registered!',
 'success',
 );
 this.$router.push({ name: 'Login' });
 })
 .catch((error) => {
 const message = error.response.data.message;
 this.$swal('Oh oo!', `${message}`, 'error');
 });
 }
 return true;
 },
 clear() {
 this.$refs.form.reset();
 },
  },
};
</script>

如果你熟悉ajax,你应该能够快速理解代码。如果不熟悉,别担心,实际上它相当简单。axios方法接受重要的参数,例如请求方法(在前述情况中为post)、数据参数或有效载荷,以及要击中的 URL 端点。它接受这些参数并将它们路由到then()方法或catch()方法,具体取决于服务器的响应。

如果请求成功,它将进入then()方法;如果不成功,它将进入catch()方法。现在,请求的成功和失败也可以根据我们的需求进行自定义。对于前述场景,如果用户没有保存到数据库中,我们将简单地传递一个错误响应。我们也可以对验证进行操作。

因此,让我们也修改controller方法中的users.js以适应这些更改:

const User = require('../models/User.js');

module.exports.controller = (app) => {
  // register a user
  app.post('/users/register', (req, res) => {
    const name = req.body.name;
    const email = req.body.email;
    const password = req.body.password;
    const newUser = new User({
      name,
      email,
      password,
    });
    User.createUser(newUser, (error, user) => {
      if (error) {
 res.status(422).json({
 message: 'Something went wrong. Please try again after some time!',
 });
 }
      res.send({ user });
    });
  });
};

如前述代码所示,如果请求失败,我们将发送一条消息说“出了点问题”。我们还可以根据服务器的响应显示不同类型的消息。

设置用户登录

现在我们已经成功实现了用户的登录过程,接下来让我们开始构建将用户登录到我们应用的功能。

修改用户模型

要将用户登录到应用中,我们将使用以下两个参数:用户的电子邮件和他们的密码。我们需要查询数据库以找到给定电子邮件的记录;因此,让我们添加一个方法来根据用户名提取用户:

...
const User = mongoose.model('User', UserSchema);
module.exports = User;

module.exports.createUser = (newUser, callback) => {
  bcryptjs.genSalt(10, (err, salt) => {
    bcryptjs.hash(newUser.password, salt, (error, hash) => {
      // store the hashed password
      const newUserResource = newUser;
      newUserResource.password = hash;
      newUserResource.save(callback);
    });
  });
};

module.exports.getUserByEmail = (email, callback) => {
 const query = { email };
 User.findOne(query, callback);
};

前述方法将返回具有给定电子邮件的用户。

正如我提到的,我们还需要检查的另一件事是密码。让我们添加一个方法来比较用户在登录时提供的密码和保存在我们数据库中的密码:

...
module.exports.getUserByEmail = (email, callback) => {
  const query = { email };
  User.findOne(query, callback);
};

module.exports.comparePassword = (candidatePassword, hash, callback) => {
 bcryptjs.compare(candidatePassword, hash, (err, isMatch) => {
 if (err) throw err;
 callback(null, isMatch);
 });
};

前述方法同时接受用户提供的密码和保存的密码,并根据密码是否匹配返回truefalse

现在我们已经准备好进入控制器部分。

添加用于登录用户的 API 端点

我们已经添加了用户能够登录所需的方法。现在,本章最重要的部分就在这里。我们需要设置 JWT auth机制,以使用户能够登录。

users.js中添加以下代码行:

const User = require('../models/User.js');

const passportJWT = require('passport-jwt');
const jwt = require('jsonwebtoken');

const ExtractJwt = passportJWT.ExtractJwt;
const jwtOptions = {};
jwtOptions.jwtFromRequest = ExtractJwt.fromAuthHeaderWithScheme('jwt');
jwtOptions.secretOrKey = 'thisisthesecretkey';

module.exports.controller = (app) => {
  // register a user
  app.post('/users/register', (req, res) => {
    const name = req.body.name;
    const email = req.body.email;
    const password = req.body.password;
    const newUser = new User({
      name,
      email,
      password,
    });
    User.createUser(newUser, (error, user) => {
      if (error) {
        res.status(422).json({
          message: 'Something went wrong. Please try again after some time!',
        });
      }
      res.send({ user });
    });
  });

  // login a user
 app.post('/users/login', (req, res) => {
 if (req.body.email && req.body.password) {
 const email = req.body.email;
 const password = req.body.password;
 User.getUserByEmail(email, (err, user) => {
 if (!user) {
 res.status(404).json({ message: 'The user does not exist!' });
 } else {
 User.comparePassword(password, user.password, (error, isMatch) => {
 if (error) throw error;
 if (isMatch) {
 const payload = { id: user.id };
 const token = jwt.sign(payload, jwtOptions.secretOrKey);
 res.json({ message: 'ok', token });
 } else {
 res.status(401).json({ message: 'The password is incorrect!' });
 }
 });
 }
 });
 }
 });
};

由于 JWT 策略是passport.js的一部分,我们还需要初始化它。我们还需要为 JWT 选项添加一些配置,以从负载中提取数据,并在请求发送到服务器时对其进行解密和重新加密。

密钥是你可以配置的东西。它基本上代表了你的应用令牌。确保它不容易被猜到。

此外,我们还添加了一个端点,它向localhost:8081/users/login发送 POST 请求,并接收用户的电子邮件和密码。以下是这个方法所做的一些事情:

  • 检查给定电子邮件的用户是否存在。如果不存在,它将发送一个 404 状态码,指出用户在我们的应用中不存在。

  • 将提供的密码与我们的应用中用户的密码进行比较。如果没有匹配,它将发送一个错误响应,指出密码不匹配。

  • 如果一切顺利,它将使用 JWT 签名对用户的负载进行签名,生成一个令牌,并响应该令牌。

现在,让我们在 Postman 中测试这个端点。你应该能够在响应中看到返回的令牌,如下所示:

图片

在前面的屏幕截图中,请注意 JWT 接受负载,对其进行签名,并生成一个随机令牌。

创建注册视图页面

现在让我们为用户添加一个登录页面。为此,就像我们在注册页面所做的那样,我们需要创建一个包含电子邮件和密码参数的表单。在src/components目录内创建一个名为Login.vue的文件,如下所示:

<template>
 <v-form v-model="valid" ref="form" lazy-validation>
 <v-text-field
 label="Email"
 v-model="email"
 :rules="emailRules"
 required
 ></v-text-field>
 <v-text-field
 label="Password"
 v-model="password"
 required
 ></v-text-field>
 <v-btn
 @click="submit"
 :disabled="!valid"
 >
 submit
 </v-btn>
 <v-btn @click="clear">clear</v-btn>
 </v-form>
</template>

vue文件是一个简单的模板文件,其中包含表单组件。接下来要做的事情是为该文件添加一个路由。

src/router/index.js中添加以下代码:

import Vue from 'vue';
import Router from 'vue-router';
import Home from '@/components/Home';
import Contact from '@/components/Contact';
import AddMovie from '@/components/AddMovie';
import Movie from '@/components/Movie';
import Register from '@/components/Register';
import Login from '@/components/Login';

Vue.use(Router);

export default new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      name: 'Home',
      component: Home,
    },
    {
      path: '/contact',
      name: 'Contact',
      component: Contact,
    },
    {
      path: '/movies/add',
      name: 'AddMovie',
      component: AddMovie,
    },
    {
      path: '/movies/:id',
      name: 'Movie',
      component: Movie,
    },
    {
      path: '/users/register',
      name: 'Register',
      component: Register,
    },
    {
 path: '/users/login',
 name: 'Login',
 component: Login,
 },
  ],
});

就这样。现在,让我们导航到http://localhost.com:8080/users/login

图片

在登录表单中添加提交和清除方法

下一步是在submitclear方法中添加功能。让我们在Login.vue中添加一些方法。clear方法与注册页面上的相同。对于submit方法,我们将在这里使用axios方法。我们已经在控制器中分类了我们的成功和错误消息。现在我们只需要确保它们在 UI 中显示:

...
</v-form>
</template>
<script>
import axios from 'axios';

export default {
 data: () => ({
 valid: true,
 email: '',
 password: '',
 emailRules: [
 v => !!v || 'E-mail is required',
 v => /\S+@\S+\.\S+/.test(v) || 'E-mail must be valid',
 ],
 }),
 methods: {
 async submit() {
 return axios({
 method: 'post',
 data: {
 email: this.email,
 password: this.password,
 },
 url: 'http://localhost:8081/users/login',
 headers: {
 'Content-Type': 'application/json',
 },
 })
 .then((response) => {
 window.localStorage.setItem('auth', response.data.token);
 this.$swal('Great!', 'You are ready to start!', 'success');
 this.$router.push({ name: 'Home' });
 })
 .catch((error) => {
 const message = error.response.data.message;
 this.$swal('Oh oo!', `${message}`, 'error');
 this.$router.push({ name: 'Login' });
 });
 },
 clear() {
 this.$refs.form.reset();
 },
 },
};
</script>

验证与注册页面上的相同。我们添加了两个方法,submitclearclear方法重置表单值,而submit方法只是简单地调用 API 端点,从表单中获取参数,并响应正确的消息,然后该消息在 UI 中显示。成功完成后,用户将被重定向到主页。

这里重要的是,由于我们在客户端进行交互,我们需要将之前生成的 JWT 令牌保存到某个地方。访问令牌的最佳方式是将它保存到浏览器的会话中。因此,我们设置了一个名为auth的键,它将 JWT 令牌保存在本地存储中。每当发出其他请求时,请求将首先检查它是否是一个有效的令牌,然后相应地执行操作。

以下是我们迄今为止所做的工作:

  • 在用户模型中添加了getUserByEmail()comparePassword()

  • 创建了登录视图页面

  • 添加了提交和清除表单的方法

  • 生成 JWT 签名令牌并将其保存到会话中以便以后重用

  • 显示成功和错误消息

在 Home.vue 中验证我们的用户

我们需要做的最后一件事是检查当前登录用户是否有权查看电影列表页面。虽然让主页(电影列表页面)对所有用户开放是有意义的,但为了学习目的,当用户访问主页时,让我们添加 JWT 授权。让我们让主页不对不在我们应用中的外部用户开放。

movies.js中添加以下代码段:

const MovieSchema = require('../models/Movie.js');
const Rating = require('../models/Rating.js');
const passport = require('passport');

module.exports.controller = (app) => {
  // fetch all movies
  app.get('/movies', passport.authenticate('jwt', { session: false }), (req, res) => {
    MovieSchema.find({}, 'name description release_year genre', (error, movies) => {
      if (error) { console.log(error); }
      res.send({
        movies,
      });
    });
  });
...

好的,就是这样!我们需要初始化 passport 并添加passport.authenticate('jwt', { session: false })。我们必须传递 JWT 令牌,passport JWT 策略会自动验证当前用户。

现在,让我们在请求电影列表页面时也发送 JWT 令牌。在Home.vue中添加以下代码:

...
<script>
import axios from 'axios';

export default {
  name: 'Movies',
  data() {
    return {
      movies: [],
    };
  },
  mounted() {
    this.fetchMovies();
  },
  methods: {
    async fetchMovies() {
 const token = window.localStorage.getItem('auth');
 return axios({
 method: 'get',
 url: 'http://localhost:8081/movies',
 headers: {
 Authorization: `JWT ${token}`,
 'Content-Type': 'application/json',
 },
 })
 .then((response) => {
 this.movies = response.data.movies;
 this.current_user = response.data.current_user;
 })
 .catch(() => {
 });
 },
  },
};
</script>

在进行axios调用时,我们将在头部传递一个额外的参数。我们需要从本地存储中读取令牌并将其通过头部传递给电影 API。

使用这种方式,任何未登录到应用的用户将无法查看电影列表页面。

为 Vue 组件提供静态文件服务

在跳入本地策略之前,让我们了解一下如何使我们的 Vue.js 组件以静态方式提供服务。由于我们使用的是独立的前端和后端,维护这两个版本可能会是一项艰巨的任务,尤其是在部署应用时,配置一切可能需要更长的时间。因此,为了更好地管理我们的应用,我们将构建 Vue.js 应用,这将是一个生产构建,并且仅使用 Node.js 服务器来提供文件。为此,我们将使用一个名为serve-static的独立包。所以,让我们继续安装这个包:

$ npm install serve-static --save 

现在,让我们将以下内容添加到我们的server.js文件中:

const express = require('express');
const bodyParser = require('body-parser');
const mongoose = require('mongoose');
const cors = require('cors');
const morgan = require('morgan');
const fs = require('fs');
const session = require('express-session');
const config = require('./config/Config');
const passport = require('passport');
const app = express();
const router = express.Router();
const serveStatic = require('serve-static');

app.use(morgan('combined'));
app.use(bodyParser.json());
app.use(cors());

...

// Include controllers
fs.readdirSync("controllers").forEach(function (file) {
  if(file.substr(-3) == ".js") {
    const route = require("./controllers/" + file)
    route.controller(app)
  }
})
app.use(serveStatic(__dirname + "/dist"));
...

使用这个命令,现在让我们构建我们的应用:

$ npm run build 

之前的命令将在应用内的dist文件夹中创建必要的静态文件,这些文件将由运行在 8081 端口的 Node.js 服务器提供。构建完成后,我们现在不需要运行以下命令:

$ npm run dev 

此外,现在由于我们只运行 node 服务器,应用应该可以通过http://localhost:8081的 URL 访问。

上述命令启动了我们的前端服务器。我们只需要使用以下命令运行 Node.js 服务器:

$ nodemon server.js

由于我们现在只有一个端口,8081,我们不需要在像之前那样在每个后端 API 中添加/api前缀,我们可以去掉这些前缀。因此,让我们更新controllersvue文件:

如下替换controllers/movies.js中的内容:

var Movie = require("../models/Movie");

module.exports.controller = (app) => {
  // fetch all movies
 app.get("/movies", function(req, res) {
    Movie.find({}, 'name description release_year genre', function 
    (error, movies) {
      if (error) { console.log(error); }
       res.send({
        movies: movies
      })
    })
  })

  // add a new movie
 app.post('/movies', (req, res) => {
    const movie = new Movie({
      name: req.body.name,
      description: req.body.description,
      release_year: req.body.release_year,
      genre: req.body.genre
    })

    movie.save(function (error, movie) {
      if (error) { console.log(error); }
      res.send(movie)
    })
  })
}

如下替换controllers/users.js中的内容:

const User = require("../models/User");
const config = require('./../config/Config');
const passport = require('passport');

module.exports.controller = (app) => {
  // local strategy
  const LocalStrategy = require('passport-local').Strategy;
  passport.use(new LocalStrategy({
      usernameField: 'email',
      passwordField: 'password'
    },
    function(email, password, done) {
      User.getUserByEmail(email, function(err, user){
        if (err) { return done(err); }
        if (!user) { return done(null, false); }
        User.comparePassword(password, user.password, function(err, 
        isMatch){
          if(isMatch) {
            return done(null, user);
          } else {
            return done(null, false);
          }
        })
      });
    }
  ));

 app.post('/users/login',
    passport.authenticate('local', { failureRedirect: '/users/login' }),
    function(req, res) {
      res.redirect('/');
    });

  passport.serializeUser(function(user, done) {
    done(null, user.id);
  });

  passport.deserializeUser(function(id, done) {
    User.findById(id, function(err, user){
      done(err, user)
    })
  });

  // register a user
 app.post('/users/register', (req, res) => {
    const email = req.body.email;
    const fullname = req.body.fullname;
    const password = req.body.password;
    const role = req.body.role || 'user';
    const newUser = new User({
      email: email,
      fullname: fullname,
      role: role,
      password: password
    })
    User.createUser(newUser, function(error, user) {
      if (error) {
        res.status(422).json({
          message: "Something went wrong. Please try again after some 
          time!"
        });
      }
      res.send({ user: user })
    })
  })
}

AddMovie.vuescript标签内容替换为以下代码:

<script>
import axios from 'axios';

export default {
  data: () => ({
    valid: true,
    name: '',
    description: '',
    genre: '',
    release_year: '',
    nameRules: [
      v => !!v || 'Movie name is required',
    ],
    genreRules: [
      v => !!v || 'Movie genre year is required',
      v => (v && v.length <= 80) || 'Genre must be less than equal to 
      80 characters.',
    ],
    releaseRules: [
      v => !!v || 'Movie release year is required',
    ],
    select: null,
    years: [
      '2018',
      '2017',
      '2016',
      '2015',
    ],
  }),
  methods: {
    submit() {
      if (this.$refs.form.validate()) {
        return axios({
          method: 'post',
          data: {
            name: this.name,
            description: this.description,
            release_year: this.release_year,
            genre: this.genre,
          },
 url: '/movies',
          headers: {
            'Content-Type': 'application/json',
          },
        })
          .then(() => {
            this.$swal(
              'Great!',
              'Movie added successfully!',
              'success',
            );
            this.$router.push({ name: 'Home' });
            this.$refs.form.reset();
          })
          .catch(() => {
            this.$swal(
              'Oh oo!',
              'Could not add the movie!',
              'error',
            );
          });
      }
      return true;
    },
    clear() {
      this.$refs.form.reset();
    },
  },
};
</script>

Home.vuescript标签内容替换为以下代码:

<script>
import axios from 'axios';

export default {
  name: 'Movies',
  data() {
    return {
      movies: [],
    };
  },
  mounted() {
    this.fetchMovies();
  },
  methods: {
    async fetchMovies() {
      return axios({
        method: 'get',
 url: '/movies',
      })
        .then((response) => {
          this.movies = response.data.movies;
        })
        .catch(() => {
        });
    },
  },
};
</script>

Login.vuescript标签内容替换为以下代码:

<script>
  import axios from 'axios';
  import bus from "./../bus.js";

  export default {
    data: () => ({
      valid: true,
      email: '',
      password: '',
      emailRules: [
        (v) => !!v || 'E-mail is required',
        (v) => /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(v) 
        || 'E-mail must be valid'
      ],
      passwordRules: [
        (v) => !!v || 'Password is required',
      ]
    }),
    methods: {
      async submit () {
        if (this.$refs.form.validate()) {
          return axios({
            method: 'post',
            data: {
              email: this.email,
              password: this.password
            },
 url: '/users/login',
            headers: {
              'Content-Type': 'application/json'
            }
          })
          .then((response) => {
            localStorage.setItem('jwtToken', response.data.token)
            this.$swal("Good job!", "You are ready to start!", 
            "success");
            bus.$emit("refreshUser");
            this.$router.push({ name: 'Home' });
          })
          .catch((error) => {
            const message = error.response.data.message;
            this.$swal("Oh oo!", `${message}`, "error")
          });
        }
      },
      clear () {
        this.$refs.form.reset()
      }
    }
  }
</script>

Register.vuescript标签内容替换为以下代码:

<script>
  import axios from 'axios';
  export default {
    data: () => ({
      e1: false,
      valid: true,
      fullname: '',
      email: '',
      password: '',
      confirm_password: '',
      fullnameRules: [
        (v) => !!v || 'Fullname is required'
      ],
      emailRules: [
        (v) => !!v || 'E-mail is required',
        (v) => /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(v) 
        || 'E-mail must be valid'
      ],
      passwordRules: [
        (v) => !!v || 'Password is required'
      ]
    }),
    methods: {
      async submit () {
        if (this.$refs.form.validate()) {
          return axios({
            method: 'post',
            data: {
              fullname: this.fullname,
              email: this.email,
              password: this.password
            },
 url: '/users/register',
            headers: {
              'Content-Type': 'application/json'
            }
          })
          .then((response) => {
            this.$swal(
              'Great!',
              `You have been successfully registered!`,
              'success'
            )
            this.$router.push({ name: 'Home' })
          })
          .catch((error) => {
            const message = error.response.data.message;
            this.$swal("Oh oo!", `${message}`, "error")
          });
        }
      },
      clear () {
        this.$refs.form.reset()
      }
    }
  }
</script>

最后,我们不再需要使用代理,因此我们可以从webpack.dev.conf.js中移除我们之前设置的代理。

如下替换devServer中的内容:

devServer: {
    clientLogLevel: 'warning',
    historyApiFallback: {
      rewrites: [
        { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 
        'index.html') },
      ],
    },
    hot: true,
    contentBase: false, // since we use CopyWebpackPlugin.
    compress: true,
    host: HOST || config.dev.host,
    port: PORT || config.dev.port,
    open: config.dev.autoOpenBrowser,
    overlay: config.dev.errorOverlay
      ? { warnings: false, errors: true }
      : false,
    publicPath: config.dev.assetsPublicPath,
    quiet: true, // necessary for FriendlyErrorsPlugin
    watchOptions: {
      poll: config.dev.poll,
    }
  },

更新后,让我们使用以下命令再次构建我们的应用程序:

$ npm run build

我们的应用程序应该按预期工作。

由于我们的应用程序是一个单页应用程序(SPA),当我们浏览嵌套路由并重新加载页面时,我们会得到一个错误。例如,如果我们通过点击主页中的链接浏览http://localhost:8081/contact页面,它会正常工作。然而,如果我们直接尝试导航到http://localhost:8081/contact页面,我们会得到一个错误,因为这是一个 SPA,这意味着浏览器只渲染静态的index.html文件。当我们尝试访问/contact页面时,它会寻找名为contact的页面,而这个页面不存在。

为了实现这一点,我们需要添加一个中间件,它作为一个回退,在我们尝试直接重新加载页面或尝试访问具有动态 ID 的页面时,渲染相同的index.html文件。

npm提供了一个中间件来满足我们的需求。让我们继续安装以下包:

$ npm install connect-history-api-fallback --save

安装完成后,让我们修改我们的server.js文件以使用中间件:

...
const passport = require('passport');
const serveStatic = require('serve-static');
const history = require('connect-history-api-fallback');
const app = express();
const router = express.Router();

...

// Include controllers
fs.readdirSync("controllers").forEach(function (file) {
  if(file.substr(-3) == ".js") {
    const route = require("./controllers/" + file)
    route.controller(app)
  }
})
app.use(history());
app.use(serveStatic(__dirname + "/dist"));
...

在这些设置到位后,我们现在应该能够直接访问所有路由。我们也可以现在重新加载页面。

由于我们仅在 Node.js 服务器上构建 Vue.js 组件并运行应用程序,因此每当我们对 Vue.js 组件进行更改时,我们都需要使用npm run build命令再次构建应用程序。

Passport 的本地策略

Passport 的本地策略很容易集成。一如既往,让我们从以下命令开始安装这个策略。

安装 Passport 的本地策略

我们可以通过运行以下命令安装 Passport 的本地策略:

$ npm install passport-local --save

以下代码应将包添加到您的package.json文件中:

...
"node-sass": "⁴.7.2",
"nodemon": "¹.14.10",
"passport": "⁰.4.0",
"passport-local": "¹.0.0",
...

配置 Passport 的本地策略

配置 Passport 的本地策略有几个步骤。我们将详细讨论每个步骤:

  1. 添加必要的本地认证路由。

  2. 添加一个中间件方法来检查认证是否成功。

让我们深入了解前面步骤的细节。

添加必要的本地认证路由

让我们继续添加必要的路由,当我们点击登录按钮时。用以下代码替换controllers/users.js的内容:

const User = require('../models/User.js');
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;

module.exports.controller = (app) => {
// local strategy
 passport.use(new LocalStrategy({
 usernameField: 'email',
 passwordField: 'password',
 }, (email, password, done) => {
 User.getUserByEmail(email, (err, user) => {
 if (err) { return done(err); }
 if (!user) { return done(null, false); }
 User.comparePassword(password, user.password, (error, isMatch) => {
 if (isMatch) {
 return done(null, user);
 }
 return done(null, false);
 });
 return true;
 });
 }));

// user login
 app.post('/users/login',
 passport.authenticate('local', { failureRedirect: '/users/login' }),
 (req, res) => {
 res.redirect('/');
 });

 passport.serializeUser((user, done) => {
 done(null, user.id);
 });

 passport.deserializeUser((id, done) => {
 User.findById(id, (err, user) => {
 done(err, user);
 });
 });

  // register a user
  app.post('/users/register', (req, res) => {
    const name = req.body.name;
    const email = req.body.email;
    const password = req.body.password;
    const newUser = new User({
      name,
      email,
      password,
    });
    User.createUser(newUser, (error, user) => {
      if (error) {
        res.status(422).json({
          message: 'Something went wrong. Please try again after some time!',
        });
      }
      res.send({ user });
    });
  });
};

在这里,我们添加了一个用户登录的路由/users/login,然后使用passport.js本地认证机制将用户登录到应用中。

此外,我们配置了passport.js,在用户登录时使用 LocalStrategy,它需要用户的usernamepassword

安装 express-session

下一步,我们需要设置一个session,这样当用户成功登录时,user数据可以存储在session中,并且在我们发出其他请求时可以轻松检索。为此,我们需要添加一个名为express-session的包。让我们继续使用以下命令安装这个包:

$ npm install express-session --save

配置 express-session

现在,我们已经有了这个包,让我们配置这个包以满足我们在session中保存用户的需要。在它里面添加以下几行代码。

如果usernamepassword匹配,用户对象将被保存在服务器的会话中,并且可以通过每个请求中的req.user访问。

此外,由于我们现在不需要 passport JWT 策略,让我们也更新我们的 vue 文件。

使用以下代码更新server.js中的内容:

const express = require('express');
const bodyParser = require('body-parser');
const mongoose = require('mongoose');
const cors = require('cors');
const morgan = require('morgan');
const fs = require('fs');
const session = require('express-session');
const config = require('./config/Config');
const passport = require('passport');
const serveStatic = require('serve-static');
const history = require('connect-history-api-fallback');

const app = express();
const router = express.Router();
app.use(morgan('combined'));
app.use(bodyParser.json());
app.use(cors());

app.use(session({
 secret: config.SECRET,
 resave: true,
 saveUninitialized: true,
 cookie: { httpOnly: false }
}))
app.use(passport.initialize());
app.use(passport.session());

//connect to mongodb
mongoose.connect(config.DB, function() {
  console.log('Connection has been made');
})
.catch(err => {
  console.error('App starting error:', err.stack);
  process.exit(1);
});

// Include controllers
fs.readdirSync("controllers").forEach(function (file) {
  if(file.substr(-3) == '.js') {
    const route = require('./controllers/' + file);
    route.controller(app);
  }
})
app.use(history());
app.use(serveStatic(__dirname + "/dist"));

router.get('/api/current_user', isLoggedIn, function(req, res) {
 if(req.user) {
 res.send({ current_user: req.user })
 } else {
 res.status(403).send({ success: false, msg: 'Unauthorized.' });
 }
})

function isLoggedIn(req, res, next) {
 if (req.isAuthenticated())
 return next();

 res.redirect('/');
 console.log('error! auth failed')
}

router.get('/api/logout', function(req, res){
 req.logout();
 res.send();
});

router.get('/', function(req, res) {
  res.json({ message: 'API Initialized!'});
});

const port = process.env.API_PORT || 8081;
app.use('/', router);
var server = app.listen(port, function() {
  console.log(`api running on port ${port}`);
});

module.exports = server

在这里,我们添加了以下代码块的 express-session 配置:

app.use(session({
 secret: config.SECRET,
 resave: true,
 saveUninitialized: true,
 cookie: { httpOnly: false }
}))
app.use(passport.initialize());
app.use(passport.session());

上述代码块使用一个必要的密钥令牌来保存用户详情。我们将在一个单独的文件中定义这个令牌,以便所有配置令牌都位于一个地方。

因此,让我们继续创建一个名为Config.js的文件,位于config目录中,并添加以下几行代码:

module.exports = {
 DB: 'mongodb://localhost/movie_rating_app',
 SECRET: 'movieratingappsecretkey'
}

我们还添加了一个名为/api/current_userGET路由来获取当前登录用户的信息。这个 API 使用一个名为isLoggedIn的中间件方法,检查用户数据是否在会话中。如果用户数据存在于会话中,则将当前用户详细信息作为响应发送回。

我们还添加了一个名为/logout的另一个端点,它简单地注销用户并销毁会话。

因此,有了这个配置,现在我们应该能够使用passport.js的 Local Strategy 成功登录。

我们现在唯一的问题是,没有方法知道用户是否成功登录。为此,我们需要显示一些用户信息,例如email,以指示已登录的用户。

为了做到这一点,我们需要从Login.vue传递用户信息到App.vue,这样我们就可以在顶部栏显示用户的电子邮件。我们可以使用Vue提供的名为emit的方法,该方法用于在Vue组件之间传递信息。让我们继续配置它。

配置 emit 方法

让我们首先创建一个可以用于不同 Vue 组件之间通信的传输器。在src目录内创建一个名为bus.js的文件,并添加以下内容:

import Vue from 'vue';

const bus = new Vue();

export default bus;

现在,将 Login.vuescript 标签内的内容替换为以下代码:

...
<script>
import axios from 'axios';
import bus from './../bus';

export default {
  data: () => ({
    valid: true,
    email: '',
    password: '',
    emailRules: [
      v => !!v || 'E-mail is required',
      v => /\S+@\S+\.\S+/.test(v) || 'E-mail must be valid',
    ],
  }),
  methods: {
    async submit() {
      return axios({
        method: 'post',
        data: {
          email: this.email,
          password: this.password,
        },
        url: 'http://localhost:8081/users/login',
        headers: {
          'Content-Type': 'application/json',
        },
      })
        .then(() => {
          this.$swal('Great!', 'You are ready to start!', 'success');
          bus.$emit('refreshUser');
          this.$router.push({ name: 'Home' });
        })
        .catch((error) => {
          const message = error.response.data.message;
          this.$swal('Oh oo!', `${message}`, 'error');
          this.$router.push({ name: 'Login' });
        });
    },
    clear() {
      this.$refs.form.reset();
    },
  },
};
</script>

在这里,我们正在发出一个名为 refreshUser 的方法,该方法将在 App.vue 中定义。将 App.vue 中的内容替换为以下代码:

<template>
  <v-app id="inspire">
    <v-navigation-drawer
      fixed
      v-model="drawer"
      app
    >
      <v-list dense>
        <router-link v-bind:to="{ name: 'Home' }" class="side_bar_link">
          <v-list-tile>
            <v-list-tile-action>
              <v-icon>home</v-icon>
            </v-list-tile-action>
            <v-list-tile-content>Home</v-list-tile-content>
          </v-list-tile>
        </router-link>
        <router-link v-bind:to="{ name: 'Contact' }" class="side_bar_link">
          <v-list-tile>
            <v-list-tile-action>
              <v-icon>contact_mail</v-icon>
            </v-list-tile-action>
            <v-list-tile-content>Contact</v-list-tile-content>
          </v-list-tile>
        </router-link>
      </v-list>
    </v-navigation-drawer>
    <v-toolbar color="indigo" dark fixed app>
      <v-toolbar-side-icon @click.stop="drawer = !drawer"></v-toolbar-side-icon>
      <v-toolbar-title>Home</v-toolbar-title>
      <v-spacer></v-spacer>
      <v-toolbar-items class="hidden-sm-and-down">
 <v-btn id="add_movie_link" flat v-bind:to="{ name: 'AddMovie' }"
 v-if="current_user">
 Add Movie
 </v-btn>
 <v-btn id="user_email" flat v-if="current_user">{{ current_user.email }}</v-btn>
 <v-btn flat v-bind:to="{ name: 'Register' }" v-if="!current_user" id="register_btn">
 Register
 </v-btn>
 <v-btn flat v-bind:to="{ name: 'Login' }" v-if="!current_user" id="login_btn">Login</v-btn>
 <v-btn id="logout_btn" flat v-if="current_user" @click="logout">Logout</v-btn>
 </v-toolbar-items>
    </v-toolbar>
    <v-content>
      <v-container fluid>
        <div id="app">
          <router-view/>
        </div>
      </v-container>
    </v-content>
    <v-footer color="indigo" app>
      <span class="white--text">&copy; 2018</span>
    </v-footer>
  </v-app>
</template>

<script>
import axios from 'axios';

import './assets/stylesheets/main.css';
import bus from './bus';

export default {
  data: () => ({
    drawer: null,
    current_user: null,
  }),
  props: {
    source: String,
  },
  mounted() {
 this.fetchUser();
 this.listenToEvents();
 },
  methods: {
    listenToEvents() {
 bus.$on('refreshUser', () => {
 this.fetchUser();
 });
 },
 async fetchUser() {
 return axios({
 method: 'get',
 url: '/api/current_user',
 })
 .then((response) => {
 this.current_user = response.data.current_user;
 })
 .catch(() => {
 });
 },
    logout() {
 return axios({
 method: 'get',
 url: '/api/logout',
 })
 .then(() => {
 bus.$emit('refreshUser');
 this.$router.push({ name: 'Home' });
 })
 .catch(() => {
 });
 },
  },
};
</script>

在这里,我们添加了名为 refreshUser 的方法,该方法在 App.vuemounted 方法中被监听。每当用户登录到应用程序时,App.vue 中的 refreshUser 方法就会被调用,并获取已登录用户的信息。

此外,我们在顶部栏中显示了用户的电子邮件,这样我们就可以知道用户是否已登录。

此外,让我们也将 JWT 验证从电影控制器中移除。将 controllers/movies.js 中的内容替换为以下代码:

const MovieSchema = require('../models/Movie.js');
const Rating = require('../models/Rating.js');

module.exports.controller = (app) => {
  // fetch all movies
  app.get('/movies', (req, res) => {
    MovieSchema.find({}, 'name description release_year genre', (error, movies) => {
      if (error) { console.log(error); }
      res.send({
        movies,
      });
    });
  });

  // fetch a single movie
  app.get('/api/movies/:id', (req, res) => {
    MovieSchema.findById(req.params.id, 'name description release_year genre', (error, movie) => {
      if (error) { console.error(error); }
      res.send(movie);
    });
  });

  // rate a movie
  app.post('/movies/rate/:id', (req, res) => {
    const newRating = new Rating({
      movie_id: req.params.id,
      user_id: req.body.user_id,
      rate: req.body.rate,
    });

    newRating.save((error, rating) => {
      if (error) { console.log(error); }
      res.send({
        movie_id: rating.movie_id,
        user_id: rating.user_id,
        rate: rating.rate,
      });
    });
  });

  // add a new movie
  app.post('/movies', (req, res) => {
    const newMovie = new MovieSchema({
      name: req.body.name,
      description: req.body.description,
      release_year: req.body.release_year,
      genre: req.body.genre,
    });

    newMovie.save((error, movie) => {
      if (error) { console.log(error); }
      res.send(movie);
    });
  });
};

通过这种方式,当用户登录到应用程序时,我们应该能够看到以下屏幕:

摘要

在本章中,我们介绍了 passport.js 的工作原理。我们还介绍了如何在 MEVN 应用程序中使用简单的 JWT 策略,并处理用户的注册和登录。

在下一章中,我们将深入探讨不同的 passport.js 策略,例如 Facebook 策略、Google 策略和 Twitter 策略。

第七章:使用 passport.js 构建 OAuth 策略

在上一章中,我们讨论了 passport-JWT 策略。我们讨论了如何利用 JWT 包来构建一个健壮的用户注册流程。我们介绍了如何实现用户的注册和登录过程。在本章中,我们将深入以下部分:

  • passport.js Facebook 策略

  • passport.js Twitter 策略

  • passport.js Google 策略

  • passport.js LinkedIn 策略

如果我们从头开始做这些工作,这些部分单独就会花费很多时间。passport.js提供了一种更简单的方法,以非常灵活的方式集成所有这些策略,并使它们更容易实现。

OAuth是一种认证协议,允许用户通过不同的外部服务登录。例如,如果用户已经在 Facebook 或 Twitter 上登录,那么通过 Facebook 或 Twitter 登录应用程序不需要用户提供用户名和密码。这节省了用户在应用程序中设置新账户的时间,使得登录过程更加顺畅。这使得登录应用程序变得更加容易;否则,用户首先需要在我们应用程序中注册,然后使用这些凭据登录。Passport 的 OAuth 策略允许用户在浏览器记住账户的情况下,通过单点登录到我们的应用程序。其余的一切都是自动完成的,并由策略本身处理。

Passport 的 Facebook 策略

Passport 的 Facebook 策略易于集成。一如既往,让我们从安装这个策略开始。

安装 Passport 的 Facebook 策略

我们可以通过运行以下命令来安装 passport 的 Facebook 策略:

$ npm install passport-facebook --save

以下代码应该会将包添加到你的package.json文件中:

...
"node-sass": "⁴.7.2",
"nodemon": "¹.14.10",
"passport": "⁰.4.0",
"passport-facebook": "².1.1",
...

配置 Passport 的 Facebook 策略

配置 Passport 的 Facebook 策略有几个步骤。我们将详细讨论每个步骤:

  1. 创建并设置一个 Facebook 应用。这将为我们提供一个App ID和一个App Secret

  2. 在我们的登录页面上添加一个按钮,允许我们的用户通过 Facebook 登录。

  3. 添加 Facebook 认证所需的必要路由。

  4. 添加一个中间件方法来检查认证是否成功。

让我们深入了解前面每个步骤的细节。

创建并设置一个 Facebook 应用

要使用 Facebook 策略,你必须首先构建一个 Facebook 应用。Facebook 的开发者门户在developers.facebook.com/

登录后,点击“开始”按钮,然后点击“下一步”。

然后,你将在屏幕右上角看到一个名为“我的应用”的下拉菜单,你可以在这里找到创建新应用程序的选项。

选择一个你想为你的应用程序命名的显示名称。在这种情况下,我们将命名为movie_rating_app

图片

点击“创建 App ID”。如果你进入设置页面,你会看到你应用的 App ID 和 App Secret:

你将需要前一个屏幕截图中提到的值。

在我们的登录页面添加一个按钮,允许用户通过 Facebook 登录

下一步是在登录页面添加一个“使用 Facebook 登录”按钮,你将链接到你的 Facebook 应用程序。将 Login.vue 的内容替换为以下内容:

<template>
  <div>
    <div class="login">
      <a class="btn facebook" href="/login/facebook"> LOGIN WITH FACEBOOK</a>
 </div>
    <v-form v-model="valid" ref="form" lazy-validation>
      <v-text-field
        label="Email"
        v-model="email"
        :rules="emailRules"
        required
      ></v-text-field>
      <v-text-field
        label="Password"
        v-model="password"
        :rules="passwordRules"
        required
      ></v-text-field>
      <v-btn
        @click="submit"
        :disabled="!valid"
      >
        submit
      </v-btn>
      <v-btn @click="clear">clear</v-btn><br/>
    </v-form>
  </div>
</template>
...

让我们也为这些按钮添加一些样式。在 src/assets/stylesheets/home.css 中,添加以下代码:

#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  width: 100%;
}

#inspire {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
}

.container.fill-height {
  align-items: normal;
}

a.side_bar_link {
  text-decoration: none;
}

.card__title--primary, .card__text {
  text-align: left;
}

.card {
  height: 100% !important;
}

.btn.facebook {
 background-color: #3b5998 !important;
 border-color: #2196f3;
 color: #fff !important;
}

.btn.twitter {
 background-color: #2196f3 !important;
 border-color: #2196f3;
 color: #fff !important;
}

.btn.google {
 background-color: #dd4b39 !important;
 border-color: #dd4b39;
 color: #fff !important;
}

.btn.linkedin {
 background-color: #4875B4 !important;
 border-color: #4875B4;
 color: #fff !important;
}

上述代码将添加一个“使用 Facebook 登录”按钮:

为 Facebook 应用添加配置

让我们配置 Facebook 策略,就像我们为本地策略所做的那样。我们将创建一个单独的文件来处理 Facebook 登录,以便代码更简单。让我们在 controllers 文件夹中创建一个名为 facebook.js 的文件,并将以下内容添加到其中:

const User = require('../models/User.js');
const passport = require('passport');
const config = require('./../config/Config');
const Strategy = require('passport-facebook').Strategy;

module.exports.controller = (app) => {
 // facebook strategy
 passport.use(new Strategy({
 clientID: config.FACEBOOK_APP_ID,
 clientSecret: config.FACEBOOK_APP_SECRET,
 callbackURL: '/login/facebook/return',
 profileFields: ['id', 'displayName', 'email']
 },
 (accessToken, refreshToken, profile, cb) => {
 // Handle facebook login
 }));
};

在上述代码中,exports 方法内部的第一行代码导入了 Facebook 策略。配置需要三个参数:clientIDclientSecret 和回调 URL。clientIDclientSecret 分别是 Facebook 应用的 App IDApp Secret

让我们将这些密钥添加到我们的配置文件中。在 config/Config.js 中,让我们添加我们的 Facebook 密钥,facebook_client_idfacebook_client_secret

module.exports = {
  DB: 'mongodb://localhost/movie_rating_app',
  SECRET: 'movieratingappsecretkey',
  FACEBOOK_APP_ID: <facebook_client_id>,
 FACEBOOK_APP_SECRET: <facebook_client_secret>
}

回调 URL 是在成功与 Facebook 交易后,你想要将应用程序路由到的 URL。

我们在这里定义的回调是 http://127.0.0.1:8081/login/facebook/return,这是我们必须要定义的。配置之后是一个函数,该函数接受以下四个参数:

  • accessToken

  • refreshToken

  • profile

  • cb(回调)

请求成功后,我们的应用程序将被重定向到主页。

添加必要的 Facebook 登录路由

现在,让我们继续添加必要的路由,以便在点击登录按钮和收到 Facebook 的回调时使用。在同一个文件 facebook.js 中,添加以下路由:

const User = require("../models/User");
const passport = require('passport');
const config = require('./../config/Config');

module.exports.controller = (app) => {
  // facebook strategy
  const Strategy = require('passport-facebook').Strategy;

  passport.use(new Strategy({
    clientID: config.FACEBOOK_APP_ID,
    clientSecret: config.FACEBOOK_APP_SECRET,
    callbackURL: '/api/login/facebook/return',
    profileFields: ['id', 'displayName', 'email']
  },
  function(accessToken, refreshToken, profile, cb) {
  }));

  app.get('/login/facebook',
 passport.authenticate('facebook', { scope: ['email'] }));

 app.get('/login/facebook/return',
 passport.authenticate('facebook', { failureRedirect: '/login' }),
 (req, res) => {
 res.redirect('/');
 });
}

在上述代码中,我们添加了两个路由。如果你还记得,在 Login.vue 中,我们添加了一个链接到 http://127.0.0.1:8081/login/facebook,这将由我们在这里定义的第一个路由提供。

此外,如果你还记得,在配置设置中,我们已经添加了一个回调函数,它将由第二个路由提供,我们也在这里定义了它。

现在,最后要做的就是使用策略实际登录用户。将 facebook.js 的内容替换为以下内容:

const User = require('../models/User.js');
const passport = require('passport');
const config = require('./../config/Config');
const Strategy = require('passport-facebook').Strategy;

module.exports.controller = (app) => {
  // facebook strategy
  passport.use(new Strategy({
    clientID: config.FACEBOOK_APP_ID,
    clientSecret: config.FACEBOOK_APP_SECRET,
    callbackURL: '/login/facebook/return',
    profileFields: ['id', 'displayName', 'email'],
  },
  (accessToken, refreshToken, profile, cb) => {
 const email = profile.emails[0].value;
 User.getUserByEmail(email, (err, user) => {
 if (!user) {
 const newUser = new User({
 fullname: profile.displayName,
 email,
 facebookId: profile.id,
 });
 User.createUser(newUser, (error) => {
 if (error) {
 // Handle error
 }
 return cb(null, user);
 });
 } else {
 return cb(null, user);
 }
 return true;
 });
 }));

  app.get('/login/facebook',
    passport.authenticate('facebook', { scope: ['email'] }));

  app.get('/login/facebook/return',
    passport.authenticate('facebook', { failureRedirect: '/login' }),
    (req, res) => {
      res.redirect('/');
    });
};

在使用 Facebook 登录时,如果用户已经在我们的数据库中存在,用户只需简单登录并保存在会话中。会话数据不是存储在浏览器 cookies 中,而是在服务器端本身。如果用户不在我们的数据库中,那么我们将使用从 Facebook 提供的电子邮件创建一个新的用户。

在这里要配置的最后一件事是添加从 Facebook 到我们应用程序的返回 URL 或重定向 URL。为此,我们可以在 Facebook 的应用设置页面中添加 URL。在应用的设置页面中,在有效的 OAuth 重定向 URI下,添加从 Facebook 到我们应用程序的重定向 URL。

现在,我们应该能够通过 Facebook 登录。当login函数成功时,它将重定向用户到主页。如果你注意到,Facebook 将我们重定向到http://localhost:8081/#*=*而不是仅仅http://localhost:8081。这是因为一个安全漏洞。我们可以在主文件index.html中添加以下代码片段来从 URL 中移除#

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons" rel="stylesheet">
    <link href="https://unpkg.com/vuetify/dist/vuetify.min.css" rel="stylesheet">
    <title>movie_rating_app</title>
  </head>
  <body>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
  <script type="text/javascript">
 if (window.location.hash == '#_=_'){
 history.replaceState
 ? history.replaceState(null, null, window.location.href.split('#')[0])
 : window.location.hash = '';
 }
 </script>
</html>

这将移除前面 URL 中的#符号。当你成功登录后,我们应该在顶部栏视图中看到你的电子邮件,类似于以下这样:

Passport 的 Twitter 策略

下一个策略是 Passport 的 Twitter 策略。让我们从安装这个策略开始。

安装 Passport 的 Twitter 策略

运行以下命令来安装 Twitter 策略:

$ npm install passport-twitter --save

前面的命令应该将包添加到你的package.json文件中:

...
"node-sass": "⁴.7.2",
"nodemon": "¹.14.10",
"passport": "⁰.4.0",
"passport-twitter": "².1.1",
...

配置 Passport 的 Twitter 策略

就像 Facebook 策略一样,我们必须执行以下步骤来配置 Passport 的 Twitter 策略:

  1. 创建和设置 Twitter 应用程序。这将为我们提供消费者密钥(API 密钥)和消费者密钥(API 密钥)。

  2. 在我们的登录页面添加一个按钮,允许我们的用户通过 TWITTER 登录。

  3. 添加必要的路由。

  4. 添加一个中间件方法来检查身份验证。

  5. 重定向用户到主页并在顶部栏显示已登录用户的电子邮件。

让我们深入了解前面每个步骤的细节。

创建和设置 Twitter 应用程序

就像 Facebook 策略一样,为了能够使用 Twitter 策略,我们同样需要构建一个 Twitter 应用程序。Twitter 的开发者门户位于apps.twitter.com/,在那里你可以看到所有应用程序的列表。如果这是第一次,你将看到一个创建新应用程序的按钮——点击创建你的 Twitter 应用程序。

你将看到一个表单,它将要求你填写应用程序名称和其他详细信息。你可以将应用程序命名为你想要的任何名称。对于这个应用程序,我们将将其命名为movie_rating_app。对于回调 URL,我们提供了http://localhost:8081/login/twitter/return,我们稍后需要定义它:

在应用程序成功创建后,你可以在“密钥和访问令牌”选项卡中看到 API 密钥(消费者密钥)和 API 密钥(消费者密钥):

这些令牌将用于我们应用程序中的身份验证。

在我们的登录页面添加一个按钮,允许用户通过 Twitter 登录

下一步是在我们的登录页面添加一个“登录 Twitter”按钮,并将其链接到我们刚刚创建的 Twitter 应用程序。

Login.vue 中,添加以下链接以通过 Twitter 登录:

<template>
  <div>
    <div class="login">
      <a class="btn facebook" href="/login/facebook"> LOGIN WITH FACEBOOK</a>
       <a class="btn twitter" href="/login/twitter"> LOGIN WITH TWITTER</a>
    </div>
    <v-form v-model="valid" ref="form" lazy-validation>
      <v-text-field
        label="Email"
        v-model="email"
        :rules="emailRules"
        required
      ></v-text-field>
...

上述代码将添加一个“登录 Twitter”按钮。让我们运行以下命令:

$ npm run build

现在,如果我们访问 URL http://localhost:8080/users/login,我们应该看到以下页面:

添加 Twitter 应用的配置

现在,下一步是添加 Twitter 登录的必要路由。为此,我们需要配置设置和回调 URL。就像我们为 Facebook 策略所做的那样,让我们创建一个单独的文件来设置我们的 Twitter 登录。让我们在 controllers 目录中创建一个名为 twitter.js 的新文件,并添加以下内容:

const User = require('../models/User.js');
const passport = require('passport');
const config = require('./../config/Config');
const Strategy = require('passport-twitter').Strategy;

module.exports.controller = (app) => {
 // twitter strategy
 passport.use(new Strategy({
 consumerKey: config.TWITTER_APP_ID,
 consumerSecret: config.TWITTER_APP_SECRET,
 callbackURL: '/login/twitter/return',
 profileFields: ['id', 'displayName', 'email'],
 },
 (accessToken, refreshToken, profile, cb) => {
 // Handle twitter login
 }));
};

正如我们在 Facebook 策略中所做的那样,第一行导入 Twitter 策略。配置需要以下三个参数:clientIDclientSecret 和一个回调 URL。consumerKeyconsumerSecret 分别是 Twitter 应用程序的 App IDApp Secret

让我们将这些秘密添加到我们的配置文件中。在 config/Config.js 中,添加 Facebook 客户端 IDFacebook 客户端密钥

module.exports = {
  DB: 'mongodb://localhost/movie_rating_app',
  SECRET: 'movieratingappsecretkey',
  FACEBOOK_APP_ID: <facebook_client_id>,
  FACEBOOK_APP_SECRET: <facebook_client_secret>, TWITTER_APP_ID: <twitter_consumer_id>,
  TWITTER_APP_SECRET: <twitter_consumer_secret>
}

回调 URL 是在与 Twitter 成功交易后,您希望将应用程序路由到的 URL。

我们在 [前面的代码片段中定义的回调是 http://localhost:8081/login/twitter/return,我们必须定义它。配置之后是一个函数,该函数接受以下四个参数:

  • accessToken

  • refreshToken

  • profile

  • cb (回调)

请求成功后,我们的应用程序将被重定向到主页。

添加 Twitter 登录的必要路由

现在,让我们添加当点击“登录”按钮和收到 Twitter 回调时所需的必要路由。在同一个文件 twitter.js 中,添加以下路由:

const User = require('../models/User.js');
const passport = require('passport');
const config = require('./../config/Config');
const Strategy = require('passport-twitter').Strategy;

module.exports.controller = (app) => {
  // twitter strategy
  passport.use(new Strategy({
    consumerKey: config.TWITTER_APP_ID,
    consumerSecret: config.TWITTER_APP_SECRET,
    callbackURL: '/login/twitter/return',
    profileFields: ['id', 'displayName', 'email'],
  },
  (accessToken, refreshToken, profile, cb) => {
    // Handle twitter login
  }));

  app.get('/login/google',
 passport.authenticate('google', { scope: ['email'] }));

 app.get('/login/google/return',
 passport.authenticate('google', { failureRedirect: '/login' }),
 (req, res) => {
 res.redirect('/');
 });
};

在前面的代码中,我们添加了两个路由:/login/google/login/google/return。如果您记得,在 Login.vue 中,我们添加了一个链接到 http://localhost:8081/login/twitter,它将由我们在这里定义的第一个路由提供服务。

现在,最后一件事就是实际使用策略登录用户。将 twitter.js 的内容替换为以下内容:

const User = require('../models/User.js');
const passport = require('passport');
const config = require('./../config/Config');
const Strategy = require('passport-twitter').Strategy;

module.exports.controller = (app) => {
  // twitter strategy
  passport.use(new Strategy({
    consumerKey: config.TWITTER_APP_ID,
    consumerSecret: config.TWITTER_APP_SECRET,
    userProfileURL: 'https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true',
    callbackURL: '/login/twitter/return',
  },
  (accessToken, refreshToken, profile, cb) => {
 const email = profile.emails[0].value;
 User.getUserByEmail(email, (err, user) => {
 if (!user) {
 const newUser = new User({
 fullname: profile.displayName,
 email,
 facebookId: profile.id,
 });
 User.createUser(newUser, (error) => {
 if (error) {
 // Handle error
 }
 return cb(null, user);
 });
 } else {
 return cb(null, user);
 }
 return true;
 });
 }));

  app.get('/login/twitter',
    passport.authenticate('twitter', { scope: ['email'] }));

  app.get('/login/twitter/return',
    passport.authenticate('twitter', { failureRedirect: '/login' }),
    (req, res) => {
      res.redirect('/');
    });
};

在这里,我们必须考虑几个事项。Twitter 默认不允许我们访问用户的电子邮件地址。为此,我们需要在设置 Twitter 应用程序时检查一个名为“从用户请求电子邮件地址”的字段,该字段位于“权限”选项卡下。

在我们这样做之前,我们还需要设置隐私政策 URL 和服务条款 URL,以便请求用户的电子邮件地址访问权限。此设置可以在“设置”选项卡下找到:

填写隐私政策和服务条款的 URL,然后在“权限”选项卡下,勾选“从用户请求电子邮件地址”的复选框,然后点击“更新设置”:

图片

我们还需要做的是指定资源 URL,以便能够访问电子邮件地址,我们通过在twitter.js中添加以下内容来实现:

...
passport.use(new Strategy({
    consumerKey: config.TWITTER_APP_ID,
    consumerSecret: config.TWITTER_APP_SECRET,
    userProfileURL: 
    "https://api.twitter.com/1.1/account/verify_credentials.json?   
    include_email=true",
    callbackURL: '/login/twitter/return',
  },
...

现在,Twitter 登录准备工作已经完成。我们应该能够通过“通过 Twitter 登录”按钮成功登录。

护照的 Google 策略

下一个策略是护照的 Google 策略。让我们从安装这个策略开始。

安装 Passport 的 Google 策略

运行以下命令安装 Passport 的 Google 策略:

$ npm install passport-google-oauth20 --save

前面的命令应该会将包添加到您的package.json文件中:

...
"node-sass": "⁴.7.2",
"nodemon": "¹.14.10",
"passport": "⁰.4.0",
"passport-google-oauth20": "¹.0.0",
...

配置护照的 Google 策略

所有策略的配置都有些类似。对于 Google 策略,配置时我们需要遵循以下步骤:

  1. 在 Google 上创建和注册应用程序。这将为我们提供消费者密钥(API 密钥)和消费者密钥(API 密钥)。

  2. 在我们的登录页面添加一个按钮,允许我们的用户通过 Google 登录。

  3. 添加必要的路由。

  4. 添加一个中间件方法来检查身份验证。

  5. 将用户重定向到主页并显示登录用户在顶部栏中的电子邮件地址。

让我们深入了解前面每个步骤的细节。

创建和设置 Google 应用程序

正如我们在 Facebook 和 Twitter 策略中所做的那样,为了能够使用 Google 策略,我们必须构建一个 Google 应用程序。Google 的开发者门户位于console.developers.google.com/

然后,点击页面左上角的“项目”下拉列表。会出现一个弹出窗口。然后,点击加号图标创建一个新的应用程序。

您只需添加您应用程序的名称。我们将应用程序命名为movieratingapp,因为 Google 不允许下划线或任何其他特殊字符:

图片

当应用程序成功创建后,点击“凭证”和“创建”,然后点击 OAuth 客户端 ID 以生成应用程序令牌。为了生成令牌,我们首先需要通过开发者控制台在console.developers.google.com/启用 Google+ API。

然后,它将带我们到“创建同意”页面,在那里我们需要填写有关我们应用程序的一些信息。之后,在“凭证”页面,我们将能够查看我们的“客户端 ID”和“客户端密钥”。

这些令牌将用于在我们的应用程序中验证身份验证:

图片

在我们的登录页面添加一个按钮,允许用户通过 Google 登录

下一步是在我们的登录页面中添加一个“使用谷歌登录”按钮,我们将将其链接到我们刚刚创建的谷歌应用程序:

<template>
  <div>
    <div class="login">
       <a class="btn facebook" href="/login/facebook"> LOGIN WITH FACEBOOK</a>
       <a class="btn twitter" href="/login/twitter"> LOGIN WITH TWITTER</a>
       <a class="btn google" href="/login/google"> LOGIN WITH GOOGLE</a>
 </div>
    <v-form v-model="valid" ref="form" lazy-validation>
      <v-text-field
        label="Email"
        v-model="email"
        :rules="emailRules"
        required
      ></v-text-field>
      <v-text-field
        label="Password"
        v-model="password"
        :rules="passwordRules"
        required
      ></v-text-field>
      <v-btn
        @click="submit"
        :disabled="!valid"
      >
        submit
      </v-btn>
      <v-btn @click="clear">clear</v-btn><br/>
    </v-form>
  </div>
</template>
...

上述代码将添加一个“使用谷歌登录”按钮:

添加谷歌应用的配置

让我们像为 Facebook 和 Twitter 策略所做的那样配置谷歌策略。我们将创建一个单独的文件来处理谷歌登录,以便代码更简单。在controllers文件夹内创建一个名为google.js的文件,并将以下内容添加到其中:

const User = require('../models/User');
const passport = require('passport');
const config = require('./../config/Config');
const Strategy = require('passport-google-oauth20').OAuth2Strategy;

module.exports.controller = (app) => {
 // google strategy
 passport.use(new Strategy({
 clientID: config.GOOGLE_APP_ID,
 clientSecret: config.GOOGLE_APP_SECRET,
 callbackURL: '/login/google/return',
 },
 (accessToken, refreshToken, profile, cb) => {
 // Handle google login
 }));
};

正如我们在 Facebook 和 Twitter 策略中所做的那样,第一行导入 Google 策略。配置需要以下三个参数:clientIDclientSecret和回调 URL。clientIDclientSecret是我们刚刚创建的谷歌应用的App IDApp Secret

让我们将这些秘密添加到我们的config文件中。在config/Config.js中添加facebook_client_idfacebook_client_secret

module.exports = {
  DB: 'mongodb://localhost/movie_rating_app',
  SECRET: 'movieratingappsecretkey',
  FACEBOOK_APP_ID: <facebook_client_id>,
  FACEBOOK_APP_SECRET: <facebook_client_secret>,
  TWITTER_APP_ID: <twitter_client_id>,
  TWITTER_APP_SECRET: <twitter_client_secret>, GOOGLE_APP_ID: <google_client_id>,
  GOOGLE_APP_SECRET: <google_client_secret>
}

回调 URL 是您希望应用程序在成功与谷歌交易后重定向到的 URL。

我们刚刚添加的回调是http://127.0.0.1:8081/login/google/return,我们必须定义它。配置后面跟着一个函数,该函数接受以下四个参数:

  • accessToken

  • refreshToken

  • profile

  • cb(回调函数)

在请求成功后,我们的应用程序将被重定向到我们尚未定义的profile页面。

添加谷歌登录所需的必要路由

现在,让我们继续添加必要的路由,当点击登录按钮以及从谷歌收到回调时。在同一个文件google.js中,添加以下路由:

const User = require('../models/User');
const passport = require('passport');
const config = require('./../config/Config');
const Strategy = require('passport-google-oauth20').OAuth2Strategy;

module.exports.controller = (app) => {
  // google strategy
  passport.use(new Strategy({
    clientID: config.GOOGLE_APP_ID,
    clientSecret: config.GOOGLE_APP_SECRET,
    callbackURL: '/login/google/return',
  },
  (accessToken, refreshToken, profile, cb) => {
    // Handle google login
  }));

  app.get('/login/google',
 passport.authenticate('google', { scope: ['email'] }));

 app.get('/login/google/return',
 passport.authenticate('google', { failureRedirect: '/login' }),
 (req, res) => {
 res.redirect('/');
 });
};

在前面的代码中,我们添加了两个路由。如果您还记得,在Login.vue中,我们添加了一个链接到http://localhost:8081/login/google,它将由我们在这里定义的第一个路由提供。

此外,如果您还记得,在配置设置中,我们添加了一个回调函数,它将由我们在这里定义的第二个路由提供。

现在,最后要做的就是实际使用策略登录用户。用以下内容替换google.js的内容:

const User = require('../models/User');
const passport = require('passport');
const config = require('./../config/Config');
const GoogleStrategy = require('passport-google-oauth20').Strategy;

module.exports.controller = (app) => {
  // google strategy
  passport.use(new GoogleStrategy({
    clientID: config.GOOGLE_APP_ID,
    clientSecret: config.GOOGLE_APP_SECRET,
    callbackURL: '/login/google/return',
  },
  (accessToken, refreshToken, profile, cb) => {
 const email = profile.emails[0].value;
 User.getUserByEmail(email, (err, user) => {
 if (!user) {
 const newUser = new User({
 fullname: profile.displayName,
 email,
 facebookId: profile.id,
 });
 User.createUser(newUser, (error) => {
 if (error) {
 // Handle error
 }
 return cb(null, user);
 });
 } else {
 return cb(null, user);
 }
 return true;
 });
  }));

  app.get('/login/google',
    passport.authenticate('google', { scope: ['email'] }));

  app.get('/login/google/return',
    passport.authenticate('google', { failureRedirect: '/login' }),
    (req, res) => {
      res.redirect('/');
    });
};

Passport 的 LinkedIn 策略

到现在为止,您必须非常清楚如何使用passport.js提供的每个策略。让我们快速复习这些策略,使用 LinkedIn 策略。这是我们将在本书中覆盖的最后一个策略。根据您的需求,您可以使用几种其他策略。您可以在github.com/jaredhanson/passport/wiki/Strategies找到列表。

现在,让我们从安装这个策略开始。

安装 Passport 的 LinkedIn 策略

运行以下命令来安装 LinkedIn 策略:

$ npm install passport-linkedin --save

上述命令应在您的package.json文件中添加以下包:

...
"node-sass": "⁴.7.2",
"nodemon": "¹.14.10",
"passport": "⁰.4.0",
"passport-linkedin-oauth2": "².1.1",
...

配置 Passport 的 LinkedIn 策略

所有策略的配置大致相似。因此,以下是我们必须遵循的步骤来配置此策略:

  1. 在 LinkedIn 上创建和注册一个应用。这将为我们提供消费者密钥(API 密钥)和消费者密钥(API 密钥)。

  2. 在我们的登录页面添加一个按钮,允许用户通过 LinkedIn 登录。

  3. 添加必要的路由。

  4. 添加一个中间件方法来检查身份验证。

  5. 将用户重定向到主页并在顶部栏显示已登录用户的电子邮件。

让我们深入了解前面每个步骤的细节。

创建并设置 LinkedIn 应用

就像我们对 Facebook 和 Twitter 策略所做的那样,为了能够使用 LinkedIn 策略,我们必须构建一个 LinkedIn 应用。LinkedIn 的开发者门户位于 www.linkedin.com/developer/apps。您将在那里看到所有应用的列表。您还会注意到一个创建新应用的按钮;点击创建应用。

我们只需添加我们应用的名称。我们可以将应用命名为任何我们想要的名称,但为了我们的应用,我们将将其命名为 movie_rating_app

在应用程序成功创建后,您可以在凭据选项卡中看到 API 密钥(clientID)和 API 密钥(client secret)。

这些令牌将用于在我们的应用程序中验证身份验证:

在我们的登录页面添加一个按钮,允许用户通过 LinkedIn 登录

下一步是在我们的登录页面添加一个 LOGIN WITH LINKEDIN 按钮,我们将将其链接到我们刚刚创建的 LinkedIn 应用。

Login.vue 中添加以下代码:

<template>
  <div>
    <div class="login">
      <a class="btn facebook" href="/login/facebook"> LOGIN WITH FACEBOOK</a>
       <a class="btn twitter" href="/login/twitter"> LOGIN WITH TWITTER</a>
       <a class="btn google" href="/login/google"> LOGIN WITH GOOGLE</a>
       <a class="btn linkedin" href="/login/linkedin"> LOGIN WITH LINKEDIN</a>
    </div>
    <v-form v-model="valid" ref="form" lazy-validation>
      <v-text-field
        label="Email"
        v-model="email"
        :rules="emailRules"
        required
      ></v-text-field>
      <v-text-field
        label="Password"
        v-model="password"
        :rules="passwordRules"
        required
      ></v-text-field>
      <v-btn
        @click="submit"
        :disabled="!valid"
      >
        submit
      </v-btn>
      <v-btn @click="clear">clear</v-btn><br/>
    </v-form>
  </div>
</template>
<script>
  import axios from 'axios';
  import bus from "./../bus.js";

  export default {
    data: () => ({
      valid: true,
      email: '',
      password: '',
      emailRules: [
        (v) => !!v || 'E-mail is required',
        (v) => /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(v) || 'E-mail must be valid'
      ],
      passwordRules: [
        (v) => !!v || 'Password is required',
      ]
    }),
    methods: {
      async submit () {
        if (this.$refs.form.validate()) {
          return axios({
            method: 'post',
            data: {
              email: this.email,
              password: this.password
            },
            url: '/users/login',
            headers: {
              'Content-Type': 'application/json'
            }
          })
          .then((response) => {
            localStorage.setItem('jwtToken', response.data.token)
            this.$swal("Good job!", "You are ready to start!", 
            "success");
            bus.$emit("refreshUser");
            this.$router.push({ name: 'Home' });
          })
          .catch((error) => {
            const message = error.response.data.message;
            this.$swal("Oh oo!", `${message}`, "error")
          });
        }
      },
      clear () {
        this.$refs.form.reset()
      }
    }
  }
</script>

前面的代码将添加一个 LOGIN WITH LINKEDIN 按钮:

添加 LinkedIn 应用的配置

让我们像配置所有其他策略一样配置 LinkedIn 策略。我们将创建一个单独的文件来处理 LinkedIn 登录,以便代码更简单。让我们在 controllers 文件夹内创建一个名为 linkedin.js 的文件,并将以下内容添加到其中:

const User = require('../models/User.js');
const passport = require('passport');
const config = require('./../config/Config');
const Strategy = require('passport-linkedin').Strategy;

module.exports.controller = (app) => {
 // linkedin strategy
 passport.use(new Strategy({
 consumerKey: config.LINKEDIN_APP_ID,
 consumerSecret: config.LINKEDIN_APP_SECRET,
 callbackURL: '/login/linkedin/return',
 profileFields: ['id', 'first-name', 'last-name', 'email-address']
 },
 (accessToken, refreshToken, profile, cb) => {
 // Handle linkedin login
 }));
};

在前面的代码中,第一行导入了 LinkedIn 策略。配置需要以下三个参数:clientIDclientSecret 和回调 URL。clientIDclientSecret 分别是我们刚刚创建的 LinkedIn 应用的 App IDApp Secret

让我们在 config 文件中添加这些密钥。在 config/Config.js 中添加 Facebook Client IDFacebook Client Secret

module.exports = {
  DB: 'mongodb://localhost/movie_rating_app',
  SECRET: 'movieratingappsecretkey',
  FACEBOOK_APP_ID: <facebook_client_id>,
  FACEBOOK_APP_SECRET: <facebook_client_secret>,
  TWITTER_APP_ID: <twitter_consumer_id>,
  TWITTER_APP_SECRET: <twitter_consumer_secret>,
  GOOGLE_APP_ID: <google_consumer_id>,
  GOOGLE_APP_SECRET: <google_consumer_secret>,
  LINKEDIN_APP_ID: <linkedin_consumer_id>,
 LINKEDIN_APP_SECRET: <linkedin_consumer_secret>
}

callbackURL 是在成功与 LinkedIn 交易后,您希望将应用程序路由到的 URL。

在前面的代码中定义的 callbackURLhttp://127.0.0.1:8081/login/linkedin/return,我们必须定义它。配置后面跟着一个函数,该函数接受以下四个参数:

  • accessToken

  • refreshToken

  • profile

  • cb (回调)

在请求成功后,我们的应用程序将被重定向到我们尚未定义的个人资料页面。

添加 LinkedIn 登录所需的路由

现在,让我们添加当点击登录按钮和从 LinkedIn 收到回调时所需的必要路由:

const User = require('../models/User.js');
const passport = require('passport');
const config = require('./../config/Config');
const Strategy = require('passport-linkedin').Strategy;

module.exports.controller = (app) => {
  // linkedin strategy
  passport.use(new Strategy({
    consumerKey: config.LINKEDIN_APP_ID,
    consumerSecret: config.LINKEDIN_APP_SECRET,
    callbackURL: '/login/linkedin/return',
    profileFields: ['id', 'first-name', 'last-name', 'email-address']
  },
  (accessToken, refreshToken, profile, cb) => {
    // Handle linkedin login
  }));

  app.get('/login/linkedin',
 passport.authenticate('linkedin'));

 app.get('/login/linkedin/return',
 passport.authenticate('linkedin', { failureRedirect: '/login' }),
 (req, res) => {
 res.redirect('/');
 });
};

在前面的代码中,我们添加了两个路由。如果你记得,在 Login.vue 中,我们添加了一个链接到 http://localhost:8081/login/linkedin,这个链接将由我们在这里定义的第一个路由提供。

此外,如果你还记得,在配置设置中,我们添加了一个回调函数,它将由我们在这里定义的第二个路由提供。

现在,最后一件事就是实际使用策略登录用户。将 linkedin.js 的内容替换为以下内容:

const User = require('../models/User');
const passport = require('passport');
const config = require('./../config/Config');
const Strategy = require('passport-linkedin').Strategy;

module.exports.controller = (app) => {
  // linkedin strategy
  passport.use(new Strategy({
    consumerKey: config.LINKEDIN_APP_ID,
    consumerSecret: config.LINKEDIN_APP_SECRET,
    callbackURL: '/login/linkedin/return',
    profileFields: ['id', 'first-name', 'last-name', 'email-address'],
  },
  (accessToken, refreshToken, profile, cb) => {
 const email = profile.emails[0].value;
 User.getUserByEmail(email, (err, user) => {
 if (!user) {
 const newUser = new User({
 fullname: profile.displayName,
 email: profile.emails[0].value,
 facebookId: profile.id,
 });
 User.createUser(newUser, (error) => {
 if (error) {
 // Handle error
 }
 return cb(null, user);
 });
 } else {
 return cb(null, user);
 }
 return true;
 });
  }));

  app.get('/login/linkedin',
    passport.authenticate('linkedin'));

  app.get('/login/linkedin/return',
    passport.authenticate('linkedin', { failureRedirect: '/login' }),
    (req, res) => {
      res.redirect('/');
    });
};

使用这个,LinkedIn 登录的一切准备工作都已经就绪。现在我们应该能够通过点击“使用 LinkedIn 登录”按钮成功登录。

摘要

在本章中,我们介绍了 OAuth 是什么以及如何将不同类型的 OAuth 集成到我们的应用程序中。我们还介绍了由 passport.js 提供的 Facebook、Twitter、Google 和 LinkedIn 策略。如果你想探索不同的策略,可以在github.com/jaredhanson/passport/wiki/Strategies找到一份可用的包列表。

在下一章中,我们将了解更多关于 Vuex 是什么以及我们如何使用 Vuex 来简化我们的应用程序。

第八章:Vuex 简介

Vuex 是一个库,我们可以与 Vue.js 一起使用来管理应用中的不同状态。如果你正在构建一个不需要组件之间大量数据交换的小型应用,你最好不使用这个库。然而,随着你的应用增长,复杂性也随之增加。应用中将有多个组件,最明显的是,你需要从一个组件交换数据到另一个组件,或者跨多个组件共享相同的数据。这就是 Vuex 出现的时候。

Vue.js 还提供了一个 emit 方法,用于在不同组件之间传递数据,我们在前面的章节中已经使用过。随着你的应用增长,当数据更新时,你可能还希望更新多个组件中的数据。

因此,Vuex 提供了一个集中位置来存储我们应用中的所有数据片段。每当数据发生变化时,这个新的数据集将存储在这个集中位置。此外,所有想要使用这些数据的组件都将从存储中获取。这意味着我们有一个单一的数据存储源,我们构建的所有组件都将能够访问这些数据。

让我们先熟悉一些 Vuex 伴随的术语:

  • 状态(State):这是一个包含数据的对象。Vuex 使用一个单一的状态树,这意味着它是一个包含应用所有数据片段的单个对象。

  • 获取器(Getters):它用于从状态树中获取数据。

  • 突变(mutations):它们是改变状态树中数据的方法。

  • 动作(Actions):它们是执行突变(mutations)的函数。

我们将在本章中讨论这些内容。

传统多网页应用

在传统的多网页应用中,当我们构建一个网页应用并通过导航到浏览器打开网站时,它会请求网页服务器获取该页面并将其提供给浏览器。当我们点击同一网站上的按钮时,它再次请求网页服务器获取另一个页面并再次提供。这个过程在我们对网站进行的每一次交互中都会发生。所以,基本上,网站在每次交互时都会重新加载,这消耗了大量的时间。

以下是一个解释多页面应用工作原理的示例图:

当浏览器发送请求时,请求被发送到服务器。然后服务器返回 HTML 内容,并加载一个全新的页面。

多页面应用(MPA)同样可以提供一些好处。选择 MPA 还是 单页面应用(SPA)并不是问题,关键在于你应用的内容。如果你的应用包含大量的用户交互,你应该选择 SPA;然而,如果你的应用唯一目的是向用户提供内容,你可以选择 MPA。我们将在本章后面进一步探讨 SPAs 和 MPAs。

单页面应用(SPA)简介

与传统的 MPAs 不同,SPA 是专门为基于 Web 的应用程序设计的。SPA 在浏览器中首次加载网站时获取所有数据。一旦所有数据都获取完毕,你就不需要再获取更多数据。当进行任何其他交互时,这些数据将通过互联网获取,无需向服务器发送请求,也不需要重新加载页面。这意味着 SPA 比传统的 MPAs 要快得多。然而,由于 SPA 在首次加载时一次性获取所有数据,因此首次页面加载时间可能会较慢。一些集成了 SPA 的应用程序包括 Gmail、Facebook、GitHub、Trello 等等。SPA 的核心理念是通过将内容放在一个单独的页面上,并避免让用户等待他们想要的信息,从而提升用户体验。

下图是一个关于 SPA 工作原理的示例图:

网站在首次页面加载时就包含了所有需要的内容。当用户点击某个内容时,它只会获取该特定区域的信息,并只刷新网页的该部分。

SPA 与 MPA 的比较

SPA 和 MPA 服务于不同的目的。你可能需要根据你的需求选择使用其中之一。在你开始你的应用之前,确保你清楚你想要构建的应用类型。

使用 MPAs 的优点

如果你想让你的应用对搜索引擎优化(SEO)友好,多页面应用(MPAs)是最佳选择。谷歌可以通过搜索你在每个页面分配的关键词来爬取你应用的不同页面,而在单页面应用(SPA)中这是不可能的,因为 SPA 只有一个页面。

使用 MPA 的缺点

使用 MPAs 有一些缺点:

  • MPA 的开发工作比 SPA 要多得多,因为前端和后端是紧密耦合的。

  • MPAs 的前端和后端紧密耦合,这使得前端和后端开发者之间的工作分离变得更加困难。

使用 SPA 的优点

SPA 提供了许多好处:

  • 减少服务器响应时间:SPA 在网站首次加载时获取所有所需数据。使用此类应用,服务器无需重新加载网站上的资源。如果需要获取新数据,它只会从服务器获取更新的信息片段,与多页面应用不同,这显著减少了服务器响应时间。

  • 更好的用户交互:服务器响应时间的减少最终改善了用户体验。每次交互,用户都会得到一个渲染速度更快的页面,这意味着满意的客户

  • 改变 UI 的灵活性:SPA 没有耦合的前端和后端。这意味着我们可以更改前端并完全重写它,而无需担心服务器端会出问题。

  • 数据缓存:SPA 在本地存储中缓存数据。它只在第一次请求时发送一次请求并保存数据。这使得即使在断网的情况下,应用仍然可用。

使用 SPA 的缺点

使用 SPA 也有一些缺点:

  • SPA 对搜索引擎优化(SEO)不友好。由于所有操作都在单个页面上完成,可爬性非常低。

  • 由于只有一个链接到该页面的链接,因此您无法与他人共享特定的信息。

  • 与 MPA 相比,单页应用(SPA)的安全问题更为严重。

Vuex 简介

Vuex 是一个专为与 Vue.js 构建的应用程序一起使用的状态管理库。它是 Vuex 的集中式状态管理。

Vuex 的核心概念

我们在简介中简要了解了这些核心概念。现在,让我们更详细地探讨每个概念:

图片

前面的图示是一个简单的图解,说明了 Vuex 是如何工作的。最初,所有内容都存储在状态中,这是唯一的真相来源。每个视图组件都会从这个状态中获取数据。每当需要更改时,动作会在数据上执行变动并将其存储回状态:

图片

当我们在浏览器中打开我们的应用程序时,所有 Vue 组件都将被加载。当我们点击按钮从组件获取某些信息时,该组件将触发一个执行数据变动的动作。当变动成功完成后,状态对象被更新,并使用新值。然后,我们可以使用新状态来为我们的组件提供数据,并在浏览器中显示。

创建一个简单的 Vuex 应用程序

我们将从头开始创建一个新的应用程序来学习 Vuex 的基础知识。让我们开始吧。

让我们首先创建一个新的应用程序:

$ vue init webpack vuex-tutorial

前面的代码片段将询问您有关应用程序设置的一些问题。您可以选择您想要保留的内容。我将选择以下配置:

图片

安装完成后,导航到项目目录:

$ cd vuex-tutorial

下一步是运行以下命令:

$ npm install

之后,运行以下命令:

$ npm run dev

前面的命令将在localhost:8080上启动服务器并打开一个端口。

安装 Vuex

下一步是安装vuex。为此,请运行以下命令:

$ npm install --save vuex

设置 Vuex

现在,让我们创建一个store文件夹来管理我们应用程序中的vuex

创建存储文件

src目录中创建一个store文件夹和一个store.js文件。然后,将以下内容添加到store.js文件中:

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

在前面的代码块中,Vue.use(Vuex)这一行导入了 Vuex 库。没有这个,我们将无法使用任何vuex功能。现在,让我们构建一个存储对象。

状态

在相同的store.js文件中,添加以下代码行:

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const state = {
 count: 0
}

export const store = new Vuex.Store({
 state
})

在前面的代码中,我们将名为count的变量的默认状态设置为0,并通过 store 导出了一个 Vuex 状态。

现在,我们需要修改src/main.js

// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import { store } from './store/store'

Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
  el: '#app',
  store,
  components: { App },
  template: '<App/>'
})

前面的代码导入了我们刚刚创建的存储文件,我们可以在我们的 Vue 组件中访问这个变量。

让我们继续创建一个将获取此存储数据的组件。当我们使用 Vue 创建一个新应用时,会创建一个默认组件。如果我们查看src/components目录,我们会找到一个名为HelloWorld.vue的文件。让我们使用相同的组件HelloWorld.vue,或者你可以创建一个新的。让我们修改这个文件以访问我们在状态中定义的count

src/components/HelloWorld.vue中添加以下代码:

<template>
  <div class="hello">
 <h1>{{ $store.state.count }}</h1>
 </div>
</template>

<script>
export default {
  name: 'HelloWorld',
  data () {
    return {
      msg: 'Welcome to Your Vue.js App'
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

以下是最终的文件夹结构:

图片

前面的截图应该在HelloWorld.vue组件中打印出计数的默认值。如果你导航到http://localhost:8080/#/,你应该看到以下截图:

图片

在前面的截图中,我们直接使用$运算符访问了存储中的计数变量,这不是首选的方法。我们已经学习了使用状态的基本知识。现在,通过使用getters来访问变量是正确的方式。

Getters

getter是一个用于从存储中访问对象的函数。让我们创建一个getter方法来获取我们在存储中拥有的计数。

store.js中添加以下代码:

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const state = {
  count: 0
}

const getters = {
 fetchCount: state => state.count
}

export const store = new Vuex.Store({
  state,
  getters
})

在前面的代码中,我们添加了一个名为fetchCount的方法,它返回count的当前值。现在,为了在我们的 vue 组件HelloWorld.vue中访问这个值,我们需要更新以下代码:

<template>
  <div class="hello">
 <h1>The count is: {{ fetchCount }}</h1>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'
export default {
  name: 'HelloWorld',
  computed: mapGetters([
 'fetchCount'
 ])
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

我们必须从 Vuex 导入一个名为mapGetters的模块,它用于导入我们在store.js中创建的作为getter方法的fetchCount函数。现在,通过重新加载浏览器检查数字;这也应该打印出计数为0

图片

mutations

让我们继续讨论mutationsmutations是执行对存储状态修改的方法。我们将像定义getters一样定义mutations

store.js中添加以下代码:

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const state = {
  count: 0
}

const getters = {
  fetchCount: state => state.count
}

const mutations = {
 increment: state => state.count++,
 decrement: state => state.count--
}

export const store = new Vuex.Store({
  state,
  getters,
  mutations
})

在前面的代码中,我们添加了两个不同的mutation函数。increment方法将计数增加 1,而decrement方法将计数减少 1。这就是我们引入行为的地方。

行为

行为是调度变异函数的方法。行为执行mutations。由于actions是异步的,而mutations是同步的,因此始终使用actions来变异状态是一个好的实践。现在,就像gettersmutations一样,让我们也定义actions。在同一个文件中,即store.js,添加以下代码行:

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const state = {
  count: 0
}

const getters = {
  fetchCount: state => state.count
}

const mutations = {
  increment: state => state.count++,
  decrement: state => state.count--
}

const actions = {
 increment: ({ commit }) => commit('increment'),
 decrement: ({ commit }) => commit('decrement')
}

export const store = new Vuex.Store({
  state,
  getters,
  mutations,
  actions
})

在前面的代码中,我们添加了两个不同的函数用于增加和减少。由于这些方法提交mutations,我们需要传递一个参数来使commit方法可用。

现在我们需要使用之前定义的actions,并在我们的 vue 组件HelloWorld.vue中使它们可用:

<template>
  <div class="hello">
    <h1>The count is: {{ fetchCount }}</h1>
  </div>
</template>

<script>
import { mapGetters, mapActions } from 'vuex'
export default {
  name: 'HelloWorld',
  computed: mapGetters([
    'fetchCount'
  ]),
  methods: mapActions([
 'increment',
 'decrement'
 ])
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

要调用这些操作,让我们创建两个按钮。在 HelloWorld.vue 中,让我们添加以下代码行:

<template>
  <div class="hello">
    <h1>The count is: {{ fetchCount }}</h1>
    <button class="btn btn-primary" @click="increment">Increase</button>
 <button class="btn btn-primary" @click="decrement">Decrease</button>
  </div>
</template>
...

以下代码行添加了两个按钮,点击这些按钮会调用一个方法来增加或减少计数。让我们也导入 Bootstrap 用于 CSS。在 index.html 中,添加以下代码:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <!-- Latest compiled and minified CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
    <title>vuex-tutorial</title>
  </head>
  <body>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

就这样。现在,如果你重新加载浏览器,你应该能看到以下结果:

当你点击相关按钮时,计数应该会增加或减少。这让你对如何在应用程序中实现 Vuex 有一个基本的了解。

在电影应用程序中安装和使用 Vuex

我们介绍了 Vuex 的基础知识——它在应用程序中的工作原理和核心概念。我们介绍了如何创建存储和突变,以及如何使用操作来分发它们,还讨论了如何使用获取器从存储中获取信息。

在前几章中,我们为电影列表页面构建了一个应用程序。我们将使用同一个应用程序来演示 Vuex。我们将执行以下操作:

  • 我们将定义一个存储,其中将存储所有电影

  • 当添加新电影时,我们将自动将其显示在电影列表页面上,而无需重新加载页面

让我们打开应用程序并运行前端和后端服务器:

$ cd movie_rating_app
$ npm run build
$ nodemon server.js

此外,使用以下命令运行 mongo 服务器:

$ mongod

电影列表页面应该看起来像这样:

让我们从安装 vuex 开始:

$ npm install --save vuex

检查你的 package.json 文件;vuex 应该列在依赖项中:

...
"vue-router": "³.0.1",
    "vue-swal": "0.0.6",
    "vue-template-compiler": "².5.14",
    "vuetify": "⁰.17.6",
    "vuex": "³.0.1"
  },
...

现在,让我们创建一个文件,我们将能够将我们定义的所有 gettersmutationsactions 放进去。

定义一个存储

让我们在 src 目录中创建一个名为 store 的文件夹,并在 store 目录中创建一个名为 store.js 的新文件,并将以下代码行添加到其中:

import Vue from 'vue';
import Vuex from 'vuex';
import axios from 'axios';

Vue.use(Vuex);

export const store = new Vuex.Store({
})

就像我们在前面的示例应用程序中所做的那样,让我们添加一个 state 变量来存储电影列表页面的当前状态。

store.js 中添加以下代码行:

import Vue from 'vue';
import Vuex from 'vuex';
import axios from 'axios';

Vue.use(Vuex);

export const store = new Vuex.Store({
  state: {
 movies: []
 },
})

这意味着应用程序的初始状态将有一个空的电影列表。

现在,我们需要将此 store 导入到 main.js 中,以便在整个组件中都可以访问。在 src/main.js 中添加以下代码行:

// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import 'bootstrap/dist/css/bootstrap.min.css';
import 'bootstrap-vue/dist/bootstrap-vue.css';

import BootstrapVue from 'bootstrap-vue';
import Vue from 'vue';
import Vuetify from 'vuetify';
import VueSwal from 'vue-swal';
import App from './App';
import router from './router';
import { store } from './store/store';

Vue.use(BootstrapVue);
Vue.use(Vuetify);
Vue.use(VueSwal);

Vue.config.productionTip = false;

/* eslint-disable no-new */
new Vue({
  el: '#app',
  store,
  router,
  components: { App },
  template: '<App/>',
});

现在,当我们在浏览器中打开位置 http://localhost:8081/ 时,我们需要获取电影。以下是我们要做的事情:

  1. 修改 Home.vue 组件以调用获取电影的行动

  2. 创建一个操作来获取所有电影

  3. 创建一个突变来存储从状态中获取的电影

  4. 创建一个获取器方法,从状态中获取电影以在主页上显示

修改 Home.vue

让我们从修改我们的 Home.vue 组件开始这一部分。更新文件的 script 部分,添加以下代码行:

<script>
export default {
  name: 'Movies',
  computed: {
 movies() {
 return this.$store.getters.fetchMovies;
 }
 },
 mounted() {
 this.$store.dispatch("fetchMovies");
 },
};
</script>

在前面的代码中,在 mounted() 方法中,我们已派发了一个名为 fetchMovies 的操作,我们将在我们的操作中定义它。

当电影成功获取时,我们将使用 computed 方法,它将被映射到 movies 变量,我们将在模板中使用它:

<template>
  <v-layout row wrap>
 <v-flex xs4 v-for="movie in movies" :key="movie._id">
      <v-card>
        <v-card-title primary-title>
        ...

创建一个操作

让我们继续添加 store.js 文件中的操作:

import Vue from 'vue';
import Vuex from 'vuex';
import axios from 'axios';

Vue.use(Vuex);

export const store = new Vuex.Store({
  state: {
    movies: []
  },
  actions: {
 fetchMovies: (context, payload) => {
 axios({
 method: 'get',
 url: '/movies',
 })
 .then((response) => {
 context.commit("MOVIES", response.data.movies);
 })
 .catch(() => {
 });
 }
 }
})

在前面的代码中,我们将 axios 部分从组件中移除。当我们得到成功的响应时,我们将提交一个名为 MOVIES 的突变,然后突变状态中的 movies 的值。

创建一个突变

让我们继续添加一个突变。在 store.js 中,将内容替换为以下代码:

import Vue from 'vue';
import Vuex from 'vuex';
import axios from 'axios';

Vue.use(Vuex);

export const store = new Vuex.Store({
  state: {
    movies: []
  },
  mutations: {
 MOVIES: (state, payload) => {
 state.movies = payload;
 }
 },
  actions: {
    fetchMovies: (context, payload) => {
      axios({
        method: 'get',
        url: '/movies',
      })
        .then((response) => {
          context.commit("MOVIES", response.data.movies);
        })
        .catch(() => {
        });
    }
  }
})

前面的 mutations 突变应用程序中电影的州。

现在我们有了 actionmutation。现在,最后一部分是添加一个 getter 方法,它从状态中获取 movies 的值。

创建一个 getter

让我们在 store.js 中添加我们创建的用于管理应用程序状态的 getter 方法:

import Vue from 'vue';
import Vuex from 'vuex';
import axios from 'axios';

Vue.use(Vuex);

export const store = new Vuex.Store({
  state: {
    movies: []
  },
  getters: {
 fetchMovies: state => state.movies,
 },
  mutations: {
    MOVIES: (state, payload) => {
      state.movies = payload;
    }
  },
  actions: {
    fetchMovies: (context, payload) => {
      axios({
        method: 'get',
        url: '/movies',
      })
        .then((response) => {
          context.commit("MOVIES", response.data.movies);
        })
        .catch(() => {
        });
    }
  }
})

就这样。当我们导航到 http://localhost:8081/movies/add 时,我们应该有一个功能齐全的 Vuex 实现,它将电影数据获取到主页上。

让我们继续实现当我们在应用程序中添加电影时的存储。我们将遵循之前相同的流程:

  1. 修改 AddMovie.vue 以调用创建电影的动作

  2. 创建一个调用 POST API 创建电影的 action

  3. 创建一个 mutation 将添加的新电影存储到 movies 存储

AddMovie.vue 中的 script 内容替换为以下代码:

<script>
export default {
  data: () => ({
    movie: null,
    valid: true,
    name: '',
    description: '',
    genre: '',
    release_year: '',
    nameRules: [
      v => !!v || 'Movie name is required',
    ],
    genreRules: [
      v => !!v || 'Movie genre year is required',
      v => (v && v.length <= 80) || 'Genre must be less than equal to 
      80 characters.',
    ],
    releaseRules: [
      v => !!v || 'Movie release year is required',
    ],
    select: null,
    years: [
      '2018',
      '2017',
      '2016',
      '2015',
    ],
  }),
  methods: {
    submit() {
 if (this.$refs.form.validate()) {
 const movie = {
 name: this.name,
 description: this.description,
 release_year: this.release_year,
 genre: this.genre,
 }
 this.$store.dispatch("addMovie", movie);
 this.$refs.form.reset();
 this.$router.push({ name: 'Home' });
 }
 return true;
 },
    clear() {
      this.$refs.form.reset();
    },
  },
};
</script>

然后,将 actionmutations 添加到 store.js 文件中:

import Vue from 'vue';
import Vuex from 'vuex';
import axios from 'axios';

Vue.use(Vuex);

export const store = new Vuex.Store({
  state: {
    movies: []
  },
  getters: {
    fetchMovies: state => state.movies,
  },
  mutations: {
    ADD_MOVIE: (state, payload) => {
 state.movies.unshift(payload);
 },
    MOVIES: (state, payload) => {
      state.movies = payload;
    }
  },
  actions: {
    addMovie: (context, payload) => {
 return axios({
 method: 'post',
 data: payload,
 url: '/movies',
 headers: {
 'Content-Type': 'application/json',
 },
 })
 .then((response) => {
 context.commit("ADD_MOVIE", response.data)
 this.$swal(
 'Great!',
 'Movie added successfully!',
 'success',
 );
 })
 .catch(() => {
 this.$swal(
 'Oh oo!',
 'Could not add the movie!',
 'error',
 );
 });
 },
    fetchMovies: (context, payload) => {
      axios({
        method: 'get',
        url: '/movies',
      })
        .then((response) => {
          context.commit("MOVIES", response.data.movies);
        })
        .catch(() => {
        });
    }
  }
})

最后,运行以下命令来构建我们的 Vue 组件的静态文件:

$ npm run build

现在,当我们以管理员用户登录并添加电影时,电影应该被添加到数据库中,并且也会列在主页上。

在这样一个小型应用中使用 Vuex 是过度的。Vuex 最好的用途是在需要在不同组件间传输和共享数据的规模较大的应用中。这让你对 Vuex 的工作原理以及如何在应用中实现它有一个了解。

摘要

在本章中,我们讨论了 Vuex 是什么——Vuex 的核心概念,包括状态、getters、mutations、actions 以及如何在应用中使用它们。我们讨论了如何构建我们的应用程序以实现 Vuex,以及当应用程序规模扩大时它带来的好处。

在下一章中,我们将介绍如何为 Vue.js 和 Node.js 应用程序编写单元测试和集成测试。

第九章:测试 MEVN 应用

让我们快速回顾一下在前几章中我们已经做了什么:

  • 我们为不同的页面创建了不同的 Vue 组件

  • 我们实现了 Vuex——Vue.js 应用的集中式状态管理,并为组件定义了状态、获取器、突变和动作

  • 我们创建了控制器和模型来与 Node.js 后端交互

在本章中,我们将讨论如何编写测试代码以确保应用中的所有内容都能正常工作。编写测试代码是任何应用的组成部分。它有助于确保我们编写的功能不会崩溃,并保持我们编写代码的质量。

在编写测试时可以遵循不同的实践。始终先编写测试代码,然后再编写实际代码是一个好习惯。编写测试代码确保我们的应用不会崩溃,并且一切都会按预期工作。

这有助于我们编写更好的代码,也有助于在问题出现之前揭示潜在的问题。

编写测试的好处

在开发应用时编写测试代码有很多好处。以下是一些:

  • 确保代码按预期工作:它有助于确保我们应用中编写的每一块功能都按预期工作。

  • 提高代码质量:它提高了代码质量。由于编写测试代码有助于预防可能出现的缺陷,因此在编写实际代码之前,它提高了代码的质量。

  • 提前识别 bug:它有助于在早期阶段识别 bug。由于为每个功能编写了测试代码,因此可以提前识别 bug 和问题。

  • 作为新开发者的文档:测试代码就像文档一样。如果我们需要新开发者开始在同一应用上工作,测试代码帮助他们理解应用是如何工作的,而不是通过所有应用代码。

  • 测试代码使应用开发更快:如果我们不编写测试代码,我们会更快地编写代码。然而,如果我们跳过这个过程,我们后来会花费大部分时间来修复开始爬入的 bug,而这些 bug 本可以用测试代码提前识别出来。

  • 应用无需运行:编写测试代码并运行它不需要应用处于运行状态。它也不需要构建应用。这显著减少了开发时间。

因此,在本章中,我们将讨论以下主题:

  • 学习为什么以及如何编写单元测试和端到端测试

  • 了解为 Vue.js 和 Node.js 应用编写测试代码的技术

  • 修改我们的应用结构以实现单元和端到端代码

  • 为 Vue 组件编写测试代码

单元测试简介

单元测试是一种软件开发过程,其中测试并检查应用程序的最小功能是否按预期工作。单元是任何应用程序的最小部分。为应用程序的每个单元编写的测试代码都是相互独立的。单元测试本身的目标是执行单个测试并确保每个部分都是正确的。

编写单元测试的约定

如果你在编写单元测试时遵循某些指南和原则,这将使你的代码易于维护和阅读。以下是一些我们可以在为任何应用程序编写单元测试时使用的技巧:

  • 单元测试应该在小的单元中进行——针对单个类或方法。

  • 单元测试应在隔离状态下进行,这意味着单元测试不应依赖于任何其他类或方法,这通过模拟这些依赖关系来实现。

  • 由于单元测试是在较小的部分中进行的,因此它们应该非常轻量级,这使得测试运行得更快。

  • 单元测试应测试应用程序单元的行为。它应期望一个特定的值并返回一个特定的输出。

  • 由于单元测试是在隔离状态下进行的,因此不同单元的测试顺序不会造成问题。

  • 遵循不要重复自己DRY)的原则;代码不应该可重复。

  • 在可能的地方添加注释,解释测试的“为什么”,以便它易于理解。

端到端测试简介

端到端测试是从头到尾测试我们的应用程序。而单元测试测试的是应用程序的功能是否独立工作——端到端测试检查应用程序的流程是否按预期执行。通常,端到端测试确保所有用户交互都按预期进行。端到端测试确保应用程序的流程按预期工作。

编写端到端测试的约定

编写端到端测试时有一些需要遵循的指南:

  • 编写测试用例时应考虑最终用户和实际场景

  • 应为不同的场景创建多个测试用例

  • 应为所有涉及的软件或应用程序收集需求

  • 对于每个需求,尽可能收集尽可能多的条件或场景

  • 为每个场景编写单独的测试用例

我们将使用的技术

这里有一些我们将用于编写应用程序测试的包:

我们将在学习过程中讨论这些技术。

介绍 Mocha

让我们创建一个单独的工作目录来学习编写测试。创建一个名为test_js的文件夹,并切换到test_js目录:

> mkdir test_js
> cd test_js

让我们在test_js文件夹内创建一个名为test的单独文件夹:

> mkdir test

要访问mocha,您必须全局安装它:

$ npm install mocha -g --save-dev

让我们在mocha中编写一个简单的测试代码。我们将为简单的函数编写一个测试,该函数接受两个参数并返回参数的和。

让我们在test文件夹内创建一个名为add.spec.js的文件,并添加以下代码:

const addUtility = require('./../add.js');

然后,从test_js文件夹中运行以下命令:

$ mocha

这个测试将失败,我们需要一个名为add.js的工具,它不存在。它显示以下错误:

让我们编写足够的代码来通过测试。在test_js项目的根目录下创建一个名为add.js的文件,然后再次运行代码,应该可以通过:

让我们继续给测试代码添加逻辑来检查我们的add函数。在add.spec.js中,添加以下代码行:

var addUtility = require('./../add.js');

describe('Add', function(){
 describe('addUtility', function(){
 it('should have a sum method', function(){
 assert.equal(typeof addUtility, 'object');
 assert.equal(typeof addUtility.sum, 'function');
 })
 })
});

现在是assert库的时间。assert库有助于检查传递的表达式是正确还是错误。在这里,我们将使用 Node.js 的内置断言库。

要包含assert库,让我们在add.spec.js中添加以下代码行:

var assert = require("assert")
var addUtility = require("./../add.js");

describe('Add', function(){
  describe('addUtility', function(){
    it('should have a sum method', function(){
      assert.equal(typeof addUtility, 'object');
      assert.equal(typeof addUtility.sum, 'function');
    })
  })
});

让我们重新运行mocha。这应该再次失败,因为我们还没有向我们的模块添加方法。所以,让我们继续并添加以下代码到add.js中:

var addUtility = {}

addUtility.sum = function () {
 'use strict';
 return true;
}

module.exports = addUtility;

让我们重新运行mocha。现在应该可以通过测试了:

现在,让我们将功能部分添加到求和方法中。在add_spec.js中添加以下代码:

var assert = require("assert")
var addUtility = require("./../add.js");

describe('Add', function(){
  describe('addUtility', function(){
    it('should have a sum method', function(){
      assert.equal(typeof addUtility, 'object');
      assert.equal(typeof addUtility.sum, 'function');
    })

    it('addUtility.sum(5, 4) should return 9', function(){
 assert.deepEqual(addUtility.sum(5, 4), 9)
 })
  })
});

然后,看看测试;它失败了。然后,向我们的模块中添加逻辑:

var addUtility = {}

addUtility.sum = function (a, b) {
  'use strict';
  return a + b;
}

module.exports = addUtility;

然后,重新运行mocha,测试应该可以通过。就这样!:

您可以继续添加更多测试案例以确保没有东西出错。

介绍 chai

让我们讨论chaichai是一个断言库,与mocha一起使用。我们也可以使用本地的assertion库,但chai提供了更多的灵活性。

chai使编写测试定义变得容易得多。让我们安装chai并修改前面的测试,使其看起来更简单易懂:

$ npm install chai -g

我们传递了-g选项来全局安装它,因为我们没有package.json配置。

让我们在之前的测试中使用chai。在add.spec.js中,添加以下代码行:

var expect = require('chai').expect;
var addUtility = require("./../add.js");

describe('Add', function(){
  describe('addUtility', function(){
    it('should have a sum method', function(){
      expect(addUtility).to.be.an('object');
 expect(addUtility).to.have.property('sum');
    })

    it('addUtility.sum(5, 4) should return 9', function(){
      expect(addUtility.sum(5, 4)).to.deep.equal(9);
    })

    it('addUtility.sum(100, 6) should return 106', function(){
      expect(addUtility.sum(100, 6)).to.deep.equal(106);
    })
  })
});

我们已经将assertion库替换为chaiexpect()方法,这使得代码更加简单易懂。

介绍 sinon

sinon 用于测试 JavaScript 测试中的 spies、stubs 和 mocks。为了了解这些,让我们继续学习我们 controller 文件 controller/movies.js 中的电影评分应用程序:

const Movie = require("../models/Movie");
const passport = require("passport");

module.exports.controller = (app) => {
  // fetch all movies
  app.get("/movies", function(req, res) {
    Movie.find({}, 'name description release_year genre', function 
    (error, movies) {
      if (error) { console.log(error); }
       res.send({
        movies: movies
      })
    })
  })

  // add a new movie
  app.post('/movies', (req, res) => {
    const movie = new Movie({
      name: req.body.name,
      description: req.body.description,
      release_year: req.body.release_year,
      genre: req.body.genre
    })

    movie.save(function (error, movie) {
      if (error) { console.log(error); }
      res.send(movie)
    })
  })
}               

在前面的代码中,每个 API 调用都需要一个请求和一个响应对象,我们需要对其进行模拟。为此,我们使用了 sinonsinon 提供了一种机制来 stubmock 请求。

sinon 提供的三个主要方法是 spies、stubs 和 mocks:

  • 间谍(Spies):间谍有助于创建假函数。我们可以使用间谍来跟踪函数是否被执行。

  • 存根(Stubs):存根帮助我们使函数返回我们想要的任何内容。当我们想要测试给定函数的不同场景时,这很有用。

  • 模拟(Mocks):模拟用于伪造网络连接。它们帮助我们创建一个模拟的类实例,这有助于设定预定的期望。

让我们在 movies 控制器中编写一个 get 调用的测试:

// fetch all movies
  app.get("/movies", function(req, res) {
    Movie.find({}, 'name description release_year genre', function 
    (error, movies) {
      if (error) { console.log(error); }
       res.send({
        movies: movies
      })
    })
  })

让我们在 test/units 文件夹内创建一个新文件,称为 movies.spec.js

var movies = require("./../../../controllers/movies.js");
var expect = require('chai').expect;

describe('controllers.movies.js', function(){
 it('exists', function(){
 expect(movies).to.exist
 })
})

这段测试代码简单地检查 controller 是否存在,当我们运行以下命令时应该通过:

$ mocha test/unit/controllers/movies.spec.js

这个命令运行我们的 controller/movies.js 的测试,并且应该通过以下输出:

图片

让我们先为这个简单的方法写一个测试。让我们创建一个只响应一个具有名称的对象的请求。在 movies.js 中,让我们添加以下代码来创建一个模拟的 API:

const Movie = require("../models/Movie");
const passport = require("passport");

module.exports.controller = (app) => {
 // send a dummy test
 app.get("/dummy_test", function(req, res) {
 res.send({
 name: 'John'
 })
 })

在前面的代码中,我们有一个简单的返回对象的函数。

让我们继续添加功能测试部分。我们将为 /dummy_test 方法编写测试。

movies.spec.js 中,让我们添加以下代码行:

var controller = require("./../../../controllers/movies.js");
let chaiHttp = require('chai-http');
let chai = require('chai');
var expect = chai.expect;
var should = chai.should();
var express = require("express");
let server = require('./../../../server.js');
var app = express();
chai.use(chaiHttp);

function buildResponse() {
 return http_mocks.createResponse({eventEmitter: require('events').EventEmitter})
}

describe('controllers.movies', function(){
 it('exists', function(){
 expect(controller).to.exist
 })
})

describe('/GET dummy_test', () => {
 it('it should respond with a name object', (done) => {
 chai.request(server)
 .get('/dummy_test')
 .end((err, res) => {
 res.should.have.status(200);
 res.body.should.be.an('object');
 done();
 });
 });
});

在前面的代码中,我们添加了一个名为 chai-http 的新包,用于模拟请求。让我们按照以下方式安装此包:

$ npm install chai-http --save

让我们现在使用以下命令运行测试:

$ mocha test/unit/controllers/movies.spec.js

前面的命令应该给出以下输出:

图片

编写 Node.js 服务器的测试

让我们从为后端部分的 node 服务器构建的应用程序编写测试开始。

我们将使用以下文件夹结构:

图片

test 文件夹内有两个文件夹。一个用于单元测试,称为 unit,另一个用于端到端测试,称为 e2e。我们将从编写单元测试开始,这些测试将放在 unit 目录下。命名约定是给每个我们将为其编写测试的文件添加 .spec 部分。

编写控制器的测试

让我们从编写我们添加的控制器测试开始。在test/unit/specs中创建一个名为controllers的文件夹,并在其中创建一个名为movies.spec.js的新文件。这将是我们创建任何组件(控制器、模型或 Vue 组件)测试文件时遵循的命名约定:实际的文件名后跟.spec.js。这有助于保持代码的可读性。

让我们先回顾一下movies.js文件中有什么:

var Movie = require("../models/Movie");

module.exports.controller = (app) => {
  // fetch all movies
  app.get("/movies", function(req, res) {
    Movie.find({}, 'name description release_year genre', function  
    (error, movies) {
      if (error) { console.log(error); }
       res.send({
        movies: movies
      })
    })
  })

  // add a new movie
  app.post('/movies', (req, res) => {
    const movie = new Movie({
      name: req.body.name,
      description: req.body.description,
      release_year: req.body.release_year,
      genre: req.body.genre
    })

    movie.save(function (error, movie) {
      if (error) { console.log(error); }
      res.send(movie)
    })
  })
}

这个控制器有两个方法——一个 GET 请求和一个 POST 请求。GET 请求用于从数据库中获取所有电影,而 POST 请求将带有给定参数的电影保存到数据库中。

让我们先添加 GET 请求的规范。在刚刚创建的movies.spec.js文件中添加以下内容:

const controller = require("./../../../../controllers/movies.js");
const Movie = require("./../../../../models/Movie.js");
let server = require('./../../../../server.js');
let chai = require('chai');
let sinon = require('sinon');
const expect = chai.expect;
let chaiHttp = require('chai-http');
chai.use(chaiHttp);
const should = chai.should();

前两行需要相应的Movie组件控制器和模型,我们稍后会用到。我们还需要服务器文件。

其他包,如chaisinonexpectshould,也是为了断言所需的。

接下来,我们需要一个名为chai-http的包来向服务器发送请求。这个包将用于 HTTP 请求断言。所以,让我们首先使用以下命令安装这个包:

$ npm install chai-http --save

现在,我们可以开始添加第一个测试。用以下代码替换movies.spec.js中的内容:

const controller = require("./../../../../controllers/movies.js");
const Movie = require("./../../../../models/Movie.js");
let server = require('./../../../../server.js');
let chai = require('chai');
let sinon = require('sinon');
const expect = chai.expect;
let chaiHttp = require('chai-http');
chai.use(chaiHttp);
const should = chai.should();

describe('controllers.movies', function(){
 it('exists', function(){
 expect(controller).to.exist
 })
})

前面的方法描述了movies控制器。它只是简单地检查我们描述的控制器是否存在。

为了确保我们的node服务器有连接,让我们从server.js导出服务器。将以下代码添加到server.js中:

...
const port = process.env.API_PORT || 8081;
app.use('/', router);
var server = app.listen(port, function() {
  console.log(`api running on port ${port}`);
});

module.exports = server

现在,让我们使用以下命令运行测试:

$ mocha test/unit/specs/controllers/movies.spec.js

测试应该通过。

让我们继续添加 GET 请求的测试。在movies.js中,我们有以下代码:

var Movie = require("../models/Movie");

module.exports.controller = (app) => {
  // fetch all movies
  app.get("/movies", function(req, res) {
 Movie.find({}, 'name description release_year genre', function 
    (error, movies) {
 if (error) { console.log(error); }
 res.send({
 movies: movies
 })
 })
 })  ...
}

由于这个方法是从数据库中获取所有现有电影,我们首先需要在这里构建模拟电影来实际测试它。让我们用以下代码替换movies.spec.js中的内容:

const controller = require("./../../../../controllers/movies.js");
const Movie = require("./../../../../models/Movie.js");
let server = require('./../../../../server.js');
let chai = require('chai');
let sinon = require('sinon');
const expect = chai.expect;
let chaiHttp = require('chai-http');
chai.use(chaiHttp);
const should = chai.should();

describe('controllers.movies', function(){
  it('exists', function(){
    expect(controller).to.exist
  })

  describe('/GET movies', () => {
 it('it should send all movies', (done) => {
 var movie1 = {
 name: 'test1',
 description: 'test1',
 release_year: 2017,
 genre: 'test1'
 };
 var movie2 = {
 name: 'test2',
 description: 'test2',
 release_year: 2018,
 genre: 'test2'
 };
 var expectedMovies = [movie1, movie2];
 sinon.mock(Movie)
 .expects('find')
 .yields('', expectedMovies);
 chai.request(server)
 .get('/movies')
 .end((err, res) => {
 res.should.have.status(200);
 res.body.should.be.an('object');
 expect(res.body).to.eql({
 movies: expectedMovies
 });
 done();
 });
 });
 });
})

让我们一步一步地了解我们在这里做了什么:

  • 我们使用sinon模拟创建了一些电影。

  • 我们使用chai创建了一个 HTTP GET 请求

  • 我们有三个期望:

    • 请求的状态应该是200

    • 请求响应应该是一个对象

    • 响应应包含我们使用模拟创建的电影列表。

让我们再次使用以下命令运行测试:

$ mocha test/unit/specs/controllers/movies.spec.js 

测试应该通过。

现在让我们继续添加movies.js的 POST 请求测试。在movies.js中,我们有以下代码:

var Movie = require("../models/Movie");

module.exports.controller = (app) => {
  ...

  // add a new movie
  app.post('/movies', (req, res) => {
    const movie = new Movie({
      name: req.body.name,
      description: req.body.description,
      release_year: req.body.release_year,
      genre: req.body.genre
    })

    movie.save(function (error, movie) {
      if (error) { console.log(error); }
      res.send(movie)
    })
  })
}

POST 方法接受电影的前四个属性并将它们保存到数据库中。让我们为这个 POST 请求添加测试。用以下代码替换movies.spec.js中的内容:

const controller = require("./../../../../controllers/movies.js");
const Movie = require("./../../../../models/Movie.js");
let server = require('./../../../../server.js');
let chai = require('chai');
let sinon = require('sinon');
const expect = chai.expect;
let chaiHttp = require('chai-http');
chai.use(chaiHttp);
const should = chai.should();

describe('controllers.movies', function(){
  it('exists', function(){
    expect(controller).to.exist
  })

  describe('/GET movies', () => {
    it('it should send all movies', (done) => {
      var movie1 = {
        name: 'test1',
        description: 'test1',
        release_year: 2017,
        genre: 'test1'
      };
      var movie2 = {
        name: 'test2',
        description: 'test2',
        release_year: 2018,
        genre: 'test2'
      };
      var expectedMovies = [movie1, movie2];
      sinon.mock(Movie)
        .expects('find')
        .yields('', expectedMovies);
      chai.request(server)
        .get('/movies')
        .end((err, res) => {
          res.should.have.status(200);
          res.body.should.be.an('object');
          expect(res.body).to.eql({
            movies: expectedMovies
          });
          done();
      });
    });
  });

  describe('POST /movies', () => {
 it('should respond with the movie that was added', (done) => {
 chai.request(server)
 .post('/movies')
 .send({
 name: 'test1',
 description: 'test1',
 release_year: 2018,
 genre: 'test1'
 })
 .end((err, res) => {
 should.not.exist(err);
 res.status.should.equal(200);
        res.body.should.be.an('object');
 res.body.should.include.keys(
 '_id', 'name', 'description', 'release_year', 'genre'
 );
 done();
 });
 });
 });
})

在前面的代码块中,我们所做的是,对于 POST 请求:

  • 我们正在使用电影参数:namedescriptionrelease_yeargenre发送 POST 请求。

  • 我们有三个预期:

    • 请求的状态应该是200

    • 请求响应应该是一个对象

    • 响应应该包含所有四个属性,以及电影的 ID

现在如果我们再次运行测试,它们都应该通过。

同样,我们也可以为其他控制器添加测试。

编写模型的测试

让我们继续添加对我们定义的模型进行的测试。在test/unit/specs中创建一个名为models的文件夹,并为我们的Movie.js模型创建一个测试文件。因此,规范文件的名称将是Movie.spec.js

让我们首先看看我们的Movie.js中有什么:

const mongoose = require('mongoose');
const Schema = mongoose.Schema
const MovieSchema = new Schema({
  name: String,
   description: String,
   release_year: Number,
   genre: String
})

const Movie = mongoose.model('Movie', MovieSchema)
module.exports = Movie

我们在这里只定义了一个Schema,它定义了Movie集合的数据类型。

让我们在Movie.spec.js中添加这个模型的规范。添加以下内容到其中:

var Movie = require("./../../../../models/Movie.js");
let chai = require('chai');
var expect = chai.expect;
var should = chai.should();

我们不需要在控制器测试中添加的所有组件。我们这里只有简单的断言测试,所以我们需要Movie模型和chai方法。

让我们像为控制器添加测试一样,添加对Movie存在的测试。将Movie.spec.js中的内容替换为以下代码:

var Movie = require("./../../../../models/Movie.js");
let chai = require('chai');
var expect = chai.expect;
var should = chai.should();

describe('models.Movie', function(){
 it('exists', function(){
 expect(Movie).to.exist
 })
})

这个测试检查我们描述的Model是否存在。让我们使用以下命令运行测试:

$ mocha test/unit/specs/models/Movie.spec.js

测试应该通过以下输出:

让我们继续添加当我们将Movierelease_year属性发送为字符串时的测试。由于我们对release_year属性有验证,向其发送字符串值应该抛出错误。

Movie.spec.js中的内容替换为以下代码:

var Movie = require("./../../../../models/Movie.js");
let chai = require('chai');
var expect = chai.expect;
var should = chai.should();

describe('models.Movie', function(){
  it('exists', function(){
    expect(Movie).to.exist
  })

  describe('Movie', function() {
 it('should be invalid if release_year is not an integer', 
    function(done){
 var movie = new Movie({
 name: 'test',
 description: 'test',
 release_year: 'test',
 genre: 'test'
 });

 movie.validate(function(err){
 expect(err.errors.release_year).to.exist;
 done();
 })
 })
 })
})

这里,我们准备了一个具有无效release_year值的电影对象。我们在这里的预期是,在验证模型时,应该发送一个错误。

让我们运行测试,它应该通过以下输出:

同样,我们也可以为其他模型添加测试。

编写 Vue.js 组件的测试

让我们继续编写我们的 Vue.js 组件的测试规范。我们将从最简单的组件开始,即Contact.vue页面。

到目前为止,我们的Contact.vue页面是这样的:

<template>
  <v-layout>
    this is contact
  </v-layout>
</template>

让我们稍微修改一下组件,使测试更易于理解。将Contact.vue中的内容替换为以下代码:

<template>
 <div class="contact">
 <h1>this is contact</h1>
 </div>
</template>

现在,让我们首先创建必要的文件夹和文件来编写我们的测试。在test/unit/specs目录中创建一个名为Contact.spec.js的文件,并将以下内容添加到其中:

import Vue from 'vue';
import Contact from '@/components/Contact';

describe('Contact.vue', () => {
 it('should render correct contents', () => {
 const Constructor = Vue.extend(Contact);
 const vm = new Constructor().$mount();
 expect(vm.$el.querySelector('.contact h1').textContent)
 .to.equal('this is contact');
 });
});

在前面的代码中,我们添加了一个测试来检查 Vue 组件Contact.vue是否渲染了正确的内容。我们期望有一个带有contact类的div元素,并且在其内部,应该有一个包含this is contact内容的h1标签。

现在,为了确保我们的测试运行,让我们验证我们是否在package.json中设置了正确的脚本以运行单元测试:

...
"scripts": {
    "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
    "start": "nodemon server.js",
    "unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run",
    "e2e": "node test/e2e/runner.js",
 "test": "npm run unit && npm run e2e",
    "lint": "eslint --ext .js,.vue src test/unit test/e2e/specs",
    "build": "node build/build.js",
    "heroku-postbuild": "npm install --only=dev --no-shrinkwrap && npm run build"
  },
...

现在,让我们使用以下命令运行测试:

$ npm run unit

测试应该通过以下输出:

让我们继续添加名为AddMovie.vue的组件的规格。在test/unit/specs内创建一个名为AddMovie.spec.js的文件,并将以下内容添加到其中:

import Vue from 'vue';
import AddMovie from '@/components/AddMovie';

describe('AddMovie', () => {
 let cmp, vm;

 beforeEach(() => {
 cmp = Vue.extend(AddMovie);
 vm = new cmp({
 data: {
 years: ['2018', '2017', '2016', '2015']
 }
 }).$mount()
 })

 it('equals years to ["2018", "2017", "2016", "2015"]', () => {
 console.log(vm.years);
 expect(vm.years).to.eql(['2018', '2017', '2016', '2015'])
 })
})

此测试声明years变量应该有给定的值,即['2018', '2017', '2016', '2015']

让我们添加另一个测试来检查我们的vue组件AddMovie.js中是否存在所需的方法。用以下代码替换AddMovie.spec.js中的内容:

import Vue from 'vue';
import AddMovie from '@/components/AddMovie';

describe('AddMovie', () => {
  let cmp, vm;

  beforeEach(() => {
    cmp = Vue.extend(AddMovie);
    vm = new cmp({
      data: {
        years: ['2018', '2017', '2016', '2015']
      }
    }).$mount()
  })

  it('equals years to ["2018", "2017", "2016", "2015"]', () => {
    console.log(vm.years);
    expect(vm.years).to.eql(['2018', '2017', '2016', '2015'])
  })

  it('has a submit() method', () => {
 assert.deepEqual(typeof vm.submit, 'function')
 })

 it('has a clear() method', () => {
 assert.deepEqual(typeof vm.clear, 'function')
 })
})

现在,让我们使用以下命令运行测试:

$ npm run unit

测试应该通过。

最后,要运行所有测试,我们可以简单地运行以下命令:

$ npm run test 

编写 e2e 测试

使用vue-cli命令创建的 vue.js 应用程序包含对使用Nightwatch的端到端测试的支持。Nightwatch是一个非常容易编写端到端测试的框架。Nightwatch使用Selenium命令来运行 JavaScript。

安装 Nightwatch

如果你还没有为e2e设置应用程序,那么让我们首先安装运行e2e测试所需的包:

$ npm install nightwatch --save

配置 Nightwatch

现在,我们需要一个配置文件来运行测试。在test文件夹内创建一个名为e2e的文件夹。添加nightwatch.conf.js文件,并将以下内容添加到其中:

require('babel-register')
var config = require('../../config')

// http://nightwatchjs.org/gettingstarted#settings-file
module.exports = {
 src_folders: ['test/e2e/specs'],
 custom_assertions_path: ['test/e2e/custom-assertions'],

 selenium: {
 start_process: true,
 server_path: require('selenium-server').path,
 host: '127.0.0.1',
 port: 4444,
 cli_args: {
 'webdriver.chrome.driver': require('chromedriver').path
 }
 },

 test_settings: {
 default: {
 selenium_port: 4444,
 selenium_host: 'localhost',
 silent: true,
 globals: {
 devServerURL: 'http://localhost:' + (process.env.PORT || config.dev.port)
 }
 },

 chrome: {
 desiredCapabilities: {
 browserName: 'chrome',
 javascriptEnabled: true,
 acceptSslCerts: true
 }
 },

 firefox: {
 desiredCapabilities: {
 browserName: 'firefox',
 javascriptEnabled: true,
 acceptSslCerts: true
 }
 }
 }
}

在前面的代码中,在test_settings属性内的设置中,我们可以看到为不同浏览器设置的不同配置。在这种情况下,Chrome、Firefox 以及开发环境在浏览器上运行的宿主和端口设置。

此外,在前面的代码中,我们指定了两个文件夹:specscustom-assertions

  • specs文件夹包含应用程序的主要测试代码。

  • custom-assertion包含一个脚本,该脚本包含在命令行上运行断言测试时显示的自定义消息。

让我们首先设置我们的custom-assertions。在custom-assertions内创建一个名为elementCount.js的文件,并将以下内容添加到其中:

// A custom Nightwatch assertion.
// The assertion name is the filename.
// Example usage:
//
// browser.assert.elementCount(selector, count)
//
// For more information on custom assertions see:
// http://nightwatchjs.org/guide#writing-custom-assertions

exports.assertion = function (selector, count) {
 this.message = 'Testing if element <' + selector + '> has count: ' + count
 this.expected = count
 this.pass = function (val) {
 return val === this.expected
 }
 this.value = function (res) {
 return res.value
 }
 this.command = function (cb) {
 var self = this
 return this.api.execute(function (selector) {
 return document.querySelectorAll(selector).length
 }, [selector], function (res) {
 cb.call(self, res)
 })
 }
}

如果你创建此应用程序时选择了e2e选项,那么你也应该有test/e2e/specs/test.js文件。如果没有,请继续创建此文件并将以下内容添加到其中:

// For authoring Nightwatch tests, see
// http://nightwatchjs.org/guide#usage

module.exports = {
 'default e2e tests': function test(browser) {
 // automatically uses dev Server port from /config.index.js
 // default: http://localhost:8080
 // see nightwatch.conf.js
 const devServer = browser.globals.devServerURL;
 console.log(devServer);

 browser
 .url(devServer)
 .waitForElementVisible('#app', 5000)
 .assert.elementPresent('.hello')
 .assert.containsText('h1', 'Welcome to Your Vue.js App')
 .assert.elementCount('img', 1)
 .end();
 },
};

这是主文件,我们将在此添加我们的应用程序测试用例。

端到端测试确保我们的应用程序的所有流程都按预期执行。当我们运行e2e测试时,我们希望应用程序的某些部分被点击并按预期的方式表现。这可以描述为测试应用程序的行为。

要能够运行e2e测试,我们需要启动一个selenium-server。如果我们查看test/e2e/nightwatch.conf.js文件,我们可以找到一个说:

...
selenium: {
 start_process: true,
    server_path: require('selenium-server').path,
    host: '127.0.0.1',
    port: 4444,
    cli_args: {
      'webdriver.chrome.driver': require('chromedriver').path
    }
  },
...

这意味着当我们运行 e2e 测试时,会自动启动 selenium-server,我们不需要运行单独的服务器。端口号定义了用于 selenium-server 的端口号。您可以保持原样并运行测试,或者您可以更改这些值并自行配置。

最后,我们需要一个 runner 文件来让 Nightwatch 运行测试。在 e2e 文件夹内创建一个名为 runner.js 的文件,并添加以下内容:

// 1\. start the dev server using production config
process.env.NODE_ENV = 'testing'

const webpack = require('webpack')
const DevServer = require('webpack-dev-server')

const webpackConfig = require('../../build/webpack.prod.conf')
const devConfigPromise = require('../../build/webpack.dev.conf')

let server

devConfigPromise.then(devConfig => {
 const devServerOptions = devConfig.devServer
 const compiler = webpack(webpackConfig)
 server = new DevServer(compiler, devServerOptions)
 const port = devServerOptions.port
 const host = devServerOptions.host
 return server.listen(port, host)
})
.then(() => {
 // 2\. run the nightwatch test suite against it
 // to run in additional browsers:
 // 1\. add an entry in test/e2e/nightwatch.conf.js under "test_settings"
 // 2\. add it to the --env flag below
 // or override the environment flag, for example: `npm run e2e -- --env chrome,firefox`
 // For more information on Nightwatch's config file, see
 // http://nightwatchjs.org/guide#settings-file
 let opts = process.argv.slice(2)
 if (opts.indexOf('--config') === -1) {
 opts = opts.concat(['--config', 'test/e2e/nightwatch.conf.js'])
 }
 if (opts.indexOf('--env') === -1) {
 opts = opts.concat(['--env', 'chrome'])
 }

 const spawn = require('cross-spawn')
 const runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' })

 runner.on('exit', function (code) {
 server.close()
 process.exit(code)
 })

 runner.on('error', function (err) {
 server.close()
 throw err
 })
})

我们将使用独立的 Selenium 服务器和端口 5555 来运行此应用程序。为此,我们首先需要安装独立服务器:

$ npm install selenium-standalone

使用以下命令运行包:

$ npx selenium-standalone start -- -port 5555

npx 是一个运行 npm 包的命令。

由于我们使用的是 5555 端口,我们还需要在 nightwatch.conf.js 文件中更新它。

使用以下代码更新 nightwatch.conf.js 中的 Selenium 配置:

...
selenium: {
    start_process: false,
    server_path: require('selenium-server').path,
    host: '127.0.0.1',
    port: 5555,
    cli_args: {
      'webdriver.chrome.driver': require('chromedriver').path
    }
  },

  test_settings: {
    default: {
      selenium_port: 5555,
      selenium_host: 'localhost',
      silent: true,
      globals: {
 devServerURL: 'http://localhost:8081'
      }
    },
...

由于我们使用 8081 端口运行 node 服务器,请确保您已更新 devServerURL 属性,正如前面代码所示。

现在,我们已经准备好使用以下命令运行测试:

$ npm run e2e

测试应该会失败,并显示以下输出:

测试失败是因为在我们的应用程序中没有具有 .hello 类的元素。因此,为了使测试通过,我们首先需要向元素添加一个标识符,我们将通过以下步骤在 e2e 测试中完成这一操作。

这里是我们想要通过 e2e 测试捕获的内容:

  1. 使用 http://localhost:8081 打开浏览器

  2. 检查具有 #inspire ID 的元素是否存在。我们已在 App.vue 中定义了以下代码:

<template>
  <v-app id="inspire">
    <v-navigation-drawer
      fixed
      v-model="drawer"
      app
    >
  1. 检查侧边栏是否包含 HomeContact 页面链接

  2. 点击 Contact 页面

  3. 联系页面应包含文本 this is contact

  4. 点击登录页面以确保登录正常工作

  5. 向我们的应用程序添加电影

  6. 评分电影

  7. 最后,添加用户退出应用程序的功能

这些是我们应用程序的重要部分。因此,我们需要为所有前面的组件添加一个标识符。向元素添加标识符的最佳实践是在构建应用程序本身时定义一个 classid。然而,我们将现在为它们分配一个标识符。

App.vue 中,使用以下代码更新高亮部分:

<template>
  <v-app id="inspire">
    <v-navigation-drawer
      fixed
      v-model="drawer"
      app
    >
      <v-list dense>
        <router-link v-bind:to="{ name: 'Home' }" class="side_bar_link">
          <v-list-tile>
            <v-list-tile-action>
              <v-icon>home</v-icon>
            </v-list-tile-action>
            <v-list-tile-content id="home">Home</v-list-tile-content>
          </v-list-tile>
        </router-link>
        <router-link v-bind:to="{ name: 'Contact' }" class="side_bar_link">
          <v-list-tile>
            <v-list-tile-action>
              <v-icon>contact_mail</v-icon>
            </v-list-tile-action>
            <v-list-tile-content id="contact">Contact</v-list-tile-content>
          </v-list-tile>
        </router-link>
      </v-list>
    </v-navigation-drawer>
    <v-toolbar color="indigo" dark fixed app>
      <v-toolbar-side-icon id="drawer" @click.stop="drawer = !drawer"></v-toolbar-side-icon>
      <v-toolbar-title>Home</v-toolbar-title>
      <v-spacer></v-spacer>
      <v-toolbar-items class="hidden-sm-and-down">
        <v-btn id="add_movie_link" flat v-bind:to="{ name: 'AddMovie' }"
          v-if="current_user && current_user.role === 'admin'">
          Add Movie
        </v-btn>
        <v-btn id="user_email" flat v-if="current_user">{{ current_user.email }}</v-btn>
        <v-btn flat v-bind:to="{ name: 'Register' }" v-if="!current_user" id="register_btn">
          Register
        </v-btn>
        <v-btn flat v-bind:to="{ name: 'Login' }" v-if="!current_user" id="login_btn">Login</v-btn>
        <v-btn id="logout_btn" flat v-if="current_user" @click="logout">Logout</v-btn>
      </v-toolbar-items>
    </v-toolbar>
    <v-content>
      <v-container fluid>
        <div id="app">
          <router-view/>
        </div>
      </v-container>
    </v-content>
    <v-footer color="indigo" app>
      <span class="white--text">&copy; 2017</span>
    </v-footer>
  </v-app>
</template>

<script>
import axios from 'axios';

import './assets/stylesheets/main.css';
import bus from './bus';

export default {
  name: 'app',
  data: () => ({
    drawer: null,
    current_user: null,
  }),
  props: {
    source: String,
  },
  mounted() {
    this.fetchUser();
    this.listenToEvents();
  },
  methods: {
    listenToEvents() {
      bus.$on('refreshUser', () => {
        this.fetchUser();
      });
    },
    async fetchUser() {
      return axios({
        method: 'get',
        url: '/api/current_user',
      })
        .then((response) => {
          this.current_user = response.data.current_user;
        })
        .catch(() => {
        });
    },
    logout() {
      return axios({
        method: 'get',
        url: '/api/logout',
      })
        .then(() => {
          bus.$emit('refreshUser');
 this.$router.push({ name: 'Home' });
        })
        .catch(() => {
        });
    },
  },
};
</script>

此外,让我们更新 AddMovie.vue 中的 id

<template>
  <v-form v-model="valid" ref="form" lazy-validation>
    <v-text-field
      label="Movie Name"
      v-model="name"
      :rules="nameRules"
      id="name"
      required
    ></v-text-field>
    <v-text-field
      name="input-7-1"
      label="Movie Description"
      v-model="description"
      id="description"
      multi-line
    ></v-text-field>
    <v-select
      label="Movie Release Year"
      v-model="release_year"
      required
      :rules="releaseRules"
      :items="years"
      id="release_year"
    ></v-select>
    <v-text-field
      label="Movie Genre"
      v-model="genre"
      id="genre"
      required
      :rules="genreRules"
    ></v-text-field>
    <v-btn
      @click="submit"
      :disabled="!valid"
      id="add_movie_btn"
    >
      submit
    </v-btn>
    <v-btn @click="clear">clear</v-btn>
  </v-form>
</template>

此外,在 Login.vue 中,让我们为表单字段添加相应的 id

<template>
  <div>
    <div class="login">
      <a href="/login/facebook">Facebook</a>
      <a href="/login/twitter">Twitter</a>
      <a href="/login/google">Google</a>
      <a href="/login/linkedin">Linkedin</a>
    </div>
    <v-form v-model="valid" ref="form" lazy-validation>
      <v-text-field
        label="Email"
        v-model="email"
        :rules="emailRules"
        id="email"
        required
      ></v-text-field>
      <v-text-field
        label="Password"
        v-model="password"
        :rules="passwordRules"
        id="password"
        required
      ></v-text-field>
      <v-btn
        @click="submit"
        :disabled="!valid"
        id="login"
      >
        submit
      </v-btn>
      <v-btn @click="clear" id="clear_input">clear</v-btn><br/>
    </v-form>
  </div>
</template>

Movie.vue 中,使用以下命令更新 Rate this Movieid

<template>
  <v-layout row wrap>
    <v-flex xs4>
      <v-card>
        <v-card-title primary-title>
          <div>
            <div class="headline">{{ movie.name }}</div>
            <span class="grey--text">{{ movie.release_year }} ‧ {{ movie.genre }}</span>
          </div>
        </v-card-title>
        <h6 class="card-title" id="rate_movie" v-if="current_user" @click="rate">
          Rate this movie
        </h6>
        <v-card-text>
          {{ movie.description }}
        </v-card-text>
      </v-card>
    </v-flex>
  </v-layout>
</template>

我们已为所有组件添加了必要的标识符。现在,让我们为之前提到的场景添加 e2e 测试。

test/e2e/specs/test.js 中的内容替换为以下代码:

// For authoring Nightwatch tests, see
// http://nightwatchjs.org/guide#usage

module.exports = {
  'default e2e tests': function test(browser) {
    // automatically uses dev Server port from /config.index.js
    // default: http://localhost:8080
    // see nightwatch.conf.js
    const devServer = browser.globals.devServerURL;
    console.log(devServer)

    browser
 .url(devServer)
 .waitForElementVisible('#inspire', 9000)
 .assert.elementPresent('.list')
 .assert.elementPresent('.list .side_bar_link')
 .assert.elementPresent('.side_bar_link #home')
 .assert.elementPresent('.side_bar_link #contact')
 .click('#drawer')
 .pause(1000)
 .click('#contact')
 .pause(1000)
 .assert.elementPresent('#inspire .contact')
 .assert.containsText('#inspire .contact h1', 'this is contact')
 .pause(1000)
 .click('#login_btn')
 .pause(1000)
 .assert.elementCount('input', 2)
 .setValue('input#email', 'get.aneeta@gmail.com')
 .setValue('input#password', 'secret')
 .pause(1000)
 .click('#login')
 .pause(1000)
 .click('.swal-button--confirm')
 .pause(1000)
 .assert.containsText('#user_email', 'GET.ANEETA@GMAIL.COM')
 .click('#add_movie_link')
 .pause(2000)
 .assert.elementCount('input', 3)
 .assert.elementCount('textarea', 1)
 .setValue('input#name', 'Avengers: Infinity War')
 .setValue('textarea#description', 'Iron Man, Thor, the Hulk and the rest of the Avengers unite 
      to battle their most powerful enemy yet -- the evil Thanos. On a mission to collect all six 
      Infinity Stones, Thanos plans to use the artifacts to inflict his twisted will on reality.')
 .click('.input-group__selections')
 .pause(1000)
 .click('.list a ')
 .setValue('input#genre', 'Fantasy/Science fiction film')
 .click('#add_movie_btn')
 .pause(1000)
 .click('.swal-button--confirm')
 .pause(1000)
 .click('.headline:nth-child(1)')
 .pause(1000)
 .assert.containsText('#rate_movie', 'Rate this movie')
 .click('#rate_movie')
 .pause(1000)
 .click('.vue-star-rating span:nth-child(3)')
 .pause(1000)
 .click('.swal-button--confirm')
 .pause(1000)
 .click('.swal-button--confirm')
 .pause(1000)
 .click('#logout_btn')
 .end();
  },
};

要运行 e2e 脚本,请确保我们在 package.json 中设置了正确的命令:

...
"scripts": {
    "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
    "start": "nodemon server.js",
    "unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run",
    "e2e": "node test/e2e/runner.js",
    "test": "npm run unit && npm run e2e",
    "lint": "eslint --ext .js,.vue src test/unit test/e2e/specs",
    "build": "node build/build.js",
    "heroku-postbuild": "npm install --only=dev --no-shrinkwrap && npm run build"
  },
...

添加 e2e 脚本后,我们应该能够使用以下命令运行测试:

$ npm run e2e 

现在,所有的测试都应该通过,输出应该看起来像这样:

摘要

在本章中,你学习了如何编写单元测试,我们讨论了你可以用来编写测试的不同技术,例如 chaimochasinon。你还学习了如何编写控制器、模型和 Vue 组件的测试。

在下一章中,你将了解持续集成以及如何使用 GitHub 将你的应用部署到 Heroku。

第十章:上线

在上一章中,我们学习了如何为我们的应用程序编写 Node.js 和 Vue.js 组件的测试。我们学习了我们可以使用哪些技术来测试 MEVN 应用程序。

在本章中,我们将学习什么是持续集成CI),它如何让我们的生活更轻松,以及我们如何在 Heroku 上部署我们的应用程序。

持续集成

CI 是软件开发过程中的一个实践,其中团队中的每个成员都会对代码进行持续的小幅更改,并将它们集成回原始代码库。每次更改后,开发者都会将其推送到 GitHub,并在该更改上自动运行测试。这有助于检查更改后的代码中是否存在任何错误或问题。

考虑一个场景,多个开发者正在同一个应用程序上工作。每个开发者都在独立的分支上工作,开发不同的功能。他们构建功能并为他们构建的功能编写测试代码。一切都很顺利。然后当功能完成时,他们尝试集成所有功能,突然一切都崩溃了。测试也失败了,许多错误开始出现。

如果应用程序很小,那不会是很大的问题,因为错误可以很容易地修复。但如果是一个大型项目,那么仅仅找出出了什么问题就很困难,更不用说修复所有错误了。这就是 CI 的起源。

CI 是为了在集成软件时减轻此类风险而出现的。CI 的规则是尽早和经常集成,这有助于在向现有代码库添加新功能的过程中早期发现错误和问题。因此,我们不必等待每个组件的完成,CI 鼓励我们在代码库中提交的每个更改上构建代码库并运行测试套件。

CI 工作流程

下面是一个解释 CI 如何工作的图解:

在现实世界的场景中,多个开发者正在同一个应用程序上工作。他们各自在自己的机器上独立工作。当他们对代码库进行更改时,他们会将其推送到他们使用的版本控制系统的仓库中。

现在,这个更改触发了我们集成到应用程序中的 CI 过程,自动运行测试套件并对我们更改的代码进行质量检查。

如果测试套件通过,则进入进一步测试完整应用程序的流程,并交给质量保证团队。

但是,如果测试失败,那么正在该应用程序上工作的开发者或整个团队都会收到通知。然后,负责该更改的开发者会进行必要的更改以修复错误,提交更改,并将修复后的代码更改推送到仓库。然后,重复同样的过程,直到测试通过。因此,如果有任何错误,它们会在早期被发现并修复。

CI 的好处

现在我们已经了解了 CI 是什么以及为什么我们应该使用它,让我们来看看它带来的好处:

  • 自动构建和测试应用程序:虽然预期开发者在将更改后的代码推送到仓库之前会构建应用程序并运行测试,但有时开发者可能会忘记。在这种情况下,集成持续集成过程有助于使流程自动化。

  • 增强部署信心:由于 CI 会检查测试套件,并且我们可以配置它来检查代码库中代码的质量,因此我们不必担心在将代码推送到 GitHub 之前忘记运行测试。

  • 易于配置:CI 非常容易配置。我们只需要创建一个包含所有配置的单个文件。

  • 错误报告:这是 CI 的强大功能之一。当构建或运行测试时出现问题时,团队会收到通知。它还可以提供有关谁做了什么更改的信息,这非常棒。

Travis CI 简介

现在我们已经了解了 CI,我们需要开始在应用程序中使用它。有许多技术可以用于跟踪任何应用程序的 CI 流程。有很多工具,每个都有其使用的优点;我们将为我们的应用程序选择的是 Travis CI

Travis CI 是一种用于构建 CI 服务器的技术。Travis CI 与 GitHub 结合使用得非常广泛。还有一些其他工具。其中一些是:

  • Circle CI

  • Jenkins

  • Semaphore CI

  • Drone

如果你想了解更多关于每个选项的信息,这里有一些好的阅读材料:

blog.github.com/2017-11-07-github-welcomes-all-ci-tools/

Travis CI 用于为 GitHub 上的每次推送构建,并且设置起来非常简单。

在应用程序中设置 Travis

让我们继续到设置部分。这里要做的第一件事是查看 Travis CI 的官方网站 travis-ci.org/

激活仓库

我们需要先注册,这可以通过使用“GitHub 登录”轻松完成。完成之后,你应该能看到你现有的仓库列表。选择你想要设置 Travis CI 的应用程序,你将能够看到以下页面:

指定 Node.js 版本

现在,激活你想要添加 Travis CI 的仓库。我们可以在我们的个人资料中看到我们的仓库列表。选择应用程序并点击勾选标记以在仓库中激活 Travis CI。接下来,需要添加配置细节。首先,需要指定我们将为应用程序使用的 node 版本。

在应用程序的根目录中创建 .travis.yml 文件:

// travis.yml
language: node_js
node_js:
 - "10.0.0"

现在,这个代码块表明这是一个 Node.js 项目,并且该项目的 Node.js 版本是 10.0.0。你必须指定应用程序中安装的 Node.js 版本。你可以使用以下命令检查版本:

$ node -v 

你也可以在 .travis.yml 文件中指定相同的版本。

如果指定的版本不是 Node.js 的标准或可用版本,则会引发错误。

我们还可以在名为 .nvmrc 的文件中指定我们想要用于构建项目的 Node.js 版本。如果 .travis.yml 文件中没有指定版本,travis.yml 文件会读取此文件的内容。

构建脚本

现在的下一步是告诉 Travis 运行测试套件。这部分在 .travis.yml 文件中的 script 键中指定。Node.js 项目的默认构建脚本为 npm test。但让我们先从添加一个运行单个文件的命令开始,这样会更快。使用以下内容更新 .travis.yml 文件的内容:

language: node_js
node_js:
  - "10.0.0"
script: npm run unit

这告诉 script 在对仓库进行任何更改时运行单元测试。

管理依赖项

下一步是安装依赖项。默认情况下,Travis CI 不会添加任何依赖项。以下命令告诉 Travis CI 在构建 script 之前下载依赖项。它使用 npm 安装依赖项,所以让我们添加一个 script 来安装这些依赖项:

language: node_js
node_js:
  - "10.0.0"
before_script:
 - npm install
script: npm run unit

就这样。我们已经成功为我们的应用程序配置了 Travis CI。

现在,让我们提交并把这个文件推送到 GitHub。当你这样做时,检查 travis.org 上的分支以查看所有构建:

图片

在这里,master 分支是我们添加了 Travis CI 构建并成功通过构建的分支。你可以通过点击构建来查看 master 分支的详细信息。

虽然查看构建是一个好的方法,但最好的方法是为每个分支创建一个 pull request,并查看该 pull request 中的构建是否通过或失败。所以,让我们创建一个新的 pull request 来看看我们如何最好地使用 Travis CI 来简化我们的生活。

让我们使用以下命令创建一个名为 setup_travis 的分支(你可以将分支命名为任何名称,但确保它表明了一个特定的更改,这样更容易识别该分支可以期待哪些更改):

$ git checkout -b setup_travis 

让我们对应用程序进行一些简单的修改,以便我们的 pull request 包含一些差异。

使用以下内容更新 README.md 文件:

# movie_rating_app

> A Vue.js project

## Build Setup

``` bash

# 安装依赖项

npm install

# 在 localhost:8080 上提供带有热重载的服务

npm run dev

# 为生产构建并启用压缩

npm run build

# 为生产构建并查看包分析报告

npm run build --report

# 运行单元测试

npm run unit

# 运行端到端测试

npm run e2e

# 运行所有测试

npm test

```js

然后,使用以下命令对更改进行 commit

$ git add README.md
$ git commit -m 'Update readme'

最后,使用以下命令将更改推送到 GitHub:

$ git push origin setup_travis

现在,如果我们访问这个应用程序的 GitHub 仓库页面,我们应该能看到以下内容:

点击“比较与拉取请求”按钮。然后添加必要的描述并点击“创建拉取请求”按钮。

一旦您创建拉取请求,Travis CI 将开始构建应用程序,并且随着您继续添加更多提交并将更改推送到,Travis CI 将为每个提交构建应用程序。

在我们将任何更改推送到 GitHub 之前运行测试是一个好习惯,Travis CI 构建可以帮助通过为每个提交构建应用程序来通知我们是否有任何东西出错。

我们还可以添加设置,以便在构建失败或成功时通过电子邮件或其他机制通知我们。默认情况下,Travis CI 将通过电子邮件通知我们,如下面的截图所示:

你可以看到这里 Travis CI 已经成功集成,并且测试也通过了:

当我们点击“详情”时,我们可以看到构建的详细日志:

一旦我们对这些更改有信心,我们就可以将拉取请求合并到主分支:

Heroku 简介

开发应用程序的最后也是最重要的部分是将它部署。Heroku 是一个云平台即服务。这是一个我们可以托管我们应用程序的云平台。Heroku 是部署和管理我们应用程序的一种简单而优雅的方式。

使用 Heroku,我们可以部署用 Node.js 编写的应用程序,以及许多其他编程语言,如 Ruby、Java 和 Python。无论编程语言是什么,Heroku 应用程序的设置对所有语言都是相同的。

使用 Heroku 部署我们的应用程序有几种方式,例如使用 Git、GitHub、Dropbox 或通过 API。在本章中,我们将专注于使用 Heroku 客户端部署我们的应用程序。

设置 Heroku 账户

要开始在 Heroku 上部署应用程序,我们首先需要创建一个账户。您可以直接从 www.heroku.com/ 创建您的账户。如果您想了解更多关于不同类型应用程序的信息,您可以查看官方文档,网址为 devcenter.heroku.com/

一旦您创建账户,您应该能看到自己的仪表板:

创建一个 Node.js 应用

Heroku 为我们将要构建的应用程序提供了很多选项。它支持 Node.js、Ruby、Java、PHP、Python、Go、Scala 和 Clojure。让我们继续从仪表板中选择 Node.js。

本文档本身将指导你在遵循每个步骤时。让我们继续并在 Heroku 上部署我们自己的应用程序。

安装 Heroku

首要的事情是安装 Heroku。

在 Windows 上安装 Heroku

我们可以通过从官方页面下载安装程序,devcenter.heroku.com/articles/heroku-cli#download-and-install,并运行安装程序来在 Windows 上简单地安装 Heroku。

在 Linux 上安装 Heroku

Heroku 可以通过单个命令在 Linux 上安装:

$ wget -qO- https://cli-assets.heroku.com/install-ubuntu.sh | sh

在 macOS X 上安装 Heroku

我们可以使用 homebrew 在 macOS 上安装 Heroku:

$ brew install heroku/brew/heroku

我们可以使用以下命令检查 Heroku 是否已安装:

$ heroku -v

这应该会打印出我们刚刚安装的 Heroku 版本。

部署到 Heroku

一旦安装了 Heroku,让我们转到 https://dashboard.heroku.com/apps,在那里我们将为我们的项目创建一个 Heroku 应用程序。点击创建新应用按钮,输入您想要提供给应用程序的应用程序名称。我们将为我们的应用程序命名为 movie-rating-app-1

这将创建一个 Heroku 应用程序。现在,让我们切换到终端中的我们的应用程序并运行以下命令:

$ cd movie_rating_app
$ heroku login

此命令将提示您输入您的电子邮件和密码:

现在,如果您已经在您的应用中初始化了 Git 仓库,您可以在以下代码片段中跳过 git init 部分:

$ git init
$ heroku git:remote -a movie-rating-app-1

此命令将把我们应用链接到我们刚刚创建的 Heroku 应用程序。

设置部分已完成。现在,我们可以继续在我们的应用程序中进行一些更改。像之前一样提交到 GitHub 仓库,并推送更改。

现在部署到 Heroku 应用的简单命令是运行以下命令:

$ git push heroku master

在这里,我们需要注意几件事情。

由于我们通过在 server.js 中使用 serve-static 包将 Vue.js 组件转换为静态文件来提供 Vue.js 组件,因此我们需要更新 package.json 中的启动脚本以运行 node 服务器。让我们在 package.json 中更新启动脚本,添加以下行:

"scripts": {
    "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
    "start": "nodemon server.js",
    "unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run",
    "e2e": "node test/e2e/runner.js",
    "test": "npm run unit && npm run e2e",
    "lint": "eslint --ext .js,.vue src test/unit test/e2e/specs",
    "build": "node build/build.js",
    "heroku-postbuild": "npm install --only=dev --no-shrinkwrap && npm run build"
  },

此外,在 config/Config.js 文件中,我们有以下内容:

module.exports = {
  DB: 'mongodb://localhost/movie_rating_app',
  SECRET: 'movieratingappsecretkey',
  FACEBOOK_APP_ID: <facebook_client_id>,
  FACEBOOK_APP_SECRET: <facebook_client_secret>,
  TWITTER_APP_ID: <twitter_consumer_id>,
  TWITTER_APP_SECRET: <twitter_consumer_secret>,
  GOOGLE_APP_ID: <google_consumer_id>,
  GOOGLE_APP_SECRET: <google_consumer_secret>,
  LINKEDIN_APP_ID: <linkedin_consumer_id>,
  LINKEDIN_APP_SECRET: <linkedin_consumer_secret>
}

在这里,我们指定了本地 MongoDB URL,当我们在 Heroku 上托管我们的应用程序时将不起作用。为此,我们可以使用一个名为 mLab 的工具。mLab 是 MongoDB 的数据库即服务工具。mLab 允许我们为沙盒数据库创建任意数量的数据库。

让我们继续创建一个 mlab.com/ 上的账户。登录后,点击创建新按钮以创建一个新的数据库:

我们可以选择我们想要的任何云服务提供商。选择计划类型为沙盒,然后点击 CONTINUE。选择任何区域,然后点击 CONTINUE 并添加您想要的应用程序数据库名称。最后,点击提交订单:

现在,如果我们点击数据库名称,我们可以看到 mLab 提供的 MongoDB URL 的链接。我们还需要创建一个数据库用户,以便能够对数据库进行认证。

前往用户选项卡,点击添加数据库用户,提供用户名和密码,然后点击创建。

我们应该能够在数据库配置页面中看到 MongoDB URL:

让我们更新我们的config/Config.js中的 MongoDB URL:

module.exports = {
  mongodb://<dbuser>:<dbpassword>@ds251849.mlab.com:51849/movie_rating_app
  SECRET: 'movieratingappsecretkey',
  FACEBOOK_APP_ID: <facebook_client_id>,
  FACEBOOK_APP_SECRET: <facebook_client_secret>,
  TWITTER_APP_ID: <twitter_consumer_id>,
  TWITTER_APP_SECRET: <twitter_consumer_secret>,
  GOOGLE_APP_ID: <google_consumer_id>,
  GOOGLE_APP_SECRET: <google_consumer_secret>,
  LINKEDIN_APP_ID: <linkedin_consumer_id>,
  LINKEDIN_APP_SECRET: <linkedin_consumer_secret>
}

我们需要更改的最后一件事是应用程序的端口。Heroku 应用程序在部署应用程序时会自动分配一个端口。我们只应使用开发环境的端口8081。因此,让我们验证我们的server.js是否具有以下代码:

const port = process.env.PORT || 8081;
app.use('/', router);
var server = app.listen(port, function() {
  console.log(`api running on port ${port}`);
});

module.exports = server

现在,让我们提交并推送更改到master分支,然后再次部署:

$ git add package.json config/Config.js server.js
$ git commit 'Update MongoDB url and app port'
$ git push origin master
$ git push heroku master

应用程序应该已成功部署到 Heroku,我们应该能够在movie-rating-app-1.herokuapp.com/查看我们的应用程序:

Heroku 错误日志

如果我们在 Heroku 部署时出现问题,我们还可以查看 Heroku 提供的以下命令的错误日志:

$ heroku logs -t

摘要

在本章中,我们学习了什么是 CI 以及如何使用它使应用程序的构建自动化。我们还学习了如何使用 Heroku 集成部署应用程序。总的来说,我们学习了如何使用 Vue.js 和 Node.js 技术构建全栈 Web 应用程序,我们集成了不同的认证机制,我们还学习了如何为应用程序编写测试以及如何部署应用程序。恭喜!

这只是您将要继续前进的旅程的开始。现在,您应该能够使用我们在这里学习到的所有技术构建从小到大的应用程序。

本书已为您提供使用 MEVN 堆栈以 JavaScript 作为唯一编程语言构建应用程序的技能。如果您计划构建自己的完整应用程序,这将是一个很好的开始。希望您喜欢阅读这本书,并且继续构建出色的应用程序!

posted @ 2025-10-25 10:35  绝不原创的飞龙  阅读(0)  评论(0)    收藏  举报