Window Controls Overlay API

Window Controls Overlay API 给 PWA 应用提供了管理默认系统的应用标题栏的能力,允许应用完全掌控应用窗口的区域,不过仅支持 PC 端 PWA 应用

该 API 通过 WindowControlsOverlay 接口提供了相关功能,并通过 navigator.windowControlsOverlay 对外暴露该接口实例

使用该 API 需要在 PWA 应用的 Manifest 文件的 display_override 选项指定 window-controls-overlay

WindowControlsOverlay 的信息

WindowControlsOverlay 接口的 visible 只读属性表示了应用标题栏的可见性

WindowControlsOverlay 接口的 getTitlebarAreaRect() 方法返回了应用标题栏的几何信息,方法返回一个 DOMRect 接口实例

WindowControlsOverlay 接口的 geometrychange 事件在应用标题栏的可见性和几何信息变化时触发,事件返回一个 WindowControlsOverlayGeometryChangeEvent 事件实例

1
2
3
4
5
6
7
8
9
10
11
navigator.windowControlsOverlay.addEventListener('geometrychange', (e) => {
const { visible, titlebarAreaRect: rect } = e

if (visible) {
console.log('visible')

console.log('rect info', rect)
} else {
console.log('not visible')
}
})

相关的 CSS 环境变量

  • titlebar-area-x 应用标题栏左上角横坐标

  • titlebar-area-y 应用标题栏左上角纵坐标

  • titlebar-area-width 应用标题栏宽度

  • titlebar-area-height 应用标题栏高度

可以通过 env() CSS 函数使用 CSS 环境变量

1
2
3
4
env(titlebar-area-x)
env(titlebar-area-y)
env(titlebar-area-width)
env(titlebar-area-height)

示例

类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface Navigator {
readonly windowControlsOverlay: WindowControlsOverlay
}

interface WindowControlsOverlay extends EventTarget {
readonly visible: boolean
getTitlebarAreaRect(): DOMRect
ongeometrychange: ((this: WindowControlsOverlay, ev: WindowControlsOverlayGeometryChangeEvent) => any) | null
}

interface WindowControlsOverlayGeometryChangeEvent extends Event {
readonly titlebarAreaRect: DOMRect
readonly visible: boolean
}

链接

Badging API

Badging API 用于设置 PWA 应用的图标上的徽章信息

该 API 需要在 Secure Context 下使用

某些情况下该 API 需要请求用户授予 notifications 权限,并可以调用 Notification.requestPermission() 方法来请求获取相关权限

设置 Badge

使用 Navigator 接口上的 setAppBadge() 方法给图标设置徽章

方法支持传递一个可选的数字参数,徽章将显示为对应的数字;若未传递,徽章将显示为对应的点

方法返回一个 Promise 的 undefined

方法不支持时会抛出 NotSupportedError 异常

1
2
navigator.setAppBadge()
navigator.setAppBadge(10)

清除 Badge

使用 Navigator 接口的 clearAppBadge() 方法清除图标上设置的徽章

方法返回一个 Promise 的 undefined

方法不支持时会抛出 NotSupportedError 异常

1
2
navigator.setAppBadge(0)
navigator.clearAppBadge()

使用 Navigator 接口的 setAppBadge() 方法并传递参数 0 同样具有类似的效果

权限 API

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

示例

类型

1
2
3
4
5
6
7
8
9
interface Navigator {
clearAppBadge(): Promise<void>
setAppBadge(contents?: number): Promise<void>
}

interface WorkerNavigator {
clearAppBadge(): Promise<void>
setAppBadge(contents?: number): Promise<void>
}

链接

Content Index API

Content Index API 允许网站注册离线启用的内容,向用户告知网站支持的离线内容并允许开发者对其进行管理

该 API 仅支持 HTML 文档对应的 URL,不支持如媒体资源等类型的 URL

该 API 仅支持列举已注册的内容,不支持进行查找等操作

一般的使用方式是利用一个列表页,用于展示已注册的内容

需要注意的是,该 API 并非直接缓存内容,实际的缓存需要利用 Cache Storage 等策略实施

介于该 API 依赖于 ServiceWorker,因此该 API 同样需在 Secure Context 下使用,且需遵循同源策略

通过 ServiceWorkerRegistration.index 暴露 ContentIndex 接口实例使用

添加离线内容

通过 ContentIndex 接口的 add() 方法添加离线内容

方法支持传入一组配置项

  • 参数 id 指定离线内容的唯一标识符
  • 参数 url 指定离线内容的 URL,需要与当前网页或脚本同源
  • 参数 title 指定离线内容的标题
  • 参数 description 指定离线内容的描述
  • 可选参数 icons 指定离线内容的图标组,每组图标对象支持指定 src 参数和 可选的 sizestype 参数,默认值是一个空数组
  • 可选参数 category 指定离线内容的类别,可选的值为 '''homepage''article''video''audio',默认值是 ''

方法返回一个 Promise 的 undefined

方法在以下情况下会抛出一个 TypeError 异常

  • 当前 ContentIndex 对应的 ServiceWorker 未激活或 ServiceWorker 未包含 FetchEvent
  • idtitledescriptionurl 参数未指定或参数类型不为字符串或参数为空串
  • icons 参数某个 icon 的 URL 的类型不是图像或获取对应 icon 出现网络异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
self.registration.index.add({
id: 'post',
url: '/posts/post.html',
title: 'Post',
description: 'Post Information',
icons: [
{
src: '/media/dark.png',
sizes: '128x128',
type: 'image/png',
},
],
category: 'article',
})

获取离线内容

通过 ContentIndex 接口的 getAll() 方法获取离线内容

方法返回一个 Promise 的代表离线内容的数组,结构同 ContentIndex.add() 方法的配置项参数

1
self.registration.index.getAll()

删除离线内容

通过 ContentIndex 接口的 delete() 方法删除离线内容

方法传入一个代表待删除的离线内容的 id 的字符串

方法返回一个 Promise 的 undefined

1
2
3
4
5
self.registration.index.delete(id).then(() => (
self.caches.open('v1').then((cache) => (
cache.delete(e.id)
))
))

需要注意的是,调用该方法同时,需要手动从存储中移除对应的离线内容

此外,当离线内容被通过用户代理移除而非手动调用 ContentIndex.delete() 方法移除时,会在 ServiceWorker 全局触发 contentdelete 事件,并返回一个 ContentIndexEvent 事件

ContentIndexEvent 事件继承自 ExtendableEvent 事件,其属性 id 反映了被删除的离线内容的 id

1
2
3
4
5
6
7
self.addEventListener('contentdelete', (e) => {
e.waitUntil(
self.caches.open('v1').then((cache) => (
cache.delete(e.id)
))
)
})

通常利用该事件同步移除与待移除页面的相关的资源存储

示例

类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type ContentCategory = '' | 'homepage' | 'article' | 'video' | 'audio'

interface ContentDescription {
id: string
title: string
description: string
category?: ContentCategory
icons?: ImageResource[];
url: string
}

interface ContentIndex {
add(description: ContentDescription): Promise<undefined>
delete(id: string): Promise<undefined>
getAll(): Promise<ContentDescription[]>
};

链接

PWA

PWA 即 Progressive Web App ———— 渐进式网络应用

PWA 技术允许用户像 Native 应用一样使用 Web 应用,支持多平台和多设备访问,并且支持在线或离线访问

PWA 技术的核心是 ServiceWorker 技术,并基于 ServiceWorker 技术支持离线访问

PWA 特点

  • 可通过 URL 访问并被搜索引擎抓取
  • 可以安装到本地(A2HS)
  • 可以使用 URL 分享
  • 可以在离线状态访问
  • 适配老版浏览器访问,并支持在新浏览器中使用更多新特性
  • 支持在有新内容时更新
  • 能适配各种尺寸屏幕
  • 仅通过 HTTPS 提供服务

PWA 创建

  1. 页面通过 link 标签引入 manifest 文件

    文件后缀名可以是 json 或者 webapp、webmanifest

    1
    <link rel="manifest" href="manifest.json" />

    该文件是一个 JSON 的语法,必须指定的项为 nameicons(对于 Chromium 系浏览器 start_urldisplaydisplay_override 也是需要指定的)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    {
    "name": "My PWA",
    "icons": [
    {
    "src": "icons/512.png",
    "type": "image/png",
    "sizes": "512x512"
    }
    ]
    }

    name 指定应用的名称,接受一个字符串

    icons 指定应用的图标,接受一个数组,数组各项可以指定 srcsizestype 项,分别代表图标的 URL、尺寸及 MIME 类型

    manifest.json 文件的详细配置可以参考相关的文档

  2. 页面中注册 ServiceWorker,且 ServiceWorker 中监听了 fetch 事件

  3. 页面须启用 Secure Context,即使用 HTTPS 协议或者为本地资源

PWA 下载

PWA 下载可以通过浏览器访问对应的网页实现

通常浏览器检测到网页支持下载为 PWA 时,会显示一个默认的“下载为 PWA”的按钮,也可以自定义“下载为 PWA”的按钮

通过监听全局的 beforeinstallprompt 事件获取到 BeforeInstallPromptEvent 事件实例并存储,通常该事件在页面加载时即触发;在必要时刻调用 BeforeInstallPromptEvent.prompt() 方法以使用户确认下载 PWA 应用;下载完成后会在全局触发 appinstalled 事件

PWA 下载亦可以通过应用商店等场景下载

PWA 原理

PWA 实质是仅将应用行为上类似于原生应用,如在桌面显示图标,在应用列表显示,支持卸载等;并不会将应用程序的资源文件主动下载至本地,具体策略由开发者通过 IndexedDB、ServiceWorker、Cache Storage 等开发实施

示例

ServiceWorker VI

ServiceWorker 导航预加载

导航预加载通过 NavigationPreloadManager 接口提供,并通过 ServiceWorkerRegistration.navigationPreload 属性暴露

对于使用 ServiceWorker 的页面,网页的网络请求会向 ServiceWorker 发送 fetch 事件直至返回响应,若此时 ServiceWorker 未启动,网页的网络请求会等待 ServiceWorker 激活后再进行处理;导航预加载允许网页的获取资源请求在 ServiceWorker 激活前提前开始下载,以避免阻碍页面的显示

启用导航预加载

NavigationPreloadManager 接口的 enable() 方法用于启用资源预加载管理

停用导航预加载

NavigationPreloadManager 接口的 disable() 方法用于停用资源预加载管理

管理导航预加载

NavigationPreloadManager 接口的 setHeaderValue() 方法用于设置导航预加载中发送的请求的请求头 Service-Worker-Navigation-Preload 的值

NavigationPreloadManager 接口的 getState() 方法用于获取导航预加载的状态

基本使用

启用导航预加载

1
2
3
4
5
self.addEventListener('activate', (e) => {
e.waitUntil(
self.registration.navigationPreload.enable()
)
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
self.addEventListener('fetch', (e) => {
e.responseWith(
(async () => {
const cache = await self.caches.match(e.request)

if (cache != null) {
return cache
}

const preload = await e.preloadResponse

if (preload != null) {
return preload
}

return fetch(e.request)
})
)
})

链接

SharedWorker

SharedWorker 是 HTML 标准定义的 Web API 的一部分,是一种特殊的 Worker,支持在多个上下文(例如 window、iframe 甚至 Worker)之间共享

同时,SharedWorker 的全局上下文 SharedWorkerGlobalScope 也与 Worker 不同

创建 SharedWorker

和 Worker 一样,通过调用 SharedWorker() 构造函数来创建

1
const worker = new SharedWorker('./worker.js')

SharedWorker() 构造函数支持传入一组可选的配置项,与 Worker() 构造函数相同
SharedWorker() 构造函数也支持直接传入一个字符串,同配置项的 name 参数;特别的,SharedWorker() 中的 name 参数作为唯一的一个标识符,在创建新的与之前的拥有相同 URL 的 SharedWorker 时有一定作用

SharedWorker 通过脚本文件 URL 和 name 参数确定是否为同一个 SharedWorker

SharedWorker 消息传递

与 Worker 不同,Client 端通过创建的 SharedWorker 实例上的 port 属性暴露的 MessagePort 接口实例,调用其上的 postMessage() 方法实现发送消息

Client 端通过监听 SharedWorker 实例上的 message 事件实现接收到消息

1
2
3
4
5
6
7
8
9
worker.port.start()

worker.port.postMessage('message from client')

worker.port.close()

worker.port.addEventListener('message', (e) => {
console.log('receive message in client: ', e.data)
})

SharedWorker 环境下接收消息,需要监听 connect 事件,从而获取到新的对应的 MessagePort 实例;监听 MessagePort 实例的 message 事件接收消息

SharedWorker 环境下接收消息,同样需要通过调用 MessagePort 实例的 postMessage() 方法实现发送消息

1
2
3
4
5
6
7
8
9
10
11
12
13
self.addEventListener('connect', (e) => {
const port = e.ports.at(0)

port.addEventListener('message', (e) => {
console.log('receive message in worker: ', e.data)
})

port.start()

port.postMessage('message from worker')

port.close()
})

通常,在 connect 事件回调函数内,会把接收到的 port 存储下来,以便之后使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const ports = []

self.addEventListener('connect', (e) => {
const port = e.ports.at(0)

port.addEventListener('message', handleReceiveMessage)

port.start()

ports.push(port)
})

function sendMessage() {
for (const port of ports) {
port.postMessage('message from worker')
}
}

卸载 SharedWorker

仅支持在 SharedWorker 环境内调用 close() 方法,来卸载当前 Worker

1
self.close()

SharedWorker 生命周期

SharedWorker 生命周期与 Client 端的生命周期独立,当任一页面创建 SharedWorker 时其生命周期开始,在没有页面使用 SharedWorker 时其生命周期结束

SharedWorker 全局环境

SharedWorker 全局环境通过 SharedWorkerGlobalScope 表示,该接口继承自 WorkerGlobalScope,它与 Worker 全局环境差别不大

示例

类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface SharedWorker extends EventTarget, AbstractWorker {
constructor(scriptURL: string | URL, options?: string | WorkerOptions);
readonly port: MessagePort;
}

interface SharedWorkerGlobalScope extends WorkerGlobalScope {
readonly name: string;
close(): void;
onconnect: ((this: SharedWorkerGlobalScope, ev: MessageEvent) => any) | null;
}

interface WorkerOptions {
credentials?: RequestCredentials
name?: string
type?: WorkerType
}

type WorkerType = 'classic' | 'module'

链接

Worker

Web Worker 是 HTML 标准定义的 Web API 的一部分,可以在后台运行一个耗时的任务,避免因长期执行 JS 任务而阻塞用户界面渲染与交互

Web Worker 可以被 Window 环境创建,也可以被其他的 Worker 创建

Web Worker 是独立于主线程的一个线程,具有独立的作用域,其中运行的任务不会阻塞主线程

Web Worker 中的全局作用域 DedicatedWorkerGlobalScope 与 Window 的全局作用域不同,Window 环境中部分 API 在 Worker 环境中不可用或受到一定的限制

Web Worker 线程与主线程之间的通信通过 message 机制实现,传递的数据通过结构化拷贝算法传递,因此通常不存在处理线程安全的需要

创建 Worker

通过调用 Worker() 构造函数,传入 Worker 脚本的 URL,来创建一个 Worker

1
const worker = new Worker('./worker.js')

Worker 脚本需要与 Client 同域

Worker() 构造函数支持传入一组可选的配置项
type 参数指定脚本的类型,值可以是 classicmodule,默认值是 classic
name 参数指定 Worker 的名称,在 debug 时候有一定作用,在 Worker 内可以通过 name 只读属性访问
credentials 参数指定 Worker 的 credentials 选项,允许的值可以是 omitsame-origininclude
若传入的 URL 解析失败,会抛出一个 SyntaxError 错误
若接收到的脚本文件并非 JavaScript 格式,会抛出 NetworkError 错误
若当前文档环境不支持创建 Worker,如未遵守同源策略,会抛出 SecurityError 错误

Worker 消息传递

无论是 Worker 端还是 Client 端,通过调用 postMessage() 方法实现发送消息,通过监听 message 事件实现接收消息

Client 发送消息

1
worker.postMessage('message from client')

Client 接收消息

1
2
3
worker.addEventListener('message', (e) => {
console.log('receive message in client: ', e.data)
})

Worker 发送消息

1
self.postMessage('message from worker')

Worker 发送消息

1
2
3
self.addEventListener('message', (e) => {
console.log('receive message in worker: ', e.data)
})

此外,可以选择传入一组数组或包含 transfer 参数的配置项,定义需要转移所有权的对象

所有权被转移后,对应对象在原环境内不再可用,而是仅在新环境内可用

普通消息

当然,传递的消息可以不仅仅是 string 类型,可以是其他任何可以被结构化拷贝算法执行的数据,包括:

  • number
  • string
  • boolean
  • null
  • undefined
  • bigint
  • 普通 object
  • Array
  • RegExp
  • Date
  • Error
  • Set
  • Map
  • Blob
  • ArrayBuffer
  • TypedArray
  • 等等

结构化拷贝算法,严格来说,与 JSON.stringfy()JSON.parse() 行为上不同。在结构化拷贝算法中,试图复制 Function 参数会抛出异常;但结构化拷贝算法支持复制包含循环对象的对象

可转移对象

可以转移的对象可以是:

  • ArrayBuffer
  • MessagePort
  • ReadableStream
  • WritableStream
  • TransformStream
  • WebTransportReceiveStream
  • AudioData
  • ImageBitmap
  • VideoFrame
  • OffscreenCanvas
  • RTCDataChannel

可共享对象

SharedArrayBuffer 可以用于多个线程之间的共享数据,并利用 Atomics 实现线程同步与线程锁功能。

启用该 API 需要 secure context,并且需要 cross-origin isolate,可以通过检测 isSecureContext 全局变量和 crossOriginIsolated 全局变量来确定是否可以使用 SharedArrayBuffer

卸载 Worker

通过调用 worker 实例的 terminate() 方法,来卸载一个 Worker

1
worker.terminate()

或者调用 Worker 环境中的 close() 方法,来卸载当前的 Worker

1
self.close()

卸载是立即执行的,不会等待 worker 内部任务的完成

Worker 全局环境

Worker 全局环境通过 DedicatedWorkerGlobalScope 表示,该接口继承自 WorkerGlobalScope

Worker 全局环境的 messageerror 事件会在传递的消息无法解析时触发,可用用于监听发送失败的消息(Worker 对象上同样存在)

Worker 全局环境的 importScripts() 方法可以导入一组同源的脚本文件,并在 Worker 全局环境下执行

示例

类型

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

interface Worker extends EventTarget, AbstractWorker {
constructor(scriptURL: string | URL, options?: WorkerOptions)
postMessage(message: any, transfer: Transferable[]): void
postMessage(message: any, options?: StructuredSerializeOptions): void
terminate(): void
}

interface DedicatedWorkerGlobalScope extends WorkerGlobalScope {
readonly name: string
close(): void
onmessage: ((this: DedicatedWorkerGlobalScope, ev: MessageEvent) => any) | null
onmessageerror: ((this: DedicatedWorkerGlobalScope, ev: MessageEvent) => any) | null
postMessage(message: any, transfer: Transferable[]): void
postMessage(message: any, options?: StructuredSerializeOptions): void
}

interface StructuredSerializeOptions {
transfer?: Transferable[]
}

interface WorkerOptions {
credentials?: RequestCredentials
name?: string
type?: WorkerType
}

type WorkerType = 'classic' | 'module'

链接

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)

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