永久儲存選項

此 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」的 VFS 可以與任何接受 VFS 名稱的 sqlite3 API 一起使用,例如 sqlite3_vfs_find()sqlite3_db_open_v2()sqlite3.oo1.DB 建構函式,請注意 OpfsDboo1.DB 的便捷子類別,它會自動使用此 VFS。對於 URI 樣式的名稱,請使用 file:my.db?vfs=opfs

⚠️注意:Safari 版本 < 17

Safari 17 以前的版本與目前的 OPFS VFS 實作不相容,因為瀏覽器處理來自子 Worker 的儲存空間時出現錯誤,而此錯誤沒有解決方法。SharedAccessHandle 集區 VFSWASMFS 支援 都提供了替代方案,應該適用於 Safari 16.4 或更高版本。

⚠️注意:COOP 和 COEP HTTP 標頭

為了提供一定程度的透明並行資料庫存取支援,OPFS VFS 需要 JavaScript 的 SharedArrayBuffer 類型,而只有當 Web 伺服器在傳遞指令碼時包含所謂的 COOPCOEP 回應標頭,該類別才可用

Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin

如果沒有這些標頭,SharedArrayBuffer 將無法使用,因此 OPFS VFS 將無法載入。該類別是協調 sqlite3_vfs OPFS 代理的同步和非同步部分之間的通訊所必需的。

COEP 標頭的值也可以是 credentialless,但這是否適用於任何給定的應用程式取決於它如何使用其他遠端資源。

如何發出這些標頭取決於底層的網路伺服器。

Apache 網路伺服器

對於 Apache 網路伺服器,這些標頭的設定方式如下所示:

<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 網路伺服器

對於 althttpd 網路伺服器,請使用 --enable-sab 旗標啟動它(「sab」是 SharedArrayBuffer 的縮寫)。

Cloudflare Pages

請參閱 https://developers.cloudflare.com/pages/configuration/headers

其他網路伺服器

如果您知道如何在其他網路伺服器中設定 COOP/COEP 標頭,請在 SQLite 論壇 上告知我們,我們會將這些資訊更新到文件中。

資料庫名稱中的目錄部分

與大多數 sqlite3_vfs 實作不同,如果資料庫是以「create」旗標開啟的,這個實作會自動建立資料庫檔案名稱的任何前導目錄部分。這種與常見慣例的差異是為了……

例如:

const db = new sqlite3.oo1.OpfsDb('/path/to/my.db','c');

如果需要,將會建立 /path/to 目錄。沒有前導斜線的路徑在功能上是等效的,從 OPFS 根目錄開始。

將資料庫匯入 OPFS

請參閱 OpfsDb 文件

並行處理和檔案鎖定

⚠️**預先警告:** 並行存取 OPFS 託管的檔案是此 VFS 的一個痛點。用戶端應用程式將無法在此環境中實現桌面應用程式級別的並行性,但在瀏覽器分頁和/或 Workers 之間可以實現一定程度的並行性。

背景:OPFS 提供了一些此 API 所需的同步 API。可以在非同步模式下開啟檔案而無需任何鎖定,但存取同步 API 需要 OPFS 所謂的「同步存取控制代碼」,它會*獨佔鎖定*檔案。只要 OPFS 檔案被鎖定,在同一個 HTTP 源 中執行的任何其他服務都無法開啟該檔案。例如,只要檔案被另一個來自相同源的索引標籤鎖定,在一個瀏覽器索引標籤中執行的程式碼就無法存取該檔案。

*實質上*,這意味著沒有兩個資料庫控制代碼可以同時開啟同一個 OPFS 託管的資料庫。如果在兩個索引標籤中開啟同一個頁面,第二個索引標籤會在嘗試開啟同一個 OPFS 託管的資料庫時遇到鎖定錯誤!

為了減輕在多個索引標籤或工作執行緒中執行的 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 閒置一段未指定的短時間(少於半秒),此時所有隱式取得的鎖定都會被釋放。

此類鎖定在內部稱為隱式鎖定或自動鎖定。它們是 OPFS 需要但 sqlite3 VFS 不需要的鎖定。通常,取得鎖定的操作在操作結束時 *不會* 自動釋放鎖定,因為這樣做會造成巨大的效能損失(在 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 支援 `delete-before-open=1` 的 URI 旗標,以指示 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 是一個使用此 VFS 的 sqlite3.oo1.DB 類別 子類別。

const db = new PoolUtil.OpfsSAHPoolDb('/filename');
// ^^^ note that all paths for this VFS must be absolute!

池管理

installOpfsSAHPoolVfs() 會回傳一個 Promise,成功時會解析為一個可用於執行檔案池基本管理的工具物件(通稱為 PoolUtil)。只要每次呼叫時都使用相同的 name 選項,則多次呼叫 installOpfsSAHPoolVfs() 在第二次和後續呼叫時會解析為相同的值。使用不同的名稱呼叫它會回傳不同的 Promise,解析為具有不同 VFS 註冊的不同物件。

其 API 包括,依字母順序排列...

並行性

opfs-sahpool VFS 無法在程式庫層級提供任何並行支援,因為它會預先分配所有潛在的 SAH,這會立即鎖定這些檔案。然而,Roy Hashimoto 撰寫了一些文章,探討了針對此問題的客戶端解決方案

此 VFS 中還有一些工作要做,以協助實作客戶端並行,例如停止和重新啟動 VFS 的能力。

使用 OPFS 的 WAL 模式

從 3.47 版開始,可以為 OPFS 托管的資料庫啟用 WAL 模式,但有一些注意事項

透過 WASMFS 使用 OPFS

(在 3.43 版中(重新)新增。)

除了 OPFS VFSSharedAccessHandle Pool VFS 之外,另一種選擇是 Emscripten 的 WASMFS,它以與這兩種 VFS 截然不同的方式支援 OPFS。它將 OPFS 公開為 Emscripten 向用戶端程式碼公開的虛擬檔案系統上的「掛載點」(目錄),並且儲存在該目錄下的所有檔案都存放在 OPFS 中。

注意: sqlite3.wasm 的標準版本中未啟用 WASMFS,因為它需要一個單獨的、可攜性較差的 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。