1. 前言
渐进式Web应用程序(PWA)是一种Web应用程序,它提供了一组功能,可以为网站提供类似App的体验。
2. 安装
==特别提醒:以下是采用了angular/cli 9.1==
- 使用脚手架将pwa集成到我们的项目里
1
// 终端命令
2
ng add @angular/pwa
3
4
5
// 以下是输出信息
6
localhost:d1 apple$ ng add @angular/pwa
7
Installing packages for tooling via npm.
8
Installed packages for tooling via npm.
9
CREATE ngsw-config.json (620 bytes)
10
CREATE src/manifest.webmanifest (1296 bytes)
11
CREATE src/assets/icons/icon-128x128.png (1253 bytes)
12
CREATE src/assets/icons/icon-144x144.png (1394 bytes)
13
CREATE src/assets/icons/icon-152x152.png (1427 bytes)
14
CREATE src/assets/icons/icon-192x192.png (1790 bytes)
15
CREATE src/assets/icons/icon-384x384.png (3557 bytes)
16
CREATE src/assets/icons/icon-512x512.png (5008 bytes)
17
CREATE src/assets/icons/icon-72x72.png (792 bytes)
18
CREATE src/assets/icons/icon-96x96.png (958 bytes)
19
UPDATE angular.json (3795 bytes)
20
UPDATE package.json (1319 bytes)
21
UPDATE src/app/app.module.ts (604 bytes)
22
UPDATE src/index.html (470 bytes)
23
✔ Packages installed successfully.
24
localhost:d1 apple$
从输出日志我们可以看出来,命令会添加service-worker 包,并建立必要的支持文件,如果你生成的文件不全或者写入失败则需要手动创建对应的文件。
3. 运行
- 由于 ng serve 对 Service Worker 无效,所以必须用一个独立的 HTTP 服务器在本地测试你的项目。这里我们选择http-server,这也是官方推荐的。
1 | // 1. 全局安装http-server(PS:如果你之前安装有可以跳过这一步) |
2 | npm i -g http-server |
3 | |
4 | // 2. 构建生产文件 |
5 | ng build --prod |
6 | |
7 | // 3. 运行项目(PS:下边的文件目录是默认目录,如果你项目做了更改则调整我自己项目构建出来的生产文件目录即可) |
8 | http-server -p 4200 -c-1 dist/<项目的名字> |
- http-server 简单介绍
- 如果你的项目是默认打包,则使用以下终端命令:-p 是 –port 的简写,-c-1是禁用浏览器cache-control max-age。
1
http-server -p 4200 -c-1 dist/<项目的名字>
- 如果你的项目是默认打包,则使用以下终端命令:-p 是 –port 的简写,-c-1是禁用浏览器cache-control max-age。
- 如果你的项目是使用了gzip压缩,则使用以下终端命令:-g 是 –gzip 的简写;-p 是 –port 的简写,-c-1是禁用浏览器cache-control max-age。
1
http-server -g -p 4200 -c-1 dist/<项目的名字>
- 如果你的项目是使用了gzip压缩,则使用以下终端命令:-g 是 –gzip 的简写;-p 是 –port 的简写,-c-1是禁用浏览器cache-control max-age。
- 如果你的项目是使用了brotli压缩,则使用以下终端命令:-b 是 –brotli 的简写,;-p 是 –port 的简写,-c-1是禁用浏览器cache-control max-age。
1
http-server -b -p 4200 -c-1 dist/<项目的名字>
- 如果你的项目是使用了brotli压缩,则使用以下终端命令:-b 是 –brotli 的简写,;-p 是 –port 的简写,-c-1是禁用浏览器cache-control max-age。
4. PWA结构介绍
- manifest.webmanifest(PS:旧版本的cli生成的文件是:manifest.json)
它是Web应用程序清单文件,json结构,主要用于浏览器识别Web应用程序。里边有很多配置项,具体可以查阅MDN Web app manifests。
1 | { |
2 | "name": "pwa", // 应用程序安装的的名字,主要用于浏览器上的显示 |
3 | "short_name": "pwa", // 移动设备或者iPad上的安装 |
4 | "theme_color": "#1976d2", |
5 | "background_color": "#fafafa", |
6 | "display": "standalone", |
7 | "scope": "./", |
8 | "start_url": "./", |
9 | "icons": [ |
10 | { |
11 | "src": "assets/icons/icon-72x72.png", |
12 | "sizes": "72x72", |
13 | "type": "image/png" |
14 | }, |
15 | { |
16 | "src": "assets/icons/icon-96x96.png", |
17 | "sizes": "96x96", |
18 | "type": "image/png" |
19 | }, |
20 | { |
21 | "src": "assets/icons/icon-128x128.png", |
22 | "sizes": "128x128", |
23 | "type": "image/png" |
24 | }, |
25 | { |
26 | "src": "assets/icons/icon-144x144.png", |
27 | "sizes": "144x144", |
28 | "type": "image/png" |
29 | }, |
30 | { |
31 | "src": "assets/icons/icon-152x152.png", |
32 | "sizes": "152x152", |
33 | "type": "image/png" |
34 | }, |
35 | { |
36 | "src": "assets/icons/icon-192x192.png", |
37 | "sizes": "192x192", |
38 | "type": "image/png" |
39 | }, |
40 | { |
41 | "src": "assets/icons/icon-384x384.png", |
42 | "sizes": "384x384", |
43 | "type": "image/png" |
44 | }, |
45 | { |
46 | "src": "assets/icons/icon-512x512.png", |
47 | "sizes": "512x512", |
48 | "type": "image/png" |
49 | } |
50 | ] |
51 | } |
52 | |
53 | // 上边是图标配置的一些信息,但是在Apple设备上会有问题,IOS在PWA的支持上目前还有点落后,为确保你的Web应用在IOS设备上也有一个完美的图标,将将以下代码加入到你项目的index.html的head的tag中。(PS:确保这些文件要存在于你的资产目录) |
54 | |
55 | <link rel="apple-touch-icon" href="/assets/icons/apple-touch-icon-iphone.png"/> |
56 | <link rel="apple-touch-icon" sizes="152x152" href="/assets/icons/apple-touch-icon-iphone.png"/> |
57 | <link rel="apple-touch-icon" sizes="167x167" href="/assets/icons/apple-touch-icon-ipad-retina.png"/> |
58 | <link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon-iphone-retina.png"/> |
app.module.ts
默认PWA是在生产模式才开启,如果你想在测试环境也开启的话,请手动修改这里。
1
import { BrowserModule } from '@angular/platform-browser';
2
import { NgModule } from '@angular/core';
3
4
import { AppRoutingModule } from './app-routing.module';
5
import { AppComponent } from './app.component';
6
import { ServiceWorkerModule } from '@angular/service-worker';
7
import { environment } from '../environments/environment';
8
9
@NgModule({
10
declarations: [
11
AppComponent
12
],
13
imports: [
14
BrowserModule,
15
AppRoutingModule,
16
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }) // <=== 手动修改这里,去掉enabled即可所有环境开启PWA
17
],
18
providers: [],
19
bootstrap: [AppComponent]
20
})
21
export class AppModule { }
- ngsw-config.json
顾名思义,是angular工程创建pwa的配置文件。
1 | { |
2 | "$schema": "./node_modules/@angular/service-worker/config/schema.json", |
3 | "index": "/index.html", |
4 | "assetGroups": [ // 资产组配置 |
5 | { |
6 | "name": "app", |
7 | "installMode": "prefetch", // 安装策略,默认拉取所有资源,好处是脱机状态下也能使用Web APP,另一种备用策略是:lazy,即按需安装。 |
8 | "resources": { |
9 | "files": [ |
10 | "/favicon.ico", |
11 | "/index.html", |
12 | "/manifest.webmanifest", |
13 | "/*.css", |
14 | "/*.js" |
15 | ] |
16 | } |
17 | }, { |
18 | "name": "assets", |
19 | "installMode": "lazy", // 资产缓存,默认是用lazy策略。 |
20 | "updateMode": "prefetch", // 使用lazy策略之后需要设置更新模式为:prefetch,这样有新的更新可以主动更新。 |
21 | "resources": { |
22 | "files": [ |
23 | "/assets/**", |
24 | "/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)" |
25 | ] |
26 | "urls": [ // 这里是使用外部服务器或者CDN的配置,比如这里我设置了使用Google字体 |
27 | "https://fonts.googleapis.com/**" |
28 | ] |
29 | } |
30 | } |
31 | ], |
32 | "dataGroups": [{ // 数据组配置,与资产组配置不同的是,这里的配置没有被打包在Web APP里,比如下边这个是使用了外部API。数组组配置支持两种策略:freshness 和 performance。freshness多用于经常更新的资源,即:始终尝试获取最新的版本资源,然后再回退到缓存里。performance策略是默认策略,对于变化不大的资源有用。 |
33 | "name": "api-freshness", |
34 | "urls": [ "https://my.apipage.com/user" ], // 这里的配置是:从/user接口拉取数据 |
35 | "cacheConfig": { // 缓存配置 |
36 | "strategy": "freshness", |
37 | "maxSize": 5, // 最多同时支持5个相应 |
38 | "maxAge": "1h", // 最多缓存一小时 |
39 | "timeout": "3s" // 超时时间是3秒 |
40 | } |
41 | } |
42 | ] |
43 | } |
5. PWA更新
PWA @angular/service-worker 中的 SwUpdate 提供更新检测,也就是说当用户正在使用Web APP或者网页版网站时,我们刚好部署了新版本,这个时候就可以使用SwUpdate的trigger机制,通知用户更新新版本。
那么如何实现这个功能呢?其实也很简单,我们创建一个服务,然后订阅这个服务就好了,当有版本更新的时候,PWA的服务会收到这个回调,我们在回调里处理我们的逻辑即可。话不多说,上代码啦~
- sw-updates.service.ts
1 | import { ApplicationRef, Injectable, OnDestroy } from '@angular/core'; |
2 | import { SwUpdate } from '@angular/service-worker'; |
3 | import { concat, interval, NEVER, Observable, Subject } from 'rxjs'; |
4 | import { first, map, takeUntil, tap } from 'rxjs/operators'; |
5 | |
6 | |
7 | /** |
8 | * SwUpdatesService |
9 | * |
10 | * @description |
11 | * 1. 实例化后检查可用的ServiceWorker更新. |
12 | * 2. 每6小时重新检查一次. |
13 | * 3. 只要有可用的更新, 就会激活更新. |
14 | * |
15 | * @propertys |
16 | * `updateActivated` {Observable<string>} - 每当激活更新时,发出版本哈希. |
17 | */ |
18 | @Injectable({ |
19 | providedIn: 'root' |
20 | }) |
21 | export class SwUpdatesService implements OnDestroy { |
22 | private checkInterval = 1000 * 60 * 60 * 6; // 6 小时 |
23 | private onDestroy = new Subject<void>(); |
24 | updateActivated: Observable<string>; |
25 | |
26 | constructor( |
27 | appRef: ApplicationRef, |
28 | private swu: SwUpdate |
29 | ) { |
30 | if (!swu.isEnabled) { |
31 | this.updateActivated = NEVER.pipe(takeUntil(this.onDestroy)); |
32 | return; |
33 | } |
34 | |
35 | // 定期检查更新(在应用稳定后). |
36 | const appIsStable = appRef.isStable.pipe(first(v => v)); |
37 | concat(appIsStable, interval(this.checkInterval)) |
38 | .pipe( |
39 | tap(() => this.log('Checking for update...')), |
40 | takeUntil(this.onDestroy), |
41 | ) |
42 | .subscribe(() => this.swu.checkForUpdate()); |
43 | |
44 | // 激活可用的更新. |
45 | this.swu.available |
46 | .pipe( |
47 | tap(evt => this.log(`Update available: ${JSON.stringify(evt)}`)), |
48 | takeUntil(this.onDestroy), |
49 | ) |
50 | .subscribe(() => this.swu.activateUpdate()); |
51 | |
52 | // 通知已激活的更新. |
53 | this.updateActivated = this.swu.activated.pipe( |
54 | tap(evt => this.log(`Update activated: ${JSON.stringify(evt)}`)), |
55 | map(evt => evt.current.hash), |
56 | takeUntil(this.onDestroy), |
57 | ); |
58 | } |
59 | |
60 | ngOnDestroy() { |
61 | this.onDestroy.next(); |
62 | } |
63 | |
64 | private log(message: string) { |
65 | const timestamp = new Date().toISOString(); |
66 | console.log(`[SwUpdates - ${timestamp}]: ${message}`); |
67 | } |
68 | } |
- app.component.ts
1 | import { Component, OnInit } from '@angular/core'; |
2 | import { SwUpdatesService } from './sw-updates.service'; |
3 | |
4 | @Component({ |
5 | selector: 'app-root', |
6 | templateUrl: './app.component.html', |
7 | styleUrls: ['./app.component.scss'] |
8 | }) |
9 | export class AppComponent implements OnInit { |
10 | title = 'pwa'; |
11 | |
12 | constructor( |
13 | private swUpdates: SwUpdatesService |
14 | ) { } |
15 | |
16 | ngOnInit(): void { |
17 | this.swUpdates.updateActivated.subscribe(_ => { |
18 | if (confirm('检测到版本更新,是否更新到最新版本?(╯#-_-)╯~~')) { |
19 | window.location.reload(); |
20 | } |
21 | }); |
22 | } |
23 | |
24 | } |
6. PWA消息推送
在PWA @angular/service-worker 的 SwPush 中,我们可以订阅并接收来着Service Worker的推送通知,当然我们需要借助服务器来实现这个机制,下边是简单的开发模式实现,后续有时间我再更新文章啦~
- 前端代码实现
1 | import { Component } from '@angular/core'; |
2 | import { SwPush } from '@angular/service-worker'; |
3 | |
4 | @Component({ |
5 | selector: 'app-root', |
6 | templateUrl: './app.component.html', |
7 | styleUrls: ['./app.component.scss'] |
8 | }) |
9 | export class AppComponent { |
10 | title = 'pwa'; |
11 | |
12 | constructor( |
13 | private swPush: SwPush |
14 | ) { |
15 | // 监听通知的点击事件 |
16 | this.swPush.notificationClicks.subscribe(event => { |
17 | console.log('消息推送: ', event); |
18 | const url = event.notification.data.url; |
19 | window.open(url, '_blank'); // 这里是点击推送的通知后跳转新页面 |
20 | }); |
21 | |
22 | } |
23 | |
24 | } |