ServiceWorker II

ServiceWorker 全局上下文

ServiceWorkerGlobalScope 接口代表 ServiceWorker 的全局上下文,在 ServiceWorker 内通过 self 全局变量或者 globalThis 全局变量访问(该接口继承自 WorkerGlobalScope)。

ServiceWorkerGlobalScope

以下代表在 ServiceWorkerGlobalScope 接口本身的属性、方法和事件

  • ServiceWorkerGlobalScope 接口的 clients 属性代表一个 Clients 实例,可用于获取 Client (可执行上下文)实例。

  • ServiceWorkerGlobalScope 接口的 registration 属性代表一个 ServiceWorkerRegistration 实例,即当前 ServiceWorker 注册的引用。

  • ServiceWorkerGlobalScope 接口的 serviceWorker 属性代表一个 ServiceWorker 实例,即当前 ServiceWorker 实例的引用。

  • ServiceWorkerGlobalScope 接口的 skipWaiting 方法强制当前 ServiceWorker 从等待状态变成激活状态,返回一个该 ServiceWorker 激活后完成的 Promise。其在 install 事件的回调中调用才具有实际意义。

WorkerGlobalScope

以下代表继承自 WorkerGlobalScope 接口的属性、方法和事件

  • WorkerGlobalScope 接口的 location 属性代表一个 WorkerLocation 实例,是 Location 的字集。

  • WorkerGlobalScope 接口的 navigator 属性代表一个 WorkerNavigator 实例,是 Navigator 的字集。

  • WorkerGlobalScope 接口的 self 属性代表 WorkerGlobalScope 接口本身。

  • WorkerGlobalScope 接口的 importScripts 方法同步导入一组脚本文件并执行,接受一组参数,代表脚本文件的 URL,其可以为绝对路径或相对路径(相对文档路径)。

  • WorkerGlobalScope 接口的 error 事件在 ServiceWorker 内发生脚本错误时触发,返回一个 Event 实例。

  • WorkerGlobalScope 接口的 languagechange 事件在用户的首选语言更改时触发,返回一个 Event 实例。

  • WorkerGlobalScope 接口的 online 事件在浏览器获得网络访问权限并且 navigator.onLine 值切换到 true 时触发,返回一个 Event 实例。

  • WorkerGlobalScope 接口的 offline 事件在浏览器获得网络访问权限并且 navigator.onLine 值切换到 false 时触发,返回一个 Event 实例。

  • WorkerGlobalScope 接口的 rejectionhandled 事件在 ServiceWorker 内处理的 Promise 拒绝事件时触发,返回一个 Event 实例。

  • WorkerGlobalScope 接口的 unhandledrejection 事件在 ServiceWorker 内未处理的 Promise 拒绝事件时触发,返回一个 Event 实例。

以下代表暴露在全局的属性、方法和事件

1
2
3
4
5
6
7
8
self.fonts
self.caches
self.crossOriginIsolated
self.crypto
self.indexedDB
self.isSecureContext
self.origin
self.performance
1
2
3
4
5
6
7
8
9
10
11
self.atob()
self.btoa()
self.clearInterval()
self.clearTimeout()
self.createImageBitmap()
self.fetch()
self.queueMicrotask()
self.reportError()
self.setInterval()
self.setTimeout()
self.structuredClone()

示例

类型

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
interface ServiceWorkerGlobalScope extends WorkerGlobalScope {
readonly clients: Clients;
readonly registration: ServiceWorkerRegistration;
readonly serviceWorker: ServiceWorker;
onactivate: ((this: ServiceWorkerGlobalScope, ev: ExtendableEvent) => any) | null;
onfetch: ((this: ServiceWorkerGlobalScope, ev: FetchEvent) => any) | null;
oninstall: ((this: ServiceWorkerGlobalScope, ev: ExtendableEvent) => any) | null;
onmessage: ((this: ServiceWorkerGlobalScope, ev: ExtendableMessageEvent) => any) | null;
onmessageerror: ((this: ServiceWorkerGlobalScope, ev: MessageEvent) => any) | null;
onnotificationclick: ((this: ServiceWorkerGlobalScope, ev: NotificationEvent) => any) | null;
onnotificationclose: ((this: ServiceWorkerGlobalScope, ev: NotificationEvent) => any) | null;
onpush: ((this: ServiceWorkerGlobalScope, ev: PushEvent) => any) | null;
onpushsubscriptionchange: ((this: ServiceWorkerGlobalScope, ev: Event) => any) | null;
skipWaiting(): Promise<void>;
}

interface WorkerGlobalScope extends EventTarget, FontFaceSource, WindowOrWorkerGlobalScope {
readonly location: WorkerLocation;
readonly navigator: WorkerNavigator;
readonly self: WorkerGlobalScope & typeof globalThis;
onerror: ((this: WorkerGlobalScope, ev: ErrorEvent) => any) | null;
onlanguagechange: ((this: WorkerGlobalScope, ev: Event) => any) | null;
onoffline: ((this: WorkerGlobalScope, ev: Event) => any) | null;
ononline: ((this: WorkerGlobalScope, ev: Event) => any) | null;
onrejectionhandled: ((this: WorkerGlobalScope, ev: PromiseRejectionEvent) => any) | null;
onunhandledrejection: ((this: WorkerGlobalScope, ev: PromiseRejectionEvent) => any) | null;
importScripts(...urls: (string | URL)[]): void;
}

interface WindowOrWorkerGlobalScope {
readonly caches: CacheStorage;
readonly crossOriginIsolated: boolean;
readonly crypto: Crypto;
readonly fonts: FontFaceSet;
readonly indexedDB: IDBFactory;
readonly isSecureContext: boolean;
readonly origin: string;
readonly performance: Performance;
atob(data: string): string;
btoa(data: string): string;
clearInterval(id: number | undefined): void;
clearTimeout(id: number | undefined): void;
createImageBitmap(image: ImageBitmapSource, options?: ImageBitmapOptions): Promise<ImageBitmap>;
createImageBitmap(image: ImageBitmapSource, sx: number, sy: number, sw: number, sh: number, options?: ImageBitmapOptions): Promise<ImageBitmap>;
fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
queueMicrotask(callback: VoidFunction): void;
reportError(e: any): void;
setInterval(handler: TimerHandler, timeout?: number, ...arguments: any[]): number;
setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number;
structuredClone<T = any>(value: T, options?: StructuredSerializeOptions): T;
}

链接

Local Font Access API

Local Font Access API 向开发者提供了获取用户本地安装的字体的信息的方式,包括字体名称、字体样式及字体族等等

获取本地字体

调用 window.queryLocalFonts() 方法来获取本地安装的字体

方法允许传入一组可选的配置项,其 postscriptNames 参数允许传入一组字符串数组,代表希望筛选的 postscriptName 名称

方法会返回 Promise 的 FontData 数组,表示本地安装的字体的列表

方法可能抛出 NotAllowedError 异常,表示用户拒绝授予开发者 'local-fonts' 权限

方法可能抛出 SecurityError 异常,表示该 API 受 Permissions Policy 的限制无法被调用或调用该方法并非缘于用户交互行为

1
2
3
window.queryLocalFonts().then((fonts) => {
// to do something
})

处理本地字体

字体信息使用 FontData 接口表示

FontData 接口的 family 属性表示字体的字体族,可以用于 CSS 的 font-family 属性或者 @font-face 规则中的 local() 函数等;

FontData 接口的 fullName 属性表示字体的全名,通常是一个用户可辨识的名称,可以用于向用户展示;

FontData 接口的 postscriptName 属性表示字体的 PostScript 名称,可以用于唯一地辨识字体;

FontData 接口的 style 属性表示字体的样式,可以用于 CSS 的 font-style 属性;

1
2
3
4
5
6
7
8
9
10
async function logFonts() {
const fonts = await window.queryLocalFonts()

for (const data of fonts) {
console.log(data.postscriptName)
console.log(data.fullName)
console.log(data.family)
console.log(data.style)
}
}

FontData 接口的 blob() 方法以 Blob 形式返回字体的源数据;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
async function getFontFormat(font) {
const data = await fontData.blob()

const version = await data.slice(0, 4).text()

let format: 'unknown' | 'truetype' | 'cff' = 'unknown'

switch (format) {
case "\x00\x01\x00\x00":
case "true":
case "typ1":
format = "truetype"
break
case "OTTO":
format = "cff"
break
}

return format
}

权限策略

该 API 调用受到 local-fonts 权限策略的控制,可以通过 Permissions-Policy 响应头指定,或通过 <iframe> 标签的 allow 属性指定

默认为 self,即允许在当前上下文或内嵌的其他同源上下文中使用

权限 API

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

类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface Window {
queryLocalFonts: (options?: QueryOptions) => Promise<FontData[]>
}

interface QueryOptions {
postscriptNames?: string[]
}

interface FontData {
readonly family: string
readonly fullName: string
readonly postscriptName: string
readonly style: string
blob: () => Promise<Blob>
}

链接

性能优化

构建相关

路由懒加载

最主要在于降低首屏加载资源大小,仅加载所需的页面资源文件,加快页面的显示

bad
1
2
3
4
5
import C from 'c'

{
component: C,
}
good
1
2
3
{
component: () => import('c'),
}

原理即将导航中的路由组件从静态 import 导入改为动态 import() 导入

组件懒加载

原理同路由懒加载

worse
1
2
3
4
5
6
7
import C from 'c'

export default {
components: {
C,
},
}
better
1
2
3
4
5
6
7
const C = () => import('c')

export default {
components: {
C,
},
}

做组件懒加载一般在某些特别条件下使用,如组件仅在特定条件下才展示、当前页面文件过大、组件复用性较强

外部依赖懒加载

较大外部依赖可动态导入

worse
1
import * as THREE from 'three'
better
1
import('three').then((THREE) => { /* do */ })

但建议谨慎采取此方式

Tree Shaking 和 SideEffects

依赖 ESM 的静态特性,进行静态分析,在生成产物中去除无用的模块或代码,从而降低生成产物的大小

webpack 默认在构建阶段会启用 Tree Shaking,在开发阶段需手动配置

webpack.config.js
1
2
3
4
5
6
module.exports = {
mode: 'development',
optimization: {
usedExports: true,
},
}
webpack.config.js
1
2
3
module.exports = {
mode: 'production',
}

使用副作用

package.json
1
2
3
{
"sideEffects": false
}

某些情况下需手动标记 /*#__PURE__*/ 以标记代码,以标记语句是可执行 Tree Shaking 的

vite 原生基于 rollup 支持在构建阶段启用 Tree Shaking

构建产物压缩

webpack 可以使用 terser-webpack-plugin 插件来执行代码构建产物的压缩

webpack.config.js
1
2
3
4
5
6
7
8
const TerserPlugin = require("terser-webpack-plugin")

module.exports = {
optimization: {
minimize: true,
minimizer: [new TerserPlugin()],
},
}

vite 内部默认集成 esbuild 进行代码构建产物的压缩,同时支持配置为使用 terser 来执行压缩

vite.config.js
1
2
3
4
5
6
7
8
9
10
11
export default {
esbuild: {},
build: {
cssMinify: 'esbuild',
minify: 'esbuild',
terserOptions: {},
},
optimizeDeps: {
esbuildOptions: {},
},
}

静态资源构建产物(特别是图片)的压缩可以使用一些插件实现,如 compression-webpack-plugin

外部库按需加载

外部库(特别是 UI 组件库)使用插件(如 babel-plugin-import 等)进行按需加载

代码分割

可以适当进行代码分割,避免一次性加载过大的资源文件,阻碍页面的展示;也需要避免过度分割,一次性执行过多的资源获取请求

vite.config.js
1
2
3
4
5
6
7
8
9
10
11
export default {
build: {
rollupOptions: {
output: {
manualChunks: {},
// or: manualChunks: (id) => id,
},
},
cssCodeSplit: {},
},
}

内联代码文件

部分小体量的 JS 文件或 CSS 文件,可以内联到 HTML 文件中,减少请求的数量

分析外部依赖

可通过 webpack-bundle-analyzer 插件或 rollup-plugin-visualizer 插件来分析查看

webpack.config.js
1
2
3
4
5
6
7
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')

module.exports = {
plugins: [
new BundleAnalyzerPlugin(),
],
}
vite.config.js
1
2
3
4
5
6
7
import { visualizer } from 'rollup-plugin-visualizer'

export default {
plugins: [
visualizer(),
],
}

检查项目的依赖包是否有重复引用的情况,避免出现使用同样名称不同版本的依赖包引用的情况

渲染相关

骨架屏

主要应用于缩短白屏时长,特别是 SPA 单页应用

原理是直接把展示骨架屏的内容放在 html 文件内,在真正内容加载完后再隐藏骨架屏的内容

虚拟滚动

只渲染可视区域的列表项,非可见区域的不渲染

原理为计算列表的总高度,并在触发滚动事件时根据滚动高度更新起始下标和结束下标,从而取出相应的数据渲染元素

Worker 长任务优化

将一些长任务逻辑移入到 Worker 中,避免长任务的执行阻碍 UI 渲染而影响用户体验

是否使用 Worker,需要比较 Worker 通信时长与运算时长相比是否具有足够的优势

利用 requestAnimationFrame 周期任务

可以利用 requestAnimationFrame 处理周期任务

特别是需要较严格固定周期频率执行的情况(setInterval 和 setTimeout 无法保证准确的时间间隔)

同时 requestAnimationFrame 支持在页面隐藏或最小化时暂停执行周期任务,以节省性能(setInterval 和 setTimeout 不会因页面隐藏或最小化等因素暂停执行)

使用 CSS 动画过渡变换替代 JS

CSS 动画 Animation、过渡 Transition、变换 Transform 相较于 JS 性能通过上更具优势,并且浏览器更易于针对性地做优化

简化 CSS 选择器

避用通配符选择器 *

减少使用标签选择器

优先使用默认的样式继承

避免层数过大的选择器

代码复用及代码封装

提升代码复用率,进行功能代码封装等

CSS 的样式简化,可以使用 TailwindCSSUnoCSS 等方案

使用防抖节流

防抖,使得指定函数至少间隔 n 秒才会执行一次

节流,使得指定函数在 n 秒中最多执行一次

对于容易连续触发的事件,如 mousemovepointermovescrolltouchmovewheelresize 等,通过将事件处理方法绑定为防抖节流版本的方法,避免持续多次重复执行方法

使用 will-change 优化动态效果

预先将执行动画的元素设置 will-change CSS 属性,以便浏览器引擎将其视为单独图层来进行优化

注意点是,避免过度应用 will-change 属性;建议仅在需要时候 JS 动态设置该属性

减少页面重排重绘

重绘指元素的非几何样式改变引起的浏览器重新渲染目标元素的现象

重绘指元素的几何样式改变引起的浏览器重新渲染整个渲染树或的现象

使用 GPU 渲染

CSS中可使用如下一些方式将目标元素独立为合成层,从而进行独立渲染,以触发 GPU 渲染

  • 指定 will-change 属性
  • 3D 或者透视变换 perspective transform
  • 使用加速视频解码的 video 元素
  • 拥有 3D 上下文(WebGL)或者加速 2D 上下文的 canvas 元素
  • 使用 opacity、filter 实现 CSS 动画或使用一个动画 webkit 变换的元素
  • 拥有加速 CSS 过滤器的元素
  • 元素有一个包含复合层的后代节点
  • 元素有一个兄弟元素在复合图层渲染,且具备较低的 z-index

避免无效请求

避免出现无效请求,例如表单提交频繁点击的问题,或路由切换时还有未完成的请求;对于服务器和用户来说,会造成不必要的困扰

网络相关

使用 HTTP2

HTTP2 支持头部压缩,能够减少数据传输量,节省消息投占用的网络的流量

且 HTTP2 支持多路复用、服务器推送等功能

gzip 压缩

HTTP 头部及资源启用 gzip 压缩,能够大大减少网络传输的数据量

可以使用 compression-webpack-pluginvite-plugin-compression 压缩打包资源至 gzip

webpack.config.js
1
2
3
4
5
const compression = require('compression-webpack-plugin')

module.exports = {
plugins: [new compression()],
}
vite.config.js
1
2
3
4
5
import compression from 'vite-plugin-compression'

export default {
plugins: [compression()]
}

对资源服务开启 gzip 支持

nginx.conf
1
2
3
4
5
6
7
8
9
gzip on;
gzip_min_length 1k;
gzip_comp_level 5;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain application/javascript application/x-javascript text/javascript text/xml text/css;
gzip_disable "MSIE [1-6]\.";
gzip_proxied any;
gzip_vary on;

一般推荐提前处理完成 gzip 文件,再直接交由 nginx 服务

执行网络请求时,浏览器会自动带上同源的 Cookie 信息,一定程度上会增大请求头的大小

可以通过精简 Cookie 的内容,来降低请求信息的大小

同时可以对静态资源单独部署,避免请求携带不必要的 Cookie 信息

启用 Keep-Alive

通过给请求头或响应头设置 Keep-Alive 头,通常用于提示连接超时时间和最大请求量

1
2
Connection: Keep-Alive
Keep-Alive: timeout=5, max=1000

减少预检请求发起

可以在跨域请求设置 Access-Control-Max-Age 响应头指定预检请求的缓存期限,从而在指定期限内的跨域请求无需进行预检请求可以直接发起请求

有效期 10min
1
Access-Control-Max-Age: 600

资源相关

script 加载方式

  • 正常模式

JS 会阻碍 DOM 渲染

<script src="main.js"></script>

  • async 模式

异步加载 JS,执行无顺序,加载完成后立即执行

可以用于加载与 DOM 无关的 JS 资源,如埋点统计等

<script async src="main.js"></script>

  • defer 模式

异步加载 JS,执行有顺序,加载完成后统一在 DOMContentLoaded 事件触发前执行

一般情况均可使用 defer 优化 JS 资源的加载,避免 JS 脚本加载与执行阻碍网页的渲染

<script defer src="main.js"></script>

  • module 模式

行为上会类似于 defer 模式

<script type="module" src="main.js"></script>

  • fetchpriority 资源加载优先级

可以利用 fetchpriority HTML 属性指定 script 脚本加载的优先级,优先加载级别高的脚本,延后加载级别低的脚本

资源预加载

需要避免 preloadprefetch 的混用,以避免不必要的二次自由加载

  • preload

预先下载当前页面将使用的资源并缓存(不会执行),会提升资源的优先级

需同时指定 as 属性与 href 属性

<link rel="preload" href="style.css" as="style" />

<link rel="preload" href="main.js" as="script" />

建议指定 type 属性,以避免浏览器下载格式不支持的资源

建议同时指定 crossorigin 属性

  • prefetch

预加载未来页面将使用的资源,并保存在缓存内一段时间,会降低资源的优先级

要求当前页面需为安全上下文

<link rel="prefetch" href="main.js" />

  • modulepreload

类似于 preload

预加载当前页面将使用的模块脚本资源,并进行解析与执行

<link rel="modulepreload" href="main.js" />

  • prerender

预加载目标资源并提前在后台处理执行

仅部分浏览器支持该非标准特性

网络预连接

一般情况下,dns-prefetchpreconnect 都是配对使用

但不建议过度使用 preconnect,仅用于未来一段时间极可能访问或请求的 origin;否则仅应用 dns-prefetch

同时 dns-prefetch 的浏览器兼容性优于 preconnect

建议使用以上两属性的同时指定 crossorigin 属性

  • dns-prefetch

提前执行目标 origin 的 DNS 解析,可以加快未来将访问或请求的 origin 的处理速度(直接使用已预先解析的 DNS 缓存)

<link rel="dns-prefetch" href="https://fonts.googleapis.com/" />

  • preconnect

提前执行目标 origin 的连接 —— DNS 解析、TCP 连接(及 TLS 握手),可以加快未来将访问或请求的 origin 的处理速度

<link rel="preconnect" href="https://fonts.googleapis.com/" />

避用外部依赖

尽量减少对非必要的外部依赖的使用,使用轻量级别替代方案或者自行实现

常见的如:

使用轻量级 day.js 替代 moment.js

使用 ESM 的 lodash-es 替代 CJS 的 lodash

使用 CDN 服务静态资源

CDN 即 Content Delivery Network,其具有分布于多个地域的服务器阵列

CDN 可以降低私有服务器的访问压力

地理位置的距离可能相对更近,一定程度上可以降低网络资源加载的时延

CDN 保证了比较正确的缓存配置

合理配置缓存策略

服务器在响应资源时,通过指定 Expires 响应头或 Cache-Control 响应头来控制浏览器该资源的缓存策略;若被指定为强缓存并且在有效期内直接使用缓存;反之若为被禁止使用缓存,则进行协商缓存,通过 If-Modified-Since 头向服务器提供浏览器缓存的资源的修改时间(在获取资源时服务器通过 Last-Modified 头指定)或通过 If-None-Match 头向服务器提供浏览器缓存的资源的标识符(在获取资源时服务器通过 ETag 头指定)

  • Expires(推荐使用 Cache-Control 代替,其优先级更高)【响应头】

    指定缓存失效的时间

  • Cache-Control【响应头,请求头】

    配置缓存的策略及有效期

    • no-store 不允许缓存

    • no-cache 允许缓存,但使用前需进行服务端验证

    • must-revalidate 允许缓存,有效期内直接使用缓存,超出有效期需进行服务端验证,通常结合 max-age=N 使用

    • max-age=N 指定缓存的有效期

  • Last-Modified【响应头】/ If-Modified-Since【请求头】

    资源最近修改时间

  • ETag【响应头】/ If-None-Match【请求头】

    资源唯一标识符

ServiceWorker 实现可控缓存

利用 ServiceWorker 结合 CacheStorage 实现可控缓存,原理是基于受 ServiceWorker 控制的上下文会在 ServiceWorker 全局触发 fetch 事件,可以通过调用返回的 FetchEventrespondWith() 方法自定义响应

图片字体相关

webp 图片

webp 格式图片大小通常比同等情况下的其他格式图片大小有较大优势,因此若浏览器支持 webp 格式图片,优先使用 webp 格式图片

可以利用离线或在线 webp 图片格式转换工具转换图片格式为 webp

图片懒加载

  • JS 手动控制

    初始不指定图片标签的 src 属性,直到图片需要展示时再指定其 src 属性,避免图片的自动预加载

    1
    <img data-src="/img/png" />
    1
    2
    3
    4
    // 适当情况下调用该方法
    function loadImg(el) {
    el.src = el.getAttribute('data-src')
    }
  • 利用 img 标签特性(更推荐)

    可以设置 img 标签的 loading 属性实现懒加载功能,将属性值指定为 lazy 以惰性加载图片

    同时可以指定 img 标签的 fetchpriority 属性,以控制获取图片资源的优先级,设定为 high 以提升获取的优先级,设定为 low 以降低获取的优先级

    同时可以指定 img 标签的 decoding 属性,以设定解码图片的模式(是否允许在图片解码完成前展示图片),设定为 sync 以同步解码图片,设定为 async 以异步解码图片

    1
    <img src="/img/png" alt="" loading="lazy" fetchpriority="auto" decoding="auto" />

字体图标

将小图标利用字体形式加载,如 IconFont

1
@import url('//at.alicdn.com/t/font_8d5l8fzk5b87iudi.css');
1
<i class="iconfont icon-xxx"></i>

通常加载资源大小会更小,并且能够避免重复加载图片并降低请求数量,且支持修改各类字体样式

内联图片

将小图片转换为 base64 编码内联入 html 文档,可以减少请求数量

webpack 中可以使用 url-loader 插件自动转换内联图片

webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module.exports = {
module: {
rules: [
{
test: /\.(png|jpg|gif)$/i,
use: [
{
loader: 'url-loader',
options: {
limit: 1024 * 8,
},
},
],
},
],
},
}

vite 原生支持内联图片,默认在图片大小小于 4KB 时启用

可以通过 build.assetsInlineLimit 选项配置启用的阈值

vite.config.js
1
2
3
4
5
export default {
build: {
assetsInlineLimit: 4096,
}
}

图片裁剪

对图片生成多个尺寸的备用图片,使用时根据需要加载不同尺寸的图片,减少不必要的资源流量

图片单独部署

将图片等静态资源部署在单独的静态资源服务器或是 CDN 上,避免直接打包到项目中

图片尺寸指定

设置图片标签的尺寸大小,防止图片加载中导致页面布局抖动,影响 CLS 指标

字体按需生成

使用第三方字体库时,尽可能按需生成,避免不必要的全量引入字体库

代码相关

JSON 字符串使用

对于大对象数据,尽量采用 JSON 格式而非 JS 对象格式,因为 JSON 语法比 JS 简单,解析速度更快

如 vite 支持将 JSON 文件打包为 JSON 字符串而非 JS 对象

vite.config.js
1
2
3
4
5
export default {
json: {
stringify: true,
},
}

if 逻辑提前跳出

提前结束的逻辑利于编译器的优化

bad
1
2
3
4
5
6
7
8
9
10
11
function () {
if (A) {
if (B) {
return 'good'
} else {
return 'bad'
}
} else {
return 'bad'
}
}
good
1
2
3
4
5
6
7
function () {
if (A && B) {
return 'good'
}

return 'bad'
}

逻辑能提前结束就提前结束

switch 连续值优化

switch 对于连续值会处理成数组,而数组会具有更高效率的随机访问性能

bad
1
2
3
4
5
6
7
function get(/* @type {1 - 10} */ level) {
if (level >= 10) return 100
if (level >= 9) return 80
if (level >= 6) return 50
if (level >= 1) return 20
return 10
}
good
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function getPrice(level) {
switch(level) {
case 10: return 100
case 9: return 80
case 8:
case 7:
case 6: return 50
case 5:
case 4:
case 3:
case 2:
case 1: return 20
default: return 10
}
}

若条件可以处理成连续的数字,可以使用 switch 来进行优化

循环减少执行次数

对于循环,满足条件或完成任务后即刻跳出,避免不必要的执行损耗

bad
1
2
3
4
5
6
7
8
9
function find(data) {
let result = null
for (let i = 0; i < data.length; i++) {
if (data[i].key === KEY) {
result = data[i]
}
}
return result
}
good
1
2
3
4
5
6
7
function find(data) {
for (let i = 0; i < data.length; i++) {
if (data[i].key === KEY) {
return data[i]
}
}
}

提取集合数组长度

在循环处理数组、集合等容器的元素,若可以保证容器的容量不会发生变化,可以提前提取容器的容量,避免在循环中重复获取

适当使用位运算

对于一些和 2 相关的乘除法或者条件相关的,可以用位运算替代

bad
1
const A = 2 ** 8
good
1
2
const A = 2 << 3
const isPowerOfTwo = n => (n > 0) && (n & (n - 1) === 0)

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;

链接


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