Angular-服务端应用开发入门指南-全-

Angular 服务端应用开发入门指南(全)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

使用 Angular 构建的 Web 应用程序可以针对搜索引擎优化(SEO)进行优化。维基百科将 SEO 定义为“影响网站或网页在搜索引擎非付费结果中的在线可见性的过程——通常被称为“自然”、“有机”或“获得”的结果。”优化你的应用程序以适应搜索引擎意味着你的应用程序在互联网上更可见,可以为你的或你的客户带来更多收入。Angular 提供了一些内置功能,可以利用这些功能确保我们的应用程序在网络上享有最大的可见性。

在这本书中,你将学习如何使用 Angular 创建一个对 SEO 有良好支持的渐进式 Web 应用程序(PWA)。这个学习之旅从确定使应用程序 SEO 友好的因素开始,并安装 Angular CLI。然后,你将构建 UI 组件和应用程序组件。在第一课结束时,你将拥有一个使用 Angular 的最佳实践构建的应用程序。在接下来的两节课中,你将在你的应用程序中实现服务器端渲染和服务工作者。你将创建服务器应用程序,实现 Express 服务器,并给你的应用程序添加动态元数据。最后,你将配置服务工作者并测试你应用程序的离线功能。

到这本书的结尾,你将能够使用 Angular CLI 和最佳实践创建现代、SEO 优化的 Web 应用程序。

第 1 课,创建基础应用程序,展示了如何安装 Angular CLI 并创建本书中将使用的 Angular 应用程序。我们将设置一些默认设置,并使用 Bootstrap 和 Font Awesome 配置我们的全局样式。然后,我们将创建我们应用程序的基本 UI 和布局。

第 2 课,创建应用程序模块和组件,解释了不同类型的组件,例如表示性组件和容器组件。然后,我们将看到如何创建 PostsComponent、ProfileComponent、PostListComponent、PostItemComponent 和 ProfileItemComponent。最后,我们将创建解析器以使用路由检索数据。

第 3 课,服务器端渲染,展示了如何将服务器端渲染添加到我们的应用程序中。我们将从生成服务器应用程序并添加其依赖项开始。然后,我们在 package.json 文件中添加脚本,在 Express.js 中实现一个网络服务器。最后,我们将看到如何向我们的页面添加动态元数据。

第 4 课,服务工作者,展示了如何与服务工作者一起工作。我们将从安装所需的依赖项开始。然后,我们将继续启用服务工作者,配置它,测试它,并最终调试它。

本书将需要以下最低硬件要求:

  • 处理器:i3

  • 内存:2 GB RAM

  • 硬盘:10 GB

  • 互联网连接

在本书中,我们将使用 Node 和 npm 来运行我们的开发环境,该环境基于 Angular CLI。此外,还需要 Git 来从 GitHub 检索内容。请确保您的机器上已安装以下内容:

  • Node 6.9.0 或更高版本

  • npm 3.0 或更高版本

  • Git

检查版本

我们可以通过检查 Node、npm 和 Git 的版本来检查我们的机器是否符合要求。这可以通过以下命令完成:

node –v
npm –v
git --version

开发 API

本书专注于构建一个作为公共网站运行的 Angular 应用程序。它将从 REST API 检索内容,尽可能接近现实生活中的用例。开发 API 可以从 GitHub 下载,并安装在本地的机器上。

API 安装

安装 API,请运行以下命令:

$ git clone <repo_url>
$ cd packt-angular-seo-api
$ npm install
$ npm start

如果一切顺利,您应该在终端中看到以下消息:

Web server listening at: http://localhost:3000
Browse your REST API at http://localhost:3000/explorer

本书面向对象

本书非常适合经验丰富的前端开发者,他们希望快速通过一个智能示例来演示 Angular 服务器端开发的全部关键特性。您需要对 Angular 有一定的了解,因为我们只是简要地概述了基础知识,然后直接进入工作。

您还必须熟悉以下概念:

  • Angular 基础知识

  • HTML

  • CSS

  • TypeScript 基础知识

约定

在本书中,您将找到许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码词汇和 C++语言关键字将以如下方式显示:“我们将更新我们的PostsComponent以读取由我们的路由器解析的数据。”

文件夹名称、文件名、文件扩展名、路径名、包含在文本中的文件名将以如下方式显示:“头文件boost/asio.hpp包含了使用 Asio 库所需的大部分类型和函数。”

代码块将以如下方式设置:

import { PostsResolver } from './resolvers/posts-resolver'
import { ProfileResolver } from './resolvers/profile-resolver'

新术语和重要词汇将以粗体显示。屏幕上显示的词汇,例如在菜单或对话框中,将以如下方式显示:“如果我们检查 Chrome 开发者工具中的网络选项卡,我们会看到我们向同一端点发送了两个请求。”

重要的新编程术语将以粗体显示。概念性术语将以斜体显示。

注意

有关主题的重要附加细节将以如下方式显示,就像侧边栏一样。

小贴士

重要的注意事项、技巧和窍门将以如下方式显示。

读者反馈

我们始终欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢或不喜欢的地方。读者反馈对我们很重要,因为它帮助我们开发出您真正能从中获得最大收益的标题。

要向我们发送一般反馈,只需发送电子邮件至<feedback@packtpub.com>,并在邮件主题中提及本书的标题。

如果你在一个你擅长的主题上有专业知识,并且你对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在你已经是 Packt 图书的骄傲拥有者,我们有一些事情可以帮助你从你的购买中获得最大收益。

下载示例代码

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

勘误

尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然可能发生。如果你在我们的书中发现错误——可能是文本或代码中的错误——如果你能向我们报告这一点,我们将不胜感激。通过这样做,你可以帮助其他读者避免挫败感,并帮助我们改进本书的后续版本。如果你发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择你的图书,点击勘误提交表单链接,并输入你的勘误详情来报告它们。一旦你的勘误得到验证,你的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。

要查看之前提交的勘误表,请访问www.packtpub.com/books/content/support,并在搜索字段中输入图书名称。所需信息将在勘误部分显示。

盗版

互联网上版权材料的盗版是所有媒体中持续存在的问题。在 Packt,我们非常重视我们版权和许可证的保护。如果你在互联网上发现任何形式的我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过电子邮件联系 <copyright@packtpub.com> 并附上涉嫌盗版材料的链接。

我们感谢你在保护我们的作者和我们提供有价值内容的能力方面的帮助。

问答

如果你在这本书的任何方面遇到问题,你可以通过电子邮件联系我们的 <questions@packtpub.com>,我们将尽力解决问题。

第一章 创建基础应用程序

我们将构建的 Angular 应用程序是一个列表,你通常在像 Twitter 这样的社交应用程序中看到这些帖子。每个帖子都是由用户发布的,我们可以点击到用户的个人资料以显示该个人资料发布的所有帖子。

由于本书旨在关注技术而非应用程序的功能,我们将故意保持应用程序简单。尽管应用程序很简单,但我们将使用 Angular 开发的所有最佳实践来开发它。

使用 Angular 构建的 Web 应用程序可以针对搜索引擎优化(SEO)。在应用程序中构建 SEO 支持意味着搜索引擎可以读取和理解页面,并且页面具有针对搜索引擎的动态数据(元标签)。这增加了应用程序的可见性,提高了搜索排名和链接数量,从而为您或您的客户带来更多收入。Angular 提供了可以利用的内置功能,以确保应用程序在网络上享有最大的可见性。

课程目标

在本课中,你将:

  • 安装 Angular CLI

  • 创建 Angular 应用程序

  • 创建应用程序的基本 UI

  • 创建应用程序的页眉和页脚

服务器端和客户端渲染

当我们谈论网站的客户端渲染时,我们通常指的是使用在服务器上运行的编程语言的应用程序或网站。在这个服务器上,网页被创建(渲染),渲染的输出(HTML)被发送到浏览器,在那里可以直接显示。

注意

本书代码包托管在 GitHub 上,网址为 github.com/TrainingByPackt/Beginning-Server-Side-Application-Development-with-Angular

当我们谈论客户端渲染时,我们通常指的是使用在浏览器中运行的 JavaScript 来显示(渲染)网页的应用程序或网站。通常有一个页面被下载,包含一个 JavaScript 文件,该文件构建实际的页面(因此称为 单页应用程序)。

安装 Angular CLI

Angular CLI 是创建和开发 Angular 应用程序的官方支持工具。它是一个由 Angular 团队维护的开源项目,并且是开发 Angular 应用程序的推荐方式。

Angular CLI 提供以下功能:

  • 创建新的应用程序

  • 以开发模式运行应用程序

  • 使用 Angular 团队的最佳实践生成代码

  • 运行单元测试和端到端测试

  • 创建生产就绪的构建

使用 Angular CLI 的主要好处之一是您不需要配置任何构建工具。所有这些都被抽象化并通过一个便捷的命令 ng 提供使用。

在整本书中,我们将使用 ng 命令来创建应用程序、生成代码、以开发模式运行应用程序以及创建构建。

注意

有关 Angular CLI 的更多信息,请参阅 GitHub 上的项目页面 (github.com/angular/angular-cli)

要在您的机器上安装 Angular CLI,请执行以下步骤:

  1. 打开您的终端。

  2. 运行以下命令:

    npm install –g @angular/cli@latest
    
  3. 一旦安装完成且没有错误,请确保 ng 命令按预期工作,运行以下命令:

    ng --version
    
  4. 验证输出是否与这里显示的输出相似:安装 Angular CLI

在本节中,我们已经安装了 Angular CLI。我们可以开始构建我们的应用程序!

生成新的应用程序

现在我们已经安装并配置了 Angular CLI,我们可以开始生成我们的新应用程序。

运行 ng new 命令将执行以下操作:

  • 创建一个名为 angular-social 的文件夹

  • 在此文件夹内创建一个新的基本应用程序

  • 添加一个路由模块(因为传递了 --routing 标志)

  • 在此文件夹内运行 npm install 以安装依赖项

  • 运行 git init 以初始化一个新的 Git 仓库

创建新的应用程序

要创建新的应用程序,请执行以下步骤:

  1. 打开您的终端并导航到您想要工作于应用程序的目录:

    cd dev
    
  2. 一旦进入工作区目录,按照以下方式调用 ng 命令:

    ng new angular-social --routing
    
  3. 此命令的输出将类似于以下内容:创建新的应用程序

让我们看看运行此命令后创建的文件夹:

  • src:此文件夹包含我们应用程序的源文件

  • src/app/:此文件夹包含应用程序文件

  • src/assets/:此文件夹包含我们可以在应用程序中使用的静态资源(例如图片)

  • src/environments/:此文件夹包含我们应用程序默认环境的定义

  • e2e:此文件夹包含我们应用程序的端到端测试

运行应用程序

要运行应用程序,请执行以下步骤:

  1. 安装完成后,我们可以打开终端并进入工作目录:

    cd angular-social
    
  2. 运行 ng serve 命令以启动开发服务器:

    ng serve
    

    命令的输出将如下所示:

    运行应用程序

查看您的应用程序

要查看您的应用程序,请执行以下步骤:

  1. 打开您的浏览器并导航到 http://localhost:4200/

  2. 您应该会看到一个默认页面,上面写着 欢迎使用 app查看应用程序

在本节中,我们使用 Angular CLI 创建了一个基本应用程序,并在浏览器中查看了它。

设置 Angular CLI 的默认值

Angular CLI 默认情况下工作得很好,默认设置提供了一个很好的配置来工作。但除了有一些合理的默认设置外,它也非常可配置。

在本书中,我们将利用这个机会来配置 Angular CLI 的默认设置,使其表现得略有不同。

我们将要更改的所有内容都与我们的代码生成(或脚手架)方式有关。

在构建组件时,默认的 Angular CLI 设置将在单独的文件中创建 HTML 模板和样式表。

为了将所有组件内容保持在单个文件中,我们将配置 Angular CLI 以生成内联模板和样式。

将所有组件内容保持在单个文件中的优点是,你可以在一个地方工作于模板、样式和实际的组件代码,而无需切换文件。

配置全局默认值

在你的终端中,运行以下命令以全局配置默认值:

ng set defaults.component.inlineStyle true
ng set defaults.component.inlineTemplate true

当我们运行 git diff 命令时,我们会看到这些设置存储在我们的应用程序的 .angular-cli.json 文件中:

配置全局默认值

在本节中,我们已经配置了 Angular CLI 以生成内联样式和模板。

配置全局样式

默认生成的 Angular 应用程序没有任何样式。

Angular 在样式方面没有规定任何内容,所以在你自己的项目中,你可以使用任何其他的样式框架,例如 Angular Material、Foundation、Semantic UI 或许多其他之一。或者,你可以从头开始创建自己的样式,以获得独特的视觉和感觉。

然而,对于这本书,我们将坚持使用 Bootstrap 4 和 Font Awesome,因为它们被广泛使用,并且它们以最少的代码提供了很好的样式。

在全局样式.css 中链接到样式表

如前所述,我们的应用程序附带一个全局样式表,src/styles.css

在这个样式表中,我们将使用 @import 命令来链接到 Bootstrap 和 Font Awesome。这将指示 Angular 下载文件并将样式应用于应用程序的全局范围。

添加 Bootstrap 和 Font Awesome

  1. 在你的编辑器中打开 src/styles.css 文件。

  2. 在文件的末尾添加以下两行:

    @import url('https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css');
    @import url('https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css');
    
  3. 在浏览器中刷新应用程序。

如你所见,应用程序的字体已更新为无衬线字体,因为这是 Bootstrap 的默认设置:

添加全局样式,链接到 Bootstrap 和 Font Awesome

在页面上显示图标

  1. 打开 src/app.component.html 文件,并将其内容替换为以下内容:

    <h1 class="text-center mt-5">
      <i class="fa fa-thumbs-up"></i>
    </h1>
    

当应用程序在浏览器中刷新时,你应该在页面中心看到点赞图标:

显示全局样式,链接到页面上显示的图标

注意

对于所有可用图标列表,你可以参考 Font Awesome 快速参考 (fontawesome.io/cheatsheet/)。

对于所有可用的 Bootstrap 样式的概述,你可以参考 Bootstrap 4 文档 (getbootstrap.com/docs/4.0/getting-started/introduction/)。

在本节中,我们已经将 Bootstrap 和 Font Awesome 设置为我们的应用程序的样式框架。这将使我们能够拥有与 Angular CLI 提供的不同的字体样式。现在我们可以开始创建我们的 UI 组件了。

创建 UI 模块和组件

与 Angular 一起工作的一个优点是它促进了以模块化和组件化的方式构建应用程序。

在 Angular 中,NgModule(或简称 Module)是将您的应用程序分组为功能逻辑块的一种方式。

Module 是一个带有 @NgModule 装饰器的 TypeScript 类。在装饰器中,我们定义 Angular 如何编译和运行模块内的代码。

在本课中,我们将构建一个模块,将我们想要在用户界面中使用的所有组件组合在一起。

我们将添加一个由 HeaderComponentFooterComponent 组成的 LayoutComponent,并在它们之间定义一个空间,用于显示我们的应用程序代码,使用 RouterOutlet 组件:

创建 UI 模块和组件

创建 UiModule

在本节中,我们将使用 ng 命令生成 UiModule 并在 AppModule 中导入 UiModule

使用 ng generate 命令,我们可以生成或构建出所有可用于 Angular 应用程序的代码。

我们将使用 ng generate module 命令来生成我们的 UiModule

此命令有一个必需的参数,即名称。在我们的应用程序中,我们将此模块称为 ui

  1. 打开您的终端并导航到项目目录。

  2. 从项目目录中运行以下命令:

    $ ng generate module ui
       create src/app/ui/ui.module.ts (186 bytes)
    

如您从命令输出中看到的那样,我们的 UiModule 在新的文件夹 src/app/ui 中生成:

创建 UiModule

当我们查看此文件时,我们可以看到空 Angular 模块的模样:

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

    @NgModule({
      imports: [
        CommonModule
      ],
      declarations: []
    })
    export class UiModule { }

导入我们的 UiModule

现在,我们的 UiModule 已创建,我们需要从 AppModule 中导入它。这样,我们就可以从 AppModule 内部的其他代码中使用 UiModule 内的代码:

  1. 在您的编辑器中打开项目。

  2. 打开 src/app/app.module.ts 文件。

  3. 在文件顶部添加 import 语句:

    import { UiModule } from './ui/ui.module'
    
  4. NgModule 装饰器内的 imports 数组中添加对 UiModule 的引用:

    @NgModule({
       ...
       imports: [
         // other imports
         UiModule
       ],
      ...
     })
    

    导入我们的 UiModule

我们的 UiModule 现已创建并导入到 AppModule 中,这使得它准备好使用。

让我们继续在 UiModule 中创建我们的第一个组件,并使其在我们的应用中显示!

显示当前路由

在构建我们的应用时,我们将大量依赖 Angular 的路由器来将所有模块和组件连接起来。

由于我们将所有功能构建在模块中,因此我们只使用主要的 AppComponent 来显示当前路由。为了使这生效,我们需要更新 AppComponent 模板并确保定义了 router-outlet

  1. 在您的编辑器中打开项目。

  2. 打开 src/app/app.component.html 文件。

  3. 删除所有内容并添加以下 HTML:

    <router-outlet></router-outlet>
    

在浏览器中刷新应用程序后,我们应该看到一个空白页。这是因为我们没有设置任何路由,因此 Angular 应用程序不知道要显示什么。让我们进入下一节创建我们的基本布局!

创建 LayoutComponent

在本节中,我们将使用ng generateUiModule内部创建LayoutComponent,并将其添加到AppRoutingModule中以便显示。

LayoutComponent是应用程序的主要模板。其功能是将HeaderComponentFooterComponent粘合在一起,并在两者之间显示实际的应用程序页面。

现在,我们将使用ng generate命令创建我们的LayoutComponent

  1. 打开您的终端并导航到项目目录。

  2. 在项目目录中运行以下命令:

    ng generate component ui/components/layout
    

当我们查看输出时,我们看到我们的组件已在新src/app/ui/components目录中创建:

创建 LayoutComponent

我们输出的最后一行显示我们的UiModule已被更新。

当我们在编辑器中打开我们的UiModule时,我们看到它为我们的LayoutComponent添加了一个import,并将其添加到NgModule装饰器中的declarations数组中。

declarations数组声明了一个模块中组件的存在,这样 Angular 就知道它们存在并且可以被使用:

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

@NgModule({
  imports: [
    CommonModule
  ],
  declarations: []
})
export class UiModule { }

添加新路由

如本节前面所述,我们将使用我们的LayoutComponent作为整个应用程序的基础。它将显示我们的页眉、页脚,并在两者之间显示实际的应用程序屏幕。

我们将利用 Angular 的内置路由机制来完成这个任务。我们将通过向routing数组中添加一个新的路由,并在该路由的组件中引用LayoutComponent来实现:

  1. 打开src/app/app-routing.module.ts文件。

  2. 在文件顶部添加一个import

    import { LayoutComponent } from './ui/components/layout/layout.component'
    
  3. 在分配给routes属性的空数组中,我们添加一个新的对象字面量。

  4. 添加path属性并将其值设置为空字符串''

  5. 添加component属性并将其值设置为刚刚导入的引用LayoutComponent。我们添加到routes数组中的代码行如下:

    { path: '', component: LayoutComponent, children: [] },
    

为了参考,完整的文件应该看起来像这样:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { LayoutComponent } from './ui/components/layout/layout.component'

const routes: Routes = [
  { path: '', component: LayoutComponent, children: [] },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

当我们的应用程序刷新时,我们应该看到文本布局工作!

添加新路由

构建我们的布局

让我们去掉这个默认文本,开始构建我们的实际布局:

  1. 打开src/app/ui/layout/layout.component.ts文件。

  2. 去掉template属性的內容。

  3. 将以下内容添加到空的template字符串中:

    app-header placeholder
    <div class="container my-5">
       <router-outlet></router-outlet>
    </div>
    app-footer placeholder
    

当我们保存文件时,我们看到浏览器输出一个空白页。

从 Chrome 开发者工具的控制台标签页查看,我们看到有一个错误指出模板解析错误:'router-outlet' 不是一个已知元素

构建我们的布局

为了让 Angular 知道如何渲染router-outlet,我们需要导入RouterModule

  1. 打开src/app/ui/ui.module.ts文件。

  2. 在文件的顶部导入列表中添加一个import语句:

    import { RouterModule } from '@angular/router';
    
  3. NgModule装饰器的imports数组中添加对RouterModule的引用。构建我们的布局

当我们现在保存文件时,我们应该看到页眉和页脚的占位符,中间有一些空白,并且控制台中的路由错误现在已消失:

构建我们的布局

现在已经完成,让我们向占位符添加一些内容!

创建 HeaderComponent

在本节中,我们将使用ng generateUiModule中创建HeaderComponent,从我们的LayoutComponent引用HeaderComponent以便显示,并实现具有动态标题和项目的导航栏。

我们将使用ng generate命令来创建我们的HeaderComponent

  1. 打开终端并导航到项目目录。

  2. 在项目目录内运行以下命令:

    ng g c ui/components/header
    

当我们查看输出时,我们看到我们的组件已在新src/app/ui/header目录中创建,并且我们的UiModule已更新,正如我们预期的那样,在我们为LayoutComponent执行了相同的操作之后:

创建 HeaderComponent

更新 LayoutComponent 以引用我们的新 HeaderComponent

现在,我们将更新LayoutComponent,使其引用我们新的HeaderComponent而不是app-header占位符:

  1. 打开src/app/ui/components/layout/layout.component.ts文件。

  2. 找到app-header占位符并将其替换为以下标签:

    <app-header></app-header>
    

当我们在浏览器中看到我们的应用程序刷新时,我们看到现在我们有了字符串header works!而不是占位符:

更新 HeaderComponent 创建 LayoutComponent 以引用我们的新 HeaderComponent

现在我们可以开始实现实际的页眉,这样我们的页面最终开始看起来像是一个应用了!

创建实际的页眉

现在我们将创建实际的页眉。我们将定义三个类属性,一个字符串属性用于应用程序的标志和标题,以及一个表示我们想要在页眉中显示的链接的对象数组。

在模板中,我们将创建一个 Bootstrap 导航栏,包括一个带有一些样式的nav元素,一个带有我们的标志和标题的链接,以及实际的导航链接。

注意

关于如何使用导航栏的更多信息,请参阅此处:getbootstrap.com/docs/4.0/components/navbar/

  1. angular.io/assets/images/logos/angular/angular.svg下载文件并将其存储为src/assets/logo.svg

  2. 打开src/app/ui/components/header/header.component.ts文件。

  3. component类内部,我们添加三个新属性:

    public logo = 'assets/logo.svg';
    public title = 'Angular Social';
    public items = [{ label: 'Posts', url: '/posts'}];
    
  4. template属性的内容替换为以下标记:

    <nav class="navbar navbar-expand navbar-dark bg-dark">
       <a class="navbar-brand" routerLink="/">
         <img [src]="logo" width="30" height="30" alt="">
    
       </a>
       <div class="collapse navbar-collapse">
         <ul class="navbar-nav">
           <li class="nav-item" *ngFor="let item of items" routerLinkActive="active">
             <a class="nav-link" [routerLink]="item.url">{{item.label}}</a>
           </li>
         </ul>
       </div>
    </nav>
    

当我们保存此文件并在浏览器中检查时,我们终于看到了应用程序的第一个真实部分被显示出来。从现在开始,事情将会迅速发展:

创建实际的页眉

让我们将本节中获得的知识应用到构建 FooterComponent 上。

创建 FooterComponent

在本节中,我们将使用 ng generate 命令在 UiModule 中创建 FooterComponent,从我们的 LayoutComponent 中引用 FooterComponent 以显示它,并实现页脚并添加一条简短的版权信息。

我们将使用 ng generate 命令创建我们的 FooterComponent

  1. 打开终端并导航到项目目录。

  2. 从项目目录中运行以下命令:

    $ ng g c ui/components/footer
    

当我们查看输出时,我们看到我们的组件在新的 src/app/ui/footer 目录中创建,并且 UiModule 被更新,类似于前几节中发生的情况:

创建 FooterComponent

更新 LayoutComponent 以引用新的 FooterComponent

我们将更新 LayoutComponent 以引用我们新的 FooterComponent 而不是 app-footer 占位符:

  1. 打开 src/app/ui/components/layout/layout.component.ts 文件。

  2. 找到 app-footer 占位符并将其替换为以下标签:

    <app-footer></app-footer>
    

就像我们的页眉一样,刷新我们的应用程序后,我们现在看到的是字符串 页脚工作! 而不是占位符:

更新 LayoutComponent 以引用新的 FooterComponent

最后一步是实现页脚,我们的基本布局就完成了!

创建实际的页脚

现在,我们将创建实际的页脚。我们将定义两个类属性,一个字符串属性用于开发者的名字,以及年份。

在模板中,我们将创建另一个 Bootstrap 导航栏,它包含一个带有一些样式和版权信息的 nav 元素,该信息使用了我们在组件类中定义的字符串属性:

  1. 打开 src/app/ui/components/footer/footer.component.ts 文件。

  2. component 类中,添加以下属性。别忘了用正确的数据更新两个占位符:

      public developer = 'YOUR_NAME_PLACEHOLDER';
      public year = 'YEAR_PLACEHOLDER';
    
  3. 删除 template 属性的内容。

  4. template 属性的内容替换为以下标记:

    <nav class="navbar fixed-bottom navbar-expand navbar-dark bg-dark">
       <div class="navbar-text m-auto">
          {{developer}} <i class="fa fa-copyright"></i> {{year}}
       </div>
    </nav>
    

当我们保存此文件并在浏览器中检查时,我们终于看到页脚正在被渲染:

创建实际的页脚

我们已经完成了我们的布局!在本节中,我们构建了页眉和页脚组件。我们还构建了布局组件并创建了一个 UiModule。让我们开始构建实际的应用程序逻辑。

摘要

在本课中,我们安装了 Angular CLI 并创建了 Angular 应用程序。我们设置了一些默认设置,并使用 Bootstrap 和 Font Awesome 配置了我们的全局样式。然后我们创建了应用程序的基本 UI 和布局。最后,我们在应用程序中实现了页眉和页脚。

在下一课中,我们将创建应用程序模块和组件。

第二章 创建应用程序模块和组件

在本课中,我们将首先创建一个包含所有与显示来自我们的 API 的帖子相关的代码的PostsModule

在这个模块中,我们将添加各种组件、一个服务和两个解析器。

组件用于在浏览器中显示数据。在本课中,我们将介绍它们的用法。服务用于从 API 检索数据。最后,我们将向我们的应用程序添加解析器;解析器确保在从一个路由导航到另一个路由时,服务中的数据可用。

课程目标

  • 探索我们应用程序中将要使用的组件类型

  • 创建并加载 PostsModule

  • 创建容器组件,例如 PostsComponent 和 ProfileComponent

  • 添加虚拟帖子和个人资料

  • 创建一个用于检索数据的服务

  • 创建表现性组件,例如 PostListComponent、PostItemComponent 和 ProfileItemComponent

  • 创建并导入解析器

组件类型

在本节中,我们将探讨如何通过区分容器表现性组件来区分我们的组件。有时,它们也被称为智能愚笨组件,这取决于每个组件对组件外部世界的知识程度。

我们可以做出的主要区别如下:

  • 表现性组件负责外观

  • 容器组件负责如何工作

当我们创建它们时,我们将深入探讨这种区分为什么很重要,但我们可以提前透露一些信息。

表现性组件

关于表现性组件,我们可以这样说:

  • 他们使用@Input()装饰器将数据传递进来

  • 任何操作都是使用@Output()装饰器传递上去的

  • 它们处理应用程序的标记和样式

  • 它们主要只包含其他表现性组件

  • 它们对任何路由或服务都没有知识(或依赖)

容器组件

关于容器组件,我们可以这样说:

  • 它们从服务或解析器中检索数据

  • 它们处理从表现性组件接收到的操作

  • 它们具有非常少的标记和样式

  • 它们通常会包含表现性和容器组件

文件夹结构

为了在我们的项目中明确区分,我们将为每种类型的组件使用不同的文件夹:

  • src/<module>/components文件夹是表现性组件所在的位置

  • src/<module>/containers文件夹是容器组件所在的位置

生成并懒加载 PostsModule

我们将使用ng命令生成PostsModule,并在AppRoutingModule中懒加载PostsModule

使用ng generate命令,我们可以生成或构建出所有可用于我们的 Angular 应用程序的代码。

我们将使用ng generate module命令生成我们的PostsModule

此命令有一个必需的参数,即名称。在我们的应用程序中,我们将此模块称为posts。还有一个可选参数被传递,以便为该模块创建一个单独的文件来保存路由,即PostsRoutingModule

  1. 打开您的终端并导航到项目目录。

  2. 在项目目录内运行以下命令:

    ng g m posts --routing
    

如您从命令输出中看到的那样,我们的PostsModule是在新的文件夹src/app/posts中生成的:

生成和懒加载 PostsModule

与我们通过将UiModule导入到AppModule中来加载UiModule的方式不同,我们将使用AppRoutingModule来懒加载PostsModule

这是对我们应用程序构建方式的优化,并确保我们的应用程序有一个更小的初始文件要下载,这是通过使用名为代码拆分的技术来实现的。这基本上是将每个懒加载的模块捆绑到其自己的文件中,浏览器被指示在需要时下载此文件,但不是在需要之前。

我们将在主应用程序文件中添加两个路由。第一个路由具有空白的path属性(我们的默认路由),其功能是重定向到/posts路由。

第二个路由是/posts路由,它懒加载PostsModule

如果用户导航到应用程序,第一个找到的路由是我们的空白重定向路由。这将告诉路由器导航到/posts。路由器找到/posts路由并将用户导航到该模块:

  1. 在您的编辑器中打开项目。

  2. 打开src/app/app-routing.module.ts文件。

  3. 定位到在routes属性中定义的唯一现有路由对象。

  4. 在这个新的children数组中,我们创建了两个看起来像这样的路由:

    { path: '', redirectTo: '/posts', pathMatch: 'full'},
    { path: 'posts', loadChildren: './posts/posts.module#PostsModule' },
    
    const routes: Routes = [
      { path: '', component: LayoutComponent, children: [
        { path: '', redirectTo: '/posts', pathMatch: 'full'},
        { path: 'posts', loadChildren: './posts/posts.module#PostsModule' },
      ] },
    ];
    

这里有一些解释这个工作原理的事情:

  • 首先,我们定义我们想要在我们的主路由下有子路由。这确保了所有子路由都在创建布局组件部分中定义的<router-outlet>中渲染。

  • 我们定义了第一个路由以响应所有路径(这就是空字符串的作用),并告诉它重定向到/posts路由。

  • 最后,我们创建一个posts路由,并告诉它从我们新的模块中加载其子路由。loadChildren属性是启用懒加载的。

当我们在浏览器中刷新页面时,我们可以看到应用程序本身没有任何变化,但我们可以看到我们的 URL 已经改变;它已经重定向到/posts

让我们继续到下一个部分来创建我们的容器组件,这样我们就可以开始看到数据了!

创建容器组件

在本节中,我们将使用ng generate来在PostsModule内部创建PostsComponentProfileComponent,为这两个组件添加路由,并添加我们可以用来构建表示组件的虚拟数据。

创建 PostsComponent 和 ProfileComponent

我们将使用 ng generate 命令创建我们的 PostsComponent。这是最终列出所有帖子概览的组件。

此组件的应用程序路由将是 /posts:

  1. 打开您的终端并导航到项目目录。

  2. 从项目目录中运行以下命令:

    ng g c posts/containers/posts
    

    创建 PostsComponent 和 ProfileComponent

  3. 打开 src/app/posts/posts-routing.module.ts 文件。

  4. 导入 PostsComponent

    import { PostsComponent } from './containers/posts/posts.component'
    
  5. 将以下路由添加到 routes 数组:

    { path: '', component: PostsComponent },
    

现在我们刷新应用程序中的页面,我们应该在页眉和页脚之间看到文本 posts works!

创建 PostsComponent 和 ProfileComponent

与我们创建 PostsComponent 的方式非常相似,我们现在将创建 ProfileComponent。这是负责显示发帖个人资料的组件。

此组件的应用程序路由将是 /posts/<id>,其中 <id> 是我们想要显示的个人资料标识符:

  1. 打开您的终端并导航到项目目录。

  2. 从项目目录中运行以下命令:

    ng g c posts/containers/profile
    

    创建 PostsComponent 和 ProfileComponent

  3. 打开 src/app/posts/posts-routing.module.ts 文件。

  4. 导入 ProfileComponent

    import { ProfileComponent } from './containers/profile/profile.component'
    
  5. 将以下路由添加到 routes 数组:

    { path: ':profileId', component: ProfileComponent },
    

当我们的应用程序刷新并且我们将浏览器导航到 http://localhost:4200/posts/1 时,我们应该看到文本 profile works!

创建 PostsComponent 和 ProfileComponent

添加虚拟帖子和个人资料数据

为了了解我们的应用程序是如何工作的,我们将在我们的组件中添加一些虚拟数据,以便我们有东西可以工作。我们将添加一个服务来从我们的 API 获取数据:

  1. 打开 src/app/posts/containers/posts/posts.component.ts 文件。

  2. 创建一个名为 posts 的新属性,并使用以下结构定义它:

    public posts = []
    
  3. 将以下导入添加到文件顶部:

    import { ActivatedRoute } from '@angular/router';
    
  4. 我们 posts 对象中的 items 数组将最终包含我们的帖子。我们将使用一个简单的循环添加一些虚拟项。

  5. 定位到 ngOnInit() 方法,并将以下代码添加到方法体中。此代码在 posts 数组中创建 10 个虚拟元素,以便我们可以显示一些数据。这些细节并不重要,因为当我们在下一课从 API 获取数据时,此块将变得过时:

    for(let i = 1; i < 10; i++) {
       this.posts.push({ id: i, text: 'This is post with id: ' + i })
     }
    
  6. 对于最后一步,我们需要更新我们的模板。从 template 属性中删除所有内容,并用以下标记替换它:

    <div *ngFor="let post of posts">
       <a [routerLink]="post.id">
         {{post.text}}
       </a>
     </div>
    

我们模板中的代码使用 ngFor 指令遍历 posts 数组的内容。对于这些项目中的每一个,它打印一个 a 标签,通过 routerLink 指令链接到帖子 id。链接文本设置为 post.text

这是我们浏览器中看到的内容:

添加虚拟帖子和个人资料数据

最后,我们将向我们的 ProfileComponent 添加一些虚拟数据:

  1. 打开 src/app/posts/containers/profile/profile.component.ts 文件。

  2. 创建一个名为profile的新属性,并使用以下结构定义它:

    public profile = { id: null };
    
  3. 在文件顶部添加以下import

    import { ActivatedRoute } from '@angular/router';
    
  4. 定位到constructor()方法,并将以下代码添加到方法参数中:

    constructor(private route:ActivatedRoute){}
    
  5. 定位到ngOnInit()方法,并将以下代码添加到方法体中。此代码订阅了当前路由的参数,当它改变时,它读取 URL 中profileId属性的值,并将其分配给this.profileid属性:

    this.route.params.subscribe(res => this.profile.id = 'profileId = ' + res['profileId'])
    
  6. 对于最后一步,我们需要更新我们的模板。从template属性中删除所有内容,并用以下标记替换:

    <p>
      {{profile.id}}
    </p>
    

当我们访问http://localhost:4200/posts/5时,我们将看到以下内容:

添加模拟帖子和个人资料数据

ngOnInit块中的代码订阅激活路由的参数。当我们通过订阅检索这些参数时,我们使用它将 ID 附加到post.text属性中的字符串,使内容动态化。

现在,我们可以从PostsContainer点击到ProfileContainer组件。我们可以使用后退按钮或页眉中的链接返回。

我们的容器组件已设置好,路由也正常工作。让我们添加一些来自 API 的真实数据!

创建一个用于检索数据的服务

在本节中,我们将使用ng generate创建PostsService,使用环境存储 API URL,并在我们的组件中使用PostsService。然后,我们在PostsService中定义我们的 API 调用,并利用HttpClientModule启用 HTTP 访问。

生成服务

我们将使用ng generate service命令生成一个服务,该服务将处理与我们的 API 的交互:

  1. 打开您的终端并导航到项目目录。

  2. 从项目目录中运行以下命令:

    ng g s posts/services/posts --module posts/posts
    

    生成服务

存储我们的 API URL

我们将使用 Angular CLI 的环境来存储我们的 API URL。使用环境,我们可以为开发和生产环境定义不同的 URL。

默认情况下,使用 Angular CLI 生成的应用程序带有两个预定义的环境。这些环境在项目根目录的.angular-cli.json中定义。

  1. 打开src/environments/environment.ts文件。

  2. 环境变量中,添加一个apiUrl键,并将其值设置为字符串http://localhost:3000/api,这是开发 API 的 URL:存储我们的 API URL

  3. 打开src/environments/environment.prod.ts文件。

  4. 环境变量中,添加一个apiUrl键,并将其值设置为字符串https://packt-angular-social.now.sh/api,这是生产 API 的 URL:存储我们的 API URL

在我们的容器组件中引用我们的新 PostsService

在此服务中,我们将在容器组件中引用我们的新PostsService,从而定义我们的服务应该是什么样子。

我们将使用 Angular 提供的 OnInit 组件生命周期钩子来调用我们的注入服务,并在该服务上调用 getPosts 方法。

我们将 映射 结果以仅获取项目,然后我们将订阅并将方法的结果设置为我们的 posts 属性。

注意我们对 PostsComponentProfileComponent 都做了同样的事情:

  1. 打开 src/app/posts/containers/posts/posts.component.ts 文件。

  2. 为我们的新 PostsService 添加一个 import 语句:

    import { PostsService } from '../../services/posts.service';
    
  3. 更新构造函数以 注入 PostsService 并使其在 private postsService 变量下可用:

    constructor(private postsService: PostsService) { }
    
  4. 删除 ngOnInit 方法的内容并更新如下:

      ngOnInit() {
         this.postsService.getPosts()
           .map(res => res['items'])
           .subscribe((result: any) => this.posts = result)
       }
    
  5. 打开 src/app/posts/containers/posts/profile.component.ts 文件。

  6. 为我们的新 PostsService 添加一个 import 语句:

    import { PostsService } from '../services/posts.service';
    
  7. 更新构造函数以 注入 PostsService 并确保留下我们的 private route 依赖项:

    constructor(private route: ActivatedRoute, private postsService: PostsService) { }
    
  8. 删除 ngOnInit 方法的内容并更新如下:

    this.postsService.getProfile(this.route.snapshot.params['profileId'])
      .subscribe((result: any) => this.profile = result)
    

定义公共方法

下一步是在我们的 PostsService 中定义我们的公共方法,并确保这些方法从我们的 API 获取所需的数据。

我们将在 PostsService 中添加两个方法。第一个方法是 getPosts 方法,它不接受任何参数并从 API 返回所有帖子。第二个方法是 getProfile 方法,它接受 profileId 作为参数。它返回与传入的 profileId 相关的配置文件,并包括该配置文件创建的所有相关帖子:

  1. 打开 src/app/posts/services/posts.service.ts 文件。

  2. 添加一个 import 语句以从 @angular/common/http 导入 HttpClient 以及一个指向我们定义 API URL 的 environment 的引用:

    import { HttpClient } from '@angular/common/http';
    import { environment } from '../../../environments/environment'
    
  3. 更新构造函数以注入 HttpClient 并使其在 private http 变量下可用:

    constructor(private http: HttpClient) { }
    
  4. 创建一个名为 getPosts() {} 的新方法并添加以下内容:

    getPosts() {
       const url = `${environment.apiUrl}/posts/timeline?filter[where][type]=text`
       return this.http.get(url)
    }
    
  5. 创建一个名为 getProfile(profileId) { } 的新方法,并添加以下内容:

    getProfile(profileId) {
       const url = `${environment.apiUrl}/profiles/${profileId}?filter[include]=posts`
       return this.http.get(url)
     }
    

在我们的 AppModule 中导入 HttpClientModule

我们几乎完成了 PostsService 的创建,但还有一件事需要修复。当我们刷新浏览器中的应用程序时,我们看到在 控制台 选项卡中有一个错误消息:

在我们的 AppModule 中导入 HttpClientModule

我们得到这个错误的原因是因为我们在服务中使用了 HttpClient,但 Angular 不知道这个模块来自哪里。为了解决这个问题,我们需要在 AppModule: 中导入 HttpClientModule

  1. 打开 src/app/app.module.ts 文件。

  2. 添加一个 import 语句以从 @angular/common/http 导入 HttpClientModule

    import { HttpClientModule } from '@angular/common/http';
    
  3. 更新 NgModule 装饰器中的 imports 数组以导入 HttpClientModule

    @NgModule({
       ...
       imports: [
         ...
         HttpClientModule,
         ...
       ],
       ...
     })
    

    在我们的 AppModule 中导入 HttpClientModule

当我们现在检查 控制台 选项卡时,我们看到还有一个错误消息,.map 不是一个函数

在 AppModule 中导入 HttpClientModule

为了解决这个问题,我们需要从 rxjs 库中导入 map 操作符。

rxjs 库是 Angular 的主要依赖之一,用于在 Angular 中实现 Observable 模式。它被 Angular 本身用于路由和 HTTP 客户端。

map 操作符是 rxjs 提供的操作符之一,可以在订阅之前对数据进行映射。如果你想要操作你正在处理的数据,这很有用:

  1. 打开 src/app/app.module.ts 文件。

  2. 添加一个 import 语句来导入 rxjs 库中的 map 操作符:

    import 'rxjs/add/operator/map';
    

当我们刷新应用程序时,我们应该看到从 API 获取的帖子列表!

让我们继续添加一些表现性组件,为我们的帖子添加一些样式!

创建表现性组件

在本节中,我们将使用 ng generate componentPostsModule 内创建 PostListComponentPostItemComponent。然后我们将为这些组件添加 UI 并在容器组件中使用这些组件。

PostListComponent 负责通过其 Input 接收一个帖子数组,并遍历这些帖子并调用 PostItemComponent

PostItemComponent 接受单个帖子作为其 Input 并显示该帖子。

创建 PostListComponent

我们将使用 ng generate 命令来创建我们的 PostListComponent。这是将遍历我们的帖子并从我们的 PostsComponent 调用的组件:

  1. 打开 src/app/posts/container/posts/posts.component.ts 文件。

  2. 将模板更新为以下内容:

    <app-post-list [posts]="posts"></app-post-list>
    
  3. 打开终端并导航到项目目录。

  4. 从项目目录中运行以下命令:

    ng g c posts/components/post-list
    

    创建 PostListComponent

  5. 打开 src/app/posts/components/post-list/post-list.component.ts 文件。

  6. 通过将其添加到现有的 import 语句中,从 @angular/core 导入 Input

  7. 在组件类中添加以下属性:

    @Input() posts: any[]
    
  8. 将模板更新为以下内容:

    <div *ngFor="let post of posts" class="mb-3">
       <app-post-item [post]="post"></app-post-item>
     </div>
    

创建 PostItemComponent

我们将使用 ng generate 命令来创建我们的 PostItemComponent

  1. 打开终端并导航到项目目录。

  2. 从项目目录中运行以下命令:

    ng g c posts/components/post-item
    

    创建 PostItemComponent

  3. 打开 src/app/posts/components/post-item/post-item.component.ts 文件。

  4. 通过将其添加到现有的 import 语句中,从 @angular/core 导入 Input

  5. 在组件类中添加以下属性:

    @Input() post: any;
    
  6. 将模板更新为以下内容:

    <!-- The row and the col make sure the content is always centered -->
     <div class="row">
       <div class="col-md-8 offset-md-2">
         <!-- The card is where the message is shown -->
         <div class="card">
           <div class="card-body">
             <!-- We use the Bootstrap 'media' component to show an avatar with content -->
             <div class="media">
               <img class="avatar mr-3 rounded" [attr.src]="post?.profile?.avatar">
               <div class="media-body">
                 <!-- The full name of the author is used to navigate to the post detail -->
                 <h5>
                   <a [routerLink]="post?.profile?.id"> {{post?.profile?.fullName}} </a>
                   <span class="date float-right text-muted">
    
                   </span>
                 </h5>
                 <!-- The text of the post is shown in a simple paragraph tag-->
                 <p>{{post?.text}}</p>
               </div>
             </div> <!-- End media -->
           </div>
         </div> <!-- End card -->
       </div>
     </div> <!-- End row-->
    
  7. 将组件的 styles 属性更新为以下内容:

    styles: [`
       img.avatar {
         height: 60px;
         width: 60px;
       }
       span.date {
         font-size: small;
       }
     `],
    

如果我们现在刷新浏览器,页面将看起来是这样的:

创建 PostItemComponent

创建 ProfileItemComponent

我们将使用 ng generate 命令来创建我们的 ProfileItemComponent

  1. 打开 src/app/posts/container/posts/profile.component.ts 文件。

  2. 将模板更新为以下内容:

    <app-profile-item [profile]="profile"></app-profile-item>
    
  3. 打开您的终端并导航到项目目录。

  4. 从项目目录中运行以下命令:

    ng g c posts/components/profile-item
      create src/app/posts/components/profile-item/profile-item.
    component.spec.ts (664 bytes)
      create src/app/posts/components/profile-item/profile-item.component.ts (273 bytes)
      update src/app/posts/posts.module.ts (846 bytes)
    
  5. 打开src/app/posts/components/profile-item/profile-item.component.ts文件。

  6. 通过将Input@angular/core导入到现有的import语句中。

  7. 在组件类中添加以下属性:

    @Input() profile: any;
    
  8. 将模板更新为以下内容:

    <div class="row">
     <div class="col-md-8 offset-md-2">
       <div class="card mb-3" *ngFor="let post of profile.posts">
         <div class="card-body">
           <div class="media">
             <img class="avatar mr-3 rounded" [attr.src]="profile?.avatar">
             <div class="media-body">
               <h5>
                 <a [routerLink]="profile?.id"> {{profile?.fullName}} </a>
                 <span class="date float-right text-muted">
    
                 </span>
               </h5>
               <p>{{post?.text}}</p>
             </div>
           </div>
         </div>
       </div>
     </div>
    
  9. 将我们组件中的styles属性更新为以下内容:

    styles: [`
      img.avatar {
       height: 60px;
        width: 60px;
      }
      span.date {
        font-size: small;
      }
    `],
    

当我们现在在浏览器中刷新应用程序时,我们看到内容被样式化,并且导航仍然按预期工作:

创建 ProfileItemComponent

我们已经成功地将检索数据和显示数据的关注点分离了。

通常来说,保持组件尽可能小和简单是一个好主意;特别是,我们的PostItemComponent可能可以被拆分成多个组件。

对于我们的目的,这完全适用,我们可以继续本课的最后一个步骤,即使用 Angular 路由器正确处理数据的检索。

使用路由器创建用于检索数据的解析器

在本节中,我们将手动创建两个作为解析器的可注入类,然后配置我们的路由器以使用这些解析器。然后我们将更新我们的容器组件以使用这些解析后的数据。

解析器是一个类,我们可以在组件显示之前使用它来获取我们在组件中使用的数据。我们在需要数据的路由中调用解析器。在我们的实现中,解析器从 API 检索数据并返回它,以便可以在组件中显示。

注意

更多关于解析器的信息可以在:angular.io/guide/router#resolve-pre-fetching-component-data找到。

我们的应用程序结构已经很整洁了,但有一件事我们可以优化。

为了看到我们的问题是什么,打开 Chrome 开发者工具并转到性能标签页:

使用路由器创建用于检索数据的解析器

点击齿轮图标,将网络设置为慢速 3G

使用路由器创建用于检索数据的解析器

如果我们现在在我们的应用程序中点击,我们会看到我们的页面导航仍然正常工作,但我们看到的是空页面。

原因是,虽然我们的组件加载正确,但它们在加载后仍然需要检索数据。这是因为我们的组件从ngOnInit方法中调用PostsService

如果我们的路由器能确保在进入页面之前组件已经加载了所有需要的数据,那就更好了。

幸运的是,Angular 路由器提供了一种使用解析器来处理这种情况的方法。它们会在进入路由之前解析数据,在我们的组件中,我们只需取这些解析后的数据并显示它。

我们创建的解析器需要@Injectable()装饰器来确保它们是 Angular 依赖注入的一部分。

创建解析器

现在,我们将创建一个解析器来解析我们的帖子:

  1. 打开终端并运行以下命令:

    ng g class posts/resolvers/posts-resolver
    

    创建解析器

  2. 打开src/app/posts/resolvers/posts-resolver.ts文件。

  3. 在文件开始处定义所需的导入:

    import { Injectable } from '@angular/core'
    import { Resolve } from '@angular/router'
    import { PostsService } from '../services/posts.service'
    
  4. 使用@Injectable操作符装饰PostsResolver类:

    @Injectable()
    export class PostsResolver {}
    
  5. 使类实现Resolve<any>

    @Injectable()
    export class PostsResolver implements Resolve<any> {
    }
    
  6. 在类内部,创建一个构造函数并注入我们的PostsService

    constructor(private postsService: PostsService) {}
    
  7. 在构造函数下方创建一个名为resolve的类方法,并使其返回PostsServicegetPosts()方法:

    resolve() {
      return this.postsService.getPosts()
    }
    

这是将用于检索所有帖子的解析器,就像我们目前在PostsComponent中这样做一样。

现在,我们将创建一个解析器来解析我们的个人资料:

  1. 打开终端并运行以下命令:

    ng g class posts/resolvers/profile-resolver
    

    创建解析器

  2. 打开src/app/posts/resolvers/profile-resolver.ts文件。

  3. 在文件开始处定义所需的导入:

    import { Injectable } from '@angular/core'
    import { ActivatedRouteSnapshot, Resolve } from '@angular/router'
    import { PostsService } from '../services/posts.service'
    
  4. 使用@Injectable操作符装饰ProfileResolver类:

    @Injectable()
    export class ProfileResolver {}
    
  5. 使类实现Resolve<any>

    @Injectable()
    export class ProfileResolver implements Resolve<any> {
    }
    
  6. 在类内部,创建一个构造函数并注入我们的PostsService

    constructor(private postsService: PostsService) {}
    
  7. 在构造函数下方创建一个名为resolve的类方法,并将route: ActivatedRouteSnapshot类传递给它:

    resolve(route: ActivatedRouteSnapshot) {
    }
    
  8. resolve方法内部,我们返回PostsServicegetProfile()方法,同时从我们的路由中获取params['profileid']

    resolve(route: ActivatedRouteSnapshot) {
      return this.postsService.getProfile(route.params['profileId'])
    }
    

这是将用于检索我们在路由中导航到的帖子的解析器。

导入我们的解析器

我们将把两个新的解析器添加到PostsRoutingModule中。我们通过导入解析器,然后为我们的两个路由添加一个resolve属性来完成此操作。resolve属性接受一个对象,其中键是解析后数据在路由器中的可用方式,值是导入的解析器的引用:

  1. 打开src/app/posts/posts-routing.module.ts文件。

  2. 导入我们刚刚创建的两个解析器:

    import { PostsResolver } from './resolvers/posts-resolver'
    import { ProfileResolver } from './resolvers/profile-resolver'
    
  3. 更新我们的两个路由以添加resolve属性并调用解析器:

    { path: '', component: PostsComponent, resolve: { posts: PostsResolver } },
    { path: ':profileId', component: ProfileComponent, resolve: { profile: ProfileResolver } },
    

当我们现在刷新页面时,我们看到没有输出显示,并且在浏览器控制台中有一个错误:

导入我们的解析器

为了修复这个错误,我们需要在我们的模块中provide解析器,就像我们处理其他注入器(如服务)一样:

  1. 打开src/app/posts/posts.module.ts文件。

  2. 导入两个解析器:

    import { PostsResolver } from './resolvers/posts-resolver'
    import { ProfileResolver } from './resolvers/profile-resolver'
    
  3. 将两个解析器添加到providers数组中:

    providers: [PostsService, PostsResolver, ProfileResolver]
    

当应用程序刷新时,我们看到控制台中的错误消失了。

如果我们在 Chrome 开发者工具的网络标签页中检查,我们会看到我们对同一个端点进行了两次请求。这是因为我们在解析器和组件中两次检索数据。让我们更新我们的容器组件,并让它们使用由路由器解析的数据。

使用路由器解析的数据

我们将更新 PostsComponent 以读取由我们的路由器解析的数据。我们订阅了活动路由的数据,并对该数据进行了两次映射。在第一次映射命令中,posts 值与我们在解析器对象中用于此路由的对象键相关:

  1. 打开 src/app/posts/container/posts/posts.component.ts 文件。

  2. @angular/router 中导入 ActivatedRoute:

    import { ActivatedRoute } from '@angular/router'
    
  3. 由于我们不再在此处使用它,请移除 PostsService 的导入。

  4. 更新构造函数以注入 private route: ActivatedRoute:

    constructor(private route: ActivatedRoute) { }
    
  5. 更新 ngOnInit() 方法并替换内容如下:

    ngOnInit() {
      this.route.data
        .map(data => data['posts'])
        .map(data => data['items'])
        .subscribe((result: any) => this.posts = result)
    }
    
  6. 由于我们到达路由时始终有数据可用,我们可以从 posts 属性中移除赋值,使其看起来如下:

    public posts: any
    

刷新页面并确保数据仍然被加载。

现在,我们将更新 ProfileComponent 以读取由我们的路由器解析的数据。我们订阅了活动路由的数据,并对该数据进行了映射。在我们的映射命令中,profile 值与我们在解析器对象中用于此路由的对象键相关:

  1. 打开 src/app/posts/container/profile/profile.component.ts 文件。

  2. 由于我们不再在此处使用它,请移除 PostsService 的导入。

  3. 更新构造函数以仅注入 private route: ActivatedRoute:

    constructor(private route: ActivatedRoute) { }
    
  4. 更新 ngOnInit() 方法并替换内容如下:

    this.route.data
      .map(data => data['profile'])
      .subscribe((result: any) => this.profile = result)
    
  5. public profile 属性中移除赋值,使其看起来如下:

    public profile: any;
    

完成了!我们的基本应用已经构建完成,尽管还有很多东西可以添加和优化,但它结构良好,并且使用了 Angular 的最佳实践。

在本节中,我们创建了容器组件并添加了虚拟数据。我们还创建了展示组件并为我们的应用实现了解析器。在下一课中,我们将通过添加 Angular Universal 来添加对服务器端渲染的支持。

摘要

在本课中,我们学习了不同类型的组件以及如何创建它们。然后我们学习了如何创建解析器以使用路由检索数据。

在下一课中,我们将探讨如何在我们的应用中实现服务器端渲染。

第三章 服务器端渲染

正常应用是如何渲染的?

让我们先看看一个没有服务器端渲染的正常 Angular 应用程序是如何表现的。

当我们在开发模式下启动服务器,使用 ng serve,并在浏览器中使用查看源代码选项来检查源代码时,我们会看到只有来自我们的 src/index.html 文件的输出被渲染,底部附加了一些脚本。

这些脚本将由浏览器下载,下载并执行后,应用程序将显示:

服务器端渲染

虽然这在某些情况下是可行的,但在其他情况下可能会出现问题。如果您的应用用户处于慢速连接或慢速设备上,加载和解析脚本将需要时间,而在等待期间,用户将看到一个空白页面。

另一个问题是最多的搜索引擎和社交媒体网站只会读取我们网站的初始负载,而不会下载和执行我们的客户端 JavaScript 文件。

这些是我们将在本节课中解决的问题。在我们添加了服务器端渲染之后,我们将添加对动态元数据和页面标题的支持。这确保了任何服务器端渲染的页面都有适当的元数据,这将使这些社交页面内容丰富,并确保搜索引擎可以正确索引页面。

要获取实际的加载时间,请使用 Chrome 开发者工具中网络标签页的状态栏。

注意

在正常 Chrome 浏览器上体验慢速连接的一种方法是在 Chrome 开发者工具中打开,转到网络标签页,并将网络速度从在线改为慢速 3G。当您加载由服务器提供的页面时,您将了解慢速连接加载应用程序需要多长时间:

服务器端渲染

课程目标

在本节课中,您将:

  • 将服务器端渲染添加到我们在上一节课中构建的应用程序中

  • 将 Angular Universal 添加到我们的应用程序中,并在 Angular CLI 配置中配置第二个应用

  • 实现一个网络服务器来托管我们的应用

  • 将动态元数据添加到我们的应用程序中

生成服务器应用

自从 Angular CLI 版本 1.6 以来,就有一个生成器用于添加对 Angular Universal 的支持。它是通过在 Angular CLI 配置文件.angular-cli.json中添加第二个应用来实现的。

我们将把这个新应用称为我们的服务器应用,而我们在上一节课中使用的应用将被称为我们的浏览器应用

那么,浏览器应用和服务器应用之间有什么区别呢?

  • 它们都会加载另一个平台,该平台的行为不同。

  • 浏览器应用使用代码拆分,将应用构建成各种较小的文件。这提高了浏览器中的加载时间。服务器构建应用时不进行代码拆分,因为在服务器上这样做没有好处。

  • 浏览器应用加载了更多的 polyfills。这些是小型的 JavaScript 库,如果浏览器尚未支持它们,则可以添加功能。对于服务器来说,这不需要。

让我们更详细地探索当我们运行生成器时会发生什么:

生成服务器应用

运行此生成器将在当前应用中更改一些内容:

  • 它将在.angular-cli.json中的apps数组中添加第二个应用。

  • 它将为@angular/platform-server包添加一个依赖项。

  • 它将更新AppModule并更改BrowserModule的导入。

  • 它将更改浏览器应用在src/main.ts中的引导方式。

此外,它还创建了一些新文件:

  • 它生成一个新文件src/app/app.server.module.ts,其中包含AppServerModule

  • 创建了src/main.server.ts文件,该文件导出AppServerModule

  • src/tsconfig.server.json中生成了一个服务器应用的 TypeScript 配置文件。

如上所述,我们在package.json中有新的依赖项。这意味着我们需要运行npm install以确保依赖项被安装。

生成 Angular 通用应用

我们将创建服务器应用并安装缺失的依赖项:

  1. 在项目目录中打开终端。

  2. 运行生成器以添加通用应用。命令如下:

    ng generate universal server
    
  3. 安装添加到package.json中的依赖项:

    npm install
    

使我们的应用保持一致

我们将对浏览器应用和服务器应用进行一些小的修改,以便它们更加一致:

  1. 在项目目录中打开终端。

  2. 运行以下命令以更新服务器应用的outDir

    ng set apps.1.outDir=dist/server
    
  3. 运行以下命令以更新浏览器应用的outDir

    ng set apps.0.outDir=dist/browser
    
  4. 运行以下命令以更新浏览器应用的name

    ng set apps.0.name=browser
    
  5. 运行以下命令以更新浏览器应用的platform

    ng set apps.0.platform=browser
    

    使我们的应用保持一致

这些更改将在.angular-cli.json中反映出来:

使我们的应用保持一致

我们现在已经安装了所需的依赖项。

在本节中,我们在已有的浏览器应用之外创建了一个新的服务器应用。让我们继续在我们的应用中添加对 Angular Universal 的支持。

为服务器应用添加依赖项

为了确保我们的服务器应用能够正确运行,我们需要确保加载 Angular 的两个依赖项:zone.jsreflect-metadata

我们的浏览器应用使用polyfills.ts加载这些依赖项,而对于服务器应用,我们将它们添加到src/main.server.ts中。

我们还需要添加另一个依赖项,即ModuleMapLoaderModule。这是一个第三方模块,它需要与 Angular Universal 应用配合使用懒加载功能。

我们将在src/main.server.ts中导入两个依赖项,以便在AppServerModule加载时导入。

此外,我们将启用生产模式,就像在src/main.ts中对浏览器应用所做的那样:

  1. 打开新创建的文件src/main.server.ts

  2. 在文件顶部添加导入语句:

    import 'zone.js/dist/zone-node';
    import 'reflect-metadata';
    
    import { enableProdMode } from '@angular/core';
    import { environment } from './environments/environment';
    
  3. 根据环境条件启用生产模式:

    if (environment.production) {
      enableProdMode();
    }
    

让我们继续将这个新应用程序添加到我们的 Angular CLI 配置中!

将服务器应用程序添加到我们的 Angular CLI 配置中

  1. 在项目目录内打开一个终端。

  2. 运行以下命令来安装依赖项:

    npm install --save @nguniversal/module-map-ngfactory-loader
    
  3. 在您的编辑器中打开 src/app/app.server.module.ts 文件。

  4. 在文件顶部添加以下 import

    import { ModuleMapLoaderModule } from '@nguniversal/module-map-ngfactory-loader';
    
  5. 将对导入模块的引用添加到 imports 数组中:

    imports: [
      ...
      ModuleMapLoaderModule,
    ],
    

在本节中,我们添加了所需的依赖项并将服务器应用程序添加到我们的配置中。在下一节中,让我们探索运行脚本并将它们添加到我们的应用程序中。

将运行脚本添加到 package.json

现在我们已经将第二个应用程序添加到我们的 Angular CLI 配置中,我们需要确保我们可以轻松地构建这两个应用程序,而无需记住确切的命令。

为了做到这一点,我们将利用所谓的 npm 脚本。这些脚本用于定义可以在我们的应用程序上执行的操作。这些操作的例子包括构建应用程序、运行测试以及将应用程序部署到预发布或生产环境。

我们可以在项目根目录下的 package.json 文件的 scripts 部分中定义我们的 npm 脚本,以便我们可以轻松地为我们的应用程序创建构建。

在这里,我们将添加三个名为 buildbuild:browserbuild:server 的脚本,其中第一个脚本将调用其他两个。

这使我们能够同时运行这两个命令,或者如果我们愿意,可以独立运行它们。

为了了解这些脚本的工作原理,请考虑以下流程:

  1. npm run build 命令将首先运行 npm run build:browser

  2. 当该命令完成后,它将运行 npm run build:server

运行 build:browserbuild:server 脚本的顺序无关紧要;这两个脚本完全独立。

添加 npm 脚本

我们将向我们的 package.json 添加一些 npm 脚本,以便我们可以轻松地为我们的应用程序创建构建。

  1. 在编辑器中打开项目根目录下的 package.json 文件。

  2. 定位到 scripts 对象并删除现有的 build 属性。

  3. 将以下键添加到 scripts 对象中:

    "build": "npm run build:browser && npm run build:server",
    "build:browser": "ng build --prod --app browser",
    "build:server": "ng build --prod --app server --output-hashing=false",
    

测试两个应用程序的构建结果

我们将测试浏览器和服务器应用程序的构建结果:

  1. 在项目目录内打开一个终端。

  2. 运行以下命令来构建浏览器应用程序:

    $ npm run build:browser
    

    测试两个应用程序的构建结果

  3. 运行以下命令来构建服务器应用程序:

    $ npm run build:server
    

    测试两个应用程序的构建结果

如果两个命令都执行而没有错误消息,我们可以继续进行下一步,即实现一个小型网络服务器来托管我们的应用程序。

实现网络服务器

现在我们两个应用程序都可以构建,我们可以继续创建一个简单的服务器来托管我们的应用程序。

为了做到这一点,我们将创建一个基于 Express.js 的简单 Node.js 服务器。

我们将在一个名为 server.ts 的 TypeScript 文件中定义我们的服务器,并使用我们将要安装的 ts-node 二进制文件运行此文件。

注意

Angular Universal 的当前实现依赖于 Node.js,因为它是用 JavaScript 实现的。

可以使用其他服务器运行 Angular Universal 应用程序,例如 ASP.NET,尽管在底层,ASP.NET 服务器将调用 Node.js 进程来处理 Angular Universal 部分。

可以在这里找到一个运行 Angular Universal 的示例仓库:github.com/MarkPieszak/aspnetcore-angular2-universal

安装服务器依赖项

我们将安装我们将用于执行服务器文件的 ts-node 二进制文件。此外,我们还将安装 Express.js 将用于加载我们的 Angular Universal 应用程序的渲染引擎:

  1. 在项目目录中打开终端。

  2. 运行以下命令安装 ts-node

    npm install --save ts-node @nguniversal/express-engine
    

    安装服务器依赖项

创建 server.ts 文件

我们将实现我们的 server.ts 文件。

在此文件中,我们将定义我们的 Express.js 服务器并配置它以渲染和提供我们的服务器应用程序:

  1. 在您的编辑器中,在项目根目录下创建一个名为 server.ts 的新文件。

  2. 在文件顶部添加以下 import 语句:

    import * as express from 'express';
    import { join } from 'path';
    import { ngExpressEngine } from '@nguniversal/express-engine';
    import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader';
    
  3. 定义我们将在服务器中使用的常量:

    const PORT = process.env.PORT || 8080;
    const staticRoot = join(process.cwd(), 'dist', 'browser');
    const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require('./dist/server/main.bundle');
    const app = express();
    
  4. 定义 html 视图引擎。这将让 Express.js 知道它使用哪个函数来渲染 HTML 文件:

    app.engine('html', ngExpressEngine({
      bootstrap: AppServerModuleNgFactory,
      providers: [
        provideModuleMap(LAZY_MODULE_MAP)
      ]
    }));
    
  5. 定义 Express.js 的其余默认设置。我们将默认视图引擎设置为 html,我们在上一步中定义的引擎。接下来,我们将视图的根目录设置为引用我们的 staticRoot

    app.set('view engine', 'html');
    app.set('views', staticRoot);
    
  6. 使用以下 Express.js 默认设置,我们定义了我们要静态提供所有非 html 类型的文件,并且默认路由(*)是渲染 index.html 文件:

    app.get('*.*', express.static(staticRoot));
    app.get('*', (req, res) => res.render('index', { req }));
    
  7. 启动服务器并记录主机和端口号:

    app.listen(PORT, () => console.log(`Server listening on http://localhost:${PORT}`));
    

将 npm 脚本添加到 package.json

我们将更新 package.json 并添加一个用于启动我们服务器的脚本:

  1. 在编辑器中打开我们项目根目录下的 package.json 文件。

  2. 定位到 scripts 对象并删除现有的 start 属性。

  3. scripts 对象中添加以下键:

    "start": "ts-node ./server",
    

启动服务器

我们将构建并启动我们的应用程序,并测试它是否正常工作!

  1. 使用以下命令运行完整的构建:

    $ npm run build
    
  2. 使用以下命令启动 Node.js 服务器:

    $ npm start
    

    启动服务器

  3. 导航到服务器端渲染的构建在:http://localhost:8080

  4. 验证应用程序是否正常工作。

  5. 从 Chrome 菜单中,转到 查看 | 开发者工具 | 查看源代码,并验证应用程序输出是否被渲染。

在本节中,我们使用 Express.js 构建的服务器运行了我们的应用程序。下一步是添加动态元数据,这将帮助我们的应用程序变得更加 SEO 友好。

添加动态元数据

现在我们可以使用服务器端渲染来渲染我们的页面,我们可以引入新的功能来增强我们应用的外观。

目前,我们的应用仍然只显示在 src/index.html 中设置的默认标题,并且不会添加任何其他 HTML 元标签。

为了提高我们页面的 SEO 友好性,并确保我们的社交预览中有有价值的信息,我们想要解决这个问题。

幸运的是,Angular 内置了 MetaTitle 类,允许我们向我们的页面添加动态标题和元数据。

当与服务器端渲染结合使用时,元数据和页面标题将确保被搜索引擎索引的页面在文档标题中设置了正确的元标签,从而提高可发现性。

在本节中,我们将添加一个服务,允许我们定义这些数据,并且在我们从解析器加载数据后,将更新我们的容器组件以调用该服务。

创建 UiService

  1. 在项目目录中打开一个终端。

  2. 运行以下命令以生成 UiService 并将其注册到 UiModule

    ng g s ui/services/ui --module ui/ui
    

    创建 UiService

  3. 在您的编辑器中打开 src/app/ui/services/ui.service.ts 文件。

  4. 在类定义中添加以下行:

    private appColor = '#C3002F';
    private appImage = '/assets/logo.svg';
    private appTitle = 'Angular Social';
    private appDescription = 'Angular Social is a Social Networking App built in Angular';
    
  5. @angular/platform-browser 中导入 TitleMeta

    import { Meta, Title } from '@angular/platform-browser';
    
  6. 在构造函数中注入 private title: Titleprivate meta: Meta

    constructor(private titleService: Title, private metaService: Meta){}
    
  7. 添加一个名为 setMetaData 的类方法,该方法接受一个 config 属性:

    public setMetaData(config) {}
    
  8. 将以下代码添加到 setMetaData 属性的主体中:

    // Get the description of the config, or use the default App Description
    const description = config.description || this.appDescription
    // Get the title of the config and append the App Title, or just use the App Title
    const title = config.title ? `${config.title} - ${this.appTitle}` : this.appTitle;
    
    // Set the Application Title
    this.titleService.setTitle(title);
    
    // Add the Application Meta tags
    this.metaService.addTags([
       { name: 'description', content: description },
       { name: 'theme-color', content: this.appColor },
       { name: 'twitter:card', content: 'summary' },
       { name: 'twitter:image', content: this.appImage },
       { name: 'twitter:title', content: title },
       { name: 'twitter:description', content: description },
       { name: 'apple-mobile-web-app-capable', content: 'yes' },
       { name: 'apple-mobile-web-app-status-bar-style', content: 'black-translucent' },
       { name: 'apple-mobile-web-app-title', content: title },
       { name: 'apple-touch-startup-image', content: this.appImage },
       { property: 'og:title', content: title },
       { property: 'og:description', content: description },
       { property: 'og:image', content: this.appImage },
    ]);
    

我们的 UiService 现在已准备好使用。我们将在下一节中将它添加到我们的组件中。

将元数据添加到 PostsComponent 和 ProfileComponent

我们将元数据添加到 PostsComponent

  1. 在您的编辑器中打开 src/app/posts/container/posts/posts.component.ts 文件。

  2. rxjs 中导入 UiServicemap 操作符:

    import { UiService } from '../../../ui/services/ui.service';
    
  3. 在构造函数中注入 private uiService: UiService

    constructor(private route: ActivatedRoute, private uiService: UiService) { }
    
  4. 添加一个名为 setMetadata() 的类方法,该方法接受一个 posts 属性:

    setMetaData(posts) {}
    
  5. 将以下内容添加到 setMetaData 方法中。在这个方法中,我们将构建动态元数据,并将其传递给 UiService 中的 setMetaData 方法:

    const { itemsPerPage, itemsTotal } = posts['counters']
    const description = `Showing ${itemsPerPage} from ${itemsTotal} posts`
    const title = 'Posts List'
    
    this.uiService.setMetaData({ description, title })
    return posts;
    
  6. 更新 ngOnInit 方法中的第一个 map 语句,如下所示。这将通过我们定义的方法将我们从 API 获取的数据传递过去:

    .map(data => this.setMetaData(data['posts']))
    

现在,我们将元数据添加到 ProfileComponent

  1. 在您的编辑器中打开 src/app/posts/container/profile/profile.component.ts 文件。

  2. rxjs 中导入 UiServicemap 操作符:

    import { UiService } from '../../../ui/services/ui.service';
    
  3. 在构造函数中注入 private uiService: UiService

    constructor(private route: ActivatedRoute, private uiService: UiService) { }
    
  4. 添加一个名为 setMetadata() 的类方法,该方法接受一个 profile 属性:

    setMetaData(profile) {}
    
  5. 将以下内容添加到 setMetaData 方法中。在这个方法中,我们将构建动态元数据,并将其传递给 UiService 中的 setMetaData 方法:

    const { fullName, posts } = profile;
    const description = `${fullName} posted ${posts.length} posts.`;
    const title = `Posts by ${fullName}`;
    this.uiService.setMetaData({ description, title });
    return profile;
    
  6. 更新 ngOnInit 方法,如下所示。这将通过我们定义的方法将我们从 API 获取的数据传递过去:

    this.route.data
       .map(data => this.setMetaData(data['profile']))
       .subscribe((result: any) => this.profile = result)
    

当你现在浏览应用程序时,你应该会看到页面标题根据你访问的页面进行更新。

你可以使用 Chrome 开发者工具中的元素检查器来验证元数据是否被添加到渲染的组件中:

将元数据添加到 PostsComponent 和 ProfileComponent

在本节中,我们已将元数据添加到我们的 PostsComponentProfileComponent。我们还创建了一个 UiService,允许我们定义这些数据。

摘要

在本课中,我们为我们的应用程序添加了服务器端渲染。我们首先生成了服务器应用程序并添加了其依赖项。然后我们在 package.json 文件中添加了脚本,在 Express.js 中实现了一个网络服务器。最后,我们看到了如何向我们的页面添加动态元数据。

在下一课中,我们将为我们的应用程序配置服务工作者。

第四章 服务工作者

在上一课中,我们学习了如何向我们的应用程序添加服务器端渲染。在下一课中,我们将为我们的应用程序配置服务工作者。

课程目标

在本课中,你将:

  • 探索服务工作者和 PWAs

  • 将服务工作者添加到我们在上一课中构建的应用程序中

  • 配置服务工作者将应用转换为渐进式网络应用

  • 探索如何调试服务工作者

让我们先了解什么是服务工作者和渐进式网络应用。

什么是服务工作者?

服务工作者是一个浏览器在后台运行的脚本,它充当网络代理以编程方式管理网络请求。它位于网络和设备之间,缓存内容,使用户能够离线体验。

除了缓存数据外,它还可以在后台同步 API 数据,并添加推送通知等功能。

什么是渐进式网络应用?

渐进式网络应用PWA)是一个用于描述以类似原生移动应用方式行为的网络应用的术语。

就像原生应用一样,它们允许在用户离线时启动应用程序,缓存 UI 元素和 API 调用来显示初始页面。这样,用户就可以在基本层面上与应用程序交互,直到连接建立。

一旦建立连接,PWA 将从服务器检索更新数据并刷新应用程序,以便用户可以使用最新数据。

注意

官方 Angular 文档有一个关于服务工作者的优秀部分:angular.io/guide/service-worker-intro

安装依赖项

Angular 内置了对服务工作者的支持。为了使用它,我们首先需要安装依赖项。

  1. 在项目目录中打开终端。

  2. 使用 npm 命令安装所需的依赖项:

    npm install @angular/service-worker
    
  3. 当安装成功时,我们应该在我们的项目 package.json 文件的 dependencies 对象中看到新添加的包:安装依赖项

在本节中,我们已经为我们的服务工作者安装了依赖项,这是在我们的应用程序中实现服务工作者的第一步。让我们继续到下一节,我们将在此处启用应用程序中的服务工作者。

启用服务工作者

现在依赖项已安装,是时候启用服务工作者了。

这涉及三个步骤:

  1. .angular-cli.json 中启用我们的浏览器应用中的服务工作者。

  2. 在我们的 AppModule 中导入和注册 ServiceWorkerModule

  3. 创建服务工作者配置文件 src/ngsw-config.json

我们将使用 ng set 命令在 .angular-cli.json 中启用对服务工作者的支持:

  1. 在项目目录中打开终端。

  2. 运行以下命令以调整 .angular-cli.json 文件:

    ng set apps.0.serviceWorker=true
    
  3. 确认在 .angular-cli.json 文件中 apps 数组的第一个应用中属性 serviceWorker 设置为 true启用服务工作者

导入 ServiceWorkerModule

我们将在 AppModule 中导入 ServiceWorkerModule 并注册它。

我们将在 ServiceWorkerModule 上调用 register 方法。此方法接受两个参数。第一个参数定义了 Angular 服务工作者的位置。在我们的情况下,应该使用值 '/ngsw-worker.js'

第二个参数是一个名为 environment 的对象,通过这个对象,我们可以控制是否启用服务工作者。我们使用 environment 对象来确定是否应该启用服务工作者,因为我们只想在生产构建中启用它:

  1. 在您的编辑器中打开 src/app/app.module.ts 文件。

  2. 在文件顶部添加以下 import 语句:

    import { ServiceWorkerModule } from '@angular/service-worker'
    import { environment } from '../environments/environment'
    

    ServiceWorkerModule 添加到 imports 数组,并使用这些参数调用 register 方法:

    ServiceWorkerModule.register('/ngsw-worker.js', {enabled: environment.production}),
    

创建服务工作者配置

  1. 创建 src/ngsw-config.json 文件并在您的编辑器中打开它。

  2. 将以下内容添加到文件中:

    {
     "index": "/index.html",
     "assetGroups": [
       {
         "name": "app",
         "installMode": "prefetch",
         "resources": {
           "files": [
             "/favicon.ico",
             "/index.html"
           ],
           "versionedFiles": [
             "/*.bundle.css",
             "/*.bundle.js",
             "/*.chunk.js"
           ]
         }
       },
       {
         "name": "assets",
         "installMode": "lazy",
         "updateMode": "prefetch",
         "resources": {
           "files": [
             "/assets/**"
           ]
         }
       }
     ]
    }
    

    在这里,我们将初始默认内容添加到 ngsw-config.json 文件中。这是 Angular 团队提供的默认配置,可以在以下位置找到:angular.io/guide/service-worker-getting-started#step-4-create-the-configuration-file-ngsw-configjson

  3. 在项目目录中打开终端。

  4. 运行 npm run build:browser 以创建生产构建。

  5. 验证构建是否成功运行,并且文件 ngsw-worker.jsngsw.json 已在 dist/browser 目录中生成。创建服务工作者,启用服务工作者配置

在本节中,我们在应用程序中启用了服务工作者并使用了默认配置。我们已经验证了生产构建生成了服务工作者配置。

让我们添加一些自定义配置选项。

配置服务工作者

在上一节中,我们将服务工作者配置文件 src/ngsw-config.json 添加到我们的项目中,但我们还没有进行任何配置。

在本节中,我们将添加两种类型的配置:资产组和数据组。

资产组和数据组

在资产组配置中,我们指定我们的服务工作者如何处理我们的应用程序的资产。当我们谈论资产时,我们应该想到样式表、图像、外部 JS 文件等等。

资产组使用以下 TypeScript 接口定义:

interface AssetGroup {
  name: string;
  installMode?: 'prefetch' | 'lazy';
  updateMode?: 'prefetch' | 'lazy';
  resources: {
    files?: string[];
    versionedFiles?: string[];
    urls?: string[];
  };
}

参数的含义如下:

  • name 唯一标识资产组

  • installMode 定义了新资源最初如何缓存

  • updateMode 定义了现有资源的缓存行为

  • resources 对象描述了实际要缓存的资源

完整的参考信息可以在以下位置找到:angular.io/guide/service-worker-config#assetgroups

在数据组配置中,我们指定了我们的 Service Worker 如何缓存我们从 API 请求的数据。

数据组使用以下 TypeScript 接口定义:

export interface DataGroup {
  name: string;
  urls: string[];
  version?: number;
  cacheConfig: {
    maxSize: number;
    maxAge: string;
    timeout?: string;
    strategy?: 'freshness' | 'performance';
  };
};

这里是这些参数的含义:

  • name 唯一标识该组

  • urls 是一个 URL 模式数组

  • version 提供了一种机制来强制重新加载缓存项

  • cacheConfig 定义了用于缓存此组的策略

完整的参考信息可以在以下位置找到:angular.io/guide/service-worker-config#datagroups

配置资产组和数据组

我们将向资产组配置中添加两个项目。

第一个资产组缓存了来自我们用于获取 CSS 和其中包含的字体所使用的域的数据。

第二个资产组缓存了我们工作中使用的 API 的静态数据;在这种情况下,是用户头像:

  1. 在您的编辑器中打开 src/ngsw-config.json 文件。

  2. 定位到 assetGroups 数组。

  3. 向此数组添加以下两个对象:

    {
       "name": "externals",
       "installMode": "prefetch",
       "updateMode": "prefetch",
       "resources": {
         "urls": [
           "https://ajax.googleapis.com/**",
           "https://fonts.googleapis.com/**",
           "https://fonts.gstatic.com/**",
           "https://maxcdn.bootstrapcdn.com/**"
         ]
       }
     },
     {
       "name": "avatars",
       "installMode": "prefetch",
       "updateMode": "prefetch",
       "resources": {
         "urls": [
           "http://localhost:3000/avatars/**",
           "https://packt-angular-social.now.sh/avatars/**"
         ]
       }
    }
    

    确保正确格式化 JSON;使用 jsonlint.com/ 来确保正确性。

我们将创建数据组配置。我们将定义一个数据组,用于缓存来自我们 API 的请求:

  1. 在您的编辑器中打开 src/ngsw-config.json 文件。

  2. 创建一个带有 dataGroups 键的顶级数组。

  3. 向此数组添加以下对象:

    {
       "name": "rest-api",
       "urls": [
         "http://localhost:3000/api/**",
         "https://packt-angular-social.now.sh/api/**"
       ],
       "cacheConfig": {
         "strategy": "freshness",
         "maxSize": 100,
         "maxAge": "1h",
         "timeout": "5s"
       }
    }
    

在本节中,我们在 Service Worker 中配置了应用程序的资产组和数据组。

使用此配置和运行中的 Service Worker,我们应该能够检索到一个完全样式化的应用程序,该应用程序显示最新的 API 数据。

测试 Service Worker

为了测试我们的 Service Worker 是否正常工作,我们必须加载我们的应用程序,然后断开浏览器与互联网的连接。

检查数据来源

使用 Chrome 开发者工具,可以轻松地看到特定资源是从哪里检索的。

使用 Chrome 开发者工具中的 Network 选项卡,您可以看到正在检索哪些文件,数据来自哪里,以及浏览器检索这些资源花费了多长时间。

下面的屏幕截图显示了一个正常的页面请求,其中每个文件都是从网络服务器下载的:

检查数据来源

在下面的屏幕截图中,在 Size 列中,您可以看到数据是从 Service Worker 中检索的。这意味着它没有向网络发出请求来获取这些项;相反,它从浏览器缓存中获取了它们:

检查数据来源

启用离线模式

网络浏览器在线是它的本质,但在现实中,我们都发现自己处于设备离线的情况,这是由于缺乏网络连接。

为了开发能够处理这些情况的应用程序,Chrome 提供了一个所谓的离线模式。它将阻止浏览器连接到网络。这样,我们可以确保我们的应用程序按预期运行。

在 Chrome 开发者工具的网络标签页中,你可以找到一个名为离线的复选框,它触发了这种行为:

启用离线模式

在勾选此框后,你将在标签名称旁边看到一个黄色的指示器,这表明网络标签页存在异常情况:

启用离线模式

运行浏览器应用程序的本地构建

我们将构建一个启用 Service Worker 的应用程序的生产版本。一旦构建完成,我们将使用一个简单的名为 http-server 的网络服务器托管该构建,并在我们的浏览器中打开它:

  1. 使用以下命令构建浏览器应用程序:

    npm run build:browser
    
  2. 使用以下命令启动应用:

    npx http-server ./dist/browser
    

    应用程序现在将在:http://localhost:8080上提供服务。

  3. 在浏览器中打开页面。你应该看到帖子列表。

  4. 在 Chrome 开发者工具中打开控制台标签页,并验证是否存在错误。

检查行为

我们将看到我们的应用程序在启用 Service Worker 时的行为:

  1. 在浏览器中打开上一个练习的页面。

  2. 在 Chrome 开发者工具中打开网络标签页。

  3. 在打开此网络标签页的情况下,重新加载 http://localhost:8080 以查看数据来源。

你应该看到数据是从 Service Worker 加载的。

将我们的应用程序设置为离线模式

我们将把我们的应用程序设置为离线模式,并验证 Service Worker 是否显示了我们应用程序的完整和缓存版本:

  1. 在浏览器中打开上一个练习的页面。

  2. 在 Chrome 开发者工具中打开网络标签页。

  3. 通过勾选复选框启用离线模式。

  4. 离线模式下,导航到:http://localhost:8080

你应该看到我们的应用程序仍然被加载并显示缓存的数据。

在本节中,我们运行了我们应用程序的本地构建,并测试了其在离线模式下的行为。即使在离线模式下,我们的应用程序也能正常运行。我们现在可以探索如何调试我们的 Service Worker。

调试 Service Worker

计算机科学中有一句著名的话:

“计算机科学中有两个难题:缓存失效,命名事物。”

-Phil Karlton

第一个适用于调试 Service Worker。

如前所述,Service Worker 在网络和设备之间添加了一个缓存层。这本质上使得调试变得困难,因为当你更新你的 Service Worker 定义或网站的配置时,你的更改可能会被缓存,因此不可见。

这是在使用支持 Service Worker 的应用程序开发中相当常见的挑战,因此了解如何调试 Service Worker 是很好的。

调试 Service Worker

Chrome 开发者工具来帮忙

Chrome 开发者工具是一个用于检查和调试网站背后技术的先进工具,幸运的是,它对服务工作者有很好的支持。

应用程序标签页中,我们可以看到已安装的服务工作者,它们的状态是什么,以及注销它们以确保我们下载最新版本。

定位正在运行的服务工作者

我们将找到可以找到正在运行的服务工作者的地方:

  1. 在浏览器中打开上一个练习的页面。

  2. 在 Chrome 开发者工具中打开应用程序标签页。

  3. 应用程序标签页的侧边栏中点击服务工作者链接。

  4. 验证服务工作者列表中是否有条目。

注销已注册的服务工作者。

我们将注销我们的服务工作者:

  1. 在浏览器中打开上一个练习的页面。

  2. 在 Chrome 开发者工具中打开应用程序标签页,并点击侧边栏中的服务工作者链接。

  3. 定位到状态设置为激活的服务工作者条目。

  4. 点击更新链接旁边的注销链接。

  5. 当您现在刷新页面时,应该加载一个新的服务工作者。

注意

如果您只是刷新页面,它将加载我们构建中的相同服务工作者。

构建服务工作者的开发周期看起来大致如下:

  1. 在 Angular 应用程序中做出更改。

  2. 使用npm run build:browser命令创建生产构建。

  3. 使用npx http-server ./dist/browser命令提供新的构建版本。

  4. 注销当前活动中的服务工作者。

  5. 浏览到新版本并验证您所做的更改是否已应用。

在本节中,我们看到了如何在浏览器中定位服务工作者。然后,我们通过注销它来调试它。

摘要

在本节课中,我们完全使用了服务工作者。我们首先安装了所需的依赖项。然后,我们继续启用服务工作者,配置它,测试它,最后调试它。

posted @ 2025-09-05 09:24  绝不原创的飞龙  阅读(12)  评论(0)    收藏  举报