sqlite3 JS API 可以加载到主线程或工作线程中,并在两者中以相同的方式使用。另一种方法是在其自己的专用工作线程中加载 sqlite3,使 sqlite3 API 完全无法访问客户端代码,除非通过本文档中描述的相对有限的 API。
通过 Worker 加载 sqlite3.js
时,请务必查看关于相对 URI 的注意事项。
在开始之前,让我们简要了解一下与同步接口相比,此异步接口的一些限制和痛点。
- 事务 仅当数据库只有一个用户或所有包含在事务中的 SQL 代码都包含在同一个 Worker 消息中时才能工作。后者很容易做到,但与同步接口相比,可能会对事务的流程施加严格的限制。使用异步接口,很容易交错事务和非事务代码,这只会造成混乱。
- 使用此异步接口无法嵌套循环查询。也就是说,无法发出异步查询,并在处理其每行结果的同时运行其他查询。“外部”查询正在运行时,通过
postMessage()
发送到 Worker1 线程的任何消息都将在事件队列的末尾排队,因此必须等到外部查询完成后才能运行。解决此问题的唯一方法是在运行任何内部查询之前收集外部查询的所有数据。根据数据的大小,这可能是可行的,也可能不可行。 - 延迟执行
postMessage()
提交的数据库请求意味着用户可以在 JS 环境开始解析任何请求之前提交任意数量的请求。这可能导致某些情况,例如一个数据库操作失败,但队列后面仍有 15 个操作将在失败后运行。在同步代码中,我们可以在失败时停止并跳过其余操作,但我们无法通过 Worker 样式的 API 轻松做到这一点。
Worker1
Worker1 API 为 sqlite3 提供了一个基于 Worker 的基本接口,用于客户端应用程序代码位于一个线程中,并希望 sqlite3 在其自己的线程中运行的情况。
它基于OO1 API,并支持在工作线程中建立多个数据库连接。在开始之前,似乎有必要指出此 API 的基于 Promise 的包装器使用起来要简单得多,因为它不需要处理 postMessage()
,并且使用户能够更好地控制请求和响应的流程。
为了允许此 API 加载到工作线程中而不会自动注册 onmessage 处理程序,初始化工作线程 API 需要调用 sqlite3.initWorker1API()
。如果从非工作线程调用此函数,则会抛出异常。每个工作线程只能调用一次。
启动工作线程的最简单方法是
const W = new Worker('sqlite3-worker1.js'); // or equivalent
W.onmessage = function(event){
event = event.data;
switch(event.type){
case 'sqlite3-api':
if('worker1-ready' === event.result){
// The worker is now ready to accept messages
}
...
}
};
sqlite3-worker1.js
是一个轻量级包装器,它加载 sqlite3.js
、其 WASM 模块,然后调用 sqlite3.initWorker1API()
,这会触发客户端应监听的工作线程消息。
{type:'sqlite3-api', result:'worker1-ready'}
这使客户端知道它已初始化。
请注意,基于工作线程的接口由于其异步特性,可能有点古怪。特别是,在开始处理任何消息之前,可能会向工作线程发布任意数量的消息。例如,如果“打开”操作失败,则其后的所有消息也可能会失败。此 API 的基于 Promise 的包装器在这方面使用起来更方便,并且使客户端能够更好地控制发布消息的顺序和速率。
Worker1 消息格式
发布到工作线程的每条消息都包含一个与操作无关的信封和与操作相关的参数。
{
type: string, // one of: 'open', 'close', 'exec', 'config-get'
messageId: OPTIONAL arbitrary value. The worker will copy it as-is
into response messages to assist in client-side dispatching.
dbId: a db identifier string (returned by 'open') which tells the
operation which database instance to work on. If not provided, the
first-opened db is used. This is an "opaque" value, with no
inherently useful syntax or information. Its value is subject to
change with any given build of this API and cannot be used as a
basis for anything useful beyond its one intended purpose.
args: ...operation-dependent arguments...
// the framework may add other properties for testing or debugging
// purposes.
}
发布回调用工作线程的线程的响应消息如下所示。
{
type: Same as the inbound message except for error responses,
which have the type 'error',
messageId: same value, if any, provided by the inbound message
dbId: the id of the db which was operated on, if any, as returned
by the corresponding 'open' operation.
result: ...operation-dependent result...
}
错误响应以与操作无关的格式报告。
{
type: "error",
messageId: ...as above...,
dbId: ...as above...
result: {
operation: type of the triggering operation: 'open', 'close', ...
message: ...error message text...
errorClass: string. The ErrorClass.name property from the thrown exception.
input: the message object which triggered the error.
stack: _if available_, a stack trace array.
}
}
Worker1 方法
可用的消息类型按字母顺序列出如下。
关闭
close
消息关闭数据库。
消息格式
{
type: "close",
messageId: ...as above...
dbId: ...as above...
args: OPTIONAL {unlink: boolean}
}
如果 dbId
不引用打开的 ID,则此操作为无操作。如果 args
对象包含真值 unlink
值,则在关闭数据库后将取消链接(删除)该数据库。无法关闭数据库(因为它未打开)或删除其文件不会触发错误。
响应
{
type: "close",
messageId: ...as above...,
result: {
filename: filename of closed db, or undefined if no db was closed
}
}
获取配置
此操作获取 sqlite3 API 配置的可序列化部分。
消息格式
{
type: "config-get",
messageId: ...as above...,
args: currently ignored and may be elided.
}
响应
{
type: "config-get",
messageId: ...as above...,
result: {
version: sqlite3.version object
bigIntEnabled: bool. True if BigInt support is enabled.
vfsList: result of sqlite3.capi.sqlite3_js_vfs_list()
}
}
执行
exec
是运行任意 SQL 的接口。它是oo1.DB.exec() 方法的包装器,并支持其大部分功能。
所有 SQL 执行都通过 exec
操作处理。它提供了oo1.DB.exec() 方法的大部分功能,但由于状态必须跨线程边界,因此存在一些限制。
消息格式
{
type: "exec",
messageId: ...as above...
dbId: ...as above...
args: string (SQL) or {... see below ...}
}
响应
{
type: "exec",
messageId: ...as above...,
dbId: ...as above...
result: {
input arguments, possibly modified. See below.
}
}
参数采用 oo1.DB.exec()
接受的相同形式,但以下列出的例外情况除外。
函数类型 args.callback
属性无法跨窗口/工作线程边界,因此在此处无用。如果 args.callback
是字符串,则假定它是一个消息类型键,在这种情况下,将应用回调函数,该函数通过以下方式发布每行结果
postMessage({
type: thatKeyType,
rowNumber: 1-based-#,
row: theRow,
columnNames: anArray
})
row
属性包含由 rowMode
选项隐含的形式(默认为 'array'
)中的行结果。rowNumber
是一个基于 1 的整数,每次调用回调时递增 1。columnNames
数组包含结果行列的列名。
在结果集结束时(无论是否生成了任何结果行),它都会发布一条相同的消息,其中 (row
=undefined
, rowNumber
=null
),以提醒调用者结果集已完成。请注意,对于某些 arg.rowMode
值,null
行值是合法的行结果。
row
=undefined
, rowNumber
=undefined
) 来指示结果集结束,因为获取它们将无法与从空对象中获取区分开来,除非客户端使用 hasOwnProperty()
(或类似方法)来区分“缺少属性”和“具有未定义值的属性”。类似地,在某些情况下,null
是 row
的合法值,而数据库层不会发出 undefined
的结果值。回调代理不得递归进入此接口。exec()
调用将占用工作线程,导致任何递归尝试等待第一个 exec()
完成。
如果 countChanges
参数属性1 为真,则返回的对象包含的 result
属性将具有一个 changeCount
属性,该属性保存由提供的 SQL 进行的更改次数。由于 SQL 可能包含任意数量的语句,因此 changeCount
是通过在评估 SQL 前后调用 sqlite3_total_changes()
来计算的。如果 countChanges
的值为 64,则 changeCount
属性将以 BigInt 的形式返回为 64 位整数(请注意,这将在 BigInt 功能不足的构建中触发异常)。在后一种情况下,更改次数是通过在评估 SQL 前后调用 sqlite3_total_changes64()
来计算的。
响应是输入选项对象(如果仅传递字符串,则为合成对象),可能已修改。options.resultRows
和 options.columnNames
可能由对 DB.exec()
的调用填充,并且 options.changeCount
可能如上所述设置。
导出
export
是sqlite3_js_db_export()的代理,它将数据库作为字节数组返回。
消息格式
{
type: "export",
messageId: ...as above...
dbId: ...as above...
}
响应
{
type: "export",
messageId: ...as above...,
dbId: ...as above...
result: {
byteArray: Uint8Array (as per sqlite3_js_db_export()),
filename: the db filename,
mimetype: "application/x-sqlite3"
}
}
如果序列化由于内存不足条件而失败,则会生成错误响应。
打开
open
消息指示工作线程打开数据库。
消息格式
{
type: "open",
messageId: ...as above...,
args:{
filename [=":memory:" or "" (unspecified)]: the db filename.
See the sqlite3.oo1.DB constructor for peculiarities and
transformations
vfs: sqlite3_vfs name. Ignored if filename is ":memory:" or "".
This may change how the given filename is resolved. The VFS may
optionally be provided via a URL-style filename argument:
filename: "file:foo.db?vfs=...". If both this argument and a
URI-style argument are provided, which one has precedence is
unspecified. By default it uses a transient database, created
anew on each request.
}
}
对于文件名,可以使用 URL 样式的名称,这些名称可能包含 VFS 名称,这使它们能够使用(例如)OPFS 支持:file:foo.db?vfs=opfs
。或者,可以在“vfs”选项中指定 VFS。
响应
{
type: "open",
messageId: ...as above...,
result: {
filename: db filename, possibly differing from the input.
dbId: see below,
persistent: true if the given filename resides in the
known-persistent storage, else false.
vfs: name of the underlying VFS
}
}
dbId
是一个不透明的 ID 值,应将其传递到此 API 中其他调用的消息信封中,以告知它们使用哪个数据库。如果未将其提供给将来的调用,它们将默认操作于第一个打开的数据库。出于 API 一致性的考虑,此属性也是包含消息信封的一部分。只有 open
操作将其包含在 result
属性中。
注意:由于 postMessage()
事件会在应用程序既无法查看也无法操作的队列中排队执行,因此客户端可能会在实际处理 open
请求之前排队任意数量的消息。如果 open
失败,则其后的所有消息也可能会失败,但客户端代码或工作线程都无法取消它们。Promiser API 可以通过使客户端能够在继续之前“等待” open
响应来解决此问题。
基于 Promise 的包装器(又名 Worker1 Promiser)
围绕 Worker1 API 的基于 Promise 的包装器提供了比 postMessage()
更友好的用户界面。与 Worker1 API 一样,此接口在其自己的专用工作线程中加载主 sqlite3 API,与所有客户端代码分开。但是,它不是通过 postMessage()
访问它,而是通过基于 Promise 的接口访问它。在幕后,它使用 postMessage()
,但 Promise 接口为客户端提供了对数据库操作时机的更多控制。
要加载它
<script src="path/to/sqlite3-worker1-promiser.js"></script>
请注意,sqlite3-worker1-promiser.js
是 sqlite3 JS/WASM 发行版的一部分,必须与 sqlite3 JS/WASM 的其余部分位于同一目录中。
Promiser 配置和实例化
它需要 sqlite3-worker1.js
和 sqlite3.js
,请注意,可以将其配置为使用不同的脚本加载 sqlite3 工作线程。
该脚本将安装一个名为 sqlite3Worker1Promiser()
的全局作用域函数,该函数充当创建 promiser 实例的工厂。它有三种调用形式
sqlite3Worker1Promiser( config )
下面详细描述了 config 对象。sqlite3Worker1Promiser()
将使用sqlite3Worker1Promiser.defaultConfig
配置对象。sqlite3Worker1Promiser( function )
等效于传递{onready: theFunction}
并接受其余选项的默认值。
可以通过修改 sqlite3Worker1Promiser.defaultConfig
对象来配置在调用 sqlite3Worker1Promiser()
之前的第二种和第三种调用形式的默认值。
config 对象从技术上讲是可选的,但其 onready
属性实际上是必需的,因为它是通知 sqlite3 模块的异步加载和初始化何时完成的唯一方法。
onready
回调而不是返回 Promise 的讽刺意味开发人员并没有忘记。事实证明,对于 sqlite3Worker1Promiser()
返回 Promise 比使用 onready
回调更笨拙。config 对象可以具有以下任何属性,除了 onready
之外,所有属性都具有可用的默认值。
onready
:function()
当 sqlite3 模块和 Worker API 的异步加载完成时调用。这是唯一知道加载已完成的方法。在 3.46 版之前,此函数不传递任何参数。从 3.46 版开始,onready()
会传递由sqlite3Worker1Promiser()
返回的函数,因为从这个回调中访问它对于某些使用模式来说更方便。同样从 3.46 版开始,promiser v2 接口 使此回调变得不再必要。worker
:Worker 或函数
加载sqlite3-worker1.js
的 Worker 实例或功能等价物。请注意,promiser 工厂替换了 worker.onmessage 属性。此配置选项也可以是函数,在这种情况下,此函数会使用调用该函数的结果重新分配此属性,从而启用 Worker 的延迟实例化。generateMessageId
:function(messageObject)
一个函数,当传递一个即将发布的消息对象时,为该消息生成一个唯一的消息 ID,然后此 API 将其分配为消息的 messageId 属性。它必须在每次调用时生成唯一的 ID,以便分派能够正常工作。如果未定义,则使用默认生成器(对于大多数或所有情况都应该足够)。debug
:function(...)
一个console.debug()
样式的函数,用于记录有关 Worker 消息的信息。onunhandled
:function(event)
一个回调,它会传递任何未由此代理处理的worker.onmessage()
事件的消息事件对象。理想情况下,“应该”永远不会发生这种情况,因为此代理旨在处理所有已知的消息类型。
配置对象到位后,promiser 会像这样实例化
const promiser = self.sqlite3Worker1Promiser(config);
Promiser 方法
Promiser 对象是一个具有两种调用签名的函数
( messageType, messageArguments )
等价于( {type: messageType, args: type-specific value} )
其中 type
始终是字符串,args
值是特定于消息类型的。它始终返回一个 Promise 对象,该对象解析为一个对象
{
type: messageType,
result: type-specific result value,
... possibly other metadata ...
}
每个消息类型对应一个 API 方法,所有这些方法都对应于 Worker1 API 中的方法,并且具有相同的参数和结果,除非在下面明确描述。
错误的报告方式与 Worker1 API 相同,但错误响应会导致 Promise 被拒绝。因此,客户端通过向其 Promise 添加 catch()
处理程序来监听这些错误。例如
promiser('open', {'filename':...}).then((msg)=>{
...
}).catch((e)=>{
// Note that the error state is _not_ an Error object, but an
// object in the same form the Worker1 API reports errors in.
// That behavior is potentially subject to change in the future,
// such that catch() always gets an Error object.
console.error(e);
})
关闭
功能类似于 Worker1 的 close
方法,但如果它关闭了特定数据库,它还会清除内部默认的 dbId
。
获取配置
除了 open
之外,这是唯一不需要数据库连接的方法。
执行
此方法的工作原理几乎与其 Worker1 对应方法相同,但存在以下差异
exec
的 {callback: STRING}
选项无法通过此接口使用(它会触发异常),但 {callback: function}
可以使用,并且工作方式与 Worker 中的 STRING 形式完全相同:回调会为结果集的每一行调用一次,并传递与 worker API 发出的相同的工作程序消息格式
{
type: typeString,
row: VALUE,
rowNumber: 1-based-#,
columnNames: anArray
}
其中 typeString
是一个内部合成的消息类型字符串,暂时用于 worker 消息分派。除了测试此 API 的客户端代码之外,所有其他客户端代码都可以忽略它。
在结果集的末尾,会使用 (row
=undefined
, rowNumber
=null
) 触发相同的事件,以指示已到达结果集的末尾。请注意,行是通过 worker 发布的消息到达的,并具有其所有含义。
打开
功能类似于 Worker1 的 open
方法,但如果这是打开的第一个数据库,它还会在内部记录来自响应的 dbId
,以便它可以将该数据库 ID 用于不提供数据库 ID 的后续操作。
Promiser v2:另一个 Promise 和 ESM
Promiser v2 在 3.46 中添加,其工作原理与 v1 接口相同,只是初始化方式不同
- 它的初始化函数返回一个 Promise 对象,而不是使用
onready()
处理程序,以在异步初始化完成时提醒调用方。- 如果传递了一个(可选的)
onready()
处理程序,则 v2 API 会在解析 Promise 之前立即调用它。如果回调抛出异常,则 Promise 会被拒绝。
- 如果传递了一个(可选的)
- 它可以通过
sqlite3-worker1-promiser.mjs
作为 ESM 模块导入使用。
以下是一个差异示例,首先以 v1 API 进行比较
// v1:
const factory = sqlite3Worker1Promiser({
onready: function(f){
/**
The promiser factory (f) is now ready for use.
f is the same function which is returned from
sqlite3Worker1Promiser().
*/
}
});
// Equivalent:
// const factory = sqlite3Worker1Promiser(function(f){...});
v2 返回一个 Promise,而不是一个函数,并且有两种加载它的选项。
首先,如果其代码以与 v1 相同的方式加载,则它可用作
const factory = await sqlite3Worker1Promiser.v2(/*optional config*/);
factory
是一个解析为函数的 Promise,而 v1 直接返回函数,但在模块完成初始化之前实际上无法使用它(客户端可以使用 v1 样式的 onready()
处理程序来检测)。
相同,但没有 await
sqlite3Worker1Promiser.v2(/*optional config*/)
.then(f=>{
// f is the function which the resulting Promise resolves to
doSomeWork(f);
});
其次,它可以作为 ESM 模块加载,如下所示。
import { default as promiserFactory } from "./jswasm/sqlite3-worker1-promiser.mjs";
const promiser = await promiserFactory(/* optional config */)
.then(func){
// func == the promiser factory function (same as `promiser` will resolve to).
// Do any necessary client-side pre-work here, if needed, then...
return func; // ensure that the promise resolves to the proper value
});
以这种方式加载时,导出的函数是 v2 函数。
之后,promiser 的使用方法与 v1 API 完全相同。只有 v2 中的初始化发生了变化。
v2 接口接受但不强制要求 onready()
回调。如果提供了回调,或者 sqlite3Worker1Promiser.v2()
传递了一个函数,则会在结果 promise 解析之前立即调用它。如果它抛出异常,则 promise 会被拒绝。例如
promiserFactory( function(f){
throw new Error("Testing onready throw.");
})
.catch(e=>{ console.error("caught:",e); });
- ^
countChanges
在 3.43 版中添加