持久化存储选项

此 API 通过localStorage/sessionStorage 和兼容浏览器中的源私有文件系统 提供数据库持久化。

⚠️警告:隐身模式和访客模式下的限制

大多数浏览器都提供“隐身”和/或“访客”浏览模式,这些模式会故意更改或禁用浏览器的某些功能。在这些模式下运行时,存储功能可能会受到不利影响,例如配额降低或完全缺乏持久性。每个浏览器施加的确切限制各不相同,但并非完全出乎意料的是,此页面上描述的持久化功能在这些“隐身”模式下运行时,要么比文档建议的更受限制,要么甚至可能完全不可用。

“我们如何提前检测这些情况?”这是一个合理的问题,但浏览器制造商有意使其难以检测这些模式,以防止例如网站限制对隐身模式用户的访问。任何当前在任何给定浏览器中检测此模式的方法都可能很快过时,因为浏览器制造商会注意到并更改内容以使这些模式对访问的网站更加不透明,因此我们无法提供有关如何规避它们的任何建议。

键值文件系统 (kvvfs):localStoragesessionStorage

kvvfs 是一种 sqlite3_vfs 实现,旨在将整个 sqlite3 数据库存储在 localStoragesessionStorage 对象中。这些对象仅在主 UI 线程中可用,而不是 Worker 线程中,因此此功能仅在主线程中可用。kvvfs 将数据库的每一页存储到存储对象的单独条目中,并将每个数据库页面编码为 ASCII 格式,以便使其对 JS 友好。

此文件系统**每个存储对象仅支持一个数据库**。也就是说,最多只能有一个localStorage 数据库和一个sessionStorage 数据库。

要使用它,请将文件系统名称“kvvfs”传递给任何接受文件系统名称的数据库打开例程。数据库的文件名**必须**为localsession,或其别名:localStorage::sessionStorage:。任何其他名称都将导致数据库打开失败。使用URI 样式名称 时,请使用以下之一

在主 UI 线程中加载时,以下实用程序方法将添加到 sqlite3.capi 命名空间中

在这两种情况下,参数都可以是("local""session""")之一。在前两种情况下,仅作用于localStoragesessionStorage,在后一种情况下,两者都将被作用于。

存储限制**很小**:通常为 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

此库提供了多种在 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 构造函数,请注意OpfsDboo1.DB 的一个便利子类,它会自动使用此文件系统。对于URI 样式名称,请使用file:my.db?vfs=opfs

⚠️警告:Safari 17 以下版本

低于 17 版本的 Safari 版本与当前的 OPFS 文件系统实现不兼容,因为浏览器子 Worker 的存储处理存在错误,对此没有解决方法。共享访问句柄池文件系统WASMFS 支持 都提供了应该适用于 16.4 或更高版本的 Safari 版本的替代方案。

⚠️警告:COOP 和 COEP HTTP 头

为了提供一定程度的透明并发数据库访问支持,OPFS 文件系统需要 JavaScript 的SharedArrayBuffer 类型,并且只有在 Web 服务器在交付脚本时包含所谓的COOPCOEP 响应头时,该类才可用

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 实现不同,此实现将自动创建数据库文件名的任何前导目录部分,**前提是**数据库是以“创建”标志打开的。这种与常用约定的偏差是为了……

例如

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 并发性的提示,尤其是在客户端通过多个标签打开应用程序的情况下。

改进 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 截然不同的策略。差异可以概括为……

优势

劣势

请注意,"opfs" VFS 和此 VFS 可以在同一个应用程序中使用,但它们将引用不同的 OPFS 级文件,即使它们使用相同的客户端级文件名,因为此 VFS 不会将客户端提供的名称直接映射到 OPFS 文件,而是将其自己的元数据中维护这些名称。

此 VFS 的特性

此 VFS 基于 Roy Hashimoto 的工作,并得到他的认可,具体来说是

安装

由于此 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(...);

如果出现以下情况,安装将失败

installOpfsSAHPoolVfs() 接受一个配置对象,其中包含以下任何选项

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 包括(按字母顺序排列)……

并发性

opfs-sahpool VFS 无法在库级别提供任何并发支持,因为它预先分配了所有潜在的 SAH,这会立即锁定这些文件。但是,Roy Hashimoto 撰写了文章探讨了客户端级别的解决方案来解决此问题

此 VFS 中还需要做一些工作来协助实现客户端并发,例如能够停止和重新启动 VFS。

使用 OPFS 的 WAL 模式

从 3.47 版开始,可以使用以下注意事项为 OPFS 托管的数据库激活 WAL 模式

通过 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 版本对于某些类型的客户端应用程序来说可能是可行的选择。

简短示例

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 存储限制

OPFS 存储限制很宽松,但每个环境都不同。有关完整详细信息,请参阅有关该主题的 MDN 文档Patrick Brosset 的这篇文章也详细介绍了该主题。

请注意,与其他存储后端一样,SQLite API 不知道限制是什么。如果超出限制,SQLite 将返回通用的 I/O 错误。

旁白:通过 OPFS 进行跨线程通信

通过 OPFS 使用 sqlite3 在 JS 中打开了一种在其他情况下不容易存在的可能性:任意线程之间的通信。

原生 JavaScript 在线程间通信方面提供的选项有限,例如 postMessage()SharedArrayBuffer 和(在非常有限的范围内)AtomicslocalStoragesessionStorage 和早已废弃的 WebSQL 仅限于主线程使用。据推测,WebSQL 不允许在 Worker 中使用,原因正是它会开启一个通信通道,并在任意线程之间产生锁定争用。

如果客户端从多个线程加载 sqlite3 模块,则可以通过 初始 OPFS VFS,通过数据库自由地进行通信。大部分情况下都可以。这种用法会在线程之间引入文件锁定争用。只要每个线程只使用非常短的交易,自动的锁重试机制会透明地处理锁定,但一旦一个线程持有交易打开时间过长,或者太多线程争用访问,就会导致与锁定相关的异常,并被转换为 C API 的 I/O 错误。

通过数据库进行跨线程通信的能力是功能还是缺陷,由客户端自行决定。

旁注:数据库的神秘消失

用户有时会报告他们的 OPFS 数据库随机消失。此项目中没有代码会在没有客户端明确请求的情况下删除数据库,但数据库有时仍会因超出此库控制范围的环境特定原因而消失,包括但不限于

请参阅 此论坛帖子,了解有关这些问题的讨论。


  1. ^ sqlite 项目的整个 JS/WASM 工作最初源于使其与 OPFS 协同工作的兴趣。
  2. ^ 另一种选择是使操作失败。
  3. ^ 在撰写本文时(2023-07-13),EMSDK 3.1.42 已知可以工作,并且不知道任何旧版本是否可以工作。WASMFS 支持在发布前一年发生了很大的变化,有时是不兼容的方式。
  4. ^ 注意,C API 也不公开此类平台特定的 API。