ServiceWorker III

ServiceWorker 中 Client 与 Worker

在 Worker 中获取 Client

在 ServiceWorker 中,会对应着多个 Client 实例,代表着受 ServiceWorker 控制的上下文,可以是网页、Worker 乃至 SharedWorker 等,而 Client 实例的访问与控制通过 Clients 实例实现,通过 clients 属性获取

同时,在 ServiceWorker 中,通过 registration 属性暴露 ServiceWorkerRegistration 实例,即当前 ServiceWorker 注册;通过 serviceWorker 属性暴露 ServiceWorker 实例,即当前 ServiceWorker 实例

Clients

在 ServiceWorker 中,Clients 接口提供对 Client 接口的访问

  • Clients 接口的 get() 方法根据给定的 id 获取对应的 Client 实例

    接收一个字符串,代表 Client 的 id

    返回一个 Promise 的 Client(或 undefined

  • Clients 接口的 matchAll() 方法获取所有匹配的 Client 实例

    接收一组可选的配置项

    配置项的 type 参数指定需匹配的 Client 类型,允许的值为 "window", "worker", "sharedworker""all",默认为 "window"

    配置项的 includeUncontrolled 参数指定是否返回未受控制的 Client,默认为 false

    返回一个 Promise 的 Client 数组

  • Clients 接口的 claim() 方法将所有匹配的 Client 实例置于当前 ServiceWorker 的控制之下

    返回一个 Promise

  • Clients 接口的 openWindow() 方法创建新的顶层上下文并加载给定的 URL

    要求该 URL 需与 ServiceWorker 同源,并且要求调用前一段时间用户需发生交互行为

    Firefox 中要求该方法必须在通知点击回调方法中调用

    返回一个 Promise 的 WindowClient 实例或 null

Client

在 ServiceWorker 中,Client 接口表示受 ServiceWorker 控制的执行上下文

其中 WindowClient 接口继承自 Client 接口,表示受 ServiceWorker 控制的 Window 执行上下文

受 ServiceWorker 控制的各执行上下文可以是 Worker、Shared Worker 以及 Window

  • Client 接口的 id 属性返回一个字符串,代表对应的执行上下文的 ID

  • Client 接口的 type 属性返回一个字符串枚举,可能为 "window""worker""sharedworker" 之一,代表对应的执行上下文的类型

  • Client 接口的 url 属性返回一个字符串,代表对应的执行上下文的 URL

  • Client 接口的 frameType 属性返回一个字符串枚举,可能为 "auxiliary""top-level""nested""none" 之一,代表对应的执行上下文的类型

  • WindowClient 接口的 focused 属性返回一个布尔值,代表对应的 Window 执行上下文是否处于聚焦状态

  • WindowClient 接口的 visibilityState 属性返回一个字符串枚举,可能的值为 "hidden""visible""prerender",代表对应的 Window 执行上下文的可见性类型

  • WindowClient 接口的 focus() 方法控制对应的执行上下文聚焦

    返回一个 Promise 的 WindowClient 实例

  • WindowClient 接口的 navigate() 方法控制对应的执行上下文加载指定 URL

    方法接收一个字符串或 URL 实例,代表 URL

    若 ServiceWorker 执行上下文与 URL 同源,返回 Promise 的 WindowClient;否则返回 Promise 的 null

在 Client 中获取 Worker

在 Client 端中,通过 ready 属性暴露控制当前页面的 ServiceWorkerRegistration 实例,通过 controller 属性暴露控制当前页面的 ServiceWorker 实例

ServiceWorkerContainer

在 ServiceWorker 中,ServiceWorkerContainer 接口包含 ServiceWorker 的相关状态与相关控制方法,用于实现对 ServiceWorker 的管理

  • ServiceWorkerContainer 接口的 ready 属性返回一个 Promise 的 ServiceWorkerRegistration,表示控制当前页面的 ServiceWorker 的注册;该属性与 ServiceWorkerGlobalScope 接口的 registration 属性类似

  • ServiceWorkerContainer 接口的 controller 属性返回一个 ServiceWorkernull,表示是否存在控制当前页面的 ServiceWorker 的实例;该属性与 ServiceWorkerGlobalScope 接口的 serviceWorker 属性类似

  • ServiceWorkerContainer 接口的 getRegistration() 方法根据给定的 URL (默认使用当前 Client 的 URL)返回与之匹配的 ServiceWorkerRegistration 对象

    方法可以接受一个参数

    方法返回一个 Promise 的 ServiceWorkerRegistration 或者 undefined,根据是否存在对应的注册对象

  • ServiceWorkerContainer 接口的 getRegistrations() 方法获取所有与当前 Client 相关的 ServiceWorkerRegistration 对象

    返回一个 ServiceWorkerRegistration 的数组

  • ServiceWorkerContainer 接口的 startMessages() 方法强制当前上下文提前开始接收发送自 ServiceWorker 的消息

  • ServiceWorkerContainer 接口的 controllerchange 事件在控制当前 Client 的 ServiceWorker 变化时触发,返回一个 Event 事件

ServiceWorkerRegistration

在 ServiceWorker 中,ServiceWorkerRegistration 接口表示 ServiceWorker 的注册对象

  • ServiceWorkerRegistration 接口的 active 属性返回一个 ServiceWorker 或者 null,代表最新注册的 state 属性为 activatingactivated 的 ServiceWorker

  • ServiceWorkerRegistration 接口的 installing 属性返回一个 ServiceWorker 或者 null,代表最新注册的 state 属性为 installing 的 ServiceWorker

  • ServiceWorkerRegistration 接口的 waiting 属性返回一个 ServiceWorker 或者 null,代表最新注册的 state 属性为 installed 的 ServiceWorker

通常而言,active 属性、installing 属性、waiting 属性三个中最多只有一个是非 null 值。

  • ServiceWorkerRegistration 接口的 scope 属性返回一个字符串,代表 ServiceWorker 的注册域

  • ServiceWorkerRegistration 接口的 updateViaCache 属性返回一个字符串枚举,可能的值为 'all''imports''none',指定 HTTP 缓存的脚本如何应用于 ServiceWorker

以上两个属性的值在注册 ServiceWorker 时指定

  • ServiceWorkerRegistration 接口的 updatefound 事件在新的 ServiceWorker 开始下载时触发,返回一个 Event 事件

ServiceWorker

在 ServiceWorker 中,ServiceWorker 接口表示 ServiceWorker 对象

  • ServiceWorker 接口的 scriptURL 属性返回一个字符串,表示 ServiceWorker 的注册脚本 URL

  • ServiceWorker 接口的 state 属性返回一个字符串枚举,可能的值包括 "parsed""installing""installed""activating""activated""redundant",表示 ServiceWorker 的状态

    • "parsed" ServiceWorker 在下载完成并且验证可运行后的初始值
    • "installing" ServiceWorker 在安装中
    • "installed" ServiceWorker 安装完成
    • "activating" ServiceWorker 在激活中
    • "activated" ServiceWorker 激活完成
    • "redundant" ServiceWorker 被替代或安装失败
  • ServiceWorker 接口的 statechange 事件在 ServiceWorker 的状态更新时触发,返回一个 Event 事件

示例

类型

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
interface Client {
readonly frameType: FrameType;
readonly id: string;
readonly type: ClientTypes;
readonly url: string;
postMessage(message: any, transfer: Transferable[]): void;
postMessage(message: any, options?: StructuredSerializeOptions): void;
}

interface Clients {
claim(): Promise<void>;
get(id: string): Promise<Client | undefined>;
matchAll<T extends ClientQueryOptions>(options?: T): Promise<ReadonlyArray<T["type"] extends "window" ? WindowClient : Client>>;
openWindow(url: string | URL): Promise<WindowClient | null>;
}

interface WindowClient extends Client {
readonly focused: boolean;
readonly visibilityState: DocumentVisibilityState;
focus(): Promise<WindowClient>;
navigate(url: string | URL): Promise<WindowClient | null>;
}

interface ServiceWorker extends EventTarget, AbstractWorker {
readonly scriptURL: string;
readonly state: ServiceWorkerState;
postMessage(message: any, transfer: Transferable[]): void;
postMessage(message: any, options?: StructuredSerializeOptions): void;
onstatechange: ((this: ServiceWorker, ev: Event) => any) | null;
}

interface ServiceWorkerContainer extends EventTarget {
readonly controller: ServiceWorker | null;
readonly ready: Promise<ServiceWorkerRegistration>;
getRegistration(clientURL?: string | URL): Promise<ServiceWorkerRegistration | undefined>;
getRegistrations(): Promise<ReadonlyArray<ServiceWorkerRegistration>>;
register(scriptURL: string | URL, options?: RegistrationOptions): Promise<ServiceWorkerRegistration>;
startMessages(): void;
oncontrollerchange: ((this: ServiceWorkerContainer, ev: Event) => any) | null;
onmessage: ((this: ServiceWorkerContainer, ev: MessageEvent) => any) | null;
onmessageerror: ((this: ServiceWorkerContainer, ev: MessageEvent) => any) | null;
}

interface ServiceWorkerRegistration extends EventTarget {
readonly active: ServiceWorker | null;
readonly installing: ServiceWorker | null;
readonly navigationPreload: NavigationPreloadManager;
readonly pushManager: PushManager;
readonly scope: string;
readonly updateViaCache: ServiceWorkerUpdateViaCache;
readonly waiting: ServiceWorker | null;
getNotifications(filter?: GetNotificationOptions): Promise<Notification[]>;
showNotification(title: string, options?: NotificationOptions): Promise<void>;
unregister(): Promise<boolean>;
update(): Promise<void>;
onupdatefound: ((this: ServiceWorkerRegistration, ev: Event) => any) | null;
}

链接

Web Periodic Background Synchronization API

Web Periodic Background Synchronization API 提供了一种注册在网络状态允许下周期性执行任务的方法,这些任务被称为周期后台同步请求。

API 用途包括在设备连接到网络时获取最新内容,或允许对应用程序进行后台更新。

调用 API 时需设置最小时间间隔,但是用户代理通常还会考虑网络连接情况或者之前网站的用户参与程度来决定触发任务的周期。

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

注册周期后台同步任务

PeriodicSyncManager 接口的 register() 方法用于注册周期后台同步任务。

方法接收一个字符串,作为周期后台同步任务的唯一标识符;可接收一组可选的配置项,其唯一属性为 minInterval,指定周期后台同步任务的执行周期。

返回一个 Promise。

1
2
3
4
5
6
7
const TAG = 'tag'

window.navigator.serviceWorker.ready.then((registration) => {
registration.periodicSync.register(TAG, {
minInterval: 24 * 60 * 60 * 1000,
})
})

查看周期后台同步任务

PeriodicSyncManager 接口的 getTags() 方法用于获取周期后台同步任务。

返回一个 Promise 的字符串数组,代表当前已注册的周期后台同步任务的标识符列表。

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

window.navigator.serviceWorker.ready.then((registration) => {
registration.periodicSync.getTags().then((tags) => {
if (tags.includes(TAG)) {
// do something
}
})
})

卸载周期后台同步任务

PeriodicSyncManager 接口的 unregister() 方法用于卸载周期后台同步任务。

方法接收一个字符串,代表周期后台同步任务的唯一标识符。

返回一个 Promise。

1
2
3
4
5
const TAG = 'tag'

window.navigator.serviceWorker.ready.then((registration) => {
registration.periodicSync.unregister(TAG)
})

执行周期后台同步任务

ServiceWorkerGlobalScope 接口的 periodicsync 事件在触发周期后台同步任务时触发。返回一个 PeriodicSyncEvent 事件。

事件触发周期大于或等于在注册时设置的最小执行周期。

1
2
3
4
5
6
7
const TAG = 'tag'

self.addEventListener('periodicsync', (e) => {
if (e.tag === TAG) {
// do something
}
})

PeriodicSyncEvent 事件继承自 ExtendableEvent 事件。其 tag 属性返回事件对应的周期后台同步任务的唯一标识符。

权限 API

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

示例

类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
interface PeriodicSyncEvent extends ExtendableEvent {
readonly tag: string;
}

interface BackgroundSyncOptions {
minInterval: number;
}

interface PeriodicSyncManager {
getTags(): Promise<ReadonlyArray<string>>;
register(tag: string, options?: BackgroundSyncOptions): Promise<void>;
unregister(tag: string): Promise<void>;
}

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

interface ServiceWorkerRegistration extends EventTarget {
readonly periodicSync: PeriodicSyncManager;
}

链接

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。

示例

链接

npm

npm

npm

npm官网

官方英文文档

中文文档

包:node.js 中的第三方模块

由第三方个人或团队开发的开源代码集

包类型:

  • 项目包
    • 开发依赖包 被记录到devDependencies节点的包,仅开发中使用
    • 核心依赖包 被记录到dependencies节点的包,开发发布后均使用
  • 全局包

命令:

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
npm -v
查看npm版本


npm help
查看npm帮助


npm init
初始化包环境
创建package.json包配置文件
* -yes -y 使用默认设置初始化包环境


npm install <package_name>
npm i <package_name>
下载npm包
在当前目录中创建node_modules目录(如果尚不存在)并将包下载到该目录
同时在该目录下添加package-lock.json文件,记录下载的包的信息,如包的版本号、下载url等
<package_name>@<tag> <package_name>@<edition> 方式下载指定版本和标签的包
未指定package_name参数时会根据package.json的dependencies属性下载包
* -global -g 全局安装包
* --save-dev -D 把包配置到devDependencies内


npm list
npm ls
列出所有已安装的包
* -global -g 显示全局安装的包
* -all -a 显示所有包及其所有的依赖项
* -depth=<number> 显示包及其给定深度的依赖项

npm uninstall <package_name>
卸载npm包
* --save 从package-lock.json文件dependencies属性中移除包
* --save-dev 从package-lock.json文件devDependencies属性中移除包
* -g 删除全局包

npm update
npm update -g
npm update -g <package_name>
升级npm包

npm outdated
npm outdated -g --depth=0
检测npm包是否为最新

包管理配置文件:package.json项目根目录

项目名称、版本号、描述等‘

项目中使用的包:区分仅开发使用与开发及部署均使用

安装的包会自动更新至package.json文件

  • name 项目名称:字符串

  • version 项目版本号:字符串

  • description 包内容描述:字符串

  • keywords 项目的关键词:字符串数组

  • homePage 项目主页:字符串

  • bugs 项目报告错误的网址:字符串数组

  • license 项目的许可证:字符串

  • author 作者:字符串 可使用name、email、url替代

  • contributors 贡献者:字符串数组

  • funding 贡献方式:字符串or字符串数组or对象(type & url)

  • files 包作为依赖项需包含的条目

  • main 标记包的根目录模块

  • browser 同上,特别指明为浏览器环境

  • bin 命令名称到本地文件的映射|

  • man 指定为man程序查找的文件名|

  • directories 指定包结构|

  • repository 指定代码所在位置,常为各种git仓库

  • scripts 指定在包生命周期运行的代码|

  • config 设置持续使用配置参数

  • dependencies 项目开发与发布均使用的包

  • devDependencies 项目开发内使用的包

  • peerDependencies 项目使用的插件|

  • peerDependenciesMeta 标记插件是否为必需|

  • bundledDependencies/bundleDependencies 发布包时将捆绑的包名称|

  • optionalDependencies 可以使用依赖项|

  • overrides 规定依赖项版本|

  • engines 规定适用的node版本

  • os 规定使用的操作系统版本

  • cpu 规定适用的cpu版本

  • private 规定能否发布私有存储库

  • publishConfig 规定发布配置

  • workspaces 描述用作工作空间的文件夹的直接路径

版本号:

  • 第一位数字:大版本
  • 第二位数字:功能版本
  • 第三位数字:bug修复版本

其他:

将node_modules文件夹添加到.gitignore文件内,以使git上传时剔除npm包


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