Background Fetch API

Background Fetch API 提供了一种管理可能需要大量时间的下载的方法,例如电影、音频文件和软件等。

其提供了一种让浏览器在后台执行某些获取的方法。然后,浏览器以用户可见的方式执行提取,向用户显示进度并为他们提供取消下载的方法。下载完成后,浏览器就会在 ServiceWorker 触发相关事件,此时应用程序可以根据需要对响应执行某些操作。

如果用户在离线状态下启动进程,后台获取 API 将启用。一旦网络连接,该过程就会开始。如果网络离线,该过程将暂停,直到用户再次上线。

该 API 通过 BackgroundFetchManager 接口提供,并基于 ServiceWorkerRegistration 接口的 backgroundFetch 属性向开发者暴露。

发起 Background Fetch

BackgroundFetchManager 接口的 fetch() 方法用于注册一条后台获取。

方法接收一个字符串参数作为该后台获取的 ID;

然后接收一个 Request 或者一组 Request,可以是代表 URL 的字符串(会被传递给 Request 构造函数)或者 Request 实例;

最后接收一组可选的配置项,用于配置浏览器向用户展示的获取进度条对话框:

配置项的 title 参数指定对话框的标题;

配置项的 icons 参数指定对话框的一组图标,浏览器会从中选择一个图标用于对话框的展示:每个图标的 src 参数指定图标路径、sizes 参数指定图标的大小(格式同 link 标签的 sizes 属性的格式相同)、type 参数指定图标的 MIME 类型、label 参数指定图标的名称;

配置项的 downloadTotal 参数指定预计的获取资源总大小(字节),若实际获取资源总大小超出该数值,获取会终止;

方法返回一个 Promise 的 BackgroundFetchRegistration 实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const ID = 'fetch'

window.navigator.serviceWorker.ready.then((registration) => {
registration.backgroundFetch.fetch(
ID,
["/ep-5.mp3", "ep-5-artwork.jpg"],
{
title: "Episode 5: Interesting things.",
icons: [
{
sizes: "300x300",
src: "/ep-5-icon.png",
type: "image/png",
label: "ep-icon",
},
],
downloadTotal: 60 * 1024 * 1024,
},
)
})

Background Fetch 信息

可以使用 BackgroundFetchManager 接口的 get() 方法根据给定的 ID 获取对应的 Background Fetch。 若存在,方法返回一个 Promise 的 BackgroundFetchRegistration 接口实例,否则返回一个 Promise 的 undefined

此外,可以使用 BackgroundFetchManager 接口的 getIds() 方法获取当前所有 Background Fetch 的 ID 列表,返回一个 Promise 的字符串数组。

BackgroundFetchRegistration 接口用于表示后台获取的实时信息,以及一些控制方法。

BackgroundFetchRegistration 接口的 id 属性表示后台获取的 ID。

BackgroundFetchRegistration 接口的 downloaded 属性表示后台获取已下载资源的大小,初始值为 0。

BackgroundFetchRegistration 接口的 downloadTotal 属性表示后台获取将下载资源的总大小,该值在初始化时设置,若未设置则为 0。

BackgroundFetchRegistration 接口的 uploaded 属性表示后台获取已成功发送内容的大小,初始值为 0。

BackgroundFetchRegistration 接口的 uploadTotal 属性表示后台获取将发送内容的总大小。

BackgroundFetchRegistration 接口的 recordsAvailable 属性表示当前是否有可以获取的请求及响应,该值同样用于表示是否可以调用 match()matchAll()

BackgroundFetchRegistration 接口的 result 属性表示后台获取是否成功,可能的值为 '' success failure

BackgroundFetchRegistration 接口的 failureReason 属性表示后台获取错误的原因,可能的值为 '' 'aborted' 'bad-status' 'fetch-error' 'quota-exceeded' 'download-total-exceeded'

BackgroundFetchRegistration 接口的 match() 方法用于匹配当前后台获取中的后台请求。返回一个 Promise 的 BackgroundFetchRecord 接口实例或 undefined,表示首个匹配。

BackgroundFetchRegistration 接口的 matchAll() 方法用于匹配当前后台获取中的后台请求。返回一个 Promise 的 BackgroundFetchRecord 接口实例数组,表示所有的匹配。

两方法支持传入一个请求实例 Request 或 URL 或路径字符串,同时支持传入一个可选的配置项,ignoreSearch 参数指定是否忽略搜索参数,ignoreMethod 参数指定是否忽略请求方法,ignoreVary 参数指定是否忽略 Vary 响应头。

BackgroundFetchRecord 接口用于表示单个后台请求及响应信息。

BackgroundFetchRecord 接口的 request 属性表示请求信息,返回一个 Request

BackgroundFetchRecord 接口的 responseReady 属性表示响应信息,返回一个 Promise 的 Response

BackgroundFetchRegistration 接口的 progress 事件在当前后台获取的信息更新时触发,包括 downloaded 属性、uploaded 属性、 result 属性、failureReason 属性,事件只抛出一个普通的 Event 事件。

注销 Background Fetch

BackgroundFetchRegistration 接口的 abort() 方法用于终止当前后台获取。返回一个 Promise 的 boolean,表示是否终止成功。

1
2
3
4
5
6
7
const ID = 'fetch'

window.navigator.serviceWorker.ready.then((registration) => {
registration.backgroundFetch.get(ID).then((registration) => {
registration.abort()
})
})

Background Fetch 结束处理

ServiceWorkerGlobalScope 接口的 backgroundfetchclick 事件在用户点击浏览器提供的下载进度条弹出框时触发。返回一个 BackgroundFetchEvent 事件。

ServiceWorkerGlobalScope 接口的 backgroundfetchabort 事件在后台获取被取消时触发。返回一个 BackgroundFetchEvent 事件。

ServiceWorkerGlobalScope 接口的 backgroundfetchfail 事件在后台获取失败时触发,即至少有一个后台获取内的网络请求失败。返回一个 BackgroundFetchUpdateUIEvent 事件。

ServiceWorkerGlobalScope 接口的 backgroundfetchsuccess 事件在后台获取完成时触发,此时所有后台获取内的网络请求已经完成。返回一个 BackgroundFetchUpdateUIEvent 事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
self.addEventListener('backgroundfetchsuccess', (e) => {
e.waitUntil(() =>
self.caches.open('movies').then((cache) =>
e.registration.matchAll().then((records) =>
Promise.all(
records.map((record) =>
record.responseReady.then((response) =>
cache.put(record.request, response)
)
)
)
)
).then(() =>
e.updateUI({
title: 'Move download complete',
})
)
)
})

self.addEventListener('backgroundfetchfail', (e) => {
e.waitUntil(() =>
self.caches.open('movies').then((cache) =>
e.registration.recordsAvailable && e.registration.matchAll().then((records) =>
Promise.all(
records.map((record) =>
record.responseReady.then((response) =>
cache.put(record.request, response)
)
)
)
)
).then(() =>
e.updateUI({
title: 'Download Fail',
})
)
)
})

self.addEventListener('backgroundfetchabort', (e) => {
e.waitUntil(() =>
self.caches.open('movies').then((cache) =>
e.registration.recordsAvailable && e.registration.matchAll().then((records) =>
Promise.all(
records.map((record) =>
record.responseReady.then((response) =>
cache.put(record.request, response)
)
)
)
)
)
)
})

self.addEventListener('backgroundfetchclick', (e) => {
if (e.registration.result === 'success') {
self.clients.openWindow('/play-movie');
} else {
self.clients.openWindow('/movie-download-progress');
}
})

BackgroundFetchEvent 接口继承自 ExtendableEvent 接口,其 registration 属性代表与之对应的 BackgroundFetchRegistration 实例。

BackgroundFetchUpdateUIEvent 接口继承自 BackgroundFetchEvent 接口,其 updateUI() 方法用于更新浏览器提供的下载进度条弹出框的信息。接收一组参数,包括 iconstitle 参数,与 BackgroundFetchManager 接口的 fetch() 方法中的相应参数相同。返回一个 Promise。

权限 API

该 API 调用需要用户授予 background-fetch 权限,可以调用 Permission.query() 方法检查用户是否已授予了该权限

示例

类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
type BackgroundFetchFailureReason = "" | "aborted" | "bad-status" | "fetch-error" | "quota-exceeded" | "download-total-exceeded";
type BackgroundFetchResult = "" | "success" | "failure";

interface BackgroundFetchEvent extends ExtendableEvent {
readonly registration: BackgroundFetchRegistration;
}

interface BackgroundFetchUpdateUIEvent extends BackgroundFetchEvent {
updateUI(options?: BackgroundFetchUIOptions);
}

interface BackgroundFetchManager {
fetch(id: string, requests: RequestInfo | RequestInfo[], options?: BackgroundFetchOptions): Promise<BackgroundFetchRegistration>;
get(id: string): Promise<BackgroundFetchRegistration | undefined>;
getIds(): Promise<ReadonlyArray<string>>;
}

interface BackgroundFetchOptions extends BackgroundFetchUIOptions {
downloadTotal: number;
}

interface BackgroundFetchRecord {
readonly request: Request;
readonly responseReady: Promise<Response>;
}

interface BackgroundFetchRegistration extends EventTarget {
abort(): Promise<boolean>;
readonly downloaded: number;
readonly downloadTotal: number;
readonly failureReason: BackgroundFetchFailureReason;
readonly id: string;
match(request: RequestInfo, options?: CacheQueryOptions): Promise<BackgroundFetchRecord | undefined>;
matchAll(request?: RequestInfo, options?: CacheQueryOptions): Promise<BackgroundFetchRecord[]>;
readonly recordsAvailable: boolean;
readonly result: BackgroundFetchResult;
readonly uploaded: number;
readonly uploadTotal: number;
onprogress: ((this: BackgroundFetchRegistration, ev: Event) => any) | null;
}

interface BackgroundFetchUIOptions {
icons: ReadonlyArray<ImageResource>;
title: string;
}

interface ImageResource {
src: string;
sizes: string;
type: string;
label: string;
}

interface ServiceWorkerRegistration extends EventTarget {
readonly backgroundFetch: BackgroundFetchManager;
}

interface ServiceWorkerGlobalScope extends WorkerGlobalScope {
onbackgroundfetchabort: ((this: ServiceWorkerGlobalScope, ev: BackgroundFetchUpdateUIEvent) => any) | null;
onbackgroundfetchclic: ((this: ServiceWorkerGlobalScope, ev: BackgroundFetchUpdateUIEvent) => any) | null;
onbackgroundfetchfail: ((this: ServiceWorkerGlobalScope, ev: BackgroundFetchUpdateUIEvent) => any) | null;
onbackgroundfetchsuccess: ((this: ServiceWorkerGlobalScope, ev: BackgroundFetchUpdateUIEvent) => any) | null;
}

链接

Web Background Synchronization API

Web Background Synchronization API 用于同步创建任务,直至用户获取到稳定的网络连接时才开始按序执行。

该 API 可以有很多应用场景:

  • 离线数据同步
  • 数据备份
  • 数据恢复
  • 数据同步

消息同步服务通过 SyncManager 接口提供,并基于 ServiceWorkerRegistration 接口的 sync 属性向开发者暴露。

注册消息同步

SyncManager 接口的 register() 方法用于注册一个消息同步事件,网络连接变为正常状态后在对应的 ServiceWorker 中触发 sync 事件。

方法接受一个字符串,代表消息同步事件的标识符,该标识符将会传递给 SyncEventtag 属性。

方法返回一个 Promise 的 undefined

在网页中:

1
2
3
4
5
6
7
8
9
const TAG = 'sync'

window.navigator.serviceWorker.ready.then((registration) => {
return registration.sync.getTags().then((tags) => {
if (!tags.includes(TAG)) {
return registration.sync.register(TAG)
}
})
})

在 ServiceWorker 中:

1
2
3
const TAG = 'sync'

self.registration.sync.register(TAG)

监听消息同步

ServiceWorkerGlobalScope 接口上的 sync 事件在 Page 或 Worker 调用 SyncManager 接口上的方法注册一个同步事件并且自注册后起网络连接处于正常状态时触发。返回一个 SyncEvent 事件。

若当前网络连接处于非正常状态,注册同步事件后,sync 事件不会马上触发,直至网络连接变为正常状态,才会触发 sync 事件。

换言之,触发 sync 事件时,网络连接一定处于正常状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const TAG = 'sync'

self.addEventListener('sync', (e) => {
if (e.lastChance && e.tag === TAG) {
e.waitUntil(sync())
}
})

function sync() {
// sync data in the background

// for example - fetch new data and re-cache new data
self.fetch('/sync').then(data => {
self.caches.open('v1').then(cache => {
cache.add('/sync', data)
})
})
}

其他

SyncManager 接口的 getTags() 方法用于获取用户定义的同步事件标识符,返回一个 Promise 的字符串数组。

可以利用该方法判断是否已注册相关的同步事件。

SyncEvent 事件继承自 ExtendableEvent 事件,其 tag 属性给出定义的同步事件标识符,其 lastChance 属性标识当前同步事件后是否有新的同步事件。

权限 API

该 API 调用需要用户授予 background-sync 权限,可以调用 Permission.query() 方法检查用户是否已授予了该权限

示例

类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface SyncEvent extends ExtendableEvent {
readonly tag: string;
readonly lastChance: boolean;
}

interface SyncManager {
getTags(): Promise<ReadonlyArray<string>>;
register(tag: string): Promise<void>;
}

interface ServiceWorkerGlobalScope extends WorkerGlobalScope {
onsync: ((this: ServiceWorkerGlobalScope, ev: SyncEvent) => any) | null;
}

interface ServiceWorkerRegistration extends EventTarget {
readonly sync: SyncManager;
}

链接

Push API

Push API 让网络应用从用户代理接收来自服务器发送的消息,无论网络应用是否运行或者在线

网页或者浏览器不在线的时候,推送消息无法被推送到客户端浏览器,此时推送消息就会被 FCM 服务器保存起来,等到网页或者浏览器上线的时候,FCM 服务器才会推送消息到网页或者浏览器

生成服务器公秘钥对

可以使用 web-push 库来生成服务器公秘钥对。

1
2
3
4
5
const webpush = require('web-push');

const vapidKeys = webpush.generateVAPIDKeys();

const { publicKey, privateKey } = vapidKeys

其中私钥放在服务器保存,公钥用于注册推送订阅。

创建消息推送服务

可以利用 ServiceWorkerRegistration 接口的 pushManager 属性获取到当前 ServiceWorker 对应的 PushManager 接口实例。

PushManager 接口的 subscribe() 方法用于订阅一个消息推送服务。

其接收一组可选的配置项,userVisibleOnly 可选参数指定返回的推送是否只用于创建对用户可见的通知(不指定 true 会在一些浏览器中报错),applicationServerKey 可选参数指定服务的公钥(某些浏览器中是必须的参数)。

返回一个 Promise 的 PushSubscription 接口实例,代表当前推送消息。

在当前 ServiceWorker 没有创建消息推送服务时,新的消息推送会被创建。

1
2
3
4
5
6
7
8
9
window.navigator.serviceWorker.ready.then((registration) => {
// 订阅消息推送
registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: '',
}).then((subscription) => {
console.log(subscription)
})
})
1
2
3
4
5
6
self.registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: '',
}).then((subscription) => {
console.log(subscription)
})

获取推送消息服务

PushManager 接口的 getSubscription() 方法用于获取已经订阅的消息推送服务。

返回一个 Promise 的 PushSubscription 接口实例,代表当前已订阅的消息推送服务,否则返回一个 Promise 的 null。

1
2
3
4
5
6
7
8
window.navigator.serviceWorker.ready.then((registration) => {
// 获取已订阅的消息推送
registration.pushManager.getSubscription().then((subscription) => {
if (subscription != null) {
console.log(subscription)
}
})
})
1
2
3
4
5
self.registration.pushManager.getSubscription().then((subscription) => {
if (subscription != null) {
console.log(subscription)
}
})

PushSubscription 接口代表了订阅的消息推送。

endpoint 属性代表与消息推送联系的 endpoint。

expirationTime 属性代表订阅的消息推送的到期时间。

options 属性代表创建消息推送时的选项。

getKey() 方法返回一个 ArrayBuffer,代表客户端公钥的密钥,可以将其发送到服务器用于加密推送消息数据。支持传入一个代表用于生成客户端密钥的加密方法的参数,可以是 p256dh 或 auth。

toJSON() 方法标准化地转换消息推送的信息,目前仅包含 endpoint 参数。

unsubscribe() 方法取消消息推送的订阅,返回一个 Promise 的布尔值,代表是否成功取消了消息订阅。

发送推送消息

可以使用 web-push 在服务端来发送推送消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import express from 'express'
import webpush from 'web-push'

const vapidKeys = {
"publicKey": "BHXrxJPYpQSwGMwcN-HprCaU_Po9POIUvqWFLFq9UUNHP5SNJKxk_Io59y8_twMTOuB5SbpbcPBwHFo2kBUj7vQ", // 之前生成的 publicKey
"privateKey": "Yhd4XF08Efh8HNF_8RDJ9VL6pF-Gos-3KOmgyMEUSf8" // 之前生成的 privateKey
}

// 在 google cloud platform 中创建的项目 ID
webpush.setGCMAPIKey('<Your GCM API Key Here>')

// 服务器运营者的联系邮箱
webpush.setVapidDetails(
'mailto:example@yourdomain.org',
vapidKeys.publicKey,
vapidKeys.privateKey
)

const app = express()

app.get('/push', (req, res) => {
// 这里的 pushSubscription 就是上面注册成功后返回的 subscription 对象
const pushSubscription = {
"endpoint": "https://fcm.googleapis.com/fcm/send/cSAH1Q7Fa6s:APA91bEgYeKNXMSO1rcGAOPzt3L9fMhyjL-zSPV5JfiKwgqtbx_Q4de_8plEY_QViLnhfe6-0fUgdo7Z3Gqpml3zIBSfO6IISDYdF9kzL2h_dbZ_FE_YKbKOG70gMG_A74xwK1vsocCv", // 推送订阅网址
"keys": {
"p256dh": "BAqZaMLZn_rtYeR7WsBLqBWG7uMiOGRyCx2uhOqm0ZaJwDdQac-ubAyRRdLXJVZDOrNe-B3mCTy3g0vHCkeyYyo", // 用户公钥
"auth": "fxDt8RtB92KHpQM7HetBUw" // 用户身份验证秘密
}
}

webpush.sendNotification(pushSubscription, 'Hello world')
.then(result => {
res.send(result);
})
})

app.listen(1701, () => {
console.log('Server start success at http://localhost:1701');
})

若使用 firebase 架构可使用 firebase 来实现消息推送。

监听消息推送

ServiceWorkerGlobalScope 接口的 push 事件在每次收到一条推送消息时触发。

返回一个 PushEvent 事件。

1
2
3
4
5
self.addEventListener('push', (e) => {
console.log(e)

self.registration.showNotification(e.data?.json().title ?? 'New Notification')
})

PushEvent 接口继承自 ExtendableEvent 接口。

其 data 属性代表该推送消息的内容,是一个 PushMessageData 实例。

PushMessageData 接口包括多种处理推送的消息的方法,类似于 fetch API 中的方法,但允许被调用多次。
arrayBuffer() 方法、blob() 方法、json() 方法、text() 方法分别将结果转换成 ArrayBuffer、Blob、JSON 解析结果、字符串。
推送的消息能够自动被加解密,无需做额外的处理。

消息推送权限

PushManager 接口的 permissionState() 方法用于获取当前的请求消息推送权限。

参数同 subscribe 方法的参数。

返回一个 Promise 的 'prompt''denied''granted' 的字符串枚举。

1
2
3
4
5
6
7
8
9
10
11
12
13
self.registration.pushManager.permissionState({
userVisibleOnly: true,
applicationServerKey: '',
}).then((state) => {
switch(state) {
case "denied":
break
case "granted":
break
case "prompt":
break
}
})

其他

PushManager 接口的 supportedContentEncodings 静态属性返回一组消息推送支持的加密方式。

ServiceWorkerGlobalScope 接口的 pushsubscriptionchange 事件在更新订阅的消息推送时触发(可能原因包括消息推送服务刷新、消息推送服务失效等)。

权限 API

该 API 调用需要用户授予 push 权限,可以调用 Permission.query() 方法或 PushManager.permissionState() 检查用户是否已授予了该权限

示例

类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
type PushEncryptionKeyName = "auth" | "p256dh";

interface PushSubscriptionJSON {
endpoint?: string;
expirationTime?: EpochTimeStamp | null;
keys?: Record<string, string>;
}

interface PushSubscriptionOptionsInit {
applicationServerKey?: BufferSource | string | null;
userVisibleOnly?: boolean;
}

interface PushEvent extends ExtendableEvent {
readonly data: PushMessageData | null;
}

interface PushManager {
getSubscription(): Promise<PushSubscription | null>;
permissionState(options?: PushSubscriptionOptionsInit): Promise<PermissionState>;
subscribe(options?: PushSubscriptionOptionsInit): Promise<PushSubscription>;
}

interface PushMessageData {
arrayBuffer(): ArrayBuffer;
blob(): Blob;
json(): any;
text(): string;
}

interface PushSubscription {
readonly endpoint: string;
readonly expirationTime: EpochTimeStamp | null;
readonly options: PushSubscriptionOptions;
getKey(name: PushEncryptionKeyName): ArrayBuffer | null;
toJSON(): PushSubscriptionJSON;
unsubscribe(): Promise<boolean>;
}

interface PushSubscriptionOptions {
readonly applicationServerKey: ArrayBuffer | null;
readonly userVisibleOnly: boolean;
}

interface ServiceWorkerRegistration extends EventTarget {
readonly pushManager: PushManager;
}

interface ServiceWorkerGlobalScope extends WorkerGlobalScope {
onpush: ((this: ServiceWorkerGlobalScope, ev: PushEvent) => any) | null;
onpushsubscriptionchange: ((this: ServiceWorkerGlobalScope, ev: Event) => any) | null;
}

链接

Notifications API

Notifications API 用于在网页端使用系统通知功能

创建通知

ServiceWorkerRegistration 接口的 showNotification() 方法用于在对应的 Service Worker 上创建一条(系统)通知,该通知的相关操作会在对应的 Service Worker 全局上下文上触发相应的事件。

该方法接受一个字符串作为通知的标题,并接受一组配置项作为通知的选项,相关参数类似 Notification() 构造函数的选项;返回一个无参的 Promise。

浏览器环境中可以利用 navigator.serviceWorker.ready 等属性或方法获取到 ServiceWorkerRegistration 实例以创建通知。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
window.navigator.serviceWorker.ready.then((registration) => {
registration.showNotification('Hello', {
body: 'this is a notification',
icon: '<url>',
actions: [
{
title: 'Yes',
action: 'Yes',
},
{
title: 'No',
action: 'No',
},
],
})
})

Service Worker 环境可以利用 self.registration 属性获取到当前 Service Worker 对应的 ServiceWorkerRegistration 实例以创建通知。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
self.registration.showNotification('Hello', {
body: 'this is a notification',
icon: '<url>',
actions: [
{
title: 'Yes',
action: 'Yes',
},
{
title: 'No',
action: 'No',
},
],
})

如果未使用 Service Worker,可以在浏览器环境中直接使用 Notification() 构造函数来创建通知

1
const notificaion = new Notification('notification')

需要注意的是,ServiceWorker 环境内无法调用 Notification() 构造函数来创建通知。

请求通知权限

需要注意的是,创建一条 Notification 需要用户授予通知权限,可以使用 Notification.permission 属性检测用户是否授予了通知权限,并使用 Notification.requestPermission() 静态方法向用户请求通知权限。

1
2
3
4
5
6
7
8
9
if (Notification.permission === 'granted') {
// just go to create the notification
} else if (Notification.permission !== 'denied') {
Notification.requestPermission().then((result) => {
if (result === 'granted') {
// then go to create the notification
}
})
}

获取通知

ServiceWorkerRegistration 接口的 getNotifications() 方法用于获取在对应的 Service Worker 上创建的(系统)通知。

该方法支持传入一组筛选项,其仅支持 tag 参数,以筛选返回结果的通知;该方法返回一个 Promise 的 Notification 列表

可以使用该方法获取到通知再进行修改。

1
2
3
4
5
6
7
window.navigator.serviceWorker.ready.then((registration) => {
registration.getNotifications({
tag: 'tag',
}).then((notifications) => {
// use notifications to do something
})
})
1
2
3
4
5
self.registration.getNotifications({
tag: 'tag',
}).then((notifications) => {
//
})

通知处理

当与当前 Service Worker 对应的通知被点击时,在 Service Worker 全局触发 notificationclick 事件。

1
2
3
4
self.addEventListener('notificationclick', (e) => {
// 返回一个 NotificationEvent 事件对象
console.log('Notification click', e)
})

当与当前 Service Worker 对应的通知被关闭时,在 Service Worker 全局触发 notificationclose 事件。

1
2
3
4
self.addEventListener('notificationclose', (e) => {
// 返回一个 NotificationEvent 事件对象
console.log('Notification close', e)
})

NotificationEvent 接口继承自 ExtendableEvent 接口

  • notification 属性代表触发事件的 Notification 实例

  • action 顺序代表触发事件的 action 的 ID

权限 API

该 API 调用需要用户授予 notifications 权限,可以调用 Permission.query() 方法或读取 Notification.permission 属性检查用户是否已授予了该权限

示例

类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
interface NotificationAction {
action: string;
icon?: string;
title: string;
}

interface NotificationOptions {
actions?: NotificationAction[];
badge?: string;
body?: string;
data?: any;
dir?: NotificationDirection;
icon?: string;
image?: string;
lang?: string;
renotify?: boolean;
requireInteraction?: boolean;
silent?: boolean | null;
tag?: string;
timestamp?: EpochTimeStamp;
vibrate?: VibratePattern;
}

interface NotificationEvent extends ExtendableEvent {
readonly action: string;
readonly notification: Notification;
}

interface Notification extends EventTarget {
constructor(title: string, options?: NotificationOptions);
static readonly permission: NotificationPermission;
readonly body: string;
readonly data: any;
readonly dir: NotificationDirection;
readonly icon: string;
readonly lang: string;
onclick: ((this: Notification, ev: Event) => any) | null;
onclose: ((this: Notification, ev: Event) => any) | null;
onerror: ((this: Notification, ev: Event) => any) | null;
onshow: ((this: Notification, ev: Event) => any) | null;
readonly silent: boolean | null;
readonly tag: string;
readonly title: string;
close(): void;
}

链接

ServiceWorker V

ServiceWorker 的缓存策略是基于 CacheStorage 实现的。

CacheStorage

CacheStorage 提供了可由 ServiceWorker 或其他类型的 Worker 或 window 范围访问的所有命名缓存的主目录,同时负责维护字符串名称到相应 Cache 实例的映射。

可以通过 self.caches 访问全局的 CacheStorage 实例。

  • CacheStorage 接口的 open() 方法根据指定的 cacheName 获取对应的 Cache 实例,返回一个 Promise<Cache>,表示对应的 Cache 实例。若对应的 Cache 实例不存在,则会创建新的 Cache 实例。
1
2
3
4
5
6
const STORE_NAME = 'key'

self.caches.open(STORE_NAME).then((cache) => {
// 返回一个 Cache 实例
console.log('worker | cache', cache)
})
  • CacheStorage 接口的 has() 方法根据指定的 cacheName 检测是否存在对应的 Cache 实例,返回一个 Promise<boolean>,表示是否存在对应的 Cache 实例。
1
2
3
4
5
6
const STORE_NAME = 'key'

self.caches.has(STORE_NAME).then((has) => {
// 返回一个 boolean
console.log('worker | has cache', has)
})
  • CacheStorage 接口的 delete() 方法根据指定的 cacheName 移除对应的 Cache 实例,返回一个 Promise<boolean>,表示是否存在对应的 Cache 实例并且已完成删除操作。
1
2
3
4
5
6
const STORE_NAME = 'key'

self.caches.delete(STORE_NAME).then((deleted) => {
// 返回一个 boolean
console.log('worker | has deleted cache', deleted)
})
  • CacheStorage 接口的 keys() 方法获取所有 Cache 实例的索引的列表,返回一个 Promise<string[]>
1
2
3
4
self.caches.keys().then((keys) => {
// 返回一个 string[]
console.log('worker | all caches keys', keys)
})
  • CacheStorage 接口的 match() 方法根据给定的 Request 实例或 URL 实例或 URL 字符串确定存储中是否存在对应的 Response,若存在则返回一个 Promise<Response> ,反之则返回一个 Promise<undefined>。该方法支持传入一组配置项,cacheName 参数指定搜索目标 Cache 实例的索引,ignoreSearch 参数指定是否考虑 URL 中的查询字符串,ignoreMethod 参数指定是否匹配请求方法,ignoreVary 指定是否匹配 Vary 头。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
self.caches.match(
'/cache',
{
ignoreSearch: false,
ignoreMethod: false,
ignoreVary: false,
}
).then((data) => {
// 返回一个 Response | undefined
if (data) {
console.log('worker | have resource', data)
} else {
console.log('worker | not have resource', data)
}
})

self.caches.match(new URL('/cache'))

self.caches.match(new Request('/cache'))

Cache

  • Cache 接口的 put() 方法将键/值对存储到当前 Cache 实例中,键可以是一个代表 URL 的字符串、一个 URL 实例或一个 Request 实例,值是一个 Response 实例。该方法会覆盖与之匹配的键/值对。
1
2
3
4
5
self.caches.open('key').then((cache) => {
cache.put('/cache', new Response('cache'))
cache.put(new URL('/cache'), new Response('cache'))
cache.put(new Request('/cache'), new Response('cache'))
})
  • Cache 接口的 add() 方法根据给定的 URL 获取响应并存储到当前 Cache 实例中,参数可以是一个代表 URL 的字符串、一个 URL 实例或一个 Request 实例。该方法会覆盖与之匹配的键/值对。
  • Cache 接口的 addAll() 方法根据给定的 URL 列表获取响应并存储到当前 Cache 实例中,列表项可以是一个代表 URL 的字符串或一个 Request 实例。该方法会覆盖与之匹配的键/值对。
1
2
3
4
5
6
7
self.caches.open('key').then((cache) => {
cache.add('/cache')
cache.add(new URL('/cache'))
cache.add(new Request('/cache'))

cache.addAll(['/cache', new Request('/cache')])
})

该方法可以视为 put() 方法的简化用法。

add() 方法和 addAll() 方法会忽略非 200 状态码的响应,若仍需要存储响应,应当采用 put() 方法。

put() 方法、add() 方法和 addAll() 方法的 URL 参数必须是 HTTP 或 HTTPS,否则会抛出 TypeError 错误。

  • Cache 接口的 match() 方法根据给定的 URL 在当前 Cache 实例中检索首个关联的响应,参数可以是一个代表 URL 的字符串、一个 URL 实例或一个 Request 实例,此外还支持传入一组可选的配置项,若存在则返回一个 Promise<Response> ,反之则返回一个 Promise<undefined>
  • Cache 接口的 matchAll() 方法根据给定的 URL 在当前 Cache 实例中检索所有关联的响应,参数可以是一个代表 URL 的字符串、一个 URL 实例或一个 Request 实例,此外还支持传入一组可选的配置项,返回一个 Promise<Response[]>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
self.caches.open('key').then((cache) => {
cache.match(
'/cache',
{
ignoreSearch: false,
ignoreMethod: false,
ignoreVary: false,
}).then((response) => {
console.log('worker | cache match', response)
})
cache.match(new URL('/cache')).then((response) => {
console.log('worker | cache match', response)
})
cache.match(new Request('/cache')).then((response) => {
console.log('worker | cache match', response)
})

cache.matchAll(
'/cache',
{
ignoreSearch: false,
ignoreMethod: false,
ignoreVary: false,
}
).then((responses) => {
console.log('worker | caches match', responses)
})
cache.matchAll(new URL('/cache')).then((responses) => {
console.log('worker | caches match', responses)
})
cache.matchAll(new Request('/cache')).then((responses) => {
console.log('worker | caches match', responses)
})
})
  • Cache 接口的 delete() 方法从当前 Cache 实例中移除对应的响应。参数可以是一个代表 URL 的字符串、一个 URL 实例或一个 Request 实例,此外还支持传入一组可选的配置项。返回一个 Promise<boolean>,表示是否存在对应的响应并且已完成删除操作。
1
2
3
4
5
6
7
8
9
10
11
self.caches.open('key').then((cache) => {
cache.delete('/cache').then((success) => {
console.log('worker | cache delete', success)
})
cache.delete(new URL('/cache')).then((success) => {
console.log('worker | cache delete', success)
})
cache.delete(new Request('/cache')).then((success) => {
console.log('worker | cache delete', success)
})
})
  • Cache 接口的 keys() 方法获取当前 Cache 实例的响应索引的列表,参数可以是一个代表 URL 的字符串、一个 URL 实例或一个 Request 实例,同时支持传入一组配置项,返回一个 Promise<string[]>
1
2
3
4
5
self.caches.open('key').then((cache) => {
cache.keys().then((keys) => {
console.log('worker | all keys in cache', keys)
})
})

keys() 方法的返回值按照插入的顺序返回。

缓存机制

ServiceWorker 的缓存策略是基于 ServiceWorker 环境全局 fetch 事件的,可以在 fetch 事件中监听请求,然后对请求进行拦截,最后返回自定义的响应

拦截请求并从缓存检索响应,若响应存在则直接返回缓存的响应,反之则发起请求获取响应,缓存获取到的响应再返回响应。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
self.addEventListener('fetch', (e) => {
e.respondWith(
self.caches
.match(e.request, {
cacheName: 'v2',
})
.then((response) => {
if (response != null) {
return response
} else {
return fetch(e.request.clone())
.then((response) => {
const res = response.clone()

self.caches.open('v2').then((cache) => {
cache.put(e.request, res)
})

return response
})
.catch(() => caches.match('/404'))
}
})
)
})

在 install 阶段预先获取资源并进行缓存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
self.addEventListener('install', (e) => {
e.waitUntil(
self.caches
.open('v2')
.then((cache) =>
cache.addAll([
'/',
'/index.html',
'/style.css',
'/main.js',
])
)
)
})

在 activate 阶段移除失效的 Cache 实例或失效的资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
self.addEventListener('activate', (e) => {
const CACHES_NEED_MOVE = ['v1']

e.waitUntil(
self.caches.keys().then((keys) =>
Promise.all(
keys.map((key) => {
if (CACHES_NEED_MOVE.includes(key)) {
return self.caches.delete(key)
}
})
)
)
)
})

示例

类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
interface Cache {
add(request: RequestInfo | URL): Promise<void>;
addAll(requests: RequestInfo[]): Promise<void>;
delete(request: RequestInfo | URL, options?: CacheQueryOptions): Promise<boolean>;
keys(request?: RequestInfo | URL, options?: CacheQueryOptions): Promise<ReadonlyArray<Request>>;
match(request: RequestInfo | URL, options?: CacheQueryOptions): Promise<Response | undefined>;
matchAll(request?: RequestInfo | URL, options?: CacheQueryOptions): Promise<ReadonlyArray<Response>>;
put(request: RequestInfo | URL, response: Response): Promise<void>;
}

interface CacheStorage {
delete(cacheName: string): Promise<boolean>;
has(cacheName: string): Promise<boolean>;
keys(): Promise<string[]>;
match(request: RequestInfo | URL, options?: MultiCacheQueryOptions): Promise<Response | undefined>;
open(cacheName: string): Promise<Cache>;
}

interface WindowOrWorkerGlobalScope {
readonly caches: CacheStorage;
}

declare var caches: CacheStorage;

链接

ServiceWorker IV

Service Worker 消息传递

Client 向 Worker 发送消息

  • Client 端发送消息

通过调用 ServiceWorker 实例上的 postMessage() 方法实现从 Client 端向对应的 ServiceWorker 发送消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
window.navigator.serviceWorker.controller?.postMessage('message from client')

window.navigator.serviceWorker.ready.then((registration) => {
registration.active?.postMessage('message from client')
})

window.navigator.serviceWorker.getRegistration().then((registration) => {
registration?.active?.postMessage('message from client')
})

window.navigator.serviceWorker.getRegistrations().then((registrations) => {
registrations.forEach((registration) => {
registration?.active?.postMessage('message from client')
})
})
  • Worker 端接收消息

通过监听 ServiceWorkerGlobalScope 环境的 message 事件接收消息。

1
2
3
4
5
6
7
self.addEventListener('message', (e) => {
console.log('message', e)
})

self.addEventListener('messageerror', (e) => {
console.log('messageerror', e)
})

Worker 向 Client 发送消息

  • Worker 端发送消息

通过调用 Client 实例上的 postMessage() 方法实现从 ServiceWorker 向对应的 Client 发送消息。

1
2
3
4
5
6
7
8
9
self.clients.get('<id>').then((client) => {
client.postMessage('message from client')
})

self.clients.matchAll().then((clients) => {
clients.forEach((client) => {
client.postMessage('message from client')
})
})
  • Client 端接收消息

通过监听 ServiceWorkerContainer 的 message 事件接收消息。

1
2
3
4
5
6
7
window.navigator.serviceWorker.addEventListener('message', (e) => {
console.log('message', e)
})

window.navigator.serviceWorker.addEventListener('messageerror', (e) => {
console.log('messageerror', e)
})

Service Worker 请求代理

当主应用程序线程发出网络请求时,会在 Service Worker 的全局范围内触发 fetch 事件。

  • ServiceWorkerGlobalScope 接口的 fetch 事件返回一个 FetchEvent 事件(继承自 ExtendableEvent),在主应用程序线程发生网络请求时触发。

请求类型包括来自主线程的显式调用,还包括浏览器在页面导航后发出的加载页面和子资源(例如 JavaScript、CSS 和图像等)的隐式网络请求,甚至包括来自浏览器安装的插件产生的网络请求。可以通过 request 属性获取到请求的信息,调用 respondWith() 方法返回自定义的响应数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
self.addEventListener('fetch', (e) => {
if (e.request.url.includes('/success')) {
e.respondWith(Response.json({
data: 'data',
}))
}
if (e.request.url.includes('/error')) {
e.respondWith(Response.error())
}
if (e.request.url.includes('/redirect')) {
e.respondWith(Response.redirect('/override/success'))
}
})
  • FetchEvent 事件的 request 属性返回一个 Request 实例,代表触发事件处理程序的请求对象。

  • FetchEvent 事件的 respondWith() 方法阻止浏览器的默认请求处理机制,并允许使用自定义的响应替代,其接收一个 Response 实例或者 Promise 的 Response 实例。

respondWith() 方法对于给定的请求只能调用该方法一次。如果 fetch 添加了多个事件监听器,它们将按照注册的顺序被调用,直到其中一个事件监听器调用该方法。

respondWith() 方法必须同步调用,不能在异步方法中调用该方法。

respondWith() 未在处理程序中调用,则用户代理会自动发出原始网络请求。

  • FetchEvent 事件的 clientId 属性返回触发 fetch 事件的 Client 的 id,可以使用 Clients.get() 方法获取到对应的 Client 实例。

  • FetchEvent 事件的 replacesClientId 属性返回若因页面导航而触发 fetch 事件的前一个 Client 的 id,否则返回一个空字符串。

  • FetchEvent 事件的 resultingClientId 属性返回若因页面导航而触发 fetch 事件的后一个 Client 的 id,否则返回一个空字符串。

  • FetchEvent 事件的 handled 属性返回 Promise 实例,表明 fetch 事件已被处理,并在消费请求后完成。

  • FetchEvent 事件的 preloadResponse 属性返回若导航预加载情况下的 Promise 的 Response,否则返回 Promise 的 undefined。

示例

链接

ServiceWorker I

ServiceWorker 概念

ServiceWorker 本质上充当位于 Web 应用程序、浏览器和网络(如果可用)之间的代理服务器,可以用于创建有效的离线体验、拦截网络请求并根据网络是否可用采取适当的操作,以及更新服务器上的资产,此外还可以用于实现推送通知和后台同步等功能。

ServiceWorker 是针对源和路径注册的事件驱动 Worker。它采用 JavaScript 文件的形式,可以控制与之关联的网页/站点,拦截和修改导航和资源请求,并以非常精细的方式缓存资源,从而可以以完全控制应用程序的行为(最明显的一种是网络不可用时的回退方案)。

ServiceWorker 在 Worker 上下文中运行:因此它没有 DOM 访问权限,并且在与为应用程序提供支持的主 JavaScript 不同的线程上运行,因此它是非阻塞的。并且,它被设计为完全异步;因此,同步 XMLHttpRequestWeb Storage 等 API 无法在 ServiceWorker 内部使用。

ServiceWorker 无法动态导入 JavaScript 模块,如果在 ServiceWorker 全局范围内调用 import() 进行动态导入,则会抛出异常,仅允许使用 import 语句进行静态导入。同时创建时需指定 ServiceWorker 为模块 Worker。

出于安全原因,ServiceWorker 只能在安全上下文中运行(可以通过全局变量 isSecureContext 来判断是否处于安全上下文)。

ServiceWorker 生命周期

ServiceWorker 生命周期依次是安装激活运行

成功注册后,ServiceWorker 将会在空闲时终止,以节省内存和处理器电量。活动的 ServiceWorker 会自动重新启动以响应事件。

  • 安装阶段,在 ServiceWorker 脚本下载成功之后,浏览器开始安装 ServiceWorker(在 ServiceWorkerGlobalScope 上触发 install 事件,返回一个 ExtendableEvent 事件)
  • 激活阶段,在安装完成之后,浏览器开始激活 ServiceWorker 的阶段(在 ServiceWorkerGlobalScope 上触发 activate 事件,返回一个 ExtendableEvent 事件)
  • 运行阶段,在激活完成之后,ServiceWorker开始运行的阶段

ExtendableEvent 事件的 waitUntil 方法,可以用于表示事件调度程序工作正在进行中并延迟生命周期的完成直至传递的 Promise 被解决。

install 事件中,waitUntil 方法用于初始化 ServiceWorker,用于将 ServiceWorker 保持在安装阶段,直到任务完成;若 Promise 被拒绝,则安装被视为失败,并且正在安装的 ServiceWorker 将被丢弃。

activate 事件中,waitUntil 方法用来缓冲功能事件,从而可以更新数据库架构并删除过时的缓存,保证正式运行时使用的是最新的架构。

ServiceWorker 基本使用

浏览器端 通过访问window.navigator.serviceWorker 属性获取 ServiceWorkerContainer 接口实例,包含各种管理 ServiceWorker 的方法,如注册、取消注册、更新以及读取状态。

注册

ServiceWorker 的注册通过调用 ServiceWorkerContainer 接口的 register() 方法实现。

方法接收一个参数,代表 ServiceWorker 脚本的 URL。

方法亦可接收一个配置项参数:

可选的 scope 参数定义 ServiceWorker 的注册范围,值默认设置为 ServiceWorker 脚本所在的目录。

可选的 type 参数指定要创建的 ServiceWorker 的类型,值可以是 'classic''module''classic' 代表 Worker 内部使用标准脚本模式;'module' 代表 Worker 内部使用模块脚本模式。

可选的 updateViaCache 参数指示在更新期间如何将 HTTP 缓存用于 ServiceWorker 脚本资源,值可以是 'all''imports''none''all' 代表 ServiceWorker 脚本资源和其导入的脚本资源均使用 HTTP 缓存,'imports' 代表仅 ServiceWorker 脚本资源不使用 HTTP 缓存,其导入的脚本资源使用 HTTP 缓存,'none' 代表 ServiceWorker 脚本资源和其导入的脚本资源均不使用 HTTP 缓存。

返回一个 ServiceWorkerRegistration 接口实例,代表注册的 ServiceWorker 对象。

1
2
3
4
5
6
7
8
window.navigator.serviceWorker.register(
'./service-worker.js',
{
scope: '/',
type: 'module',
updateViaCache: 'all',
}
)

关于 scope 参数的举例

  • 页面 / 与 ServiceWorker 脚本路径 /sw.js 允许控制 / 以下的页面

  • 页面 /product 与 ServiceWorker 脚本路径 /product/sw.js 与 scope ./ 允许控制 /product 以下的页面

  • 页面 / 与 ServiceWorker 脚本路径 /sw.js 与 scope /product/允许控制 /product 以下的页面

更新

ServiceWorker 的更新通过调用与 ServiceWorker 对应的 ServiceWorkerRegistration 接口的 update() 方法实现。

返回一个 Promise 的 ServiceWorkerRegistration

1
2
3
4
5
navigator.serviceWorker.ready.then((registration) => {
registration.addEventListener('updatefound', () => {
registration.update()
})
})

卸载

ServiceWorker 的卸载通过调用对应的 ServiceWorkerRegistration 接口实例的 unregister() 方法实现。

返回一个 Promise 的布尔值,表示是否卸载成功。

1
2
3
4
5
navigator.serviceWorker.ready.then((registration) => {
window.addEventListener('beforeunload', () => {
registration.unregister()
})
})

ServiceWorker 补充

当用户首次访问 ServiceWorker 控制的站点/页面时,ServiceWorker 会立即下载。

之后,它会在导航到范围内的页面或在 ServiceWorker 上触发了一个事件且未在过去 24 小时内下载的情况下更新。

当发现下载的文件是新的时,就会尝试安装 - 要么与现有的 ServiceWorker 不同(按字节比较),要么与此页面/站点遇到的第一个 ServiceWorker 不同。

如果这是首次使 ServiceWorker 可用,则会尝试安装,然后在成功安装后将其激活。

但如果存在可用的现有 ServiceWorker,则新版本会在后台安装,但尚未激活 - 此时它称为等待中的 Worker。仅当不再加载任何仍在使用旧 ServiceWorker 的页面时,它才会被激活。一旦没有更多页面需要加载,新的 ServiceWorker 就会激活(成为活动 ServiceWorker)。不过可以手动提前终止当前 ServiceWorker 并启用新的 ServiceWorker。

示例

链接


:D 一言句子获取中...