使用Angular构建您的第一个PWA


渐进式Web应用程序(PWA)在过去几年中一直是一个非常流行的词,但是它到底是什么呢?

PWAs利用许多现代浏览器技术来改善总体用户体验。PWA的核心组件是一个服务工作者,它是一段JavaScript代码,运行在网站的后台,截取和获取所有浏览器请求。

如果服务工作者发现它在缓存中具有该资源的最新版本,它将转而提供缓存的资源。此外,应用程序清单允许在浏览器中安装应用程序。这使得启动PWA在移动设备上,即使该设备处于脱机状态。

在这篇文章中,我们将使用Angular 7,Angular CLI和Angular Material来构建一个PWA,让用户使用这个OpenLibrary服务。我们潜进去吧。

你可能还喜欢:Developing a PWA Using Angular 7

使用Angular创建单页应用程序

首先用Angular 7创建一个单页应用程序。我将假设您的系统上安装了Node。首先,您需要安装Angular命令行工具。打开shell并输入以下命令。

npm install -g @angular/cli@7.1.3


这将安装ng命令。根据您的系统设置,您可能需要使用sudo。曾经npm已完成安装,您将准备创建一个新的Angular项目。在shell中,导航到要在其中创建应用程序的目录,然后键入以下命令。

ng new AngularBooksPWA


这将创建一个名为AngularBooksPWA和一个角度的应用。脚本会问您两个问题。当您被问到是否要在项目中使用路由器时,请回答是。路由器将允许您使用浏览器的URL在不同的应用程序组件之间导航。接下来,系统将提示您输入您希望使用的CSS技术。

在这个简单的项目中,我将使用普通CSS。对于较大的项目,您应该将其切换到其他技术之一。回答完问题后,ng会将所有必要的包安装到新创建的应用程序目录中,并创建多个文件来帮助您快速入门。

添加棱角材料

接下来,导航到项目的目录并运行以下命令。

npm install @angular/material@7.1.0 @angular/cdk@7.1.0 @angular/animations@7.1.0 @angular/flex-layout@7.0.0-beta.19


此命令将安装使用材料设计所需的所有包。材料设计使用图标字体来显示图标。这种字体托管在谷歌的CDN上。若要包含图标字体,请打开src/index.html文件中添加以下行<head>标签。

<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">


src/app/app.module.ts包含将在整个应用程序中可用的模块的导入。为了导入您将要使用的角材料模块,打开文件并更新它以匹配以下内容。

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';

import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FlexLayoutModule } from "@angular/flex-layout";

import { MatToolbarModule,
         MatMenuModule,
         MatIconModule,
         MatCardModule,
         MatButtonModule,
         MatTableModule,
         MatDividerModule,
         MatProgressSpinnerModule } from '@angular/material';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent,
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    BrowserAnimationsModule,
    FlexLayoutModule,
    FormsModule,
    ReactiveFormsModule,
    MatToolbarModule,
    MatMenuModule,
    MatIconModule,
    MatCardModule,
    MatButtonModule,
    MatTableModule,
    MatDividerModule,
    MatProgressSpinnerModule,
    AppRoutingModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }


应用程序的主要组件的模板存在于src/app/app.component.html文件。打开此文件,并用以下代码替换内容。

<mat-toolbar color="primary" class="expanded-toolbar">
    <span>
      <button mat-button routerLink="/">{{title}}</button>
      <button mat-button routerLink="/"><mat-icon>home</mat-icon></button>
    </span>
    <div fxLayout="row" fxShow="false" fxShow.gt-sm>
        <form [formGroup]="searchForm" (ngSubmit)="onSearch()">
          <div class="input-group">
            <input class="input-group-field" type="search" value="" placeholder="Search" formControlName="search">
            <div class="input-group-button"><button mat-flat-button color="accent"><mat-icon>search</mat-icon></button></div>
          </div>
        </form>
    </div>
    <button mat-button [mat-menu-trigger-for]="menu" fxHide="false" fxHide.gt-sm>
     <mat-icon>menu</mat-icon>
    </button>
</mat-toolbar>
<mat-menu x-position="before" #menu="matMenu">
    <button mat-menu-item routerLink="/"><mat-icon>home</mat-icon> Home</button>

    <form [formGroup]="searchForm" (ngSubmit)="onSearch()">
      <div class="input-group">
        <input class="input-group-field" type="search" value="" placeholder="Search" formControlName="search">
        <div class="input-group-button"><button mat-button routerLink="/"><mat-icon>magnify</mat-icon></button></div>
      </div>
    </form>
</mat-menu>
<router-outlet></router-outlet>


您可能会注意到routerLink在各种地方使用的属性。这些是指将在本教程后面添加的组件。另外,请注意HTML<form>标记和formGroup属性。这是允许您搜索书名的搜索表单。我将在实现应用程序组件时提到这一点。

接下来,我将添加一点造型。Angular将样式表分离为单个全局样式表和每个组件的局部样式表。首先,在中打开全局样式表src/style.css并将以下内容粘贴到其中。

@import "~@angular/material/prebuilt-themes/deeppurple-amber.css";

body {
  margin: 0;
  font-family: sans-serif;
}

h1, h2 {
  text-align: center;
}

.input-group {
  display: flex;
  align-items: stretch;
}

.input-group-field {
  margin-right: 0;
}

.input-group .input-group-button {
  margin-left: 0;
  border: none;
}

.input-group .mat-flat-button {
  border-radius: 0;
}


此样式表中的第一行必须将正确的样式应用于Angular Material组件。主应用程序组件的本地样式可在src/app/app.component.css在此处添加工具栏样式。

.expanded-toolbar {
  justify-content: space-between;
  align-items: center;
}


添加具有Angular的搜索功能

现在,您终于准备好实现主应用程序组件了。打开src/app/app.component.ts并将其内容替换为以下内容。

import { Component } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { Router } from "@angular/router";

import { BooksService } from './books/books.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'AngularBooksPWA';
  searchForm: FormGroup;

  constructor(private formBuilder: FormBuilder,
              private router: Router) {
  }

  ngOnInit() {
    this.searchForm = this.formBuilder.group({
      search: ['', Validators.required],
    });
  }

  onSearch() {
    if (!this.searchForm.valid) return;
    this.router.navigate(['search'], { queryParams: {query: this.searchForm.get('search').value}});
  }
}


关于这段代码,有两件事需要注意。这个searchForm属性是一个FormGroup,它是使用FormBuilder。构建器允许创建表单元素,这些元素可以与验证器相关联,以便轻松地验证任何用户输入。

当用户提交表单时,onSearch()函数被调用。这将检查有效的用户输入,然后简单地将呼叫转发到路由器。注意查询字符串是如何传递到路由器的。这将把查询追加到URL,并使其可用于search路线。路由器将选择适当的组件,图书搜索在该组件内处理。

这意味着执行搜索请求的责任被封装在单个组件的本地作用域中。在构建更大的应用程序时,这种职责分离是保持代码简单和可维护性的重要技术。

创建与OpenLibrary API对话的BookService

接下来,创建一个服务,它将为OpenLibrary API提供高级接口。要让Angular创建服务,请在应用程序根目录中再次打开shell并运行以下命令。

ng generate service books/books


这将在src/app/books目录。打开books.service.ts文件,并将其内容替换为以下内容。

import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';

const baseUrl = 'http://openlibrary.org';

@Injectable({
  providedIn: 'root'
})
export class BooksService {

  constructor(private http: HttpClient) { }

  async get(route: string, data?: any) {
    const url = baseUrl+route;
    let params = new HttpParams();

    if (data!==undefined) {
      Object.getOwnPropertyNames(data).forEach(key => {
        params = params.set(key, data[key]);
      });
    }

    const result = this.http.get(url, {
      responseType: 'json',
      params: params
    });

    return new Promise<any>((resolve, reject) => {
      result.subscribe(resolve as any, reject as any);
    });
  }

  searchBooks(query: string) {
    return this.get('/search.json', {title: query});
  }
}


为了保持简单,您可以使用到OpenLibrary API的单一路由。这个search.jsonroute接受一个搜索请求,并返回一个图书列表以及一些有关这些图书的信息。注意函数如何返回Promise对象。这将使以后使用async/await技巧。

使用Angular CLI为PWA生成角度分量

现在,是时候将注意力转移到构成该书搜索应用程序的组件上了。总共将有三个组成部分。这个Home组件显示初始屏幕,Search列出图书搜索结果和Details显示单本书的详细信息。要创建这些组件,请打开shell并执行以下命令。

ng generate component home
ng generate component search
ng generate component details


创建了这三个组件之后,必须使用Router。打开src/app/app-routing.module.ts并为刚刚创建的组件添加路由。

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { SearchComponent } from './search/search.component';
import { DetailsComponent } from './details/details.component';

const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'search', component: SearchComponent },
  { path: 'details', component: DetailsComponent },
];

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


Home组件。此组件将仅由两个简单的标题组成。打开src/app/home/home.component.html然后输入下面的行。

<h1>Angular Books PWA</h1>
<h2>A simple progressive web application</h2>


接下来,通过更改中的代码来实现搜索组件src/app/search/search.component.ts看起来像下面这样。

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { MatTableDataSource } from '@angular/material';
import { BooksService } from '../books/books.service';
import { Subscription } from 'rxjs';

@Component({
  selector: 'app-search',
  templateUrl: './search.component.html',
  styleUrls: ['./search.component.css']
})
export class SearchComponent implements OnInit {
  private subscription: Subscription;

  displayedColumns: string[] = ['title', 'author', 'publication', 'details'];
  books = new MatTableDataSource<any>();

  constructor(private route: ActivatedRoute,
              private router: Router,
              private bookService: BooksService) { }

  ngOnInit() {
    this.subscription = this.route.queryParams.subscribe(params => {
      this.searchBooks(params['query']);
    });
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }

  async searchBooks(query: string) {
    const results = await this.bookService.searchBooks(query);

    this.books.data = results.docs;
  }

  viewDetails(book) {
    console.log(book);
    this.router.navigate(['details'], { queryParams: {
      title: book.title,
      authors: book.author_name && book.author_name.join(', '),
      year: book.first_publish_year,
      cover_id: book.cover_edition_key
    }});
  }
}


这里发生了一些事情。在组件初始化期间,搜索查询是通过订阅ActivatedRoute.queryParams可观察到的。每当值更改时,这将调用searchBooks方法。在此方法中,BooksService,用于获取图书列表。结果传递给MatTableDataSource对象,该对象允许用角形素材库显示漂亮的表格。

请看一下src/app/search/search.component.html并更新其HTML以匹配下面的模板。

<h1 class="h1">Search Results</h1>
<div fxLayout="row" fxLayout.xs="column" fxLayoutAlign="center" class="products">
  <table mat-table fxFlex="100%" fxFlex.gt-sm="66%" [dataSource]="books" class="mat-elevation-z1">
    <ng-container matColumnDef="title">
      <th mat-header-cell *matHeaderCellDef>Title</th>
      <td mat-cell *matCellDef="let book"> {{book.title}} </td>
    </ng-container>
    <ng-container matColumnDef="author">
      <th mat-header-cell *matHeaderCellDef>Author</th>
      <td mat-cell *matCellDef="let book"> {{book.author_name && book.author_name.join(', ')}} </td>
    </ng-container>
    <ng-container matColumnDef="publication">
      <th mat-header-cell *matHeaderCellDef>Pub. Year</th>
      <td mat-cell *matCellDef="let book"> {{book.first_publish_year}} </td>
    </ng-container>
    <ng-container matColumnDef="details">
      <th mat-header-cell *matHeaderCellDef></th>
      <td mat-cell *matCellDef="let book">
        <button mat-icon-button (click)="viewDetails(book)"><mat-icon>visibility</mat-icon></button>
      </td>
    </ng-container>
    <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
    <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
  </table>
</div>


此表使用数据源显示搜索结果。最后一个组件显示书的详细信息,包括它的封面图像。就像Search组件,通过订阅路由参数获取数据。打开src/app/details/details.component.ts并将内容更新为以下内容。

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';

@Component({
  selector: 'app-details',
  templateUrl: './details.component.html',
  styleUrls: ['./details.component.css']
})
export class DetailsComponent implements OnInit {
  private subscription: Subscription;
  book: any;

  constructor(private route: ActivatedRoute) { }

  ngOnInit() {
    this.subscription = this.route.queryParams.subscribe(params => {
      this.updateDetails(params);
    });
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }

  updateDetails(book) {
    this.book = book;
  }
}


模板只是显示了本书数据结构中的一些字段。将以下内容复制到src/app/details/details.component.html

<h1 class="h1">Book Details</h1>
<div fxLayout="row" fxLayout.xs="column" fxLayoutAlign="center" class="products">
  <mat-card fxFlex="100%" fxFlex.gt-sm="66%" class="mat-elevation-z1">
    <h3>{{book.title}}</h3>
    <img src="http://covers.openlibrary.org/b/OLID/{{book.cover_id}}-M.jpg" />
    <h4>Authors</h4>
    <p>
      {{book.authors}}
    </p>
    <h4>Published</h4>
    <p>
      {{book.year}}
    </p>
  </mat-card>
</div>


运行您的角度PWA

申请现已完成。您现在可以启动并测试应用程序。生成PWA时,不应使用ng serve命令来运行应用程序。这在开发过程中是可以的,但是它会禁用许多对PWA的性能来说是必需的特性。相反,您需要以生产模式构建应用程序,并使用http-server-spa高曼。请运行以下命令。

npm install -g http-server-spa@1.3.0
ng build --prod --source-map
http-server-spa dist/AngularBooksPWA/ index.html 8080


您只需要运行第一个命令一次,就可以安装http-server-spa高曼。第二行构建您的Angular应用程序。用--source-map选项,您将生成帮助您在浏览器中调试的源映射。最后一个命令启动HTTP服务器。打开浏览器,导航到http://localhost:8080,并在搜索栏中输入书名。你应该会看到一个书单,有点像这样。

Search results

搜索结果



向您的Angular PWA添加身份验证

一个完整的应用程序必须有一些用户身份验证,以限制对应用程序中包含的一些信息的访问。Okta允许您以快速,简单和安全的方式实现身份验证。在本节中,我将向您展示如何使用Angular的Okta库实现身份验证。如果您还没有这样做,请在OKTA注册一个开发人员帐户。

Okta landing page

Okta登陆页


打开浏览器并导航到developer.okta.com。点击 Create Free Account。在下一个屏幕上,输入您的详细信息,然后单击“开始”。您将被带到您的Okta开发人员仪表板。每个使用Okta身份验证服务的应用程序都需要在仪表板中注册。单击Add Application创建一个新的应用程序。

添加新应用程序


您正在创建的PWA属于单页应用程序类别。选择“单页应用程序”,然后单击“下一步”。

选择单页应用程序



在下一页上,您将看到应用程序的设置。您可以保持默认设置不变,然后单击“完成”。在下面的屏幕上,您将看到一个客户机ID。这在您的应用程序中是需要的。

要向PWA添加身份验证,首先安装Angular的Okta库。

npm install @okta/okta-angular@1.0.7 --save-exact


打开app.module.ts并导入OktaAuthModule

import { OktaAuthModule } from '@okta/okta-angular';


添加OktaAuthModule到…的名单importsapp

OktaAuthModule.initAuth({
  issuer: 'https://{yourOktaDomain}/oauth2/default',
  redirectUri: 'http://localhost:8080/implicit/callback',
  clientId: '{yourClientId}'
})


{yourClientId}必须由注册应用程序时获得的客户端ID替换。接下来,打开app.component.ts,并导入服务。

import { OktaAuthService } from '@okta/okta-angular';


创建一个isAuthenticated字段作为AppComponent

isAuthenticated: boolean;


然后,修改构造函数以注入服务并订阅它。

constructor(private formBuilder: FormBuilder,
            private router: Router,
            public oktaAuth: OktaAuthService) {
  this.oktaAuth.$authenticationState.subscribe(
    (isAuthenticated: boolean)  => this.isAuthenticated = isAuthenticated
  );
}


每当身份验证状态更改时,这将反映在isAuthenticated财产。您仍然需要在加载组件时对其进行初始化。在ngOnInit()方法添加行

this.oktaAuth.isAuthenticated().then((auth) => {this.isAuthenticated = auth});


您希望应用程序能够对登录和注销请求作出反应。为此,请实现login()logout()方法如下。

login() {
  this.oktaAuth.loginRedirect();
}

logout() {
  this.oktaAuth.logout('/');
}


打开app.component.html并将以下行添加到顶部栏中<div fxLayout="row" fxShow="false" fxShow.gt-sm>

<button mat-button *ngIf="!isAuthenticated" (click)="login()"> Login </button>
<button mat-button *ngIf="isAuthenticated" (click)="logout()"> Logout </button>


最后,您需要注册将用于登录请求的路由。打开app-routing.module.ts并添加以下导入。

import { OktaCallbackComponent, OktaAuthGuard } from '@okta/okta-angular';


添加implicit/callback路由到routes阵列。

{ path: 'implicit/callback', component: OktaCallbackComponent }


这是完成身份验证后,Okta授权服务将返回的路由。下一步是保护search以及details路由,请将以下设置添加到两条路由中。

canActivate: [OktaAuthGuard]


在这些更改之后,您的路由数组应该如下所示。

const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'search', component: SearchComponent, canActivate: [OktaAuthGuard] },
  { path: 'details', component: DetailsComponent, canActivate: [OktaAuthGuard] },
  { path: 'implicit/callback', component: OktaCallbackComponent }
];


就是这个了。每当用户试图访问应用程序的Search或Details视图时,他们将被重定向到Okta登录页面。一旦登录,用户将被重定向回他们最初想要查看的视图。与前面一样,您可以通过运行以下命令来构建和启动应用程序:

ng build --prod --source-map
http-server-spa dist/AngularBooksPWA/ index.html 8080


用Angular创建一个渐进式Web应用程序

应用程序可以运行和工作,但它不是一个渐进的Web应用程序。为了查看应用程序的性能,我将使用Lighthouse扩展。如果你不想安装Lighthouse,也可以使用谷歌Chrome内置的审计工具。

这是Lighthouse的一个稍微不那么最新的版本,可以通过Developer Tools>Audits访问。安装Lighthouse extension对于Chrome浏览器。此扩展允许您分析网页和应用程序的性能和兼容性。

安装完成后,打开你的应用程序,点击小灯塔标识,运行测试。目前,您的Books应用程序在PWA中的得分可能很低,只有46%。


blog/first-angular-pwa/lighthouse-46.png

灯塔


在过去的一年里,Angular的开发人员已经使得将您的常规应用程序转换为PWA变得非常容易。关闭服务器并运行以下命令。

ng add @angular/pwa --project AngularBooksPWA


重建您的应用程序,启动服务器,并再次运行Lighthouse。我试过了,结果我得了92%的分数。应用程序没有达到100%的唯一原因是由于它没有通过https协议。

添加PWA支持做了什么?最重要的改进是增加了一名服务工作者。服务工作者可以截获对服务器的请求,并尽可能返回缓存的结果。这意味着应用程序应该在您脱机时工作。要对此进行测试,请打开开发人员控制台,打开network选项卡,并勾选脱机复选框。当您现在单击“重新加载”按钮时,页面应该仍然可以工作并显示一些内容。

添加PWA支持还创建了各种大小的应用程序图标(在src/assets/icons/目录)。当然,你会想用你自己的图标来替换它们。使用任何常规的图像处理软件来创建一些很酷的标识。最后,一个web应用程序清单被添加到文件中src/manifest.json清单为浏览器提供在用户设备上本地安装应用程序所需的信息。

这是否意味着您已经完成了将应用程序转换为PWA的工作?一点也不!还有许多其他特性没有经过Lighthouse的测试,但仍然可以成为一个很好的渐进Web应用程序。检查谷歌的Progressive Web App Checklist以获得一个好的PWA的特性列表。

缓存最近得请求与响应

在浏览器中启动Books应用程序并搜索一本书。现在点击其中一本书的眼睛图标来查看它的详细信息。在加载了详细信息页面之后,打开开发人员控制台并切换到脱机模式(网络选项卡>检查脱机)。

在此模式下,单击浏览器中的“后退”按钮。您会注意到内容已经消失。应用程序正在尝试再次从OpenLibrary API请求资源。理想情况下,您希望在缓存中保留一些搜索结果。另外,让用户知道他们是在脱机模式下使用应用程序也是很好的。

我将从缓存开始。下面的代码改编自Tamas Piros’ great article about caching HTTP requests。首先创建两个新服务。

ng generate service cache/request-cache
ng generate service cache/caching-interceptor


现在,更改src/app/cache/request-cache.service.ts文件来镜像下面的代码。

import { Injectable } from '@angular/core';
import { HttpRequest, HttpResponse } from '@angular/common/http';

const maxAge = 30000;
@Injectable({
  providedIn: 'root'
})
export class RequestCache  {

  cache = new Map();

  get(req: HttpRequest<any>): HttpResponse<any> | undefined {
    const url = req.urlWithParams;
    const cached = this.cache.get(url);

    if (!cached) return undefined;

    const isExpired = cached.lastRead < (Date.now() - maxAge);
    const expired = isExpired ? 'expired ' : '';
    return cached.response;
  }

  put(req: HttpRequest<any>, response: HttpResponse<any>): void {
    const url = req.urlWithParams;
    const entry = { url, response, lastRead: Date.now() };
    this.cache.set(url, entry);

    const expired = Date.now() - maxAge;
    this.cache.forEach(expiredEntry => {
      if (expiredEntry.lastRead < expired) {
        this.cache.delete(expiredEntry.url);
      }
    });
  }
}


RequestCache服务充当内存中的缓存。这个putget方法将存储和检索HttpResponses基于请求数据。现在替换src/app/cache/caching-interceptor.service.ts与以下。

import { Injectable } from '@angular/core';
import { HttpEvent, HttpRequest, HttpResponse, HttpInterceptor, HttpHandler } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
import { RequestCache } from './request-cache.service';

@Injectable()
export class CachingInterceptor implements HttpInterceptor {
  constructor(private cache: RequestCache) {}

  intercept(req: HttpRequest<any>, next: HttpHandler) {
    const cachedResponse = this.cache.get(req);
    return cachedResponse ? of(cachedResponse) : this.sendRequest(req, next, this.cache);
  }

  sendRequest(req: HttpRequest<any>, next: HttpHandler,
    cache: RequestCache): Observable<HttpEvent<any>> {
    return next.handle(req).pipe(
      tap(event => {
        if (event instanceof HttpResponse) {
          cache.put(req, event);
        }
      })
    );
  }
}


CachingInterceptor可以截获任何HttpRequest。它使用RequestCache服务查找已存储的数据,并在可能的情况下返回该数据。要设置拦截器,请打开src/app/app.module.ts并添加以下导入。

import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { RequestCache } from './cache/request-cache.service';
import { CachingInterceptor } from './cache/caching-interceptor.service';


更新providers节以包括服务。

providers: [{ provide: HTTP_INTERCEPTORS, useClass: CachingInterceptor, multi: true }],


通过这些更改,应用程序将缓存最近的请求及其响应。这意味着您可以从详细信息页导航回来,并且仍然可以在脱机模式下看到搜索结果。

注意:在这个版本中,我将缓存保存在内存中,而不是将其持久化在浏览器的localStorage。这意味着当您强制重新加载应用程序时,您将丢失搜索结果。如果希望持久存储响应,则必须修改RequestCache因此。

切记不要使用ng serve命令来测试你的PWA。始终先生成项目,然后使用以下命令启动服务器:

ng build --prod --source-map
http-server-spa dist/AngularBooksPWA/ index.html 8080


监视网络状态

打开src/app/app.component.ts并添加以下属性和方法。

offline: boolean;

onNetworkStatusChange() {
  this.offline = !navigator.onLine;
  console.log('offline ' + this.offline);
}


然后编辑ngOnInit方法并添加以下行。

window.addEventListener('online',  this.onNetworkStatusChange.bind(this));
window.addEventListener('offline', this.onNetworkStatusChange.bind(this));


最后,在中的顶栏中添加一个通知src/app/app.component.html,在div包含搜索表单的。

<div *ngIf="offline">offline</div>


应用程序现在将显示一个脱机当网络不可用时,顶部栏中的消息。

图书详细信息

很酷,你不觉得吗?!

了解有关PWAs和Angular的更多信息

在本教程中,我向您展示了如何使用Angular 7创建一个渐进的web应用程序。由于Angular开发人员投入的努力,您的PWA比以往任何时候都更容易获得满分。只需一个命令,所有必要的资源和基础设施就可以到位,使您的应用程序脱机就绪。为了创建一个真正出色的PWA,您可以对应用程序进行更多的改进。我已经向您展示了如何实现HTTP请求的缓存以及告诉用户何时脱机的指示器。

该项目的完整代码可以在https://github.com/oktadeveloper/okta-angular-pwa-example

为了进一步改进应用程序,您可以考虑以下几点。您可以在用户处于脱机模式时为书籍封面添加占位符图像。您还应该确保延迟加载图像不会使页面跳转。因为PWA主要对移动设备有用,所以检查是否始终可以将表单输入滚动到视图中是很重要的,即使屏幕上的键盘是打开的。

要了解更多关于Angular 7的信息,请查看其他近期教程:

您可能还会发现以下链接很有帮助: