sqlite3 JS API 可以載入於主執行緒或 Worker 執行緒中,並以相同的方式使用。另一種選擇是將 sqlite3 載入其專用的 Worker 中,使客戶端程式碼完全無法存取 sqlite3 API,只能透過本文檔中描述的相對有限的 API 進行存取。
透過 Worker 載入 sqlite3.js
時,請務必參閱關於相對 URI 的注意事項。
在開始之前,讓我們簡要了解一下與同步介面相比,此非同步介面的一些限制和痛點。
- 交易 僅在資料庫只有一個使用者,或所有包含在交易中的 SQL 程式碼都包含在同一個 Worker 訊息中時才能運作。後者很容易做到,但與同步介面相比,可能會對交易的流程施加嚴格的限制。使用非同步介面,很容易交錯交易和非交易程式碼,這只會造成混亂。
- 使用此非同步介面,使用查詢的巢狀迴圈是不可能的。也就是說,無法發送非同步查詢,並且在處理其每一列結果時執行其他查詢。當「外部」查詢正在執行時,透過
postMessage()
傳送到 Worker1 執行緒的任何訊息都會排在事件佇列的末尾,因此在外部查詢完成之前無法執行。唯一的解決方法是在執行任何內部查詢之前收集外部查詢的所有資料。根據資料大小,這可能可行也可能不可行。 postMessage()
提交的資料庫請求的延遲執行意味著使用者可以在 JS 環境開始解析任何請求之前提交任意數量的請求。這可能會導致一種情況,例如一個資料庫操作失敗,但佇列中還有 15 個操作將在失敗後執行。在同步程式碼中,我們可以在失敗時停止並跳過其餘操作,但我們無法透過 Worker 類型的 API 輕鬆做到這一點。
Worker1
Worker1 API 提供了一個基於 Worker 的基本 sqlite3 介面,適用於客戶端應用程式程式碼位於一個執行緒中,並希望 sqlite3 在其自己的執行緒中運作的情況。
它基於OO1 API,並支援 Worker 執行緒中的多個資料庫連線。在開始之前,似乎有必要指出,此 API 的基於 Promise 的包裝器使用起來要簡單得多,因為它不需要處理 postMessage()
,並且它讓使用者可以更好地控制請求和回應的流程。
為了允許在 Worker 執行緒中載入此 API 而不自動註冊 onmessage 處理程式,初始化 worker API 需要呼叫 sqlite3.initWorker1API()
。如果從非 worker 執行緒呼叫此函式,則會擲出例外狀況。每個 Worker 只能呼叫一次。
啟動 Worker 的最簡單方法是:
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()
,這會觸發客戶端應該監聽的 Worker 訊息
{type:'sqlite3-api', result:'worker1-ready'}
這讓客戶端知道它已初始化。
請注意,基於 Worker 的介面由於其非同步性質,可能會略有不穩定。特別是,在 Worker 開始處理任何訊息之前,可能已向其發送了任意數量的訊息。例如,如果「開啟」操作失敗,任何後續訊息都將失敗。此 API 的 Promise 型包裝器在這方面更易於使用,並使客戶端能夠更好地控制訊息發送的順序和速率。
Worker1 訊息格式
發送到 Worker 的每個訊息都有一個與操作無關的信封和與操作相關的參數。
{
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.
}
回應訊息,發送回調用 Worker 的執行緒,如下所示:
{
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 (關閉)
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
}
}
config-get (取得設定)
此操作會擷取 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 (執行)
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
屬性無法跨越 window/Worker 邊界,因此在這裡沒有用處。如果 args.callback
是一個字串,則假設它是一個訊息類型鍵,在這種情況下,將套用一個回呼函數,該函數通過以下方式發送每個列結果:
postMessage({
type: thatKeyType,
rowNumber: 1-based-#,
row: theRow,
columnNames: anArray
})
row
屬性包含以 rowMode
選項(預設為 'array'
)暗示的形式的列結果。rowNumber
是一個以 1 為基底的整數值,每次呼叫回呼時都會遞增 1。columnNames
陣列包含結果列的欄位名稱。
在結果集的末尾(無論是否產生任何結果列),它將發送一個相同的訊息(row
=undefined
,rowNumber
=null
),以通知呼叫者結果集已完成。請注意,列值 null
對於某些 arg.rowMode
值是合法的列結果。
row
=undefined
,rowNumber
=undefined
)來表示結果結束,因為擷取這些值與從空物件擷取值將無法區分,除非客戶端使用 hasOwnProperty()
(或類似方法)來區分「缺少屬性」和「具有 undefined 值的屬性」。同樣地,在某些情況下,null
是 row
的合法值,而資料庫層不會發出 undefined
的結果值。回呼代理不得遞迴進入此介面。exec()
呼叫將佔用 Worker 執行緒,導致任何遞迴嘗試都必須等待第一個 exec()
完成。
如果 countChanges
參數屬性1 為真值,則返回物件中包含的 result
屬性將具有一個 changeCount
屬性,該屬性保存由提供的 SQL 所做的更改次數。由於 SQL 可能包含任意數量的語句,因此 changeCount
是通過在評估 SQL 之前和之後呼叫 sqlite3_total_changes()
來計算的。如果 countChanges
的值為 64,則 changeCount
屬性將以 BigInt 形式返回一個 64 位元整數(請注意,如果在不支援 BigInt 的建置中使用,則會觸發異常)。在後一種情況下,更改次數是通過在評估 SQL 之前和之後呼叫 sqlite3_total_changes64()
來計算的。
回應是輸入的 options 物件(如果只傳入字串,則會合成一個),可能會被修改。options.resultRows
和 options.columnNames
可能會被 DB.exec()
呼叫填入,而 options.changeCount
也可能會如上所述被設定。
export (匯出)
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 (開啟)
open
訊息指示 worker 開啟資料庫。
訊息格式
{
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
失敗,則後續的所有訊息也可能會失敗,但用戶端程式碼或 worker 無法取消它們。Promiser API 可以透過讓用戶端在繼續之前「等待」open
回應來解決這個問題。
基於 Promise 的包裝器(又稱 Worker1 Promiser)
圍繞 Worker1 API 的基於 Promise 的包裝器提供了比 postMessage()
更友善的介面。與 Worker1 API 類似,此介面在其專用的 Worker 執行緒中載入主要的 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 worker。
該腳本將安裝一個名為 sqlite3Worker1Promiser()
的全域範圍函式,該函式作為建立 promiser 實例的工廠。它有三種呼叫形式
sqlite3Worker1Promiser( config )
config 物件的詳細說明如下。sqlite3Worker1Promiser()
將使用sqlite3Worker1Promiser.defaultConfig
config 物件。sqlite3Worker1Promiser( function )
等同於傳遞{onready: theFunction}
並接受其餘選項的預設值。
可以在呼叫 sqlite3Worker1Promiser()
之前,透過修改 sqlite3Worker1Promiser.defaultConfig
物件來配置第二種和第三種呼叫形式的預設值。
config 物件技術上是可選的,但其 onready
屬性實際上是必需的,因為這是唯一能夠在 sqlite3 模組的非同步載入和初始化完成時收到通知的方法。
onready
回呼而不是返回 Promise 的諷刺意味。事實證明,為 sqlite3Worker1Promiser()
返回 Promise 比使用 onready
回呼更笨拙。設定物件 (config object) 可以包含以下任何屬性,除了 onready
之外,所有屬性都有可用的預設值。
onready
:function()
當 sqlite3 模組和 Worker API 的非同步載入完成時呼叫。這是唯一知道載入已完成的方法。在 3.46 版之前,這個函式沒有傳遞任何參數。從 3.46 版開始,onready()
會傳入sqlite3Worker1Promiser()
返回的函式,因為從這個回呼函式存取它對於某些使用模式來說更方便。同樣從 3.46 版開始,promiser v2 介面 避免了對此回呼函式的需求。worker
: Worker 或 function
載入sqlite3-worker1.js
的 Worker 實例或功能等效項。請注意,promiser factory 會取代 worker.onmessage 屬性。此設定選項也可以是一個函式,在這種情況下,此函式會使用呼叫該函式的結果重新指派此屬性,從而啟用 Worker 的延遲實例化。generateMessageId
:function(messageObject)
一個函式,當傳入一個即將發佈的訊息物件時,會為該訊息產生一個*唯一*的訊息 ID,此 API 接著會將其指派為訊息的 messageId 屬性。它*必須*在每次呼叫時產生唯一的 ID,以便 dispatching 可以正常運作。如果未定義,則使用預設產生器(這對於大多數或所有情況應該足夠)。debug
:function(...)
一個類似console.debug()
的函式,用於記錄有關 Worker 訊息的資訊。onunhandled
:function(event)
一個回呼函式,會傳入此代理未處理的任何worker.onmessage()
事件的訊息事件物件。理想情況下,「應該」永遠不會發生這種情況,因為此代理旨在處理所有已知的訊息類型。
設定好設定物件後,promiser 的實例化方式如下:
const promiser = self.sqlite3Worker1Promiser(config);
Promiser 方法
Promiser 物件是一個具有兩個呼叫簽章的函式
( messageType, messageArguments )
等同於( {type: messageType, args: 類型特定的值} )
其中 type
永遠是一個字串,而 args
值是訊息類型特定的。它總是返回一個 Promise 物件,該物件解析為一個物件
{
type: messageType,
result: type-specific result value,
... possibly other metadata ...
}
每個訊息類型對應一個 API 方法,所有這些方法都對應於 Worker1 API 中的方法,並且具有相同的參數和結果,除非以下明確說明。
錯誤的報告方式與 Worker1 API 相同,但錯誤回應會導致 Promise 被拒絕。因此,客戶端可以透過將 catch()
處理程式新增到他們的 Promise 來監聽這些錯誤。例如
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);
})
close (關閉)
功能類似於 Worker1 的 close
方法,但*如果*它關閉了特定的資料庫,也會清除內部的預設 dbId
。
config-get (取得設定)
除了 open
之外,這是唯一不需要資料庫連線的方法。
exec (執行)
此方法的工作方式幾乎與其 Worker1 對應方法相同,但以下不同之處:
exec
的 {callback: STRING}
選項無法透過此介面運作(它會觸發例外狀況),但 {callback: function}
可以運作,並且其工作方式與 STRING 形式在 Worker 中的工作方式完全相同:每次呼叫回呼函式一次,傳入與 worker API 發出的 worker 訊息格式相同的參數。
{
type: typeString,
row: VALUE,
rowNumber: 1-based-#,
columnNames: anArray
}
其中 typeString
是一個內部合成的訊息類型字串,暫時用於 worker 訊息分派。除了測試此 API 的程式碼之外,所有客戶端程式碼都可以忽略它。
在結果集的末尾,會以 (row
=undefined
, rowNumber
=null
) 觸發相同的事件,以指示已到達結果集的末尾。請注意,資料列是透過 worker 發佈的訊息到達的,並具有所有相關的含義。
open (開啟)
功能類似於 Worker1 的 open
方法,但也會在內部記錄來自回應的 dbId
,*如果*這是開啟的第一個資料庫,以便它可以將該資料庫 ID 用於未來未提供資料庫 ID 的操作。
Promiser v2:另一個 Promise 與 ESM
Promiser v2 於 3.46 版中新增,其運作方式與 v1 介面相同,除了初始化方式之外。
- 它的初始化函式會返回一個 Promise 物件,而不是使用
onready()
處理函式,以便在非同步初始化完成時通知呼叫者。- 如果傳入一個(可選的)
onready()
處理函式,v2 API 會在 resolve Promise 之前立即呼叫它。如果回呼函式拋出錯誤,Promise 就會被 reject。
- 如果傳入一個(可選的)
- 它可以透過
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,它會 resolve 為一個函式,而 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 resolve 之前立即被呼叫。如果它拋出錯誤,promise 就會被 reject。例如
promiserFactory( function(f){
throw new Error("Testing onready throw.");
})
.catch(e=>{ console.error("caught:",e); });
- ^
countChanges
於 3.43 版中新增