NodeJS-微服务开发-全-

NodeJS 微服务开发(全)

原文:zh.annas-archive.org/md5/4F011ED53DB2D88764152F518B13B69D

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

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

过去几年中 JavaScript 的崛起是如此迅猛,以至于它已成为开发现代 Web 应用程序的强大语言。

MEVN 是用于开发现代 Web 应用程序的堆栈之一,除了 MEAN 和 MERN。本书提供了使用 MEVN 技术逐步构建全栈 Web 应用程序的方法,其中包括 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 作为前端层

  • 在应用程序中添加身份验证层

  • 添加自动化脚本和测试

本书适合对象

本书旨在帮助对使用 Mongo DB、Express.js、Vue.js 和 Node.js 技术堆栈构建全栈应用程序感兴趣的 Web 开发人员学习。

本书适合具有 HTML、CSS 和 JavaScript 基本知识的初学者和中级开发人员。如果您是 Web 或全栈 JavaScript 开发人员,并且已经尝试过传统的堆栈,如 LAMP、MEAN 或 MERN,并希望探索具有现代 Web 技术的新堆栈,那么本书适合您。

本书涵盖的内容

第一章,“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. 在搜索框中输入书名,然后按照屏幕上的说明操作。

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

  • WinRAR/7-Zip 适用于 Windows

  • Zipeg/iZip/UnRarX 适用于 Mac

  • 7-Zip/PeaZip 适用于 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

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

警告或重要说明会出现在这样。

技巧和窍门会出现在这样。

第一章:介绍 MEVN

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

本书适用于有兴趣学习使用 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 仅在 Web 应用程序中用于添加一些常规 HTML 和 CSS 无法实现的视觉效果或动画。但今天,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 之外,数据库的选项还有 MySQL,PostgreSQL,Cassandra 等。

介绍 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是 Advanced Package Tool 的缩写,用于在 Debian 和 Linux 发行版上安装软件。基本上,这相当于 macOS 中的 Homebrew 命令。

在 Windows 上安装 Node.js

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

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

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

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

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

介绍 NVM

NVM 代表 Node Version Manager。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 使开发人员能够轻松地保持其代码更新,并重用许多其他开发人员共享的代码。

开发人员经常对包和模块这两个术语感到困惑。然而,这两者之间有明显的区别。

模块

模块是可以通过 require 命令由 Node.js 加载并具有命名空间的东西。一个模块有一个与之关联的 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:这是用于查找包的主要入口点。基本上,它是一个文件路径,因此当用户安装此包时,它知道从哪里开始查找模块。

  • scripts:这个字段包含可以在应用程序的各种状态下运行的命令。它是一个键值对。key 是应该运行命令的事件,value 是实际命令。

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

  • license:当提供许可字段时,用户可以轻松使用我们的软件包。这有助于确定在使用软件包时的权限和限制。

创建一个 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 的优势

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

要从 Homebrew 在 macOS 上安装 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)下载 Windows 的 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,这意味着关于我们发送的参数来更新标题为MEVN的记录,posts集合中有一个匹配查询的文档。

另一个关键称为modifiedCount,它给出了更新的文档数量。

删除文档

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

删除符合给定条件的文档

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

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

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

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

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

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

删除所有记录

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

> db.posts.remove({})

介绍 Git

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

要开始使用这个软件,我们需要先安装它。在每个操作系统上都有一种非常简单的安装方法。

在 Windows 上安装 Git

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

下载 Windows 的可执行安装程序文件,并按照逐步说明进行操作。

在 Mac 上安装 Git

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

$ brew install git 

在 Linux 上安装 Git

在 Linux 上安装 Git 就像在 macOS 上安装 Git 一样容易。只需输入以下命令并按 Enter 键在 Linux 上安装 Git:

$ sudo apt-get install git

介绍 GitHub

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

为什么要使用 GitHub?

GitHub 对软件工程师来说是救星。GitHub 提供了几个优势,使得使用它非常值得。GitHub 提供的一些好处列在这里:

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

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

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

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

  • 合作很容易:GitHub 跟踪每个合作者在项目上的每一个活动,并发送电子邮件通知。它还提供社交媒体功能,如动态、评论、表情符号和提及。

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

使用 GitHub

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

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

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

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

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

  • Push:推送将我们所做的提交发送回我们的存储库。

  • Pull:与推送相反,拉取是从远程存储库到我们的本地项目获取提交的过程。

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

  • Pull requests:创建pull request基本上是将我们对代码库所做的更改发送给其他开发人员进行批准。我们可以开始讨论一个pull request来检查代码的质量,并确保更改不会破坏任何东西。

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

设置 GitHub 存储库

现在我们知道了 GitHub 的基础知识,让我们开始为我们想要构建的项目创建一个 GitHub 存储库:

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

  2. 转到您的个人资料。在存储库选项卡下,单击新建,如下所示:

  1. 将此存储库命名为blog

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

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

当完成后,访问 GitHub 存储库,在那里您将找到对我们存储库所做的提交的历史,如下所示:

就是这样。现在,当我们想要进行更改时,我们将首先创建一个分支并将更改推送到该分支。

总结

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

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

第二章:构建 Express 应用程序

Express.js 是一个 Node.js Web 应用程序框架。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 应用程序的脚手架或模块化结构的工具之一。每个生成器工具可能都有自己的构建文件结构的方式,可以很容易地定制。

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

要开始,首先我们需要在深入 Express.js 之前更多地了解 Node.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++编写的开源项目。该引擎用于客户端和服务器端的 JavaScript Web 应用程序。

Node.js 编程

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

$ node

这将启动一个新的node进程。我们可以在这里编写普通的 JavaScript。

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

> var a = 1;

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

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

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

$ cd tutorial $ node tutorial.js

我们应该看到类似以下的东西:

这不会返回任何东西,因为我们还没有为tutorial.js编写任何内容。

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

console.log('Hello World');

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

$ node tutorial.js

我们将看到一个输出,上面写着Hello World。这就是我们在 Node.js 中执行文件的方式。

除了在 V8 引擎上运行并在 Web 浏览器中执行 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 的极简的服务器端 Web 框架。它是建立在 Node.js 之上的,以便轻松管理 Node.js 服务器。Express.js 最重要的优势是它使路由非常非常容易。它提供的强大 API 非常容易配置。它很容易接收来自前端的请求,也很容易连接到数据库。Express.js 也是 Node.js 最流行的 Web 框架。它使用模型视图控制器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 应用程序的主入口页面。Web 服务器代码驻留在这个文件中。

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

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

  • routes:这个文件夹包含了我们将要构建的访问应用程序不同页面的所有路由的完整列表。我们将在后续章节中更多地讨论这个问题。

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

所以,这些是我们需要知道的基本事情。现在,使用你喜欢的文本编辑器来处理应用程序,让我们开始吧。现在,如果我们查看package.json,会发现有一些包我们没有安装,但在依赖项中列出了:

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

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

  • debug:这是一个 JavaScript 实用程序包,可以对console.log返回的内容进行漂亮的格式化

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

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

  • jade:如前所述,这是 Node.js 的默认模板引擎。在使用express命令创建应用程序时,应该会看到一个警告,指出默认视图引擎在将来的版本中将不再是 jade。这是因为jade将被pug取代;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".

  1. 重命名views文件夹中的文件:
  • error.jade to error.pug

  • index.jade to index.pug

  • layout.jade to layout.pug

  1. 最后,在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参数以在页面上显示。现在,查看views文件夹中的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下。它们处理数据库。它们处理应用程序的所有逻辑部分。

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

  • **Controller: **Controllers基本上连接这些models和视图。它负责将在models中进行的逻辑计算传递到views部分:

在我们构建的应用程序中,不需要实现 MVC 平台。JavaScript 是一种模式不可知的语言,这意味着我们可以创建自己的文件夹结构。与其他编程语言不同,我们可以选择最适合我们的结构。

为什么要使用 MVC?

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

  • 清晰地分离业务逻辑和视图。这种分离允许我们在整个应用程序中重用业务逻辑。

  • 开发过程变得更快。这是显而易见的,因为各部分被清晰地分离出来。我们只需将视图添加到我们的视图文件夹中,并在models文件夹中添加逻辑。

  • 修改现有代码变得容易。当多个开发人员在同一个项目上工作时,这非常方便。任何人都可以从任何地方接手应用程序并开始对其进行更改。

改变文件夹结构以包含 MVC

现在我们已经了解了足够多关于 MVC 的知识,让我们修改我们创建的应用程序express_app的文件结构。首先,我们需要在根目录中创建这三个文件夹。已经有一个视图文件夹,所以我们可以跳过它。让我们继续创建modelscontrollers文件夹。

在我们的app.js中,我们需要包含我们的控制器文件。为了做到这一点,我们首先要引入一个叫做文件系统的新包。这个模块使得执行与文件相关的操作变得容易,比如读取/写入文件。

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

$ npm install file-system --save 

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

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

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 服务器:

$ node app.js

有了这个,当我们访问http://localhost:3000/users时,我们将能够看到以下内容:

我们已经成功设置了 MVC 架构的controllersviews部分。我们将在后续章节中更多地涵盖models部分。

在上一章中,我们谈到了 GitHub 以及如何使用它来通过进行小的提交来制作代码历史。不要忘记设置一个 repo 并持续将代码推送到 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' });
  })
}

这意味着控制器想要加载users文件,该文件位于views文件夹中。让我们继续在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' });

render 方法有两个参数:它想要加载的视图和要传递给该视图的变量。在这个例子中,Users被传递给了 title 变量。在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 数据一词,意思是它可以处理大量数据。MongoDB 是一种面向文档的数据库架构。它使我们能够更快地开发和扩展。在关系数据库设计中,我们通过创建表和行来存储数据,但是使用 MongoDB,我们可以将数据建模为 JSON 文档,这与关系数据库相比要简单得多。如果我们灵活并且需求经常变化,并且需要进行持续部署,那么 MongoDB 就是我们的选择。作为基于文档的数据模型,MongoDB 也非常灵活。

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

如果我们的业务增长迅速,我们需要更快地扩展,我们需要以更灵活的方式访问数据,如果我们需要对数据进行更改而不必担心更新应用程序的数据库模式,那么 MongoDB 是我们的最佳选择。在关系数据库管理系统中添加新列也会导致一些性能问题。但是,由于 MongoDB 是无模式的,添加新字段可以立即完成,而不会影响我们应用程序的性能。

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

以下是本章节将涵盖的内容的简要总结:

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

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

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

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

为什么选择 MongoDB?

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

  • 灵活的文档:MongoDB 集合包含多个文档。每个集合下的文档可以具有可变的字段名称,也可以具有不同的大小,这意味着我们不必定义模式。

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

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

MongoDB 查询

我们在第一章中快速回顾了 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" },
  ]
)

输出应该如下:

检索文档

在 MongoDB 中检索集合中的文档是使用find()命令完成的。有许多使用此命令的方法。

查找所有文档

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

> 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。输出为:

正如输出所说,modifiedCount1matchedCount1,这意味着找到并更新了具有给定条件的文档。

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 是本地托管的数据库 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 中更新文档是readcreate(save)方法的组合。要更新文档,我们首先需要使用 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" }定义。这将Petername设置为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,那么这将非常有用。例如,如果我们想要在前端显示一个消息,说用户{name}已被删除。然后我们可以传递useruser的其他属性;在这种情况下,它是要在前端显示的名字。

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

字符串

字符串本身有几个验证器,如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} is not a valid phone number

我们还可以在 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 服务的工作原理:

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

介绍 REST API

REST API 采用 RESTful 架构设计。根据 RESTful 架构原则构建的 API 称为 RESTful API。RESTful 架构也被称为无状态架构,因为客户端和服务器之间的连接不会被保留。在客户端和服务器之间的每次交易之后,连接都会被重置。

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

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

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

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 错误请求:当请求参数格式不正确,或者语法错误时,服务器会返回 400 状态代码。

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

  • 403 Forbidden:这与 401 相似。这检查执行 API 请求的一方的授权。当执行 API 的不同用户有不同的权限设置时,就会执行这个操作。

  • 404 未找到:当服务器在数据库中找不到我们要执行某些操作的资源时,返回此状态。

5XX 代码

500 范围的状态代码告诉我们,在给定资源中执行的操作出现了问题:

  • 500 内部服务器错误:当操作未成功执行时,显示此状态代码。与 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')
})

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

在我们的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是用户的 Mongo 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 中创建一个新的查询,具有以下参数。我们将创建一个GET请求,URL 为http://localhost:3000/users/:user_id,其中user_id是您在数据库中创建的任何用户的id。通过这个设置,我们应该能够看到类似这样的东西:

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

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

让我们看一个例子。让我们创建一个 API,该 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参数。但是由于在定义 User 模型时我们没有添加age到我们的 Schema 中,它会丢弃 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 动词和状态码,以及如何开发 RESTful API 并使用 Postman 进行测试。

在下一章中,我们将进入 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 应用程序:runtime + compiler,或者仅运行时。这与模板编译器有关:

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

  • Runtime + compiler:此选项包括模板编译器,这意味着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/#/时,应该能看到类似于这样的东西:

就是这样。您已成功创建了一个基本的静态两页 Web 应用程序!

理解 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>

脚本标记内的代码是一个数据变量,它将其中定义的数据绑定到此组件。当您更改该变量 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>

第一个屏幕将如下所示:

当您单击 REPLY 时,您将看到以下内容:

这些是我们将在应用程序中主要使用的指令。还有很多其他指令,我们将在途中探索。如果您想了解更多关于这些的信息,您可以访问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.vuescript标签内:

<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-form v-model="valid" ref="form" lazy-validation> 

这里的v-model="valid"部分的作用是,它确保表单在为 true 之前不会被提交,这再次与我们在底部添加的脚本相关联。此外,让我们看看我们已经添加到表单中的验证。

第一个基本验证是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)时,您应该有一个如下的表单:

电影名称中的*表示它是一个必填字段。

如果您注意到,我们一直在添加#到我们添加的所有路由。这是因为这是 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

添加一个 Movie 模型

接下来要做的是在提交表单时将电影添加到数据库。让我们继续在根目录中创建一个名为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);
    });
  });
};

现在,如果您访问 URLhttp://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 并在主页上显示 DB 记录。

添加电影详情页面

现在,我们需要一个页面,用户可以在该页面上对电影进行评分。为此,让我们在主页上电影的标题旁添加一个链接。在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 this Movie”时调用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 TokenJWT)策略

  • 了解更多关于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.jsUser模型:

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模型中的上述方法createUser接受user对象,将用户提供的密码转换为加密密码,然后保存到数据库中。

添加 API 端点以注册用户

现在我们的模型已经准备好了,让我们继续创建一个端点来创建用户。为此,让我们首先在controllers文件夹中创建一个名为users.js的控制器,以管理所有与用户相关的请求。由于我们已经添加了一个代码块来初始化server.jscontrollers目录中的所有文件,所以我们不需要在这里要求这些文件。

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发出 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

在注册表单中添加 submit 和 clear 方法

下一步是为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方法接受重要参数,如request方法(在前面的情况下是post)、数据参数或有效载荷,以及要命中的 URL 端点。它接受这些参数并将它们路由到then()方法或catch()方法,具体取决于服务器的响应。

如果请求成功,它进入then()方法;如果不成功,它进入catch()方法。现在,请求的成功和失败也可以根据我们的需求进行自定义。对于前面的情况,如果user未保存到数据库,我们将简单地传递错误响应。我们也可以对验证进行同样的操作。

因此,让我们还修改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 获取有效负载,对其进行签名,并生成一个随机令牌。

创建一个注册视图页面

现在让我们为用户添加一个登录视图页面。为此,就像我们在注册页面上所做的那样,我们需要创建一个接受电子邮件和密码参数的表单。创建一个名为Login.vue的文件,放在src/components中,如下所示:

<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 令牌保存在本地存储中。每当进行任何其他请求时,请求将首先检查它是否是有效令牌,然后相应地执行操作。

到目前为止,我们已经做了以下工作:

  • 向 Users 模型添加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.authenticate('jwt', { session: false })。我们必须传递 JWT 令牌,护照 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 

此外,现在我们只运行我们的节点服务器,应用程序应该在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"));
...

有了这些,我们现在应该能够直接访问所有路由。我们现在也可以重新加载页面。

由于我们正在构建我们的 Vue.js 组件并仅在 Node.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访问。

此外,我们也需要更新我们的 vue 文件,因为我们现在不需要 passport JWT 策略。

使用以下代码更新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目录中创建一个名为Config.js的文件,并添加以下代码行:

module.exports = {
 DB: 'mongodb://localhost/movie_rating_app',
 SECRET: 'movieratingappsecretkey'
}

我们还添加了一个名为/api/current_userGET路由,用于获取当前登录用户的详细信息。此 api 使用一个名为isLoggedIn的中间件方法,用于检查用户的数据是否在会话中。如果用户的数据存在于会话中,则当前用户的详细信息将作为响应返回。

我们添加的另一个端点是/logout,它简单地注销用户并销毁会话。

因此,通过这个配置,现在我们应该能够成功使用passport.js本地策略登录。

我们现在唯一的问题是我们无法知道用户是否成功登录。为此,我们需要显示一些用户信息,比如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的方法,该方法在mounted方法中被App.vue监听。每当用户登录应用程序时,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 策略

在前一章中,我们讨论了护照-JWT 策略。我们讨论了如何利用 JWT 包来构建强大的用户入职流程。我们讨论了如何为用户实现注册和登录流程。在本章中,我们将深入以下部分:

  • passport.js Facebook 策略

  • passport.js Twitter 策略

  • passport.js Google 策略

  • passport.js LinkedIn 策略

如果我们从头开始做所有这些部分,每个部分都会消耗大量时间。passport.js提供了一种更简单的方式来以非常灵活的方式集成所有这些策略,并使它们更容易实现。

OAuth是一种认证协议,它允许用户通过不同的外部服务登录。例如,通过 Facebook 或 Twitter 登录应用程序不需要用户提供用户名和密码,如果用户已经登录到 Facebook 或 Twitter,则无需提供。这可以节省用户在应用程序中设置新帐户的时间,使登录流程更加顺畅。这使得登录应用程序变得更容易;否则,用户首先需要注册我们的应用程序,然后使用这些凭据登录。护照的 OAuth 策略允许用户通过单击登录到我们的应用程序,如果浏览器记住了该帐户,则其他所有操作都将自动完成并由策略本身处理。

护照的 Facebook 策略

护照的 Facebook 策略易于集成。一如既往,让我们从安装这个策略开始。

安装护照的 Facebook 策略

我们可以通过运行以下命令来安装护照的 Facebook 策略:

$ npm install passport-facebook --save

以下代码应该将包添加到您的package.json文件中:

...
"node-sass": "⁴.7.2",
"nodemon": "¹.14.10",
"passport": "⁰.4.0",
"passport-facebook": "².1.1",
...

配置护照的 Facebook 策略

配置护照的 Facebook 策略有几个步骤。我们将详细讨论每个步骤:

  1. 创建并设置一个 Facebook 应用。这将为我们提供一个“应用 ID”和一个“应用密钥”。

  2. 在我们的登录页面上添加一个按钮,允许用户通过 Facebook 登录。

  3. 为 Facebook 认证添加必要的路由。

  4. 添加一个中间件方法来检查认证是否成功。

让我们深入讨论前面每个步骤的细节。

创建并设置一个 Facebook 应用

要使用 Facebook 策略,您必须首先构建一个 Facebook 应用程序。Facebook 的开发者门户网站位于developers.facebook.com/

登录后,点击“开始”按钮,然后点击“下一步”。

然后,您将在屏幕右上角看到一个名为“我的应用程序”的下拉菜单,在那里您可以找到创建新应用程序的选项。

选择您想要为应用程序命名的显示名称。在这种情况下,我们将其命名为movie_rating_app

点击“创建应用 ID”。如果您转到设置页面,您将看到应用程序的应用 ID 和应用密钥:

您将需要前面截图中提到的值。

在我们的登录页面上添加一个按钮,允许用户通过 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 登录时,如果用户已经存在于我们的数据库中,用户将简单地登录并保存在会话中。会话数据不存储在浏览器 cookie 中,而是存储在服务器端本身。如果用户在我们的数据库中不存在,则我们将使用来自 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,稍后我们将不得不定义它:

成功创建应用程序后,您可以在“Keys and Access Tokens”选项卡中看到 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',
  },
...

现在,一切准备就绪,可以使用 LOGIN WITH TWITTER 按钮成功登录。

Passport 的 Google 策略

接下来的策略是 Passport 的 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",
...

配置 Passport 的 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 登录

下一步是在我们的登录页面中添加一个 LOGIN WITH GOOGLE 按钮,我们将把它链接到我们刚创建的 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>
...

上述代码将添加一个 LOGIN WITH GOOGLE 按钮:

添加 Google 应用程序的配置

让我们像为 Facebook 和 Twitter 策略一样配置 Google 策略。我们将创建一个单独的文件来处理 Google 登录,以使代码简单。让我们在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是我们刚创建的 Google 应用程序的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 是您希望在与 Google 成功交易后将您的应用程序路由到的 URL。

我们刚刚添加的回调是http://127.0.0.1:8081/login/google/return,我们必须定义它。配置后跟一个函数,该函数接受以下四个参数:

  • accessToken

  • refreshToken

  • profile

  • cb(回调)

在成功的请求之后,我们的应用程序将被重定向到我们尚未定义的profile页面。

为 Google 登录添加必要的路由

现在,让我们继续添加必要的路由,当我们点击登录按钮时以及当我们从 Google 收到回调时。在同一个文件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 秘密(客户端秘密)。

这些令牌将用于验证我们应用程序中的身份验证:

在我们的登录页面上添加一个按钮,允许用户通过 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 登录”按钮成功登录了。

摘要

在本章中,我们介绍了 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 而来的术语:

  • 状态:这是一个包含数据的对象。Vuex 使用单一状态树,这意味着它是一个包含应用程序所有数据片段的单一对象。

  • 获取器:用于从状态树中获取数据。

  • 变化:它们是改变状态树中数据的方法。

  • 操作:它们是执行变化的函数。

我们将在本章中讨论这些内容。

传统的多网页应用程序

在传统的多网页应用程序中,当我们构建一个 Web 应用程序并通过浏览器导航打开网站时,它会请求 Web 服务器获取该页面并提供给浏览器。当我们在同一网站上单击按钮时,它再次请求 Web 服务器获取另一个页面并再次提供。这个过程对我们在网站上进行的每一次交互都会发生。因此,基本上每次交互网站都会重新加载,这需要大量时间。

以下是一个解释多页面应用程序工作原理的示例图:

当从浏览器发送请求时,请求被发送到服务器。服务器然后返回 HTML 内容并提供一个全新的页面。

多页面应用程序MPA)也可以提供一些好处。选择 MPA 还是单页面应用程序SPA)并不是问题,而是完全取决于您的应用程序内容。如果您的应用程序包含大量用户交互,您应该选择 SPA;然而,如果您的应用程序唯一目的是为用户提供内容,您可以选择 MPA。我们将在本章后面更多地探讨 SPA 和 MPA。

单页面应用程序的介绍

与传统 MPA 相反,SPA 专门为基于 Web 的应用程序设计。当您首次在浏览器中加载网站时,SPA 会获取所有数据。一旦所有数据都被获取,您就不需要再获取任何数据。当进行任何其他交互时,该数据通过互联网获取,无需向服务器发送请求,也无需重新加载页面。这意味着 SPA 比传统 MPA 快得多。然而,由于 SPA 在第一次加载时一次性获取所有内容,因此第一页加载时间可能会很慢。一些具有 SPA 集成的应用程序包括 Gmail、Facebook、GitHub、Trello 等。SPA 的目标是通过将内容放在一个页面上,而不是让用户等待他们想要的信息,从而提高用户体验。

以下是单页面应用程序工作原理的示例图:

网站在第一次加载时就拥有了所有所需的内容。当用户点击某些内容时,它只会获取该特定区域的信息,并刷新网页的那部分。

SPA 与 MPA

SPA 和 MPA 有不同的用途。根据您的需求,您可能希望选择其中一种。在开始应用程序之前,请确保您清楚您想要构建的应用程序类型。

使用 MPA 的优点

如果您希望使应用程序对 SEO 友好,MPA 是最佳选择。Google 可以通过搜索您在每个页面上分配的关键字来抓取应用程序的不同页面,而在 SPA 中是不可能的,因为它只有一个页面。

使用 MPA 的缺点

使用 MPA 也有一些缺点:

  • 与 SPA 相比,MPA 的开发工作要大得多,因为前端和后端紧密耦合。

  • MPA 具有紧密耦合的前端和后端,这使得在前端和后端开发人员之间分离工作变得更加困难。

使用 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,并通过存储导出了一个 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组件中 count 的默认值。如果你导航到http://localhost:8080/#/,你应该看到以下截图:

在上面的截图中,我们直接使用$运算符在存储中访问了 count 变量,这不是首选的方法。我们已经学会了使用状态的基础知识。现在,访问变量的正确方法是使用getters

Getters

getter是用来访问存储中的对象的函数。让我们创建一个getter方法来获取存储中的 count。

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中创建的fetchCount方法作为getter方法。现在,通过重新加载浏览器来检查数字;这也应该打印出0的计数:

mutations

让我们继续讨论mutationsmutations是执行对存储状态进行修改的方法。我们将定义mutations,就像我们定义getters一样。

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方法将 count 增加 1,而decrement方法将 count 减少 1。这就是我们引入 actions 的地方。

动作

动作是调度 mutation 函数的方法。动作执行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,并在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 的基础知识——它在应用程序中的工作原理和核心概念。我们介绍了如何创建存储和突变,以及如何使用动作来调度它们,还讨论了如何使用 getter 从存储中获取信息。

在之前的章节中,我们为电影列表页面构建了一个应用程序。我们将使用相同的应用程序来使用 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. 创建一个 getter 方法,从状态中获取电影以显示在主页上

修改 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以调用创建电影的action

  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>

然后,在store.js文件中添加actionmutations

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 的核心概念状态、获取器、突变、操作,以及如何在应用程序中使用它们。我们讨论了如何构建我们的应用程序来实现 Vuex,并且在应用程序变得更大时它所带来的好处。

在下一章中,我们将介绍如何为 Vue.js 和 Node.js 应用程序编写单元测试和集成测试。

第九章:测试 MEVN 应用程序

让我们快速回顾一下我们在之前章节中所做的工作:

  • 我们为不同的页面创建了不同的 Vue 组件

  • 我们实现了 Vuex——用于 Vue.js 应用程序的集中状态管理,并为组件定义了状态、获取器、变化和操作

  • 我们创建了控制器和模型来与 Node.js 后端交互

在本章中,我们将讨论如何编写测试代码,以确保应用程序中的一切都能正常工作。编写测试代码是任何应用程序的重要组成部分。它有助于确保我们编写的功能不会出错,并保持代码的质量。

编写测试时可以遵循不同的实践。在编写实际代码之前,首先编写测试代码总是一个很好的实践。编写测试可以确保我们的应用不会出错,并且一切都会按预期工作。

这有助于我们编写更好的代码,也有助于在问题出现之前揭示潜在问题。

编写测试的好处

在开发应用程序时编写测试代码有很多好处。其中一些如下:

  • 确保代码按预期工作:它有助于确保我们在应用程序中编写的每个功能都能按预期工作。

  • 提高代码质量:它提高了代码的质量。由于编写测试代码有助于在编写实际代码之前预防可能出现的缺陷,因此它提高了代码的质量。

  • 提前识别错误:它有助于在早期阶段识别错误。由于为每个功能编写了测试代码,因此可以在早期识别出错误和问题。

  • 为新开发人员提供文档:测试代码就像文档。如果我们需要新的开发人员开始在同一个应用程序上工作,测试代码可以帮助他们理解应用程序的工作方式,而不必查看所有应用程序代码。

  • 使用测试代码加快应用程序开发速度:如果我们不编写测试代码,编写代码会更快。然而,如果跳过这个过程,后来我们将花费大部分时间来修复可能已经出现的错误,而这些错误本可以在测试代码中提前识别出来。

  • 应用程序不需要运行:编写测试代码并运行它不需要应用程序运行。它也不需要构建应用程序。这显著减少了开发时间。

因此,在本章中,我们将讨论以下主题:

  • 了解为什么以及如何编写单元测试和端到端测试

  • 了解为 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。现在规范应该通过了:

现在,让我们为 sum 方法添加功能部分。在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);
    })
  })
});

我们已经用chaiexpect()方法替换了assertion库,这使得代码变得更简单和易懂。

介绍 sinon

sinon用于测试 JavaScript 测试的间谍、存根和模拟。要了解这些,让我们继续进行我们在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提供的三种主要方法是间谍、存根和模拟:

  • 间谍:间谍有助于创建虚假函数。我们可以使用间谍来跟踪函数是否被执行。

  • 存根:存根帮助我们使函数返回我们想要的任何内容。当我们想要测试给定函数的不同场景时,这是很有用的。

  • 模拟:模拟用于伪造网络连接。它们有助于创建一个虚拟的类实例,这有助于设置预定的期望。

让我们为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 请求做了以下操作:

  • 我们正在发送带有电影参数的 POST 请求:namedescriptionrelease_yeargenre

  • 我们有三个期望:

  • 请求的状态应该是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元素,并且在其中应该有一个h1标签,其中应该包含this is contact内容。

现在,为了确保我们的测试运行,让我们验证我们在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 

编写端到端测试

使用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. 检查是否存在具有#inspireID 的元素。我们在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

  • 信号量 CI

  • 无人机

如果您想了解每个选项的更多信息,可以阅读以下内容:

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分支的详细信息。

虽然这是查看构建的一个好方法,但最好的方法是为每个分支创建一个拉取请求,并在该拉取请求本身中查看构建是否通过或失败。因此,让我们创建一个新的拉取请求,以查看如何最好地利用 Travis CI 来使我们的生活更轻松。

让我们创建一个名为setup_travis的分支(您可以为分支命名任何名称,但请确保它指示特定更改,以便更容易识别该分支可以期望的更改)使用以下命令:

$ git checkout -b setup_travis 

让我们对应用程序进行简单更改,以便我们的拉取请求包含一些差异。

使用以下内容更新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

我们可以通过从官方页面下载安装程序并运行安装程序来简单地在 Windows 中安装 Heroku,网址为devcenter.heroku.com/articles/heroku-cli#download-and-install

在 Linux 中安装 Heroku

在 Linux 中,可以通过一个命令安装 Heroku:

$ 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

这里有几件事情需要注意。

由于我们正在使用serve-static包将 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/上创建一个帐户。一旦您登录,单击“创建新”按钮创建一个新数据库:

我们可以选择任何我们想要的云提供商。选择沙箱计划类型,然后单击“继续”。选择任何地区,然后单击“继续”,并添加您想要为应用程序使用的数据库名称。最后,单击“提交订单”:

现在,如果我们单击数据库名称,我们可以看到 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 应用程序,集成了不同的身份验证机制,还学会了如何为应用程序编写测试并进行部署。恭喜!

这只是你将要继续前行的旅程的开始。现在你应该能够使用我们在这里学到的所有技术来制作小到大规模的应用程序。

这本书为你提供了使用 JavaScript 作为唯一编程语言,使用 MEVN 堆栈构建应用程序的技能。如果你打算构建自己的完整应用程序,这可能是一个很好的开始。希望你喜欢阅读这本书,并继续构建令人敬畏的应用程序!

posted @ 2024-05-23 15:59  绝不原创的飞龙  阅读(43)  评论(0编辑  收藏  举报