此 API 通过localStorage/sessionStorage 和兼容浏览器中的源私有文件系统 提供数据库持久化。
⚠️警告:隐身模式和访客模式下的限制
大多数浏览器都提供“隐身”和/或“访客”浏览模式,这些模式会故意更改或禁用浏览器的某些功能。在这些模式下运行时,存储功能可能会受到不利影响,例如配额降低或完全缺乏持久性。每个浏览器施加的确切限制各不相同,但并非完全出乎意料的是,此页面上描述的持久化功能在这些“隐身”模式下运行时,要么比文档建议的更受限制,要么甚至可能完全不可用。
“我们如何提前检测这些情况?”这是一个合理的问题,但浏览器制造商有意使其难以检测这些模式,以防止例如网站限制对隐身模式用户的访问。任何当前在任何给定浏览器中检测此模式的方法都可能很快过时,因为浏览器制造商会注意到并更改内容以使这些模式对访问的网站更加不透明,因此我们无法提供有关如何规避它们的任何建议。
键值文件系统 (kvvfs):localStorage
和 sessionStorage
kvvfs
是一种 sqlite3_vfs 实现,旨在将整个 sqlite3 数据库存储在 localStorage
或 sessionStorage
对象中。这些对象仅在主 UI 线程中可用,而不是 Worker 线程中,因此此功能仅在主线程中可用。kvvfs
将数据库的每一页存储到存储对象的单独条目中,并将每个数据库页面编码为 ASCII 格式,以便使其对 JS 友好。
此文件系统**每个存储对象仅支持一个数据库**。也就是说,最多只能有一个localStorage
数据库和一个sessionStorage
数据库。
要使用它,请将文件系统名称“kvvfs”传递给任何接受文件系统名称的数据库打开例程。数据库的文件名**必须**为local
或session
,或其别名:localStorage:
和:sessionStorage:
。任何其他名称都将导致数据库打开失败。使用URI 样式名称 时,请使用以下之一
file:local?vfs=kvvs
file:session?vfs=kvvs
在主 UI 线程中加载时,以下实用程序方法将添加到 sqlite3.capi
命名空间中
sqlite3_js_kvvfs_size(which='')
返回 kvvfs 使用的存储字节数的**估计值**。sqlite3_js_kvvfs_clear(which='')
清除所有 kvvfs 拥有的状态并返回它删除的记录数(每个数据库页面一个记录)。
在这两种情况下,参数都可以是("local"
、"session"
、""
)之一。在前两种情况下,仅作用于localStorage
或sessionStorage
,在后一种情况下,两者都将被作用于。
存储限制**很小**:通常为 5MB,请注意 JS 使用两字节字符编码,因此有效存储空间小于此值。将数据库编码为 JS 可使用的格式速度很慢,并且会消耗大量空间,因此这些存储选项不建议用于任何“严肃工作”。相反,添加它们的主要原因是为了让没有OPFS 支持 的客户端至少能够获得某种形式的持久性。
存储空间满时,修改数据库的数据库操作将失败。由于将数据库存储在持久 JS 对象中固有的低效性,这需要以文本形式对其进行编码,因此 kvvfs 中的数据库比其磁盘上的对应文件更大,并且速度也慢得多(从计算角度来看,尽管对于许多客户端来说,感知到的性能可能足够快)。
JsStorageDb:简单易用的 kvvfs
使用OO1 API 可以更轻松地使用 kvvfs。有关详细信息,请参阅JsStorageDb 类。
将数据库导入 kvvfs
将现有数据库导入 kvvfs 最直接的方法是使用来自单独数据库的VACUUM INTO
。例如
let db = new sqlite3.oo1.DB();
db.exec("create table t(a); insert into t values(1),(2),(3)");
db.exec("VACUUM INTO 'file:local?vfs=kvvfs'");
// Will fail if there's already a localStorage kvvfs:
// sqlite3.js:14022 sqlite3_step() rc= 1 SQLITE_ERROR SQL = VACUUM INTO 'file:local?vfs=kvvfs'
// But we can fix that by clearing the storage:
sqlite3.capi.sqlite3_js_kvvfs_clear('local');
// Then:
db.exec("VACUUM INTO 'file:local?vfs=kvvfs'");
db.close();
let ldb = new sqlite3.oo1.JsStorageDb('local');
ldb.selectValues('select a from t order by a'); // ==> [1,2,3]
源私有文件系统 (OPFS)
源私有文件系统OPFS 是一个提供浏览器端持久存储的 API,sqlite3 可以用它来存储数据库1。
OPFS 仅在 Worker 线程上下文中可用,而不是主 UI 线程中。
截至 2023 年 7 月,已知以下浏览器具有必要的 API
- 大约从 2022 年年中开始发布的 Chromium 派生浏览器。从 v108(2022 年 11 月)开始,一些 OPFS API 从异步更改为同步,这会影响客户端代码(即此库)必须如何处理它们。
- Firefox v111(2023 年 3 月)及更高版本
- Safari 16.4(2023 年 3 月)及更高版本
此库提供了多种在 OPFS 中存储数据库的解决方案,每种解决方案都有不同的权衡。
通过 sqlite3_vfs 使用 OPFS
此支持仅在从 Worker 线程加载sqlite3.js
时可用,无论它是在其自己的专用 Worker 中加载,还是与客户端代码一起在 Worker 中加载。此 OPFS 包装器完全用 JavaScript 实现了一个sqlite3_vfs 包装器。
如果浏览器似乎具有支持它的必要 API,则会自动激活此功能。可以使用以下之一在 JS 代码中对其进行测试
if(sqlite3.capi.sqlite3_vfs_find("opfs")){ ... OPFS VFS is available ... }
// Alternately:
if(sqlite3.oo1.OpfsDb){ ... OPFS VFS is available ... }
如果可用,则名为“opfs”的文件系统可与任何接受文件系统名称的 sqlite3 API 一起使用,例如sqlite3_vfs_find()
、sqlite3_db_open_v2()
和sqlite3.oo1.DB
构造函数,请注意OpfsDb
是oo1.DB
的一个便利子类,它会自动使用此文件系统。对于URI 样式名称,请使用file:my.db?vfs=opfs
。
⚠️警告:Safari 17 以下版本
低于 17 版本的 Safari 版本与当前的 OPFS 文件系统实现不兼容,因为浏览器子 Worker 的存储处理存在错误,对此没有解决方法。共享访问句柄池文件系统 和WASMFS 支持 都提供了应该适用于 16.4 或更高版本的 Safari 版本的替代方案。
⚠️警告:COOP 和 COEP HTTP 头
为了提供一定程度的透明并发数据库访问支持,OPFS 文件系统需要 JavaScript 的SharedArrayBuffer
类型,并且只有在 Web 服务器在交付脚本时包含所谓的COOP 和COEP 响应头时,该类才可用
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin
如果没有这些头,则SharedArrayBuffer
将不可用,因此 OPFS 文件系统将无法加载。该类是协调sqlite3_vfs OPFS 代理的同步和异步部分之间的通信所必需的。
COEP 标头也可能具有credentialless
值,但它在任何给定应用程序的上下文中是否有效取决于它如何使用其他远程资产。
如何发出这些头取决于底层 Web 服务器。
Apache Web 服务器
对于Apache Web 服务器,这些头使用类似以下内容设置
<Location "/">
Header always append Cross-Origin-Embedder-Policy "require-corp"
Header always append Cross-Origin-Opener-Policy: "same-origin"
AddOutputFilterByType DEFLATE application/wasm
</Location>
Althttpd Web 服务器
对于althttpd Web 服务器,请使用--enable-sab
标志启动它(“sab”是 SharedArrayBuffer 的缩写)。
Cloudflare Pages
请参阅https://developers.cloudflare.com/pages/configuration/headers。
其他 Web 服务器
如果您知道如何在其他 Web 服务器中设置 COOP/COEP 头,请在SQLite 论坛 上告知我们,我们会更新这些文档以包含这些信息。
数据库名称中的目录部分
与大多数 sqlite3_vfs 实现不同,此实现将自动创建数据库文件名的任何前导目录部分,**前提是**数据库是以“创建”标志打开的。这种与常用约定的偏差是为了……
- 使 Web 开发人员的生活更轻松。
- 避免必须向客户端代码公开创建目录的 OPFS 特定 API。理想情况下,客户端数据库相关代码应该独立于所使用的存储。
例如
const db = new sqlite3.oo1.OpfsDb('/path/to/my.db','c');
如果需要,将创建/path/to
目录。没有前导斜线的路径在功能上等效,从 OPFS 根目录开始。
将数据库导入 OPFS
请参阅OpfsDb 文档。
并发和文件锁定
⚠️**预警:**对 OPFS 托管文件的并发访问是此文件系统的一个痛点。客户端应用程序将无法在此环境中实现桌面应用程序级别的并发性,但可以在浏览器选项卡和/或 Worker 之间实现一定程度的并发性。
背景:OPFS 提供了一些此 API 所需的同步 API。可以在没有任何锁定的情况下以异步模式打开文件,但获取对同步 API 的访问权限需要 OPFS 所谓的“同步访问句柄”,它会**独占锁定**文件。只要 OPFS 文件被锁定,同一HTTP 源 中运行的任何其他服务都无法打开它。例如,只要另一个选项卡从同一源运行,一个浏览器选项卡中的代码就无法访问该文件。
本质上,这意味着在任何给定时间,不能有两个数据库句柄打开同一个 OPFS 托管数据库。如果在两个选项卡中打开同一页面,则第二个选项卡在尝试打开同一个 OPFS 托管数据库时会立即遇到锁定错误!
为了帮助缓解在多个选项卡或 Worker 线程中运行的 sqlite3 实例之间的争用,sqlite3 仅在数据库 API 需要锁定时才获取写模式句柄。如果无法获取锁定,它将等待一小段时间,然后再次尝试,在放弃之前重复此操作几次。无法获得锁将以通用 I/O 错误的形式冒泡到客户端代码。OPFS API 没有方法可以明确区分与锁定相关的错误和其他 I/O 错误,因此客户端将看到 I/O 错误。
在给定 OPFS 托管数据库上的连接可靠限制未知,并且很大程度上取决于环境和数据库的使用方式。基本测试表明,如果将工作限制在较小的块中,则 3 个连接可以可靠地协作。对于每个超出该数量的连接,锁定失败的可能性都会迅速增加。但是,该值高度依赖于环境。例如,已观察到 Chrome 116 及更高版本在相对较快的机器(3GHz+)上可靠地运行 5 个连接。
以下是一些有助于改进 OPFS 并发性的提示,尤其是在客户端通过多个标签打开应用程序的情况下。
- 在知道应用程序需要数据库之前,不要打开数据库。
- 永远不要在同一线程内使用两个指向同一数据库文件的数据库句柄,因为这可能会导致某些死锁情况。
- 始终以“小”块执行工作,其中“小”以毫秒的 I/O 而不是数据大小来衡量。数据库在 I/O 上花费的时间越少,争用的可能性就越低。
- 不要长时间保持事务打开状态。打开的事务必然会锁定 OPFS 文件。
- 请参阅下一节中描述的“opfs-unlock-asap”标志。
改进 OPFS 托管数据库的并发支持工作正在进行中。随着 OPFS 的锁定支持的发展以及对锁定的更细粒度控制的广泛可用,sqlite3 VFS 将利用它来帮助改进并发性。
其他 OPFS VFS 功能
尽快解锁模式
有时 sqlite3 会在未事先显式获取存储上的锁的情况下调用 VFS(例如,在日志文件上)。当它这样做时,需要同步访问句柄的操作必然会自行获取锁2,并一直持有它,直到 VFS 在某个未指定的短暂时间段内(不到半秒)空闲,此时所有隐式获取的锁都将被释放。
此类锁在内部称为隐式锁或自动锁。它们是 sqlite3 VFS 不需要但 OPFS 需要锁。通常,获取锁的操作在操作结束时不会自动释放锁,因为这样做会造成巨大的性能损失(在 I/O 密集型基准测试中,运行时间最多增加 400%)。但是,通过告诉 VFS 在最早的机会立即释放此类锁,可以大大提高并发性。这俗称为“unlock-asap”模式,由于性能损失,它默认情况下处于禁用状态,但客户端可以使用 URI 样式的数据库名称 在每个数据库连接的基础上启用它。
file:db.file?vfs=opfs&opfs-unlock-asap=1
该字符串可以提供给允许 URI 样式文件名的所有 API。例如
new sqlite3.oo1.OpfsDb('file:db.file?opfs-unlock-asap=1');
仅当应用程序存在特定与并发相关的問題时,才应使用此标志。如果其他所有方法都失败,opfs-unlock-asap=1
可能会有所帮助,但它是否真正有效很大程度上取决于数据库的使用方式。例如,长时间运行的事务将锁定它,无论是否使用opfs-unlock-asap
选项。
OPFS 打开前删除
从 3.46 版开始,“opfs” VFS 支持 URI 标志 delete-before-open=1
,以指示 VFS 在尝试打开数据库文件之前无条件地删除该文件。例如,这可用于确保干净的状态或从损坏的数据库中恢复,而无需访问 OPFS 特定的 JS API 来消除它。
删除文件失败将被忽略,但可能会导致后续错误。例如,如果另一个标签已打开句柄,则删除可能会失败。
不用说,从另一个实例下删除文件会导致未定义的行为。
示例
const db = new sqlite3.oo1.OpfsDb("file:foo.db?delete-before-open=1");
OPFS SyncAccessHandle 池文件系统
(在 SQLite v3.43 中添加。)
"opfs-sahpool"
(“sah”=SyncAccessHandle)VFS 是一个基于 OPFS 的 sqlite3_vfs 实现,它采用了与 "opfs" VFS 截然不同的策略。差异可以概括为……
优势
- 应该适用于 2023 年 3 月以来发布的所有主要浏览器。
- 不需要 COOP/COEP HTTP 标头(以及相关的限制)。
- 在本文档中描述的选项中,OPFS 性能最高。
劣势
- 不支持多个同时连接。
- 没有文件系统透明性,即客户端分配给其数据库的名称与此 VFS 存储它们的名称不同,并且 VFS 管理一种虚拟文件系统。
请注意,"opfs"
VFS 和此 VFS 可以在同一个应用程序中使用,但它们将引用不同的 OPFS 级文件,即使它们使用相同的客户端级文件名,因为此 VFS 不会将客户端提供的名称直接映射到 OPFS 文件,而是将其自己的元数据中维护这些名称。
此 VFS 的特性
提供给它的路径必须是绝对路径。相对路径将无法正确识别。这可能是一个错误,但纠正它需要在没有执行此类操作业务的例程中进行一些跳跃。
可以在不同的名称下安装多个实例,每个实例在其自己的私有目录中相互隔离。此功能主要作为一种方法,使给定 HTTP 来源中的不同应用程序可以使用此 VFS,而不会在它们之间引入锁定问题。
此 VFS 基于 Roy Hashimoto 的工作,并得到他的认可,具体来说是
- github:/rhashimoto/wa-sqlite/discussions/67
- github:/rhashimoto/wa-sqlite/blob/master/src/examples/AccessHandlePoolVFS.js
安装
由于此 VFS 不支持并发,因此通过同一来源的两个选项卡重复初始化它(例如)将导致第二个及后续实例失败。为了使来源能够仅在选定的页面上使用此 VFS,而不会被可能打开的其他页面锁定,必须通过应用程序级代码显式启用 VFS。最简单的方法如下所示
await sqlite3.installOpfsSAHPoolVfs();
或
sqlite3.installOpfsSAHPoolVfs().then((poolUtil)=>{
// poolUtil contains utilities for managing the pool, described below.
// VFS "opfs-sahpool" is now available, and poolUtil.OpfsSAHPoolDb
// is a subclass of sqlite3.oo1.DB to simplify usage with
// the oo1 API.
}).catch(...);
如果出现以下情况,安装将失败
- VFS 已经在同一 HTTP 来源中的另一个浏览上下文中使用相同的目录名称处于活动状态(请参见下文)。
- 未检测到正确的 OPFS API。请注意,它们仅在工作线程中可用,而不是在主 UI 线程中。
installOpfsSAHPoolVfs()
接受一个配置对象,其中包含以下任何选项
clearOnInit
:(默认值为 false)如果为真,则在 VFS 初始化期间获取每个 SAH 时,将从中删除内容和文件名映射,从而使 VFS 的存储处于原始状态。仅对不需要保留页面重新加载的数据库使用此选项。initialCapacity
:(默认值为 6)指定 VFS 的默认容量,即它可能包含的文件数量。不应将其设置过高,因为 VFS 必须为池中的每个条目打开(并保持打开)一个文件。此设置仅在池最初为空时有效。如果池已存在,则它没有任何影响。请注意,此数字必须至少是预期数据库文件数量的两倍(以考虑日志文件),并且可能需要比数据库数量的三倍加一还要大,具体取决于TEMP_STORE
pragma 的值以及数据库的使用方式。库无法估算理想值 - 它必须由客户端提供。directory
:(默认值为"."+options.name
)指定要存储 VFS 元数据的 OPFS 目录名称。此 VFS 的一个实例只能同时使用同一个目录。为每个应用程序使用不同的目录名称可以使此 VFS 的不同实例共存于同一个 HTTP 来源中,但它们的数据彼此不可见。更改此名称将有效地使存储在先前名称下的任何数据库成为孤儿。此选项可以包含多个路径元素,例如“/foo/bar/baz”,并且它们将自动创建。实际上,没有必要更改它。
注意:假定此目录中的所有文件都由 VFS 管理。不要在此目录中放置其他文件,因为 VFS 可能会删除或以其他方式修改它们。name
:(默认值为"opfs-sahpool"
)设置在此 VFS 下注册的名称。通常不应更改此名称,但可以在此 VFS 下注册多个名称,只要每个名称都有自己的独立目录可供使用即可。每个存储对所有其他存储都不可见。该名称必须是与sqlite3_vfs_register()
及其相关函数兼容的字符串,并适合在 URI 样式的数据库文件名中使用。
注意:如果提供了自定义name
,则如果任何其他实例使用默认目录注册,则还必须提供自定义directory
。两个实例不能使用相同的目录。如果未显式提供目录,则将从name
选项合成目录名称。forceReinitIfPreviouslyFailed
:(默认值为false
,从 3.47 版开始可用)是针对特定浏览器怪癖的可选解决方法,该怪癖会导致此 VFS 的初始化在第一次尝试时失败,但在短时间后进行第二次尝试时会成功(请参阅 此工单 中的讨论)。
预警:此标志绝对不应使用,因为需要此解决方法的环境对于此 VFS 的目的来说本质上是可疑的,但它提供给希望不顾一切并寄希望于最好的开发人员。
它的作用:当此 VFS 初始化时,结果会被缓存(无论是成功还是失败),以便将来对installOpfsSAHPoolVfs()
的调用可以返回一致的结果,如下一节所述。此标志将覆盖缓存的失败结果,而是尝试第二次初始化 VFS。在受 激励工单 影响的环境中,第二次尝试很可能会成功。出于该工单讨论线程中解释的原因,库不会在这种情况下自动重试。
installOpfsSAHPoolVfs()
返回的 Promise 的解析值(在下面抽象地称为 PoolUtil
(尽管该对象没有固有的名称,并且如果需要,引用必须由客户端持有和命名))在下一节中描述。
异步(必然如此)安装例程将在成功时使用选项对象中指定的名称注册 VFS。可以使用 sqlite3_vfs_find(options.name)
检测 VFS 的存在。PoolUtil.OpfsSAHPoolDb
是一个 sqlite3.oo1.DB 类 子类,它使用此 VFS
const db = new PoolUtil.OpfsSAHPoolDb('/filename');
// ^^^ note that all paths for this VFS must be absolute!
池管理
installOpfsSAHPoolVfs()
返回一个 Promise,如果成功,则解析为一个实用程序对象,可用于执行文件池的基本管理(俗称 PoolUtil
)。只要在每次调用中使用相同的 name
选项,多次调用 installOpfsSAHPoolVfs()
将在第二次及后续调用中解析为相同的值。使用不同的名称调用它将返回不同的 Promise,这些 Promise 解析为具有不同 VFS 注册的不同对象。
其 API 包括(按字母顺序排列)……
[异步] number addCapacity(n)
向当前池中添加n
个条目。此更改在会话之间是持久性的,因此不应在每个应用程序启动时自动调用(但请参阅reserveMinimumCapacity()
)。其返回的 Promise 解析为新容量。由于此操作必然是异步的,因此 C 级 VFS API 无法根据需要自行调用它。OpfsSAHPoolDb
是 sqlite3.oo1.DB 的子类,配置为使用此 VFS。byteArray exportFile(name)
同步读取给定文件的内容到一个 Uint8Array 并返回。如果给定的名称当前未处于活动使用状态或发生 I/O 错误,则会抛出异常。请注意,给定的名称不会直接在 OPFS 中可见(或者,如果可见,则不是来自此 VFS)。这样做的原因是,此 VFS 以一种间接的方式管理名称到文件的映射,以维护其 SAH 列表。number getCapacity()
返回 SAH 池中当前包含的文件数量。默认容量仅足以容纳一个或两个数据库及其关联的临时文件。number getFileCount()
返回当前分配给 VFS 槽的文件数量。这与文件被“打开”并不相同。array getFileNames()
返回当前分配给 VFS 槽的文件名称的数组。此列表的长度与getFileCount()
相同。int importDb(name, byteArray)
导入 SQLite 数据库的内容,以字节数组或 ArrayBuffer 的形式提供,并在给定名称下导入,覆盖任何现有内容。如果它用于打开的数据库,则结果未定义。如果池没有可用的文件槽、发生 I/O 错误或输入看起来不像数据库,则抛出异常。在后一种情况下,只会进行粗略的检查。请注意,此例程仅用于导入数据库文件,而不是任意文件,原因是此 VFS 会自动清理任何非数据库文件,因此导入它们毫无意义。在写入错误时,句柄将从池中删除并可供重新使用。成功时,将返回写入的字节数。
如果导入的数据库处于 WAL 模式,则出于历史原因将其强制退出 WAL 模式,这些原因在 3.47 版之后不再严格适用,但必须为了向后兼容而保留(有关 WAL 的更多详细信息,请参阅WAL 模式)。[异步] int importDb(name, function)
(在 3.44 版中添加)
如果将其第二个参数传递给一个函数,则其行为将更改为异步,并且它将分块导入其数据,这些数据由给定的回调函数提供。它会重复调用回调函数(它可能是异步的),期望一个 Uint8Array 或 ArrayBuffer(表示新输入)或undefined
(表示 EOF)。只要回调函数继续返回非undefined
,它就会将传入的数据追加到给定的 VFS 托管数据库文件中。以这种方式调用时,返回的 Promise 的已解析值是写入目标文件的字节数。
如果导入的数据库处于 WAL 模式,则将其强制退出 WAL 模式,因为此版本不支持 WAL。[异步] number reduceCapacity(n)
从池中删除最多n
个条目,但前提是它只能删除当前未使用的条目。它返回一个 Promise,该 Promise 解析为实际删除的条目数。[异步] boolean removeVfs()
取消注册 VFS 并将其目录从 OPFS 中删除(这意味着所有客户端内容都将被销毁)。调用此函数后,VFS 将不再可以使用,并且目前无法重新添加它,除非重新加载当前 JavaScript 上下文。- 如果数据库当前正在使用此 VFS,则结果未定义。
- 如果执行了删除操作,则返回的 Promise 解析为 true,如果 VFS 未安装,则解析为 false。
- 如果 VFS 具有多级目录,例如“/foo/bar/baz”,则仅删除最底层的目录,因为此 VFS 无法确定上层目录是否包含应删除的数据。
[异步] number reserveMinimumCapacity(min)
如果当前容量小于min
,则容量将增加到min
,否则返回而没有任何副作用。生成的 Promise 解析为新的容量。boolean unlink(filename)
如果虚拟文件以给定名称存在,则将其与池分离并返回 true,否则返回 false 且没有任何副作用。如果文件当前处于活动使用状态,则结果未定义。请记住,名称需要使用绝对路径(以斜杠开头)。string vfsName
在此池的 VFS 注册下的 SQLite VFS 名称。[异步] void wipeFiles()
清除所有 SAH 的所有客户端定义状态,并使它们都可供池重新使用。如果任何此类句柄当前正在被 sqlite3 数据库实例使用,则结果未定义。
并发性
opfs-sahpool
VFS 无法在库级别提供任何并发支持,因为它预先分配了所有潜在的 SAH,这会立即锁定这些文件。但是,Roy Hashimoto 撰写了文章探讨了客户端级别的解决方案来解决此问题
- https://github.com/rhashimoto/wa-sqlite/discussions/81
- https://github.com/rhashimoto/wa-sqlite/discussions/84
此 VFS 中还需要做一些工作来协助实现客户端并发,例如能够停止和重新启动 VFS。
使用 OPFS 的 WAL 模式
从 3.47 版开始,可以使用以下注意事项为 OPFS 托管的数据库激活 WAL 模式
- 由于 WASM 版本没有共享内存 API,因此激活 WAL 需要客户端在打开数据库句柄后立即专门激活独占锁定模式,然后再对它执行任何其他操作,如WAL 文档中所述并在此处总结
pragma locking_mode=exclusive
- WAL 模式不会在此环境中提供任何并发优势。相反,对独占锁定的要求消除了来自
"opfs"
VFS的所有并发支持。 "opfs-sahpool"
VFS可能会根据主机环境在使用 WAL 时获得轻微的性能提升。测试尚未发现"opfs"
VFS具有同等的优势。
通过 WASMFS 使用 OPFS
(在 3.43 版中(重新)添加。)
替代OPFS VFS或共享访问句柄池 VFS的是 Emscripten 的WASMFS,它以与这两种 VFS 完全不同的方式支持 OPFS。它将 OPFS 作为 Emscripten 向客户端代码公开的虚拟文件系统上的“挂载点”(目录),并且存储在该目录下的所有文件都位于 OPFS 中。
注意:WASMFS 在sqlite3.wasm
的规范版本中未启用,因为它需要单独的、不太便携的 WASM 版本,并且与其他使用 OPFS 的选项相比,提供的优势很少。构建它需要在 Linux 系统上本地检出 sqlite3 源代码树和“最新版本”的 Emscripten SDK3
$ ./configure --enable-all
$ cd ext/wasm
$ make wasmfs
生成的结果文件是jswasm/sqlite3-wasmfs.*
和(可选)jswasm/sqlite3-opfs-async-proxy.js
,尽管后者仅在客户端也需要访问OPFS VFS时才需要。除了 WASMFS 支持之外,它的用法与非 WASMFS 结果文件相同。
优点
- 简单的用法:WASMFS 将具体存储机制隐藏在 Emscripten 的文件系统 API 后面,并允许将多个存储后端“挂载”到单个虚拟文件系统中。调用
sqlite3.capi.sqlite3_wasmfs_opfs_dir()
初始化(如有必要)WASMFS+OPFS 组合以及它返回的路径,该路径是 OPFS“挂载点”的最顶层路径。存储在该目录下的所有文件都存储在当前来源的 OPFS 存储中。如果该函数返回空字符串,则客户端上将没有 WASMFS+OPFS 组合可用。 - 它应该适用于 Safari 16.x 版,与 OPFS VFS 不同。
- 高性能。此文件系统通常优于OPFS VFS,但存在以下功能成本...
缺点
- WASMFS是一个第三方项目,截至 2024 年 7 月,被标记为“正在开发中”,随时可能更改。我们无法保证 WASMFS 的长期 API/使用稳定性。
- 数据库没有并发支持。每个对 WASMFS 托管的 OPFS 文件的句柄都会在文件打开期间保持独占锁定。如果客户端网页同时在两个选项卡中打开,则第二个选项卡将无法打开数据库。可以想象,可以通过例如根据需要打开和关闭数据库并在应用程序级别引入高级锁定,并使用WebLocks来协调并发。
- 整个库都需要COOP/COEP 标头,而不是仅用于 OPFS 支持。也就是说,除非发出这些标头,否则此版本根本无法部署。
- 它不如规范版本便携。例如,在上次在其上进行测试时,它不适用于 ARM64 平台(某些移动设备)。
- 此版本仅作为 ES6 模块可用,而不是“普通”JS,并且仅在从 Worker 加载时有效。WASMFS+OPFS 不适用于主线程。
尽管存在缺点,但 WASMFS 版本对于某些类型的客户端应用程序来说可能是可行的选择。
简短示例
const dirName = sqlite3.capi.sqlite3_wasmfs_opfs_dir()
if( dirName ) {
/* WASMFS OPFS is active ... All files stored under
the path named by dirName are housed in OPFS. */
}
else {
/* WASMFS OPFS is not available */
}
尽管挂载点名称旨在保持稳定,但客户端代码应避免在任何地方硬编码它,并且始终使用sqlite3_wasmfs_opfs_dir()
来获取它。它在单个会话的生命周期内不会更改,因此可以保存以供重用,但不要硬编码。在没有 WASMFS+OPFS 支持的版本中,该函数始终返回空字符串。
维护 OPFS 托管的文件
出于 SQLite 的目的,OPFS API 是一个内部实现细节,不会直接向客户端代码公开。这意味着,例如,SQLite API 不能用于遍历存储在 OPFS 中的文件列表,也不能用于删除数据库文件4。尽管最初似乎可以通过提供一个虚拟表来提供 OPFS 托管的文件列表以及删除它们的功能,但这无法实现,因为相关的 OPFS API 全部都是异步的,这使得它们无法与 C 级别的 SQLite API 一起使用。
在撰写本文时,已知以下可能性可用于管理此类文件
- 从浏览器的开发者控制台使用OPFS API。这并非易事,但在紧急情况下可以使用。
- OPFS 资源管理器是基于 Chromium 的浏览器的扩展程序,它为给定的 HTTP 来源提供了一个 OPFS 托管文件的交互式树。
OPFS 存储限制
OPFS 存储限制很宽松,但每个环境都不同。有关完整详细信息,请参阅有关该主题的 MDN 文档。Patrick Brosset 的这篇文章也详细介绍了该主题。
请注意,与其他存储后端一样,SQLite API 不知道限制是什么。如果超出限制,SQLite 将返回通用的 I/O 错误。
旁白:通过 OPFS 进行跨线程通信
通过 OPFS 使用 sqlite3 在 JS 中打开了一种在其他情况下不容易存在的可能性:任意线程之间的通信。
原生 JavaScript 在线程间通信方面提供的选项有限,例如 postMessage()
、SharedArrayBuffer
和(在非常有限的范围内)Atomics
。localStorage
、sessionStorage
和早已废弃的 WebSQL 仅限于主线程使用。据推测,WebSQL 不允许在 Worker 中使用,原因正是它会开启一个通信通道,并在任意线程之间产生锁定争用。
如果客户端从多个线程加载 sqlite3 模块,则可以通过 初始 OPFS VFS,通过数据库自由地进行通信。大部分情况下都可以。这种用法会在线程之间引入文件锁定争用。只要每个线程只使用非常短的交易,自动的锁重试机制会透明地处理锁定,但一旦一个线程持有交易打开时间过长,或者太多线程争用访问,就会导致与锁定相关的异常,并被转换为 C API 的 I/O 错误。
通过数据库进行跨线程通信的能力是功能还是缺陷,由客户端自行决定。
旁注:数据库的神秘消失
用户有时会报告他们的 OPFS 数据库随机消失。此项目中没有代码会在没有客户端明确请求的情况下删除数据库,但数据库有时仍会因超出此库控制范围的环境特定原因而消失,包括但不限于
- 病毒扫描程序
- "电脑清理"软件
- 浏览器级别的存储权限
- 浏览器内部自行清理的决定
请参阅 此论坛帖子,了解有关这些问题的讨论。