File System API

File System API 允许使用内置文件系统,包含读取、修改和移除文件,扩展的 File System Access API 允许访问本地文件系统

  • FileSystemHandle 接口作为文件句柄或目录句柄的通用接口
  • FileSystemFileHandle 接口作为文件句柄的接口
  • FileSystemDirectoryHandle 接口作为目录句柄的接口
  • FileSystemSyncAccessHandle 接口作为同步访问句柄的接口
  • FileSystemWritableFileStream 接口作为读写本地文件流的接口
  • StorageManager 接口上的 getDirectory() 方法用于获取 Origin Private File System 的根目录句柄

通常而言,File System API 需要用户明确的许可,但 OPFS —— Origin Private File System 机制除外。

通常 File System API 的大多数操作是异步的,支持同步的 FileSystemSyncAccessHandle 接口仅在 Web Worker 内可用且仅允许使用于 OPFS。

初始化

获取 OPFS 的根目录句柄

使用 StorageManager 接口的 getDirectory() 方法获取 OPFS 的根目录句柄

返回一个 Promise 的 FileSystemDirectoryHandle 对象

并在用户代理无法使用请求的文件目录建立本地 OPFS 索引时抛出 SecurityError 异常

文件及目录操作

通过 FileSystemHandle 接口进行文件目录相关的操作,该接口是 FileSystemFileHandle 接口与 FileSystemDirectoryHandle 接口的父接口

句柄类型

FileSystemHandle 接口的 kind 属性返回一个字符串枚举值,代表句柄的类型

当当前句柄为 FileSystemFileHandle 时,返回 'file'

当当前句柄为 FileSystemDirectoryHandle 时,返回 'directory'

句柄名称

FileSystemHandle 接口的 name 属性返回一个字符串,代表句柄对应的文件或目录的名称

句柄比较

FileSystemHandle 接口的 isSameEntry() 方法判断当前句柄与传入的句柄是否指向同一个文件或目录

方法接收一个 FileSystemHandle

方法返回一个 Promise 的 boolean

权限操作

FileSystemHandle 接口的 queryPermission() 方法用于枚举当前句柄的指定权限

FileSystemHandle 接口的 requestPermission() 方法用于请求当前句柄的指定权限

方法均支持传入一组可选的描述符,唯一 mode 参数可以为 'read''readwrite' 之一,默认为 'read'

方法均返回一个 Promise 的 PermissionStatus 接口实例

文件操作

通过 FileSystemFileHandle 接口进行文件相关的操作,该接口继承自 FileSystemHandle 接口

获取文件

FileSystemFileHandle 接口的 getFile() 方法可以用于读取文件相关信息及用于网络传输

返回一个 Promise 的 File 对象

抛出一个 NotAllowedError 若未授予访问权限

获取同步访问句柄

FileSystemFileHandle 接口的 createSyncAccessHandle() 方法可以用于同步读写文件内容,但仅允许在专属 Worker 内部使用,且目前仅适用于 OPFS 机制

返回一个 Promise 的 FileSystemSyncAccessHandle 对象

抛出一个 NotAllowedError 若未授予访问权限

抛出一个 NoModificationAllowedError 若无法建立文件锁

抛出一个 InvalidStateError 若句柄无法表示 OPFS 中的文件

获取可写文件流

FileSystemFileHandle 接口的 createWritable() 方法可以用于修改文件内容

可以传入一组可选的配置项,唯一选项 keepExistingData 指定

返回一个 Promise 的 FileSystemWritableFileStream 对象

抛出一个 NotAllowedError 若未授予访问权限

目录操作

通过 FileSystemDirectoryHandle 接口进行目录相关的操作,该接口继承自 FileSystemHandle 接口

目录遍历

FileSystemDirectoryHandle 接口支持异步遍历,包含 [@@asyncIterator]() 以及 entries()keys()values() 等方法

获取路径

FileSystemDirectoryHandle 接口的 resolve() 方法用于获取当前句柄到指定句柄的相对路径

需要传入一个 FileSystemHandle,代表目标句柄

返回一个 Promise 的字符串数组或 null

获取子目录

FileSystemDirectoryHandle 接口的 getDirectoryHandle() 方法用于获取当前目录下的指定名称的子目录的句柄

需要传入一个字符串参数,代表目录的名称,与 FileSystemHandle.name 相符

可以传入一组配置项,唯一参数 create 为一个布尔值,指定目录不存在情况下是否创建目录,默认值为 false

返回一个 Promise 的 FileSystemDirectoryHandle 对象

抛出一个 TypeErrorname 参数不是字符串或为非法的文件系统名称

抛出一个 TypeMismatchError 若匹配到的为文件而非目录

抛出一个 NotFoundError 若未找到目录且 create 选项设定为 false

抛出一个 NotAllowedError 若未授予访问权限

获取子文件

FileSystemDirectoryHandle 接口的 getFileHandle() 方法用于获取当前目录下的指定名称的子文件的句柄

需要传入一个字符串参数,代表目录的名称,与 FileSystemHandle.name 相符

可以传入一组配置项,唯一参数 create 为一个布尔值,指定目录不存在情况下是否创建目录,默认值为 false

返回一个 Promise 的 FileSystemFileHandle 对象

抛出一个 TypeErrorname 参数不是字符串或为非法的文件系统名称

抛出一个 TypeMismatchError 若匹配到的为目录而非文件

抛出一个 NotFoundError 若未找到文件且 create 选项设定为 false

抛出一个 NotAllowedError 若未授予访问权限

删除子目录或子文件

FileSystemDirectoryHandle 接口的 removeEntry() 方法用于删除当前目录下的指定名称的子目录或子文件的句柄

需要传入一个字符串参数,代表目录的名称,与 FileSystemHandle.name 相符

可以传入一组配置项,唯一参数 recursive 为一个布尔值,指定是否递归删除,默认值为 false

返回一个 Promise 的 undefined

抛出一个 TypeErrorname 参数不是字符串或为非法的文件系统名称

抛出一个 InvalidModificationError 若目标为目录,并且包含子文件或子目录,并且 recursive 设置为 false

抛出一个 NotFoundError 若未找到文件或目录

抛出一个 NotAllowedError 若未授予访问权限

文件读取

文件读取通过 FileSystemFileHandle 接口的 getFile() 方法获取到对应的 File 实例实现

文件修改

文件修改通过 FileSystemFileHandle 接口的 createWritable() 方法获取到对应的 FileSystemWritableFileStream 实例实现

FileSystemWritableFileStream 接口继承自 WritableStream 接口

FileSystemWritableFileStream 接口的更改不会立即反映到实际的文件上,仅在关闭流之后才会同步其产生的更改;原因是对流的更改,至少会存储到一个临时文件中,仅在流关闭之后,才会将更改同步到实际的文件中

移动指针

使用 FileSystemWritableFileStream 接口的 seek() 方法移动文件指针的位置

需要传入一个正整数 position 参数,代表文件指针的位置

返回一个 Promise

抛出一个 NotAllowedError 若未授予访问权限

抛出一个 TypeErrorposition 参数不是正整数或未传递

文件写入

使用 FileSystemWritableFileStream 接口的 write() 方法用于向文件中写入内容

可以传入一个 data 参数,代表需要写入文件的内容,可以是 ArrayBuffer TypedArray DataView Blobstring

亦可以传入一组配置项:

type 选项传入一组字符串枚举,指定操作的模式,可以是 "write" "seek""truncate"

data 选项,代表需要写入文件的内容,可以是 ArrayBuffer TypedArray DataView Blobstring,在 "write" 模式下是必须的

position 选项,代表需要移动的文件指针的目标位置,是一个正整数,在 "seek" 模式下是必须的;同样可以在 "write" 模式下使用,此时代表写入内容的目标位置

size 选项,代表需要文件流的大小,是一个正整数,在 "truncate" 模式下是必须的

返回一个 Promise

抛出一个 NotAllowedError 若未授予访问权限

抛出一个 TypeError 若传入参数非法

抛出一个 InvalidStateErrorposition 选项的值超出文件大小

文件尺寸修改

使用 FileSystemWritableFileStream 接口的 truncate() 方法改变文件的尺寸;方法可能会改变文件指针的位置

需要传入一个正整数 size 参数,代表目标的文件尺寸;若参数超出原有尺寸,则扩大当前文件并使用空内容填充扩大的部分,反之会裁剪当前文件

返回一个 Promise

抛出一个 NotAllowedError 若未授予访问权限

抛出一个 TypeErrorsize 参数不是正整数或未传递

文件同步读写

文件同步读写通过 FileSystemFileHandle 接口的 createSyncAccessHandle() 方法获取到对应的 FileSystemSyncAccessHandle 实例实现

FileSystemSyncAccessHandle 实例仅支持在专属 Worker 中使用,且目前仅适用于 OPFS 机制

因为其无需进行权限检查,其相对而言具有更好的性能

创建 FileSystemSyncAccessHandle 实例会创建与之对应的文件锁,阻止对该文件创建其他的 FileSystemSyncAccessHandle 实例或 FileSystemWritableFileStream 实例,直到 FileSystemSyncAccessHandle 实例被销毁

FileSystemSyncAccessHandle 接口的 read() 方法读取文件内容

方法传入一个 buffer 参数,可以是 ArrayBuffer SharedArrayBuffer TypedArrayDataView,代表用于存储读取文件内容的缓存区

方法支持传入一个可选的配置项:其 at 参数指定开始读取文件内容的起始位置

方法返回一个正整数,代表读取的文件内容的字节数

方法在若对应的句柄已关闭的情况下,抛出一个 InvalidStateError

FileSystemSyncAccessHandle 接口的 write() 方法向文件写入内容

方法传入一个 buffer 参数,可以是 ArrayBuffer SharedArrayBuffer TypedArrayDataView,代表将用于写入的文件内容

方法可以传入一个可选的配置项:其 at 参数指定开始写入文件内容的起始位置

方法返回一个正整数,代表写入的文件内容的字节数

方法在若对应的句柄已关闭的情况下,抛出一个 InvalidStateError

读取尺寸

FileSystemSyncAccessHandle 接口的 getSize() 方法返回文件的尺寸

方法返回一个正整数,代表文件内容的字节数

方法在若对应的句柄已关闭的情况下,抛出一个 InvalidStateError

更改尺寸

FileSystemSyncAccessHandle 接口的 truncate() 方法用于更改文件的尺寸

方法传入一个 newSize 参数,需要是一个正整数,代表将更改的文件的目标大小

方法在若对应的句柄已关闭的情况下,抛出一个 InvalidStateError

方法在若对应的句柄已关闭的情况下,抛出一个 InvalidStateError

刷新缓冲区

FileSystemSyncAccessHandle 接口的 flush() 方法用于将缓冲的更改同步至存储,通常只在特定时间段需要将缓冲的更改同步至存储时使用,否则可以让底层自行处理

关闭句柄

FileSystemSyncAccessHandle 接口的 close() 方法用于关闭当前句柄,释放文件锁

类型

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 FileSystemHandle {
readonly kind: FileSystemHandleKind
readonly name: string
isSameEntry(other: FileSystemHandle): Promise<boolean>
}

interface FileSystemFileHandle extends FileSystemHandle {
readonly kind: 'file'
createSyncAccessHandle(): Promise<FileSystemSyncAccessHandle>
createWritable(options?: FileSystemCreateWritableOptions): Promise<FileSystemWritableFileStream>
getFile(): Promise<File>
}

interface FileSystemDirectoryHandle extends FileSystemHandle {
readonly kind: 'directory'
getDirectoryHandle(name: string, options?: FileSystemGetDirectoryOptions): Promise<FileSystemDirectoryHandle>
getFileHandle(name: string, options?: FileSystemGetFileOptions): Promise<FileSystemFileHandle>
removeEntry(name: string, options?: FileSystemRemoveOptions): Promise<void>
resolve(possibleDescendant: FileSystemHandle): Promise<string[] | null>
}

interface FileSystemWritableFileStream extends WritableStream {
seek(position: number): Promise<void>
truncate(size: number): Promise<void>
write(data: FileSystemWriteChunkType): Promise<void>
}

interface FileSystemSyncAccessHandle {
close(): void;
flush(): void;
getSize(): number;
read(buffer: AllowSharedBufferSource, options?: FileSystemReadWriteOptions): number;
truncate(newSize: number): void;
write(buffer: AllowSharedBufferSource, options?: FileSystemReadWriteOptions): number;
}

interface FileSystemCreateWritableOptions {
keepExistingData?: boolean
}

interface FileSystemGetDirectoryOptions {
create?: boolean
}

interface FileSystemGetFileOptions {
create?: boolean
}

interface FileSystemReadWriteOptions {
at?: number
}

interface FileSystemRemoveOptions {
recursive?: boolean
}

type FileSystemWriteChunkType = BufferSource | Blob | string | WriteParams

链接

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[]>
};

链接

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>
}

链接

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;
}

链接


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