Angular6-和-Laravel5--Web-全栈开发实用指南(全)

Angular6 和 Laravel5 Web 全栈开发实用指南(全)

原文:zh.annas-archive.org/md5/b37ef01c0005efc4aa3cccbea6646556

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

自其诞生以来,Web 开发已经走过了很长的路。今天,我们希望的是快速、强大和引人入胜的 Web 应用程序,而渐进式 Web 应用程序(PWA)是前进的道路。在这本书中,我们将利用 Angular 和 Laravel 这两个最流行的框架来构建强大的 Web 应用程序。

Angular 是用于创建现代快速 PWA 的最流行的前端 JavaScript 框架之一。除了非常多才多艺和完整之外,Angular 还包括用于生成模块、组件、服务和许多其他实用工具的 Angular CLI 工具。另一方面,我们有 Laravel 框架,这是用于开发 Web 应用程序的强大工具,探讨了约定优于配置的范式的使用。

这本书将为您提供从头开始使用 Angular 和 Laravel RESTful 后端构建现代全栈 Web 应用程序的实际知识。它将带您了解使用这两个框架开发的最重要的技术方面,并演示如何将这些技能付诸实践。

这本书是为谁准备的

这本书适用于初学 Angular 和 Laravel 的开发人员。需要了解 HTML、CSS 和 JavaScript 和 PHP 等脚本语言的知识。

本书的内容涵盖了软件工程生命周期的所有阶段,涵盖了现代工具和技术,包括但不限于 RESTful API、基于令牌的身份验证、数据库配置以及 Docker 容器和镜像。

本书涵盖了什么

第一章,理解 Laravel 5 的核心概念,介绍了 Laravel 框架作为开发 Web 应用程序的强大工具,并探讨了约定优于配置的范式的使用。我们将看到,Laravel 默认情况下具有构建现代 Web 应用程序所需的所有功能,包括基于令牌的身份验证、路由、资源等。此外,我们将了解为什么 Laravel 框架是当今最流行的 PHP 框架之一。我们将学习如何设置环境,了解 Laravel 应用程序的生命周期,并学习如何使用 Artisan CLI。

第二章,TypeScript 的好处,探讨了 TypeScript 如何使您能够编写一致的 JavaScript 代码。我们将研究它包括的功能,例如静态类型和其他在面向对象语言中非常常见的功能。此外,我们将研究如何使用最新版本的 ECMAScript 的新功能,并了解 TypeScript 如何帮助我们编写干净和组织良好的代码。在本章中,我们将看到 TypeScript 相对于传统 JavaScript 的好处,了解如何使用静态类型,并理解如何使用接口、类和泛型,以及导入和导出类。

第三章,理解 Angular 6 的核心概念,深入探讨了 Angular,这是用于开发前端 Web 应用程序的最流行的框架之一。除了非常多才多艺和完整之外,Angular 还包括用于生成模块、组件、服务和许多其他实用工具的 Angular CLI 工具。在本章中,我们将学习如何使用新版本的 Angular CLI,理解 Angular 的核心概念,并掌握组件的生命周期。

第四章,“构建基线后端应用程序”,是我们开始构建示例应用程序的地方。在本章中,我们将使用 RESTful 架构创建一个 Laravel 应用程序。我们将更仔细地研究一些在第一章中简要提到的要点,例如使用 Docker 容器来配置我们的环境,以及如何保持我们的数据库填充。我们甚至将查看如何使用 MySQL Docker 容器,如何使用迁移和数据库种子,以及如何使用 Swagger UI 创建一致的文档。

第五章,“使用 Laravel 创建 RESTful API - 第 1 部分”,将介绍 RESTful API。您将学习如何使用 Laravel 框架的核心元素构建 RESTful API - 控制器,路由和 eloquent 对象关系映射(ORM)。我们还展示了我们正在构建的应用程序的一些基本线框。此外,我们将更仔细地研究一些您需要熟悉的关系,例如一对一,一对多和多对多。

第六章,“使用 Laravel 创建 RESTful API - 第 2 部分”,继续我们构建示例 API 的项目,尽管在那时,我们在 Laravel 中仍有很长的路要走。我们将学习如何使用一些在 Web 应用程序中非常常见的功能,例如基于令牌的身份验证,请求验证和自定义错误消息;我们还将看到如何使用 Laravel 资源。此外,我们将看到如何使用 Swagger 文档来测试我们的 API。

第七章,“使用 Angular CLI 创建渐进式 Web 应用程序”,涵盖了自上一个 Angular 版本以来影响 angular-cli.json 的变化。angular-cli.json 文件现在改进了对多个应用程序的支持。我们将看到如何使用ng add命令创建 PWA,以及如何组织我们的项目结构,以留下一个可扩展项目的单一基础。此外,我们将看到如何使用 Angular CLI 创建 service-work 和清单文件。

第八章,“处理 Angular 路由器和组件”,是单页应用程序(SPA)中最重要的部分之一,即路由的使用。幸运的是,Angular 框架提供了一个强大的工具来处理应用程序路由:@angular/router 依赖项。在本章中,我们将学习如何使用其中一些功能,例如路由器出口和子视图,以及如何创建主细节页面。此外,我们将开始创建前端视图。

第九章,“创建服务和用户身份验证”,我们将创建许多新东西,并进行一些重构以记忆重要细节。这是以常规和渐进的方式学习新知识的好方法。此外,我们将深入研究 Angular 框架的 HTTP 模块的操作和使用,现在称为 httpClient。此外,我们将研究拦截器,处理错误,使用授权标头以及如何使用 route guards来保护应用程序路由。

第十章,“使用 Bootstrap 4 和 NgBootstrap 创建前端视图”,解释了如何使用 Angular CLI 的新ng add命令在运行中的 Angular 应用程序中包含 Bootstrap CSS 框架和 NgBootstrap 组件。此外,我们将看到如何将我们的 Angular 服务与组件连接起来,以及如何使用后端 API 将它们整合在一起。我们将学习在后端 API 上配置 CORS,以及如何在我们的 Angular 客户端应用程序中使用它。我们还将学习处理 Angular 管道,模板驱动表单,模型驱动表单和表单验证。

第十一章,构建和部署 Angular 测试,介绍了如何安装、自定义和扩展 Bootstrap CSS 框架,以及如何使用 NgBootstrap 组件以及如何将 Angular 服务与组件和 UI 界面连接。我们将学习编写 Angular 单元测试,配置应用程序的 linter(用于 SCSS 和 Tslint)以保持代码一致性,创建 NPM 脚本,以及创建 Docker 镜像并部署应用程序。

充分利用本书

一些命令行、Docker 和 MySQL 的知识将非常有帮助;但是,这并非完全必需,因为所有命令和示例都附有简要说明。

您需要在您的计算机上安装以下工具:

  • Node.js 和 NPM

  • Docker

  • 代码编辑器——我们建议您使用 Visual Studio Code

  • 推荐使用 Git 源代码控制,但不是必需的

下载示例代码文件

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

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

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

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

  3. 点击“代码下载和勘误表”。

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

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

  • Windows 上的 WinRAR/7-Zip

  • Mac 上的 Zipeg/iZip/UnRarX

  • Linux 上的 7-Zip/PeaZip

本书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Hands-On-Full-Stack-Web-Development-with-Angular-6-and-Laravel-5。如果代码有更新,将在现有的 GitHub 存储库中进行更新。

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

下载彩色图像

我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。您可以在这里下载它www.packtpub.com/sites/default/files/downloads/HandsOnFullStackWebDevelopmentwithAngular6andLaravel5_ColorImages.pdf

使用的约定

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

CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。这里

这是一个例子:“所有使用 Composer 的 PHP 项目在根项目中都有一个名为composer.json的文件。”

代码块设置如下:

{
 "require": {
     "laravel/framework": "5.*.*",
 }
}

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

composer create-project --prefer-dist laravel/laravel chapter-01

粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词会以这样的形式出现在文本中。这是一个例子:“

“搜索chapter-01文件夹,然后点击打开。”

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

第一章:理解 Laravel 5 的核心概念

正如本章的标题所暗示的,我们将提供 Laravel 框架的概述,涵盖与使用 Web 服务架构开发 Web 应用程序相关的主要概念。更确切地说,我们将在本书中使用 RESTful 架构。

我们假设您已经对 RESTful 架构以及 Web 服务(这里我们称之为应用程序编程接口API)端点)的工作原理有基本了解。

但是,如果您对这个概念还很陌生,不用担心。我们将帮助您入门。

Laravel 框架将是一个有用的工具,因为使用它,我们控制器中的所有数据将默认转换为 JSON 格式。

Laravel 框架是开发 Web 应用程序的强大工具,使用“约定优于配置”的范式。 Laravel 开箱即用具有构建现代 Web 应用程序所需的所有功能,使用模型视图控制器MVC)。此外,Laravel 框架是当今最受欢迎的 PHP 框架之一,用于开发 Web 应用程序。

从现在到本书结束,我们将简称 Laravel 框架为 Laravel。

Laravel 生态系统绝对令人难以置信。诸如 Homestead、Valet、Lumen 和 Spark 之类的工具进一步丰富了使用 PHP 进行 Web 软件开发的体验。

有许多方法可以使用 Laravel 开始开发 Web 应用程序,这意味着有许多方法可以配置您的本地环境或生产服务器。本章不偏向任何特定方式;我们理解每个开发人员随着时间的推移都有自己的偏好。

无论您对工具、服务器、虚拟机、数据库等有何偏好,我们将专注于主要概念,并不假设某种方式是对还是错。本章仅用于说明主要概念和需要执行的操作。

请记住,无论您选择哪种方法(使用 Homestead、WAMP、MAMP 或 Docker),Laravel 都有一些极其必要的依赖项(或服务器要求),这对于开发 Web 应用程序非常重要。

您可以在官方 Laravel 文档中找到更多有用的信息:laravel.com/docs/5.6

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

  • 搭建环境

  • Laravel 应用程序的基本架构

  • Laravel 应用程序生命周期

  • Artisan CLI

  • MVC 和路由

  • 与数据库连接

搭建环境

请记住,无论您如何配置环境来使用 PHP 和 Laravel 开发 Web 应用程序,牢记主要的服务器要求,您将能够跟随本章的示例。

需要注意的是,某些操作系统没有安装 PHP。例如 Windows 机器,这里有一些替代方案供您创建开发环境:

安装 Composer 包管理器

Laravel 使用Composer,这是 PHP 的依赖管理器,与 Node.js 项目的Node Package Manager(NPM)、Python 的 PIP 和 Ruby 的 Bundler 非常相似。让我们看看官方文档对此的说法:

“Composer 是 PHP 中的依赖管理工具。它允许您声明项目所依赖的库,并将为您管理(安装/更新)它们。”

因此,让我们按照以下步骤安装 Composer:

转到getcomposer.org/download/并按照您的平台的说明进行操作。

您可以在getcomposer.org/doc/00-intro.md上获取更多信息。

请注意,您可以在本地或全局安装 Composer;现在不用担心。选择对您来说最容易的方式。

所有使用 Composer 的 PHP 项目在根项目中都有一个名为composer.json的文件,看起来类似于以下内容:

{
 "require": {
     "laravel/framework": "5.*.*",
 }
}

这也与 Node.js 和 Angular 应用程序上的package.json文件非常相似,我们将在本书后面看到。

这是关于基本命令的有用链接:getcomposer.org/doc/01-basic-usage.md

安装 Docker

我们将在本章中使用 Docker。尽管 Laravel 的官方文档建议使用带有虚拟机和 Vagrant 的 Homestead,但我们选择使用 Docker,因为它启动快速且易于使用,我们的主要重点是 Laravel 的核心概念。

您可以在www.docker.com/what-docker上找到有关 Docker 的更多信息。

根据 Docker 文档的说法:

Docker 是推动容器运动的公司,也是唯一一个能够应对混合云中的每个应用程序的容器平台提供商。今天的企业面临着数字转型的压力,但受到现有应用程序和基础设施的限制,同时需要合理化日益多样化的云、数据中心和应用程序架构组合。Docker 实现了应用程序和基础设施以及开发人员和 IT 运营之间的真正独立,释放了它们的潜力,并创造了更好的协作和创新模式。

让我们按照以下步骤安装 Docker:

  1. 转到docs.docker.com/install/

  2. 选择您的平台并按照安装步骤进行操作。

  3. 如果你遇到任何问题,请查看docs.docker.com/get-started/上的入门链接。

由于我们正在使用 Docker 容器和镜像来启动我们的应用程序,并且不会深入探讨 Docker 在幕后的工作原理,这里是一些 Docker 命令的简短列表:

命令 描述
docker ps 显示正在运行的容器
docker ps -a 显示所有容器
docker start 启动容器
docker stop 停止容器
docker-compose up -d 在后台启动容器
docker-compose stop 停止docker-compose.yml文件上的所有容器
docker-compose start 启动docker-compose.yml文件上的所有容器
docker-compose kill 杀死docker-compose.yml文件上的所有容器
docker-compose logs 记录docker-compose.yml文件上的所有容器

您可以在docs.docker.com/engine/reference/commandline/docker/上查看完整的 Docker 命令列表。以及在docs.docker.com/compose/reference/overview/#command-options-overview-and-help上查看 Docker-compose 命令。

配置 PHPDocker.io

PHPDocker.io 是一个简单的工具,它帮助我们使用 Compose 构建 PHP 应用程序的 Docker/容器概念。它非常易于理解和使用;因此,让我们看看我们需要做什么:

  1. 转到phpdocker.io/

  2. 单击生成器链接。

  3. 填写信息,如下截图所示。

  4. 单击“生成项目存档”按钮并保存文件夹:

PHPDocker 界面

数据库配置如下截图所示:

数据库配置请注意,我们在前面的配置中使用了 MYSQL 数据库的最新版本,但您可以选择任何您喜欢的版本。在接下来的示例中,数据库版本将不重要。

设置 PHPDocker 和 Laravel

既然我们已经填写了之前的信息并为我们的机器下载了文件,让我们开始设置我们的应用程序,以便更深入地了解 Laravel 应用程序的目录结构。

执行以下步骤:

  1. 打开bash/Terminal/cmd

  2. 在 Mac 和 Linux 上转到Users/yourname,或者在 Windows 上转到C:/

  3. 在文件夹内打开您的终端并输入以下命令:

composer create-project --prefer-dist laravel/laravel chapter-01

在您的终端窗口底部,您将看到以下结果:

Writing lock file Generating autoload files > Illuminate\Foundation\ComposerScripts::postUpdate > php artisan optimize Generating optimized class loader
php artisan key:generate
  1. 在终端窗口中,输入:
cd chapter-01 && ls

结果将如下所示:

终端窗口输出

恭喜!您有了您的第一个 Laravel 应用程序,使用了Composer包管理器构建。

现在,是时候将我们的应用程序与从 PHPDocker(我们的 PHP/MySQL Docker 截图)下载的文件连接起来了。要做到这一点,请按照以下步骤进行操作。

  1. 获取下载的存档hands-on-full-stack-web-development-with-angular-6-and-laravel-5.zip,并解压缩它。

  2. 复制所有文件夹内容(一个phpdocker文件夹和一个名为docker-compose.yml的文件)。

  3. 打开chapter-01文件夹并粘贴内容。

现在,在chapter-01文件夹内,我们将看到以下文件:

chapter-01 文件夹结构

让我们检查一下,确保一切都会顺利进行我们的配置。

  1. 打开您的终端窗口并输入以下命令:
docker-compose up -d

重要的是要记住,在这一点上,您需要在您的机器上启动和运行 Docker。如果您完全不知道如何在您的机器上运行 Docker,您可以在github.com/docker/labs/tree/master/beginner/找到更多信息。

  1. 请注意,此命令可能需要更多时间来创建和构建所有的容器。结果将如下所示:

Docker 容器已启动

前面的截图表明我们已成功启动了所有容器:memcachedwebserver(Nginx),mysqlphp-fpm

打开您的浏览器并输入http://localhost:8081;您应该看到 Laravel 的欢迎页面。

此时,是时候在文本编辑器中打开我们的示例项目,并检查所有的 Laravel 文件夹和文件。您可以选择您习惯使用的编辑器,或者,如果您愿意,您可以使用我们将在下一节中描述的编辑器。

安装 VS Code 文本编辑器

在本章和整本书中,我们将使用Visual Studio CodeVS Code),这是一个免费且高度可配置的多平台文本编辑器。它也非常适用于在 Angular 和 TypeScript 项目中使用。

按照以下步骤安装 VS Code:

  1. 转到下载页面,并在code.visualstudio.com/Download选择您的平台。

  2. 按照您的平台的安装步骤进行操作。

VS Code 拥有一个充满活力的社区,有大量的扩展。您可以在marketplace.visualstudio.com/VSCode上研究并找到扩展。在接下来的章节中,我们将安装并使用其中一些扩展。

现在,只需从marketplace.visualstudio.com/items?itemName=robertohuertasm.vscode-icons安装 VS Code 图标。

Laravel 应用程序的基本架构

正如之前提到的,Laravel 是用于开发现代 Web 应用程序的 MVC 框架。它是一种软件架构标准,将信息的表示与用户对其的交互分开。它采用的架构标准并不是很新;它自上世纪 70 年代中期以来就一直存在。它仍然很流行,许多框架今天仍在使用它。

您可以在en.wikipedia.org/wiki/Model-view-controller中了解更多关于 MVC 模式的信息。

Laravel 目录结构

现在,让我们看看如何在 Laravel 应用程序中实现这种模式:

  1. 打开 VS Code 编辑器。

  2. 如果这是您第一次打开 VS Code,请点击顶部菜单,然后导航到文件 | 打开。

  3. 搜索chapter-01文件夹,并点击打开

  4. 在 VS Code 的左侧展开app文件夹。

应用程序文件如下:

Laravel 根文件夹phpdocker文件夹和docker-compose.yml文件不是 Laravel 框架的一部分;我们在本章的前面手动添加了这些文件。

MVC 流程

在一个非常基本的 MVC 工作流中,当用户与我们的应用程序交互时,将执行以下截图中的步骤。想象一个简单的关于书籍的 Web 应用程序,有一个搜索输入框。当用户输入书名并按下Enter时,将发生以下流程循环:

MVC 流程

MVC 由以下文件夹和文件表示:

MVC 架构 应用程序路径 文件
模型 app/ User.php
视图 resources/views welcome.blade.php
控制器 app/Http/Controllers Auth/AuthController.php Auth/PasswordController.php

请注意,应用程序模型位于app文件夹的根目录,并且应用程序已经至少有一个文件用于 MVC 实现。

还要注意,app文件夹包含我们应用程序的所有核心文件。其他文件夹的名称非常直观,例如以下内容:

引导 缓存,自动加载和引导应用程序
配置 应用程序配置
数据库 工厂,迁移和种子
公共 JavaScript,CSS,字体和图像
资源 视图,SASS/LESS 和本地化
存储 此文件夹包含分离的应用程序,框架和日志
测试 使用 PHPunit 进行单元测试
供应商 Composer 依赖项

现在,让我们看看 Laravel 结构是如何工作的。

Laravel 应用程序生命周期

在 Laravel 应用程序中,流程与前面的示例几乎相同,但稍微复杂一些。当用户在浏览器中触发事件时,请求到达 Web 服务器(Apache/Nginx),我们的 Web 应用程序在那里运行。因此,服务器将请求重定向到public/index.php,整个框架的起点。在bootstrap文件夹中,启动autoloader.php并加载由 composer 生成的所有文件,检索 Laravel 应用程序的实例。

让我们看一下以下的截图:

Laravel 应用程序生命周期

该图表对于我们的第一章来说已经足够复杂了,因此我们不会详细介绍用户请求执行的所有步骤。相反,我们将继续介绍 Laravel 中的另一个非常重要的特性,即 Artisan 命令行界面(CLI)

您可以在官方文档的laravel.com/docs/5.2/lifecycle中了解更多关于 Laravel 请求生命周期的信息。

Artisan 命令行界面

现在,通过使用命令行创建 Web 应用程序是一种常见的做法;随着 Web 开发工具和技术的发展,这变得非常流行。

我们将提到 NPM 是最受欢迎的之一。但是,对于使用 Laravel 开发应用程序,我们有一个优势。当我们创建 Laravel 项目时,Artisan CLI 会自动安装。

让我们看看 Laravel 官方文档对 Artisan CLI 的说法:

Artisan 是 Laravel 附带的命令行界面的名称。它为您在开发应用程序时使用的一些有用的命令提供了帮助。

chapter-01文件夹中,我们找到了 Artisan bash 文件。它负责在 CLI 上运行所有可用的命令,其中有许多命令,用于创建类、控制器、种子等等。

在对 Artisan CLI 进行了简要介绍之后,最好的事情莫过于看一些实际的例子。所以,让我们动手操作,不要忘记启动 Docker:

  1. chapter-01文件夹中打开您的终端窗口,并键入以下命令:
docker-compose up -d
  1. 让我们进入php-fpm 容器并键入以下内容:
docker-compose exec php-fpm bash

现在我们在终端中有所有 Artisan CLI 命令可用。

这是与我们的 Docker 容器内的 Teminal 进行交互的最简单方式。如果您正在使用其他技术来运行 Laravel 应用程序,正如本章开头所提到的,您不需要使用以下命令:

docker-compose exec php-fpm bash

您可以在终端中键入下一步的相同命令。

  1. 仍然在终端中,键入以下命令:
php artisan list

你将看到框架版本和所有可用命令的列表:

Laravel Framework version 5.2.45
Usage:
 command [options] [arguments]
Options:
 -h, --help            Display this help message
 -q, --quiet           Do not output any message
 -V, --version         Display this application version
 --ansi            Force ANSI output
 --no-ansi         Disable ANSI output
 -n, --no-interaction  Do not ask any interactive question
 --env[=ENV]       The environment the command should run under.
 -v|vv|vvv, --verbose  Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
...

正如您所看到的,命令列表非常长。请注意,上面的代码片段中,我们没有列出php artisan list命令的所有选项,但我们将在下面看到一些组合。

  1. 在您的终端中,键入以下组合:
php artisan -h migrate

输出将详细解释migrate命令可以做什么以及我们有哪些选项,如下面的屏幕截图所示:

输出 php artisan -h migrate

也可以看到我们对migrate命令有哪些选项。

  1. 仍然在终端中,键入以下命令:
php artisan -h make:controller

您将看到以下输出:

输出 php artisan -h make:controller

现在,让我们看看如何在 Laravel 应用程序中使用 Artisan CLI 创建 MVC。

MVC 和路由

如前所述,我们现在将使用 Artisan CLI 分别创建模型、视图和控制器。但是,正如我们的标题所暗示的,我们将包括另一个重要项目:路由。我们已经在本章中提到过它们(在我们的 Laravel 请求生命周期图表中,以及在 MVC 本身的示例图表中)。

在本节中,我们将专注于创建文件,并在创建后检查它。

创建模型

让我们动手操作:

  1. chapter-01文件夹中打开您的终端窗口,并键入以下命令:
php artisan make:model Band

在命令之后,您应该看到一个绿色的成功消息,指出:模型成功创建。

  1. 返回到您的代码编辑器;在app文件夹中,您将看到Band.php文件,其中包含以下代码:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Band extends Model
{
    //
}

创建控制器

现在是使用 artisan 生成我们的控制器的时候了,让我们看看我们可以如何做到:

  1. 返回到终端窗口,并键入以下命令:
php artisan make:controller BandController 

在命令之后,您应该看到一个绿色的消息,指出:控制器成功创建。

  1. 现在,在app/Http/Controllers中,您将看到BandController.php,其中包含以下内容:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Requests;
class BandController extends Controller
{
    //
}

作为一个良好的实践,始终使用后缀<Somename>Controller创建您的控制器。

创建视图

正如我们之前在使用php artisan list命令时所看到的,我们没有任何别名命令可以自动创建应用程序视图。因此,我们需要手动创建视图:

  1. 返回到您的文本编辑器,并在resources/views文件夹中创建一个名为band.blade.php的新文件。

  2. 将以下代码放入band.blade.php文件中:

<div class="container">
    <div class="content">
        <div class="title">Hi i'm a view</div>
    </div>
</div>

创建路由

Laravel 中的路由负责指导来自用户请求的所有 HTTP 流量,因此路由负责 Laravel 应用程序中的整个流入,正如我们在前面的图表中看到的那样。

在本节中,我们将简要介绍 Laravel 中可用的路由类型,以及如何为我们的 MVC 组件创建一个简单的路由。

在这一点上,只需要看一下路由是如何工作的。在本书的后面,我们将深入研究应用程序路由。

因此,让我们看看在 Laravel 中可以用来处理路由的内容:

代码 HTTP | 方法 |动词
Route::get($uri, $callback); 获取
Route::post($uri, $callback); 发布
路由::放置(\(uri,\)callback); 放置
Route::patch($uri, $callback); 补丁
Route::delete($uri, $callback); 删除
Route::options($uri, $callback); 选项

每个可用的路由都负责处理一种类型的 HTTP 请求方法。此外,我们可以在同一个路由中组合多种方法,就像下面的代码一样。现在不要太担心这个问题;我们将在本书的后面看到如何处理这种类型的路由:

Route::match(['get', 'post'], '/', function () {
    //
});

现在,让我们创建我们的第一个路由:

  1. 在文本编辑器中,打开routes文件夹中的web.php,并在welcome view之后添加以下代码:
Route::get('/band', function () {
 return view('band');
 });
  1. 在浏览器中打开http://localhost:8081/band,您将看到以下消息:

嗨,我是一个视图

不要忘记使用docker-compose up -d命令启动所有 Docker 容器。如果您遵循了前面的示例,您将已经拥有一切都在正常运行。

太棒了!我们已经创建了我们的第一个路由。这是一个简单的例子,但我们已经把所有东西都放在了正确的位置,并且一切都运行良好。在下一节中,我们将看看如何将模型与控制器集成并呈现视图。

连接到数据库

正如我们之前所看到的,控制器由路由激活,并在模型/数据库和视图之间传递信息。在前面的示例中,我们在视图中使用静态内容,但在更大的应用程序中,我们几乎总是会有来自数据库的内容,或者在控制器内生成并传递给视图的内容。

在下一个示例中,我们将看到如何做到这一点。

在 Docker 容器内设置数据库

现在是时候配置我们的数据库了。如果您使用 Homestead,您可能已经配置并且数据库连接正常工作。要检查,请打开终端并输入以下命令:

php artisan tinker
DB::connection()->getPdo();

如果一切顺利,您将看到以下消息:

数据库连接消息

然而,对于这个例子,我们正在使用 Docker,我们需要做一些配置来完成这个任务:

  1. 在根项目内,打开.env文件并查看第 8 行(数据库连接),如下所示:
 DB_CONNECTION=mysql
 DB_HOST=127.0.0.1
 DB_PORT=3306
 DB_DATABASE=homestead
 DB_USERNAME=homestead
 DB_PASSWORD=secret

现在,用以下行替换前面的代码:

 DB_CONNECTION=mysql
 DB_HOST=mysql
 DB_PORT=3306
 DB_DATABASE=laravel-angular-book
 DB_USERNAME=laravel-angular-book
 DB_PASSWORD=123456

请注意,我们需要稍微更改一下以获取 Docker MySQL 容器的指示;如果您不记得在PHPDocker.io生成器中选择了什么,可以从容器配置中复制它。

  1. 在根目录打开docker-compose.yml

  2. 从 MySQL 容器设置中复制环境变量:

mysql:
  image: mysql:8.0
  entrypoint: ['/entrypoint.sh', '--character-set-server=utf8', '--
  collation-server=utf8_general_ci']
  container_name: larahell-mysql
  working_dir: /application
  volumes:
    - .:/application
  environment:
    - MYSQL_ROOT_PASSWORD=larahell
    - MYSQL_DATABASE=larahell-angular-book
    - MYSQL_USER=larahell-user
    - MYSQL_PASSWORD=123456
  ports:
    - "8083:3306"

现在是时候测试我们的连接了。

  1. 在您的终端窗口中,输入以下命令:
docker-compose exec php-fpm bash
  1. 最后,让我们检查一下我们的连接;输入以下命令:
php artisan tinker DB::connection()->getPdo();

您应该看到与上一个截图相同的消息。然后,您将拥有继续进行示例所需的一切。

创建迁移文件和数据库种子

迁移文件在一些 MVC 框架中非常常见,例如 Rails,Django 和当然,Laravel。通过这种类型的文件,我们可以使我们的数据库与我们的应用程序保持一致,因为我们无法对数据库方案进行版本控制。迁移文件帮助我们存储数据库中的每个更改,以便我们可以对这些文件进行版本控制,并保持项目的一致性。

数据库种子用于在数据库的表中填充一批初始记录;当我们从头开始开发 Web 应用程序时,这非常有用。初始加载的数据可以是各种各样的,从用户表到管理对象,如密码和令牌,以及我们需要的其他所有内容。

让我们看看如何在 Laravel 中为Bands模型创建迁移文件:

  1. 打开您的终端窗口并输入以下命令:
php artisan make:migration create_bands_table
  1. 打开database/migrations文件夹,您将看到一个名为<timestamp>create_bands_table.php的文件。

  2. 打开此文件,并在public function up()中粘贴以下代码:

Schema::create('bands', function (Blueprint $table) {
   $table->increments('id');
   $table->string('name');
   $table->string('description');
   $table->timestamps();
});
  1. 将以下代码粘贴到public function down()中:
Schema::dropIfExists('bands');
  1. 最终结果将是以下代码:
<?php
use Illuminate\Support\Facades\Schema;
 use Illuminate\Database\Schema\Blueprint;
 use Illuminate\Database\Migrations\Migration;
class CreateBandsTable extends Migration
 {
     /**
     * Run the migrations.
     *
     * @return void
    */
     public function up()
     {
         Schema::create('bands', function (Blueprint $table) {
         $table->increments('id');
         $table->string('name');
         $table->string('description');
         $table->timestamps();
         });
     }
    /**
     * Reverse the migrations.
     *
     * @return void
     */
     public function down()
     {
         Schema::dropIfExists('bands');
     }
 }
  1. database/factories文件夹中,打开ModalFactory.php文件,并在User Factory之后添加以下代码。请注意,我们在factory函数中使用了一个名为faker的 PHP 库,以生成一些数据:
$factory->define(App\Band::class, function (Faker\Generator $faker) {
return [
 'name' => $faker->word,
 'description' => $faker->sentence
 ];
 });
  1. 返回到您的终端窗口并创建一个数据库种子。要做到这一点,请输入以下命令:
php artisan make:seeder BandsTableSeeder
  1. database/seeds文件夹中,打开BandsTableSeeder.php文件,并在public function run()中输入以下代码:
factory(App\Band::class,5)->create()->each(function ($p) {
 $p->save();
 });
  1. 现在,在database/seeds文件夹中,打开DatabaseSeeder.php文件,并在public function run()中添加以下代码:
$this->call(BandsTableSeeder::class);

您可以在github.com/fzaninotto/Faker上阅读更多关于 Faker PHP 的信息。

在我们继续之前,我们需要对Band模型进行一些小的重构。

  1. 在应用程序根目录中,打开Band.php文件并在Band类中添加以下代码:
protected $fillable = ['name','description'];
  1. 返回到您的终端并输入以下命令:
php artisan migrate

在命令之后,您将在终端窗口中看到以下消息:

 Migration table created successfully.

前面的命令只是用来填充我们的种子数据库。

  1. 返回到您的终端并输入以下命令:
php artisan db:seed

我们现在有五个项目可以在我们的数据库中使用。

让我们看看一切是否会顺利进行。

  1. 在您的终端中,要退出php-fpm 容器,请输入以下命令:
exit
  1. 现在,在应用程序根文件夹中,在终端中输入以下命令:
docker-compose exec mysql mysql -ularavel-angular-book -p123456

前面的命令将使您可以在mysql Docker 容器中访问 MySQL 控制台,几乎与我们如何访问php-fpm 容器相同。

  1. 在终端中,输入以下命令以查看所有数据库:
show databases;

如您所见,我们有两个表:information_schemalaravel-angular-book

  1. 让我们访问laravel-angular-book表;输入以下命令:
use laravel-angular-book;
  1. 现在,让我们检查我们的表,如下所示:
show tables;
  1. 现在,让我们从bands表中SELECT所有记录:
SELECT * from bands;

我们将看到类似以下截图的内容:

数据库 bands 表

  1. 现在,使用以下命令退出 MySQL 控制台:
exit

使用资源标志创建 CRUD 方法

让我们看看 Artisan CLI 的另一个功能,使用单个命令创建所有的创建读取更新删除(CRUD)操作。

首先,在app/Http/Controllers文件夹中,删除BandController.php文件:

  1. 打开您的终端窗口并输入以下命令:
php artisan make:controller BandController --resource

这个动作将再次创建相同的文件,但现在它包括 CRUD 操作,如下面的代码所示:

<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class BandController extends Controller
 {
     /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
     public function index()
     {
         //
     }
    /**
     * Show the form for creating a new resource.
     *
     * @return \Illuminate\Http\Response
     */
     public function create()
     {
         //
     }
    /**
     * Store a newly created resource in storage.
     *
     * @param \Illuminate\Http\Request $request
     * @return \Illuminate\Http\Response
     */
     public function store(Request $request)
     {
         //
     }
    /**
     * Display the specified resource.
     *
     * @param int $id
     * @return \Illuminate\Http\Response
     */
     public function show($id)
     {
         //
     }
    /**
     * Show the form for editing the specified resource.
     *
     * @param int $id
     * @return \Illuminate\Http\Response
     */
     public function edit($id)
     {
         //
     }
    /**
     * Update the specified resource in storage.
     *
     * @param \Illuminate\Http\Request $request
     * @param int $id
     * @return \Illuminate\Http\Response
     */
     public function update(Request $request, $id)
     {
         //
     }
    /**
     * Remove the specified resource from storage.
     *
     * @param int $id
     * @return \Illuminate\Http\Response
     */
     public function destroy($id)
     {
         //
     }
 }

在这个例子中,我们将只编写两种方法:一种用于列出所有记录,另一种用于获取特定记录。不要担心其他方法;我们将在接下来的章节中涵盖所有方法。

  1. 编辑public function index()并添加以下代码:
$bands = Band::all();
 return $bands;
  1. 现在,编辑public function show()并添加以下代码:
$band = Band::find($id);
 return view('bands.show', array('band' => $band));
  1. App\Http\Requests之后添加以下行:
use App\Band;
  1. 更新routes.php文件,将其更改为以下代码:
Route::get('/', function () {
 return view('welcome');
 });
Route::resource('bands', 'BandController');
  1. 打开浏览器,转到http://localhost:8081/bands,您将看到以下内容:
[{
  "id": 1,
  "name": "porro",
  "description": "Minus sapiente ut libero explicabo et voluptas harum.",
  "created_at": "2018-03-02 19:20:58",
  "updated_at": "2018-03-02 19:20:58"}
...]

如果你的数据与之前的代码不同,不要担心;这是由于 Faker 生成了随机数据。请注意,我们直接将 JSON 返回给浏览器,而不是将数据返回给视图。这是 Laravel 的一个非常重要的特性;它默认序列化和反序列化数据。

创建刀片模板引擎

现在,是时候创建另一个视图组件了。这一次,我们将使用刀片模板引擎来显示数据库中的一些记录。让我们看看官方文档对刀片的说法:

刀片是 Laravel 提供的简单而强大的模板引擎。与其他流行的 PHP 模板引擎不同,刀片不限制您在视图中使用纯 PHP 代码。所有刀片视图都会被编译成纯 PHP 代码并缓存,直到被修改,这意味着刀片对您的应用基本上没有额外开销。

现在,是时候看到这个行为的实际效果了:

  1. 返回到代码编辑器,在resources/views内创建一个名为bands的文件夹。

  2. resources/views/bands内创建一个名为show.blade.php的文件,并将以下代码放入其中:

<h1>Band {{ $band->id }}</h1>
<ul>
<li>band: {{ $band->name }}</li>
<li>description: {{ $band->description }}</li>
</ul>

你可以在laravel.com/docs/5.2/blade了解更多关于刀片的信息。

  1. 在浏览器中打开http://localhost:8081/bands/1。你会看到模板在运行中,结果类似以下:

模板引擎的视图

请注意,这里我们使用刀片模板引擎来显示数据库中的记录。现在,让我们创建另一个视图来渲染所有的记录。

  1. resources/views/bands内创建一个名为index.blade.php的文件,并将以下代码放入其中:
@foreach ($bands as $band)
<h1>Band id: {{ $band->id }}</h1>
<h2>Band name: {{ $band->name }}</h2>
<p>Band Description: {{ $band->description }}</p>
@endforeach
  1. 返回到你的浏览器,访问http://localhost:8081/bands/,你会看到类似以下的结果:

视图模板引擎

总结

我们终于完成了第一章,并涵盖了 Laravel 框架的许多核心概念。即使在本章讨论的简单示例中,我们也为 Laravel 的所有功能提供了相关的基础。只凭这些知识就可以创建令人难以置信的应用。但是,我们打算深入探讨一些值得单独章节的概念。在整本书中,我们将使用 RESTful API、Angular 和一些其他工具创建一个完整的应用,比如 TypeScript,我们将在下一章中讨论。

第二章:TypeScript 的好处

TypeScript 使您能够编写 JavaScript 代码。它包括静态类型和其他在面向对象语言中非常常见的特性。此外,使用 TypeScript,您可以使用 ECMAScript 6 的所有特性,因为编译器将它们转换为当前浏览器可读的代码。

TypeScript 的一个特性是用户可以创建类型化的变量,就像在 Java 或 C#中一样(例如,const VARIABLE_NAME: Type = Value),不仅如此,TypeScript 还帮助我们编写干净、组织良好的代码。这就是为什么 Angular 团队为当前版本的框架采用了 TypeScript 的原因之一。

在开始之前,让我们看一下官方 TypeScript 文档中的内容:

"TypeScript 是 JavaScript 的一种有类型的超集,可以编译为普通的 JavaScript。

任何浏览器。任何主机。

在本章中,我们将在我们的环境中全局安装 TypeScript,以了解 TypeScript 文件在转换为 JavaScript 时会发生什么。不用担心;Angular 应用程序已经为我们提供了内置的 TypeScript 编译器。

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

  • 安装 TypeScript

  • 使用 TypeScript 的好处

  • 如何将 TypeScript 文件转译为 JavaScript 文件

  • 使用静态类型编写 JavaScript 代码

  • 理解 TypeScript 中的接口、类和泛型

安装 TypeScript

安装和开始使用 TypeScript 非常简单。您的机器上必须安装 Node.js 和 Node 包管理器(NPM)。

如果您还没有它们,请前往nodejs.org/en/download/,并按照您的平台的逐步安装说明进行操作。

让我们按照以下步骤安装 TypeScript:

  1. 打开终端并输入以下命令以安装 TypeScript 编译器:
npm install -g typescript

请注意,-g标志表示在您的机器上全局安装编译器。

  1. 让我们检查一下可用的 TypeScript 命令。在终端中输入以下命令:
tsc --help

上述命令将提供有关 TypeScript 编译器的大量信息;我们将看到一个简单的示例,演示如何将 TypeScript 文件转译为 JavaScript 文件。

示例:

 tsc hello.ts

 tsc --outFile file.js file.ts

前面几行的描述如下:

  • tsc命令编译hello.ts文件。

  • 告诉编译器创建一个名为hello.js的输出文件。

创建一个 TypeScript 项目

一些文本编辑器,如 VS Code,让我们有能力将 TS 文件作为独立单元处理,称为文件范围。尽管这对于孤立的文件(如下面的示例)非常有用,但建议您始终创建一个 TypeScript 项目。然后,您可以模块化您的代码,并在将来的文件之间使用依赖注入。

使用名为tsconfig.json的文件在目录的根目录创建了一个 TypeScript 项目。您需要告诉编译器哪些文件是项目的一部分,编译选项以及许多其他设置。

一个基本的tsconfig.json文件包含以下代码:

{ "compilerOptions":
  { "target": "es5",
   "module": "commonjs"
  }
}

尽管前面的代码非常简单和直观,我们只是指定了我们将在项目中使用的编译器,以及使用的模块类型。如果代码片段指示我们使用 ECMAScript 5,所有 TypeScript 代码将被转换为 JavaScript,使用 ES5 语法。

现在,让我们看看如何可以借助tsc编译器自动创建此文件:

  1. 创建一个名为chapter-02的文件夹。

  2. chapter-02文件夹中打开您的终端。

  3. 输入以下命令:

tsc --init

我们将看到由tsc编译器生成的以下内容:

{
"compilerOptions": {
/* Basic Options */
/* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
"target": "es5",
/* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
"module": "commonjs",
...
/* Strict Type-Checking Options */
/* Enable all strict type-checking options. */
"strict": true,
...
/* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"esModuleInterop": true
/* Source Map Options */
...
/* Experimental Options */
...
}
}

请注意,我们省略了一些部分。您应该看到所有可用的选项;但是,大多数选项都是被注释掉的。现在不用担心这一点;稍后,我们将更详细地查看一些选项。

现在,让我们创建一个 TypeScript 文件,并检查一切是否顺利。

  1. chapter-02文件夹中打开 VS Code,创建一个名为sample-01.ts的新文件。

  2. 将以下代码添加到sample-01.ts中:

console.log('First Sample With TypeScript');
  1. 回到你的终端,输入以下命令:
tsc sample-01.ts

在 VS Code 中,你可以使用集成终端;在顶部菜单栏上,点击 View | Integrate Terminal [ˆ`]。

请注意,另一个文件出现了,但扩展名是.js

如果你比较这两个文件,它们完全相同,因为我们的例子非常简单,我们使用的是一个简单的console.log()函数。

由于 TypeScript 是 JavaScript 的超集,这里也提供了所有的 JS 功能。

TypeScript 的好处

以下是使用 TypeScript 的好处的一个小列表:

  • TypeScript 是强大的、安全的,易于调试。

  • TypeScript 代码在转换为 JavaScript 之前被编译,因此我们可以在运行代码之前捕捉各种错误。

  • 支持 TypeScript 的 IDE 具有改进代码完成和检查静态类型的能力。

  • TypeScript 支持面向对象编程(OOP),包括模块、命名空间、类等。

TypeScript 受欢迎的一个主要原因是它已经被 Angular 团队采用;而且,由于 Angular 是用于开发现代 Web 应用程序的最重要的前端框架之一,这激励了许多开发人员从 AngularJS 的 1.x 版本迁移到 2/4/5/6 版本学习它。

这是因为大多数 Angular 教程和示例都是用 TypeScript 编写的。

  1. 打开sample-01.ts,在console.log()函数之后添加以下代码:
class MyClass {
  public static sum(x:number, y: number) {
  console.log('Number is: ', x + y);
  return x + y;
 }
}
MyClass.sum(3, 5);
  1. 回到你的终端,输入以下代码:
tsc sample-01.ts
  1. 现在,当你打开sample-01.js文件时,你会看到以下截图中显示的结果:

使用 TypeScript 与生成的 JavaScript 进行比较

请注意,sum 类参数(x:number, y:number)被赋予了类型 number。这是 TypeScript 的一个优点;然而,由于我们根据类型和在函数调用MyClass.sum(3, 5)中使用数字,我们无法看到它的强大之处。

让我们做一个小改变,看看区别。

  1. MyClass.sum()函数调用更改为MyClass.sum('a', 5)

  2. 回到你的终端,输入以下命令:

tsc sample-01.ts

请注意,我们收到了一个 TypeScript 错误:

error TS2345: Argument of type '"a"' is not assignable to parameter of type 'number'.

如果你使用 VS Code,你会在执行编译文件之前看到以下截图中的消息:

编译错误消息

如前所述,VS Code 是 TypeScript 语言的强大编辑器;除了具有集成终端外,我们还能清楚地看到编译错误。

我们可以对 TS 文件进行一些修改,而不是每次都输入相同的命令。我们可以使用--watch标志,编译器将自动运行我们对文件所做的每一次更改。

  1. 在你的终端中,输入以下命令:
tsc sample-01.ts --watch
  1. 现在,让我们修复它;回到 VS Code,用以下代码替换MyClass.sum()函数:
MyClass.sum(5, 5);

要停止 TS 编译器,只需按下Ctrl + C

使用静态类型编写 JavaScript 代码

在使用 TypeScript 时,你会注意到的第一件事是它的静态类型,以及下表中指示的所有 JavaScript 类型:

基本类型 对象
字符串 函数
数字 数组
原型
未定义
布尔
符号

这意味着你可以声明变量的类型;给变量分配类型非常简单。让我们看一些例子,只使用 JavaScript 类型:


function Myband () {
  let band: string;
  let active: boolean;
  let numberOfAlbuns: number;
}

使用 TypeScript,我们有更多的类型,我们将在以下部分中看到。

创建一个元组

元组就像一个有组织的类型数组。让我们创建一个看看它是如何工作的:

  1. chapter-02文件夹中,创建一个名为tuple.ts的文件,并添加以下代码:
const organizedArray: [number, string, boolean] = [0, 'text',
      false];
let myArray: [number, string, boolean];
myArray = ['text', 0, false]
console.log(myArray);

前面的代码在 JavaScript 中看起来很好,但在 TypeScript 中,我们必须尊重变量类型;在这里,我们试图传递一个字符串,而我们必须传递一个数字。

  1. 在你的终端中,输入以下命令:
tsc tuple.ts

你将看到以下错误消息:

tuple.ts(4,1): error TS2322: Type '[string, number, false]' is not assignable to type '[number, string, boolean]'.
 Type 'string' is not assignable to type 'number'.

在 VS Code 中,你会在编译文件之前看到错误消息。这是一个非常有用的功能。

当我们用正确的顺序修复它(myArray = [0, 'text', false])时,错误消息消失了。

还可以创建一个元组类型,并将其用于分配一个变量,就像我们在下一个例子中看到的那样。

  1. 返回到你的终端,并将以下代码添加到tuple.ts文件中:
// using tuple as Type
type Tuple = [number, string, boolean];
let myTuple: Tuple;
myTuple = [0, 'text', false];
console.log(myTuple);

这时,你可能会想知道为什么前面的例子有console.log输出。

借助我们之前安装的 Node.js,我们可以运行示例并查看console.log()函数的输出。

  1. 在终端中,输入以下命令:
node tuple.js

请注意,你需要运行 JavaScript 版本,就像前面的例子一样。如果你尝试直接运行 TypeScript 文件,你可能会收到错误消息。

使用 void 类型

在 TypeScript 中,定义函数的返回类型是强制的。当我们有一个没有返回值的函数时,我们使用一个叫做void的类型。

让我们看看它是如何工作的:

chapter-02文件夹内创建一个名为void.ts的新文件,并添加以下代码:

function myVoidExample(firstName: string, lastName: string): string {
    return firstName + lastName;
}
console.log(myVoidExample('Jhonny ', 'Cash'));

在前面的代码中,一切都很好,因为我们的函数返回一个值。如果我们删除返回函数,我们将看到以下错误消息:

void.ts(1,62): error TS2355: A function whose declared type is neither 'void' nor 'any' must return a value.

在 VS Code 中,你会看到以下内容:

VS Code 输出错误

要修复它,用void替换类型string

function myVoidExample(firstName: string, lastName: string): void {
const name = firstName + lastName;
}

这非常有用,因为我们的函数并不总是返回一个值。但请记住,我们不能在返回值的函数中声明void

选择退出类型检查 - any

当我们不知道从函数中期望什么时(换句话说,当我们不知道我们将返回哪种类型时),any类型非常有用:

  1. chapter-02文件夹中创建一个名为any.ts的新文件,并添加以下代码:
let band: any;
band = {
    name: "Motorhead",
    description: "Heavy metal band",
    rate: 10
}
console.log(band);
band = "Motorhead";
console.log(band);

请注意,第一个band赋值是一个对象,而第二个是一个字符串。

  1. 返回到你的终端,编译并运行这段代码;输入以下命令:
tsc any.ts
  1. 现在,让我们看一下输出。输入以下命令:
node any.js

你将在终端看到以下消息:

{ name: 'Motorhead', description: 'Heavy metal band', rate: 10 }
 Motorhead

在这里,我们可以将任何东西赋给我们的band变量。

使用枚举

enum允许我们使用更直观的名称对值进行分组。有些人更喜欢称枚举列表为其他名称。让我们看一个例子,以便更容易理解这在实践中是如何工作的:

  1. chapter-02文件夹中创建一个名为enum.js的文件,并添加以下代码:
enum bands {
    Motorhead,
    Metallica,
    Slayer
}
console.log(bands);
  1. 在你的终端中,输入以下命令以转换文件:
tsc enum.ts
  1. 现在,让我们执行这个文件。输入以下命令:
node enum.js

你将在终端看到以下结果:

{ '0': 'Motorhead',
 '1': 'Metallica',
 '2': 'Slayer',
 Motorhead: 0,
 Metallica: 1,
 Slayer: 2 }

现在,我们可以通过名称而不是位置来获取值。

  1. console.log()函数之后添加以下代码行:
let myFavoriteBand = bands.Slayer;
console.log(myFavoriteBand);

现在,执行步骤 2步骤 3中的命令以检查结果。你将在终端中看到以下输出:

{ '0': 'Motorhead',
 '1': 'Metallica',
 '2': 'Slayer',
 Motorhead: 0,
 Metallica: 1,
 Slayer: 2 }
 My Favorite band is:  Slayer

请注意,band对象中声明的所有值(乐队名称)都被转换为字符串,放在一个索引对象中,就像你在前面的例子中看到的那样。

使用 never 类型

never类型是在 TypeScript 2.0 中引入的;它意味着永远不会发生的值。乍一看,它可能看起来很奇怪,但在某些情况下可以使用它。

让我们看看官方文档对此的解释:

never类型表示永远不会发生的值的类型。具体来说,never是永远不会返回的函数的返回类型,也是永远不会为type保护下的变量为真的类型。

假设在另一个函数内调用的消息传递函数指定了回调。

它看起来会像以下代码:

const myMessage = (text: string): never => {
    throw new Error(text);
}
const myError = () => Error('Some text here');

另一个例子是检查同时是字符串和数字的值,例如以下代码:

function neverHappen(someVariable: any) {
    if (typeof someVariable === "string" && typeof someVariable ===
     "number") {
    console.log(someVariable);
    }
}
neverHappen('text');

类型:未定义和空

在 TypeScript 中,undefinednull本身就是类型;这意味着 undefined 是一种类型(undefined),null 是一种类型(null)。令人困惑?undefined 和 null 不能是类型变量;它们只能被分配为变量的值。

它们也是不同的:null 变量意味着变量被设置为 null,而 undefined 变量没有分配值。

let A = null;
    console.log(A) // null
    console.log(B) // undefined

理解 TypeScript 中的接口、类和泛型

面向对象编程OOP)是一个非常古老的编程概念,用于诸如 Java、C#和许多其他语言中。

使用 TypeScript 的优势之一是能够将其中一些概念带入您的 JavaScript Web 应用程序中。除了能够使用类、接口等,我们还可以轻松扩展导入类和导入模块,正如我们将在接下来的示例中看到的那样。

我们知道在纯 JavaScript 中使用类已经是一个选项,使用 ECMAScript 5。虽然它很相似,但也有一些区别;我们不会在本章中讨论它们,以免混淆我们的读者。我们只会专注于 TypeScript 中采用的实现。

创建一个类

理解 TypeScript 中的类的最佳方法是创建一个。一个简单的类看起来像以下代码:

class Band {
    public name: string;
    constructor(text: string) {
    this.name = text;
    }
}

让我们创建我们的第一个类:

  1. 打开您的文本编辑器,创建一个名为my-first-class.ts的新文件,并添加以下代码:
class MyBand {
    // Properties without prefix are public
    // Available is; Private, Protected
    albums: Array<string>;
    members: number;
    constructor(albums_list: Array<string>, total_members: number) {
        this.albums = albums_list;
        this.members = total_members;
    }
    // Methods
    listAlbums(): void {
        console.log("My favorite albums: ");
        for(var i = 0; i < this.albums.length; i++) {
            console.log(this.albums[i]);
        }
    }
}
// My Favorite band and his best albums
let myFavoriteAlbums = new MyBand(["Ace of Spades", "Rock and Roll", "March or Die"], 3);
// Call the listAlbums method.
console.log(myFavoriteAlbums.listAlbums());

我们在以前的代码中添加了一些注释以便理解。

一个类可以有尽可能多的方法。在前一个类的情况下,我们只给出了一个方法,列出我们最喜欢的乐队专辑。您可以在终端上测试这段代码,将任何您想要的信息传递给新的MyBand()构造函数。

这很简单,如果您已经接触过 Java、C#甚至 PHP,您可能已经看到了这个类结构。

在这里,我们可以将继承(OOP)原则应用于我们的类。让我们看看如何做到这一点:

  1. 打开band-class.ts文件,并在console.log()函数之后添加以下代码:
/////////// using inheritance with TypeScript ////////////
class MySinger extends MyBand {
    // All Properties from MyBand Class are available inherited here
    // So we define a new constructor.
    constructor(albums_list: Array<string>, total_members: number) {
        // Call the parent's constructor using super keyword.
        super(albums_list, total_members);
    }
    listAlbums(): void{
        console.log("Singer best albums:");
        for(var i = 0; i < this.albums.length; i++) {
            console.log(this.albums[i]);
        }
    }
}
// Create a new instance of the YourBand class.
let singerFavoriteAlbum = new MySinger(["At Falson Prision", "Among out the Stars", "Heroes"], 1);
console.log(singerFavoriteAlbum.listAlbums());

在 Angular 中,类非常有用于定义组件,正如我们将在第三章中看到的那样,理解 Angular 6 的核心概念

声明一个接口

在使用 TypeScript 时,接口是我们的盟友,因为它们在纯 JavaScript 中不存在。它们是一种有效的方式来对变量进行分组和类型化,确保它们始终在一起,保持一致的代码。

让我们看一个声明和使用接口的实际方法:

  1. 在您的文本编辑器中,创建一个名为band-interface.ts的新文件,并添加以下代码:
interface Band {
    name: string,
    total_members: number
}

要使用它,请将接口分配给函数类型,就像以下示例中那样。

  1. band-interface.ts文件中的接口代码之后添加以下代码:
interface Band {
    name: string,
    total_members: number
}
function unknowBand(band: Band): void {
    console.log("This band: " + band.name + ", has: " +                 band.total_members + " members");
}

请注意,在这里,我们使用Band接口来为我们的function参数命名。因此,当我们尝试使用它时,我们需要在新对象中保持相同的结构,就像以下示例中的那样:

// create a band object with the same properties from Band interface:
let newband = {
    name: "Black Sabbath",
    total_members: 4
}
console.log(unknowBand(newband));

请注意,您可以通过键入以下命令来执行所有示例文件

在您的终端中键入tsc band-interface.tsband-interface.js节点。

因此,如果您遵循前面的提示,您将在终端窗口中看到相同的结果:

This band: Black Sabbath, has: 4 members

正如您所看到的,TypeScript 中的接口非常棒;我们可以用它们做很多事情。在本书的课程中,我们将看一些更多使用接口在实际 Web 应用程序中的例子。

创建泛型函数

泛型是创建灵活类和函数的非常有用的方式。它们与 C#中使用的方式非常相似。它非常有用,可以在多个地方使用。

我们可以通过在函数名称后添加尖括号并封装数据类型来创建泛型函数,就像以下示例中的示例一样:

function genericFunction<T>( arg: T ): T [] {
    let myGenericArray: T[] = [];
    myGenericArray.push(arg);
    return myGenericArray;
}

请注意,尖括号内的t<t>)表示genericFunction()是通用类型。

让我们看看实际操作:

  1. 在您的代码编辑器中,创建一个名为generics.ts的新文件,并添加以下代码:
function genericFunction<T>( arg: T ): T [] {
    let myGenericArray: T[] = [];
    myGenericArray.push(arg);
    return myGenericArray;
}
let stringFromGenericFunction = genericFunction<string>("Some string goes here");
console.log(stringFromGenericFunction[0]);
let numberFromGenericFunction = genericFunction(190);
console.log(numberFromGenericFunction[0]);

让我们看看我们的通用函数会发生什么。

  1. 回到您的终端并输入以下命令:
tsc generics.ts
  1. 现在,让我们使用以下命令执行文件:
node generics.js

我们将看到以下结果:

Some string goes here
 190

请注意,编译器能够识别我们作为function参数传递的数据类型。在第一种情况下,我们明确将参数作为字符串传递,而在第二种情况下,我们不传递任何东西。

尽管编译器能够识别我们使用的参数类型,但始终确定我们要传递的数据类型是非常重要的。例如:

let numberFromGenericFunction = genericFunction<number>(190);
console.log(numberFromGenericFunction[0]);

使用模块

在使用 TypeScript 开发大型应用程序时,模块非常重要。它们允许我们导入和导出代码、类、接口、变量和函数。这些函数在 Angular 应用程序中非常常见。

然而,它们只能通过使用库来实现,这可能是浏览器的 Require.js,或者是 Node.js 的 Common.js。

在接下来的章节中,我们将说明如何在实践中使用这些特性。

使用类导出功能

任何声明都可以被导出,正如我们之前提到的;要这样做,我们只需要添加export关键字。在下面的例子中,我们将导出band类。

在您的文本编辑器中,创建一个名为export.ts的文件,并添加以下代码:

export class MyBand {
    // Properties without prefix are public
    // Available is; Private, Protected
    albums: Array<string>;
    members: number;
    constructor(albums_list: Array<string>, total_members: number) {
        this.albums = albums_list;
        this.members = total_members;
    }
    // Methods
    listAlbums(): void {
        console.log("My favorite albums: ");
        for(var i = 0; i < this.albums.length; i++) {
            console.log(this.albums[i]);
        }
    }
}

现在我们的Myband类可以被导入到另一个文件中了。

导入和使用外部类

使用关键字import可以实现导入,并且可以根据您使用的库的不同方式进行声明。使用 Require.js 的示例如下:

  • 回到您的文本编辑器,创建一个名为import.ts的文件,并添加以下代码:
import MyBand = require('./export');
console.log(Myband());

使用 Common.js 的示例如下:

import { MyBand } from './export';
console.log(new Myband(['ZZ Top', 'Motorhead'], 3));
  • 第二种方法已被 Angular 团队采用,因为 Angular 使用 Webpack,这是一个构建现代 Web 应用程序的模块捆绑器。

摘要

在本章中,您看到了 TypeScript 的基本原则。我们只是触及了表面,但是我们为您提供了一个处理使用 TypeScript 开发 Angular 应用程序的坚实基础。

在本书的过程中,随着我们创建 Web 应用程序的进展,我们将增强您的理解。

第三章:理解 Angular 6 的核心概念

Angular 框架已经成为全球最流行的前端应用程序开发工具之一。除了非常多功能(与其他库如React.jsVue.js非常不同,这些库只用于一个目的),Angular 是一个完整的框架,并且随着 Angular 6 的新更新,我们现在有更多资源可用于创建令人惊叹和快速的 Web 应用程序。此外,Angular 团队每年提出两次重大更新。

Angular 的另一个优势是其包含用于创建 Web 应用程序的 Angular 命令行界面CLI)。这为我们提供了额外的能力;通过终端中的一个简单命令,我们可以非常快速和轻松地创建应用程序的样板代码。然而,一切并不像我们希望的那样甜蜜,因此我们需要了解 Angular 的基本概念,并知道如何避免一些问题。这可以通过采用基于组件和模块的开发思维模型来轻松解决。在接下来的示例中,我们将仔细创建可扩展和模块化项目的基本结构。

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

  • Angular 6 - 更小,更快,更容易

  • Angular 和组件方法用于开发现代 Web 应用程序

  • 安装工具:Git,Angular CLI,HTTP 服务器和 VS Code 插件

  • 创建一个简单的 Angular 应用程序

  • 简单部署

Angular 6 - 更小,更快,更容易

以下功能不仅适用于版本 6,而且从版本 5 开始就已包含;我们在这里提到它们是因为它们是构建现代 Web 应用程序的强大功能:

  • Webpack:您现在可以使用作用域托管技术生成更小的模块。

  • 您可以通过使用 JavaScript 的 RxJS 6 库减少常见用例的捆绑大小。

  • Angular CLI 允许使用命令,如ng update,来更新所有依赖项。

  • 您将有选择使用 Angular Material Design 启动应用程序。

  • ng add命令支持创建渐进式 Web 应用程序或将现有应用程序转换为渐进式 Web 应用程序PWA)。

  • 您将有机会使用 Bazel 构建应用程序的库,并与其他团队共享库。

  • Angular 使得可以打包自定义 HTML/JavaScript 元素以供第三方应用程序使用。

您可以在bazel.build/了解有关 Bazel 的更多信息。

当然,Angular 6 版本中还有许多其他改进和功能;请注意,本书是在 Angular 6 beta 7 版本上编写的,接下来的章节将有关于当前 Angular 版本的更多新闻。

Angular 和组件方法用于开发现代 Web 应用程序

Angular 组件类似于 Web 组件;它们用于组合网页,甚至其他组件。一个 Web 应用程序中可能有数十个组件。

组件定义视图和模板,并且它们属于应用程序中的一个模块;每个应用程序至少有一个根模块,由 Angular CLI 命名为AppModule.ts

app.module.ts文件包含了 Angular 应用程序的所有引导代码和配置,如下面的代码块所示:

import { NgModule } from '@angular/core';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

上述代码是 Angular 应用程序的最基本配置;我们从 Angular 核心库中导入NgModule并将其用作装饰器:@NgModule

组件和服务都只是类,带有标记其类型并提供元数据的装饰器,告诉 Angular 如何使用它们。

您可以在www.webcomponents.org/introduction了解有关 Web 组件的更多信息。

Angular 的主要构建模块

使用 Angular 框架创建的每个应用程序都有五个非常重要的连接到彼此的点,并建立了应用程序的基本架构:

  • 模块:使用装饰器@NgModule

  • 服务:使用装饰器@Injectable

  • 组件:使用装饰器@component

  • 模板:带有data-bind和指令的视图

  • 路由:将 URL 路径设置为视图

让我们以一个简单的博客页面作为 Angular 应用程序来看待,使用组件构建:

Angular 组件

前述图表说明了组件如何用于构建一个简单的应用程序。

前述图表与 Angular 应用程序的五个基本概念的比较如下:

  • 一个模块:blog.module.ts

  • 一个页面组件:blog.component.ts

  • 博客页面的路由

  • 加载博客文章的服务

还有一些其他组件,如HeaderPostPagination

请注意,Header 组件属于应用程序的主模块(在本例中为AppModule),而 Post 和 Pagination 组件属于BlogModule的一部分。

随着我们在本章中的深入,我们将更仔细地研究模块和组件之间的关系。现在,我们将看一下组件的生命周期。

组件生命周期

在 Angular 组件的生命周期中,在实例化后,组件从开始到结束都会运行一条明确的执行路径。最基本的理解方式是通过观察以下代码:

export class HelloComponent implements OnInit, OnDestroy {
   constructor() { }

   ngOnInit() {
... Some code goes here
}
ngOnDestroy() {
... Some code goes here
}
}  

在上面的例子中,您可以看到名为ngOnInit()ngOnDestroy的方法;这些名称非常直观,向我们展示了我们有一个开始和一个结束。ngOnInit()方法是通过其OnInit接口实现的,ngOnDestroy()方法也是如此。正如您在前一章中看到的,TypeScript 中的接口非常有用 - 这里也不例外。

在下图中,我们将看一下我们可以在组件上实现的主要接口。在图中,在Constructor()方法之后,有八个接口(也称为钩子);每个接口在特定时刻负责一件事:

Angular 组件生命周期您可以在官方的 Angular 文档中了解每个接口的更多信息angular.io/guide/lifecycle-hooks

我们不会在本章逐一描述接口,以免给您带来过多负担,但在本书的过程中,我们将在我们构建的应用程序中使用它们。此外,上述链接包含了关于每个接口和钩子的详细信息。

安装工具 - Git,Angular CLI 和 VS Code 插件

从本章到本书的结束,我们将采用 VS Code 文本编辑器 - 它是免费的,轻量级的,非常适合创建 Web 应用程序。

此外,对于源代码使用版本控制系统非常重要;这将帮助我们跟踪代码库中的所有更改。

接下来,我们将介绍 Git 源代码控制。

安装 Git

作为对 Git 的简单快速介绍,我们可以描述如下。

Git 是一个文件版本控制系统。通过使用它,我们可以开发项目,让许多人可以同时贡献,编辑和创建新文件,使它们可以存在而不会被覆盖。

在使用 Git 时非常常见的情况是同时在云中使用服务(如 GitHub 或 Bitbucket)来存储代码,以便我们可以共享它。

此外,几乎所有的开源项目(框架和库)今天都在 GitHub 上。因此,您可以通过报告错误,甚至发送代码和建议来做出贡献。

如果您是开发人员,但尚未拥有 GitHub,那么您已经晚了 - 现在是开始使用它的时候。因此,让我们安装 Git。

转到 git-scm.com/downloads 并下载并安装适用于您平台的 Git。

安装后,打开您的终端并输入以下命令:

git --version

您必须看到已安装在您系统上的当前版本。

此外,git help命令非常有用,列出所有可用的命令。

您可以在 git-scm.com/book/en/v2/Getting-Started-Git-Basics 上阅读有关 Git 基础知识的更多信息。

安装 Angular CLI

在框架的世界中,无论使用哪种语言,我们经常会发现可以帮助我们进行日常软件开发的工具,特别是在有重复任务时。

Angular CLI 是一个命令行界面,可以以非常高效的方式创建、开发和维护 Angular 应用程序。它是由 Angular 团队自己开发的开源工具。

通过使用 Angular CLI,我们能够创建整个 Angular 应用程序的基本结构,以及模块、组件、指令、服务等。它有自己的开发服务器,并帮助我们构建应用程序。

现在,是时候安装它了:

  1. 打开您的终端并输入以下命令:
npm install -g @angular/cli@latest

安装后,您将在终端中看到以下输出:

+ @angular/cli@1.7.3 added 314 packages, removed 203 packages, updated 170 packages and moved 7 packages in 123.346s

删除和更新的软件包数量以及 Angular CLI 版本可能会有所不同。不用担心。

  1. 您可以使用以下命令删除旧版本的 Angular CLI 并安装最新版本:
npm uninstall -g angular-cli
npm cache verify
npm install -g @angular/cli@latest

如果您在尝试在 Windows 机器上更新 Angular CLI 版本时遇到一些npm问题,您可以查看 docs.npmjs.com/troubleshooting/try-the-latest-stable-version-of-npm#upgrading-on-windows 获取信息。

请注意,上述命令将在您的环境/机器上全局安装 Angular CLI。通常,当我们使用 Angular 框架和 Angular CLI 进行开发时,我们会看到关于版本差异的警告消息。这意味着,即使您在您的环境中安装了最新版本的 Angular CLI,Angular CLI 也会检查当前项目中使用的版本,并将其与您的机器上安装的版本进行比较,并使用当前项目版本。

当您在第三方项目上工作并需要保持全局安装在您的机器上的 Angular CLI 与node_modules项目文件夹中安装的本地项目版本之间的依赖一致性时,这非常有用。

  1. 在您当前的 Angular 项目中,输入以下命令:
rm -rf node_modules
npm uninstall --save-dev angular-cli
npm install --save-dev @angular/cli@latest
npm install

与我们书中使用的其他命令一样,Angular CLI 有一个名为ng help的命令。通过它,我们可以访问大量的选项。

其中一个命令在我们使用 Angular 开发应用程序并需要在官方文档中查询内容时特别有用,而无需离开终端。

  1. 返回您的终端并输入以下命令:
ng doc HttpClient

上述命令将在您的默认浏览器中打开HttpClient文档 API,使用 angular.io/api?query=HttpClient。因此,您可以将ng doc命令与您想要搜索的 API 中的任何内容结合使用。

现在我们已经拥有了开始使用 Angular CLI 开发 Web 应用程序所需的一切,但在深入构建示例应用程序之前,我们将使用一些非常有用的工具更新我们的工具包。

安装 VS Code Angular 插件

正如前几章所述,VS Code 文本编辑器是使用 JavaScript 和 TypeScript 开发 Web 应用程序的绝佳 IDE,Angular 也是如此。

在本节中,我们将看一些扩展(也称为插件),这些扩展可以帮助我们进行开发。

让我们来看看软件包名称和存储库 URL:

  • Angular Language Servicegithub.com/angular/vscode-ng-language-service。由官方 Angular 团队提供,此扩展可帮助我们在模板文件和模板字符串中进行补全,并为模板和 Angular 注释提供诊断。

  • Angular v5 Snippetsgithub.com/johnpapa/vscode-angular-snippets。扩展名为 Angular v5;GitHub 项目存储库没有指定名称。因此,我们可以期望从插件作者那里获得未来版本的 Angular 的代码片段。这是一个强大的工具,可以帮助我们几乎在 Angular 应用程序中创建任何东西;您可以在 GitHub 存储库中看到完整的列表。

  • Angular Supportgithub.com/VismaLietuva/vscode-angular-support

转到并从中查看定义:

interpolation {{ someVar }}
input [(...)]="someVar"
output (...)="someMethod"
templateUrl or styleUrls in @Component decorator
component <some-component></some-component>

最后但同样重要的是,我们建议您使用 GitLens 插件。这个扩展非常重要,因为它帮助我们在 Git 存储库中可视化我们的代码,同时还提供与 GitHub 或 Bitbucket 的集成。

增强内置于 Visual Studio Code 中的 Git 功能。

– Gitlens

  • 您可以探索存储库和文件历史记录的导航

  • 您还可以探索提交并可视化分支、标签和提交之间的比较

  • 有一个作者代码镜头,显示文件顶部和/或代码块上最近的提交和作者数量

  • GitLens 插件gitlens.amod.io/。这个扩展非常重要,因为它帮助我们在 Git 存储库中可视化我们的代码,同时还提供与 GitHub 或 Bitbucket 的集成。

此外,还可以通过 IDE 本身安装任何扩展。要做到这一点,请按照以下步骤操作:

  1. 打开 VS Code。

  2. 单击左侧边栏上的最后一个图标;您可以在以下截图中看到它:

VS Code 扩展安装

只需在搜索输入字段中键入要搜索的内容,然后单击安装。

现在,我们已经拥有了开始开发 Angular 应用程序所需的一切。在下一节中,我们将看看如何使用 Angular CLI 创建 Angular 应用程序。

创建一个简单的 Angular 应用程序

在本章中,我们将涵盖使用 Angular 框架和 Angular CLI 开发 Web 应用程序的所有要点。现在,是时候接触代码并从头到尾开发一个应用程序了。

在这个示例项目中,我们将开发一个简单的前端应用程序来消耗 API 的数据并在屏幕上显示它 - 类似于一个简单的博客。打开您的终端并键入以下命令:

ng new chapter03 --routing

请注意,--routing标志是可选的,但由于我们的下一个示例将使用路由,因此最好在启动应用程序时使用该标志。安装了 Angular CLI 后,您应该在终端上看到以下消息:

Testing binary
Binary is fine
added 1384 packages in 235.686s
You can `ng set --global packageManager=yarn`.
Project 'chapter03' successfully created.

Angular 应用程序的结构

现在我们已经创建了我们的应用程序,让我们检查一些重要的文件。尽管这些文件已经设置好并准备好使用,但在现实世界的应用程序中,我们经常需要添加设置,甚至其他模块。

在 VS Code 中打开chapter03文件夹;您将在 VS Code 资源管理器选项卡中看到以下屏幕:

Angular 项目结构

因此,在/src/app文件夹中,除了服务(我们很快将看到)外,我们还有 Angular 应用程序的五个主要块:

app.routing.module.ts 路由
app.component.css 样式表
app.component.html 模板
app.component.spec.ts 测试
app.component.ts @Component
app.module.ts @NgModule

package.json 文件

package.json文件在使用 Node.js 模块的 Web 应用程序中非常常见。如今,它经常出现在前端应用程序中,除了使用 Node.js 的服务器端应用程序。对于 Angular 框架来说,它也不例外;这是新版本 Angular 的一个巨大优势,因为我们只能导入对应用程序非常必要的模块,从而减小了大小和构建时间。让我们看一下package.json文件的内容。我们在每个重要部分之前添加了一些注释:

{
"name": "chapter03",
"version": "0.0.0",
"license": "MIT",
// Npm commands, based on Angular/Cli commands, including: test and     build.
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build --prod",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
 },
"private": true,
// Dependencies to work in production, including:
@angular/core, @angular/common, @angular/route and many more. "dependencies":{
...
},
//  Dependencies only in development environment, including modules for test, TypeScript version, Angular/Cli installed locally and others.  "devDependencies": { ...
}
} 

当我们安装新模块时,此文件会自动更改。而且,我们经常在标签脚本内添加一些命令,正如您将在接下来的章节中看到的那样。您可以在官方 npm 文档的docs.npmjs.com/files/package.json中阅读更多关于package.json文件的信息。

Dotfiles - .editorconfig,.gitignore 和.angular-cli.json

Dotfiles 是以点开头的配置文件;它们始终在项目的后台,但它们非常重要。它们用于自定义您的系统。名称 dotfiles 源自类 Unix 系统中的配置文件。在 Angular 项目中,我们将看到其中三个文件:

  • .editorconfig:此文件配置文本编辑器以使用特定的代码样式,以便项目保持一致,即使它由多人和多种文本编辑器编辑。

  • .gitignore:顾名思义,它会忽略确定的文件夹和文件,以便它们不被源代码控制跟踪。我们经常发现node_modulesdist文件夹不需要版本控制,因为它们在每次安装应用程序或运行构建命令时都会生成。

  • .angular-cli.json:存储项目设置,并在执行构建或服务器命令时经常使用。在单个项目中可能有几个 Angular 应用程序。让我们看一些细节并检查.angular-cli.json

{
    "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
    "project": {
    "name": "chapter03"
    },
    // Here we determinate the projects, for this example we have     only one app.
    "apps": [
    {
    "root": "src",
    "outDir": "dist",
    "assets": [
    "assets",
    "favicon.ico"
    ],
    "index": "index.html",
    "main": "main.ts",
    "polyfills": "polyfills.ts",
    "test": "test.ts",
    "tsconfig": "tsconfig.app.json",
    "testTsconfig": "tsconfig.spec.json",
    "prefix": "app",
    "styles": [
    "styles.css"
    ],
    "scripts": [],
    "environmentSource": "environments/environment.ts",
    // Configuration for both environment, developing and production
    "environments": {
    "dev": "environments/environment.ts",
    "prod": "environments/environment.prod.ts"
    }
    }
    ],
    // Configuration for end to end tests and unit tests
    "e2e": {
    "protractor": {
    "config": "./protractor.conf.js"
    }
    },
    "lint": [
    {
    "project": "src/tsconfig.app.json",
    "exclude": "**/node_modules/**"
    },
    {
    "project": "src/tsconfig.spec.json",
    "exclude": "**/node_modules/**"
    },
    {
    "project": "e2e/tsconfig.e2e.json",
    "exclude": "**/node_modules/**"
    }
    ],
    "test": {
    "karma": {
    "config": "./karma.conf.js"
    }
    },
    // Stylesheet configiration, for this example we are using CSS
    "defaults": {
    "styleExt": "css",
    "component": {}
    }
}

环境

src/environments文件夹中,我们找到两个配置文件。一个称为environment.prod.ts,另一个是environment.ts。Angular CLI 将根据我们使用的命令来决定使用哪一个;例如,考虑以下命令:

 ng build --env = prod 

如果我们使用它,那么 Angular 将使用environment.prod.ts文件,对于其他命令,比如ng serve,它将使用environment.ts。这非常有用,特别是当我们有一个本地 API 和一个在production中时,使用不同的路径。

两个文件几乎具有相同的代码;请参阅environment.prod.ts,如下所示:

export const environment = {
    production: true
};

environment.ts文件如下:

export const environment = {
    production: false
};

请注意,在这个第一阶段,true(在生产中)和false(在开发中)是这两个文件之间唯一的区别。显然,除了我们提到的文件之外,Angular 应用程序中还有许多其他文件,它们都非常重要。但是,现在让我们专注于这些。别担心;在本书的过程中,我们将详细了解更多内容,在开发我们的示例应用程序时。现在,我们将专注于创建本章中使用的简单示例。

运行示例应用程序

现在我们已经启动了我们的项目,我们将运行内置的 Angular CLI 服务器,以查看我们的应用程序的外观:

  1. 在项目根目录中打开 VS Code 到chapter03文件夹。

  2. 在这个例子中,我们将使用集成终端进行编码;为此,请点击顶部菜单中的view,然后点击Integrated Terminal

  3. 在终端中键入以下命令:

npm start

您将看到类似以下的消息:

 ** NG Live Development Server is listening on localhost:4200, open  
 your  
 browser on http://localhost:4200/ **
 Date: xxxx 
 Hash: xxxx
 Time: 16943ms
 chunk {inline} inline.bundle.js (inline) 3.85 kB [entry] [rendered]
 chunk {main} main.bundle.js (main) 20.8 kB [initial] [rendered]
 chunk {polyfills} polyfills.bundle.js (polyfills) 549 kB [initial]  
 [rendered]
 chunk {styles} styles.bundle.js (styles) 41.5 kB [initial]  
 [rendered]
 chunk {vendor} vendor.bundle.js (vendor) 8.45 MB [initial] 
 [rendered]
  1. 在幕后,Angular CLI 将使用 webpack 模块管理器。在本书的后面,您将看到如何导出和自定义 webpack 文件。

  2. 现在,转到http://localhost:4200并检查结果;您将看到我们之前创建的样板应用程序的欢迎页面。您可以在src/app/app.component.html中找到这个页面的代码 - 这是我们的模板。

现在,是时候向我们的应用程序添加一个新模块了。

添加新模块

在这个例子中,我们将演示如何使用 Angular CLI 构建应用程序。即使在这个非常基本的例子中,我们也将涵盖以下几点:

  • 如何组织一个 Angular 应用程序

  • 创建模块

  • 创建服务

  • 模板数据绑定

  • 在生产环境中运行应用程序

现在,让我们创建一个显示啤酒列表的模块:

  1. 打开 VS Code,在集成终端内输入以下命令:
ng g module beers

请注意,命令ng g moduleng generate module <module-name>的快捷方式,这个命令只是创建模块;我们需要添加路由、组件和模板,并且在app文件夹的根目录的app.modules.ts中导入beers模块。上述命令将在我们的项目中生成以下结构和文件内容:src/app/beers/beers.module.tsbeers.module.ts的内容如下:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
    @NgModule({
    imports: [
    CommonModule
    ],
    declarations: []
    })
export class BeersModule { }

这是一个非常简单的样板代码,但非常有用。现在,我们将添加缺失的部分。

  1. beers模块添加到您的app模块;打开app.module.ts并用以下行替换代码:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { BeersModule } from './beers/beers.module';
    @NgModule({
    declarations: [
        AppComponent
    ],
    imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule,
    BeersModule
    ],
    providers: [],
    bootstrap: [AppComponent]
})
export class AppModule { }

请注意,我们导入了BeersModule并将其添加到imports数组中。

添加新组件

现在,我们需要一个组件来显示啤酒列表,因为我们刚刚创建了一个名为Beers的模块。稍后,您将看到如何使用 API 和 Angular 服务来加载啤酒列表;现在,我们将专注于创建我们的组件。

在根文件夹内,并在集成的 VS Code 终端中,输入以下命令:

ng g component beers

前面的命令将生成以下结构:

BeersModuleComponent文件已经创建。现在我们有了我们的模块、模板和组件文件。让我们添加一个新的路由。

添加新路由

如您之前所见,路由是每个 Web 应用程序的一部分。现在,我们将添加一个新的路由,以便我们可以访问我们的beers模块的内容。打开src/app/app-routing.module.ts并用以下代码替换:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { AppComponent } from './app.component';
import { BeersComponent } from './beers/beers.component';
const routes: Routes = [
    { path: '', redirectTo: 'beers', pathMatch: 'full' },
    { path: 'beers', component: BeersComponent }
];
@NgModule({
    imports: [RouterModule.forRoot(routes)],
    exports: [RouterModule]
})
export class AppRoutingModule { }

请注意,我们只是将新路由添加到现有的路由文件中(在这种情况下是app.routing.module.ts),因为这个例子非常简单。但是,在更大的应用程序中,建议为每个应用程序模块创建单独的路由文件。

创建一个 Angular 服务

Angular 服务用于处理数据;它可以是内部数据(从一个组件到另一个组件)或外部数据,比如与 API 端点通信。几乎所有使用 JavaScript 框架的前端应用程序都使用这种技术。在 Angular 中,我们称之为服务,并且我们使用一些内置在 Angular 框架中的模块来完成任务:HttpClientHttpClientModule

让我们看看 Angular CLI 如何帮助我们:

  1. 打开 VS Code,在集成终端内输入以下命令:
ng g service beers/beers

上述命令将在beers文件夹中生成两个新文件:

beers.service.spec.tsbeers.service.ts

  1. 将新创建的Service作为依赖提供者添加到beers.module.ts。打开src/app/beers/beers.module.ts并添加以下行:
import { BeersService } from './beers.service'; @NgModule({
    providers: [BeersService] })

在 VS Code 中,我们有导入模块支持,所以当您开始输入模块的名称时,您将看到以下帮助屏幕:

最终的beers.module.ts代码将如下所示:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BeersComponent } from './beers.component';
import { BeersService } from './beers.service';
@NgModule({
    imports: [
        CommonModule
    ],
    declarations: [BeersComponent],
    providers: [BeersService
    ]
})
export class BeersModule { }

现在,是时候使用服务连接到 API 了。为了尽可能接近真实应用程序,我们将在这个例子中使用一个公共 API。在接下来的步骤中,我们将有效地创建我们的服务并将数据绑定到我们的模板上。

在这个例子中,我们将使用免费的punkapi.com/ API:

  1. 打开beers.service.ts,并用以下行替换代码:
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/throw';
import { catchError } from 'rxjs/operators';
@Injectable()
export class BeersService {
    private url = 'https://api.punkapi.com/v2/beers?';
    constructor(private http: HttpClient) { }
/**
* @param {page} {perpage} Are Page number and items per page
*
* @example
* service.get(1, 10) Return Page 1 with 10 Items
*
* @returns List of beers
*/
    get(page: number, per_page: number) {
        return this.http.get(this.url + 'page=' + page +
         '&per_page=' + per_page)
        .pipe(catchError(error => this.handleError(error)));
    }

    private handleError(error: HttpErrorResponse) {
        return Observable.throw(error);
    }
}

现在,我们需要告诉组件我们需要使用这个服务来加载数据并将其传输到我们的模板中。

  1. 打开src/app/beers/beers.component.ts,并用以下代码替换代码:
import { Component, OnInit } from '@angular/core';
import { BeersService } from './beers.service';
@Component({
    selector: 'app-beers',
    templateUrl: './beers.component.html',
    styleUrls: ['./beers.component.css']
})
export class BeersComponent implements OnInit {
    public beersList: any [];
    public requestError: any;
    constructor(private beers: BeersService) { }
    ngOnInit() {
        this.getBeers();
    }
    /**
    * Get beers, page = 1, per_page= 10
    */
    public getBeers () {
        return this.beers.get(1, 20).subscribe(
            response => this.handleResponse(response),
            error => this.handleError(error)
        );
    }
    /**
    * Handling response
    */
    protected handleResponse (response: any) {
        this.requestError = null;
        return this.beersList = response;
    }
    /**
    * Handling error
    */
    protected handleError (error: any) {
        return this.requestError = error;
    }
}

模板数据绑定

现在我们有了一个连接到 API 端点并接收 JSON 文件的服务,让我们对我们的视图进行一些小的更改,即 Angular 世界中称为模板的视图。模板是module文件夹中的 HTML 文件:

  1. 打开src/app/app.component.html,并删除<router-outlet></route-outlet>标签之前的所有代码。

  2. 打开src/app/beers/beers.component.html,并在beers工作段落之后添加以下代码:

<div class="row">
    <div class="col" href="" *ngFor="let item of beersList">
        <figure>
            <img [src]="item.image_url" [alt]="item.name" />
        <figcaption>
            <h1>{{item.name}}</h1>
                <p>{{item.tagline}}</p>
        </figcaption>
        </figure>
    </div> </div>

请注意,我们使用花括号模板标签({{}})和*ngFor指令来显示我们的数据。让我们看一些 Angular 数据绑定类型:

{{ some.property }} One way Binding
[(ngModel)]="some.value" Two way Binding (click)="showFunction($event)" Event Binding
  1. 现在,我们需要为beers.component.html添加一些样式;打开src/app/beers/beers.component.css,并添加以下代码:
body {
    margin: 40px;
}
.row {
    display: grid;
    grid-template-columns: 300px 300px 300px;
    grid-gap: 10px;
    background-color: #fff;
    color: #444;
}
.col {
    background-color: #d1d1d1;
    border-radius: 5px;
    padding: 10px;
}
figure {
    text-align: center;
}
img {
    height:250px;
}

我们现在非常接近完成我们的示例应用程序。最后一步是构建我们的应用程序并查看最终结果。

简单部署

现在我们已经准备好了一切,让我们看看如何构建我们的应用程序。

首先,我们将在更改后查看应用程序:

  1. 打开 VS Code,单击顶部菜单栏中的视图,然后单击集成终端。

  2. 在您的终端中输入以下命令:

npm start
  1. 打开您的默认浏览器,转到http://localhost.com:4200/beers

  2. 恭喜;您应该看到以下截图:

请注意,我们正在使用npm start命令后面的ng serve命令进行开发。

现在,让我们使用命令构建应用程序,并检查结果:

  1. 返回 VS Code,然后输入Ctrl + C停止服务器。

  2. 输入以下命令:

npm run build

上述命令将准备应用程序进行生产;Angular CLI 将为我们完成所有繁重的工作。现在,我们在chapter03根目录下有一个文件夹,如下截图所示:

dist 文件夹

如您所见,我们的整个应用程序都在这个文件夹中,尽可能地进行了优化;但是,要查看内容,我们需要一个 Web 服务器。在本例中,我们将使用http-server节点包,这是一个非常有用的 Node.js 模块,可以将特定目录放在简单的 Web 服务器上。您可以在www.npmjs.com/package/http-server找到有关 http-server 的更多信息:

  1. 返回 VS Code 和集成终端,输入以下命令:
npm install http-server -g
  1. 仍然在集成终端中,输入以下命令:
cd dist && http-server -p 8080
  1. 您将在终端中看到以下消息:
 Starting up http-server, serving ./
 Available on:
 http://127.0.0.1:8080
 http://192.168.25.6:8080
 Hit CTRL-C to stop the server

这意味着一切进行顺利,您现在可以在浏览器中访问dist文件夹的内容了。

  1. 打开您的默认浏览器,转到http://localhost.com:8080/beers

我们完成了;现在,让我们使用一些 Git 命令将我们在本地存储库中chapter03文件夹中所做的一切保存起来。这一步对于接下来的章节并不是必需的,但强烈建议这样做。

  1. chapter03文件夹中打开您的终端,并输入以下命令:
git add .git commit -m "chapter03 initial commit"
  1. 在上一个命令之后,您将在终端中看到以下输出:
 [master c7d7c18] chapter03 initial commit
 10 files changed, 190 insertions(+), 24 deletions(-) rewrite  
 src/app/app.component.html (97%)
 create mode 100644 src/app/beers/beers.component.css
 create mode 100644 src/app/beers/beers.component.html
 create mode 100644 src/app/beers/beers.component.spec.ts
 create mode 100644 src/app/beers/beers.component.ts
 create mode 100644 src/app/beers/beers.module.ts
 create mode 100644 src/app/beers/beers.service.spec.ts
 create mode 100644 src/app/beers/beers.service.ts

总结

好了,我们已经到达了另一章的结尾,现在您应该了解如何使用 Angular 创建应用程序。在本章中,我们涵盖了使 Angular 成为强大框架的主要要点。您可以直接从 GitHub 下载我们在本章中使用的代码示例,网址为github.com/PacktPublishing。在下一章中,我们将深入了解后端 API。

第四章:构建基线后端应用程序

现在,我们将开始构建我们应用程序的基线。在本章中,我们将使用 RESTful 架构创建一个 Laravel 应用程序。正如我们在上一章中看到的,Laravel 将为我们提供构建坚实和可扩展的应用程序所需的基础设施。

我们将更仔细地看一些我们在第一章中简要提到的点,理解 Laravel 5 的核心概念,例如使用 Docker 容器来配置我们的环境,以及如何保持我们的数据库始终填充,即使使用 MySQL Docker 容器。

正如我们之前提到的,完全可以在开发环境中使用不同的配置,并且我们在第一章中提到了一些方法,理解 Laravel 5 的核心概念。然而,我们强烈建议您使用 Docker。

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

  • 关于 Laravel 与 Docker 的一些额外说明

  • 使用 PHP Composer 来搭建一个 Laravel 应用程序

  • 设置 MySQL 数据库

  • 迁移和数据库种子

  • 使用 Swagger 框架进行 API 文档化

关于 Laravel 与 Docker 的一些额外说明

在本节中,我们将使用我们在第一章中创建的相同基础设施,理解 Laravel 5 的核心概念。使用PHPDocker.io生成器,我们将对其进行自定义,以便更多地了解 Docker 容器内部发生了什么。

因此,让我们进行更详细的解释,没有比亲自动手更好的了。

创建 Docker Compose 基础

首先,我们将为应用程序创建基础(Dockerfiledocker-compose)文件,与我们在第一章中所做的方式不同。相反,我们将手动创建文件,但基于我们在第一章中使用的文件,理解 Laravel 5 的核心概念

按照以下步骤进行

  1. 创建一个名为chapter-04的文件夹。

  2. chapter-04文件夹中,创建一个名为phpdocker的文件夹。

  3. phpdocker文件夹中,添加两个文件夹,一个名为nginx,另一个名为php-fpm

配置 nginx

现在,是时候为nginxphp-fpm服务器创建配置文件了,因此我们将使用 nginx 反向代理来为 Web 提供我们的 PHP 文件。

nginx文件夹中,创建一个名为nginx.conf的新文件,并添加以下代码:

server {
    listen 80 default;
    client_max_body_size 308M;
    access_log /var/log/nginx/application.access.log;
    root  /application/public;
    index index.php;
    if (!-e $request_filename) {
        rewrite ^.*$ /index.php last;
    }
    location ~ \.php$ {
        fastcgi_pass php-fpm:9000;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME
         $document_root$fastcgi_script_name;
        fastcgi_param PHP_VALUE
         "error_log=/var/log/nginx/application_php_errors.log";
        fastcgi_buffers 16 16k;
        fastcgi_buffer_size 32k;
        include fastcgi_params;
    }
}

上一个文件将端口80设置为我们的 Web 服务器的默认端口,并将端口9000设置为php-fpm,这意味着我们的 nginx 容器将通过端口9000php-fpm通信,并且为了与 Web 通信,将通过端口80使用公共视图。稍后,在docker-compose.yml中,我们将配置内部 Docker 容器端口到外部世界,这种情况下是我们的主机机器。

配置 php-fpm

为了配置php-fpm,请按照以下步骤进行:

  1. php-fpm文件夹中,创建一个名为Dockerfile的文件,并添加以下行:
FROM phpdockerio/php72-fpm:latest
WORKDIR "/application"
# Install selected extensions and other stuff
RUN apt-get update \
    && apt-get -y --no-install-recommends install php7.2-mysql
     libmcrypt-dev \
    && apt-get clean; rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
    /usr/share/doc/*

现在,是时候创建我们的覆盖php.ini文件了,这是我们可以手动覆盖运行在服务器上的 PHP 设置的PHP.ini文件。

在 Apache 服务器的情况下,仅在服务器启动时运行一次,在我们的情况下,我们使用 nginx 与 php-fpm。关于这一点,我们使用了fastCgi环境,并且该文件在每次服务器调用时都会被读取。

使用fastCgi环境而不是传统的 Apache 环境有一些优势:

  • 自适应过程增长

  • 基本统计信息(类似于 Apache mod_status

  • 具有优雅的启动/停止的高级进程管理

  • 启动具有不同uidgidchrootenvironmentphp.ini(替换safe_mode)的工作进程的能力

  • stdoutstderr创建日志

  • 紧急重启以防意外代码破坏(缓存)

  • 支持加速上传

  • 对您的 FastCGI 进行了几项改进

其他在服务器上使用 PHP 的方法如下:

  • Apache 模块(mod_php

  • CGI

  • FastCGI

  • PHP - FastCGI 进程管理器FPM

  • 命令行CLI

  1. php-fpm文件夹中,创建一个名为php-ini-overrides.ini的新文件,并添加以下代码:
upload_max_filesize = 300M
post_max_size = 308M
[Xdebug]
zend_extension=/usr/local/lib/php/extensions/no-debug-non-zts-20151012/xdebug.so
xdebug.remote_enable=1
xdebug.remote_autostart=1
xdebug.remote_host=111.111.11.1 # you must use your own IP address here
xdebug.remote_port=9009

请注意,我们这里只是设置 Xdebug,这是一个用于调试 PHP 应用程序的 PHP 扩展。

创建一个 docker-compose 配置文件

现在,是时候配置我们的组合文件并挂载我们应用程序中将使用的所有容器了。我们将逐步构建这个文件,所以首先要做的是创建文件:

  1. 在根目录下,chapter-04文件夹创建一个名为docker-compose.yml的文件。

  2. docker-compose.yml中,添加以下代码:

version: "3.1"
services:
mysql:
  image: mysql:5.7
  container_name: chapter-04-mysql
  working_dir: /application
  volumes:
    - .:/application
  environment:
    - MYSQL_ROOT_PASSWORD=123456
    - MYSQL_DATABASE=chapter-04
    - MYSQL_USER=chapter-04
    - MYSQL_PASSWORD=123456
  ports:
    - "8083:3306"

这里的第一个代码块是用来配置 MySQL 服务器的。我们使用 Docker 的官方 MySQL 镜像。我们设置了环境变量和端口,但请注意,在主机机器上,我们使用端口8083访问 MySQL,在容器内部,我们使用3306默认端口。在本章的后面,我们将看到如何将 MySQL 客户端连接到我们的 MySQL 容器。

您可以从官方 Docker 网站上找到更多信息,网址是:hub.docker.com/explore/store.docker.com/

请注意,我们只是为了示例使用了一个非常简单的密码。在生产环境中,我们将使用全局环境变量。

让我们添加一个新的代码块。

  1. 仍然在docker-compose.yml文件中,在第一个代码块之后添加以下代码:
webserver:
  image: nginx:alpine
  container_name: chapter-04-webserver
  working_dir: /application
  volumes:
    - .:/application
    - ./phpdocker/nginx/nginx.conf:/etc/nginx/conf.d/default.conf
  ports:
    - "8081:80"

在前面的代码中,使用内部端口 80 配置我们的nginx容器,就像我们之前看到的那样,并在我们的主机机器上使用端口8081。我们还将在容器内设置nginx.conf。在这里,我们使用nginx/alpine Docker 镜像。

您可以在这里阅读有关 alpine 镜像的更多信息,网址是:store.docker.com/images/alpine

  1. 最后但并非最不重要的是,让我们配置php-fpm。在 web 服务器配置块之后添加以下代码块:
php-fpm:
build: phpdocker/php-fpm
container_name: chapter-04-php-fpm
working_dir: /application
volumes:
- .:/application
- ./phpdocker/php-fpm/php-ini-overrides.ini:/etc/php/7.2/fpm/conf.d/99-overrides.ini

在这里,我们只是在 Docker 容器内的php-fpm配置目录中设置php-ini-overrides.ini

构建应用程序容器

现在,是时候检查一切是否按我们计划的那样工作了。让我们创建我们应用程序中将使用的容器。在第一章中,理解 Laravel 5 的核心概念,我们已经看到了一些非常有用的 Docker 命令,现在我们将再次使用它们。让我们看看。

首先,我们将停止前一章仍在运行的任何容器,或者您的机器上的任何容器。

不要忘记您需要在本地机器上启动并运行 Docker 守护程序。

  1. 打开您的终端窗口,输入以下命令:
docker ps -a

如果您是第一次在您的机器上使用 Docker,您将看到类似以下输出:

列出的 Docker 容器

但是,如果您之前在您的机器上使用过 Docker,前面的命令将列出您机器上的所有 Docker 容器。在这种情况下,请注意步骤 2,因为该命令将停止并删除您机器上的所有容器。

因此,如果您想保留以前的容器,我们建议您只停止并删除使用本书教程创建的容器,使用以下命令:

docker stop <containerId> 

您也可以使用以下命令来代替步骤 23中执行的命令:

docker rm <containerId> 
  1. 输入以下命令以停止所有容器:
docker stop $(docker ps -a -q)
  1. 通过输入以下命令来删除所有容器:
docker rm $(docker ps -a -q)

干得好!现在,我们可以创建我们需要运行应用程序的镜像。让我们测试我们在上一节中建立的设置。

  1. chapter-04文件夹中,键入以下命令:
docker-compose build

在终端窗口的末尾,我们将看到类似于以下的输出:

---> 5f8ed0da2be9 Successfully built 5f8ed0da2be9 Successfully tagged chapter-04_php-fpm:latest mysql uses an image, skipping

前面的输出告诉我们,我们创建了一个名为chapter-04_php-fpm:latest的新 Docker 镜像,现在,让我们创建应用程序容器。

  1. chapter-04文件夹中,在您的终端上,键入以下命令:
docker-compose up -d

前面命令的输出将给我们所有三个我们之前设置的 Docker 容器的状态,类似于以下输出:

---> 5f8ed0da2be9 Successfully built 5f8ed0da2be9 Successfully tagged chapter-04_php-fpm:latest mysql uses an image, skipping
  1. 现在,我们可以使用以下命令检查新创建的容器:
docker ps -a

终端上的输出将非常类似于以下消息:

Docker 容器正在运行

请注意,我们在chapter-04文件夹中还没有任何应用程序代码,因此,如果我们尝试使用http://localhost:8081/地址访问服务器,我们将看到一个文件未找到的消息。这是完全预期的,因为我们实际上还没有在我们的服务器上运行任何应用程序。

使用 PHP Composer 来创建 Laravel 应用程序的脚手架

我们已在我们的服务器上创建了一个坚实的基础。我们使用的 PHP 镜像已经具有 Laravel 运行应用程序所需的所有依赖项,包括 Composer。

因此,我们将使用php-fpm容器内的 Composer,而不是使用我们在机器上全局安装的 Composer。

这是避免版本冲突的最安全方式。让我们检查一下php-fpm容器内有什么:

  1. 打开您的终端窗口,并键入以下命令:
docker-compose exec php-fpm bash
  1. 现在,我们在php-fpm bash/终端中,让我们使用以下命令检查 Composer 版本:
composer --version
  1. 我们将在终端上看到以下输出:
Composer version 1.6.3

恭喜!我们已经能够配置我们的所有环境,并且准备开始构建我们的应用程序。

创建应用程序脚手架

为了保持本书应用程序与您将使用示例代码的时刻之间的一致性,我们将修复将在您的环境中安装的 Laravel 版本。

因此,让我们继续以下步骤:

  1. 打开您的终端窗口,并键入以下命令:
composer create-project laravel/laravel=5.6.12 project --prefer-dist

在撰写本书时,我们已安装了 Laravel 5.6.12 版本。尽管我们应该没有问题安装更新的版本,但我们强烈建议您保持在 5.6 版本。*。

使用前面的命令后,您将在终端窗口上看到以下消息:

Generating optimized autoload files > Illuminate\Foundation\ComposerScripts::postAutoloadDump > @php artisan package:discover Discovered Package: fideloper/proxy Package: laravel/tinker Discovered Package: nunomaduro/collision Package manifest generated successfully. > @php artisan key:generate

这意味着一切都进行得很顺利。

请注意,我们在名为project的目录中创建了 Laravel 应用程序。这样,我们将拥有以下应用程序结构:

应用程序文件夹结构

请注意,我们已将 Laravel 应用程序的内容与 Docker 配置文件夹分开。这种做法是非常推荐的,因为我们可以在项目文件夹内进行任何类型的更改,而不会意外损坏任何 Docker 或docker-compose文件。

但是,由于这个小改动,我们需要调整docker-compose.yml文件,以适应新创建的路径。

  1. 打开docker-compose.yml,让我们调整php-fpm卷标的新路径,如以下代码块所示:
php-fpm:
  build: phpdocker/php-fpm
  container_name: chapter-04-php-fpm
  working_dir: /application
  volumes:
    - ./project:/application
    - ./phpdocker/php-fpm/php-ini-
    overrides.ini:/etc/php/7.2/fpm/conf.d/99-overrides.ini

运行应用程序

为了使我们刚刚做出的更改生效,我们需要停止并重新启动我们的容器:

  1. 在您的终端上,键入exit以退出php-fpm bash。

  2. 现在,在chapter-04文件夹的根目录中,仍然在终端中,键入以下命令:

docker-compose kill

您将看到以下输出消息:

Stopping chapter-04-webserver ... done Stopping chapter-04-mysql ... done Stopping chapter-04-php-fpm ... done
  1. 在您的终端上,键入以下命令以再次运行容器:
docker-compose up -d

现在,我们可以看到php-fpm容器已被重新创建,并将反映我们的更改:

Recreating chapter-04-php-fpm ... done Starting chapter-04-webserver ... done Starting chapter-04-webserver ... done

强烈建议您在对nginxphp-fpm服务器进行任何更改时重复此过程。

  1. 现在,让我们检查 Laravel 的安装和配置。打开您的默认浏览器,转到链接http://localhost:8081/

我们将在 Laravel 框架中看到欢迎屏幕,如下截图所示:

Laravel 欢迎屏幕

设置 MySQL 数据库

到目前为止,我们已经走了很长的路,为我们的 RESTful 应用程序建立了坚实的基础,尽管我们还必须采取一些步骤才能最终开始开发。

在这一步中,我们将为我们的应用程序配置 MySQL 数据库,并对我们的 Docker MySQL 容器进行一些更改,以使我们的应用程序的数据在我们断开连接或停止 MySQL 容器时仍然存在于我们的数据库中。

添加一个存储文件夹

存储 MySQL 数据的本地文件夹非常重要,因为我们的docker-compose.yml文件中没有包含任何已配置用于存储数据库中创建的数据的卷。

请记住,我们正在使用 Docker,我们的 MySQL 容器是基于数据库镜像的。这样,每当我们完成容器进程时,我们的数据库就会被删除,下次启动时,我们的数据库将为空。

打开docker-compose.yml文件,并在 MySQL 配置块的应用程序卷之后添加- ./storage-db:/var/lib/mysql,如下面的代码所示:

mysql:
    image: mysql:5.7
    container_name: chapter-04-mysql
    working_dir: /application
    volumes:
    - .:/application
    - ./storage-db:/var/lib/mysql

上面的代码将storage-db文件夹设置在我们的项目/机器上,以存储来自 MySQL 容器的所有 MySQL 数据。稍后在本节中,我们将看到这些更改的结果,但现在,让我们配置我们的数据库。

配置.env 文件

  1. 打开project文件夹根目录下的.env文件,并用以下行替换数据库配置:
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=chapter-04
DB_USERNAME=chapter-04
DB_PASSWORD=123456

让我们检查连接。

  1. 在终端窗口中,输入以下命令:
docker-compose exec php-fpm bash
  1. php-fpm bash 中,输入以下命令:
php artisan tinker
  1. 最后,输入以下命令:
DB::connection()->getPdo();
  1. 您将看到类似以下输出:
=> PDO {#760
inTransaction: false,
attributes: {
CASE: NATURAL,
ERRMODE: EXCEPTION,
AUTOCOMMIT: 1,
PERSISTENT: false,
DRIVER_NAME: "mysql",
SERVER_INFO: "Uptime: 2491 Threads: 1 Questions: 9 Slow queries: 0 Opens: 105 Flush tables: 1 Open tables: 98 Queriesper second avg: 0.003",
ORACLE_NULLS: NATURAL,
CLIENT_VERSION: "mysqlnd 5.0.12-dev - 20150407 - $Id: 38fea24f2847fa7519001be390c98ae0acafe387 $",
SERVER_VERSION: "5.7.21",
STATEMENT_CLASS: [
"PDOStatement",
],
EMULATE_PREPARES: 0,
CONNECTION_STATUS: "mysql via TCP/IP",
DEFAULT_FETCH_MODE: BOTH,
},
}

这意味着一切都进行得很顺利。恭喜!我们有了一个数据库。现在是时候生成我们的本地数据库文件夹storage-db了。

如果您仍然在php-fpm bash 中,请输入exit命令以退出并返回到终端。

  1. 在您的终端窗口中,输入以下命令:
docker-compose kill
  1. 让我们删除之前创建的 MySQL 容器:
docker-compose rm mysql
  1. 现在,让我们重新创建容器,使其具有更新并运行的更改。输入以下命令:
docker-compose up -d

您将在终端上看到以下输出:

Creating chapter-04-mysql ... done Starting chapter-04-php-fpm ... done Starting chapter-04-webserver ... done

请注意,MySQL 容器已经创建,并且storage-db文件夹已经正确配置。我们的项目将具有以下结构:

项目文件夹结构

使用 MySQL 外部客户端

仅使用命令行管理数据库可能并不容易,从视觉上讲也不容易。为了帮助我们进行此过程,所有数据库都有一个用于此任务的工具,MySQL 也不例外。

有几种开源和多平台工具可以帮助我们进行此过程,MySQL 也有自己的工具称为Workbench

您可以在官方 MySQL 文档的 Workbench 部分阅读更多信息www.mysql.com/products/workbench/

在本节中,我们将看到如何使用具有图形界面的工具来访问我们的 Docker 容器中的数据库。

  1. 前往dev.mysql.com/downloads/workbench/,选择您的平台,并按照安装步骤进行安装。

  2. 打开 Workbench,单击顶部的 Database 菜单,然后单击连接到数据库。

您将看到以下屏幕:

Workbench 连接屏幕

  1. 请记住,工作台正在主机机器上运行,而不是在 MySQL Docker 容器内部运行。向其中添加以下信息:

主机名:127.0.0.1 端口:8083 用户名:chapter-04 密码:123456

请注意,我们正在使用与 Laravel.env文件中相同的配置。

  1. 点击“确定”按钮。现在,我们连接到在 Docker 容器内运行的 MySQL 数据库。您将看到类似于以下屏幕截图的内容:

工作台欢迎界面

注意左侧模式面板。我们已经通过 Docker 容器创建了我们的数据库,并且准备好使用它。此时,它仍然是空的,如下面的屏幕截图所示:

模式左侧面板

迁移和数据库种子

现在,让我们练习一些我们在第一章中简要看到的命令,并以不同的方式创建我们的迁移和种子。

  1. chapter-04文件夹上打开您的终端窗口并键入以下命令:
docker-compose exec php-fpm bash
  1. 在容器根 bash 内,键入以下命令:
php artisan make:model Bike -m

请注意,我们使用了-m标志来创建迁移文件以及创建 Bike 模型。所以现在,我们的应用程序中有两个新文件:

  • project/app/Bike.php

  • project/database/migrations/XXXX_XX_XX_XXXXXX_create_bikes_table.php

创建迁移样板

正如我们之前所看到的,新文件只有 Laravel 引擎创建的样板代码。让我们向Bike模型和迁移文件添加一些内容。

  1. 打开project/app/Bike.php并在 Bike 模型函数内添加以下代码:
protected $fillable = [
    'make',
    'model',
    'year',
    'mods',
    'picture'
];
  1. 现在,我们需要向之前创建的迁移文件添加相同的属性。打开project/database/migrations/XXXX_XX_XX_XXXXXX_create_bikes_table.php并在up()函数内添加以下代码:
Schema::create('bikes', function (Blueprint $table) {
$table->increments('id');
$table->string('make');
$table->string('model');
$table->string('year');
$table->text('mods');
$table->string('picture');
$table->timestamps();
});

恭喜!您已经创建了我们的第一个迁移文件,现在是时候执行以下命令来填充我们的数据库了。

  1. 打开您的终端窗口并键入以下命令:
php artisan migrate

上一个命令的输出将类似于以下内容:

Migration table created successfully. Migrating: XXXX_XX_XX_XXXXXX_create_users_table Migrated: XXXX_XX_XX_XXXXXX_create_users_table Migrating: XXXX_XX_XX_XXXXXX_create_password_resets_table Migrated: XXXX_XX_XX_XXXXXX_create_password_resets_table Migrating: XXXX_XX_XX_XXXXXX_create_bikes_table Migrated: XXXX_XX_XX_XXXXXX_create_bikes_table

现在,在工作台模式下,我们可以看到我们的新表格由migrate命令填充,如下面的屏幕截图所示:

工作台模式面板

创建我们的第一个数据库种子

在前面的步骤中,我们基本上遵循了与第一章相同的程序,理解 Laravel 5 的核心概念,所以现在我们不会使用 Faker 库来创建我们的数据。Faker 是一个非常有用的工具,因为它在使用 Laravel 开发应用程序期间易于使用且快速创建数据。

在这个例子中,我们希望保持我们创建的数据与我们正在创建的应用程序更一致,因此我们将使用一个外部 JSON 文件,其中包含我们要插入到数据库中的数据。

  1. project/database文件夹内,创建一个名为data-sample的新文件夹。

  2. project/database/data-sample文件夹内,创建一个名为bikes.json的新文件并添加以下代码:

[{
  "id": 1,
  "make": "Harley Davidson",
  "model": "XL1200 Nightster",
  "year": "2009",
  "mods": "Nobis vero sint non eius. Laboriosam sed odit hic quia
    doloribus. Numquam laboriosam numquam quas quis.",
  "picture": "https://placeimg.com/640/480/nature
  }, {
  "id": 2,
  "make": "Harley Davidson",
  "model": "Blackline",
  "year": "2008",
  "mods": "Nobis vero sint non eius. Laboriosam sed odit hic quia
   doloribus. Numquam laboriosam numquam quas quis.",
  "picture": "https://placeimg.com/640/480/nature"
 }, {
  "id": 3,
  "make": "Harley Davidson",
  "model": "Dyna Switchback",
  "year": "2009",
  "mods": "Nobis vero sint non eius. Laboriosam sed odit hic quia
   doloribus. Numquam laboriosam numquam quas quis.",
  "picture": "https://placeimg.com/640/480/nature"
 }, {
  "id": 4,
  "make": "Harley Davidson",
  "model": "Dyna Super Glide",
  "year": "2009",
  "mods": "Nobis vero sint non eius. Laboriosam sed odit hic quia
  doloribus. Numquam laboriosam numquam quas quis.",
  "picture": "https://placeimg.com/640/480/nature"
 },{
  "id": 5,
  "make": "Harley Davidson",
  "model": "Dyna Wild Glide",
  "year": "2005",
  "mods": "Nobis vero sint non eius. Laboriosam sed odit hic quia
   doloribus. Numquam laboriosam numquam quas quis.",
  "picture": "https://placeimg.com/640/480/nature"
}]

请注意,我们保留了一些占位文本和图像路径。现在不要担心这个问题;在本书的后面,我们将使用我们的前端应用程序替换所有这些数据。

  1. 现在,是时候创建我们的种子文件了。在您的终端窗口中,键入以下命令:
php artisan make:seeder BikesTableSeeder

上一个命令在project/database/seeds文件夹内添加了一个名为BikesTableSeeder.php的新文件。

  1. 打开project/database/seeds/BikesTableSeeder.php并用以下代码替换其中的代码:
use Illuminate\Database\Seeder;
use App\Bike;
class BikesTableSeeder extends Seeder
    {
    /**
    * Run the database seeds.
    *
    * @return void
    */
    public function run()
    {
        DB::table('bikes')->delete();
        $json = File::get("database/data-sample/bikes.json");
        $data = json_decode($json);
        foreach ($data as $obj) {
        Bike::create(array(
            'id' => $obj->id,
            'make' => $obj->make,
            'model' => $obj->model,
            'year' => $obj->year,
            'mods' => $obj->mods,
            'picture'=> $obj->picture
        ));
        }
    }
}

请注意,在第一行中,我们使用了 Eloquent ORM 快捷方式函数(DB::table())来删除自行车表,并使用Bike::create()函数来创建我们的记录。在下一章中,我们将更深入地了解 Eloquent ORM,但现在让我们专注于创建我们的第一个种子。

  1. 打开project/database/seeds/DatabaseSeeder.php并在UsersTableSeeder注释之后添加以下代码行:
$this->call(BikesTableSeeder::class);

现在,是时候运行我们的 seed 并填充数据库了。我们可以以两种方式进行。我们可以单独运行BikeSeeder命令php artisan db:seed --class=BikesTableSeeder,也可以使用php artisan db:seed命令,这将运行我们应用程序中的所有 seed。

由于我们现在处于开发的开始阶段,我们将执行命令以加载所有 seed。

  1. 打开您的终端窗口并键入以下命令:
php artisan db:seed

在上一个命令的末尾,我们将在终端上看到一个成功的消息,Seeding: BikesTableSeeder。太棒了!现在,我们在chapter-04数据库上有了我们的第一条记录。

探索 Workbench 表视图

现在,我们将使用 Workbench 的可视界面来查看我们刚刚放入数据库中的数据。为此,请打开 Workbench 并执行以下步骤:

  1. 在右侧模式面板上,单击“Tables”菜单项。

  2. 右键单击 bikes 并单击选择行 - 限制 1000。

我们将在右侧看到一个新面板,如下截图所示:

Workbench 界面上的 Bike 表

请注意,我们现在在 bike 表中的数据库中有五条记录,这是我们在bike.json文件中创建的相同数据。

使用 Swagger 框架进行 API 文档编制

让我们休息一下,解决 RESTful 应用程序开发中一个非常重要的主题:如何使用 API 端点的文档。

尽管我们还没有创建任何控制器或路由来在浏览器中查看我们的 API 数据,但我们将介绍一个新工具,它将帮助我们开发我们的前端应用程序,称为 Swagger 框架。

Swagger 是一个开源的语言无关框架,用于描述、记录、消费和可视化 REST API。

如今,使用公共和私有 API 来创建前端 Web 应用程序非常常见,我们熟悉多个 API,如 Twitter、LinkedIn 等。

文档化您的应用程序是开发过程中的重要部分。每个 API 都需要进行文档编制,以便更容易地由内部团队或第三方开发人员使用和测试。

这样做的最简单方法是在开发过程的开始阶段。

您可以在官方网站上阅读有关 Swagger 框架的更多信息:swagger.io/

安装 L5-Swagger 库

在本节中,我们将使用 L5-Swagger 项目。将使用 Swagger-PHP 和 Swagger-UI 的包装器与 Laravel 5 框架一起使用:

  1. 仍然在您的终端窗口上,键入以下命令:
composer require "darkaonline/l5-swagger"

您可以在官方 GitHub 存储库上阅读有关 L5-Swagger 的更多信息:github.com/DarkaOnLine/L5-Swagger

在命令行的末尾,我们将看到以下输出:

Composer L5-Swagger 安装过程

  1. 打开project/config/app.php文件,并在ServiceProvider注释的末尾添加以下代码:
\L5Swagger\L5SwaggerServiceProvider::class
  1. 键入以下命令以发布该软件包:
php artisan vendor:publish --provider "L5Swagger\L5SwaggerServiceProvider"

上一个命令的输出将在我们的应用程序上创建一些新文件,如下截图所示:

L5-Swagger UI

创建应用程序 API 控制器

作为良好的实践方式,我们将在我们的应用程序中创建一个新的控制器,用作 API 文档的基础,并使用 Swagger 注释保存一些基本的 API 信息。

  1. 在您的终端窗口上,键入以下命令以创建一个新的控制器:
php artisan make:controller ApiController
  1. 打开project/app/Http/Controllers/ApiController.php并用以下注释替换Class ApiController comments
* Class ApiController
*
* @package App\Http\Controllers
*
* @SWG\Swagger(
* basePath="",
* host="localhost:8081",
* schemes={"http"},
* @SWG\Info(
* version="1.0",
* title="Custom Bikes",
* @SWG\Contact(name="Developer Contact", url="https://www.example.com"),
* )
* )
*/

生成和发布 API 文档

现在,是时候发布我们的文档并通过 Web 浏览器访问它了。因此,让我们按照以下步骤来做。

仍然在您的终端窗口上,输入以下命令:

php artisan l5-swagger:generate

干得好!现在,我们的 API 文档已经准备好实施了。

转到http://localhost:8081/api/documentation,您将看到类似以下截图的结果:

Swagger UI

Swagger 框架已经在我们的本地机器上运行起来了。

添加 Swagger 定义

Swagger 为使用 API 的我们的注释生成文档,这是框架本身的一种自我编写,通过标签,我们可以定义每个元素的作用。让我们开始构建我们的 API 文档。

第一步是为我们在之前创建的Bike模型添加一些定义。

  1. 打开project/app/Bike.php模型文件,并用以下代码替换原有代码:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
    /**
    * @SWG\Definition(
    * definition="Bike",
    * required={"make", "model", "year", "mods"},
    * @SWG\Property(
    * property="make",
    * type="string",
    * description="Company name",
    * example="Harley Davidson, Honda, Yamaha"
    * ),
    * @SWG\Property(
    * property="model",
    * type="string",
    * description="Motorcycle model",
    * example="Xl1200, Shadow ACE, V-Star"
    * ),
    * @SWG\Property(
    * property="year",
    * type="string",
    * description="Fabrication year",
    * example="2009, 2008, 2007"
    * ),
    * @SWG\Property(
    * property="mods",
    * type="string",
    * description="Motorcycle description of modifications",
    * example="New exhaust system"
    * ),
    * @SWG\Property(
    * property="picture",
    * type="string",
    * description="Bike image URL",
    * example="http://www.sample.com/my.bike.jpg"
    * )
    * )
    */
class Bike extends Model
{
        /**
        * The attributes that are mass assignable.
        *
        * @var array
        */
        protected $fillable = [
            'make',
            'model',
            'year',
            'mods',
            'picture'
        ];
}

前面的注释非常简单明了;我们只是描述了每个模型字段,并使用有用的示例设置了数据类型。

  1. 返回到您的终端窗口,并输入以下命令来生成文档:
php artisan l5-swagger:generate
  1. 现在,让我们检查我们的文档 URL 并看看发生了什么。转到http://localhost:8081/api/documentation,我们将看到我们的第一个模型已经被记录下来,如下面的截图所示:

Swagger UI

请注意,我们已经在一个对象内解释了所有的模型属性,每个属性都有数据类型、描述和示例。这是由于Swagger @SWG\Property的定义:

* @SWG\Property( * property="make", * type="string", * description="Company name", * example="Harley Davidson, Honda, Yamaha"

随着我们的应用程序的增长,我们将添加所有需要使用我们的 API 的文档。

您可以在以下链接找到有关 Swagger 可视化界面的更多信息:swagger.io/swagger-ui/

总结

我们已经来到了另一个章节的结束。我们在这里付出了很多努力,做了很多事情,比如配置 Docker 来维护我们将在 MySQL 数据库中创建的数据。我们还创建了一个大型应用程序并配置了数据库,并学会了如何使用 Workbench 查看应用程序加载的数据。

我们创建了我们的第一个模型及其迁移文件,并且还创建了一个种子来在我们的数据库中进行初始数据加载。最后,我们建立了一个坚实的基础来扩展我们的 RESTful API。

在下一章中,我们将更深入地讨论如何使用 Eloquent ORM 创建控制器、路由和表之间的关系,以及其他内容。

第五章:使用 Laravel 创建 RESTful API - 第 1 部分

在开始之前,让我们简要介绍一种称为 RESTful API 的软件开发标准。

应用程序编程接口(API)是一组用于访问基于互联网的应用程序的指令、例程和编程模式。这允许计算机或其他应用程序理解此应用程序中的指令,解释其数据,并将其用于与其他平台和软件集成,生成将由此软件或计算机执行的新指令。

通过这种方式,我们了解到 API 允许应用程序之间的互操作性。换句话说,这是客户端和服务器端之间的通信。

表述状态转移(REST)是 Web 架构的一种抽象。简而言之,REST 由原则、规则和约束组成,遵循这些原则、规则和约束可以创建一个具有明确定义接口的项目。

RESTful 服务中可用的功能可以从一组默认预定义的操作中访问或操作。这些操作使得可以使用 HTTP 协议从消息中创建(PUT)、读取(GET)、更改(POST)和删除(DELETE)资源。

尽管 Laravel 是一个 MVC 框架,但我们可以构建非常强大和可扩展的 RESTful 应用程序。

在本章中,您将学习如何使用 Laravel 框架的核心元素构建 RESTful API,例如控制器、路由和 Eloquent 对象关系映射(ORM)。主要,我们将涵盖以下主题:

  • 准备应用程序并了解我们正在构建的内容

  • Eloquent ORM 关系

  • 控制器和路由

准备应用程序并了解我们正在构建的内容

让我们使用在上一章中开始开发的应用程序来开始本节课。但是,在继续之前,我们将进行一些调整。首先,我们将把我们的代码添加到版本控制中。这样,我们就不会丢失在上一章中所做的进展。

  1. chapter-04文件夹中,创建一个名为.gitignore的新文件,并添加以下代码:
storage-db
.DS_Store
  • 有关忽略文件的更多信息,请参阅help.github.com/articles/ignoring-files

  • 如果您发现自己正在忽略文本编辑器或操作系统生成的临时文件,您可能希望添加一个全局忽略,而不是git config --global core.excludesfile '~/.gitignore_global'

  • 忽略storage文件夹的大小

先前的代码只是将storage-db文件夹添加到未跟踪的文件中。

  1. 让我们把更改添加到源代码控制中。在终端窗口内,输入以下命令:
git init

最后,让我们添加我们的第一个提交。

  1. 在终端内,输入以下命令:
git add .
git commit -m "first commit"

太棒了!我们的代码已经在 Git 源代码控制下了。

重构应用程序文件

现在是时候更改一些文件以适应chapter-05了:

  1. 复制chapter-04的所有内容,并将其粘贴到一个名为chapter-05的新文件夹中。

  2. 打开docker-compose.yml文件,并用以下行替换代码:

version: "3.1"
services:
 mysql:
 screenshot: mysql:5.7
 container_name: chapter-05-mysql
 working_dir: /application
 volumes:
 - .:/application
 - ./storage-db:/var/lib/mysql
 environment:
 - MYSQL_ROOT_PASSWORD=123456
 - MYSQL_DATABASE=chapter-05
 - MYSQL_USER=chapter-05
 - MYSQL_PASSWORD=123456
 ports:
 - "8083:3306"
 webserver:
 screenshot: nginx:alpine
 container_name: chapter-05-webserver
 working_dir: /application
 volumes:
 - .:/application
 - ./phpdocker/nginx/nginx.conf:
 /etc/nginx/conf.d/default.conf
 ports:
 - "8081:80"
 php-fpm:
 build: phpdocker/php-fpm
 container_name: chapter-05-php-fpm
 working_dir: /application
 volumes:
 - ./project:/application
 - ./phpdocker/php-fpm/php-ini-
 overrides.ini:/etc/php/7.2/fpm/conf.d/99-overrides.ini

请注意,我们更改了MYSQL_DATABASEMYSQL_USER,还将容器名称更改为符合chapter-05标题。

  1. 使用新的数据库信息编辑project/.env文件,如下所示:
DB_CONNECTION=mysql
 DB_HOST=mysql
 DB_PORT=3306
 DB_DATABASE=chapter-05
 DB_USERNAME=chapter-05
 DB_PASSWORD=123456
  1. 现在,删除storage-db文件夹。不用担心,我们稍后会使用docker-compose命令创建一个新的文件夹。

  2. 现在是时候提交我们的新更改了,但这次我们将以另一种方式进行。这次,我们将使用 Git Lens VS Code 插件。

  3. 打开 VS Code。在左侧边栏中,单击源代码控制的第三个图标。

  4. 在左侧边栏的消息框中添加以下消息Init chapter 05

  5. 在 macOSX 上按 Command+Enter,或在 Windows 上按 Ctrl+Enter,然后单击 Yes。

干得好。现在,我们可以用新的文件基线开始第五章

我们正在构建什么

现在,让我们稍微谈谈自从本书开始以来我们一直在构建的应用程序。

正如我们所看到的,到目前为止我们已经构建了很多东西,但是我们仍然不清楚我们在项目中做了什么。这是学习和练习 Web 应用程序开发的最佳方式。

很多时候,当我们第一次学习或做某事时,我们倾向于密切关注最终项目,在这一点上,我们急于完成我们开始做的事情,无法专注于建设过程和细节。

在这里,我们已经有了40%的项目准备就绪。然后,我们可以透露更多关于我们正在做什么的细节。

请记住,到目前为止,我们已经使用 Docker 准备了一个高度可扩展的开发环境,在我们的开发中安装了一些非常重要的工具,并学会了如何启动一个稳固的 Laravel 应用程序。

该应用将被称为 Custom Bike Garage,这是一种针对自定义摩托车文化爱好者的 Instagram/Twitter。在开发结束时,我们将拥有一个与以下线框截图非常相似的 Web 应用程序:

主页

前面的截图只是一个基本的应用程序主页,带有导航链接和一个呼吁行动的按钮:

摩托车列表页面

应用摘要

正如我们在前面的截图中所看到的,我们的应用程序有:

  • 一个主页,我们将称之为主页

  • 一个摩托车页面,我们将称之为摩托车列表页面

  • 一个摩托车详细页面,我们将称之为摩托车详情页面

  • 一个构建者页面,我们将称之为构建者列表页面

  • 一个构建者详细页面,我们将称之为构建者详情页面

  • 一个注册页面,我们将称之为注册页面

  • 一个登录页面,我们将称之为登录页面

  • 一个评分页面,用户可以在摩托车上投票

想象我们正在为一场展览会构建一个自定义摩托车应用程序。每个会议都有一个名称和客户级别。

用户可以注册,对最好的摩托车进行投票,并插入他们自己的摩托车。会议展示了一些知名摩托车制造商定制的摩托车,每辆摩托车都有许多自定义项目。

因此,为了完成应用程序的后端,我们还需要做以下工作:

  • BuilderItemGarageRating创建模型

  • BuilderItemGarageRating创建迁移文件

  • 种子数据库

  • BikeBuilderItemGarageRating创建控制器

  • 应用模型之间的关系

  • 使用资源表示关系

  • 创建基于令牌的身份验证

创建模型和迁移文件

让我们开始使用-m标志创建builders模型和迁移文件。就像我们在本书中之前做的那样,我们可以同时创建这两个文件:

  1. 打开您的终端窗口,键入以下命令:
php artisan make:model Builder -m
  1. 仍然在您的终端窗口上,键入以下命令:
php artisan make:model Item -m
  1. 仍然在您的终端窗口上,键入以下命令:
php artisan make:model Garage -m
  1. 仍然在您的终端窗口上,键入以下命令:
php artisan make:model Rating -m

步骤 1步骤 4将在我们的应用程序中产生以下新文件:

project/app/Builder.php project/database/migrations/XXXX_XX_XX_XXXXXX_create_builders_table.php project/app/Item.php project/database/migrations/XXXX_XX_XX_XXXXXX_create_items_table.php project/app/Garage.php project/database/migrations/XXXX_XX_XX_XXXXXX_create_garages_table.php project/app/Rating.php project/database/migrations/XXXX_XX_XX_XXXXXX_create_ratings_table.php

注意迁移文件名之前的XXXX_XX_XX_XXXXXX。这是文件创建时的时间戳。

在这一点上,我们可以在 VS Code 左侧面板上看到之前的六个模型,就像以下截图中一样:

左侧面板

请注意,我们已经在第四章中创建了Bike模型,并且默认情况下,Laravel 为我们创建了User模型。

  1. 现在,就像我们之前做的那样,让我们提交新创建的文件,并单击 VS Code 左侧面板上的源控制图标

  2. 在消息输入字段中键入以下文本:添加模型和迁移文件

  3. 在 macOSX 上按C**ommand + Enter,或在 Windows 上按C**trl + Enter,然后单击Yes按钮。

向迁移文件添加内容

现在,让我们创建我们迁移文件的内容。请记住,迁移文件是使用 Laravel 创建数据库方案的最简单和最快的方法:

  1. 打开project/database/migrations/XXXX_XX_XX_XXXXXX_create_builders_table.php并用以下代码替换内容:
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateBuildersTable extends Migration
{
    /**
    * Run the migrations.
    *
    * @return void
    */
    public function up()
    {
    Schema::create('builders', function (Blueprint $table) {
        $table->increments('id');
        $table->string('name');
        $table->text('description');
        $table->string('location');
        $table->timestamps();
        });
    }
    /**
    * Reverse the migrations.
    *
    * @return void
    */
    public function down()
    {
        Schema::dropIfExists('builders');
    }
}
  1. 打开project/database/migrations/XXXX_XX_XX_XXXXXX_create_items_table.php并用以下代码替换内容:
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateItemsTable extends Migration
{
    /**
    * Run the migrations.
    *
    * @return void
    */
    public function up()
    {
        Schema::create('items', function (Blueprint $table) {
        $table->increments('id');
        $table->string('type');
        $table->string('name');
        $table->text('company');
        $table->unsignedInteger('bike_id');
        $table->timestamps();
        });
    }
    /**
    * Reverse the migrations.
    *
    * @return void
    */
    public function down()
    {
        Schema::dropIfExists('items');
    }
}
  1. 注意 Bike 表的$table->unsignedInteger('bike_id')外键。在本章的后面,我们将深入研究模型关系/关联,但现在让我们专注于迁移文件。

  2. 打开project/database/migrations/XXXX_XX_XX_XXXXXX_create_garages_table.php并用以下代码替换内容:

<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateGaragesTable extends Migration
{
    /**
    * Run the migrations.
    *
    * @return void
    */
    public function up()
    {
        Schema::create('garages', function (Blueprint $table) {
        $table->increments('id');
        $table->string('name');
        $table->integer('customer_level');
        $table->timestamps();
        });
    }
    /**
    * Reverse the migrations.
    *
    * @return void
    */
    public function down()
    {
        Schema::dropIfExists('garages');
    }
}

现在,我们需要另一个表,只是为了建立BikeGarage之间的关系。我们使用artisan命令创建迁移文件,因为对于这种关系,我们不需要模型。这个表也被称为pivot表。

  1. 打开您的终端窗口并输入以下命令:
php artisan make:migration create_bike_garage_table
  1. 打开project/database/migrations/XXXX_XX_XX_XXXXXX_create_bike_garage_table.php并用以下代码替换内容:
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateBikeGarageTable extends Migration
{
    /**
    * Run the migrations.
    *
    * @return void
    */
    public function up()
    {
        Schema::create('bike_garage', function (Blueprint $table) {
        $table->increments('id');
        $table->integer('bike_id');
        $table->integer('garage_id');
        $table->timestamps();
        });
    }
    /**
    * Reverse the migrations.
    *
    * @return void
    */
    public function down()
    {
        Schema::dropIfExists('bike_garage');
    }
}

请记住,自行车迁移文件是在上一章中创建的。

  1. 打开您的终端窗口并输入以下命令:
php artisan make:migration create_ratings_table
  1. 打开project/database/migrations/XXXX_XX_XX_XXXXXX_create_ratings_table.php并用以下代码替换内容:
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateRatingsTable extends Migration
{
    /**
    * Run the migrations.
    *
    * @return void
    */
    public function up()
    {
        Schema::create('ratings', function (Blueprint $table) {
        $table->increments('id');
        $table->unsignedInteger('user_id');
        $table->unsignedInteger('bike_id');
        $table->unsignedInteger('rating');
        $table->timestamps();
        });
    }
    /**
    * Reverse the migrations.
    *
    * @return void
    */
    public function down()
    {
        Schema::dropIfExists('ratings');
    }
}

好了,现在是时候更深入地了解我们在本节中所做的事情了,所以让我们进入下一节,了解Eloquent是如何工作的。

Eloquent ORM 关系

Eloquent 是 Laravel 数据库查询背后的 ORM。它是活动记录实现的抽象。

正如我们之前看到的,每个应用程序模型在我们的数据库中都有一个相应的表。有了这个,我们可以查询、插入、删除和更新记录。

Eloquent ORM 使用类的蛇形复数名称作为表名,除非另一个名称被明确指定。例如,我们的Bike模型类有自己的表bikes

应用程序模型有以下表:

应用程序模型 数据库表
Bike.php bikes
Builder.php builders
Garage.php garages
Item.php items
Rating.php ratings
Builder.php builders
User.php users

请注意,我们保留表约定名称,但也可以使用自定义表名。在本书的范围内,我们将保留 Laravel 生成的表名。

您可以在官方 Laravel 文档的laravel.com/docs/5.6/eloquent#defining-models上阅读更多关于表名和模型约定的信息。

Eloquent ORM 支持模型之间的以下关系:

  • 一对一

  • 一对多

  • 属于(反向=一对多)

  • 多对多

  • 有多个

  • 多态关系

  • 多对多多态关系

我们将详细介绍前四种关系;然而,我们无法在我们的书中详细介绍所有关系。在许多框架中,理解关系(也称为关联)是非常简单的。

您可以在laravel.com/docs/5.6/eloquent-relationships上阅读更多关于关系的信息。

一对一关系

让我们建立BuilderBike之间的一对一关系。这意味着Bike将只有一个Builder

  1. 打开project/app/Builder.php并用以下代码替换内容:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
/**
* @SWG\Definition(
* definition="Builder",
* required={"name", "description", "location"},
* @SWG\Property(
* property="name",
* type="string",
* description="Builder name",
* example="Jesse James"
* ),
* @SWG\Property(
* property="description",
* type="string",
* description="Famous Motorcycle builder from Texas",
* example="Austin Speed Shop"
* ),
* @SWG\Property(
* property="location",
* type="string",
* description="Texas/USA",
* example="Austin, Texas"
* ),
* )
*/
class Builder extends Model
{
    /**
    * The table associated with the model.
    *
    * @var string
    */
    protected $table = 'builders';
    /**
    * The attributes that are mass assignable.
    *
    * @var array
    */
    protected $fillable = [
        'name',
        'description',
        'location'
    ];
    /**
    * Relationship.
    *
    * @var array
    */
    public function bike() {
        return $this->hasOne('App\Bike');
    }
}

请注意,我们像在上一章中那样添加了 Swagger 文档定义。bike()函数创建了一对一关系。您可以在关系函数上使用任何名称,但我们强烈建议您使用相同的模型名称,在我们的案例中是Bike模型类。

  1. 现在,让我们为Bike模型添加相应的关系。打开project/app/Bike.php并在protected fillable函数之后立即添加以下代码:
/**
* Relationship.
*
* @var string
*/
public function builder() {
     return $this->belongsTo('App\Builder');
}

注意,belongsTo关系是一对多的反向关系。

  1. 打开project/app/Item.php并用以下代码替换内容:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
/**
* @SWG\Definition(
* definition="Item",
* required={"type", "name", "company"},
* @SWG\Property(
* property="type",
* type="string",
* description="Item Type",
* example="Exhaust"
* ),
* @SWG\Property(
* property="name",
* type="string",
* description="Item name",
* example="2 into 1 Exhaust"
* ),
* @SWG\Property(
* property="company",
* type="string",
* description="Produced by: some company",
* example="Vance and Hines"
* )
* )
*/
class Item extends Model
{
    /**
    * The table associated with the model.
    *
    * @var string
    */
    protected $table = 'items';
    /**
    * The attributes that are mass assignable.
    *
    * @var array
    */
    protected $fillable = [
        'type',
        'name',
        'company',
        'bike_id'
    ];
    /**
    * Relationship.
    *
    * @var string
    */
    public function bike() {
        return $this->belongsTo('App\Bike');
    }
}

一对多关系

一对多关系将应用于BikeItems之间,这意味着一个bike将有许多自定义items

仍然在project/app/Bike.app文件中,让我们在ItemBike模型之间添加一对多关系。

builder()函数之后立即添加以下代码:

public function items() {
    return $this->hasMany('App\Item');
}

多对多关系

对于多对多关系,我们将通过枢轴表在许多 Garages 中有许多 Bikes。

在多对多关系中,我们需要遵守一些命名规则。

枢轴表的名称应由两个表的单数名称组成,用下划线符号分隔,并且这些名称应按字母顺序排列。

默认情况下,应该只有两个枢轴表字段和每个表的外键,在我们的情况下是bike_idgarage_id

仍然在project/app/Bike.app文件中,让我们在BikeGarage模型之间添加多对多关系。

items()函数之后立即添加以下代码:

public function garages() {
    return $this->belongsToMany('App\Garage');
}

请注意,在上面的代码中,我们正在创建BikeGarage之间的关系,这将在第三个表中,称为枢轴表中保存与关系相关的信息,正如我们之前解释的那样。

现在,是时候在用户和评分与自行车之间添加关系了。在garages()函数之后立即添加以下代码:

public function user() {
        return $this->belongsTo('App\User');
public function ratings() {
        return $this->hasMany('App\Rating');
}

在这一点上,我们将在Bike模型中有以下关系:

/**
* Relationship.
*
* @var string
*/
public function builder() {
    return $this->belongsTo('App\Builder');
}
public function items() {
    return $this->hasMany('App\Item');
}
public function garages() {
    return $this->belongsToMany('App\Garage');
}
public function user() {
    return $this->belongsTo(User::class);
}
public function ratings() {
    return $this->hasMany(Rating::class);
}

现在,让我们在project/app/Garage.app模型中添加关系。用以下代码替换其内容:

<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
/**
* @SWG\Definition(
* definition="Garage",
* required={"name", "custumer_level"},
* @SWG\Property(
* property="name",
* type="string",
* description="Jhonny Garage",
* example="Exhaust"
* ),
* @SWG\Property(
* property="customer_level",
* type="integer",
* description="Whats the garage level",
* example="10"
* )
* )
*/
class Garage extends Model
{
    /**
    * The table associated with the model.
    *
    * @var string
    */
    protected $table = 'garages';
    /**
    * The attributes that are mass assignable.
    *
    * @var array
    */
    protected $fillable = [
        'name',
        'costumer_level'
    ];
    /
    *
    * @var string
    */
    public function bikes() {
        return $this->belongsToMany('App\Bike', 'bike_garage',
        'bike_id', 'garage_id');
    }
}
* Relationship.

请注意,我们使用的是belongsToMany()而不是hasMany()hasMany()用于一对多关系。

现在,让我们在project/app/User.app模型中添加关系。用以下代码替换其内容:

<?php
namespace App;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
/**
* @SWG\Definition(
* definition="User",
* required={"name", "email", "password"},
* @SWG\Property(
* property="name",
* type="string",
* description="User name",
* example="John Conor"
* ),
* @SWG\Property(
* property="email",
* type="string",
* description="Email Address",
* example="john.conor@terminator.com"
* ),
* @SWG\Property(
* property="password",
* type="string",
* description="A very secure password",
* example="123456"
* ),
* )
*/class User extends Authenticatable
{
    use Notifiable;
    /**
    * The attributes that are mass assignable.
    *
    * @var array
    */
    protected $fillable = [
        'name', 'email', 'password',
    ];
    /**
    * The attributes that should be hidden for arrays.
    *
    * @var array
    */
    protected $hidden = [
        'password', 'remember_token',];
    * Relationship.
    ** @var string
    /
    public function bikes()
    {
        return $this->hasMany(App\Bike);
    }
}

打开project/app/Rating.app模型,并用以下代码替换其内容:

<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
/**
* @SWG\Definition(
* definition="Rating",
* required={"bike_id", "user_id", "rating"},
* @SWG\Property(
* property="biker_id",
* type="integer",
* description="Bike id",
* example="1"
* ),
* @SWG\Property(
* property="user_id",
* type="integer",
* description="User id",
* example="2"
* ),
* @SWG\Property(
* property="rating",
* type="integer",
* description="Vote by rating",
* example="10"
* )
* )
*/
class Rating extends Model
{
    /**
    * The attributes that are mass assignable.
    *
    * @var array
    */
    protected $fillable = [
        'bike_id',
        'user_id',
        'rating'
    ];
    /**
    * Relationship.
    *
    * @var string
    */
    public function bike() {
        return $this->belongsTo('App\Bike');
    }
}

现在我们已经准备好迁移文件和应用程序模型,我们可以创建种子文件来填充我们的数据库。但在继续之前,让我们将表迁移到数据库中。在您的终端窗口中,输入以下命令:

php artisan migrate

干得好!我们已成功迁移了所有表,现在我们的数据库已经准备就绪。

如果在尝试使用migrate命令时遇到问题,请使用refresh参数:

php artisan migrate:refresh

填充我们的数据库

请记住,在上一章中,我们已经创建了 Bike 种子,所以现在我们只需要创建另外三个种子,它们将是BuildersItemsGarage

  1. 打开您的终端窗口,输入以下命令:
php artisan make:seeder BuildersTableSeeder
  1. 将以下代码添加到app/database/seeds/BuildersTableSeeder.phprun()公共函数中:
DB::table('builders')->delete();
$json = File::get("database/data-sample/builders.json");
$data = json_decode($json);
foreach ($data as $obj) {
    Builder::create(array(
        'id' => $obj->id,
        'name' => $obj->name,
        'description' => $obj->description,
        'location' => $obj->location
    ));
}
  1. 仍然在您的终端窗口中,输入以下命令:
php artisan make:seeder ItemsTableSeeder
  1. 将以下代码添加到app/database/seeds/ItemsTableSeeder.php中:
DB::table('items')->delete();
$json = File::get("database/data-sample/items.json");
$data = json_decode($json);
foreach ($data as $obj) {
    Item::create(array(
        'id' => $obj->id,
        'type' => $obj->type,
        'name' => $obj->name,
        'company' => $obj->company,
        'bike_id' => $obj->bike_id
    ));
}
  1. 在您的终端窗口中,输入以下命令:
php artisan make:seeder GaragesTableSeeder
  1. 将以下代码添加到app/database/seeds/GaragesTableSeeder.phprun()公共函数中:
DB::table('garages')->delete();
$json = File::get("database/data-sample/garages.json");
$data = json_decode($json);
foreach ($data as $obj) {
    Garage::create(array(
        'id' => $obj->id,
        'name' => $obj->name,
        'customer_level' => $obj->customer_level
    ));
}
  1. 将以下代码添加到app/database/seeds/UsersTableSeeder.php文件夹的公共函数run()中:
DB::table('users')->insert([
'name' => 'Johnny Cash',
'email' => 'johnny@cash.com',
'password' => bcrypt('123456')
]);
    DB::table('users')->insert([
        'name' => 'Frank Sinatra',
        'email' => 'frank@sinatra.com',
        'password' => bcrypt('123456')
    ]);

请注意,我们正在使用与上一章中相同的函数来加载示例数据。现在,是时候创建 JSON 文件了。

  1. project/database/data-sample/中,创建一个名为builders.json的新文件,并添加以下代码:
[{
    "id": 1,
    "name": "Diamond Atelier",
    "description": "Diamond Atelier was founded by two fellow riders
     who grew tired of the same played-out custom bike look and feel
     they and their friends had grown accustomed to witnessing.",
    "location": "Munich, Germany"
},{
    "id": 2,
    "name": "Deus Ex Machina's",
    "description": "Established in Australia back in 2006\. And what     started on the East Coast of Australia has spread across the
     world, building an empire of cafe racers.",
    "location": "Sydney, Australia"
},{
    "id": 3,
    "name": "Rough Crafts",
    "description": "A true testament to how far the custom bike
     world has come since the introduction of motorcycles in the
     early 20th century, Taiwan-based Rough Crafts is a design
     powerhouse.",
    "location": "Taiwan"
},{
    "id": 4,
    "name": "Roldand Sands",
    "description": "Is an American motorcycle racer and designer of
    custom high-performance motorcycles.",
    "location": "California, USA"
},{
    "id": 5,
    "name": "Chopper Dave",
    "description": "An artist, a biker, a builder and an innovator     among other things, but what it comes down to is David
     “ChopperDave” Freston is a motorcycle builder and fabricator     that is passionate about motorcycles",
    "location": "California, USA"
}]
  1. project/database/data-sample/中,创建一个名为items.json的新文件,并添加以下代码:
[{
    "id": 1,
    "type": "Handlebars",
    "name": "Apes Hanger 16 ",
    "company": "TC Bros",
    "bike_id": 2
},{
    "id": 2,
    "type": "Seat",
    "name": "Challenger",
    "company": "Biltwell Inc",
    "bike_id": 3
},{
    "id": 3,
    "type": "Exhaust",
    "name": "Side Shots",
    "company": "Vance and Hines",
    "bike_id": 3
}]
  1. 现在,我们需要创建一些更多的种子,这样我们的应用程序就有了所有数据库样板。在终端窗口中,输入以下命令:
php artisan make:seeder BikesGaragesTableSeeder
  1. 将以下代码添加到app/database/seeds/BikesGaragesTableSeeder.phprun()公共函数中:
DB::table('bike_garage')->insert([
    'bike_id' => 1,
    'garage_id' => 2
]);
DB::table('bike_garage')->insert([
    'bike_id' => 2,
    'garage_id' => 2
]);
  1. 请注意,在上面的代码中,我们只是使用 Eloquent 的insert()方法手动插入记录,而不是为此任务创建 JSON 文件。

  2. 现在,打开project/database/data-sample/bikes.json,并用以下代码替换内容:

[{
    "id": 1,
    "make": "Harley Davidson",
    "model": "XL1200 Nightster",
    "year": "2009",
    "mods": "Nobis vero sint non eius. Laboriosam sed odit hic quia     doloribus. Numquam laboriosam numquam quas quis."
    "picture": "https://placeimg.com/640/480/nature","user_id": 2,
    "builder_id": 1
}, {
    "id": 2,
    "make": "Harley Davidson",
    "model": "Blackline",
    "year": "2008",
    "mods": "Nobis vero sint non eius. Laboriosam sed odit hic quia     doloribus. Numquam laboriosam numquam quas quis.",
    "picture": "https://placeimg.com/640/480/nature",
    "user_id": 1,
    "builder_id": 2
}, {
    "id": 3,
    "make": "Harley Davidson",
    "model": "Dyna Switchback",
    "year": "2009",
    "mods": "Nobis vero sint non eius. Laboriosam sed odit hic quia     doloribus. Numquam laboriosam numquam quas quis.",
    "picture": "https://placeimg.com/640/480/nature",
    "user_id": 2,
    "builder_id": 3
}, {
    "id": 4,
    "make": "Harley Davidson",
    "model": "Dyna Super Glide",
    "year": "2009",
    "mods": "Nobis vero sint non eius. Laboriosam sed odit hic quia     doloribus. Numquam laboriosam numquam quas quis.",
    "picture": "https://placeimg.com/640/480/nature",
    "user_id": 4,
    "builder_id": 4
},{
    "id": 5,
    "make": "Harley Davidson",
    "model": "Dyna Wild Glide",
    "year": "2005",
    "mods": "Nobis vero sint non eius. Laboriosam sed odit hic quia     doloribus. Numquam laboriosam numquam quas quis.",
    "picture": "https://placeimg.com/640/480/nature",
    "user_id": 5,
    "builder_id": 5
}]

在上面的代码中,我们为每辆自行车记录添加了builder_iduser_id,以建立自行车与其构建者以及用户与其自行车之间的关联。请记住,我们在上一章中创建了project/database/data-sample/bikes.json

请注意,我们将自行车45分配给user_id45。现在不要担心这个问题,因为在本书的后面,您将明白我们为什么现在这样做。

  1. 打开project/database/seeds/databaseSeeder.php,取消注释用户的种子。

  2. 让我们使用seed命令来填充我们的数据库。在您的终端窗口中输入以下命令:

php artisan migrate:fresh --seed

使用上一个命令后,我们将得到以下输出:

 Seeding: UsersTableSeeder
 Seeding: BikesTableSeeder
 Seeding: BuildersTableSeeder
 Seeding: ItemsTableSeeder
 Seeding: GaragesTableSeeder
 Seeding: BikeGarageTableSeeder

这意味着目前一切都是正确的。

migrate:fresh命令将从数据库中删除所有表,然后执行migrate命令进行全新安装。

使用 Tinker 查询数据库

Tinker是一个命令行应用程序,允许您与您的 Laravel 应用程序进行交互,包括 Eloquent ORM、作业、事件等。要访问 Tinker 控制台,请运行artisan tinker命令,我们之前用来检查数据库连接的第一章中的内容,理解 Laravel 5 的核心概念

  1. 打开您的终端窗口,输入以下命令:
php artisan tinker

由于我们还没有为我们的应用程序创建任何控制器或路由,所以无法使用浏览器访问 API 端点来检查我们的数据。

然而,使用 Tinker,可以与我们的数据库进行交互,并检查我们的迁移文件和数据库种子是否一切顺利。

让我们去builders表,确保一切都设置正确。

  1. 在您的终端和 Tinker 控制台中,输入以下命令:
DB::table('builders')->get();

您的终端上的输出将是一个非常类似于以下 JSON 结构的构建者列表:

 >>> DB::table('builders')->get();
=> Illuminate\Support\Collection {#810
     all: [
       {#811
         +"id": 1,
         +"name": "Diamond Atelier",
         +"description": "Diamond Atelier was founded by two fellow
         riders who grew tired of the same played-out custom
         bike look and feel they and their friends had grown
         accustomed     to witnessing.",
         +"location": "Munich, Germany",
         +"created_at": "XXXX",
         +"updated_at": "XXXX",
       },
       ...
       }]

请注意,您可以省略构建者列表上的所有记录,因此在您的代码块中不要重复。

  1. 在您的终端和 Tinker 控制台中,输入以下命令:
DB::table('builders')->find(3);

在这里,我们只有一个记录,id 为3,在find()函数中,正如我们在以下输出中所看到的:

>>> DB::table('builders')->find(3);
=> {#817
     +"id": 3,
     +"name": "Rough Crafts",     +"description": "A true testament      to how far the custom bike world has come since the
     introduction     of motorcycles i
     n the early 20th century, Taiwan-based Rough Crafts is a design      powerhouse.",
     +"location": "Taiwan",
     +"created_at": "XXXX",
     +"updated_at": "XXXX",
   }

现在,让我们看看如何从上一个命令中获得相同的结果,但这次使用Where子句和Builder模型实例。

  1. 在您的终端和 Tinker 控制台中,输入以下命令:
Builder::where('id', '=', 3)->get();

我们将得到以下输出作为查询结果:

>>> Builder::where('id', '=', 3)->get();
=> Illuminate\Database\Eloquent\Collection {#825
     all: [
       App\Builder {#828
         id: 3,
         name: "Rough Crafts",
         description: "A true testament to how far the custom bike          world has come since the introduction of motorcycles
         in the early 20th century, Taiwan-based Rough Crafts is a          design powerhouse.",
         location: "Taiwan",
         created_at: "XXXX",
         updated_at: "XXXX",
       },
     ],
   }

但是,请等一下,您一定在问自己,自行车数据在哪里?请记住,我们在种子中将自行车归属于构建者。让我们介绍关联查询。

  1. 因此,假设我们想查询所有定制自行车。在您的终端和 Tinker 控制台中,输入以下命令:
Builder::with('bike')->find(3);

请注意,上一个命令将使用find()方法和::with()方法关联返回 id 为3的构建者记录。这次,我们可以看到自行车的信息,如以下输出所示:

>>> Builder::with('bike')->find(3);
=> App\Builder {#811
     id: 3,
     name: "Rough Crafts",     description: "A true testament to how
     far the custom bike world has come since the introduction of
     motorcycles in t
     he early 20th century, Taiwan-based Rough Crafts is a design
     powerhouse.",
     location: "Taiwan",
     created_at: "XXXX",
     updated_at: "XXXX",
     bike: App\Bike {#831
       id: 3,
       make: "Harley Davidson",
       model: "Dyna Switchback",
       year: "2009",
       mods: "Nobis vero sint non eius. Laboriosam sed odit hic quia
       doloribus. Numquam laboriosam numquam quas quis.",
       picture: "https://placeimg.com/640/480/nature",
       builder_id: 3,
       created_at: "XXXX",
       updated_at: "XXXX",
     },
   }

现在,让我们看看如何提交查询以获取所有模型关联,这次使用 Builder 模型实例。

  1. 在您的终端和 Tinker 控制台中,输入以下命令:
Bike::with(['items', 'builder'])->find(3);

请注意,我们在::with()方法中使用了一个数组来获取itemsbuilderassociations,正如我们在以下输出中所看到的:

>>> Bike::with(['items', 'builder'])->find(3);
[!] Aliasing 'Bike' to 'App\Bike' for this Tinker session.
=> App\Bike {#836
     id: 3,     make: "Harley Davidson",
     model: "Dyna Switchback",
     year: "2009",
     mods: "Nobis vero sint non eius. Laboriosam sed odit hic quia
     doloribus. Numquam laboriosam numquam quas quis.",
     picture: "https://placeimg.com/640/480/nature",
     builder_id: 3,
     created_at: "XXXX",
     updated_at: "XXXX",
     items: Illuminate\Database\Eloquent\Collection {#837
       all: [
         App\Item {#843
           id: 2,
           type: "Seat",
           name: "Challenger",
           company: "Biltwell Inc",
           bike_id: 3,
           created_at: "XXXX",
           updated_at: "XXXX",
         },
         App\Item {#845
           id: 3,
           type: "Exhaust",
           name: "Side Shots",
           company: "Vance and Hines",
           bike_id: 3,
           created_at: "XXXX",
           updated_at: "XXXX",
         },
       ],
     },
     builder: App\Builder {#844
       id: 3,
       name: "Rough Crafts",description: "A true testament to how
       far the custom bike world has come since the introduction of
       motorcycles in the early 20th century, Taiwan-based Rough
       Crafts is a design powerhouse.",
       location: "Taiwan",
       created_at: "XXXX",
       updated_at: "XXXX",
     },
   }

创建控制器和路由

我们已经快完成了,但还有一些步骤要走,这样我们才能完成我们的 API。现在是时候创建 API 控制器和 API 路由了。

在最新版本(5.6)的 Laravel 中,我们有一个新的可用于执行此任务的命令。这就是--api标志。让我们看看它在实践中是如何工作的。

创建和更新控制器函数

  1. 打开你的终端窗口,输入以下命令:
php artisan make:controller API/BuilderController --api

请注意,--api标志在BuilderController类中为我们创建了四个方法:

  • index() = GET

  • store() = POST

  • show($id) = GET

  • update(Request $request, $id) = PUT

  • destroy($id) = POST

  1. 打开project/app/Http/Controllers/API/BuilderController.php,并在控制器导入后添加App\Builder代码。

  2. 现在,让我们为每个方法添加内容。打开project/app/Http/Controllers/API/BuilderController.php,并用以下代码替换内容:

<?php
namespace App\Http\Controllers\API;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Builder;
class BuilderController extends Controller
{
    /**
    * Display a listing of the resource.
    *
    * @return \Illuminate\Http\Response
    *
    * @SWG\Get(
    * path="/api/builders",
    * tags={"Builders"}
    * summary="List Builders",
    * @SWG\Response(
    * response=200,
    * description="Success: List all Builders",
    * @SWG\Schema(ref="#/definitions/Builder")
    * ),
    * @SWG\Response(
    * response="404",
    * description="Not Found"
    * )
    * ),
    */
    public function index()
    {
        $listBuilder = Builder::all();
        return $listBuilder;

    }
  1. 现在,让我们为store/create方法添加代码。在index()函数后添加以下代码:
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*
* @SWG\Post(
* path="/api/builders",
* tags={"Builders"},
* summary="Create Builder",
* @SWG\Parameter(
*          name="body",
*          in="body",
*          required=true,
*          @SWG\Schema(ref="#/definitions/Builder"),
*          description="Json format",
*      ),
* @SWG\Response(
* response=201,
* description="Success: A Newly Created Builder",
* @SWG\Schema(ref="#/definitions/Builder")
* ),
* @SWG\Response(
* response="422",
* description="Missing mandatory field"
* ),
* @SWG\Response(
* response="404",
* description="Not Found"
* ),
* @SWG\Response(
     *          response="405",
     *          description="Invalid HTTP Method
* )
* ),
*/
public function store(Request $request)
{
    $createBuilder = Builder::create($request->all());
    return $createBuilder;
}

现在,让我们为按id获取方法添加代码。在store()函数后添加以下代码:


/**
* Display the specified resource.
*
* @param int $id* @return \Illuminate\Http\Response
*
* @SWG\Get(
* path="/api/builders/{id}",
* tags={"Builders"},
* summary="Get Builder by Id",
* @SWG\Parameter(
* name="id",
* in="path",
* required=true,
* type="integer",
* description="Display the specified Builder by id.",
*      ),
* @SWG\Response(
* response=200,
* description="Success: Return the Builder",
* @SWG\Schema(ref="#/definitions/Builder")
* ),
* @SWG\Response(
* response="404",
* description="Not Found"
* ),
* @SWG\Response(
     *          response="405",
     *          description="Invalid HTTP Method"
     * )
* )
*/
public function show($id)
{
    $showBuilderById = Builder::with('Bike')->findOrFail($id);
    return $showBuilderById;
}

让我们为更新方法添加代码。在show()函数后添加以下代码:

/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*
* @SWG\Put(
* path="/api/builders/{id}",
* tags={"Builders"},
* summary="Update Builder",
* @SWG\Parameter(
* name="id",
* in="path",
* required=true,
* type="integer",
* description="Update the specified Builder by id.",
*      ),
* @SWG\Parameter(
*          name="body",
*          in="body",
*          required=true,
*          @SWG\Schema(ref="#/definitions/Builder"),
*          description="Json format",
*      ),
* @SWG\Response(
* response=200,
* description="Success: Return the Builder updated",
* @SWG\Schema(ref="#/definitions/Builder")
* ),
* @SWG\Response(
* response="422",
* description="Missing mandatory field"
* ),
* @SWG\Response(
* response="404",
* description="Not Found"
* ),
* @SWG\Response(
     *          response="405",
     *          description="Invalid HTTP Method"
     * )
* ),
*/
public function update(Request $request, $id)
{
    $updateBuilderById = Builder::findOrFail($id);
    $updateBuilderById->update($request->all());
    return $updateBuilderById;
}

现在,让我们为删除方法添加代码。在update()函数后添加以下代码:

/**
* Remove the specified resource from storage.
*
* @param int $id
* @return \Illuminate\Http\Response
*
* @SWG\Delete(
* path="/api/builders/{id}",
* tags={"Builders"},
* summary="Delete Builder",
* description="Delete the specified Builder by id",
* @SWG\Parameter(
* description="Builder id to delete",
* in="path",
* name="id",
* required=true,
* type="integer",
* format="int64"
* ),
* @SWG\Response(
* response=404,
* description="Not found"
* ),
* @SWG\Response(
     *          response="405",
     *          description="Invalid HTTP Method"
     * ),
* @SWG\Response(
* response=204,
* description="Success: successful deleted"
* ),
* )
*/
public function destroy($id)
{
    $deleteBikeById = Bike::find($id)->delete();
    return response()->json([], 204);
    }
}

请注意,在index()函数中,我们使用all()方法列出所有自行车,并且只在show($id)函数中使用关联的::with()方法。

我们已经将 Swagger 定义添加到了控制器中,但不要担心:稍后在本章中,我们将详细讨论这个问题。

模型关联查询列出自行车并显示自行车详细信息,是一个简单的 API 决定。正如你所看到的,我们在返回自行车列表时没有返回所有关联,只在按 id 获取自行车时返回关联。在每个请求中返回每个关联都没有意义,所以在自行车列表中,我们只显示自行车的详细信息,当我们点击详细信息时,我们将看到所有模型关联的完整信息。所以现在不要担心这个,因为在第十章 使用 Bootstrap 4 和 NgBootstrap 创建前端视图中,我们将看到如何做到这一点。

  1. 打开你的终端窗口,输入以下命令:
php artisan make:controller API/ItemController --api

打开project/app/Http/Controllers/API/ItemController.php,并在控制器导入后添加以下代码:use App\Item;

  1. 现在,让我们为每个方法添加内容。打开project/app/Http/Controllers/API/ItemController.php,并为每个方法添加以下代码:
<?php
namespace App\Http\Controllers\API;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Item;
class ItemController extends Controller
{
    /**
    * Display a listing of the resource.
    *
    * @return \Illuminate\Http\Response
    *
    * @SWG\Get(
    * path="/api/items",
    * tags={"Items"},
    * summary="List Items",
    * @SWG\Response(
    * response=200,
    * description="Success: List all Items",
    * @SWG\Schema(ref="#/definitions/Item")
    * ),
    * @SWG\Response(
    * response="404",
    * description="Not Found"
    * )
    * ),
    */
    public function index()
    {
        $listItems = Item::all();
        return $listItems;
    }
    /**
    * Store a newly created resource in storage.
    *
    * @param \Illuminate\Http\Request $request
    * @return \Illuminate\Http\Response
    *
    * @SWG\Post(
    * path="/api/items",
    * tags={"Items"},
    * summary="Create Item",
    * @SWG\Parameter(
    *           name="body",
    *           in="body",
    *           required=true,
    *           @SWG\Schema(ref="#/definitions/Item"),
    *           description="Json format",
    *       ),
    * @SWG\Response(
    * response=201,
    * description="Success: A Newly Created Item",
    * @SWG\Schema(ref="#/definitions/Item")
    * ),
    * @SWG\Response(
    * response="422",
    * description="Missing mandatory field"
    * ),
    * @SWG\Response(
    * response="404",
    * description="Not Found"
    * )
    * ),
    */
    public function store(Request $request)
    {
        $createItem = Item::create($request->all());
        return $createItem;
    }
    /**
    * Display the specified resource.
    *
    * @param int $id
    * @return \Illuminate\Http\Response
    *
    * @SWG\Get(
    * path="/api/items/{id}",
    * tags={"Items"},
    * summary="Get Item by Id",
    * @SWG\Parameter(
    * name="id",
    * in="path",
    * required=true,
    * type="integer",
    * description="Display the specified Item by id.",
    *       ),
    * @SWG\Response(
    * response=200,
    * description="Success: Return the Item",
    * @SWG\Schema(ref="#/definitions/Item")
    * ),
    * @SWG\Response(
    * response="404",
    * description="Not Found"
    * )
    * ),
    */
    public function show($id)
    {
        $showItemById = Item::with('Bike')->findOrFail($id);
        return $showItemById;
    }
    /**
    * Update the specified resource in storage.
    *
    * @param \Illuminate\Http\Request $request
    * @param int $id
    * @return \Illuminate\Http\Response
    *
    * @SWG\Put(
    * path="/api/items/{id}",
    * tags={"Items"},
    * summary="Update Item",
    * @SWG\Parameter(
    * name="id",
    * in="path",
    * required=true,
    * type="integer",
    * description="Update the specified Item by id.",
    *       ),
    * @SWG\Parameter(
    *           name="body",
    *           in="body",
    *           required=true,
    *           @SWG\Schema(ref="#/definitions/Item"),
    *           description="Json format",
    *       ),
    * @SWG\Response(
    * response=200,
    * description="Success: Return the Item updated",
    * @SWG\Schema(ref="#/definitions/Item")
    * ),
    * @SWG\Response(
    * response="422",
    * description="Missing mandatory field"
    * ),
    * @SWG\Response(
    * response="404",
    * description="Not Found"
    * )
    * ),
    */
    public function update(Request $request, $id)
    {
        $updateItemById = Item::findOrFail($id);
        $updateItemById->update($request->all());
        return $updateItemById;
    }
    /**
    * Remove the specified resource from storage.
    *
    * @param int $id
    * @return \Illuminate\Http\Response
    *
    * @SWG\Delete(
    * path="/api/items/{id}",
    * tags={"Items"},
    * summary="Delete Item",
    * description="Delete the specified Item by id",
    * @SWG\Parameter(
    * description="Item id to delete",
    * in="path",
    * name="id",
    * required=true,
    * type="integer",
    * format="int64"
    * ),
    * @SWG\Response(
    * response=404,
    * description="Not found"
    * ),
    * @SWG\Response(
    * response=204,
    * description="Success: successful deleted"
    * ),
    * )
    */
    public function destroy($id)
    {
        $deleteItemById = Item::findOrFail($id)->delete();
        return response()->json([], 204);
    }
}
  1. 打开你的终端窗口,输入以下命令:
php artisan make:controller API/BikeController --api

打开project/app/Http/Controllers/API/BikeController.php,并在控制器导入后添加以下代码:

use App\Bike;
  1. 现在,让我们为每个方法添加内容。打开project/app/Http/Controllers/API/BikeController.php,并为每个方法添加以下代码:
<?php
namespace App\Http\Controllers\API;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Bike;
class BikeController extends Controller
{
    /**
    * Display a listing of the resource.
    ** @return \Illuminate\Http\Response
    *
    * @SWG\Get(
    * path="/api/bikes",
    * tags={"Bikes"},
    * summary="List Bikes",
    * @SWG\Response(
    * response=200,
    * description="Success: List all Bikes",
    * @SWG\Schema(ref="#/definitions/Bike")
    * ),
    * @SWG\Response(
    * response="404",
    * description="Not Found"
    * )
    * ),
    */
    public function index()
    {
        $listBikes = Bike::all();
        return $listBikes;
    }
    /**
    * Store a newly created resource in storage.
    *
    * @param \Illuminate\Http\Request $request
    * @return \Illuminate\Http\Response
    *
    * @SWG\Post(
    * path="/api/bikes",
    * tags={"Bikes"},
    * summary="Create Bike",
    * @SWG\Parameter(
    *           name="body",
    *           in="body",
    *           required=true,
    *           @SWG\Schema(ref="#/definitions/Bike"),
    *           description="Json format",
    *       ),
    * @SWG\Response(
    * response=201,
    * description="Success: A Newly Created Bike",
    * @SWG\Schema(ref="#/definitions/Bike")
    * ),
    * @SWG\Response(
    * response="422",
    * description="Missing mandatory field"
    * ),
    * @SWG\Response(
    * response="404",
    * description="Not Found"
    * )
    * ),
    */
    public function store(Request $request)
    {
        $createBike = Bike::create($request->all());
        return $createBike;
    }
    /**
    * Display the specified resource.
    *
    * @param int $id
    * @return \Illuminate\Http\Response
    *
    * @SWG\Get(
    * path="/api/bikes/{id}",
    * tags={"Bikes"},
    * summary="Get Bike by Id",
    * @SWG\Parameter(
    * name="id",
    * in="path",
    * required=true,
    * type="integer",
    * description="Display the specified bike by id.",
    *       ),
    * @SWG\Response(
    * response=200,
    * description="Success: Return the Bike",
    * @SWG\Schema(ref="#/definitions/Bike")
    * ),
    * @SWG\Response(
    * response="404",
    * description="Not Found"
    * )
    * ),
    */
    public function show($id)
    {
        $showBikeById = Bike::with(['items', 'builder', 'garages'])-
        >findOrFail($id);
        return $showBikeById;
    }
    /**
    * Update the specified resource in storage.
    *
    * @param \Illuminate\Http\Request $request
    * @param int $id
    * @return \Illuminate\Http\Response
    *
    * @SWG\Put(
    * path="/api/bikes/{id}",
    * tags={"Bikes"},
    * summary="Update Bike",
    * @SWG\Parameter(
    * name="id",
    * in="path",
    * required=true,
    * type="integer",
    * description="Update the specified bike by id.",
    *       ),
    * @SWG\Parameter(
    *           name="body",
    *           in="body",
    *           required=true,
    *           @SWG\Schema(ref="#/definitions/Bike"),
    *           description="Json format",
    *       ),
    * @SWG\Response(
    * response=200,
    * description="Success: Return the Bike updated",
    * @SWG\Schema(ref="#/definitions/Bike")
    * ),
    * @SWG\Response(
    * response="422",
    * description="Missing mandatory field"
    * ),
    * @SWG\Response(
    * response="404",
    * description="Not Found"
    * )
    * ),
    */
    public function update(Request $request, $id)
    {
        $updateBikeById = Bike::findOrFail($id);
        $updateBikeById->update($request->all());
        return $updateBikeById;
    }
    /**
    * Remove the specified resource from storage.
    *
    * @param int $id* @return \Illuminate\Http\Response
    *
    * @SWG\Delete(
    * path="/api/bikes/{id}",
    * tags={"Bikes"},
    * summary="Delete bike",
    * description="Delete the specified bike by id",
    * @SWG\Parameter(
    * description="Bike id to delete",
    * in="path",
    * name="id",
    * required=true,
    * type="integer",
    * format="int64"
    * ),
    * @SWG\Response(
    * response=404,
    * description="Not found"
    * ),
    * @SWG\Response(
    * response=204,
    * description="Success: successful deleted"
    * ),
    * )
    */
    public function destroy($id)
    {
        $deleteBikeById = Bike::find($id)->delete();
        return response()->json([], 204);
    }
}

打开你的终端窗口,输入以下命令:

php artisan make:controller API/RatingController --api
  1. 打开project/app/Http/Controllers/API/RatingController.php,并在控制器导入后添加以下代码:
use App\Rating;
  1. 现在,让我们为每个方法添加内容。打开project/app/Http/Controllers/API/RatingController.php,并为每个方法添加以下代码:
<?php
namespace App\Http\Controllers\API;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Bike;
use App\Rating;
use App\Http\Resources\RatingResource;
class RatingController extends Controller
{
    /**
    * Store a newly created resource in storage.
    *
    * @param \Illuminate\Http\Request $request
    * @return \Illuminate\Http\Response
    *
    * @SWG\Post(
    * path="/api/bikes/{bike_id}/ratings",
    * tags={"Ratings"},
    * summary="rating a Bike",
    * @SWG\Parameter(
    * in="path",
    * name="id",
    * required=true,
    * type="integer",
    * format="int64",
    *           description="Bike Id"
    *       ),
    * @SWG\Parameter(
    *           name="body",
    *           in="body",
    *           required=true,
    *           @SWG\Schema(ref="#/definitions/Rating"),
    *           description="Json format",
    *        ),
    * @SWG\Response(
    * response=201,
    * description="Success: A Newly Created Rating",
    * @SWG\Schema(ref="#/definitions/Rating")
    * ),
    * @SWG\Response(
    * response=401,
    * description="Refused: Unauthenticated"
    * ),
    * @SWG\Response(
    * response="422",
    * description="Missing mandatory field"
    * ),
    * @SWG\Response(
    * response="404",
    * description="Not Found"
    * ),
    * @SWG\Response(
    *        response="405",
    *    description="Invalid HTTP Method"
    * ),
    * security={
    *        { "api_key":{} }
    * }
    * ),
    */
    public function store(Request $request, Bike $bike)
    {
        $rating = Rating::firstOrCreate(
            [
                'user_id' => $request->user()->id,
                'bike_id' => $bike->id,
            ],
            ['rating' => $request->rating]
        );
        return new RatingResource($rating);
    }
}

你应该在评分控制器代码中发现一些奇怪的东西。其中,我们有一些新的错误代码,422405,以及 Swagger 文档中的一个安全标签,还有一个叫做rating resource的新导入。

这可能听起来奇怪,但不要惊慌;我们将在接下来的章节中详细讨论这个问题。

创建 API 路由

现在是时候创建一些 API 路由并检查我们到目前为止构建的内容了。我们正在使用apiResource的新功能。

打开project/routes/api.php,并添加以下代码:

Route::apiResources([
    'bikes' => 'API\BikeController',
    'builders' => 'API\BuilderController',
    'items' => 'API\ItemController',
    'bikes/{bike}/ratings' => 'API\RatingController'
]);

此时,我们已经为我们的 API 添加了必要的代码,所以我们需要做一些小的调整并解释更多的东西。

生成 Swagger UI 文档

从先前的示例中可以看出,我们已经通过 Swagger 定义向我们最近创建的控制器添加了 API 的文档。这与我们在先前的示例中使用的代码相同。让我们在 Swagger UI 上生成文档。

打开您的终端窗口,输入以下命令:

php artisan l5-swagger:generate

根据先前的 Swagger 定义中的错误消息,您可以注意到我们有一些新的 HTTP 错误,比如422

这意味着如果用户尝试输入一些数据,其中一个或多个必填字段缺失,我们的 API 必须返回一个 HTTP 错误代码。这将是422。因此,让我们看看如何实现一些验证并验证一些常见的 API HTTP 代码。

总结

我们已经完成了本章的第一部分,我们为 API 创建了一个强大且可扩展的 RESTful 基础。我们学会了如何创建控制器、路由,以及如何处理 Eloquent 关系。

我们还有很多工作要做,因为我们需要处理错误消息、资源和基于令牌的身份验证。在下一章中,我们将看到如何完成这些工作。

第六章:使用 Laravel 构建 RESTful API - 第 2 部分

在本章中,我们将继续构建我们的 API。在 Laravel 中,我们还有很长的路要走。

我们将学习如何使用一些在每个 Web 应用程序中非常常见的功能,例如身份验证和 API 错误的自定义。

请记住,我们正在创建一个 RESTful API,与传统的应用程序(如 MVC)不同,我们的错误模型非常多样化,并且始终在使用 JSON 格式时返回。

在本章中,您将学习如何通过以下方式构建一个坚实的 RESTful API:

  • 处理请求验证和错误消息

  • 使用基于令牌的身份验证

  • 处理 Laravel 资源

处理请求验证和错误消息

Laravel 框架为我们提供了许多显示错误消息的方法,默认情况下,Laravel 的基础控制器类使用ValidatesRequests特性,提供了验证传入 HTTP 请求的方法,包括许多默认规则,如必填、电子邮件格式、日期格式、字符串等等。

您可以在laravel.com/docs/5.6/validation#available-validation-rules了解更多有关可能的验证规则的信息。

使用请求验证非常简单,如下面的代码块所示:

$validatedData = $request->validate([
'field name' => 'validation rule, can be more than one',
'field name' => 'validation rule',
'field name' => 'validation rule',
...
]);

例如,让我们看看如何使用 HTTP POST方法验证对localhost:8081/api/bikesbikes端点的传入请求。

验证代码如下所示:

$validatedData = $request->validate([
'make' => 'required',
'model' => 'required',
'year'=> 'required',
'mods'=> 'required'
]);

之前的操作失败是因为我们故意没有在我们的虚构请求中发送所需的文件。然而,返回消息中有一些有趣的东西:

  • HTTP 状态码:422

  • 以下 JSON 响应错误消息:

{
    "message": "The given data was invalid.",
    "errors": {
    "": [
    "The field is required."
    ]}
}

相当简单,对吧?Laravel 在幕后执行所有验证,并给我们提供了详细的错误消息。

但是,如果我们想控制所有的消息字段怎么办?答案是,我们可以使用validator门面和验证器实例进行手动验证。这是我们接下来要看的内容。

HTTP 状态码

在我们进一步实现验证之前,让我们暂停一下,回顾一些 HTTP 状态码。正如我们之前看到的,我们有一个名为不可处理的实体的 422 HTTP 状态码。

以下表格显示了最常见和有用的错误代码:

代码 名称 描述
200 正常 一切都好!
201 已创建 资源创建成功。
202 已接受 请求已被接受进行进一步处理,稍后将完成。
204 正常 资源删除成功删除。
302 重定向 常见的重定向响应;您可以在位置响应标头中获取 URI 的表示。
304 未修改 没有新数据返回。
400 错误请求 客户端错误。
401 未经授权 您未登录,例如,您未使用有效的访问令牌。
403 禁止 您已经经过身份验证,但无权进行您正在尝试的操作。
404 未找到 您请求的资源不存在。
405 方法不允许 不允许该请求类型,例如,/bikes 是一个资源,POST /bikes 是一个有效操作,但 PUT /bikes 不是。
409 冲突 资源已经存在。
422 不可处理的实体 验证失败。请求和格式有效,但请求无法处理。例如,当发送的数据未通过验证测试时会发生这种情况。
500 服务器错误 服务器发生错误,而不是消费者的错误。

您可以在www.restapitutorial.com/httpstatuscodes.html了解更多有关状态码的信息。

实现控制器验证

好吧,我们已经学习了很多理论,现在是时候写一些代码了。让我们在 API 控制器上实现Validator

  1. 打开project/app/Http/Controllers/API/BikeController.php,并在use App\Bike语句之后添加以下代码:
use Validator;
  1. 现在,在store(Request $request)方法中添加以下代码:
$validator = Validator::make($request->all(), [
    'make' => 'required',
    'model' => 'required',
    'year'=> 'required',
    'mods'=> 'required',
    'builder_id' => 'required'
]);
if ($validator->fails()) {
    return response()->json($validator->errors(), 422);
}

请注意,在上面的代码中,我们使用响应 JSON 格式,并将错误和状态代码设置为json()方法的参数。

  1. 我们将使用相同的代码块从步骤 2中为update(Request request,request,id)方法做同样的操作。

  2. 打开project/app/Http/Controllers/API/BuilderController.php,并在use App\Builder语句之后添加以下代码:

use Validator;
  1. 现在,在store(Request $request)方法中添加以下代码:
$validator = Validator::make($request->all(), 
    ['name' => 'required',
    'description' => 'required',
    'location'=> 'required'
]);
if ($validator->fails()) {
    return response()->json($validator->errors(), 422);
}
  1. 我们将使用相同的代码块从步骤 5中为update(Request request,request,id)方法做同样的操作。

  2. 打开project/app/Http/Controllers/API/ItemController.php,并在use App\Item语句之后添加以下代码:

use Validator;
  1. 现在,在store(Request $request)方法中添加以下代码:
$validator = Validator::make($request->all(), [
    'type' => 'required',

    'name' => 'required',
    'company'=> 'required',
    'bike_id'=> 'required'
]);
if ($validator->fails()) {
    return response()->json($validator->errors(), 422);
}
  1. 我们将使用相同的代码块从步骤 7中为update(Request request,request,id)方法做同样的操作。

所有验证样板代码都放在了store()update()方法中,所以现在是时候编写一些错误处理程序了。

添加自定义错误处理

默认情况下,Laravel 具有非常强大的错误处理引擎,但它完全专注于 MVC 开发模式,正如我们之前提到的。在接下来的几行中,我们将看到如何改变这种默认行为,并为我们的 API 添加一些特定的错误处理:

  1. 打开project/app/Exceptions/Handler.php,并在render($request, Exception, $exception)函数中添加以下代码:
// This will replace our 404 response from the MVC to a JSON response.
if ($exception instanceof ModelNotFoundException
    && $request->wantsJson() // Enable header Accept:
     application/json to see the proper error msg
) {
    return response()->json(['error' => 'Resource not found'], 404);
}
if ($exception instanceof MethodNotAllowedHttpException) {
    return response()->json(['error' => 'Method Not Allowed'], 405);
}
if ($exception instanceof UnauthorizedHttpException) {
    return response()->json(['error' => 'Token not provided'], 401);
}
// JWT Auth related errors
if ($exception instanceof JWTException) {
    return response()->json(['error' => $exception], 500);
}
if ($exception instanceof TokenExpiredException) {
    return response()->json(['error' => 'token_expired'], 
    $exception->getStatusCode());
} else if ($exception instanceof TokenInvalidException) {
    return response()->json(['error' => 'token_invalid'],
     $exception->getStatusCode());
}
return parent::render($request, $exception);

在上面的代码中,除了映射我们的 API 的主要错误之外,我们还需要为涉及 JWT 身份验证的操作添加一些自定义错误。别担心,在下一节中,我们将看到如何使用 JWT 来保护我们 API 的一些路由。

  1. 现在,让我们在文件顶部添加以下代码,放在ExceptionHandler导入之后:
use Illuminate\Database\Eloquent\ModelNotFoundException as ModelNotFoundException;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException as UnauthorizedHttpException;
use Tymon\JWTAuth\Exceptions\JWTException as JWTException;
use Tymon\JWTAuth\Exceptions\TokenExpiredException as TokenExpiredException;
use Tymon\JWTAuth\Exceptions\TokenInvalidException as TokenInvalidException;

现在,我们将能够看到正确的消息,而不是来自 Laravel 的默认错误页面。

请注意,我们保留了默认的 Laravel 错误页面,并添加了自定义处理。非常重要的是,我们发送header: accept: application / json。这样,Laravel 就可以确定应该以 JSON 格式发送响应,而不是发送标准的错误页面。

  1. 让我们进行简短的测试,看看当我们尝试访问受保护的 URL 时会发生什么。打开终端窗口,输入以下代码:
curl -X GET "http://localhost:8081/api/bikes/3" -H "accept: application/json" -H "X-CSRF-TOKEN: "

结果将是一个 JSON,内容如下:

{"message":"Unauthenticated."}
  1. 现在,让我们尝试另一个错误,看看当我们尝试发送 POST 方法时会发生什么。在终端中输入以下代码:
curl -X POST "http://localhost:8081/api/bikes/3" -H "accept: application/json" -H "X-CSRF-TOKEN: "

结果将是一个 JSON,内容如下:

{"error":"Method Not Allowed"}

使用 Swagger UI 检查 API URL

在所有这些样板代码之后,现在是测试 API 并看到我们在本章中所做的所有工作生效的时候了:

  1. 打开终端,输入以下命令:
php artisan l5-swagger:generate

不要忘记使用以下命令进入php-fpm容器的 bash:docker-compose exec php-fpm bash

  1. 打开默认浏览器,转到http://localhost:8081/api/documentation

我们将看到所有 API 都被正确记录的以下结果:

Swagger UI

让我们检查一些操作。

获取所有记录

让我们看看如何使用 Swagger UI 上的GET方法从我们的 API 中检索自行车列表:

  1. 点击GET /api/bikes以打开面板。

  2. 点击试一下按钮。

  3. 点击执行按钮。

我们将看到类似以下截图的内容:

GET 请求

按 ID 获取记录

让我们看看如何从我们的 API 中获取自行车列表:

  1. 点击GET /api/bikes/{id}以打开面板。

  2. 点击试一下按钮。

  3. 在 ID 输入框中输入3

  4. 点击执行按钮。

将看到类似以下截图的内容:

按 ID 请求获取

检查 API 响应错误

现在,是时候检查一些错误消息了:

  1. 单击PUT /api/bikes/{id}打开面板。

  2. 单击尝试按钮。

  3. 在 ID 输入框中输入1

  4. 用以下代码替换示例值占位符:

{
 "make": "Yamaha",
 "model": "V-Star",
 "year": "2001",
 "mods": "New exhaust system and Grips",
 "picture": "http://www.sample.com/my.bike.jpg"
 }
  1. 单击执行按钮。

我们将看到类似以下截图的内容:

更新失败,带有错误消息

正如我们所观察到的,一切都如预期的那样发生了。

基于令牌的身份验证

让我们更深入地了解使用 Laravel 进行 API 身份验证。尽管 Laravel 是一个 MVC 框架,但我们可以使用基于令牌的身份验证功能。即使 Laravel 本身也有一个名为 Passport 的库。

Laravel Passport 是一个与 OAuth2 标准配合使用的库。这种模式确定了通过令牌对 Web 应用程序(API)执行应用程序身份验证的方法,而 JWT 侧重于通过令牌对用户进行身份验证。

Laravel Passport 比简单的 JWT 更大的抽象层,它主要设计为完全成熟且易于设置和使用作为 OAuth2 服务器。

这种情况的替代方案是使用诸如tymon/jwt-auth之类的库。

实际上,Laravel Passport 使用 JWT 进行身份验证,但这只是一个实现细节。tymon/jwt-auth更接近于简单的基于令牌的身份验证,尽管它仍然非常强大和有用。

对于我们正在构建的 API 类型,JWT 是我们实现的理想方法。

您可以在github.com/tymondesigns/jwt-auth上阅读有关jwt-auth的更多信息。

安装 tymon-jwt-auth

让我们学习如何安装和配置tymon/jwt-auth

安装过程非常简单,但是由于tymon/jwt-auth库在不断发展,我们应该注意我们将使用的版本:

  1. 打开project/composer.json文件,并在Laravel/Tinker之后添加以下行代码:
"tymon/jwt-auth": "1.0.*"
  1. 现在,是时候发布供应商包了。仍然在您的终端窗口和 Tinker 控制台中,输入以下命令:
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"

请注意,我们正在使用 Laravel 5.6 和jwt-auth 1.0,因此我们不需要执行任何额外的操作来加载 JWT 提供程序。jwt-auth文档有时看起来很混乱,非常令人困惑,但不要担心,只需按照我们的步骤进行操作,您就不会受到不匹配的文档的影响。

  1. 让我们生成密钥。仍然在终端窗口中,输入以下命令:
 php artisan jwt:secret
  1. 上一个命令将在您的.env文件中生成一个密钥,并且看起来类似以下行:
JWT_SECRET=McR1It4Bw9G8jU1b4XJhDMeZs4Q5Zwear

到目前为止,我们已经成功安装了jwt-auth,但是我们需要采取一些额外的步骤来使我们的 API 安全。

更新用户模型

现在,我们需要更新User模型,以便我们可以开始使用用户身份验证来保护 API 端点。

首先,我们需要在我们的User模型上实现Tymon\JWTAuth\Contracts\JWTSubject合同,这需要两种方法:getJWTIdentifier()getJWTCustomClaims()

打开project/User.php并用以下代码替换其内容:

 <?php
 namespace  App;
 use  Illuminate\Notifications\Notifiable;
 use  Illuminate\Foundation\Auth\User  as  Authenticatable;
 use  Tymon\JWTAuth\Contracts\JWTSubject;
 /**
 * @SWG\Definition(
 * definition="User",
 * required={"name", "email", "password"},
 * @SWG\Property(
 * property="name",
 * type="string",
 * description="User name",
 * example="John Conor"
 * ),
 * @SWG\Property(
 * property="email",
 * type="string",
 * description="Email Address",
 * example="john.conor@terminator.com"
 * ),
 * @SWG\Property(
 * property="password",
 * type="string",
 * description="A very secure password",
 * example="123456"
 * ),
 * )
 */
 class  User  extends  Authenticatable  implements  JWTSubject
 {
     use  Notifiable;
     /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
     protected  $fillable = [
         'name', 'email', 'password',
     ];
     /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
     protected  $hidden = [
         'password', 'remember_token',
     ];
     /**
     * Get JSON WEB TOKEN methods.
     *
     * @var array
     */
     public  function  getJWTIdentifier()
     {
         return  $this->getKey();
     } 
     public  function  getJWTCustomClaims()
     {
         return [];
     }  
     /**
     * Relationship.
     *
     * @var string
     */   
     public  function  bikes()
     {
         return  $this->hasMany(App\Bike);
     }
 }

设置身份验证守卫

现在,让我们对config.auth.php文件进行一些调整,以保护一些路由:

  1. 打开project/config/auth.php并用以下代码替换 API 驱动程序:
 'defaults' => [         'guard'  =>  'api',
        'passwords'  =>  'users',
 ],
 'guards'  => [
                'web'  => [
                        'driver'  =>  'session',
                        'provider'  =>  'users',
        ],        
 'api'  => [
                'driver'  =>  'jwt',
                'provider'  =>  'users',
        ],
 ],
  1. 请注意,我们用apijwt替换了默认的 Laravel 身份验证驱动程序。

创建 authController

对于我们的应用程序,我们将只使用一个控制器来包含我们的所有注册和登录操作,即注册,登录和注销。

在本书的后面,您将了解为什么我们在一个控制器中使用所有操作,而不是为每个操作创建一个控制器:

  1. 打开您的终端窗口并输入以下命令:
php artisan make:controller API/AuthController
  1. 打开project/app/Http/Controllers/API/AuthController.php并用以下代码替换其内容:
 <?php
 namespace  App\Http\Controllers\API;
 use  Illuminate\Http\Request;
 use  App\Http\Controllers\Controller;
 use  App\User;
 use  Validator;
 class  AuthController  extends  Controller
 {
     /**
     * Register a new user.
     *
     * @param \Illuminate\Http\Request $request
     * @return \Illuminate\Http\Response
     *
     * @SWG\Post(
     * path="/api/register",
     * tags={"Users"},
     * summary="Create new User",
     * @SWG\Parameter(
     * name="body",
     * in="body",
     * required=true,
     * @SWG\Schema(ref="#/definitions/User"),
     * description="Json format",
     * ),
     * @SWG\Response(
     * response=201,
     * description="Success: A Newly Created User",
     * @SWG\Schema(ref="#/definitions/User")
     * ),
     * @SWG\Response(
     * response=200,
     * description="Success: operation Successfully"
     * ),
     * @SWG\Response(
     * response=401,
     * description="Refused: Unauthenticated"
     * ),
    * @SWG\Response(
    * response="422",
    * description="Missing mandatory field"
    * ),
    * @SWG\Response(
    * response="404",
    * description="Not Found"
    * )
    * ),
    */
    public  function  register(Request  $request)
    {
        $validator = Validator::make($request->all(), [
            'email' => 'required|string|email|max:255|unique:users',
            'name' => 'required',
            'password'=> 'required'
        ]);
        if ($validator->fails()) {
            return  response()->json($validator->errors(), 422);
            }
        $user = User::create([
        'name' => $request->name,
        'email' => $request->email,
        'password' => bcrypt($request->password),
        ]);
        $token = auth()->login($user);
        return  response()->json([
            'access_token' => $token,
            'token_type' => 'bearer',
            'expires_in' => auth()->factory()->getTTL() * 60
            ], 201);
        }
    /**
    * Log in a user.
    *
    * @param \Illuminate\Http\Request $request
    * @return \Illuminate\Http\Response
    *
    * @SWG\Post(
    * path="/api/login",
    * tags={"Users"},
    * summary="loggin an user",
    * @SWG\Parameter(
    * name="body",
    * in="body",
    * required=true,
    * @SWG\Schema(ref="#/definitions/User"),
    * description="Json format",
    * ),
    * @SWG\Response(
    * response=200,
    * description="Success: operation Successfully"
    * ),
    * @SWG\Response(
    * response=401,
    * description="Refused: Unauthenticated"
    * ),
    * @SWG\Response(
    * response="422",
    * description="Missing mandatory field"
    * ),
    * @SWG\Response(
    * response="404",
    * description="Not Found"
    * )
    * ),
    */
    public  function  login(Request  $request)
    {
        $validator = Validator::make($request->all(), [
            'email' => 'required|string|email|max:255',
            'password'=> 'required'
        ]);
        if ($validator->fails()) {
            return  response()->json($validator->errors(), 422);
            }
        $credentials = $request->only(['email', 'password']);
        if (!$token = auth()->attempt($credentials)) {
            return  response()->json(['error' => 'Invalid
             Credentials'], 400);
        }
        $current_user = $request->email;
            return  response()->json([
            'access_token' => $token,
            'token_type' => 'bearer',
            'current_user' => $current_user,
            'expires_in' => auth()->factory()->getTTL() * 60
            ], 200);
            }
    /**
    * Register a new user.
    *
    * @param \Illuminate\Http\Request $request
    * @return \Illuminate\Http\Response
    *
    * @SWG\Post(
    * path="/api/logout",
    * tags={"Users"},
    * summary="logout an user",
    * @SWG\Parameter(
    * name="body",
    * in="body",
    * required=true,
    * @SWG\Schema(ref="#/definitions/User"),
    * description="Json format",
    * ),
    * @SWG\Response(
    * response=200,
    * description="Success: operation Successfully"
    * ),
    * @SWG\Response(
    * response=401,
    * description="Refused: Unauthenticated"
    * ),
    * @SWG\Response(
    * response="422",
    * description="Missing mandatory field"
    * ),
    * @SWG\Response(
    * response="404",
    * description="Not Found"
    * ),
    * @SWG\Response(
    * response="405",
    * description="Invalid input"
    * ),
    * security={
    * { "api_key":{} }
    * }
    * ),
    */
    public  function  logout(Request  $request){
        auth()->logout(true); // Force token to blacklist
        return  response()->json(['success' => 'Logged out
         Successfully.'], 200); }
}

在前面的代码中几乎没有什么新的内容——我们只是在registerloginlogout函数中返回了 JSON 响应,正如我们在前面的行中所看到的。

  1. register()函数中:
 $token = auth()->login($user);
        return  response()->json([
                'access_token' => $token,
                'token_type' => 'bearer',
                'expires_in' => auth()->factory()->getTTL() * 60
 ], 201);

创建user后,我们返回了201的 HTTP 状态代码,带有access_token和到期日期。

  1. login()函数中:
 $current_user = $request->email;
        return  response()->json([
                'access_token' => $token,
                'token_type' => 'bearer',
                'current_user' => $current_user,
                'expires_in' => auth()->factory()->getTTL() * 60
 ], 200);

login()函数中,我们根据用户的电子邮件地址返回了当前用户,一个access_token和到期日期。

  1. logout()函数中:
auth()->logout(true); // Force token to blacklist
    return  response()->json(['success' => 'Logged out
     Successfully.'], 200);

请注意,logout()函数中的true参数告诉jwt-auth永久使令牌无效。

创建用户路由

现在,是时候为注册、登录和注销操作创建新路由,并在我们的 API 中保护一些路由,就像本章开头讨论的那样。我们的用户可以与应用程序的部分内容进行交互,但是要访问其所有内容,必须创建用户并登录到应用程序。

打开project/routes/api.php并用以下代码替换其内容:

 <?php
 use  Illuminate\Http\Request;
 use  App\Bike;
 use  App\Http\Resources\BikesResource;

 /*
 |--------------------------------------------------------------------------
 | API Routes
 |--------------------------------------------------------------------------
 |
 | Here is where you can register API routes for your application. These
 | routes are loaded by the RouteServiceProvider within a group whic
 | is assigned the "api" middleware group. Enjoy building your API!
 |
 */

 // Register Routes
 Route::post('register', 'API\AuthController@register');
 Route::post('login', 'API\AuthController@login');
 Route::post('logout', 'API\AuthController@logout');

 Route::apiResources([

     'bikes' => 'API\BikeController',

     'builders' => 'API\BuilderController',

     'items' => 'API\ItemController',

     'bikes/{bike}/ratings' => 'API\RatingController'

 ]);

Route::middleware('jwt.auth')->get('me', function(Request $request){
    return auth()->user();
});

最后一步是保护端点;我们在project/routes/api.php文件中或直接在控制器函数中执行此操作。我们将在控制器函数中执行此操作。

保护 API 路由

使用应用程序控制器保护我们的路由非常简单。我们只需要编辑Controller文件并添加以下代码。

打开project/Http/Controllers/API/BikeController.php并在GET方法之前添加以下代码:

 /**
 * Protect update and delete methods, only for authenticated users.
 *
 * @return Unauthorized
 */
 public  function  __construct()
 {
        $this->middleware('auth:api')->except(['index']);
 }

前面的代码意味着我们正在使用auth:api中间件来保护所有骑手路由,除了index()方法。因此,我们的用户可以查看自行车列表,但是要查看自行车的详细信息并发布自行车,他们必须登录。稍后,在第九章,创建服务和用户身份验证,在 Angular 中,我们将详细讨论基于令牌的身份验证。

创建和登录用户

现在,是时候检查用户路由了。由于我们没有用户界面,我们将使用一个名为 Restlet Client 的 Chrome 扩展。它是免费且非常强大。

您可以在restlet.com/modules/client了解更多信息并下载它:

  1. 打开 Restlet 扩展并填写以下字段,如下屏幕截图所示:

注册端点

  1. 结果将是以下响应:

创建响应

  1. 现在,让我们使用新创建的用户登录。填写如下屏幕截图中显示的字段:

用户登录

结果将是以下响应:

用户登录响应

好了,我们的 API 身份验证已经准备就绪。稍后,在第九章,创建服务和用户身份验证,在 Angular 中,我们将详细讨论身份验证过程。

处理 Laravel 资源

在以前的一些 Laravel 版本中,可以使用一个名为 Fractal 的功能来处理 JSON web API,但是在这个新版本的 Laravel 中,我们有资源功能,这是一个处理 JSON web API 的非常强大的工具。

在这一部分,我们将看到如何使用资源功能,以便我们可以充分利用我们的 API。资源类是一种将数据从一种格式转换为另一种格式的方法。

在处理资源并将其转换为客户端响应时,我们基本上有两种类型:项目和集合。项目资源,正如你可能已经猜到的那样,基本上是我们模型的一对一表示,而集合是许多项目的表示。集合还可以具有元数据和其他导航信息,我们将在本节后面看到。

创建 BikesResource

因此,让我们创建我们的第一个资源:

  1. 打开您的终端窗口,输入以下命令:
php artisan make:resource BikesResource

上一个命令将生成以下文件:

App\Http\Resource\BikesResource.php

  1. 打开App\Http\Resource\BikesResource.php并添加以下代码:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
use App\Builder;
class BikesResource extends JsonResource
{
    /**
    * Transform the resource into an array.
    *
    * @param \Illuminate\Http\Request $request
    * @return array
    */
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'make' => $this->make,
            'model' => $this->model,
            'year' => $this->year,
            'mods' => $this->mods,
            'picture' => $this->picture,
            'garages' => $this->garages,
            'items' => $this->items,
            'builder' => $this->builder,
            'user' => $this->user,
            'ratings' => $this->ratings,
            'average_rating' => $this->ratings->avg('rating'),
            // Casting objects to string, to avoid receive create_at             and update_at as object
            'created_at' => (string) $this->created_at,
            'updated_at' => (string) $this->updated_at
        ];
    }
}

请注意,我们在数组函数中包含了bike模型的所有关系。

创建 BuildersResource

现在,让我们使用make命令创建BuildersResource

  1. 打开您的终端窗口,输入以下命令:
php artisan make:resource BuildersResource
  1. 上一个命令将生成以下文件:

App\Http\Resource\BuildersResource.php

  1. 打开App\Http\Resource\BuildersResource.php并添加以下代码:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class BuildersResource extends JsonResource
{
    /**
    * Transform the resource into an array.
    *
    * @param \Illuminate\Http\Request $request
    * @return array
    */
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'description' => $this->description,
            'location' => $this->location,
            'bike' => $this->bike,
            // Casting objects to string, to avoid receive create_at             and update_at as object
            'created_at' => (string) $this->created_at,
            'updated_at' => (string) $this->updated_at,
        ];
    }
}

创建 ItemsResource

现在,让我们使用make命令创建ItemsResource

  1. 打开您的终端窗口,输入以下命令:
php artisan make:resource ItemsResource
  1. 上一个命令将生成以下文件:

App\Http\Resource\ItemsResource.php

  1. 打开App\Http\Resource\ItemsResource.php并添加以下代码:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class ItemsResource extends JsonResource
{
    /**
    * Transform the resource into an array.
    *
    * @param \Illuminate\Http\Request $request
    * @return array
    */
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'type' => $this->type,
            'name' => $this->name,
            'company' => $this->company,
            'bike_id' => $this->bike_id,
            // Casting objects to string, to avoid receive create_at             and update_at as object
            'created_at' => (string) $this->created_at,
            'updated_at' => (string) $this->updated_at
        ];
    }
}

创建 ratingResource

现在,让我们创建一个新的Resource,这次是为了评分:

  1. 打开您的终端窗口,输入以下命令:
php artisan make:resource ratingResource
  1. 上一个命令将生成以下文件:

App\Http\Resource\RatingResource.php

  1. 打开App\Http\Resource\RatingResource.php并添加以下代码:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
use App\Bike;
class RatingResource extends JsonResource
{
    /**
    * Transform the resource into an array.
    *
    * @param \Illuminate\Http\Request $request
    * @return array
    */
    public function toArray($request)
    {
        return [
            'user_id' => $this->user_id,
            'bike_id' => $this->bike_id,
            'rating' => $this->rating,
            'bike' => $this->bike,
            'average_rating' => $this->bike->ratings->avg('rating'),
            // Casting objects to string, to avoid receive 
             create_at and update_at as object
             'created_at' => (string) $this->created_at,
             'updated_at' => (string) $this->updated_at
         ];
     }
}

将资源添加到控制器

现在,我们需要对我们的控制器进行一些微小的更改,以便使用我们刚刚创建的资源。为了避免任何错误,我们将查看所有控制器的代码:

  1. 通过用以下代码替换App/Http/Controllers/API/BikeController.php中的内容来编辑Bike控制器:
<?php
namespace App\Http\Controllers\API;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Bike;
use Validator;
use App\Http\Resources\BikesResource;
class BikeController extends Controller
{
    /**
    * Protect update and delete methods, only for authenticated
     users.
    *
    * @return Unauthorized
    */
    public function __construct()
    {
        $this->middleware('auth:api')->except(['index']);
    }
    /**
    * Display a listing of the resource.
    *
    * @return \Illuminate\Http\Response
    *
    * @SWG\Get(
    * path="/api/bikes",
    * tags={"Bikes"},
    * summary="List Bikes",
    * @SWG\Response(
    * response=200,
    * description="Success: List all Bikes",
    * @SWG\Schema(ref="#/definitions/Bike")
    * ),
    * @SWG\Response(
    * response="404",
    * description="Not Found"
    * ),
    * @SWG\Response(
    *          response="405",
    *          description="Invalid HTTP Method"
    * )
    * ),
    */
    public function index()
    {
        $listBikes = Bike::all();
        return $listBikes;
        // Using Paginate method We explain this later in the book
        // return BikesResource::collection(Bike::with('ratings')-
        >paginate(10));
    }

现在,让我们为store/create方法添加代码。在index()函数之后添加以下代码:

/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*
* @SWG\Post(
* path="/api/bikes",
* tags={"Bikes"},
* summary="Create Bike",
* @SWG\Parameter(
*          name="body",
*          in="body",
*          required=true,
*          @SWG\Schema(ref="#/definitions/Bike"),
*          description="Json format",
*      ),
* @SWG\Response(
* response=201,
* description="Success: A Newly Created Bike",
* @SWG\Schema(ref="#/definitions/Bike")

* ),
* @SWG\Response(
* response=401,
* description="Refused: Unauthenticated"
* ),
* @SWG\Response(
* response="422",
* description="Missing mandatory field"
* ),
* @SWG\Response(
* response="404",
* description="Not Found"
* ),
* @SWG\Response(
     *          response="405",
     *          description="Invalid HTTP Method"
     * ),
     * security={
     *       { "api_key":{} }
     *      }
* ),
*/
public function store(Request $request)
{
    $validator = Validator::make($request->all(), [
        'make' => 'required',
        'model' => 'required',
        'year'=> 'required',
        'mods'=> 'required',
        'builder_id' => 'required'
        ]);
    if ($validator->fails()) {
        return response()->json($validator->errors(), 422);
    }
    // Creating a record in a different way
    $createBike = Bike::create([
        'user_id' => $request->user()->id,
        'make' => $request->make,
        'model' => $request->model,
        'year' => $request->year,
        'mods' => $request->mods,
        'picture' => $request->picture,
    ]);
    return new BikesResource($createBike);
}

Get by id方法添加以下代码。在store()函数之后添加以下代码:

/**
* Display the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*
* @SWG\Get(
* path="/api/bikes/{id}",
* tags={"Bikes"},
* summary="Get Bike by Id",
* @SWG\Parameter(
* name="id",
* in="path",
* required=true,
* type="integer",
* description="Display the specified bike by id.",
*      ),
* @SWG\Response(
* response=200,
* description="Success: Return the Bike",
* @SWG\Schema(ref="#/definitions/Bike")
* ),
* @SWG\Response(
* response="404",
* description="Not Found"
* ),
* @SWG\Response(
     *          response="405",
     *          description="Invalid HTTP Method"
     * ),
* security={
*       { "api_key":{} }
*   }
* ),
*/
public function show(Bike $bike)
{
    return new BikesResource($bike);
}

现在,让我们为update方法添加代码。在show()函数之后添加以下代码:

/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*
* @SWG\Put(
* path="/api/bikes/{id}",
* tags={"Bikes"},* summary="Update Bike",
* @SWG\Parameter(
* name="id",
* in="path",
* required=true,
* type="integer",
* description="Update the specified bike by id.",
*      ),
* @SWG\Parameter(
*          name="body",
*          in="body",
*          required=true,
*          @SWG\Schema(ref="#/definitions/Bike"),
*          description="Json format",
*      ),
* @SWG\Response(
* response=200,
* description="Success: Return the Bike updated",
* @SWG\Schema(ref="#/definitions/Bike")
* ),
* @SWG\Response(
* response="422",
* description="Missing mandatory field"
* ),
* @SWG\Response(
* response="404",
* description="Not Found"
* ),
* @SWG\Response(
     *          response="403",
     *          description="Forbidden"
     * ),

* @SWG\Response(
     *          response="405",
     *          description="Invalid HTTP Method"
     * ),
     * security={
     *       { "api_key":{} }
     *      }
* ),
*/
public function update(Request $request, Bike $bike)
{
    // check if currently authenticated user is the bike owner
    if ($request->user()->id !== $bike->user_id) {
        return response()->json(['error' => 'You can only edit your
         own bike.'], 403);
    }
        $bike->update($request->only(['make', 'model', 'year',
         'mods',     'picture']));
    return new BikesResource($bike);
}

最后一个方法是删除所有记录。在update()函数之后添加以下代码:

/**
* Remove the specified resource from storage.
*
* @param int $id
* @return \Illuminate\Http\Response
*
* @SWG\Delete(
* path="/api/bikes/{id}",
* tags={"Bikes"},
* summary="Delete bike",
* description="Delete the specified bike by id",
* @SWG\Parameter(
* description="Bike id to delete",
* in="path",
* name="id",
* required=true,
* type="integer",
* format="int64"
* ),
* @SWG\Response(
* response=404,
* description="Not found"
* ),
* @SWG\Response(
* response=204,
* description="Success: successful deleted"
* ),
* @SWG\Response(
     *          response="405",
     *          description="Invalid HTTP Method"
     * ),
     * security={
     *       { "api_key":{} }
     *      }
* )
*/
public function destroy($id)
{
    $deleteBikeById = Bike::findOrFail($id)->delete();
    return response()->json([], 204);
    }
}

然后我们将为Builders控制器做同样的事情。

  1. 通过用以下代码替换App/Http/Controllers/API/BuilderController.php中的内容来编辑Builder控制器:
<?php
namespace App\Http\Controllers\API;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Builder;
use Validator;
use App\Http\Resources\BuildersResource;
class BuilderController extends Controller
{
    /**
    * Display a listing of the resource.
    *
    * @return \Illuminate\Http\Response
    *
    * @SWG\Get(
    * path="/api/builders",
    * tags={"Builders"},
    * summary="List Builders",
    * @SWG\Response(
    * response=200,
    * description="Success: List all Builders",
    * @SWG\Schema(ref="#/definitions/Builder")
    * ),
    * @SWG\Response(
    * response="404",
    * description="Not Found"
    * )
    * ),
    */
    public function index()
    {
        $listBuilder = Builder::all();
        return $listBuilder;
    }

现在,让我们为store/create方法添加代码。在index()函数之后添加以下代码:

/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*
* @SWG\Post(
* path="/api/builders",
* tags={"Builders"},
* summary="Create Builder",
* @SWG\Parameter(
*          name="body",
*          in="body",
*          required=true,
*          @SWG\Schema(ref="#/definitions/Builder"),
*          description="Json format",
*      ),
* @SWG\Response(
* response=201,
* description="Success: A Newly Created Builder",
* @SWG\Schema(ref="#/definitions/Builder")
* ),
* @SWG\Response(
* response="422",
* description="Missing mandatory field"
* ),
* @SWG\Response(
* response="404",
* description="Not Found"
* ),
* @SWG\Response(
     *          response="405",
     *          description="Invalid HTTP Method"
     * )
* ),
*/
public function store(Request $request)
{
    $validator = Validator::make($request->all(), [
        'name' => 'required',
        'description' => 'required',
        'location'=> 'required'
        ]);
    if ($validator->fails()) {
        return response()->json($validator->errors(), 422);
    }
    $createBuilder = Builder::create($request->all());
        return $createBuilder;
 }

让我们为Get by id方法添加代码。在store()函数之后添加以下代码:

/**
* Display the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*
* @SWG\Get(
* path="/api/builders/{id}",
* tags={"Builders"},
* summary="Get Builder by Id",
* @SWG\Parameter(
* name="id",
* in="path",
* required=true,
* type="integer",
* description="Display the specified Builder by id.",
*      ),
* @SWG\Response(
* response=200,
* description="Success: Return the Builder",
* @SWG\Schema(ref="#/definitions/Builder")
* ),
* @SWG\Response(
* response="404",
* description="Not Found"
* ),
* @SWG\Response(
     *          response="405",
     *          description="Invalid HTTP Method"
     * )
* ),
*/
public function show(Builder $builder)
{
    // $showBuilderById = Builder::with('Bike')->findOrFail($id);
    // return $showBuilderById;
    return new BuildersResource($builder);
}

现在,让我们添加update方法的代码。在show()函数之后添加以下代码:

/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*
* @SWG\Put(
* path="/api/builders/{id}",
* tags={"Builders"},
* summary="Update Builder",
* @SWG\Parameter(
* name="id",
* in="path",
* required=true,
* type="integer",
* description="Update the specified Builder by id.",
*      ),
* @SWG\Parameter(
*          name="body",
*          in="body",
*          required=true,
*          @SWG\Schema(ref="#/definitions/Builder"),
*          description="Json format",
*      ),
* @SWG\Response(
* response=200,
* description="Success: Return the Builder updated",
* @SWG\Schema(ref="#/definitions/Builder")
* ),
* @SWG\Response(
* response="422",
* description="Missing mandatory field"
* ),
* @SWG\Response(
* response="404",
* description="Not Found"
* ),
* @SWG\Response(
     *          response="405",
     *          description="Invalid HTTP Method"
     * )
* ),
*/
public function update(Request $request, $id)
{
    $validator = Validator::make($request->all(), [
        'name' => 'required',
        'description' => 'required',
        'location'=> 'required'
        ]);
    if ($validator->fails()) {
        return response()->json($validator->errors(), 422);
    }
    $updateBuilderById = Builder::findOrFail($id);
    $updateBuilderById->update($request->all());
    return $updateBuilderById;
}

最后一个方法用于删除所有记录。在update()函数之后添加以下代码:

/**
* Remove the specified resource from storage.
*
* @param int $id
* @return \Illuminate\Http\Response
*
* @SWG\Delete(
* path="/api/builders/{id}",
* tags={"Builders"},
* summary="Delete Builder",
* description="Delete the specified Builder by id",
* @SWG\Parameter(
* description="Builder id to delete",
* in="path",
* name="id",
* required=true,
* type="integer",
* format="int64"
* ),
* @SWG\Response(
* response=404,
* description="Not found"
* ),
* @SWG\Response(
     *          response="405",
     *          description="Invalid HTTP Method"
     * ),
* @SWG\Response(
* response=204,
* description="Success: successful deleted"
* ),
* )
*/
public function destroy($id)
{
    $deleteBikeById = Bike::find($id)->delete();
    return response()->json([], 204);
    }
}
  1. 为了编辑Rating控制器,用以下代码替换App/Http/Controllers/API/RatingController.php中的内容:
<?php
namespace App\Http\Controllers\API;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Bike;
use App\Rating;
use App\Http\Resources\RatingResource;
class RatingController extends Controller
{
    /**
    * Protect update and delete methods, only for authenticated         users.
    *
    * @return Unauthorized
    */
    public function __construct()
    {
        $this->middleware('auth:api');
    }
    /**
    * Store a newly created resource in storage.
    *
    * @param \Illuminate\Http\Request $request
    * @return \Illuminate\Http\Response
    *
    * @SWG\Post(
    * path="/api/bikes/{bike_id}/ratings",
    * tags={"Ratings"},
    * summary="rating a Bike",
    * @SWG\Parameter(
    * in="path",
    * name="id",
    * required=true,
    * type="integer",
    * format="int64",
    *      description="Bike Id"
    *    ),
    * @SWG\Parameter(
    *      name="body",
    *      in="body",
    *      required=true,
    *      @SWG\Schema(ref="#/definitions/Rating"),
    *      description="Json format",
    *     ),
    * @SWG\Response(
    * response=201,
    * description="Success: A Newly Created Rating",
    * @SWG\Schema(ref="#/definitions/Rating")
    * ),
    * @SWG\Response(
    * response=401,
    * description="Refused: Unauthenticated"
    * ),
    * @SWG\Response(
    * response="422",
    * description="Missing mandatory field"
    * ),
    * @SWG\Response(
    * response="404",
    * description="Not Found"
    * ),
    * @SWG\Response(
       *     response="405",
       *   description="Invalid HTTP Method"
       * ),
    * security={
    *     { "api_key":{} }
    * }
    * ),
    */
    public function store(Request $request, Bike $bike)
    {
        $rating = Rating::firstOrCreate(
        [
        'user_id' => $request->user()->id,
        'bike_id' => $bike->id,
        ],
        ['rating' => $request->rating]
        );
        return new RatingResource($rating);
    }
}

干得好!现在,我们有必要的代码来继续进行 API JSON。在接下来的几章中,您将更详细地了解我们迄今为止所取得的成就。我们已经准备好了我们的 API。

总结

我们又完成了一章。我们学会了如何构建基于令牌的身份验证,如何仅保护已登录用户的端点,以及如何处理自定义错误消息。

我们还学会了如何使用 Laravel 资源返回 JSON API 格式。

我们正在进行中,但是我们需要构建所有的界面并实现 Angular 前端应用程序,以便我们的应用程序可以有一个愉快的视觉结果。

在下一章中,我们将看到如何将 Angular 以及一些更多的工具集成到我们的应用程序中。

第七章:使用 Angular CLI 构建渐进式 Web 应用程序

正如我们在第三章中提到的,了解 Angular 6 的核心概念,Angular 是基于 JavaScript 开发现代 Web 应用程序的主要框架之一。

在第六章中,使用 Laravel 框架创建 RESTful API-2,我们使用 Laravel 资源、eloquent 关系和基于令牌的身份验证完成了后端 API。现在,我们已经拥有连接前端应用程序到后端所需的一切;在我们这样做之前,让我们看看本章将学到什么。

在本章中,我们将看到angular-cli.json文件中发生的一些更改,该文件现在提供了对多个应用程序的改进支持。

我们还将看看如何使用ng add创建渐进式 Web 应用程序PWA),以及如何将项目组织为模块。

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

  • 使用 Angular CLI 启动 Web 应用程序

  • 构建 PWA 的基线

  • 创建样板组件

使用 Angular CLI 启动 Web 应用程序

当我们开始撰写本章时,Angular 框架已推出了最新版本:版本 6。在之前的章节中,我们已经评论了这个版本中存在的一些新功能。

新版本更加专注于开发工具(如 Angular CLI)而不是框架本身的演进。我们可以引用 Angular CLI 的新功能,如ng updateng add命令,这些对于更新包和添加新包非常有用。

我们需要做的第一件事是更新机器上的 Angular CLI;打开您的终端窗口并输入以下命令:

npm install -g @angular/cli

上述命令将在您的机器上全局安装 Angular CLI 6.0.0。

准备基线代码

现在,我们需要准备我们的基线代码,这个过程与之前的章节非常相似。按照以下步骤进行:

  1. 复制chapter-05文件夹中的所有内容。

  2. 将文件夹重命名为chapter-07

  3. 删除storage-db文件夹。

现在,让我们对docker-compose.yml文件进行一些更改,以适应新的数据库和服务器容器。

  1. 打开docker-compose.yml并用以下内容替换其中的内容:
version: "3.1"
services:
    mysql:
      image: mysql:5.7
      container_name: chapter-07-mysql
      working_dir:     /application
      volumes:
        - .:/application
        - ./storage-db:/var/lib/mysql
      environment:
        - MYSQL_ROOT_PASSWORD=123456
        - MYSQL_DATABASE=chapter-06
        - MYSQL_USER=chapter-07
        - MYSQL_PASSWORD=123456
      ports:
        - "8083:3306"
    webserver:
      image: nginx:alpine
      container_name: chapter-07-webserver
      working_dir: /application
      volumes:
        - .:/application-
        ./phpdocker/nginx/nginx.conf:/etc/nginx/conf.d/default.conf
      ports:
        - "8081:80"
    php-fpm:
      build: phpdocker/php-fpm
      container_name: chapter-07-php-fpm
      working_dir: /application
      volumes:
        - ./Server:/application
        - ./phpdocker/php-fpm/php-ini-overrides.ini:
          /etc/php/7.2/fpm/conf.d/99-overrides.ini

请注意,我们更改了容器名称、数据库和 MySQL 用户:

  • container_name: chapter-07-mysql

  • container_name: chapter-07-webserver

  • container_name: chapter-07-php-fpm

  • MYSQL_DATABASE=chapter-07

  • MYSQL_USER=chapter-07

另一个需要注意的重要点是php-fpm容器卷的配置,我们现在将其命名为Server,而不是在之前的章节中命名为project,根据以下突出显示的代码:

php-fpm:
        build: phpdocker/php-fpm
        container_name: chapter-07-php-fpm
        working_dir: /application
        volumes:
        - ./Server:/application
        - ./phpdocker/php-fpm/php-ini-overrides.ini:/etc/php/7.2/fpm/conf.d/99-overrides.ini
  1. vs.code中打开chapter-07并将项目文件夹重命名为Server

正如您在之前的章节中看到的,Laravel 框架有一种明确定义其视图使用方式;这是由于 Laravel 构建在 MVC 标准之上。

此外,Laravel 使用一个名为 Vue.js 的 JavaScript 框架,可以在./Server/resources/assets/js文件夹中找到。

为了不混淆,我们将在一个名为Client的文件夹中创建我们的前端应用程序,与新命名的Server文件夹处于同一级别。

  1. chapter-07文件夹的根目录下创建一个名为Client的新文件夹。

在这些更改结束时,您应该看到与以下屏幕截图相同的项目结构:

应用程序结构

这是保持应用程序与 API 解耦的最佳方法。通过这种方法,我们有一些优势:

  • 前端代码与应用程序的其余部分隔离;我们可以将其托管在静态 Web 服务中,例如亚马逊网络服务AWS)存储桶,或任何其他 Web 服务器。

  • 应用部署可以分开进行,以便 API 独立于前端应用程序进行演进,反之亦然。

将我们对 Git 源代码所做的更改添加到源代码控制中。打开终端窗口,输入以下命令:

git add .
git commit -m "Initial commit chapter 07"

使用 Angular CLI 搭建 Web 应用

让我们开始使用 Angular CLI 构建我们的前端应用程序的新版本:

  1. 在项目根目录打开终端窗口,输入以下命令:
ng new Client --style=scss --routing
  1. 前面的命令将创建我们需要的所有样板代码,这次使用 SCSS 语法进行样式表和--routing标志来创建应用程序路由。

  2. 在上一个命令结束时,我们的应用程序将具有以下结构:

新的应用程序结构

  1. Angular 和 Angular CLI 版本 6 带来的变化之一是angular.json文件,之前的名称是angular-cli.json。它的结构非常不同,如下面的截图所示:

Angular JSON 文件

  1. 至于应用程序文件,我们几乎有与之前相同的代码组织和文件,如下面的截图所示:

新的 Angular 应用结构

在前面的截图中,请注意我们现在有一个名为browserlist的文件;它用于向 CSS 生成的代码添加特定于浏览器的前缀。

创建目录结构

为了方便我们的开发,我们将在应用程序中创建一些目录,这样我们的项目将准备好进行扩展。这意味着我们可以以有组织的方式添加任何我们想要的模块/功能。

这一步非常重要,因为有时项目内部的结构是固定的;不建议更改它。

在这一步中,我们将使用模块或页面的命名约定。我们将使用前一章中制作的 API 定义服务作为基线:

  • 一个名为home的主页

  • 一个名为bike-list的摩托车页面

  • 一个名为bike-details的自行车详情页面

  • 一个名为builders-list的构建者页面

  • 一个名为builder-details的构建者详情页面

  • 一个名为register的注册页面

  • 一个名为login的登录页面

根据前述描述,我们的应用程序将具有以下页面或模块:

  • bike

  • builder

  • register

  • login

  • home

我们更喜欢在这个时候使用模块页面的命名约定,而不是组件,以免与 Angular 提出的组件术语混淆,其中一切都基于组件。

最后,这只是一种不同的方式来指代应用程序结构。

  1. 打开 VS Code,在Client/src/app中,创建一个名为pages的新文件夹。

  2. 在 VS Code 中,进入Client/src/app,创建一个名为layout的新文件夹。

  3. 在 VS Code 中,进入Client/src/app,创建一个名为shared的新文件夹。

让我们看看以下表中的文件夹名称的含义:

文件夹 描述
pages 包含应用程序的所有模块和页面;例如,pages/bike/bike-component.htmlpages/builder/builder-component.html
layout 包含所有布局组件;例如,layout/nav/nav-component.htmllayout/footer/footer-component.html
shared 包含共享服务、管道等;例如,所有应用程序页面或组件共享的服务。

因此,在第 3 步结束时,我们将拥有以下结构:

文件夹结构

构建 PWA 的基线

正如我们之前讨论的,现在我们可以使用新的ng add命令来创建 PWA。但在此之前,让我们先了解一下 PWA 的概念。

PWA 是一套用于开发 Web 应用程序的技术,逐渐添加了以前只在原生应用中可能的功能。

用户的主要优势是他们不必在知道是否值得下载应用程序之前就下载应用程序。此外,我们可以列举以下优势:

  • 渐进式:适用于任何用户,无论使用的是哪种浏览器

  • 响应式:适用于任何设备:台式机、平板电脑和移动设备

  • 连接:即使用户处于离线状态也能工作

  • 类似应用程序:用户感觉自己就像在本机应用程序中

  • 更新:无需下载应用程序更新;浏览器将自动检测并更新,如果有必要的话

  • 安全:只有使用 HTTPs

  • 吸引力:通过推送通知,用户可以保持持续参与

  • 可安装:您可以通过单击一个图标将其添加到智能手机的主屏幕上

  • SEO 友好:搜索引擎可以找到应用程序的内容(这有利于用户和企业)

您可以在developers.google.com/web/progressive-web-apps/上阅读更多关于渐进式 Web 应用程序的信息。

尽管 PWA 在构建本机应用程序方面仍然存在一些缺点,如下所示:

  • PWA 尚未完全控制设备的硬件;蓝牙、联系人列表和 NFC 是一些无法通过 PWA 访问的功能的例子。

  • 尽管谷歌、微软和 Mozilla 对 PWA 抱有很高的期望,但苹果并没有。

  • Safari 仍然不支持两个重要功能:推送通知和离线操作。但苹果已经在考虑实现 PWA,尽管它可能没有太多选择。

对于所有的负面因素,这只是时间问题——想想看,Angular 团队已经为我们提供了使用 Angular CLI 创建 PWA 的支持。

使用 ng add 添加 PWA 功能

现在,让我们看看我们如何做到这一点。

chapter-06/Client文件夹中打开您的终端窗口,并输入以下命令:

ng add @angular/pwa

前面的命令将生成类似以下截图的输出:

Angular PWA 输出

了解 PWA 中的关键文件

让我们检查一些在我们的应用程序文件中进行的重要更改。前面的命令将在根文件夹中添加两个新文件。

manifest.json文件用于设置:

  • 主题颜色

  • 应用程序名称

  • 默认显示模式

  • 图标配置和大小

还可以设置描述标签、相关应用程序和平台。

一个ngsw-config.json文件(也称为 service worker 配置),用于设置 assetsGroup、dataGroups、navigationUrls 和 cache。

src/assets中创建了一个名为icons的新文件夹;此图标将显示为移动电话屏幕上的应用程序书签。

以下文件已更新:

  • angular.json

  • package.json添加:@angular/pwa@angular/service-worker

  • app.module.ts在生产中注册了 service-worker。这意味着我们可以通过使用生产命令来看到 service-worker 的工作;在本章的后面,我们将看到如何使用它。

  • index.html<head>标签中添加了manifest.json文件和主题颜色。

PWA 在行动

正如我们在第 4 步中提到的,Angular 引擎只在生产模式下将 service work 应用于应用程序;也就是说,只有在使用ng build命令时才会应用。

所以,让我们看看这在实践中是如何工作的。但首先,让我们看看是否一切都按预期发生了,包括应用程序的创建和@angular/pwa的安装:

  1. ./Client文件夹中打开您的终端窗口,并输入以下命令:
npm start

请记住,npm start命令与ng server相同;您可以在package.jsonscripts标签中检查所有npm别名。在那里,我们有以下别名:

     "scripts": {
                "ng": "ng",
                "start": "ng serve",
                "build": "ng build",
                "test": "ng test",
                "lint": "ng lint",
                "e2e": "ng e2e"
        }

在前面的命令结束时,我们可以看到以下消息作为输出:

** Angular Live Development Server is listening on localhost: 4200, open your browser on http://localhost:4200/ **

接下来是类似以下的输出:

Angular 开发服务器输出

  1. 打开您的默认浏览器,并导航到http://localhost:4200/

现在,您可以看到欢迎屏幕:

Angular 欢迎屏幕

让我们检查manifest.json文件。几乎所有新的浏览器都有一个 Web 检查器,我们可以在其中调试任何网站或 Web 应用程序。对于下一个检查,我们将使用 Chrome 浏览器,但您可以使用您的默认或喜爱的浏览器。

  1. 在浏览器中,点击“打开”以打开 Web 检查器。

  2. 如果您在 Chrome 中,请点击应用程序菜单选项卡。

  3. 点击左侧菜单上的清单,您应该会看到一个类似于以下截图的面板:

Web 检查器

正如您在上一张截图中所看到的,一切都如预期那样;我们的manifest.json文件可用,其中包含我们之前看到的所有配置。

请注意 Identity 标题右侧的“添加到主屏幕”链接;这意味着我们可以将此应用程序添加到手机主屏幕或浏览器应用程序的选项卡上。

  1. 但是,如果您点击此链接,您将看到一个控制台错误,如下截图所示:

服务工作者控制台错误

这意味着我们没有服务工作者,这是真的。请记住,Angular 只会在生产中注入服务工作者,我们在幕后使用**ng server **。

此外,如果您点击服务工作者右侧菜单,您将看到一个空面板。

在生产模式下运行应用程序

现在,是时候在生产模式下检查我们的应用程序,了解服务是如何工作的:

  1. 返回您的终端窗口,并使用以下命令停止 Angular 服务器:
control + c
  1. 仍然在终端中,键入build命令:
ng build --prod

请注意,前面的npm build别名命令没有使用

--prod标志。所以,你需要使用ng build --prod

命令,或使用--prod标志更新npm build命令。

在上一个命令的末尾,我们可以看到Client目录中的另一个文件夹,名为dist

Angular 服务-工作者在行动

现在,是时候启动生成在./Client/dist/Client文件夹中的应用程序,以查看服务工作者的工作情况。现在不要担心这个路径;在本书的后面,我们会进行更改:

  1. ./Client/dist/Client文件夹中打开您的终端窗口,并键入以下命令:
http-server -p 8080

请记住,我们在上一章中安装了 HTTP 服务器;如果您还没有这样做,请转到www.npmjs.com/package/http-server并按照安装过程进行操作。

  1. 在浏览器中打开http://localhost:4200/

  2. 在浏览器中,打开 Web 检查器面板,点击右侧菜单中的应用程序选项卡菜单。

您将看到以下内容:

Web 检查器应用程序面板

现在,我们已经正确配置并在我们的应用程序中运行服务工作者。

  1. 返回浏览器,点击右侧菜单中的清单菜单。

  2. 现在,点击“添加到主屏幕”链接。

恭喜!您已将我们的应用程序添加到您的应用程序面板中。如果您在 Chrome 中,您将看到以下内容:

应用程序图标

因此,如果您点击 Angular 图标,您将被重定向到http://localhost:8080/

此刻,我们已经有了 PWA 的基础。

不要担心应用程序名称;我们使用的是Client,但在现实世界中,您可以选择自己的名称。

调试渐进式 Web 应用程序

现在,我们将介绍一个非常有用的工具,用于调试渐进式 Web 应用程序。这是 Chrome 导航器的一个扩展,称为 Lighthouse:

您可以在chrome.google.com/webstore/detail/lighthouse/blipmdconlkpinefehnmjammfjpmpbjk/related?hl=us-EN获取有关 Lighthouse 的更多信息。

  1. 打开 Chrome 浏览器,点击右侧的 Lighthouse 扩展,如下截图所示:

Lighthouse 扩展

  1. 点击生成报告按钮。

生成报告后,您将看到类似以下截图的结果:

Lighthouse 报告

Lighthouse 将分析五个主要项目:

  • 性能

  • PWA

  • 可访问性

  • 最佳实践

  • 搜索引擎优化SEO

请注意,即使没有任何内容,我们在每个类别中都有一个高分级别;现在让我们专注于 SEO 类别。

让我们看看如何改进 SEO。

  1. 在左侧菜单中点击 SEO;您将看到以下截图:

上述警告告诉我们,我们的应用程序在index.html上没有 meta 描述标签。所以,让我们修复它。

./Client/src/index.html中,在 viewport meta 标签之后添加以下代码:

<metaname="description" content="Hands-On Full-Stack Web Development with Angular 6 and Laravel 5">

如果我们再次检查,我们将看到以下报告:

请注意,我们在 SEO 方面得分为 100%

这样,我们可以找到应用程序中的所有问题并正确地进行修正。

我们现在已经准备好让我们的应用程序消耗我们的 API,但是我们仍然有很多工作要做来构建前端应用程序。

在接下来的步骤中,我们将看看如何使用 Angular CLI 添加我们的组件。

创建样板 Angular 组件

正如我们之前所看到的,我们的应用程序有一些页面用于注册、登录以及摩托车列表、建造者列表和摩托车投票方案的可视化。在这一点上,我们将创建所有必要的代码来组成这些功能。

创建主页模块和组件

在接下来的几行中,我们将创建home模块和组件:

  1. ./Client/src/app中打开您的终端窗口,并键入以下命令:
ng generate module pages/home --routing

正如我们之前所看到的,上述命令将生成三个新文件:

  • src/app/pages/home/home-routing.module.ts

  • src/app/pages/home/home.modules.spec.ts

  • src/app/pages/home/home.module.ts

现在,我们只需要生成home组件。

  1. 仍然在终端中,键入以下命令:
ng g c pages/home

在上一个命令结束时,您将在pages文件夹中看到以下结构:

主页模块结构

请注意,我们创建了一个完整的模块/文件夹,就像我们之前解释的那样。现在,我们可以称新文件夹为home。我们需要将新创建的home模块导入到我们的主项目中;让我们看看如何做到这一点。

  1. 打开src/app/app.modules.ts并添加以下代码行:
// Application modules
import { HomeModule } from './pages/home/home.module';
@NgModule({
    declarations: [
    AppComponent
    ],
imports: [
    BrowserModule,
    AppRoutingModule,
    HomeModule,
    ServiceWorkerModule.register('/ngsw-worker.js', { enabled:         environment.production })
    ],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

创建摩托车模块和组件

现在,是时候创建另一个模块和组件了;让我们看看如何做到这一点:

  1. 仍然在您的终端窗口中,在./Client/src/app中,键入以下命令:
ng generate module pages/bikes --routing

正如我们之前所看到的,上述命令将生成三个新文件:

  • src/app/pages/bikes/bikes-routing.module.ts

  • src/app/pages/bikes/bikes.modules.spec.ts

  • src/app/pages/bikes/bikes.module.ts

现在,我们只需要生成bike组件。

  1. 键入以下命令:
ng g c pages/bikes

在上一个命令结束时,您将在pages文件夹中看到以下结构:

摩托车模块结构

现在,我们可以称新文件夹为bikes(作为 Bikes 模块)。我们需要将新创建的bikes模块导入到我们的主项目中;让我们看看如何做到这一点。

  1. 打开src/app/app.modules.ts并添加以下代码行:
// Application modules
import { BikesModule } from './pages/bikes/bikes.module';
@NgModule({
    declarations: [
    AppComponent
    ],
imports: [
    BrowserModule,
    AppRoutingModule,
    HomeModule,
    BikesModule,
    ServiceWorkerModule.register('/ngsw-worker.js', { enabled:         environment.production })
    ],
providers: [],
    bootstrap: [AppComponent]
})
export class AppModule { }

请注意,我们正在将新创建的BikesModule注入为app.module的依赖项。

现在,是时候为 Builders、Login 和 Register 页面执行相同的操作了。

创建 builders 模块和组件

是时候使用 Angular CLI 创建builders模块了。让我们看看我们如何做到这一点:

  1. 打开您的终端窗口并输入以下命令:
ng generate module pages/builders --routing

正如您之前所看到的,上述命令将生成三个新文件:

  • src/app/pages/builders/builders-routing.module.ts

  • src/app/pages/builders/builders.modules.spec.ts

  • src/app/pages/builders/builders.module.ts

  1. 仍然在您的终端窗口中,输入以下命令来生成组件:
ng g c pages/builders
  1. 将新创建的模块添加到应用程序模块中;打开src/app/app.modules.ts并添加以下代码:
// Application modules
import { BikesModule } from './pages/bikes/bikes.module';
import { BuildersModule } from './pages/builders/builders.module';
@NgModule({
    declarations: [
    AppComponent
    ],
imports: [
    BrowserModule,
    AppRoutingModule,
      HomeModule,
    BikesModule,
    BuildersModule,
    ServiceWorkerModule.register('/ngsw-worker.js', { enabled:         environment.production })
    ],
providers: [],
    bootstrap: [AppComponent]
})
export class AppModule { }

准备 Auth 路由 - 登录、注册和注销组件

现在,我们可以创建 Auth 路由,包括LoginRegister;同样,我们将使用 Angular CLI 的强大功能来创建新的模块和组件:

  1. 打开您的终端窗口并输入以下命令:
ng generate module pages/auth --routing
  1. 仍然在您的终端窗口中,输入以下命令来生成组件:
ng g c pages/auth/login
  1. 将新创建的模块添加到应用程序模块中;打开src/app/auth/auth.modules.ts并添加以下代码:
 import { LoginComponent } from  './login/login.component';

     @NgModule({

     imports: [

     CommonModule,

     AuthRoutingModule

     ],

     declarations: [LoginComponent]

 }) 

注意;这次,我们将LoginComponent添加到auth.module.ts中,

并没有将其添加到app.module.ts中。

现在,是时候在auth.module中创建register组件了。

  1. 打开您的终端窗口并输入以下命令:
ng g c pages/auth/register
  1. 将新创建的模块添加到应用程序模块中;打开src/app/auth/auth.modules.ts并添加以下代码:
import { RegisterComponent } from  './register/register.component';

    @NgModule({

    imports: [

    CommonModule,

    AuthRoutingModule

    ],

    declarations: [LoginComponent, RegisterComponent]

})
  1. 打开您的终端窗口并输入以下命令:
ng g c pages/auth/logout
  1. 将新创建的模块添加到应用程序模块中;打开src/app/auth/auth.modules.ts并添加以下代码:
import { LogoutComponent } from  './logout/logout.component';

@NgModule({

    imports: [

    CommonModule,

    AuthRoutingModule

    ],

    declarations: [LoginComponent, RegisterComponent, 
    LogoutComponent]

})

此时,我们的认证模块已经完成;也就是说,我们拥有了所有我们将使用的组件 - registerloginlogout。但是我们仍然需要将新模块注入到主应用程序模块中。

  1. 打开应用程序模块,打开src/app/app.modules.ts并添加以下代码:
// Application modules
import { BikesModule } from './pages/bikes/bikes.module';
import { BuildersModule } from './pages/builders/builders.module';
import { AuthModule } from './pages/auth/auth.module';
@NgModule({
    declarations: [
    AppComponent
    ],
imports: [
    BrowserModule,
    AppRoutingModule,
    BikesModule,
    BuildersModule,
    AuthModule,
    ServiceWorkerModule.register('/ngsw-worker.js', { enabled:
environment.production })
    ],
    providers: [],
    bootstrap: [AppComponent]
    })
export class AppModule { }

在这一步结束时,您将拥有以下结构:

应用程序模块结构

创建布局组件

在本节的最后一步中,我们将为应用程序的主导航创建一个布局组件。请注意,这次我们只会创建组件本身,而不包括模块和路由。

仍然在您的终端窗口中,输入以下命令:

ng g c layout/nav

上述命令将生成以下结构:

布局文件夹结构

摘要

恭喜;您刚刚完成了又一章,现在您拥有一个坚固的前端应用程序,准备接收所有需要的功能。

在本章中,我们使用 Angular 创建了一个渐进式 Web 应用程序,使用了代码组织的高级技术。您还学会了如何使用 Angular CLI 创建模块和组件。

在下一章中,我们将学习如何创建应用程序的组件和路由。

第八章:处理 Angular 路由和组件

我们来到了单页应用程序SPA)中最重要的部分之一:使用路由。正如您在第三章中所看到的,理解 Angular 6 的核心概念,Angular 框架提供了一个强大的工具来处理应用程序路由:@ angular/router 依赖项。

在接下来的几节中,您将学习如何使用其中一些功能,例如子视图,以及如何创建主细节页面。此外,我们将开始构建应用程序的视觉字段,填充模板与 HTML 标记。

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

  • 准备基线代码

  • 向应用程序添加组件

  • 处理 Angular 路由

  • 为详细页面配置子路由

  • 构建前端视图

准备基线代码

现在,我们需要准备我们的基线代码,这个过程与之前的章节非常相似。让我们按照以下步骤进行:

  1. 复制chapter-08文件夹中的所有内容。

  2. 将文件夹重命名为chapter-08

  3. 删除storage-db文件夹。

现在,让我们对docker-compose.yml文件进行一些更改,以适应新的数据库和服务器容器。

  1. 打开docker-compose.yml并用以下代码替换内容:
version: "3.1"
services:
    mysql:
      image: mysql:5.7
      container_name: chapter-08-mysql
      working_dir: /application
      volumes:
        - .:/application
        - ./storage-db:/var/lib/mysql
      environment:
        - MYSQL_ROOT_PASSWORD=123456
        - MYSQL_DATABASE=chapter-08
        - MYSQL_USER=chapter-08
        - MYSQL_PASSWORD=123456
      ports:
        - "8083:3306"
    webserver:
      image: nginx:alpine
      container_name: chapter-08-webserver
      working_dir: /application
      volumes:
        - .:/application
        - ./phpdocker/nginx/nginx.conf:/etc/nginx/conf.d/default
          .conf
      ports:
        - "8081:80"
    php-fpm:
      build: phpdocker/php-fpm
      container_name: chapter-08-php-fpm
      working_dir: /application
      volumes:
        - ./Server:/application
        - ./phpdocker/php-fpm/php-ini-
          overrides.ini:/etc/php/7.2/fpm/conf.d/99-overrides.ini

请注意,我们更改了容器名称、数据库和 MySQL 用户:

  • container_name: chapter-08-mysql

  • container_name: chapter-08-webserver

  • container_name: chapter-08-php-fpm

  • MYSQL_DATABASE=chapter-08

  • MYSQL_USER=chapter-08

  1. 将我们所做的更改添加到 Git 源代码控制中。打开您的终端窗口,输入以下命令:
 git add .
 git commit -m "Initial commit chapter 08"

向我们的应用程序添加组件

现在,我们将继续向我们的应用程序添加一些组件。我们必须记住,在应用程序摘要中,我们定义了一个页面,用于自行车列表,该页面指向我们 API 的api/bikes端点;此外,我们将有一个自行车详细信息页面,该页面指向api/bikes/id端点,包含所选自行车的详细信息。而且,我们将对api/builders端点做同样的处理。

所以,让我们开始创建组件:

  1. ./Client/src/app中打开您的终端窗口,输入以下命令:
 ng g c pages/bikes/bike-detail

在上一个命令的末尾,您将看到bikes模块中的以下结构:

自行车模块结构

上述命令将创建一个根bikes文件夹,用于存储与bikes端点相关的每个模块;这种模式允许我们拥有一个模块化的应用程序,其中每个新功能(例如bikes-detailbike-list)都将以相同的方式组织起来。

例如,我们可以添加一个新的库存模块,该模块将在其自己的模块(inventory.module.ts)中创建,并存储在bikes模块目录中。

将此视为一种良好的实践,并以这种方式组织您的模块和组件;避免将多个组件分组在同一文件夹的根目录中。这可以防止您的代码变成意大利面代码。

  1. ./Client/src/app中打开您的终端窗口,输入以下命令:
 ng g c pages/builders/builder-detail

现在,您将看到builders模块的以下结果:

构建者文件夹结构

请注意,builders模块(位于./Client/src/app/pages/builders/builders.module.ts)已更新,新添加了 Builder-detail 组件到 declarations 属性中,如下面的突出显示代码所示:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BuildersRoutingModule } from './builders-routing.module';
import { BuildersComponent } from './builders.component';
import { BuilderDetailComponent } from './builder-detail/builder-detail.component';
@NgModule({
    imports: [
        CommonModule,
        BuildersRoutingModule
    ],
    declarations: [BuildersComponent, BuilderDetailComponent]
    })
export class BuildersModule { }

最好的部分是,Angular CLI 足够聪明,可以将新创建的组件添加到其所属的模块中。当我们创建bike-detail组件时也是这样做的。

处理 Angular 路由

在这一点上,我们将继续开发我们的示例应用程序。在上一章中,我们为前端应用程序创建了一些 Angular 组件,但在编写每个组件的内容之前,我们将创建一些路由。

在我们深入代码之前,你需要了解 Angular 路由器的工作原理。

当你点击链接或转到 URL(例如http://localhost:4200/bikes)时,Angular 路由器:

  1. 检查浏览器 URL。

  2. 查找与 URL 对应的路由器状态。

  3. 应用路由守卫,如果它们在路由器状态中被定义。

  4. 激活相应的 Angular 组件以显示页面。

此外,每个路由可以包含以下属性:

  • path:字符串;匹配 URL 的路径

  • patchMatch:字符串;如何匹配 URL

  • component:类引用;当路由被激活时要激活的组件

  • redirectTo:字符串;当激活此路由时要重定向到的 URL

  • data:要分配给路由的静态数据

  • resolve:要解析和合并到数据中的动态数据,当解析时

  • children:子路由

在接下来的章节中,我们将看到两种为我们的应用程序创建路由的方法,其中一种使用子路由。

你可以在官方文档的angular.io/guide/router中了解更多关于路由的信息。

创建身份验证路由

让我们来看看我们身份验证模块的当前文件夹结构:

Auth 模块文件夹结构

在上面的截图中,请注意我们只在auth文件夹的根目录中创建了一个路由文件;我们没有在auth文件夹内的任何其他文件夹/模块中包含任何路由文件,比如loginregisterlogout。这是因为我们将使用auth-routing.module.ts文件来创建与身份验证相关的所有路由。

现在,我们将创建身份验证路由:

  1. 打开./Client/src/app/pages/auth目录中的auth-routing.module.ts文件,并在Router import之后添加以下代码块:
 // Auth Routes Imports
 import { RegisterComponent } from  './register/register.component';
 import { LoginComponent } from  './login/login.component';
 import { LogoutComponent } from  './logout/logout.component';
  1. 现在,在routes常量内添加以下代码:
 const  routes:  Routes  = [
        { path:  'register', component:  RegisterComponent },
        { path:  'login', component:  LoginComponent },
        { path:  'logout', component:  LogoutComponent }
 ];

现在,让我们开始处理应用程序的其他路由,从home模块开始。

创建 home 路由

现在,我们将创建home路由,如下所示:

  1. 打开./Client/src/app/pages/home/home-routing.module.tsimport组件:
 // Home Routes Imports
 import { HomeComponent } from  './home.component';
  1. 打开./Client/src/app/pages/home/home-routing.module.ts并在routes常量内添加以下路由对象:
 const  routes:  Routes  = [
        { path: " '', component:  HomeComponent }
 ];

由于我们的主页非常简单,只包含一个路由;稍后在其他模块中,你将看到更复杂的路由。

配置详细页面的子路由

我们将使用另一种方法来在 Angular 中创建 builders 和 bikes 路由。我们将使用子路由,也称为嵌套视图。

当你使用多个子路由时,非常重要的是要小心处理路由对象的顺序。

当路由器接收到 URL 时,它会按顺序遵循内容,从数组的第一个元素开始;如果找到与完整 URL 匹配的内容,它将停止并实例化相应的组件。

在接下来的章节中,你将看到如何实现一个名为master detail page的著名 UI 模式。我们将创建另一个组件来帮助我们组织文件夹结构。

添加 builders 子路由

让我们为我们前端应用程序中的以下视图创建子路由:

  • builders-list

  • builders-detail

  1. 打开./Client/src/app/pages/builders/builders-routing.module.tsimport组件:
imports
import { BuilderDetailComponent } from './builder-detail/builder-detail.component';
import { BuilderListComponent } from './builder-list/builder-list.component';
  1. 仍然在./Client/src/app/pages/builders/builders-routing.module.ts中,在routes常量内添加以下routes对象:
const routes: Routes = [
{
    path: 'builders',
    children: [
    {
    path: '',
component: BuilderListComponent
},
    {
    path: ':id',
    component: BuilderDetailComponent
    }
    ]
    }
];

在上面的代码片段中,你会注意到两件不同的事情:一是我们使用了children路由数组属性,另一个是一个新的组件,名为BuilderListComponent。所以,让我们创建这个新组件。

  1. ./Client/src/app内,输入以下命令:
 ng g c pages/builders/builder-list

你将在builders模块中看到以下结构:

带有 builder-list 模块的 Builders 模块

添加 bikers 子路由

让我们为我们前端应用程序中的以下视图创建子路由:

  • bike-list

  • bike-detail

现在我们将在文件顶部导入组件:

  1. 打开./Client/src/app/pages/bikes/bikes-routing.module.tsimport组件:
// Bikes Routes Imports
 import { BikeDetailComponent } from  './bike-detail/bike-detail.component';
 import { BikeListComponent } from  './bike-list/bike-list.component';
  1. 仍然在./Client/src/app/pages/bikes/bikes-routing.module.ts中,在routes常量内添加以下路由对象:
 const  routes:  Routes  = [
   { path:  'bikes',
     children: [
    {
      path:  "'',
      component:  BikeListComponent
    },{
      path:  ':id',
      component:  BikeDetailComponent
    }]
  }
 ];

现在,是时候创建新的BikeListComponent了,就像我们之前用Builders一样。

  1. ./Client/src/app中,输入以下命令:
 ng g c pages/bikes/bike-list

你将在bikes模块中看到以下结构:

带有 bike-list 模块的 bikes 模块

重构 app.component.html

正如我们之前讨论的,让我们现在让我们的视图更具吸引力。

让我们添加我们的导航组件。现在,我们不会在这个文件中放内容;我们以后会做。

打开./Client/src/app/app.component.html并用以下代码替换原代码:

 <app-nav></app-nav>
 <router-outlet></router-outlet>
     <footer  class="footer">
     <div  class="pl-3">
         <span  class="text-muted">2018 &copy; All Rights
         Reserved</span>
     </div>
     </footer>

请注意,上述代码目前没有任何内容 - 只是页脚注释的简单标记。在下一节中,你将看到如何添加更有趣的内容。

构建前端视图

大多数我们用 Angular 创建的组件都会有一个 HTML 模板,就像你在之前的章节中看到的那样:

@Component({
        selector:  'app-nav',
        templateUrl:  './nav.component.html',
        styleUrls: ['./nav.component.scss']
})

框架具有创建与其相应视图连接的组件的能力是很棒的。它具有这个功能。它还包括一个完全独立于应用程序其余部分的样式表,正如你在前面的代码中所看到的。

在下一步中,我们将添加必要的 HTML 来让我们的应用程序看起来更加愉快,就像我们在之前的章节中建议的那样。

创建导航组件

打开./Client/src/app/layout/nav/nav.component.html并用以下代码替换段落中的nav works字符串:

<header>
<nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
<a class="navbar-brand" [routerLink]="['/']" (click)="setTitle('Custom Bikes Garage')">Custom Bikes Garage</a>
    <button class="navbar-toggler" type="button" data-toggle="collapse"     data-target="#navbarCollapse" aria-controls="navbarCollapse"
    aria-expanded="false" aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
    </button>
    <div class="collapse navbar-collapse" id="navbarCollapse">
    <ul class="navbar-nav ml-auto">
    <li class="nav-item">
    <a class="nav-link" [routerLink]="['/bikes']" 
    routerLinkActive="active" (click)="setTitle('Bikes')">Bikes</a>
    </li>
    <li class="nav-item">
    <a class="nav-link" [routerLink]="['/builders']"
             routerLinkActive="active"
         (click)="setTitle('Builders')">Builders</a>
    </li>
    <li class="nav-item">
    <a class="nav-link" [routerLink]="['/login']"
     routerLinkActive="active" (click)="setTitle('Login')">Login</a>
    </li>
    <li class="nav-item">
    <a class="nav-link" [routerLink]="['/register']"
     routerLinkActive="active"
         (click)="setTitle('Register')">Register</a>
    </li>
    <li class="nav-item">
    <a class="nav-link" [routerLink]="['/logout']"
     routerLinkActive="active">Logout</a>
    </li>
    </ul>
    </div>
</nav></header>

关于上述代码有两个重要的事情:

  • 我们正在使用routerLink属性;在本章的后面部分,你将看到如何使用它。

  • 我们正在使用Title服务来使用<title>标签设置页面标题,这是 Angular 内置的服务。由于我们正在构建一个 SPA,我们需要使用这个资源来给我们的视图一个标题;如果没有它,我们应用程序中的所有页面都将具有相同的客户端名称。请记住,当我们首次使用 Angular CLI 创建应用程序时,Title标签已经设置好了,并且将接收我们定义的应用程序名称。

让我们更新<title>标签,如下所示:

  1. 打开./Client/src/app/layout/nav/nav.component.ts并添加以下代码:
import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
@Component({
    selector: 'app-nav',
        templateUrl: './nav.component.html',
        styleUrls: ['./nav.component.scss']
    })
    export class NavComponent implements OnInit {
    public constructor(private titleTagService: Title ) { }
    public setTitle( pageTitle: string) {
    this.titleTagService.setTitle( pageTitle );
    }
    ngOnInit() {
    }
}
  1. 打开./Client/src/app/app.module.ts并将Title导入添加到文件顶部:
import { BrowserModule, Title } from  '@angular/platform-browser';
  1. 现在,将Title提供者添加到@ngModules提供者中:
providers: [
Title
],

因此,如果我们再次在浏览器中检查相同的 URL(http://localhost:4200/),我们可以看到一个链接列表,并且我们可以通过它们进行导航。结果将类似于以下截图:

导航链接

不要担心我们标记中的类名;在本书的后面,我们将添加一些样式表,包括一些 Bootstrap 组件。

创建 home 视图和模板

打开./Client/src/app/pages/home/home.component.html并用以下代码替换段落中的home works字符串:

<main role="main">
<div class="jumbotron">
<div class="container text-center">
<h1 class="display-3 ">Custom Bikes Garage</h1>
<p>Motorcycle builders and road lovers</p>
<p>
<a class="btn btn-primary btn-lg" [routerLink]="['/register']"role="button">Register</a>
</p>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-md-4">
<h2>Heading</h2>
<p>Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum
nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui.
</p>
<p>
<a class="btn btn-secondary" href="#" role="button">View details &raquo;</a>
</p>
</div>
<div class="col-md-4">
<h2>Heading</h2>
<p>Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum
nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui.
</p>
<p>
<a class="btn btn-secondary" href="#" role="button">View details &raquo;</a>
</p>
</div>
<div class="col-md-4">
<h2>Heading</h2>
<p>Donec sed odio dui. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Vestibulum id ligula porta felis euismod
semper. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet
risus.</p>
<p>
<a class="btn btn-secondary" href="#" role="button">View details &raquo;</a>
</p>
</div>
</div>
</div>
</main>

创建 bikes router-outlet

打开./Client/src/app/pages/bikes/bikes.component.html并用以下代码替换段落中的bikes works字符串:

<router-outlet></router-outlet>

创建 bike-list 视图和模板

打开./Client/src/app/pages/bikes/bike-list/bike-list.component.html并用以下代码替换段落中的bike-list works 字符串:

<main role="main">
<div class="py-5 bg-light">
<div class="container">
<form>
<div class="form-group row">
<label for="search" class="col-sm-2 col-form-label">Bike List</label>
<div class="col-sm-8">
<input type="text" class="form-control" id="search"placeholder="Search">
</div>
<div class="col-sm-2">
<div class="dropdown">
<button class="btn btn-outline-primary dropdown-toggle btn-block" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Filter
</button>
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
<a class="dropdown-item" href="#">Action</a>
</div>
</div>
</div>
</div>
</form>
<div class="row">
<div class="col-md-4">
<div class="card mb-4 box-shadow">
<img class="card-img-top" src="https://dummyimage.com/640x480/717171/fff.jpg&text=placeholder-image" alt="Card image cap">
<div class="card-body">
<p>Model | year</p>
<p class="card-text">This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.</p>
<div class="d-flex justify-content-between align-items-center">
    <div class="btn-group">
    <button routerLink="/bikes/1" type="button" class="btn btn-sm
      btn-    outline-primary">View</button>
    <button type="button" class="btn btn-sm btn-outline-
        primary">Vote</button>
</div>
<small class="text-muted">4 ratings</small>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card mb-4 box-shadow">
<img class="card-img-top"src="https://dummyimage.com/640x480/717171/fff.jpg&text=placeholder-image" alt="Card image cap">
<div class="card-body">
<p>Model | year</p>
<p class="card-text">This is a wider card with supporting text below as a natural lead-in to additional content. This content is
a little bit longer.</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<button routerLink="/bikes/2" type="button" class="btn btn-sm btn-outline-primary">View</button>
<button type="button" class="btn btn-sm btn-outline-primary">Vote</button>
</div>
<small class="text-muted">9 ratings</small>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card mb-4 box-shadow">
<img class="card-img-top" src="https://dummyimage.com/640x480/717171/fff.jpg&text=placeholder-image" alt="Card image cap">
<div class="card-body">
<p>Model | year</p>
<p class="card-text">This is a wider card with supporting text below as a natural lead-in to additional content. This content is
a little bit longer.</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<button routerLink="/bikes/3" type="button" class="btn btn-sm btnoutline-primary">View</button>
<button type="button" class="btn btn-sm btn-outline-primary">Vote</button>
</div>
<small class="text-muted">5 ratings</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</main>

创建 bike-detail 视图和模板

打开./Client/src/app/pages/bikes/bike-detail/bike-detail.component.html并用以下代码替换段落中的bike-detail works 字符串:

<main role="main">
<div class="py-5">
<div class="container">
<div class="row">
<div class="col-md-4">
   <img class="card-img-top"  
   src="https://dummyimage.com/340x280/717171/fff.jpg&text=placeholder-
   image" alt="Card image cap">
</div>
<div class="col-md-8">
<div class="card">
<div class="card-body">
    <h5 class="card-title">Card title | Year | Ratings</h5>
    <p class="card-text">Some quick example text to build on the card
     title and make up the bulk of the card's content.</p>
</div>
    <div class="card-header">
        Builder Name
    </div>
<div class="card-header">
    Featured items
</div>
<ul class="list-group list-group-flush">
    <li class="list-group-item">Cras justo odio</li>
    <li class="list-group-item">Dapibus ac facilisis in</li><li
         class="list-group-item">Vestibulum at eros</li>
</ul>
    <div class="card-body">
        <a href="#" class="card-link">Vote</a>
    </div>
</div>
</div>
</div>
</div>
</div>
</main>

创建构建器 router-outlet

打开./Client/src/app/pages/builders/builders.component.html并用以下代码替换带有builders works 字符串的段落:

<router-outlet></router-outlet>

创建构建者列表视图和模板

打开./Client/src/app/pages/builders/builder-list/builder-list.component.html并用以下代码替换带有以下代码的段落:

<main role="main">
<div class="py-5 bg-light">
<div class="container">
<div class="card-deck mb-3 text-center">
<div class="card mb-4 box-shadow">
<div class="card-header">
<h4 class="my-0 font-weight-normal">Builder Name</h4>
</div>
<div class="card-body">
    <p class="mt-3 mb-4">
    Lorem ipsum dolor sit amet consectetur, adipisicing elit. Quam
     aspernatur sit cum necessitatibus.
    </p>
    <button routerLink="/builders/1" type="button" class="btn btn-lg     btn-block btn-outline-primary">View Bikes</button>
</div>
<div class="card-footer text-muted">
City/State
</div>
</div>
<div class="card mb-4 box-shadow">
<div class="card-header">
    <h4 class="my-0 font-weight-normal">Builder Name</h4>
</div>
<div class="card-body">
    <p class="mt-3 mb-4">
    Lorem ipsum dolor sit amet consectetur, adipisicing elit. Quam
     aspernatur sit cum necessitatibus.
</p>
    <button routerLink="/builders/2" type="button" class="btn btn-lg
     btn-block btn-outline-primary">View Bikes</button>
</div>
<div class="card-footer text-muted">
City/State
</div>
</div>
<div class="card mb-4 box-shadow">
<div class="card-header">
    <h4 class="my-0 font-weight-normal">Builder Name</h4>
</div>
<div class="card-body">
    <p class="mt-3 mb-4">
    Lorem ipsum dolor sit amet consectetur, adipisicing elit. Quam
     aspernatur sit cum necessitatibus.
</p>
    <button routerLink="/builders/3" type="button" class="btn btn-lg
     btn-block btn-outline-primary">View Bikes</button>
</div>
<div class="card-footer text-muted">
City/State
</div>
</div>
</div>
</div>
</div>
</main>

创建构建者详情视图和模板

打开./Client/src/app/pages/builders/builder-detail/builder-detail.component.html并用以下代码替换带有builder-detail works 字符串的段落:

<main role="main">
<div class="py-5">
<div class="container">
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-body">
    <h5 class="card-title">Builder Name</h5>
    <p class="card-text">Some quick example text to build on the card     title and make up the bulk of the card's content.</p>
</div>
<div class="card-header">
    Featured Bikes
</div>
    <ul class="list-group list-group-flush">
    <li class="list-group-item">Cras justo odio</li>
    <li class="list-group-item">Dapibus ac facilisis in</li>
    <li class="list-group-item">Vestibulum at eros</li>
    </ul>
</div>
</div>
</div>
</div>
</div>
</main>

创建登录视图和模板

打开./Client/src/app/pages/auth/login/login.component.html并用以下代码替换带有login works 字符串的段落:

<main role="main">
<div class="container">
<form class="form-signin">
<div class="text-center mb-4">
    <h1 class="h3 mt-3 mb-3 font-weight-normal">Welcome</h1>
    <p>Motorcycle builders and road lovers</p>
    <hr>
</div>
<div class="form-group">
    <label for="email">Email address</label>
    <input type="email" class="form-control" id="email"
     ariadescribedby="emailHelp" placeholder="Enter email">
</div>
    <div class="form-group">
    <label for="password">Password</label>
    <input type="password" class="form-control" id="password" 
        placeholder="Password">
</div>
    <button class="btn btn-lg btn-primary btn-block mt-5"
         type="submit">Login</button>
</form>
</div>
</main>

创建注册视图和模板

打开./Client/src/app/pages/auth/register/register.component.html并用以下代码替换带有register works 字符串的段落:

<main role="main">
<div class="container">
<form class="form-signin">
<div class="text-center mb-4">
<h1 class="h3 mt-3 mb-3 font-weight-normal">Welcome</h1>
<p>Motorcycle builders and road lovers</p>
<hr>
</div>
<div class="form-group">
<label for="name">Name</label><input type="name" class="form-control" id="name" aria-describedby="nameHelp" placeholder="Enter your name">
</div>
<div class="form-group">
    <label for="email">Email address</label>
    <input type="email" class="form-control" id="email" aria-
    describedby="emailHelp" placeholder="Enter email">
</div>
<div class="form-group">
    <label for="password">Password</label>
    <input type="password" class="form-control" id="password"
     placeholder="Password">
</div>
    <button class="btn btn-lg btn-primary btn-block mt-5" 
    type="submit">Register</button>
</form>
</div>
</main>

我们现在在模板中有了必要的代码。但是目前不用担心样式表;在接下来的章节中,您将看到应用样式表之前应用的一些更重要的点。让我们来看看我们目前有什么。

测试路由和视图

让我们以开发模式启动应用程序并检查一些 URL,以查看我们路由和模板的结果:

  1. ./Client文件夹中打开您的终端窗口,然后输入以下命令:
npm start
  1. 打开您的默认浏览器,然后转到http://localhost:4200/bikes/1

您将看到一个非常类似于以下截图的结果:

自行车详情页面

总结

您已经完成了另一章的学习。在这一章中,您学会了如何在模块中创建额外的组件,比如bikes模块。您使用了 Angular 路由添加了一些路由,并学会了如何使用子路由。此外,您还学会了如何创建导航组件并使用 Angular 默认服务更新页面的<title>标签。

第九章:创建服务和用户认证

在本章中,我们有很多工作要做。我们将创建许多新东西,并对一些东西进行重构。这是以一种规律和渐进的方式学习东西的好方法。

我们将深入研究 Angular 的 HTTP 模块的操作和使用,该模块被称为HttpClient

此外,我们将看到如何使用拦截器和处理错误。

Angular 的新版本提供了非常有用的工具来创建现代 Web 应用程序,在本章中,我们将使用其中许多资源。

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

  • 处理模型和类

  • 使用新的HttpModuleHttpModuleClient来处理 XHR 请求

  • 处理HttpErrorHandler服务

  • 如何使用授权头

  • 如何使用路由守卫保护应用程序路由

准备基线代码

现在,我们需要准备我们的基线代码,这个过程与我们在上一章中所做的非常相似。让我们按照以下步骤进行:

  1. 复制chapter-08文件夹中的所有内容。

  2. 将文件夹重命名为chapter-09

  3. 删除storage-db文件夹。

现在,让我们对docker-compose.yml文件进行一些更改,以使其适应新的数据库和服务器容器。

  1. 打开docker-compose.yml并用以下代码替换其内容:
 version: "3.1"
 services:
     mysql:
       image: mysql:5.7
       container_name: chapter-09-mysql
       working_dir: /application
       volumes:
         - .:/application
         - ./storage-db:/var/lib/mysql
       environment:
         - MYSQL_ROOT_PASSWORD=123456
         - MYSQL_DATABASE=chapter-09
         - MYSQL_USER=chapter-09
         - MYSQL_PASSWORD=123456
       ports:
         - "8083:3306"
     webserver:
       image: nginx:alpine
       container_name: chapter-09-webserver
       working_dir: /application
       volumes:
         - .:/application
         -./phpdocker/nginx/nginx.conf:/etc/nginx/conf.d/default
         .conf
       ports:
         - "8081:80"
     php-fpm:
       build: phpdocker/php-fpm
       container_name: chapter-09-php-fpm
       working_dir: /application
       volumes:
         - ./Server:/application
         - ./phpdocker/php-fpm/php-ini-
           overrides.ini:/etc/php/7.2/fpm/conf.d/99-overrides.ini

请注意,我们更改了容器名称、数据库和 MySQL 用户:

  • container_name: chapter-09-mysql

  • container_name: chapter-09-webserver

  • container_name: chapter-09-php-fpm

  • MYSQL_DATABASE=chapter-09

  • MYSQL_USER=chapter-09

  1. 将我们所做的更改添加到 Git 源代码控制中。打开您的终端窗口并输入以下命令:
 git add .
 git commit -m "Initial commit chapter 09"

处理模型和类

由 Angular 开发者社区认为是良好实践(我们认为是必不可少的)的是创建类以将其用作模型。这些也被称为领域模型

我们认为创建类来存储我们的模型是创建大型应用程序甚至小型应用程序的一个非常重要的资源。这有助于保持代码的组织性。

想象一下,如果我们的项目规模更大——如果所有数据都存储在普通对象中,那么新开发人员将很难找到数据存储的位置。

这也是使用类来存储我们的模型信息的一个很好的理由。

创建用户类模型

让我们首先创建一个类来存储我们的用户信息。按照惯例,我们将把这个文件命名为user.ts

  1. 打开您的终端窗口。

  2. 转到./Client/src/app并输入以下命令:

 ng g class pages/auth/user
  1. 上一个命令将在./app/pages/auth/auth.ts中创建一个新文件。打开此文件并添加以下代码:
 export  class  User {
        name?:  string;
        email?:  string;
        password?:  string;
        constructor() {}
 }

创建构建者类模型

现在,让我们为构建者创建模型,并更好地理解类作为模型的操作。在此之前,我们将观察当我们对api/builders/1端点进行 GET 请求时 API 的返回,如下面的屏幕截图所示:

构建者详细 JSON 结果

在先前的屏幕截图中,我们已经在构建者详细请求中包含了自行车信息。让我们看看如何使用builders类来实现这一点:

  1. 仍然在您的终端中,输入以下命令:
 ng g class pages/builders/builder
  1. 上一个命令将在./app/pages/builders/builder.ts中创建一个新文件。打开此文件并添加以下代码:
 import { Bike } from  '../bikes/bike';

 export  class  Builder {
        id:  number;
        name:  string;
        description:  string;
        location:  string;
        bike?:  Bike;

        constructor() {}
 }

请注意,在先前的代码中,我们添加了一个可选的bike属性,并将其类型设置为Bike模型。

创建 Bike 类模型

现在,是时候创建自行车模型类了,但首先让我们检查一下我们在自行车详细端点api/bikes/2上的 JSON 格式,如下面的屏幕截图所示:

自行车详细 JSON 结果

在这里,我们可以注意到bike-detail结果指向garagesitemsbuilderuserratings。对于我们正在构建的示例应用程序,我们将只使用构建者和用户模型。不用担心其他的;我们在这里使用的示例足以理解模型领域:

  1. 仍然在您的终端中,输入以下命令:
 ng g class pages/bikes/bike
  1. 上一个命令将在./app/pages/bikes/bike.ts中创建一个新的文件。打开这个文件并添加以下代码:
 import { User } from  './../auth/user';
 import { Builder } from  '../builders/builder';

 export  class  Bike {
        id:  number;
        make:  string;
        model:  string;
        year:  string;
        mods:  string;
        picture:  string;
        user_id:  number;
        builder_id:  number;
        average_rating?: number;
        user?:  User;
        builder?:  Builder;
        items?:  any;
        ratings?:  any;

        constructor() {}
 }

请注意,在上一个代码中,我们使用了上一个截图中的所有属性,包括itemsratings,作为类型为any的可选属性,因为我们没有为这些属性创建模型。

使用新的 HttpClient 处理 XHR 请求

如今,绝大多数 Web 应用程序都使用XMLHttpRequest(XHR)请求,而使用 Angular 制作的应用程序也不例外。为此,我们有HTTPClient模块取代了以前版本中的旧 HTTP 模块。

在这个会话中,我们将了解如何在我们的 Angular 服务中使用 XHR 请求。

强烈建议您使用 Angular 服务来处理这种类型的请求,以便组件的代码更有组织性和易于维护。

您可以在developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest上阅读更多关于 XHR 请求的信息。

创建认证服务

让我们创建一个将存储我们认证模块所需代码的文件:

  1. 仍然在您的终端中,输入以下命令:
 ng g service pages/auth/_services/auth

上一个命令将在./app/pages/auth/_services/auth.service.ts中创建一个新的文件夹和文件。现在,让我们添加一些代码。

  1. 打开./app/pages/auth/_services/auth.service.ts并在文件顶部添加以下导入:
 import { HttpClient, HttpParams, HttpErrorResponse } from  '@angular/common/http';
 import { HttpHeaders } from  '@angular/common/http';
 import { Router } from  '@angular/router';
 import { Observable, throwError } from  'rxjs';
 import { catchError, map, tap } from  'rxjs/operators';

 // App imports
 import { environment } from  './../../../environments/environment';
 import { User } from  './user';

现在,我们将使用HttpHeaders来设置我们的 XHR 请求的内容类型。

  1. 在导入文件后添加以下代码:
 // Setup headers
 const httpOptions  = {
        headers: new  HttpHeaders({
                'Content-Type': 'application/json'
        })
 };

上一个代码示例将使用HttpHeaders为我们的请求添加一个新的头。

  1. AuthService类内部,添加以下代码:
 public  currentUser:  User;
 private  readonly  apiUrl  =  environment.apiUrl;
 private  registerUrl  =  this.apiUrl  +  '/register';
 private  loginUrl  =  this.apiUrl  +  '/login';

您一定会问为什么currentUserpublic而其他的是private,对吧?

嗯,currentUser属性是public的,因为我们将在其他文件中访问它,正如我们将在本节后面看到的那样。因此,其他属性将不会在AuthService之外可用。

  1. 现在,让我们创建我们的constructor函数。在constructor函数内部,添加以下代码:
 private  http:  HttpClient, private  router:  Router
  1. constructor类将如下代码所示:
     constructor(
                private  http:  HttpClient,
                private  router:  Router) {}

请注意,我们在这里使用了HttpClientRouter模块,所以现在是时候编写我们的函数来看看这个模块的实际应用了。

创建注册函数

让我们创建Register函数。在constructor函数之后,添加以下代码:

     onRegister(user: User): Observable<User> {
                const request  =  JSON.stringify(
                        { name: user.name, email: user.email, password:
                 user.password }
                );
                return  this.http.post(this.registerUrl, request,
                httpOptions)
                .pipe(
                        map((response:  User) => {
                                // Receive jwt token in the response
                                const  token: string  =
                                response['access_token'];
                                // If we have a token, proceed
                                if (token) {
                                        this.setToken(token);
                                        this.getUser().subscribe();
                                }
                                return  response;
                        }),
                catchError(error  =>  this.handleError(error))
                );
        }

请注意,我们在这里使用了Reactive Extensions Library for JavaScriptRxJS)中包含的pipe()map()catchError()函数。

在使用 RxJS 库之前,在 AngularJS 应用程序中使用一个叫做 Lodash 的库来操作结果是非常常见的。

您可以在官方文档链接rxjs-dev.firebaseapp.com/api中阅读更多关于 RxJS 库的信息。

我们使用pipe()函数,它允许我们链接其他函数,当我们使用可观察对象时,这是非常有趣的。在pipe()函数内部,这正是我们在map()catchError()函数中所做的。

此外,我们还使用了三个名为setToken()getUser()handleError()的本地函数,我们稍后会看到它们。

请记住,函数名非常重要。尽量使用像我们在setTokengetUser中所做的那样自解释的名称。

创建登录函数

Login函数的结构几乎与Register函数相同。不同之处在于我们只是将电子邮件地址和密码发送到服务器。

onRegister()函数之后添加以下代码:

     onLogin(user: User): Observable<User> {
                const request  =  JSON.stringify(
                        { email: user.email, password: user.password }
                );
                return  this.http.post(this.registerUrl, request,
                httpOptions)
                .pipe(
                        map((response:  User) => {
                                // Receive jwt token in the response
                                const  token: string  = 
                                response['access_token'];
                                // If we have a token, proceed
                                if (token) {
                                        this.setToken(token);
                                        this.getUser().subscribe();
                                }
                                return  response;
                        }),
                catchError(error  =>  this.handleError(error))
                );
        }

请注意,我们使用setToken()函数保存用户令牌,并使用getUser()函数获取用户的详细信息。我们将在本节后面详细介绍这一点。

创建注销函数

对于注销函数,我们将使用不同的方法。我们将使用tap()操作符,而不是使用map()操作符。

onLogin()函数之后添加以下代码:

onLogout():  Observable<User> {
        return  this.http.post(this.apiUrl  +  '/logout',
          httpOptions).pipe(
                tap(
                        () => {
                                localStorage.removeItem('token');
                                this.router.navigate(['/']);
                                }
                        )
                );
}

在上述代码中,我们只是从localStorage中删除令牌,并将用户重定向到主页。现在,是时候创建处理数据的本地函数了。

创建设置令牌和获取令牌函数

我们几乎已经完成了我们的身份验证服务,但我们仍然需要创建一些辅助函数,这些函数将在其他应用程序块中使用。

让我们创建处理用户令牌的函数。重新创建我们在 Laravel 后端中使用的jwt-auth库来进行调用,用于验证我们的用户。

在本示例中,我们使用localStorage来存储用户的令牌。因此,让我们创建两个非常简单的函数来写入和检索此令牌。

logout()函数之后,添加以下代码块:

setToken(token:  string):  void {
        return  localStorage.setItem('token', token );
}

getToken():  string {
        return  localStorage.getItem('token');
}

创建获取用户函数

现在,我们将看到如何获取已登录用户的信息。请记住,我们的 API 有一个端点,根据认证令牌为我们提供已登录用户的信息。

让我们看看如何以简单的方式做到这一点。

getToken()函数之后添加以下代码:

getUser():  Observable<User> {
        return  this.http.get(this.apiUrl  +  '/me').pipe(
                tap(
                        (user: User) => {
                                this.currentUser  =  user;
                        }
                )
        );
}

上述代码从 API 接收用户信息,并将其应用于currentUser属性。

创建 isAuthenticated 函数

现在,我们将创建一个额外的函数。这个函数将帮助我们确定用户是否已登录。

getUser()函数之后添加以下代码:

  isAuthenticated():  boolean { // get the token
  const  token:  string  =  this.getToken();
  if (token) {
  return  true;
 }  return  false;
 }

现在,我们可以在任何地方使用AuthService.currentUserAuthService.isAuthenticated方法来使用这些信息。

创建 handleError 函数

您应该已经注意到login()register()函数具有指向另一个名为handleError的函数的catchError函数。此刻,我们将创建这个函数,负责显示我们的请求可能出现的错误。

getUser()函数之后添加以下代码:

private  handleError(error:  HttpErrorResponse) {
        if (error.error  instanceof  ErrorEvent) {
                // A client-side error.
                console.error('An error occurred:',
                error.error.message);
        } else {
                // The backend error.
                return  throwError(error);
        }
        // return a custom error message
        return  throwError('Ohps something wrong happen here; please try again later.');
}

我们将错误消息记录到浏览器控制台,仅供本示例使用。

创建自行车服务

现在,我们将创建一个服务来保存所有自行车操作。请记住,对于自行车和建造者,我们的服务必须具有用于列出、详细信息、创建、更新和删除的方法:

  1. 仍然在您的终端中,键入以下命令:
 ng g service pages/bikes/_services/bike

上述命令将在./app/pages/bikes/_services/bike.service.ts中创建一个新的文件夹和文件。现在,让我们添加一些代码片段。

  1. 打开./app/pages/bikes/_services/bike.service.ts并将以下导入添加到文件顶部:
 import { Injectable } from  '@angular/core';
 import { HttpClient, HttpParams, HttpErrorResponse } from  '@angular/common/http';
 import { HttpHeaders } from  '@angular/common/http';
 import { Observable, throwError } from  'rxjs';
 import { catchError } from  'rxjs/operators';

 // App import
 import { environment } from  '../../../../environments/environment';
 import { Bike } from  '../bike';
  1. bikesService类中,添加以下属性:
 private  readonly  apiUrl  =  environment.apiUrl;
 private  bikesUrl  =  this.apiUrl  +  '/bikes';
  1. 现在,让我们创建我们的constructor函数。在constructor函数中,添加以下代码:
 constructor(private  http:  HttpClient) {}

现在,我们准备创建我们的自行车服务的函数。

创建 CRUD 函数

正如我们之前提到的,CRUD代表CreateReadUpdateDelete。我们将一次性添加操作的代码,然后进行必要的注释。

constructor()函数之后添加以下代码块:

 /** GET bikes from bikes endpoint */
 getBikes ():  Observable<Bike[]> {
        return  this.http.get<Bike[]>(this.bikesUrl)
        .pipe(
                catchError(error  =>  this.handleError(error))
        );
 }

 /** GET bike detail from bike-detail endpoint */
 getBikeDetail (id:  number):  Observable<Bike[]> {
        return  this.http.get<Bike[]>(this.bikesUrl  +  `/${id}`)
        .pipe(
                catchError(error  =>  this.handleError(error))
        );
 }

 /** POST bike to bikes endpoint */
 addBike (bike:  Bike):  Observable<Bike> {
        return  this.http.post<Bike>(this.bikesUrl, bike)
        .pipe(
                catchError(error  =>  this.handleError(error))
        );
 }

 /** PUT bike to bikes endpoint */
 updateBike (bike:  Bike, id:  number):  Observable<Bike> {
        return  this.http.put<Bike>(this.bikesUrl  +  `/${id}`, bike)
        .pipe(
                catchError(error  =>  this.handleError(error))
        );
 }

 /** DELETE bike bike endpoint */
 deleteBike (id:  number):  Observable<Bike[]> {
        return  this.http.delete<Bike[]>(this.bikesUrl  +  `/${id}`)
        .pipe(
                catchError(error  =>  this.handleError(error))
        );
 }

 /** Vote on bike */
 voteOnBike (vote:  any, bike:  number):  Observable<any> {
        const  rating  =  vote;
        return  this.http.post(this.bikesUrl  +  `/${bike}/ratings`, {rating})
        .pipe(
                catchError(error  =>  this.handleError(error))
        );
 }

上述代码与我们在身份验证服务中使用的内容没有特别不同,除了使用模板字符串:

this.bikesUrl  +  `/${id}`
this.bikesUrl  +  `/${bike}/ratings`, {rating}

这些由反引号(`)字符包围,而不是单引号或双引号,以及以美元符号开头的表达式。

创建 voteOnBike 函数

我们的服务仍然有一个功能,我们将用它来发送用户对特定自行车的投票。请记住,每当需要使用 HTTPClient 模块时,请在服务中执行此操作。这在 Angular 开发中被认为是一个良好的实践。

deleteBike() 函数之后添加以下代码:

     /** Vote on bike */
        voteOnBike (vote:  number, bike:  number):  Observable<any> {
                const  rating  =  vote;
                return  this.http.post(this.bikesUrl  + 
                `/${bike}/ratings`, {rating})
                .pipe(
                        catchError(error  =>  this.handleError(error))
                );
        }

创建 handleError 函数

现在,让我们为自行车服务添加错误处理。在 voteOnBike() 函数之后添加以下代码:


     /** Error handler */
        private  handleError(error:  HttpErrorResponse) {
                if (error.error  instanceof  ErrorEvent) {
                        // A client-side error.
                        console.error('An error occurred:', 
                error.error.message);
                } else {
                        // The backend error.
                        return  throwError(error);
                }
                // return a custom error message
                return  throwError('Something bad happened; please try
                again later.');
        }

正如我们所看到的,在自行车服务中的 handleError() 函数与认证服务相同,并且在构建者服务上也是一样的。每当需要多次编写相同的代码时,强烈建议使用服务来避免代码的重复。

之后,我们将创建一个解决这个问题的服务,但现在我们将创建构建者服务。

创建构建者服务

现在,我们将创建 builder 服务,其中包括 CreateReadUpdateDelete 方法:

  1. 仍然在你的终端中,输入以下命令:

ng g service pages/builders/_services/builder

前述命令将在 ./app/pages/builders/_services/builder.service.ts 中创建一个新的文件夹和文件。现在,让我们添加一些代码片段。

  1. 打开 ./app/pages/builders/_services/builder.service.ts,并将其代码替换为以下代码块:

     import { Injectable } from  '@angular/core';

        import { HttpClient, HttpParams, HttpErrorResponse } from
       '@angular/common/http';
        import { HttpHeaders } from  '@angular/common/http';
        import { Observable, throwError } from  'rxjs';
        import { catchError } from  'rxjs/operators';

        // App import
        import { environment } from
        '../../../../environments/environment';
        import { Builder } from  '../builder';
        @Injectable({
                providedIn: 'root'
        })

        export  class  BuildersService {
                private  readonly  apiUrl  =  environment.apiUrl;
                private  buildersUrl  =  this.apiUrl  +
         '/builders';
                
                constructor(private  http:  HttpClient) { }

                /** GET builders from builders endpoint */
                getBuilders ():  Observable<Builder[]> {
                        return  this.http.get<Builder[]>
      (this.buildersUrl)
                                .pipe(
                                        catchError(error  =>
       this.handleError(error))
                                );
                }

                /** GET builder detail from builder-detail endpoint
        */
                getBuilderDetail (id:  number):
        Observable<Builder[]> {
                return  this.http.get<Builder[]>(this.buildersUrl  +  
        `/${id}`)
                        .pipe(
                                catchError(error  => 
        this.handleError(error))
                        );
                }

                /** POST builder to builders endpoint */
                addBuilder (builder:  Builder):  Observable<Builder> 
         {
                        return  this.http.post<Builder>
           (this.buildersUrl, builder)
                                .pipe(
                                        catchError(error  =>
           this.handleError(error))
                                );
                }

                /** PUT builder to builders endpoint */
                updateBuilder (builder:  Builder, id:  number):
           Observable<Builder> {
                        return  this.http.put<Builder>
           (this.buildersUrl  +  `/${id}`, builder)
                                .pipe(
                                        catchError(error  =>
            this.handleError(error))
                                );
                }

                /** DELETE builder builder endpoint */
                deleteBuilder (id:  number):  Observable<Builder[]>
            {
                        return  this.http.delete<Builder[]>
            (this.buildersUrl  +  `/${id}`)
                                .pipe(
                                        catchError(error  =>
            this.handleError(error))
                                );
                }

                /** Error handler */
                private  handleError(error:  HttpErrorResponse) {
                        if (error.error  instanceof  ErrorEvent) {
                                // A client-side error.
                                console.error('An error occurred:',
             error.error.message);
                        } else {
                                // The backend error.
                                return  throwError(error);
                        }
                        // return a custom error message
                        return  throwError('Something bad happened;
             please try again later.');
                }
        }

前述代码与自行车服务几乎相同,我们可以注意到最后一个函数是 handleError() 函数,因此现在是学习如何创建错误服务的时候了。

处理 HttpErrorHandler 服务

如前所述,在现代 Web 应用程序中重复代码并不是一个好的实践,因此我们可以使用许多资源来避免这种实践。在 Angular 开发中,我们可以使用共享服务在一个地方处理应用程序错误。

创建错误处理服务

如本章早些时候提到的,让我们创建我们的错误处理程序服务:

  1. ./Client/src/app 内打开你的终端窗口,然后输入以下命令:

ng g service pages/shared/_services/httpHandleError

上述命令将在 pages/shared 文件夹内创建一个名为 _services 的新文件夹,原因很简单:我们将在 bikesbuildersauth 模块中创建的所有服务之间共享此服务。上述命令还创建了一个名为 http-handle-error.service.ts 的文件。

  1. 打开 ./Client/src/app/shared/_services/http-handle-error.service.ts 并添加以下导入:

import { HttpErrorResponse } from  '@angular/common/http';
import { Observable, of } from  'rxjs';

  1. 让我们为我们的错误创建一个 Angular type。在导入之后添加以下代码:

export  type  HandleError  =
        <T> (operation?:  string, result?:  T) => (error:  HttpErrorResponse) =>  Observable<T>;

上述代码创建了一个名为 HandleError 的新类型,并且我们将在接下来的行中使用它。

请记住,Angular 有许多类型,如数组、空、任何更多。我们在第三章 理解 Angular 6 的核心概念 中已经看到了这一点。

  1. 让我们添加错误函数。在 constructor() 函数之后添加以下代码块:

     /** Pass the service name to map errors */
        createHandleError  = (serviceName  =  '') => <T>
                (operation  =  'operation', result  = {} as  T) =>
        this.handleError(serviceName, operation, result)
        handleError<T> (serviceName  =  '', operation  =
       'operation', result  = {} as  T) {
                return (response:  HttpErrorResponse):
                Observable<T> => {
                        // Optionally send the error to a third part
                      error logging service
                        console.error(response);
                        
                        // Show a simple alert if error
                        const  message  = (response.error
                        instanceof  ErrorEvent) ?
                        response.error.message  :
                        `server returned code ${response.status}
                        with body "${response.error.error}"`;
                        
                        // We are using alert just for example, on
                        real world avoid this pratice
                        alert(message);
                        
                        // Keep running and returning a safe result.
                        return  of( result );
                };
        }

上面的代码创建了一个名为handleError的函数,接收三个参数——serviceNameoperationresult——并返回一个名为HandleError的可观察类型。

我们还使用基本内置的 JavaScript 函数来向用户显示警报,如果出现错误,则使用console.log()函数显示所有 HTTP 响应。

如今,使用付费日志记录服务来监视 Web 应用程序并向用户发出静默错误已经非常普遍。

一些私人服务,例如 Rollbar、TrackJS、Bugsnag 和 Sentry。它们都提供了一个强大的 API,用于在生产模式下跟踪错误,并将其发送到一个易于使用的仪表板面板,而不会引起应用程序用户的警报或搜索应用程序日志。

我们还建议,对于测试版和内部测试应用程序,可以在www.bugsnag.com/platforms/javascript/上免费注册一个 bugsnag 账户。

将 HttpErrorHandler 导入到 app.module.ts

现在,我们需要将我们的服务添加到应用程序的中央模块中。请记住,我们正在使用一个名为shared的目录;将我们的服务放在app.module.ts文件中的适当位置:

  1. 打开./Client/src/app/app.module.ts文件,并在NavComponent导入之后添加以下代码:

import { HttpErrorHandler } from  './shared/_services/http-handle-error.service';

  1. ./Client/src/app/app.module.ts中,将HttpErrorHandler属性添加到providers数组中的Title属性之后:

 providers: 
        Title,
        HttpErrorHandler,

在这一步结束时,我们的应用程序中有以下目录结构:

![共享服务文件夹

重构构建者服务

现在我们已经创建了错误处理服务,我们需要重构我们的构建者和自行车服务以使用新的错误处理。

打开./app/pages/builders/_services/builder.service.ts,并用以下代码替换其内容:


     import { Injectable } from  '@angular/core';
        import { HttpClient, HttpParams, HttpErrorResponse } from  
        '@angular/common/http';
        import { HttpHeaders } from  '@angular/common/http';
        import { Observable, throwError } from  'rxjs';
        import { catchError } from  'rxjs/operators';
        // App import
        import { environment } from
        '../../../../environments/environment';
        import { Builder } from  '../builder';
        import { HttpErrorHandler, HandleError } from
        '../../../shared/_services/http-handle-error.service';

        @Injectable({
                providedIn: 'root'
        })

        export  class  BuildersService {
                private  readonly  apiUrl  =  environment.apiUrl;
                private  buildersUrl  =  this.apiUrl  +
                '/builders';
                private  handleError:  HandleError;

                constructor(
                        private  http:  HttpClient,
                        httpErrorHandler:  HttpErrorHandler ) {
                        this.handleError  =
  httpErrorHandler.createHandleError('BuildersService');
                }
                
                /** GET builders from builders endpoint */
                getBuilders ():  Observable<Builder[]> {
                        return  this.http.get<Builder[]>
                (this.buildersUrl)
                                .pipe(
                                         
                catchError(this.handleError('getBuilders', []))
                                );
                }

                /** GET builder detail from builder-detail endpoint
                 */
                getBuilderDetail (id:  number): 
                Observable<Builder[]> {
                        return  this.http.get<Builder[]>
                (this.buildersUrl  +  `/${id}`)
                                .pipe(
                                 
                catchError(this.handleError('getBuilderDetail', []))
                                );
                }

                /** POST builder to builders endpoint */
                addBuilder (builder:  Builder):  Observable<Builder> {
                        return  this.http.post<Builder> 
               (this.buildersUrl, builder)
                                .pipe(
                                       
            catchError(this.handleError('addBuilder', builder))
                                );
                }

                /** PUT builder to builders endpoint */
                updateBuilder (builder:  Builder, id:  number):
                Observable<Builder> {
                        return  this.http.put<Builder>(this.buildersUrl
           +  `/${id}`, builder).pipe(                            
             catchError(this.handleError('updateBuilder', builder))
                                );
                }

                /** DELETE builder builder endpoint */
                deleteBuilder (id:  number):  Observable<Builder[]> {
                        return  this.http.delete<Builder[]>
                (this.buildersUrl  +  `/${id}`)
                                .pipe(
                          catchError(this.handleError('deleteBuilder'))
                                );
                }
        }

在上面的代码中,我们替换了本地错误函数以使用新的错误服务。我们添加了一个名为handleError的新属性,并创建了一个名为BuildersService的新处理程序,代码如下:


this.handleError = httpErrorHandler.createHandleError ('BuildersService');

每个处理程序都接收serviceName,如getBuildersgetBuilderDetailaddBuilderupdateBuilderdeleteBuilder

现在,我们将为自行车服务执行相同的操作。

重构自行车服务

现在,让我们为自行车服务添加新的错误处理。

打开./app/pages/bikes/_services/bike.service.ts,并用以下代码替换其内容:


     import { Injectable } from  '@angular/core';
        import { HttpClient, HttpParams, HttpErrorResponse } from  '@angular/common/http';
        import { HttpHeaders } from  '@angular/common/http';
        import { Observable, throwError } from  'rxjs';
        import { catchError } from  'rxjs/operators';
        // App import
        import { environment } from  '../../../../environments/environment';
        import { Bike } from  '../bike';
        import { HttpErrorHandler, HandleError } from  '../../../shared/_services/http-handle-error.service';

        @Injectable({
                providedIn: 'root'
        })

        export  class  BikesService {
                private  readonly  apiUrl  =  environment.apiUrl;
                private  bikesUrl  =  this.apiUrl  +  '/bikes';
                private  handleError:  HandleError;
                
                constructor(
                        private  http:  HttpClient,
                        httpErrorHandler:  HttpErrorHandler ) {
                        this.handleError  = 
                httpErrorHandler.createHandleError('BikesService');
                }

                /** GET bikes from bikes endpoint */
                getBikes ():  Observable<Bike[]> {
                        return  this.http.get<Bike[]>(this.bikesUrl)
                                .pipe(
                   
                 catchError(this.handleError('getBikes', []))
                                );
                }

                /** GET bike detail from bike-detail endpoint */
                getBikeDetail (id:  number):  Observable<Bike[]> {
                        return  this.http.get<Bike[]>(this.bikesUrl  +  
                `/${id}`)
                                .pipe(
                                         
                catchError(this.handleError('getBikeDetail', []))
                                );
                }

                /** POST bike to bikes endpoint */
                addBike (bike:  Bike):  Observable<Bike> {
                        return  this.http.post<Bike>(this.bikesUrl, 
                bike)
                                .pipe(
                                         
               catchError(this.handleError('addBike', bike))
                                );
                }

                /** PUT bike to bikes endpoint */
                updateBike (bike:  Bike, id:  number):  
                Observable<Bike> {
                        return  this.http.put<Bike>(this.bikesUrl  +  
                `/${id}`, bike)
                                .pipe(
                                        
                catchError(this.handleError('updateBike', bike))
                                );
                }

                /** DELETE bike bike endpoint */
                deleteBike (id:  number):  Observable<Bike[]> {
                        return  this.http.delete<Bike[]>(this.bikesUrl  
                +  `/${id}`)
                                .pipe(
                                        
                catchError(this.handleError('deleteBike'))
                                );
                }
                
                /** Vote on bike */
                voteOnBike (vote:  number, bike:  number):  
                Observable<any> {
                        const  rating  =  vote;
                        return  this.http.post(this.bikesUrl  +  
                `/${bike}/ratings`, {rating})
                                .pipe(
                                        
                 catchError(this.handleError('voteOnBike', []))
                                );
                        }
                }

在上面的代码中,我们与构建者服务中所做的一样,并添加了每个处理程序,其中serviceNamegetBikesgetBikeDetailaddBikeupdateBikedeleteBike

如何使用授权头

当我们谈论头部授权时,基本上是在讨论对应用程序头部进行一些修改以发送某种授权。在我们的情况下,我们具体讨论的是由我们的 API 后端生成的授权令牌。

最好的方法是使用 Angular 拦截器。拦截器正如其名称所示,允许我们简单地拦截和配置请求,然后再将其发送到服务器。

这使我们能够做很多事情。其中一个示例是在任何请求上配置令牌验证,或者突然添加我们的应用程序可能需要的自定义标头,直到我们在完成请求之前处理答案。

当 JWT 令牌被发送到后端时,请记住我们在我们的 Laravel API 上使用了 jwt-auth 库:它预期在 HTTP 请求的授权标头中。

在 Angular 中添加授权标头到 HTTP 请求的最常见方法是创建一个拦截器类,并通过将 JWT(或其他形式的访问令牌)作为授权标头附加到请求中来让拦截器对请求进行修改,就像我们之前解释的那样。

创建一个 HTTP 拦截器。

让我们看看如何使用 Angular 的 HttpInterceptor 接口来进行身份验证的 HTTP 请求。

当我们在 Angular 应用中处理身份验证时,大多数情况下,最好将所需的一切都放在一个专用的服务中,就像我们之前做的那样。

任何身份验证服务都应该有几个基本方法,允许用户登录和退出。它还应该包括一种获取 JSON Web Token 并将其放入 localStorage 中的方法(就像我们之前所做的那样),在客户端,并确定用户是否经过身份验证的方式,我们的情况下,使用 auth.service.ts 上的 isAuthenticated() 函数。

因此,让我们创建 HTTP 拦截器:

  1. 在你的终端窗口中打开 ./Client/src/app,并输入以下命令:

ng g service shared/_services/http-interceptor

上一条命令将生成以下文件:./Client/src/app/shared/_services/app-http-interceptor.service.ts。再次,我们正在创建一个文件在我们的 shared 目录中,因为我们可以在应用程序中的任何地方使用这个服务。

  1. 打开 ./Client/src/app/shared/_services/app-http-interceptor.service.ts 文件,并添加以下代码:

     import { Injectable, Injector } from  '@angular/core';
        import { HttpEvent, HttpHeaders, HttpInterceptor, HttpHandler, HttpRequest, HttpErrorResponse, HttpResponse } from  '@angular/common/http';
        import { Observable } from  'rxjs';
        import { catchError, map, tap } from  'rxjs/operators';
        import { Router } from  '@angular/router';
        // App import
        import { AuthService } from 
  '../../pages/auth/_services/auth.service';
        
        @Injectable()
        export  class  AppHttpInterceptorService  implements
        HttpInterceptor {
          
        constructor(public  auth:  AuthService, private  router:
        Router ) { }

        intercept(req:  HttpRequest<any>, next:  HttpHandler):
        Observable<HttpEvent<any>> {
                console.log('interceptor running');
                
                // Get the token from auth service.
                const  authToken  =  this.auth.getToken();
                if (authToken) {
                        // Clone the request to add the new header.
                        const  authReq  =  req.clone(
                                { headers:
         req.headers.set('Authorization', `Bearer ${authToken}`)}
                        );                      
                        console.log('interceptor running with new
         headers');
                        
                        // send the newly created request
                        return  next.handle(authReq).pipe(
                                tap((event:  HttpEvent<any>) => {
                                        if (event instanceof
          HttpResponse) {
                                        // Response wiht
          HttpResponse type
                                        console.log('TAP function',
          event);
                                        }
                                }, (err:  any) => {
                                console.log(err);
                                if (err  instanceof 
          HttpErrorResponse) {
                                        if (err.status ===  401) {
                                      
          localStorage.removeItem('token');
                                 
          this.router.navigate(['/']);
                                        }
                                }
                                })
                        );
                } else {
                        console.log('interceptor without changes');
                        return  next.handle(req);
                }
        }
  1. 在前面的代码中,首先我们检查 localStorage 中是否有一个令牌,使用 AuthServicethis.auth.getToken(); 函数。所以,如果我们有一个令牌,我们添加它作为一个新的标头,使用以下方式:

     const  authReq  =  req.clone(
                { headers: req.headers.set('Authorization', `Bearer ${authToken}`)}
                );
  1. 如果令牌无效,或者 API 返回了 401 错误,我们将使用以下方式将用户发送到主路由:

this.router.navigate(['/']);

将 AppHttpInterceptorService 添加到主模块中。

现在我们已经配置好了拦截器,并准备好使用了,我们需要将其添加到主应用程序模块中:

  1. 打开 ./Client/src/app/app.module.ts 文件,并在 HttpErrorHandler 导入之后添加以下导入:

import { AppHttpInterceptorService } from  './shared/_services/app-http-interceptor.service';

  1. providers 数组中添加以下代码,在 HttpErrorHandler 属性之后:

{
 {
        provide: HTTP_INTERCEPTORS,
        useClass: AppHttpInterceptorService ,
        multi: true
 }

  1. 在前面的步骤结束时,我们的主应用程序模块将包含以下代码:

     import { BrowserModule, Title } from  '@angular/platform-browser';
        import { NgModule } from  '@angular/core';
        import { HttpClientModule, HTTP_INTERCEPTORS } from  '@angular/common/http';
        import { AppRoutingModule } from  './app-routing.module';
        import { ServiceWorkerModule } from  '@angular/service-worker';
        
        // Application modules
        import { AppComponent } from  './app.component';
        import { environment } from  '../environments/environment';
        import { HomeModule } from  './pages/home/home.module';
        import { BikesModule } from  './pages/bikes/bikes.module';
        import { BuildersModule } from  './pages/builders/builders.module';
        import { AuthModule } from  './pages/auth/auth.module';
        import { NavComponent } from  './layout/nav/nav.component';
        import { HttpErrorHandler } from  './shared/_services/http-handle-error.service';
        import { AppHttpInterceptorService } from  './shared/_services/app-http-interceptor.service';
          
        @NgModule({
        declarations: [
                AppComponent,
                NavComponent
        ],
        imports: [
                BrowserModule,
                AppRoutingModule,
                HttpClientModule,
                HomeModule,
                BikesModule,
                BuildersModule,
                AuthModule,
                ServiceWorkerModule.register('/ngsw-worker.js', { enabled: environment.production })
        ],
        providers: [
                Title,
                HttpErrorHandler,
                {
                provide: HTTP_INTERCEPTORS,
                useClass: AppHttpInterceptorService ,
                multi: true
                }
        ],
        bootstrap: [AppComponent]
        })
        export  class  AppModule { }

请注意,我们将 Angular 导入与应用程序导入分开。这是一个好的做法,有助于保持代码的组织。

恭喜!现在,我们可以拦截应用程序中的每个请求。

如何使用路由守卫保护应用程序路由

在本节中,我们将讨论 Angular 框架的另一个强大功能。我们称之为守卫,甚至更好地称之为路由守卫。

它在 Angular CLI 中可用,正如我们将在下面的代码行中看到的那样,但首先让我们更深入地了解一下守卫。

当构建现代 Web 应用程序时,保护路由是一项非常常见的任务,因为我们希望防止用户访问他们不被允许访问的区域,在我们的情况下是自行车的详细信息。请记住,我们在./Server/app/Http/Controllers/API/BikeController.php中定义了对自行车详细信息的访问:


     /**
        * Protect update and delete methods, only for authenticated
        users.
        *
        * @return  Unauthorized
        */
        public  function  __construct()
        {
                $this->middleware('auth:api')->except(['index']);
        }

前面的代码表示只有索引路由不应受到保护。

我们可以使用四种不同的守卫类型来保护我们的路由:

  • CanActivate:选择是否可以激活路由

  • CanActivateChild:选择是否可以激活路由的子路由

  • CanDeactivate:选择是否可以停用路由

  • CanLoad:选择是否可以延迟加载模块

在下一个示例中,我们将使用CanActivate功能。

创建自行车详细信息的路由守卫

守卫是作为服务实现的,因此我们通常使用 Angular CLI 创建一个守卫类:

  1. 打开您的终端窗口,并输入以下命令:

ng g guard pages/auth/_guards/auth

前面的代码将生成以下文件:./Client/src/app/pages/auth/_guards/auth.guard.ts

  1. 打开./Client/src/app/pages/auth/_guards/auth.guard.ts文件,并在 observable 导入之后添加以下导入:

import { AuthService } from  '../_services/auth.service';

  1. 现在,让我们在constructor()函数内添加RouterAuthService,如下所示的代码中:

 constructor(
        private  router:  Router,
        private  auth:  AuthService) {}

  1. return属性之前,添加以下代码块到canActivate()函数内:

 if (this.auth.isAuthenticated()) {
 // logged in so return true
        return  true;
 }
 // not logged in so redirect to login page with the return url
 this.router.navigate(['/login'], { queryParams: { returnUrl: state.url }});

在上面的代码中,我们使用AuthService中的auth.isAuthenticated()函数来检查用户是否已经认证。这意味着,如果用户未经身份验证/登录,我们将重定向他们到登录屏幕。

我们还使用queryParamsreturnUrl函数将用户发送回他们来自的位置。

这意味着,如果用户点击查看自行车的详细信息,而他们没有登录到应用程序,他们将被重定向到登录屏幕。登录后,用户将被重定向到他们打算查看的自行车的详细信息。

最后一步是将AuthGuard添加到bike-detail路由。

  1. 打开./Client/src/app/bikes/bikes-routing.module.ts,并在路由导入之后添加以下导入:

import { AuthGuard } from '../auth/_guards/auth.guard';

  1. 现在,在bikeDetailComponent之后添加canActivate属性,如下所示的代码中:

 {
        path: ':id',
        component: BikeDetailComponent,
        canActivate: [AuthGuard]
 }

看!我们的bike-detail路由现在受到了保护。

总结

现在,我们离看到我们的应用程序处于工作状态非常接近。然而,我们仍然需要执行一些步骤,我们将在接下来的章节中进行讨论。

与此同时,我们已经学习了一些构建现代 Web 应用程序的重要要点,比如创建服务来处理 XHR 请求,学习如何保护我们的路由,以及创建路由拦截器和处理错误的服务。

在下一章中,我们将深入探讨如何在我们的组件中使用我们刚刚创建的服务,并且我们还将为我们的应用程序应用一个视觉层。

第十章:使用 Bootstrap 4 和 NgBootstrap 的前端视图

在本章中,我们将看看如何使用 Angular CLI 的新add功能在运行的 Angular 应用程序中包含 Bootstrap 框架。

Bootstrap 框架是最重要的 UI 框架之一,结合 Angular 指令/组件,我们可以在 Angular 应用程序中拥有 Bootstrap 的全部功能。

我们还将看看如何将我们的 Angular 服务与组件连接起来,以及如何使用后端 API 将它们整合在一起。最后,我们将学习如何在后端 API 上配置跨源资源共享CORS)以及如何在我们的 Angular 客户端应用程序中使用它。

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

  • 安装 Bootstrap CSS 框架

  • 使用 Bootstrap 编写 Angular 模板

  • 如何在 Laravel 后端设置 CORS

  • 将 Angular 服务与应用程序组件连接起来

  • 处理 Angular 管道、表单和验证

准备基线代码

现在,我们需要准备我们的基线代码,这个过程与我们在上一章中执行的非常相似。让我们按照以下步骤进行:

  1. 复制chapter-9文件夹中的所有内容。

  2. 重命名chapter-10文件夹。

  3. 删除storage-db文件夹。

  4. 现在,让我们对docker-compose.yml文件进行一些更改,以便我们可以适应新的数据库和服务器容器。打开docker-compose.yml并用以下代码替换其内容:

 version: "3.1"
 services:
     mysql:
       image: mysql:5.7
       container_name: chapter-10-mysql
       working_dir: /application
       volumes:
         - .:/application
         - ./storage-db:/var/lib/mysql
       environment:
         - MYSQL_ROOT_PASSWORD=123456
         - MYSQL_DATABASE=chapter-10
         - MYSQL_USER=chapter-10
         - MYSQL_PASSWORD=123456
       ports:
         - "8083:3306"
     webserver:
       image: nginx:alpine
       container_name: chapter-10-webserver
       working_dir: /application
       volumes:
         - .:/application
         -./phpdocker/nginx/nginx.conf:/etc/nginx/
           conf.d/default.conf
        ports:
          - "8081:80"
     php-fpm:
       build: phpdocker/php-fpm
       container_name: chapter-10-php-fpm
       working_dir: /application
       volumes:
         - ./Server:/application
         - ./phpdocker/php-fpm/php-ini-
           overrides.ini:/etc/php/7.2/fpm/conf.d/99-overrides.ini

请注意,我们更改了容器名称、数据库和 MySQL 用户:

  • container_name: chapter-10-mysql

  • container_name: chapter-10-webserver

  • container_name: chapter-10-php-fpm

  • MYSQL_DATABASE=chapter-10

  • MYSQL_USER=chapter-10

  1. 使用连接字符串更新.env文件:
 DB_CONNECTION=mysql
 DB_HOST=mysql
 DB_PORT=3306
 DB_DATABASE=chapter-10
 DB_USERNAME=chapter-10
 DB_PASSWORD=123456
  1. 将我们所做的更改添加到 Git 源代码控制中。打开您的终端窗口,输入以下命令:
 git add .
 git commit -m "Initial commit chapter 10"
  1. 现在,让我们使用以下命令启动我们的 Docker 容器:
 docker-compose up -d

安装 Bootstrap CSS 框架

在本节中,我们将再次使用 Angular CLI 6 中可用的最新功能:add命令。使用这个命令,我们将向我们的应用程序添加 Bootstrap 4:

  1. chapter-10Client文件夹中,打开您的终端窗口并输入以下命令:
 ng add @ng-bootstrap/schematics
  1. 上一个命令将创建并更新以下文件:
+ @ng-bootstrap/schematics@2.0.0-alpha.1
added 3 packages in 26.372s
Installed packages for tooling via npm.
UPDATE package.json (1589 bytes)
UPDATE src/app/app.module.ts (1516 bytes)
UPDATE angular.json (3706 bytes)
  1. package.json文件中,我们将添加以下依赖项:
     "@ng-bootstrap/schematics": "².0.0-alpha.1",
        "@ng-bootstrap/ng-bootstrap": "².0.0-alpha.0",
        "bootstrap": "⁴.0.0"
  1. src/app/app.module.ts文件中,我们将添加以下行:
     import { NgbModule } from  '@ng-bootstrap/ng-bootstrap';

        imports: [
                ...
                NgbModule.forRoot()
        ],
  1. angular.json文件中,我们将添加以下行:
     "styles": [
                "src/styles.scss",
                {
                        "input": "./node_modules/bootstrap/dist/css/bootstrap.css"
                }
        ],

在这里,我们可以看到 Angular CLI 的全部功能,因为所有这些更改都是自动完成的。

但是,我们可以看到bootstrap.css文件的使用方式使应用程序冻结,使其难以定制。

在下一节中,我们将探讨一种更灵活使用 Bootstrap 的方法。

移除 Bootstrap CSS 导入

首先,我们将删除通过NgBootstrap安装命令注入到我们的angular.json文件中的从 Bootstrap 编译的 CSS。

打开angular.json文件并删除input标签。只保留styles标签,如下所示:

     "styles": [
                "src/styles.scss"
        ],

添加 Bootstrap SCSS 导入

现在,我们将使用node_modules文件夹中安装的文件作为我们主样式表./Client/src/style.scss的导入:

  1. 打开./Client/src/style.scss并在文件顶部添加以下代码:
/*! * Bootstrap v4.1.1 (https://getbootstrap.com/) * Copyright 2011-2018 The Bootstrap Authors * Copyright 2011-2018 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) */ @import "../node_modules/bootstrap/scss/functions"; @import "../scss/bootstrap/_variables.scss"; @import "../node_modules/bootstrap/scss/_variables.scss"; @import "../node_modules/bootstrap/scss/mixins"; @import "../node_modules/bootstrap/scss/root"; @import "../node_modules/bootstrap/scss/reboot"; @import "../node_modules/bootstrap/scss/type"; @import "../node_modules/bootstrap/scss/images"; @import "../node_modules/bootstrap/scss/code"; @import "../node_modules/bootstrap/scss/grid"; @import "../node_modules/bootstrap/scss/tables"; @import "../node_modules/bootstrap/scss/forms"; @import "../node_modules/bootstrap/scss/buttons"; @import "../node_modules/bootstrap/scss/transitions"; @import "../node_modules/bootstrap/scss/dropdown"; @import "../node_modules/bootstrap/scss/button-group"; @import "../node_modules/bootstrap/scss/input-group"; @import "../node_modules/bootstrap/scss/custom-forms"; @import "../node_modules/bootstrap/scss/nav"; @import "../node_modules/bootstrap/scss/navbar"; @import "../node_modules/bootstrap/scss/card"; @import "../node_modules/bootstrap/scss/breadcrumb"; @import "../node_modules/bootstrap/scss/pagination"; @import "../node_modules/bootstrap/scss/badge"; @import "../node_modules/bootstrap/scss/jumbotron"; @import "../node_modules/bootstrap/scss/alert"; @import "../node_modules/bootstrap/scss/progress"; @import "../node_modules/bootstrap/scss/media"; @import "../node_modules/bootstrap/scss/list-group"; @import "../node_modules/bootstrap/scss/close"; @import "../node_modules/bootstrap/scss/modal"; @import "../node_modules/bootstrap/scss/tooltip"; @import "../node_modules/bootstrap/scss/popover"; @import "../node_modules/bootstrap/scss/carousel"; @import "../node_modules/bootstrap/scss/utilities"; @import "../node_modules/bootstrap/scss/print";

请注意,我们保留了文件顶部的 Bootstrap 注释,只是为了在易于找到的地方记录 Bootstrap 版本。

  1. 如果您愿意,您可以复制node_modules/bootstrap/scss/bootstrap.scss文件的内容,并只需调整导入路径为../node_modules/bootstrap/scss

现在,我们的应用程序直接从bootstrap/scss文件夹编译 SCSS 代码。

这样做的一些优势包括:

  • 我们可以根据应用程序使用的组件选择要导入的 SCSS 模块。

  • 我们减少将不会使用的 SCSS 代码。

  • 我们可以轻松地覆盖 Bootstrap 变量。

覆盖 Bootstrap 变量

在这一步中,我们将看到如何在我们的应用程序中覆盖Boostrap变量:

  1. Client文件夹的根目录下创建一个名为scss的新文件夹。

  2. ./Client/scss文件夹中,添加一个名为bootstrap的新文件夹。

  3. ./Client/scss/bootstrap中,添加一个名为_variable.scss的新文件。

  4. node_modules/bootstrap/scss/_variables.scss中复制内容,并粘贴到./Client/scss/bootstrap/_variables.scss中。

非常简单;恭喜!我们已经准备好覆盖 Bootstrap 变量。

最后一步是将新的_variables.scss文件导入到我们的主要style.scss文件中。

  1. 打开./Client/style.scss文件,并用以下内容替换行@import "../node_modules/bootstrap/scss/_variables.scss"
 <pre>Error: ENOENT: no such file or directory, open '/Users/fernandomonteiro/_bitbucket/scss/bootstrap/_variables.scss'</pre>  

我们还有一个选项,即只使用我们将要覆盖的变量,而不使用关键字Default放置这个变量文件。这样,文件会变得更短,因为我们不会覆盖这样一个小项目中的所有变量。让我们看看我们如何做到这一点。

  1. 假设我们只想覆盖所有组件的border-radius并删除box-shadow。我们只能使用这些变量,因此我们的_variables.scss文件将如下所示:
     // Variables
        //
        // Removing border-radius and box-shadow from components

        $border-radius: 0;
        $border-radius-lg: 0;
        $border-radius-sm: 0;

        $box-shadow-sm: none;
        $box-shadow: none;
        $box-shadow-lg: none;
  1. 为了使这些更改生效,我们需要对./Client/style.scss进行一些小调整,并在 Bootstrapvariables文件之前添加新的变量文件,如下所示:
/*!
 * Bootstrap v4.1.1 (https://getbootstrap.com/) * Copyright 2011-2018 The Bootstrap Authors * Copyright 2011-2018 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) */  @import  "../node_modules/bootstrap/scss/functions";
  @import  "../scss/bootstrap/_variables.scss";
  @import  "../node_modules/bootstrap/scss/_variables.scss";

使用 Bootstrap 编写 Angular 模板

此刻,我们的应用程序已经可以使用 Bootstrap CSS 进行可视化,这是我们在上一节中所做的。回想一下,在之前的章节中,我们已经向一些模板中添加了 HTML 标记。

它们都已经包含了 Bootstrap 类,我们已经可以在浏览器窗口中可视化到目前为止的内容。让我们来看看:

  1. ./Client文件夹中打开您的终端窗口,并键入以下命令:
 npm start
  1. 打开您的默认浏览器,转到http://localhost:4200/

您将看到以下结果:

哇!现在,我们有了一个 Web 应用。您会注意到我们已经有一个完美运行的应用程序。

  1. 让我们点击bikes链接,看看我们到目前为止有什么:

随意浏览应用程序的其余部分并检查其他页面。

然而,我们目前只有占位符,所以现在是学习如何在我们的模板中应用 Angular 模板语法的时候了。

向导航组件添加模板绑定

现在,让我们在模板中做一些更改,以便我们可以使用 Angular 语法:

  1. 打开./Client/src/app/layout/nav/nav.component.html,并用以下代码替换其内容:
<header> 
<nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark"> 
<a class="navbar-brand" [routerLink]="['/']" (click)="setTitle('Custom Bikes Garage')">Custom
Bikes Garage</a> 
<button class="navbar-toggler" type="button" data-toggle="collapse" data-
target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria- label="Toggle
navigation"> 
<span class="navbar-toggler-icon"></span> 
</button> 
<div class="collapse navbar-collapse" id="navbarCollapse"> 
<ul class="navbar-nav ml-auto"> <li class="nav-item"> <a class="nav-link" [routerLink]="['/bikes']" routerLinkActive="active" (click)="setTitle('Bikes')">Bikes</a> 
</li> <li class="nav-item"> <a class="nav-link" [routerLink]="['/builders']" routerLinkActive="active" (click)="setTitle('Builders')">Builders</a> </li> 
<li *ngIf="!auth.isAuthenticated()" class="nav-item"> <a class="nav-link" [routerLink]="['/login']" routerLinkActive="active" (click)="setTitle('Login')">Login</a> </li> 
<li *ngIf="!auth.isAuthenticated()" class="nav-item"> <a class="nav-link" [routerLink]="['/register']" routerLinkActive="active" (click)="setTitle('Register')">Register</a> </li>
 <li *ngIf="auth.isAuthenticated()" class="nav-item"> 
<div ngbDropdown class="d-inline-block">
<button class="btn btn-secondary" id="dropdownBasic1" ngbDropdownToggle>{{ auth.currentUser?.name }}</button 
<div ngbDropdownMenu aria-labelledby="dropdownBasic1"> 
<button class="dropdown-item" (click)="onLogout();">Logout</button>
</div> 
</div>
</li>
</ul>
</div>
</nav> 
</header>

请注意,在上面的代码中,我们使用了ngbDropdown组件,并且还使用auth.isAuthenticated()来确定用户是否已登录。还要注意,我们在下拉菜单中包含了注销链接。

现在,让我们调整登录和注册的模板。

向登录页面添加模板绑定

在第七章 使用 Angular-cli 创建渐进式 Web 应用中,我们已经为应用的所有视图/模板添加了 HTML 标记,但是,我们需要向模板添加 Angular 绑定和模型,以便一切都能正常工作:

  1. 打开./Client/src/app/auth/login/login.component.html

  2. 将以下绑定函数添加到标签中:

 (ngSubmit)="onSubmit(loginForm)" #loginForm="ngForm"

现在,我们将在./Client/src/app/auth/login/login.component.html中为电子邮件和密码输入添加ngModel

  1. 将以下代码添加到email输入中:
 <input  type="email" [(ngModel)]="user.email" name="email" #email="ngModel" class="form-control"  id="email"  aria-describedby="emailHelp"  placeholder="Enter email">
  1. 将以下代码添加到password输入中:
 <input  type="password" [(ngModel)]="user.password" name="password" #password="ngModel" class="form-control"  id="password"  placeholder="Password">

向注册页面添加模板绑定

现在,让我们在注册页面模板上重复相同的操作:

  1. 打开./Client/src/app/auth/register/register.component.html

  2. 将以下绑定函数添加到标签中:

 [formGroup]="registerForm" (ngSubmit)="onSubmit()"  class="form-signin"  novalidate

注意formGroup属性的使用。它是 Angular 响应式表单的一部分,但现在不用担心这个;在本书的后面,我们将讨论模板驱动表单和响应式表单。

现在,在./Client/src/app/auth/register/register.component.html中,我们将为nameemailpassword输入添加formControlName

  1. 将以下代码添加到name输入:
 <input type="name"  formControlName="name"  class="form-control"  id="name"  aria-describedby="nameHelp"  placeholder="Enter your name">
  1. 将以下代码添加到email输入:
 <input type="email"  formControlName="email" class="form-control"  id="email"  aria-describedby="emailHelp"  placeholder="Enter email">
  1. 将以下代码添加到password输入:
 <input  formControlName="password"  type="password"  name="password" class="form-control"  id="password"  placeholder="Password">

向 bike-detail 页面添加模板绑定

现在,让我们对bike-detail页面模板进行一些调整:

  1. 打开./Client/src/app/bikes/bike-detail/bike-detail.component.html

  2. 用以下代码替换其内容:

 <main role="main">
        <div class="py-5">
        <div class="container">
        <div *ngIf="isLoading" class="spinner">
                <div class="double-bounce1"></div>
                <div class="double-bounce2"></div>
        </div>
        <ngb-tabset type="pills" *ngIf="!isLoading">
                <ngb-tab title="Bike Detail">
                        <ng-template ngbTabContent>
                        <br>
                        <div class="row">
                                <div class="col-md-4">
                                <img class="card-img-top" src="{{ bike?.picture }}" alt="Card image cap">
                                </div>
                                <div class="col-md-8">
                                <div class="card">
                                        <div class="card-body">
                                        <h5 class="card-title">{{ bike?.model }} | {{ bike?.year }} | Ratings: {{ bike?.average_rating }}
                                                <span *ngIf="userVote">| Your Vote: {{ userVote }}</span>
                                        </h5>
                                        <p class="card-text">{{ bike?.mods }}</p>
                                        </div>
                                        <div *ngIf="bike?.builder" class="card-header">
                                        <strong>Builder</strong>:
                                        <a routerLink="/builders/{{bike?.builder['id']}}">{{ bike?.builder['name'] }}</a>
                                        </div>
                                        <div *ngIf="bike?.items" class="card-header">
                                        <strong>Featured items</strong>:
                                        </div>
                                        <ul class="list-group list-group-flush">
                                        <li *ngFor="let item of bike?.items" class="list-group-item">
                                                <strong>Type</strong>: {{ item.type }} |
                                                <strong>Name</strong>: {{ item.name }} |
                                                <strong>Company</strong>: {{ item.company }}
                                        </li>
                                        </ul>
                                        <div class="card-body">
                                        <ul class="list-unstyled list-inline">
                                                <li class="list-inline-item">Vote: </li>
                                                <li class="list-inline-item">
                                                <a (click)="onVote('1')" class="btn btn-outline-secondary">1</a>
                                                </li>
                                                <li class="list-inline-item">
                                                <a (click)="onVote('2')" class="btn btn-outline-primary">2</a>
                                                </li>
                                                <li class="list-inline-item">
                                                <a (click)="onVote('3')" class="btn btn-outline-success">3</a>
                                                </li>
                                        </ul>
                                        </div>
                                </div>
                                </div>
                        </div>
                        </ng-template>
                </ngb-tab>
                <ngb-tab>
                        <ng-template ngbTabTitle *ngIf="checkBikeOwner()">Edit bike</ng-template>
                        <ng-template ngbTabContent>
                        <br>
                        <form (ngSubmit)="onSubmit(bikeAddForm)" #bikeAddForm="ngForm" name=bikeAddForm class="bg-light px-4 py-4">
                                <div class="form-group">
                                <label for="make">Make</label>
                                <input type="text" [(ngModel)]="bike.make"  name="make" class="form-control" id="make" placeholder="Enter make">
                                </div>
                                <div class="form-group">
                                <label for="model">Model</label>
                                <input type="text" [(ngModel)]="bike.model" name="model" class="form-control" id="model" placeholder="Enter model">
                                </div>
                                <div class="form-group">
                                <label for="year">Year</label>
                                <input type="text" [(ngModel)]="bike.year" name="year" class="form-control" id="year" placeholder="Enter year, ex: 1990, 2000">
                                </div>
                                <div class="form-group">
                                <label for="mods">Mods</label>
                                <textarea type="text" [(ngModel)]="bike.mods" name="mods" class="form-control" id="mods" placeholder="Enter modifications"></textarea>
                                </div>
                                <div class="form-group">
                                <label for="picture">Picture</label>
                                <input type="text" [(ngModel)]="bike.picture" name="picture" class="form-control" id="picture" placeholder="Enter picture url">
                                </div>
                                <div class="form-group">
                                <label for="inputState">Builder</label>
                                <select [(ngModel)]="bike.builder.id" name="builder_id" class="form-control">
                                        <option *ngFor="let builder of builders" [(ngValue)]="builder['id']">{{builder['name']}}</option>
                                </select>
                                </div>
                                <button type="submit" class="btn btn-primary">Submit</button>
                        </form>
                        </ng-template>
                </ngb-tab>
                </ngb-tabset>
        </div>
 </div>
 </main>

请注意,我们使用*ngIf指令来隐藏我们的自行车,直到自行车对象可用为止。我们还使用点击绑定函数(click)="onVote('1')"对自行车进行投票,我们使用*ngFor="let item of bike?.items"来列出自行车项目。

我们还使用了来自NgBootstrapngb-tabngb-tabset指令在此页面上创建两个视图:一个用于显示自行车的详细信息,另一个用于显示编辑表单,以便我们可以编辑自行车的详细信息。请注意,我们使用了一个名为checkBikeOwner()的函数来进行简单的检查,以查看登录的用户是否是自行车的所有者。否则,我们会隐藏该选项卡。

(?)符号被称为安全导航运算符。

预期结果是我们在下面的截图中看到的:

现在不用担心表单,因为我们将在本章末尾详细讨论它。

向 bike-list 页面添加模板绑定

好了,现在是时候创建bike-list模板绑定了:

  1. 打开./Client/src/app/bikes/bike-list/bike-list.component.html

  2. 用以下代码替换其内容:

<main role="main">
  <div class="py-5 bg-light">
    <div class="container">
      <form>
        <div class="form-group row">
          <label for="search" class="col-sm-2 col-form-label">Bike List</label>
          <div class="col-sm-8">
            <input [(ngModel)]="searchText" [ngModelOptions]="{standalone: true}" placeholder="buscar" type="text" class="form-control"
              id="search" placeholder="Search">
          </div>
          <div class="col-sm-2">
            <div ngbDropdown class="d-inline-block">
              <button class="btn btn-primary" id="dropdownBasicFilter" ngbDropdownToggle>Filter</button>
              <div ngbDropdownMenu aria-labelledby="dropdownBasicFilter">
                <button class="dropdown-item">Year</button>
              </div>
            </div>
          </div>
        </div>
      </form>
      <div *ngIf="isLoading" class="spinner">
        <div class="double-bounce1"></div>
        <div class="double-bounce2"></div>
      </div>
      <div class="row">
        <div class="col-md-4" *ngFor="let bike of bikes | bikeSearch: searchText ">
          <div class="card mb-4 box-shadow">
            <img class="card-img-top" src="{{ bike.picture }}" alt="{{ bike.model }}">
            <div class="card-body">
              <p>{{ bike.model }} | {{ bike.year }}</p>
              <p class="card-text">{{ bike.mods }}</p>
              <a routerLink="/bikes/{{ bike.id }}" class="card-link">Vote</a>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</main>

请注意,我们正在使用ngbDropdownngbDropdownTogglengbDropdownMenu组件,并且我们还使用*ngFor="let bike of bikes"来列出bikes 数组中的所有自行车,并使用*ngIf来显示和隐藏加载消息。

现在,我们可以看到 Angular 的强大之处。通过一些更改,我们的静态模板已经准备好与后端进行交互。但我们仍然需要编写组件的逻辑来将所有内容整合在一起。

在我们这样做之前,让我们调整构建者模板。

向 builder-detail 页面添加模板绑定

让我们添加builder-detail页面:

  1. 打开./Client/src/app/builders/builder-detail/builder-detail.component.html

  2. 用以下代码替换其内容:

<main role="main">
  <div class="py-5">
  <div class="container">
  <div *ngIf="isLoading" class="spinner">
    <div class="double-bounce1"></div>
    <div class="double-bounce2"></div>
  </div>
  <ngb-tabset type="pills" *ngIf="!isLoading">
    <ngb-tab title="Bike Detail">
      <ng-template ngbTabContent>
      <br>
      <div class="row">
        <div class="col-md-4">
        <img class="card-img-top" src="{{ bike?.picture }}" alt="Card image cap">
        </div>
        <div class="col-md-8">
        <div class="card">
          <div class="card-body">
          <h5 class="card-title">{{ bike?.model }} | {{ bike?.year }} | Ratings: {{ bike?.average_rating }}
            <span *ngIf="userVote">| Your Vote: {{ userVote }}</span>
          </h5>
          <p class="card-text">{{ bike?.mods }}</p>
          </div>
          <div *ngIf="bike?.builder" class="card-header">
          <strong>Builder</strong>:
          <a routerLink="/builders/{{bike?.builder['id']}}">{{ bike?.builder['name'] }}</a>
          </div>
          <div *ngIf="bike?.items" class="card-header">
          <strong>Featured items</strong>:
          </div>
          <ul class="list-group list-group-flush">
          <li *ngFor="let item of bike?.items" class="list-group-item">
            <strong>Type</strong>: {{ item.type }} |
            <strong>Name</strong>: {{ item.name }} |
            <strong>Company</strong>: {{ item.company }}
          </li>
          </ul>
          <div class="card-body">
          <ul class="list-unstyled list-inline">
            <li class="list-inline-item">Vote: </li>
            <li class="list-inline-item">
            <a (click)="onVote('1')" class="btn btn-outline-secondary">1</a>
            </li>
            <li class="list-inline-item">
            <a (click)="onVote('2')" class="btn btn-outline-primary">2</a>
            </li>
            <li class="list-inline-item">
            <a (click)="onVote('3')" class="btn btn-outline-success">3</a>
            </li>
          </ul>
          </div>
        </div>
        </div>
      </div>
      </ng-template>
    </ngb-tab>
    <ngb-tab>
      <ng-template ngbTabTitle *ngIf="checkBikeOwner()">Edit bike</ng-template>
      <ng-template ngbTabContent>
      <br>
      <form (ngSubmit)="onSubmit(bikeAddForm)" #bikeAddForm="ngForm" name=bikeAddForm class="bg-light px-4 py-4">
        <div class="form-group">
        <label for="make">Make</label>
        <input type="text" [(ngModel)]="bike.make"  name="make" class="form-control" id="make" placeholder="Enter make">
        </div>
        <div class="form-group">
        <label for="model">Model</label>
        <input type="text" [(ngModel)]="bike.model" name="model" class="form-control" id="model" placeholder="Enter model">
        </div>
        <div class="form-group">
        <label for="year">Year</label>
        <input type="text" [(ngModel)]="bike.year" name="year" class="form-control" id="year" placeholder="Enter year, ex: 1990, 2000">
        </div>
        <div class="form-group">
        <label for="mods">Mods</label>
        <textarea type="text" [(ngModel)]="bike.mods" name="mods" class="form-control" id="mods" placeholder="Enter modifications"></textarea>
        </div>
        <div class="form-group">
        <label for="picture">Picture</label>
        <input type="text" [(ngModel)]="bike.picture" name="picture" class="form-control" id="picture" placeholder="Enter picture url">
        </div>
        <div class="form-group">
        <label for="inputState">Builder</label>
        <select [(ngModel)]="bike.builder.id" name="builder_id" class="form-control">
          <option *ngFor="let builder of builders" [(ngValue)]="builder['id']">{{builder['name']}}</option>
        </select>
        </div>
        <button type="submit" class="btn btn-primary">Submit</button>
      </form>
      </ng-template>
    </ngb-tab>
    </ngb-tabset>
    </div>
  </div>
</main>

在构建者模板中,我们使用了与之前模板相同的技术。

向 builder-list 页面添加模板绑定

现在,是时候添加builder-list模板了:

  1. 打开./Client/src/app/builders/builder-list/builder-list.component.html

  2. 用以下代码替换其内容:

 <main  role="main">
        <div  class="py-5 bg-light">
                <div  class="container">
                        <div *ngIf="isLoading"  class="spinner">
                                <div  class="double-bounce1"></div>
                                <div  class="double-bounce2"></div>
                        </div>
                        <div  class="row">
                                <div  class="col-md-4" *ngFor="let builder of builders">
                                        <div  class="card mb-4 box-shadow">
                                                <div  class="card-header">
                                                        <h4  class="my-0 font-weight-normal">{{ builder?.name }}</h4>
                                                </div>
                                                <div  class="card-body">
                                                        <p  class="mt-3 mb-4">{{ builder?.description }</p>
                                                        <button  routerLink="/builders/{{ builder?.id }}"  type="button"  class="btn btn-lg btn-block btn-outline-primary">View Bike</button>
                                                </div>
                                                <div  class="card-footer text-muted">
                                                        {{ builder?.location }}
                                                </div>
                                        </div>
                                </div>
                        </div>
                </div>
        </div>
 </main>

现在,我们有足够的代码让我们的模板呈现后端的内容。为此,我们只需要对后端进行一些微小的调整,并在组件中编写逻辑。

在 Laravel 后端设置 CORS

在我们的后端进行必要的更改之前,让我们谈谈今天现代 Web 应用中非常重要且非常常见的一个主题,即 CORS。

当我们使用XMLHttpRequestFetch API从给定服务器获取数据时,这个调用通常是从另一个应用程序和其他地方执行的。

出于安全原因,浏览器限制跨源 HTTP 请求。

理解 CORS 工作原理的一个简单例子是:想象一个在特定域中运行的前端应用,例如http://mysimpledomain.com,向另一个域中的另一个应用http://myanothersimpledomain.com发送请求。

CORS 是一种机制,它使用额外的 HTTP 头来告诉浏览器允许一个 Web 应用程序在一个起源http://mysimpledomain.com上运行,并且有权限从不同起源的服务器http://myanothersimpledomain访问选定的资源。

您可以在www.w3.org/TR/cors/上阅读有关 CORS 的更多信息。

设置 Laravel CORS

Laravel 在其应用程序中使用 CORS 具有出色的支持。让我们看看如何使用一个名为barryvdh/laravel-cors的库来配置它:

  1. chapter-10文件夹中打开您的终端窗口。

  2. 输入以下命令:

 docker-compose up -d
  1. 现在,在php-fpm容器中,输入以下命令:
 docker-compose exec php-fpm bash

这一步非常重要。如果您忘记了这个命令,很可能会出现错误,或者您可能会冒着使用本地 composer 版本来执行以下命令的风险。

  1. 在容器的 bash 中,输入以下命令:
 composer require barryvdh/laravel-cors

由于最新版本的 Laravel(5.6),我们的新库已经准备好使用。让我们只做一个小小的改变。

  1. 打开./Server/app/Http/Kernel.php文件并将以下代码添加到middlewareGroup API 中:
 protected $middlewareGroups = [

        'web'  => [
                ...
        ],
        'api'  => [
                \Barryvdh\Cors\HandleCors::class,
                'throttle:60,1',
                'bindings',
        ],

非常重要的一点是,我们在 API 标签的依赖项的第一行中添加了\Barryvdh\Cors\HandleCors :: class。这非常重要,因为我们避免在前端应用程序上获得状态码 0 的错误。

我们已经准备好了!

将 Angular 服务与应用程序组件连接起来

现在,我们将连接我们在本书中创建的所有 Angular 服务和模板。为此,我们将创建我们将在组件中使用的逻辑和函数。

在开始之前,让我们将 API 的端点设置为 Angular 环境文件中的一个变量。

添加环境配置

正如其名称所示,此文件用于在我们的应用程序中设置环境变量。最好的部分是,Angular 默认配置了一个 dev 和 prod 环境,并且非常简单易用。我们还可以设置各种变量。

在此示例中,我们正在使用开发文件来设置后端 URL。

打开./Client/src/environments/environment.ts文件并添加以下 URL:

     export  const  environment  = {
                production: false,
                apiUrl: 'http://localhost:8081/api'
        };

如您所见,environments文件夹中还有一个名为environment.prod.ts的文件。

现在不要担心这个文件,因为我们将在书中稍后使用它。

创建导航方法

现在,是时候在nav.component.ts中创建导航行为了,让我们看看我们可以如何做到这一点:

  1. 打开./Client/src/layout/nav/nav.component.ts并在核心导入之后添加以下导入:
 import { Router } from  '@angular/router';
 import { Title } from  '@angular/platform-browser';

 // App imports
 import { AuthService } from  '../../pages/auth/_services/auth.service';
  1. 仍然在./Client/src/layout/nav/nav.component.ts中,让我们创建constructor()函数:
 public  constructor(
        private  titleTagService:  Title,
        public  auth:  AuthService,
        private  router:  Router ) {}

在这里,我们使用内置的 Angular 服务Title来在导航模板之间更新页面的标题标签。请记住,我们的应用程序是一个 SPA,我们不希望在所有页面上保持相同的标题。

此外,我们将使用身份验证服务来显示已登录到应用程序的用户的名称,并且我们还将使用此服务的注销功能来注销用户。因此,让我们创建这个函数。

  1. 在析构函数之后添加以下代码:
 public  setTitle( pageTitle:  string) {
        this.titleTagService.setTitle( pageTitle );
 }
  1. 现在,在ngOnInit()函数中,添加以下代码:
 if (this.auth.getToken()) {
        this.auth.getUser().subscribe();
 }
  1. 最后一步是在ngOnInit()函数之后添加logout()函数。添加以下代码:
 onLogout() {
        this.auth.onLogout().subscribe();
 }

现在,我们的应用程序导航已经准备好使用。预期结果如下截图所示:

导航视图

创建 bike-detail 方法

让我们创建bike-detail组件:

  1. 打开./Client/src/pages/bikes/bike-detail/bike-detail.component.ts并在核心导入之后添加以下导入:
 import { ActivatedRoute } from  '@angular/router';

 // App imports
 import { Bike } from  '../bike';
 import { BikesService } from  '../_services/bikes.service';
 import { AuthService } from  '../../auth/_services/auth.service';
 import { User } from  './../../auth/user';
  1. BikeDetailComponent类声明后添加以下属性:
 bike:  Bike;
 isLoading:  Boolean  =  false;
 userVote:  number;
 builders: Array<Object> = [
        {id: 1, name: 'Diamond Atelier'},
        {id: 2, name: 'Deus Ex Machina\'s'},
        {id: 3, name: 'Rough Crafts'},
        {id: 4, name: 'Roldand Sands'},
        {id: 5, name: 'Chopper Dave'}
 ];

请注意,我们正在使用Bike模型作为我们的bike属性的类型,并创建一个简单的数组来保存我们的构建者。

请注意,在真实的网络应用程序中,最好从服务器获取构建者列表,以避免在组件内部变得硬编码。

  1. ./Client/src/pages/bikes/bike-detail/bike-detail.component.ts中,让我们创建constructor()函数:
 constructor(
        private  bikeService:  BikesService,
        private  route:  ActivatedRoute,
        private  auth:  AuthService ) {}

我们将使用ActivatedRoute来在本节后面获取自行车 ID。

  1. ngOnInit()函数中,添加以下代码:
 // Get bike details
 this.getBikeDetail();

现在,让我们创建getBikeDetail()函数。

  1. ngOnInit()函数之后添加以下代码:
 getBikeDetail():  void {
        this.isLoading  =  true;
        const  id  =  +this.route.snapshot.paramMap.get('id');
        this.bikeService.getBikeDetail(id)
                .subscribe(bike  => {
                        this.isLoading  =  false;
                        this.bike  =  bike['data'];
        });
 }
  1. 现在,让我们添加onVote()函数。在getBikeDetail()函数之后添加以下代码:
 onVote(rating:  number, id:  number):  void {
        // Check if user already vote on a bike
        if (this.checkUserVote(this.bike.ratings)) {
                alert('you already vote on this bike');
                return;
        }
        // Get bike id
        id  =  +this.route.snapshot.paramMap.get('id');
        // post vote
        this.bikeService.voteOnBike(rating, id)
                .subscribe(
                        (response) => {
                                this.userVote  =  response.data.rating;
                                // Update the average rating and rating object on bike
                                this.bike['average_rating'] =  response.data.average_rating;
                                // Update ratings array
                                this.bike.ratings.push(response.data);
                        }
                );
 }
  1. 现在,我们将创建一个函数,检查已登录用户是否已经对所选自行车进行了投票。请记住,RatingController.php正在使用firstOrCreate方法:
     public  function  store(Request $request, Bike $bike)
        {
                $rating =  Rating::firstOrCreate(
                        [
                        'user_id'  => $request->user()->id,
                        'bike_id'  => $bike->id,
                        ],
                        ['rating'  => $request->rating]
                );
                return  new  RatingResource($rating);
        }

我们只会注册第一次投票。因此,我们需要向用户显示一个简单的消息作为Vote函数的反馈。

  1. onVote()函数之后添加以下代码:
 checkUserVote(ratings:  any[]):  Boolean {
        const  currentUserId  =  this.auth.currentUser.id;
        let  ratingUserId:  number;
        Object.keys(ratings).forEach( (i) => {
                ratingUserId  =  ratings[i].user_id;
        });
        if ( currentUserId  ===  ratingUserId ) {
                return  true;
        } else {
                return  false;
        }
 }
  1. 以下方法使用提交函数来更新bike记录。在checkUserVote()函数之后添加以下代码:
 onSubmit(bike) {
        this.isLoading = true;
        const id = +this.route.snapshot.paramMap.get('id');
        this.bikeService.updateBike(id, bike.value)
        .subscribe(response => {
                this.isLoading = false;
                this.bike = response['data'];
        });
 }

请注意,在此步骤中,我们正在使用bikeServiceupdateBike方法。

  1. 最后一个方法是一个简单的函数,用于检查自行车所有者。请记住,用户只能编辑自己的自行车。在onSubmit()函数之后添加以下代码:
 checkBikeOwner(): Boolean {
        if (this.auth.currentUser.id === this.bike.user.id) {
                return true;
        } else {
                return false;
        }
 }

在此代码中,我们使用authService来获取User.id,然后与bike.user.id进行比较。

当我们访问http://localhost:4200/bikes/3 URL 时,此页面的预期结果将类似于以下屏幕截图:

自行车详细信息屏幕

请注意,我们可以在此自行车上看到编辑按钮,因为我们的应用程序种子已经用一些示例信息填充了数据库。

因此,如果我们点击编辑自行车按钮,我们将看到类似于以下的东西:

编辑自行车表单

创建自行车列表方法

让我们创建bike-list组件:

  1. 打开./Client/src/pages/bikes/bike-list/bike-list.component.ts并在核心导入之后添加以下导入:
 import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap/dropdown/dropdown.module';

 // App imports
 import { Bike } from '../bike';
 import { BikesService } from '../_services/bikes.service';
  1. bike-list.component类声明之后添加以下属性:
 // Using Bike Model class
 bikes: Bike[];
 isLoading: Boolean = false;
 public searchText: string;
  1. ./Client/src/pages/bikes/bike-list/bike-list.component.ts中,让我们创建constructor()函数:
 constructor(
        private bikeService: BikesService) {}
  1. ngOnInit()函数中,添加以下代码:
 // Get bike list
 this.getBikes();

现在,让我们创建this.getBikes()函数。

  1. ngOnInit()函数之后添加以下代码:
 getBikes(): void {
 this.isLoading = true;
 this.bikeService.getBikes()
        .subscribe(
        response => this.handleResponse(response),
        error => this.handleError(error));
 }

请注意,在此代码中,我们使用两个函数来处理成功和错误响应。可以将所有内容写在subscribe()函数内,但更好的组织技术是将它们分开。

  1. getBikes()函数之后添加以下代码:
 protected handleResponse(response: Bike[]) {
        this.isLoading = false,
        this.bikes = response;
 }

 protected handleError(error: any) {
        this.isLoading = false,
        console.error(error);
 }

在受保护的handleError方法中,我们只是使用console.log()来显示错误。

当我们访问http://localhost:4200/bikes URL 时,此页面的预期结果将类似于以下屏幕截图:

自行车列表页面

创建 builder-detail 方法

现在,是时候创建builder-detail组件了。让我们看看:

  1. 打开./Client/src/pages/builders/builder-detail/builder-detail.component.ts并在核心导入之后添加以下导入:
 import { ActivatedRoute } from '@angular/router';

 // App imports
 import { Builder } from './../builder';
 import { BuildersService } from '../_services/builders.service';
  1. builder-detail.component类声明之后添加以下属性:
   builder: Builder;
   isLoading: Boolean = false;
  1. ./Client/src/pages/builders/builder-detail/builder-detail.component.ts中,让我们创建constructor()函数:
 constructor(
        private buildersService: BuildersService,
        private route: ActivatedRoute) { }
  1. ngOnInit()函数中,添加以下代码:
 ngOnInit() {
        // Get builder detail
        this.getBuilderDetail();
 }

现在,让我们创建this.getBuilderDetail()函数。

  1. ngOnInit()函数之后添加以下代码:
 getBuilderDetail(): void {
        this.isLoading = true;
        const id = +this.route.snapshot.paramMap.get('id');
        this.buildersService.getBuilderDetail(id)
                .subscribe(builder => {
                this.isLoading = false;
                this.builder = builder['data'];
        });
 }

当我们访问http://localhost:4200/builders/4URL 时,此页面的预期结果将类似于以下屏幕截图:

建筑师详细页面

创建builder-list方法

现在,让我们创建builder-list方法来列出所有建筑师:

  1. 打开./Client/src/pages/builders/builder-list/builder-list.component.ts并在核心导入后添加以下导入:
 // App imports
 import { Builder } from './../builder';
 import { BuildersService } from '../_services/builders.service';
  1. BuilderListComponent类声明后添加以下属性:
 // Using Builder Model class
 builders: Builder[];
 isLoading: Boolean = false;
  1. 仍然在./Client/src/pages/builders/builder-list/builder-list.component.ts中,让我们创建constructor()函数:
 constructor(private builderService: BuildersService) { }
  1. ngOnInit()函数内添加以下代码:
 ngOnInit() {
        // Get builder detail
        this.getBuilders();
 }
  1. ngOnInit()函数后面添加以下代码:
 getBuilders(): void {
 this.isLoading = true;
 this.builderService.getBuilders()
        .subscribe(
        response => this.handleResponse(response),
        error => this.handleError(error));
 }

请注意,在此代码中,我们使用两个函数来处理成功和错误响应。可以将所有内容写在subscribe()函数内,但更好的组织技术是将它们分开。

  1. getBuilders()函数后面添加以下代码:
 protected handleResponse(response: Builder[]) {
        this.isLoading = false,
        this.builders = response;
 }
 protected handleError(error: any) {
        this.isLoading = false,
        console.error(error);
 }

最后,我们已经准备好所有组件。

当我们访问http://localhost:4200/buildersURL 时,此页面的预期结果将类似于以下屏幕截图:

建筑师列表页面

处理 Angular 管道、表单和验证

在本节中,我们将看到如何在自行车列表页面内创建一个简单的搜索组件,使用新的管道功能。我们还将看看如何以两种方式创建 Angular 表单:使用模板驱动表单和响应式表单。最后,我们将向您展示如何在 Bootstrap CSS 中使用表单验证。

创建管道过滤器

在 Angular 中,管道是一种简单的过滤和转换数据的方式,与旧的 AngularJS 过滤器非常相似。

在 Angular 中,我们有一些默认的管道(DatePipeUpperCasePipeLowerCasePipeCurrencyPipePercentPipe),我们也可以创建自己的管道。

要创建自定义管道,我们可以使用 Angular CLI 为我们生成脚手架。让我们看看它是如何工作的:

  1. 打开您的终端窗口,并在./Client/src/app内输入以下命令:
 ng g pipe pages/bikes/_pipes/bikeSearch

像往常一样,Angular CLI 会负责创建文件和适当的导入。

  1. 打开./Client/src/app/pages/bikes/_pipes/bike-search.pipe.ts并在BikeSearchPipe类内添加以下代码:
 transform(items: any, searchText: string): any {
 if (searchText) {
        searchText = searchText.toLowerCase();
        return items.filter((item: any) => item.model.toLowerCase().indexOf(searchText) > -1);
 }
 return items;
 }

先前的transform函数接收两个参数:来自自行车列表页面搜索框的输入字段的列表和搜索字符串。因此,让我们看看如何在bike-list模板内使用它们。

  1. 打开./Client/src/app/pages/bikes/bike-list/bike-list.component.ts并在搜索输入字段内添加以下属性:
 <input [(ngModel)]="searchText" [ngModelOptions]="{standalone: true}" placeholder="buscar" type="text" class="form-control"
       id="search" placeholder="Search">

既然我们已经有了搜索模型,让我们在*ngFor循环上添加管道过滤器。

  1. *ngFor属性内添加以下代码:
 <div class="col-md-4" *ngFor="let bike of bikes | bikeSearch: searchText ">...</div>

因此,当我们在搜索输入框中输入自行车型号时,我们将看到以下屏幕截图:

搜索字段工作

现在,让我们看看如何实现 Angular 表单。

介绍 Angular 表单

众所周知,表单是任何现代 Web 应用程序的重要组成部分,用于登录用户到应用程序,添加产品,并向博客发送评论。有些表单非常简单,但其他表单可能有一系列字段,甚至有许多步骤和页面,带有大量输入字段。

在 Angular 中,我们可以实现两种类型的表单:

  • 模板驱动表单

  • 响应式表单或模型驱动表单

两者同样强大,并属于@angular/forms库。它们基于相同的表单控件类。然而,它们有不同的哲学、编程风格和技术,验证也不同。在下一节中,我们将看到每种技术的独特之处。

理解 Angular 模板驱动表单

正如我们之前解释的,模板驱动表单非常类似于 AngularJS 表单,并使用诸如ngModel和可能requiredminlengthmaxlength等指令。当我们使用这些表单指令时,我们让模板在幕后完成工作。

审查登录表单模板和组件

一个很好的例子来理解模板驱动表单是登录表单。让我们看一下login.component.htmllogin.component.ts

  1. 打开./Client/src/app/pages/auth/login/login.component.html并审查模板输入标签:
 [(ngModel)]="user.email"  name="email"
 [(ngModel)]="user.password" name="password"

请注意,我们正在使用ngModel = [(ngModel)]的双向数据绑定语法。这意味着我们可以从登录组件类设置初始数据,但也可以更新它。

请记住,Angular 的ngModel可以以三种不同的方式使用:

  • ngModel:没有绑定或赋值,并且依赖于 name 属性

  • [ngModel]:单向数据绑定语法

  • [(ngModel)]:双向数据绑定语法

对于提交按钮事件,我们只是使用了(ngSubmit)="onSubmit(loginForm)" #loginForm="ngForm"指令,传递loginForm

现在我们的login.component.ts完整了,我们唯一需要的是onSubmit函数。

  1. 现在,让我们通过用以下代码替换login.component.ts来编辑它:
 import { Component, OnInit } from '@angular/core';
 import { Router, ActivatedRoute } from '@angular/router';

 // App imports
 import { AuthService } from '../_services/auth.service';
 import { User } from '../user';

 @Component({
 selector: 'app-login',
 templateUrl: './login.component.html',
 styleUrls: ['./login.component.scss']
 })
 export class LoginComponent implements OnInit {
        user: User = new User();
        error: any;
        returnUrl: string;

        constructor(
                private authService: AuthService,
                private router: Router,
                private route: ActivatedRoute) { }

        ngOnInit() {
                //  Set the return url
                this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/';
        }

        onSubmit(loginForm): void {
                this.authService.onLogin(this.user).subscribe(
                (response) => {
                        // get return url from route parameters or default to '/'
                        this.router.navigate([this.returnUrl]);
                },
                (error) => {
                        this.error = error.error;
                }
                );
                // Clear form fields
                loginForm.reset();
        }

 }

请注意,我们将loginForm传递给onSubmit(loginForm)函数,并使用authService将数据发送到端点。

理解 Angular 响应式/模型驱动表单

响应式/模型驱动表单和模板驱动表单之间的一个区别是使用诸如ngModel之类的指令。

这背后的原则是,我们使用表单 API 将指令负责地传递到component.ts代码中。这具有更大的能力,对于工作来说非常高效,将所有逻辑保留在同一个地方,我们很快就会看到。

审查注册表单模板和组件

一个很好的例子来理解模型驱动表单是注册表单。让我们看一下register.component.htmlregister.component.ts

  1. 打开./Client/src/app/pages/auth/register/register.component.html并审查模板输入标签:
 formControlName="name"
 formControlName="email"
 formControlName="password"

这几乎是我们在模板驱动表单中使用的相同符号,但更清晰一些。在这里,我们不需要name属性。

对于提交按钮事件,我们只是使用了[formGroup]="registerForm" (ngSubmit)="onSubmit()"属性和绑定函数。

  1. 现在,让我们创建register.component.ts。用以下代码替换它的代码:
 import { Component, OnInit } from '@angular/core';
 import { Router } from '@angular/router';
 import { FormBuilder, FormGroup, Validators } from '@angular/forms';

 // App imports
 import { User } from '../user';
 import { AuthService } from '../_services/auth.service';

 @Component({
 selector: 'app-register',
 templateUrl: './register.component.html',
 styleUrls: ['./register.component.scss']
 })
 export class RegisterComponent implements OnInit {

        user: User = new User();
        error: any;
        registerForm: FormGroup;

        constructor(private authService: AuthService, private router: Router, private fb: FormBuilder) {
                this.createForm();
        }

        ngOnInit() {}

        createForm() {
                this.registerForm = this.fb.group({
                name: [this.user.name, Validators.compose([Validators.required])],
                email: [this.user.email, Validators.compose([Validators.required, Validators.email ])],
                password: [this.user.password, Validators.compose([Validators.required, Validators.minLength(6)])],
                });
        }

        onSubmit(): void {

                this.authService.onRegister(this.registerForm.value).subscribe(
                (response) => {
                        this.router.navigate(['bikes']);
                },
                (response) => {
                        if (response.status === 422) {
                        Object.keys(response.error).map((err) => {
                                this.error = `${response.error[err]}`;
                        });

                        } else {
                        this.error = response.error;
                        }
                }
                );
        }

 }

请注意,在这段代码中,我们正在处理提交函数上的错误消息。在接下来的示例中,我们将看看如何在两种表单上实现表单验证,但现在让我们回顾一些重要的要点。

  1. 打开./Client/src/app/pages/auth/register/register.component.ts;让我们回顾registerComponent类。

我们可以注意到的第一个区别是文件顶部的FormBuilderFormGroupValidators的导入:

     import { FormBuilder, FormGroup, Validators } from
     '@angular/forms';

我们还需要在auth.module.ts内导入ReactiveFormsModule

     import { FormsModule, ReactiveFormsModule } from '@angular/forms';

我们可以使用FormBuilder API 在createForm()函数内创建表单:

     createForm() {
                this.registerForm = this.fb.group({
                        name: [this.user.name, Validators.compose([Validators.required])],
                        email: [this.user.email, Validators.compose([Validators.required, Validators.email ])],
                        password: [this.user.password, Validators.compose([Validators.required, Validators.minLength(6)])],
                });
        }

在这里,我们使用Validators直接从component.ts代码中添加表单验证。很棒,对吧?

请记住,fb变量保存了我们放在构造函数中的FormBuilder:private fb:FormBuilder。我们还将registerForm设置为RegisterClass内的FormGroup

添加前端表单验证

正如我们今天所知道的,当谈到用户体验时,向最终用户显示持续的反馈是一个很好的做法,因此在将表单发送到后端之前验证表单是一个很好的做法。

在本节中,我们将看看如何向登录和注册表单添加表单验证。

处理模板驱动表单上的表单验证

打开./Client/src/app/pages/auth/login/login.component.html并用以下代码替换表单标记:

 <form class="form-signin" (ngSubmit)="onSubmit(loginForm)" #loginForm="ngForm">
        <div class="text-center mb-4">
                <h1 class="h3 mt-3 mb-3 font-weight-normal">Welcome</h1>
                <p>Motorcycle builders and road lovers</p>
                <hr>
        </div>
        <div class="form-group" [ngClass]="{ 'has-error': !email.valid && (email.dirty || email.touched) }">
                <label for="email">Email address</label>
                <input type="email" [(ngModel)]="user.email"  name="email" #email="ngModel" required class="form-control" id="email" aria-describedby="emailHelp" placeholder="Enter email">
                <div *ngIf="email.invalid && (email.dirty || email.touched)" class="form-feedback">
                        <div *ngIf="email?.errors.required">Email is required</div>
                        <div *ngIf="email?.errors.email">Email must be a valid email address</div>
                </div>
        </div>
        <div class="form-group" [ngClass]="{ 'has-error': !password.valid && (password.dirty || password.touched) }">
                <label for="password">Password</label>
                <input type="password" [(ngModel)]="user.password" name="password" #password="ngModel" required minlength="6" class="form-control" id="password" placeholder="Password">
                <div *ngIf="password.invalid && (password.dirty || password.touched)" class="form-feedback">
                        <div *ngIf="password?.errors.required">Password is required</div>
                        <div *ngIf="password?.errors.minlength">Password must be at least 6 characters</div>
                </div>
        </div>
        <div  *ngIf="error" class="alert alert-danger" role="alert">
                Ops: {{ error.error }}
        </div>
        <button [disabled]="!loginForm.valid" class="btn btn-lg btn-primary btn-block mt-5" type="submit">Login</button>
 </form>

让我们回顾一下之前的代码。

请注意,我们正在使用内置在 Angular 指令中的[ngClass]将错误类应用于div表单组,如果表单无效:

     // Email field
        class="form-group" [ngClass]="{ 'has-error': !email.valid && (email.dirty || email.touched) }"
        // Password field
        class="form-group" [ngClass]="{ 'has-error': !password.valid && (password.dirty || password.touched) }"

为了显示错误消息,我们将在输入字段后创建两个新的 div:

     // Email validation
        <div *ngIf="email.invalid && (email.dirty || email.touched)" class="form-feedback">
                <div *ngIf="email?.errors.required">Email is required</div>
                <div *ngIf="email?.errors.email">Email must be a valid email address</div>
        </div>
        // Password validation
        <div *ngIf="password.invalid && (password.dirty || password.touched)" class="form-feedback">
                <div *ngIf="password?.errors.required">Password is required</div>
                <div *ngIf="password?.errors.minlength">Password must be at least 6 characters</div>
        </div>

借助ngIf和表单状态(脏、触摸),我们可以看到每个错误,如果输入字段符合此条件。

下一个规则是,以下div显示可能发生的后端错误:

     <div  *ngIf="error" class="alert alert-danger" role="alert">
                Ops: {{ error.error }}
        </div>

最后,使用[disabled]指令在提交按钮上设置验证:

     <button [disabled]="!loginForm.valid" class="btn btn-lg btn-primary btn-block mt-5" type="submit">Login</button>

我们表单的最终结果将类似于以下内容:

登录表单验证

处理基于模型的表单验证

打开./Client/src/app/pages/auth/register/register.component.html并用以下代码替换表单标签:

 <form [formGroup]="registerForm" (ngSubmit)="onSubmit()"  class="form-register" novalidate>
        <div class="text-center mb-4">
                <h1 class="h3 mt-3 mb-3 font-weight-normal">Welcome</h1>
                <p>Motorcycle builders and road lovers</p>
                <hr>
        </div>
        <div class="form-group" [ngClass]="{ 'has-error': !registerForm.get('name').valid && (registerForm.get('name').dirty || registerForm.get('name').touched) }">
                <label for="name">Name</label>
                <input type="name" formControlName="name" class="form-control" id="name" aria-describedby="nameHelp" placeholder="Enter your name">
                <div class="form-feedback"
                        *ngIf="registerForm.get('name').errors && (registerForm.get('name').dirty || registerForm.get('name').touched)">
                        <div *ngIf="registerForm.get('name').hasError('required')">Name is required</div>
                </div>
        </div>
        <div class="form-group" [ngClass]="{ 'has-error': !registerForm.get('email').valid && (registerForm.get('email').dirty || registerForm.get('email').touched) }">
                <label for="email">Email address</label>
                <input type="email" formControlName="email" class="form-control" id="email" aria-describedby="emailHelp" placeholder="Enter email">
                <div class="form-feedback"
                *ngIf="registerForm.get('email').errors && (registerForm.get('email').dirty || registerForm.get('email').touched)">
                        <div *ngIf="registerForm.get('email').hasError('required')">Email is required</div>
                        <div *ngIf="registerForm.get('email').hasError('email')">Email must be a valid email address</div>
                </div>
        </div>
        <div class="form-group" [ngClass]="{ 'has-error': !registerForm.get('password').valid && (registerForm.get('password').dirty || registerForm.get('password').touched) }">
                <label for="password">Password</label>
                <input type="password" formControlName="password"  class="form-control" id="password" placeholder="Password">
                <div class="form-feedback"
                *ngIf="registerForm.get('password').errors && (registerForm.get('password').dirty || registerForm.get('password').touched)">
                        <p *ngIf="registerForm.get('password').hasError('required')">Password is required</p>
                        <p *ngIf="registerForm.get('password').hasError('minlength')">Password must be 6 characters long, we need another {{registerForm.get('password').errors['minlength'].requiredLength - registerForm.get('password').errors['minlength'].actualLength}} characters </p>
                </div>
        </div>
        <div  *ngIf="error" class="alert alert-danger" role="alert">
                Ops: {{ error }}
        </div>
        <button [disabled]="!registerForm.valid" class="btn btn-lg btn-primary btn-block mt-5" type="submit">Register</button>
 </form>

让我们回顾一下之前的代码。

请注意,我们正在使用内置在 Angular 中的[ngClass]error类应用于div表单组,如果表单无效:

     // Name field
        class="form-group" [ngClass]="{ 'has-error': !registerForm.get('name').valid && (registerForm.get('name').dirty || registerForm.get('name').touched) }"
        // Email field
        class="form-group" [ngClass]="{ 'has-error': !registerForm.get('email').valid && (registerForm.get('email').dirty || registerForm.get('email').touched) }"
        // Password field
        class="form-group" [ngClass]="{ 'has-error': !registerForm.get('password').valid && (registerForm.get('password').dirty || registerForm.get('password').touched) }"

在这里,您可以注意到我们正在使用registerForm.get()方法,使输入字段与登录表单有所不同。

为了显示错误消息,我们将在输入字段后创建三个新的div

     // Name validation
        <div class="form-feedback"
                *ngIf="registerForm.get('name').errors && (registerForm.get('name').dirty || registerForm.get('name').touched)">
                <div *ngIf="registerForm.get('name').hasError('required')">Name is required</div>
        </div>

        // Email validation
        <div class="form-feedback"
                *ngIf="registerForm.get('email').errors && (registerForm.get('email').dirty || registerForm.get('email').touched)">
                <div *ngIf="registerForm.get('email').hasError('required')">Email is required</div>
                <div *ngIf="registerForm.get('email').hasError('email')">Email must be a valid email address</div>
        </div>

        // Password validation
        <div class="form-feedback"
                *ngIf="registerForm.get('password').errors && (registerForm.get('password').dirty || registerForm.get('password').touched)">
                <p *ngIf="registerForm.get('password').hasError('required')">Password is required</p>
                <p *ngIf="registerForm.get('password').hasError('minlength')">Password must be 6 characters long, we need another {{registerForm.get('password').errors['minlength'].requiredLength - registerForm.get('password').errors['minlength'].actualLength}} characters </p>
        </div>

下一个规则是,以下div用于显示可能发生的后端错误:

     <div  *ngIf="error" class="alert alert-danger" role="alert">
                Ops: {{ error }}
        </div>

最后,使用[disabled]指令在提交按钮上设置验证:

     <button [disabled]="!registerForm.valid" class="btn btn-lg btn-
      primary btn-block mt-5" type="submit">Register</button>

我们表单的最终结果将类似于以下内容:

注册表单验证

在下一个截图中,我们可以看到后端错误,这是我们尝试插入一个已经在使用中的电子邮件地址的地方:

后端错误消息

总结

我们完成了另一个章节,我们的示例应用程序具有现代 Web 应用程序的所有关键要点。我们学会了如何安装、定制和扩展 Bootstrap CSS 框架,并学会了如何使用NgBootstrap组件。

我们还了解了如何设置组件和服务,表单验证以及许多其他非常有用的技术。

在下一章中,我们将看到如何为 SCSS 和 TS 文件设置 linter,以及如何使用 Docker 镜像进行部署。

第十一章:构建和部署 Angular 测试

在上一章中,您学习了如何安装、定制和扩展 Bootstrap CSS 框架;如何使用NgBootstrap组件;以及如何将 Angular 服务与组件和 UI 界面连接起来。现在,让我们看看 Angular 应用程序中的另一个关键点:测试。

测试是检查应用程序代码以查找问题的一种很好的方法。在本章中,您将学习如何测试 Angular 应用程序,如何配置应用程序的 linter(用于 SCSS 和 TSLint 文件)以保持代码一致性,以及如何创建npm构建脚本。此外,您还将学习如何为前端应用程序创建 Docker 镜像。

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

  • 设置应用程序 linter

  • 了解 Angular 测试

  • 编写单元测试和端到端测试

  • 应用部署

准备基线代码

首先,我们需要准备我们的基线代码,这个过程与之前的章节非常相似。按照以下步骤:

  1. 复制所有内容从chapter-10文件夹。

  2. 重命名文件夹chapter-11

  3. 删除storage-db文件夹。

现在,让我们对docker-compose.yml文件进行一些更改,以适应新的数据库和服务器容器。

  1. 打开docker-compose.yml并用以下代码替换内容:
 version: "3.1"
 services:
     mysql:
       image: mysql:5.7
       container_name: chapter-11-mysql
       working_dir: /application
       volumes:
         - .:/application
         - ./storage-db:/var/lib/mysql
       environment:
         - MYSQL_ROOT_PASSWORD=123456
         - MYSQL_DATABASE=chapter-11
         - MYSQL_USER=chapter-11
         - MYSQL_PASSWORD=123456
       ports:
         - "8083:3306"
     webserver:
       image: nginx:alpine
       container_name: chapter-11-webserver
       working_dir: /application
       volumes:
         - .:/application
         - ./phpdocker/nginx/nginx.conf:/etc/nginx/conf.d/default.
            conf
       ports:
         - "8081:80"
     php-fpm:
       build: phpdocker/php-fpm
       container_name: chapter-11-php-fpm
       working_dir: /application
       volumes:
         - ./Server:/application
         - ./phpdocker/php-fpm/php-ini-
            overrides.ini:/etc/php/7.2/fpm/conf.d/99-overrides.ini

请注意,我们更改了容器名称、数据库和 MySQL 用户:

  • container_name: chapter-11-mysql

  • container_name: chapter-11-webserver

  • container_name: chapter-11-php-fpm

  • MYSQL_DATABASE=chapter-11

  • MYSQL_USER=chapter-11

  1. 使用以下连接字符串更新.env文件:
 DB_CONNECTION=mysql
 DB_HOST=mysql
 DB_PORT=3306
 DB_DATABASE=chapter-11
 DB_USERNAME=chapter-11
 DB_PASSWORD=123456
  1. 添加我们对 Git 源代码所做的更改。打开终端窗口并输入以下命令:
 git add .
 git commit -m "Initial commit chapter 11"

设置应用程序 linter

我们都希望有一个干净和一致的代码库。无论采用的编程语言是什么,使用 JavaScript 和其他语言的 linter 是非常常见的。但是,当我们讨论 CSS 或 SCSS/LESS 时,这种做法并不常见;我们很少为我们的样式表使用 linter。

linter是一种分析代码并报告错误的工具。我们设置规则,当一段代码不符合 linter 配置中定义的规则时,linter 会报告一个错误。当团队在壮大并需要保持代码库一致性时,这个功能非常有用。

如果您没有严格的编码风格规则,代码很快就会变得一团糟。即使您是独自工作,保持代码一致性也是一种良好的实践。

在接下来的章节中,您将学习如何为 SCSS 和 TypeScript 文件应用 linter。

为 SCSS 文件添加 stylelint

我们将使用stylelint,一个强大的、现代的样式表 linter,支持 CSS、LESS 和 SASS。stylelint有很多默认可用的规则,并且非常容易通过我们自己的规则进行扩展,它完全没有意见。另一个优点是,所有规则默认都是禁用的,我们只启用我们想要使用的规则。让我们看看它的实际应用。

./Client文件夹内打开终端窗口,并输入以下命令:

 npm install stylelint --save-dev &&
 npm install stylelint-config-standard --save-dev &&
 npm install stylelint-scss --save-dev

前面的命令非常清晰,对吧?我们正在安装默认配置标准插件,以及 SCSS 插件。

您可以在官方文档github.com/stylelint/stylelint中了解更多关于stylelint的信息。

向 package.json 文件添加新的脚本

打开./Client文件夹中的package.json文件,并在lint任务之后添加以下代码:

     "sasslint": "./node_modules/.bin/stylelint \"src/**/*.scss\" --syntax scss || echo \"Ops: Stylelint faild for some file(s).\"",

请注意,我们正在使用来自本地node_modules文件夹的Stylelint。这有助于确保整个团队使用相同的插件版本,避免兼容性问题。

添加.stylelintrc 配置

让我们添加我们自己的规则,如下所示:

  1. ./Client文件夹内,创建一个名为.stylelintrc的新文件。

  2. 将以下规则添加到./Client/.stylelintrc文件中:

     {
        "extends": ["stylelint-config-standard"],
        "rules": {
                "font-family-name-quotes": "always-where-recommended",
                "function-url-quotes": [
                        "always",
                        {
                        "except": ["empty"]
                        }
                ],
                "selector-attribute-quotes": "always",
                "string-quotes": "double",
                "max-nesting-depth": 3,
                "selector-max-compound-selectors": 3,
                "selector-max-specificity": "0,3,2",
                "declaration-no-important": true,
                "at-rule-no-vendor-prefix": true,
                "media-feature-name-no-vendor-prefix": true,
                "property-no-vendor-prefix": true,
                "selector-no-vendor-prefix": true,
                "value-no-vendor-prefix": true,
                "no-empty-source": null,
                "selector-class-pattern": "[a-z-]+",
                "selector-id-pattern": "[a-z-]+",
                "selector-max-id": 0,
                "selector-no-qualifying-type": true,
                "selector-max-universal": 0,
                "selector-pseudo-element-no-unknown": [
                        true,
                        {
                        "ignorePseudoElements": ["ng-deep"]
                        }
                ],
                "unit-whitelist": ["px", "%", "em", "rem", "vw", "vh", "deg"],
                "max-empty-lines": 2
        }
 }
  1. 请注意,您可以使用任何您想要的规则;没有对错之分。这只是一种口味和团队偏好的问题。例如,如果您的团队选择只在整个项目中使用px像素,那么您的unit-whitelist配置将如下所示:
"unit-whitelist": ["px"],
  1. 让我们进行一个简短的测试,以确保一切进行顺利。在./Client中打开终端窗口,并输入以下命令:
npm run sasslint

前面的命令报告了我们项目中的 77 个错误。这怎么可能?我们只有几行代码,其中大部分是在style.scss文件中的代码缩进。这是预期的,因为这是唯一一个包含 SCSS 的文件。请记住,我们没有在components.scss文件中添加任何 SCSS 代码。

为 VS Code 安装 Stylelint 插件

如果您使用vs.code(我希望您是),请按照以下步骤安装 Stylelint 插件:

  1. 在 VS Code 中,打开左侧的extensions面板。

  2. 在搜索输入框中输入stylelint

  3. 选择stylelint扩展。

  4. 重新启动 VS Code。

为新的 linter 设置 VS Code

现在,让我们配置 VS Code 仅使用stylelint规则;这将防止我们在 VS Code 集成终端中看到双重错误消息(如果您使用不同的代码编辑器,不用担心)。步骤如下:

  1. 在 VS Code 中,导航到顶部菜单中的 Code | Preferences | Settings。

  2. 在右侧面板中添加以下代码:

 {
        "css.validate": false,
        "less.validate": false,
        "scss.validate": false
 }

要查看插件的效果,请在 VS Code 中打开./Client/src/style.scss文件。您将在底部面板中看到以下内容:

stylelint 扩展记录的错误

这些是我们在使用npm run sass-lint命令时看到的相同输出错误,但在这里,我们可以导航文件。如果您使用的是 macOS,请使用Command +鼠标点击。如果您使用的是 Windows 或 Linux,请使用Ctrl +鼠标点击。

在 style.scss 上应用 stylelint 规则

验证style.scss文件非常简单。让我们读一下错误消息。

从第 9 行到第 44 行,错误是关于缩进空格的,所以让我们去掉空格。

删除所有 Bootstrap 导入的@import左侧的空格。

现在,我们有 41 个错误。如果您在 VS Code 中,点击底部面板上的错误链接(在 Problems 选项卡上),并按照以下截图中所示的方式打开文件:

VS Code stylelint 插件错误

如果您没有使用 VS Code,在运行npm run sass-lint后,终端消息将与以下截图中所示的相同:

VS Code 终端 stylelint 错误

修复 SCSS 错误

让我们修复style.scss文件中的所有错误消息。

打开./Client/src/style.scss,并将@imports后的内容替换为以下代码:

 /* Sticky footer styles
 -------------------------------------------------- */
 html {
        position: relative;
        min-height: 100%;
 }

 body {
        /* Margin bottom by footer height */
        margin-bottom: 60px;
 }

 .footer {
        position: absolute;
        bottom: 0;
        width: 100%;
        /* Set the fixed height of the footer here */
        height: 60px;
        line-height: 60px; /* Vertically center the text there */
        background-color: #f5f5f5;
 }

 main {
        padding-top: 3.5em;
 }

 form {
        .form-signin,
        .form-register {
                width: 80%;
                margin: 0 auto;
        }

        .form-group {
                height: 80px;
        }

        .has-error {
                .form-control {
                        border-color: red;
                }

                .form-feedback {
                        color: red;
                        font-size: 0.9rem;
                }
        }
 }

 // Loading spinner
 .spinner {
        width: 40px;
        height: 40px;
        position: relative;
        margin: 100px auto;
 }

 .double-bounce1,
 .double-bounce2 {
        width: 100%;
        height: 100%;
        border-radius: 50%;
        background-color: #333;
        opacity: 0.6;
        position: absolute;
        top: 0;
        left: 0;
        animation: sk-bounce 2 infinite ease-in-out;
 }
 .double-bounce2 {
        animation-delay: -1;
 }
 @keyframes sk-bounce {
        0%,
        100% { transform: scale(0); }
        50% { transform: scale(1); }
 }
 @keyframes sk-bounce {
        0%,
        100% { transform: scale(0); }
        50% { transform: scale(1); }
 }

现在没有错误了,我们的项目将会安全并且符合我们的规则。接下来,让我们看看如何在项目中使用内置的 TypeScript linter。

将 TSLint-angular 添加到 package.json 文件中

正如我们之前提到的,代码一致性是一个成功项目的关键点。默认情况下,Angular CLI 已经将tslint添加到了我们的项目中,我们可以在package.json文件和 scripts 标签中看到,使用ng-lint命令。

然而,在我们编写本章时,Angular CLI 出现了一个小错误,当我们使用ng-lint命令时报告了错误消息两次。为了避免这种情况,让我们在package.json文件中的sass-lint脚本之后添加以下行:

"tslint": "./node_modules/.bin/tslint --project tsconfig.json || echo \"Ops: TSlint faild for some file(s).\"",

在前面的行中,我们使用了node_modules文件夹中的本地tslint二进制文件。这将帮助我们避免兼容性问题。

由于我们正在一个 Angular 项目中工作,遵循 Angular 官方的样式指南对我们来说将非常有帮助,因为它已经在开发者社区中得到了采纳。

您可以在官方文档中了解有关 Angular 样式指南的更多信息angular.io/guide/styleguide

为了帮助我们遵循样式指南,我们将使用一个名为tslint-angular的包:

  1. 打开终端窗口并输入以下命令:
 npm install tslint-angular --save-dev
  1. 现在,打开./Client/src/tslint.json文件,并用以下代码替换内容:
 {
        "extends": ["../tslint.json", "../node_modules/tslint-angular"],
        "rules": {
                "angular-whitespace": [true, "check-interpolation", "check-semicolon"],
                "no-unused-variable": true,
                "no-unused-css": true,
                "banana-in-box": true,
                "use-view-encapsulation": true,
                "contextual-life-cycle": true,
                "directive-selector": [
                        true,
                        "attribute",
                        "app",
                        "camelCase"
                ],
                "component-selector": [
                        true,
                        "element",
                        "app",
                        "kebab-case"
                ]
        }
 }

请注意,在前面的代码中,我们使用extends属性来扩展./Client/tslint.ts中的默认配置和我们的node_modules文件夹中的tslint-angular

您可以在github.com/mgechev/codelyzer#recommended-configuration了解更多关于推荐的 Angular 规则的信息。

在 package.json 中创建 linter 任务

现在,我们将创建一些任务来运行我们刚刚设置的 linters。

打开./Client/package.json并在sasalint脚本之前添加以下行:

 "lint:dev": "npm run sasslint && npm run tslint",

前面的代码将执行两个命令:一个用于sasslint,另一个用于tslint。因此,我们已经准备好开始测试我们的应用程序并准备部署。

您可以在官方文档中了解有关 TSlint-angular 的更多信息github.com/mgechev/tslint-angular

理解 Angular 测试

测试对于任何现代 Web 应用程序都非常重要,Angular 默认包括一些测试工具,如 Jasmine、Karma 和用于单元测试和端到端测试的保护程序。让我们看看每个工具的主要重点,以便了解它们之间的区别:

单元测试 端到端测试
测试单个组件、服务、管道等。 测试整个应用程序
测试单个特定行为。 测试真实世界的情况
需要模拟后端以进行测试。 测试完整应用程序上的重要功能
测试最详细级别的边缘情况。 不测试边缘情况

前面的表格很简单,但我们可以看到单元测试和端到端测试之间的所有主要区别,也称为e2e 测试。此外,这两个工具都使用 Jasmine 框架,这是一个用于测试 JavaScript 代码的行为驱动开发框架。

您可以在jasmine.github.io/了解更多关于 Jasmine 的信息。

如前所述,当我们使用 Angular CLI 生成应用程序时,这两个工具都已安装。

对于单元测试,我们将使用 Karma 测试运行器;在继续之前,让我们看看karma.conf.js以更好地了解我们已经拥有的内容。

打开./Client文件夹中的karma.conf.js并检查plugins标签:

plugins: [
  require('karma-jasmine'),
  require('karma-chrome-launcher'),
  require('karma-jasmine-html-reporter'),
  require('karma-coverage-istanbul-reporter'),
  require('@angular-devkit/build-angular/plugins/karma')
],

默认情况下,我们已经安装了一些插件,正如我们在前面的代码块中所看到的。

你可以在官方文档中了解有关 Karma 测试运行器的更多信息karma-runner.github.io/2.0/index.html

我们还有用于测试的浏览器的配置;默认情况下,我们已经安装了 Chrome:

browsers: ['Chrome'],

如果您想要使用不同的浏览器来运行测试怎么办?这很容易做到;只需安装您喜欢的浏览器。Karma 测试运行器支持最流行的浏览器,例如:

  • Safari

  • 火狐

  • Internet Explorer

此时,我们已经准备好开始测试我们的应用程序。让我们看看一切是如何运作的。

编写单元测试和端到端测试

现在,您将学习如何运行测试,以便更好地了解应用程序发生了什么。

在开始之前,让我们运行命令来执行测试。

打开终端窗口并输入以下命令:

ng test

前面的代码将执行所有单元测试;之后,我们将在终端中看到所有错误。

最后一行将与以下行非常相似:

Executed 25 of 25 (18 FAILED) (1.469 secs / 0.924 secs)

每个失败的测试都标记为红色,并且后面跟着一个错误消息,正如您在以下摘录中所看到的:

AppHttpInterceptorService should be created FAILED
                Error: StaticInjectorError(DynamicTestModule)[BuildersService -> HttpClient]:
                StaticInjectorError(Platform: core)[BuildersService -> HttpClient]:
                        NullInjectorError: No provider for HttpClient!

在终端中输出了如此多行,甚至很难看到已通过的测试。请注意,有七个测试。

在终端中监视测试可能不是最容易的任务,因此我们可以使用以下命令在浏览器中运行测试:

ng test --watch 

上述命令将打开 Chrome 并开始测试,但请记住,您必须在计算机上安装 Chrome 浏览器。测试完成后,您现在可以以更有效的方式查看结果:

浏览器中的 Karma 运行器

前面的屏幕截图比终端窗口要好得多,对吧?因此,当我们点击Spec List选项卡菜单时,我们可以看到以下内容:

测试视图

此外,还可以单击测试套件并检查该套件中的所有相关测试。让我们在下一节中看看这个功能。

修复单元测试

现在是时候开始修复所有测试了。让我们看看如何使所有测试都通过:

  1. 仍然在 Chrome 浏览器中,单击名为AppComponent 应该创建应用程序的第一个测试套件。您将看到以下页面:

AppComponent

请注意,在上一张屏幕截图中,您只能看到与AppComponent相关的测试。

  1. 返回到Spec List,并点击AppComponent 应该创建应用程序;您将看到以下页面:

AppComponent 应该创建应用程序

上述错误消息非常清晰:

Failed: Template parse errors: 'app-nav' is not a known element:
1\. If 'app-nav' is an Angular component, then verify that it is part of this module.
2\. If 'app-nav' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. ("[ERROR ->]<app-nav></app-nav> <router-outlet class="main"></router-outlet> <footer class="footer">

我们有一个模板错误,Angular 建议两种处理方法。第一个建议是检查app.module.ts,看看我们是否添加了app-nav组件导入。让我们检查一下:

import { NavComponent } from './layout/nav/nav.component';
@NgModule({
declarations: [
        AppComponent,
        NavComponent
],

前面的片段取自app.module.ts文件,并且我们导入了NavComponent。我们的操作是将@NgModule.schemas添加到我们的测试规范中:

  1. 打开./Client/src/app/app.component.spec.ts并用以下代码替换内容:
 import { TestBed, async, ComponentFixture } from '@angular/core/testing';
 import { RouterTestingModule } from '@angular/router/testing';
 import { NO_ERRORS_SCHEMA } from '@angular/core';

 // App imports
 import { AppComponent } from './app.component';

 describe('AppComponent', () => {
        let component: AppComponent;
        let fixture: ComponentFixture<AppComponent>;

        beforeEach(async(() => {
                TestBed.configureTestingModule({
                imports: [
                        RouterTestingModule
                ],
                declarations: [
                        AppComponent
                ],
                schemas: [NO_ERRORS_SCHEMA]
                }).compileComponents();
        }));

        beforeEach(() => {
                fixture = TestBed.createComponent(AppComponent);
                component = fixture.componentInstance;
                fixture.detectChanges();
        });

        it('should create', async(() => {
                expect(component).toBeTruthy();
        }));

        it('should render footer tag', async(() => {
                const compiled = fixture.debugElement.nativeElement;
                expect(compiled.querySelector('footer').textContent).toContain('2018 © All Rights Reserved');
        }));
 });

请注意,我们添加了schemas标签,以及我们的路由模块,以便测试通过,如下片段所示:

     TestBed.configureTestingModule({
                imports: [
                        RouterTestingModule
                ],
                declarations: [
                        AppComponent
                ],
                schemas: [NO_ERRORS_SCHEMA]
        }).compileComponents();

现在,如果我们再次检查浏览器,将会看到以下结果:

AppComponent 成功

接下来失败的测试是NavComponent 应该创建;让我们看看错误消息:

Failed: Template parse errors:
Can't bind to 'routerLink' since it isn't a known property of 'a'.

再次,错误消息很明确;我们需要在nav.component.spec.ts中添加RouterTestingModule

  1. 打开./Client/src/app/layout/nav.component.spec.ts并用以下代码替换内容:
 import { async, ComponentFixture, TestBed } from '@angular/core/testing';

 import { NavComponent } from './nav.component';
 import { RouterTestingModule } from '@angular/router/testing';
 import { HttpClientModule } from '@angular/common/http';

 describe('NavComponent', () => {
        let component: NavComponent;
        let fixture: ComponentFixture<NavComponent>;

        beforeEach(async(() => {
                TestBed.configureTestingModule({
                imports: [
                        RouterTestingModule,
                        HttpClientModule
                ],
                declarations: [ NavComponent ]
                })
                .compileComponents();
        }));

        beforeEach(() => {
                fixture = TestBed.createComponent(NavComponent);
                component = fixture.componentInstance;
                fixture.detectChanges();
        });

        it('should create', () => {
                expect(component).toBeTruthy();
        });
 });

现在我们可以看到我们的NavComponent测试通过了,如下图所示:

NavComponent 工作

让我们深呼吸,考虑下一行。

以下步骤与我们迄今为止执行的步骤非常相似。我们应该提到,我们在应用程序中使用路由,因此我们需要在所有测试的TestBed.configureTestingModule配置中的imports标签中添加RoutingTestingModule

imports: [ 
        RouterTestingModule
        ...
], 

此外,我们必须将相同的依赖项注入到所有使用服务的组件中(例如BikeServiceBuilderService),就像我们在components.ts文件中所做的那样。

在接下来的几节中,我们将替换许多文件的代码。不用担心-当某些内容很重要时,我们会提到它。

修复 authGuard 测试

打开./Client/src/app/pages/auth/_guards/auth.guard.spec.ts并用以下代码替换内容:

import { RouterTestingModule } from '@angular/router/testing';
import { TestBed, async, inject } from '@angular/core/testing';
import { HttpClient, HttpHandler } from '@angular/common/http';
import { Router } from '@angular/router';

//  App imports
import { AuthGuard } from './auth.guard';
import { AuthService } from '../_services/auth.service';

describe('AuthGuard Tests: ', () => {
const router = {
        navigate: jasmine.createSpy('navigate')
};

beforeEach(async(() => {
        TestBed.configureTestingModule({
        imports: [
                RouterTestingModule.withRoutes([
                {path: 'bikes:id'}
                ])
        ],
        providers: [AuthGuard, AuthService, HttpClient, HttpHandler, { provide: Router, useValue: router } ]
        });
}));

it('should AuthGuartd to be defined', inject([AuthGuard], (guard: AuthGuard) => {
        expect(guard).toBeTruthy();
}));

it('should AuthService to be defined', inject([AuthService], (auth: AuthService) => {
        expect(auth).toBeTruthy();
}));

});

请注意,我们正在将AuthService作为提供者注入;现在不要担心这个。在本章后面,我们将更详细地解释它。让我们专注于测试。

修复 authService 测试

打开./Client/src/app/pages/auth/_services/auth.service.spec.ts并用以下代码替换内容:

 import { TestBed, inject } from '@angular/core/testing';
 import { AuthService } from './auth.service';
 import { HttpClientModule } from '@angular/common/http';
 import { RouterTestingModule } from '@angular/router/testing';
 describe('AuthService', () => {
 beforeEach(() => { 
         TestBed.configureTestingModule({ 
         imports: [ 
                 RouterTestingModule, 
                 HttpClientModule 
                 ], 
                 providers: [AuthService]
                 }); 
 }); it('should be created', inject([AuthService], 
 (service: AuthService) => 
  { expect(service).toBeTruthy();
 })); 
});

修复登录测试

打开./Client/src/app/pages/auth/login/login.component.spec.ts并用以下代码替换内容:

import { RouterTestingModule } from '@angular/router/testing';
import { HttpClientModule } from '@angular/common/http';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';

// App imports
import { LoginComponent } from './login.component';
import { AuthService } from '../_services/auth.service';

describe('LoginComponent', () => {
let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;

beforeEach(async(() => {
        TestBed.configureTestingModule({
        imports: [
                RouterTestingModule,
                FormsModule,
                HttpClientModule
        ],
        declarations: [ LoginComponent ],
        providers: [AuthService]
        })
        .compileComponents();
}));

beforeEach(() => {
        fixture = TestBed.createComponent(LoginComponent);
        component = fixture.componentInstance;
        fixture.detectChanges();
});

it('should create', () => {
        expect(component).toBeTruthy();
});
});

正如我们之前提到的,几乎所有的错误消息都与我们是否包含了依赖项有关,比如服务或直接的 Angular 依赖项。

修复 register 测试

打开./Client/src/app/pages/auth/register/register.component.spec.ts并用以下代码替换内容:

import { RouterTestingModule } from '@angular/router/testing';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';

// App imports
import { RegisterComponent } from './register.component';
import { HttpClientModule } from '@angular/common/http';
import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms';

describe('RegisterComponent', () => {
let component: RegisterComponent;
let fixture: ComponentFixture<RegisterComponent>;

beforeEach(async(() => {
        TestBed.configureTestingModule({
        imports: [
                RouterTestingModule,
                HttpClientModule,
                FormsModule,
                ReactiveFormsModule
        ],
        declarations: [ RegisterComponent ],
        schemas: [NO_ERRORS_SCHEMA],
        providers: [FormBuilder]
        })
        .compileComponents();
}));

beforeEach(() => {
        fixture = TestBed.createComponent(RegisterComponent);
        component = fixture.componentInstance;
        fixture.detectChanges();
});

it('should create', () => {
        expect(component).toBeTruthy();
});
});

修复 bike 服务测试

打开./Client/src/app/pages/bikes/_services/bikes.service.spec.ts并用以下代码替换内容:

import { TestBed, inject } from '@angular/core/testing';
import { HttpClientModule } from '@angular/common/http';

// App imports
import { BikesService } from './bikes.service';
import { HttpErrorHandler } from '../../../shared/_services/http-handle-error.service';

describe('BikesService', () => {
beforeEach(() => {
        TestBed.configureTestingModule({
        imports: [
                HttpClientModule
        ],
        providers: [
                BikesService,
                HttpErrorHandler
        ]
        });
});

it('should be created', inject([BikesService], (service: BikesService) => {
        expect(service).toBeTruthy();
}));
});

修复 bike-detail 测试

打开./Client/src/app/pages/bikes/bike-detail/bike-detail.component.spec.ts并用以下代码替换内容:

import { RouterTestingModule } from '@angular/router/testing';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';

// App imports
import { BikeDetailComponent } from './bike-detail.component';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { HttpErrorHandler } from '../../../shared/_services/http-handle-error.service';

describe('BikeDetailComponent', () => {
let component: BikeDetailComponent;
let fixture: ComponentFixture<BikeDetailComponent>;

beforeEach(async(() => {
        TestBed.configureTestingModule({
        imports: [
                RouterTestingModule,
                FormsModule,
                HttpClientModule
        ],
        declarations: [
                BikeDetailComponent
        ],
        schemas: [NO_ERRORS_SCHEMA],
        providers: [HttpErrorHandler]
        })
        .compileComponents();
}));

beforeEach(() => {
        fixture = TestBed.createComponent(BikeDetailComponent);
        component = fixture.componentInstance;
        fixture.detectChanges();
});

it('should create', () => {
        expect(component).toBeTruthy();
});
});

修复 bike-list 测试

打开./Client/src/app/pages/bikes/bike-list/bike-list.component.spec.ts并用以下代码替换内容:

import { RouterTestingModule } from '@angular/router/testing';
import { HttpClientModule } from '@angular/common/http';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';

// App imports
import { BikeListComponent } from './bike-list.component';
import { BikeSearchPipe } from '../_pipes/bike-search.pipe';
import { HttpErrorHandler } from './../../../shared/_services/http-handle-error.service';

describe('BikeListComponent', () => {
let component: BikeListComponent;
let fixture: ComponentFixture<BikeListComponent>;

beforeEach(async(() => {
        TestBed.configureTestingModule({
        imports: [
                RouterTestingModule,
                HttpClientModule
        ],
        declarations: [
                BikeListComponent,
                BikeSearchPipe
        ],
        schemas: [NO_ERRORS_SCHEMA],
        providers: [HttpErrorHandler]
        })
        .compileComponents();
}));

beforeEach(() => {
        fixture = TestBed.createComponent(BikeListComponent);
        component = fixture.componentInstance;
        fixture.detectChanges();
});

it('should create', () => {
        expect(component).toBeTruthy();
});
});

修复 bike 测试

打开./Client/src/app/pages/bikes/bikes.component.spec.ts并用以下代码替换内容:

import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';

// App imports
import { BikesComponent } from './bikes.component';

describe('BikesComponent', () => {
let component: BikesComponent;
let fixture: ComponentFixture<BikesComponent>;

beforeEach(async(() => {
        TestBed.configureTestingModule({
        imports: [
                RouterTestingModule
        ],
        declarations: [
                BikesComponent
        ]
        })
        .compileComponents();
}));

beforeEach(() => {
        fixture = TestBed.createComponent(BikesComponent);
        component = fixture.componentInstance;
        fixture.detectChanges();
});

it('should create', () => {
        expect(component).toBeTruthy();
});

});

修复 builders 服务测试

打开./Client/src/app/pages/builders/_gservices/builders.service.spec.ts并用以下代码替换内容:

import { HttpClientModule } from '@angular/common/http';
import { TestBed, inject } from '@angular/core/testing';

// App imports
import { BuildersService } from './builders.service';
import { HttpErrorHandler } from './../../../shared/_services/http-handle-error.service';

describe('BuildersService', () => {
beforeEach(() => {
        TestBed.configureTestingModule({
        imports: [
                HttpClientModule
        ],
        providers: [
                BuildersService,
                HttpErrorHandler
        ]
        });
});

it('should be created', inject([BuildersService], (service: BuildersService) => {
        expect(service).toBeTruthy();
}));
});

修复 builder-detail 测试

打开./Client/src/app/pages/builders/builder-detail/builder-detail.component.spec.ts并用以下代码替换内容:

import { RouterTestingModule } from '@angular/router/testing';
import { HttpClientModule } from '@angular/common/http';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';

import { BuilderDetailComponent } from './builder-detail.component';
import { HttpErrorHandler } from '../../../shared/_services/http-handle-error.service';

describe('BuilderDetailComponent', () => {
let component: BuilderDetailComponent;
let fixture: ComponentFixture<BuilderDetailComponent>;

beforeEach(async(() => {
        TestBed.configureTestingModule({
        imports: [
                RouterTestingModule,
                HttpClientModule
        ],
        declarations: [
                BuilderDetailComponent
        ],
        providers: [HttpErrorHandler]
        })
        .compileComponents();
}));

beforeEach(() => {
        fixture = TestBed.createComponent(BuilderDetailComponent);
        component = fixture.componentInstance;
        fixture.detectChanges();
});

it('should create', () => {
        expect(component).toBeTruthy();
});
});

修复 builder-list 组件

打开./Client/src/app/pages/builders/builder-list/builder-list.component.spec.ts并用以下代码替换内容:

import { RouterTestingModule } from '@angular/router/testing';
import { HttpClientModule } from '@angular/common/http';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';

// App imports
import { BuilderListComponent } from './builder-list.component';
import { HttpErrorHandler } from '../../../shared/_services/http-handle-error.service';

describe('BuilderListComponent', () => {
let component: BuilderListComponent;
let fixture: ComponentFixture<BuilderListComponent>;

beforeEach(async(() => {
        TestBed.configureTestingModule({
        imports: [
                RouterTestingModule,
                HttpClientModule
        ],
        declarations: [
                BuilderListComponent
        ],
        providers: [HttpErrorHandler]
        })
        .compileComponents();
}));

beforeEach(() => {
        fixture = TestBed.createComponent(BuilderListComponent);
        component = fixture.componentInstance;
        fixture.detectChanges();
});

it('should create', () => {
        expect(component).toBeTruthy();
});
});

修复 builders 测试

打开./Client/src/app/pages/builders/builders.component.spec.ts并用以下代码替换内容:

import { RouterTestingModule } from '@angular/router/testing';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';

// App imports
import { BuildersComponent } from './builders.component';

describe('BuildersComponent', () => {
let component: BuildersComponent;
let fixture: ComponentFixture<BuildersComponent>;

beforeEach(async(() => {
        TestBed.configureTestingModule({
        imports: [
                RouterTestingModule
        ],
        declarations: [
                BuildersComponent
        ]
        })
        .compileComponents();
}));

beforeEach(() => {
        fixture = TestBed.createComponent(BuildersComponent);
        component = fixture.componentInstance;
        fixture.detectChanges();
});

it('should create', () => {
        expect(component).toBeTruthy();
});

});

修复 home 测试

打开./Client/src/app/pages/home/home.component.spec.ts并用以下代码替换内容:

import { TestBed , async, ComponentFixture } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';

// App imports
import { HomeComponent } from './home.component';

describe('HomeComponent', () => {
let component: HomeComponent;
let fixture: ComponentFixture<HomeComponent>;

beforeEach(async(() => {
        TestBed.configureTestingModule({
        imports: [
                RouterTestingModule
        ],
        declarations: [
                HomeComponent
        ]
        }).compileComponents();
}));

beforeEach(() => {
        fixture = TestBed.createComponent(HomeComponent);
        component = fixture.componentInstance;
        fixture.detectChanges();
});

it('should create', () => {
        expect(component).toBeTruthy();
});

});

修复应用程序测试

打开./Client/src/app/app.component.spec.ts并用以下代码替换内容:

import { TestBed, async, ComponentFixture } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';

// App imports
import { AppComponent } from './app.component';

describe('AppComponent', () => {
let component: AppComponent;
let fixture: ComponentFixture<AppComponent>;

beforeEach(async(() => {
        TestBed.configureTestingModule({
        imports: [
                RouterTestingModule
        ],
        declarations: [
                AppComponent
        ],
        schemas: [NO_ERRORS_SCHEMA]
        }).compileComponents();
}));

beforeEach(() => {
        fixture = TestBed.createComponent(AppComponent);
        component = fixture.componentInstance;
        fixture.detectChanges();
});

it('should create', async(() => {
        expect(component).toBeTruthy();
}));
});

修复应用拦截器测试

打开./Client/src/app/shared/_services/app-http-interceptor.service.spec.ts并用以下代码替换内容:

import { HttpClientModule } from '@angular/common/http';
import { TestBed, inject } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';

// App imports
import { AppHttpInterceptorService } from './app-http-interceptor.service';

describe('AppHttpInterceptorService', () => {
beforeEach(() => {
        TestBed.configureTestingModule({
        imports: [
                RouterTestingModule,
                HttpClientModule
        ],
        providers: [AppHttpInterceptorService]
        });
});

it('should be created', inject([AppHttpInterceptorService], (service: AppHttpInterceptorService) => {
        expect(service).toBeTruthy();
}));
});

我们现在已经修复了所有的测试,所以让我们再添加一些。

添加单元测试

我们已经走了很长的路,现在所有的测试都通过了。所以,是时候创建一些新的测试了。

以下行非常简单,我们在之前的示例中已经遵循了这条路径,所以,如果有新的内容出现,我们会在代码块的末尾提到它。

让我们在应用程序中创建一些单元测试,如下所示:

  1. 打开./Client/src/app/app.component.spec.ts并添加以下代码:
it('should create router-outlet', async(() => {
 const compiled = fixture.debugElement.nativeElement;
 expect(compiled.querySelector('router-outlet')).toBeDefined();
 }));

上述代码将检查app.component.spec.ts内的router-outlet标签。

  1. 打开./Client/src/app/pages/auth/_guards/auth.guard.spec.ts并添加以下代码:
it('should AuthService to be defined', inject([AuthService], (auth: AuthService) => {
 expect(auth).toBeTruthy();
 }));
it('should not allow user to pass', inject([AuthGuard],     (guard: AuthGuard) => {
expect(guard.canActivate(new ActivatedRouteSnapshot(), fakeSnapshot)).toBe(false);
}));

请注意,我们正在创建两个新的测试:一个用于检查AuthService,另一个用于检查AuthGuard

  1. 打开./Client/src/app/pages/bikes/bikes.component.spec.ts并添加以下代码:
it('should create router-outlet', async(() => {
 const compiled = fixture.debugElement.nativeElement;
 expect(compiled.querySelector('router-outlet')).toBeDefined();
 }));
  1. 打开./Client/src/app/pages/builders/builders.component.spec.ts并添加以下代码:
it('should create router-outlet', async(() => {
 const compiled = fixture.debugElement.nativeElement;
 expect(compiled.querySelector('router-outlet')).toBeDefined();
 }));
  1. 打开./Client/src/app/pages/home/home.component.spec.ts并添加以下代码:
it('should render title tag', async(() => {
 const compiled = fixture.debugElement.nativeElement;
 expect(compiled.querySelector('h1').textContent).toContain('Custom Bikes Garage');
 }));
  1. 打开./Client/src/app/app.component.spec.ts并添加以下代码:
it('should render footer tag', async(() => {
 const compiled = fixture.debugElement.nativeElement;
 expect(compiled.querySelector('footer').textContent).toContain('2018 © All Rights Reserved');
 }));

我们现在已经完成了示例单元测试。如果我们使用ng test执行测试,我们将在终端中看到以下结果:

Executed 24 of 24 SUCCESS (2.695 secs / 2.398 secs)

修复 e2e 测试

此时,我们将运行e2e测试,正如我们在本章前面提到的。

  1. 打开./Client/e2e/src/app.e2e-spec.ts并用以下代码替换内容:
 import { AppPage } from './app.po';

 describe('workspace-project App', () => {
        let page: AppPage;

        beforeEach(() => {
                page = new AppPage();
        });

        it('should display app title', () => {
                page.navigateTo();
                expect(page.getParagraphText()).toEqual('Custom Bikes Garage');
        });
 });
  1. 打开终端窗口并输入以下命令:
 npm run e2e

上述命令的结果将类似于以下截图:

e2e 测试结果

记住,你需要在./Client文件夹内运行 Angular 命令;否则,你会看到一个错误消息,因为 Angular CLI 需要angular.json文件来执行ng命令,而这个文件在./Client文件夹内。

应用程序部署

我们现在已经完成了测试我们的应用程序的所有必要步骤。测试可以非常全面,也可以非常简单;这将取决于您(或您的团队)选择的方法类型。

社区内有很多关于测试的争论。有些人支持面向开发的测试,比如行为驱动开发BDD)或测试驱动开发TDD)。

再次强调,最重要的是你的代码、结构和测试是一致的,无论采用何种类型的开发。

在最后一节中,我们将看看如何准备我们的应用程序以在生产中发布。从本书的开始,我们一直在使用 Docker,而且我们不会以其他方式结束这本书。

因此,让我们看看如何使用一些 Docker 功能来准备我们的应用程序。

为前端应用程序创建 Docker 镜像

正如我们之前讨论过的,我们已经配置了一个 Docker 环境,但它只涵盖了我们的后端应用程序,因为我们使用我们机器上安装的 Angular CLI 来运行我们的前端代码。

在开发环境中,这不是一个问题,因为我们需要我们在主机上编写的代码在我们的 Docker 容器中更新。然而,当我们讨论部署时,我们考虑到我们的代码已经准备好运行,而不需要对文件进行任何更改。

请注意,我们不讨论写入磁盘或数据持久性;我们只会提到应用程序源代码的更改。

考虑到这一点,让我们使用 Docker 的一些功能来创建我们的前端应用程序的镜像。

创建一个 Dockerfile

./Client中创建一个名为Dockerfile的新文件,并添加以下代码:

FROM nginx:alpine

COPY nginx.conf /etc/nginx/nginx.conf

WORKDIR /usr/share/nginx/html
COPY dist/ .

前面的代码非常简单;我们正在使用nginx:alpine的镜像,这是一个 Linux 发行版。

您可以在hub.docker.com/explore/了解更多关于官方 Docker 镜像的信息。

此外,我们正在使用一个 nginx 服务器的配置文件。请记住,Angular 应用程序必须由 Web 服务器托管。

创建一个 nginx 文件

./Client中,创建一个名为nginx.conf的新文件,并添加以下代码:

worker_processes  1;

events {
        worker_connections  1024;
}

http {
        server {
                listen 81;
                server_name  localhost;

                root   /usr/share/nginx/html;
                index  index.html index.htm;
                include /etc/nginx/mime.types;

                gzip on;
                gzip_min_length 1000;
                gzip_proxied expired no-cache no-store private auth;
                gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript;

                location / {
                        try_files $uri $uri/ /index.html;
                }
        }
}

这里没有什么新鲜的东西 - 这只是一个用于提供 Angular 文件的基本 nginx 配置。

创建 npm 构建任务

有了 Dockerfile,我们只需要使用我们可用的npm来创建一个构建过程。

打开./Client/package.json并添加以下代码:

"build:docker":"npm run lint:dev && npm run test && npm run e2e && npm rum build && npm rum docker:image",
 "docker:image":"./_scripts/create-docker-image.sh"

让我们解释一下我们在前面的代码中做了什么:

  • 脚本标签:docker:image将使用一个 bash 脚本文件来生成一个 Docker 镜像;稍后,我们将详细介绍这个文件。

  • 脚本标签:build:docker将执行以下步骤:

  1. 运行 SASS 监听。

  2. 运行Tslint

  3. 运行单元测试。

  4. 运行端到端测试。

  5. 构建应用程序。

  6. 创建 Docker 镜像。

在我们继续之前,让我们解释一下为什么我们要使用一个 bash 文件来创建 Docker 镜像。

Bash 文件在许多地方都非常有用,在任何构建过程中都没有什么不同,正如我们将在下面的行中看到的执行一些 Docker 命令。为了避免在npm包中增加更多的复杂性,我们将使用create-docker-image.sh文件的调用来执行生成我们的镜像所需的命令。

创建 bash 脚本

现在,我们将在我们的前端应用程序中添加一个新的目录,用来存储我们的应用程序可能有的所有 bash 脚本文件。在这个例子中,我们只会使用一个,但在真实的应用程序中,这个文件夹可以存储一系列的 bash 文件:

  1. ./Client中,创建一个名为_scripts的新文件夹。

  2. ./Client/_scripts文件夹中,创建一个名为create-docker-image.sh的新文件,并添加以下代码:

 #!/bin/bash
 set -e
 # Docker command to create the front-end application
 docker image build -t angular-laravel-book .

请注意,您可以为您的应用程序使用任何名称;我们在书的例子中使用angular-laravel-book

运行 npm 构建脚本

现在,让我们对angular.json文件进行一些小的调整;从output标签中删除Client文件夹:

"outputPath": "dist",

最后一步是运行build命令,以测试和创建我们的应用程序。

./Client文件夹内打开终端窗口,然后键入以下命令:

npm run build:docker

构建过程将需要几分钟;最后,您将在终端中看到类似以下的消息:

端到端测试如果您遇到权限错误,请执行以下操作。在./Client/_scripts文件夹内打开终端窗口,然后键入chmod 755 create-docker-image.sh

审查 Docker 命令

以下是本章结束时的一些观察:

  1. 在书的开头,我们使用 Docker 创建了开发环境。

  2. 在本节中,我们为前端应用程序创建了一个图像。

所以,现在是检查我们迄今为止所做的工作的合适时机。

从第四章开始,构建基线应用程序,我们一直在使用 Docker 创建后端 API 应用程序。在本章中,我们一直在使用 Docker 将前端 Angular 应用程序转换为 Docker 图像。因此,我们可以假设我们有一个用于后端的图像,其中包含服务器和数据库,另一个用于前端应用程序,也称为客户端应用程序。

这使我们有了分开托管服务的优势,正如我们在本书中早些时候提到的。

请记住,我们的后端 API 完全独立于前端应用程序。

生产环境构建应用程序

让我们在我们的docker-compose.yml文件中做一些调整,并添加前端应用程序的图像。

打开./Client/docker-compose.yml并添加以下代码:

appserver:
  image: 'angular-laravel-book'
  container_name: chapter-11-appserver
  # Build the image if don't exist
  build: './Client'
  ports:
    - 3000:81

注意注释行。作为build命令的一部分,我们使用了使用npm run build:docker命令创建的angular-laravel-book图像。因此,如果您忘记运行构建脚本,每次运行docker-compose up -d命令时,都会创建图像(如果尚不存在)。

测试 Docker 图像

现在是时候检查 Docker 容器和图像了。

注意,下一个命令将删除您机器上所有的 Docker 图像和容器。如果您除了本书示例之外还使用 Docker 进行其他项目,我们建议您只删除与我们示例应用程序相关的图像和容器。

以下命令将删除您机器上的所有图像和容器:

docker system prune -a

让我们检查容器,如下所示:

  1. 打开终端窗口,然后键入以下命令:
 docker ps -a

返回的输出将是一个空表。

  1. 仍然在终端中,键入以下命令:
 docker images -a

最后,您将再次看到一个空表。

  1. 仍然在终端中,键入以下命令:
 docker-compose up -d

恭喜!我们已成功构建了所有图像和容器。

  1. 重复步骤 1 以列出所有容器。

结果将是以下输出:

容器 ID 图像 名称
容器 ID chapter-11_php-fpm chapter-11-php-fpm
容器 ID nginx:alpine chapter-11-webserver
容器 ID mysql:5.7 chapter-11-mysql
容器 ID angular-laravel-book chapter-11-appserver

请注意,容器名称与我们在docker-compose.yml文件中选择的名称相同。

以下图像代表了我们的应用程序:

  • 前端angular-laravel-book

  • 后端phpdockerio/php72-fpm

我们现在已经准备好在云上部署。

总结

我们现在有必要的基线代码来将我们的应用程序投入生产。接下来的步骤是最多样化的,因为许多云服务都能够存储用于生产网站和应用程序的 Docker 图像,通常涉及使用付费服务。但是现在我们有一个使用最新技术(即 Angular 6 和 Laravel 5)构建的强大和可扩展的应用程序。

自从本书开始以来,我们已经走了很长的路,解释和介绍了先进的 Web 开发技术。现在你能够从头开始创建一个应用程序,一直到部署。

确保始终保持自己的最新状态,并牢记一致的代码总是能帮助你。

posted @ 2024-05-05 00:11  绝不原创的飞龙  阅读(1)  评论(0编辑  收藏  举报