更新中javascript
提示:在测试程序的时候尽可能使用Chrome的隐身模式,确保 Service Worker 不会从之前的残留状态中读取数据!!css
在sudo ng new pwa
新建工程以后,在工程的根目录上运行sudo ng add @angular/pwa
,此时就会自动添加Service Worker文件,Manifest.json文件和各类不一样尺寸的icon文件。 Angular PWA中文网传送门html
在app.component.ts
中引入import { SwUpdate } from '@angular/service-worker';
来加载SW的更新模块,每次PWA程序有更新均可以在这里使用SwUpdate模块获取更新,并使用以下代码可实现程序的更新操做:java
export class AppComponent {
update: boolean;
constructor(updates: SwUpdate, private data: DataService) {
updates.available.subscribe( event => {
this.update = true;
updates.activateUpdate().then(() =>
document.location.reload()
);
}
);
}
title = 'PWA';
}
复制代码
SwUpdate文档传送门node
而后在html中使用一个*ngIf
来判断是否更新,(是则显示text,不是则不显示):web
<span *ngIf="update">There's an update associated with your progressive web application!</span>
复制代码
每次更新了程序都要从新build production程序,在根目录上运行sudo ng build --prod
,而后进入cd dist/PWA
,最后运行http-server -o
在服务器上运行更新后的程序。npm
因为 ng serve
对 Service Worker
无效,因此必须用一个独立的 HTTP 服务器在本地测试项目。 可使用任何 HTTP 服务器,我使用的是来自 npm 中的 http-server 包。固然也能够自定义端口以防止port冲突:json
http-server -p 8080 -c-1 dist/<project-name>
复制代码
当使用http-server
打开服务器后,却没法正常打开网页的时候,我曾遇到过ERR_INVALID_REDIRECT
这样的问题致使没法正常显示网页。更换http-server
版本就能够解决这个问题: npm install -g http-server@0.9.0
。后端
注意: 若是想按期更新PWA,也就是使用interval建立一个周期轮询方法,须要先让应用注册Aervice worker的进程进入稳定状态,再让它开始执行轮询的过程,若是不断轮询更新(好比调用 interval())将阻止应用程序达到稳定态,也就永远不会往浏览器中注册 ServiceWorker 脚本。另外:应用中所执行的各类轮询都会阻止它达到稳定态api
constructor(appRef: ApplicationRef, updates: SwUpdate) {
// Allow the app to stabilize first, before starting polling for updates with `interval()`.
const appIsStable$ = appRef.isStable.pipe(first(isStable => isStable === true));
const everySixHours$ = interval(6 * 60 * 60 * 1000);
const everySixHoursOnceAppIsStable$ = concat(appIsStable$, everySixHours$);
everySixHoursOnceAppIsStable$.subscribe(() => updates.checkForUpdate());
}
复制代码
因此对于自动更新模块的使用总结:
constructor(appRef: ApplicationRef, updates: SwUpdate, private data: DataService) {
const appIsStable$ = appRef.isStable.pipe(first(isStable => isStable === true));
const everySixHours$ = interval(6 * 1000);
const everySixHoursOnceAppIsStable$ = concat(appIsStable$, everySixHours$);
everySixHoursOnceAppIsStable$.subscribe(() => {
updates.checkForUpdate();
// console.log('check update in Service Worker');
});
updates.available.subscribe(event => {
console.log('gotta new version here', event.available);
updates.activateUpdate().then(() => document.location.reload());
});
}
复制代码
每6秒检测一次更新版本,若是没有updates.activateUpdate().then(() => document.location.reload());
则只是在检测到新版本时候提醒并不刷新并更新程序。 测试的时候须要从新ng build --prod
而后http-server -p 8080 -c-1 dist/PWA
从新运行http服务器,这时候在原来的页面上的console上就会发现出现了新版本的提醒。
(其实每次运行build命令都会出现版本更新不管是否更改代码,当应用的一个新的构建发布时,Service Worker 就把它看作此应用的一个新版本,版本是由 ngsw.json 文件的内容决定的,包含了全部已知内容的哈希值。 若是任何一个被缓存的文件发生了变化,则该文件的哈希也将在ngsw.json中随之变化,从而致使 Angular Service Worker 将这个活动文件的集合视为一个新版本)
全在nsgw-config.json
文件中定义PWA缓存,好比想缓存google的Montserrat字体和API地址,该文件中全部的代码形式都是glob格式,也就是:
好比:
在实际代码中这样作:
<link href="https://fonts.googleapis.com/css?family=Montserrat" rel="stylesheet">
复制代码
在已经被建立的assetGroups
中添加:
"urls": [
"https://fonts.googleapis.com/**"
]
复制代码
AssetGroup遵循的TypeScript接口规则为:
interface AssetGroup {
name: string;
installMode?: 'prefetch' | 'lazy';
// prefetch 告诉 Angular Service Worker 在缓存当前版本的应用时要获取每个列出的资源。 这是个带宽密集型的模式,但能够确保这些资源在请求时可用,即便浏览器正处于离线状态
// lazy 不会预先缓存任何资源。相反,Angular Service Worker 只会缓存它收到请求的资源。 这是一种按需缓存模式。永远不会请求的资源也永远不会被缓存。 这对于像为不一样分辨率提供的图片之类的资源颇有用,那样 Service Worker 就只会为特定的屏幕和设备方向缓存正确的资源。
updateMode?: 'prefetch' | 'lazy';
// prefetch 会告诉 Service Worker 当即下载并缓存更新过的资源
// lazy 告诉 Service Worker 不要缓存这些资源,而是先把它们看做未被请求的,等到它们再次被请求时才进行更新。
lazy 这个 updateMode 只有在 installMode 也一样是 lazy 时才有效。
resources: {
files?: string[];
/** @deprecated As of v6 `versionedFiles` and `files` options have the same behavior. Use `files` instead. */
versionedFiles?: string[];
urls?: string[];
};
}
复制代码
在下方建立dataGroups
缓存API地址:
"dataGroups": [
{
"name": "jokes-api",
"urls": [
"https://api.chucknorris.io/jokes/random"
],
"cacheConfig": {
"strategy": "freshness",
"maxSize": 20,
"maxAge": "1h",
"timeout": "5s"
}
}
]
复制代码
dataGroups的配置遵循下面的接口:
export interface DataGroup {
name: string;
urls: string[];
version?: number;
cacheConfig: {
maxSize: number;
maxAge: string;
timeout?: string;
strategy?: 'freshness' | 'performance';
};
}
复制代码
其中的缓存设置中的几个项目分别是:
测试push notification API的功能没法在隐身模式下测试
npm install web-push -g
,而后建立VAPID key: web-push generate-vapid-keys --json
。得到相似以下的VAPID:{
"publicKey":"BApAO10ISTLAR1bWho_6f4yL5-5z2RWHgnkqzG7SB81WdcsLkDdxrc1iWwHZ49trIUFekIEFGyBjomxjuKDZGc8",
"privateKey":"7y1-NPiG_igcck_iIJ5sidurBa7ghC4Py0MTQPOFLGM"
}
复制代码
subscribeToNotifications() {
this.swPush.requestSubscription({
serverPublicKey: this.VAPID_PUBLIC_KEY
}) // 浏览器弹出消息请求,若是请求赞成会得到一个Promise
.then(sub => this.newsletterService.addPushSubscriber(sub).subscribe()) // 这里会得到一个PushSubscription object
.catch(err => console.error("Could not subscribe to notifications", err));
}
复制代码
PushSubscription object:
{
"endpoint": "https://fcm.googleapis.com/fcm/send/cbx2QC6AGbY:APA91bEjTzUxaBU7j-YN7ReiXV-MD-bmk2pGsp9ZVq4Jj0yuBOhFRrUS9pjz5FMnIvUenVqNpALTh5Hng7HRQpcUNQMFblTLTF7aw-yu1dGqhBOJ-U3IBfnw3hz9hq-TJ4K5f9fHLvjY",
"expirationTime": null,
"keys": {
"p256dh": "BOXYnlKnMkzlMc6xlIjD8OmqVh-YqswZdut2M7zoAspl1UkFeQgSLYZ7eKqKcx6xMsGK7aAguQbcG9FMmlDrDIA=",
"auth": "if-YFywyb4g-bFB1hO9WMw=="
}
}
复制代码
const notificationPayload = {
"notification": {
"title": "Angular News",
"body": "Newsletter Available!",
"icon": "assets/main-page-logo-small-hat.png",
"vibrate": [100, 50, 100],
"data": {
"dateOfArrival": Date.now(),
"primaryKey": 1
},
"actions": [{
"action": "explore",
"title": "Go to the site"
}]
}
};
复制代码
Promise.all(allSubscriptions.map(sub => webpush.sendNotification(
sub, JSON.stringify(notificationPayload) )))
.then(() => res.status(200).json({message: 'Newsletter sent successfully.'}))
.catch(err => {
console.error("Error sending notification, reason: ", err);
res.sendStatus(500);
});
复制代码
本身定义的话须要本身创建两个新的文件:sw-custom.js
和 sw-master.js
。
sw-custom.js
里面定义咱们想添加的listener,好比:(function () {
'use strict';
self.addEventListener('notificationclick', (event) => {
event.notification.close();
console.log('notification details: ', event.notification);
});
}());
复制代码
sw-master.js
用于将sw-custom.js
和ngsw-worker.js
两个Service Worker文件结合,不丢失原有功能:importScripts('./ngsw-worker.js');
importScripts('./sw-custom.js');
复制代码
文件创建好了以后须要让Angular在初始化渲染的时候可以将咱们自定义的文件包含进去,因此在angular.json
文件中的assets部分添加"src/sw-master.js"
。最后在app.module.ts
中注册service worker的地方注册咱们的新service worker文件:
ServiceWorkerModule.register('/sw-master.js', { enabled: environment.production })
复制代码
烦人的一点:由于Angular封装好的 ngsw-worker.js 只能在 ng build --prod以后建立的dist文件夹中,因此不build就无法使用service worker。这就是为何测试PWA的Service Worker功能时候无法在ng serve上运行。因此若是咱们在测试本身的功能的时候,为了不麻烦能够将ServiceWorkerModule.register('/sw-master.js', { enabled: environment.production })
注释掉,换成ServiceWorkerModule.register('sw-custom.js')
,这时候咱们无法使用原来的那些Angular封装好的功能可是能够测试咱们本身的listener。
解决方法: 在environment.ts
和environment.prod.ts
文件中添加新的环境变量,这样就能够在不同的环境下运行不一样的Service Worker依赖包。
environment.ts
中添加: serviceWorkerScript: 'sw-custom.js'
environment.prod.ts
中添加: serviceWorkerScript: 'sw-master.js'
ServiceWorkerModule.register(environment.serviceWorkerScript)