源私有檔案系統

Baseline 已廣泛支援

此功能已成熟,並可在許多裝置和瀏覽器版本上執行。自 2023 年 3 月以來,它已在各種瀏覽器中可用。

安全上下文: 此功能僅在安全上下文(HTTPS)中可用,且支援此功能的瀏覽器數量有限。

注意:此功能在 Web Workers 中可用。

源私有檔案系統(OPFS)是作為檔案系統 API 的一部分提供的儲存端點,它僅對頁面的源可見,不像常規檔案系統那樣對使用者可見。它提供了對一種特殊檔案的訪問,這種檔案在效能方面經過高度最佳化,並允許對其內容進行原地寫入訪問。

使用檔案系統訪問 API 處理檔案

擴充套件了檔案系統 API檔案系統訪問 API,透過選擇器方法提供對檔案的訪問。例如:

  1. Window.showOpenFilePicker() 允許使用者選擇要訪問的檔案,這將返回一個FileSystemFileHandle 物件。
  2. 呼叫FileSystemFileHandle.getFile() 來訪問檔案內容,然後使用FileSystemFileHandle.createWritable() / FileSystemWritableFileStream.write() 修改內容。
  3. 使用FileSystemHandle.requestPermission({mode: 'readwrite'}) 請求使用者儲存更改的許可權。
  4. 如果使用者接受許可權請求,更改將儲存回原始檔案。

這可行,但有一些限制。這些更改是針對使用者可見的檔案系統進行的,因此存在許多安全檢查(例如,Chrome 中的安全瀏覽)來防止惡意內容寫入該檔案系統。這些寫入不是原地進行的,而是使用臨時檔案。原始檔案只有在透過所有安全檢查後才會被修改。

因此,這些操作相當緩慢。在進行小的文字更新時,這不太明顯,但在進行更重大的、大規模的檔案更新時,例如SQLite 資料庫修改時,效能會受到影響。

OPFS 如何解決這些問題?

OPFS 提供低級別的、逐位元組的檔案訪問,它僅對頁面的源可見,對使用者不可見。因此,它不需要同樣一系列的安全檢查和許可權授予,從而比檔案系統訪問 API 呼叫更快。它還提供了一組同步呼叫(其他檔案系統 API 呼叫是非同步的),這些呼叫只能在 Web Worker 中執行,以免阻塞主執行緒。

總結 OPFS 與使用者可見檔案系統的區別

  • 與任何其他源分割槽儲存機制(例如IndexedDB API)一樣,OPFS 受瀏覽器儲存配額限制。您可以透過navigator.storage.estimate() 訪問 OPFS 使用的儲存空間量。
  • 清除網站的儲存資料會刪除 OPFS。
  • 訪問 OPFS 中的檔案不需要許可權提示和安全檢查。
  • 瀏覽器將 OPFS 的內容持久化到磁碟的某個位置,但您不能期望找到一一對應的建立的檔案。OPFS 不打算對使用者可見。

如何訪問 OPFS?

要訪問 OPFS,首先需要呼叫navigator.storage.getDirectory() 方法。這將返回一個FileSystemDirectoryHandle 物件引用,該物件代表 OPFS 的根目錄。

在主執行緒中操作 OPFS

從主執行緒訪問 OPFS 時,將使用非同步的、基於Promise 的 API。透過在代表 OPFS 根目錄(以及建立的子目錄)的FileSystemDirectoryHandle 物件上呼叫FileSystemDirectoryHandle.getFileHandle()FileSystemDirectoryHandle.getDirectoryHandle(),可以分別訪問檔案(FileSystemFileHandle)和目錄(FileSystemDirectoryHandle)控制代碼。

注意:{ create: true } 傳遞給上述方法會導致在檔案或資料夾不存在時建立它們。

js
// Create a hierarchy of files and folders
const fileHandle = await opfsRoot.getFileHandle("my first file", {
  create: true,
});
const directoryHandle = await opfsRoot.getDirectoryHandle("my first folder", {
  create: true,
});
const nestedFileHandle = await directoryHandle.getFileHandle(
  "my first nested file",
  { create: true },
);
const nestedDirectoryHandle = await directoryHandle.getDirectoryHandle(
  "my first nested folder",
  { create: true },
);

// Access existing files and folders via their names
const existingFileHandle = await opfsRoot.getFileHandle("my first file");
const existingDirectoryHandle =
  await opfsRoot.getDirectoryHandle("my first folder");

讀取檔案

  1. 呼叫FileSystemDirectoryHandle.getFileHandle() 返回一個FileSystemFileHandle 物件。
  2. 呼叫FileSystemFileHandle.getFile() 物件返回一個File 物件。這是一種特殊的Blob 型別,因此可以像任何其他 Blob 一樣對其進行操作。例如,您可以透過Blob.text() 直接訪問文字內容。

寫入檔案

  1. 呼叫FileSystemDirectoryHandle.getFileHandle() 返回一個FileSystemFileHandle 物件。
  2. 呼叫FileSystemFileHandle.createWritable() 返回一個FileSystemWritableFileStream 物件,這是一種特殊的WritableStream 型別。
  3. 使用FileSystemWritableFileStream.write() 呼叫向其中寫入內容。
  4. 使用WritableStream.close() 關閉流。

刪除檔案或資料夾

您可以對父目錄呼叫FileSystemDirectoryHandle.removeEntry(),並將要刪除的項的名稱作為引數傳遞。

js
directoryHandle.removeEntry("my first nested file");

您還可以對代表要刪除的項的FileSystemFileHandleFileSystemDirectoryHandle 呼叫FileSystemHandle.remove()。要刪除一個資料夾及其所有子資料夾,請傳遞 { recursive: true } 選項。

js
await fileHandle.remove();
await directoryHandle.remove({ recursive: true });

以下提供了一種快速清空整個 OPFS 的方法

js
await (await navigator.storage.getDirectory()).remove({ recursive: true });

列出資料夾內容

FileSystemDirectoryHandle 是一個非同步迭代器。因此,您可以使用 for await...of 迴圈和標準方法(如entries()values()keys())對其進行迭代。

例如

js
for await (let [name, handle] of directoryHandle) {
}
for await (let [name, handle] of directoryHandle.entries()) {
}
for await (let handle of directoryHandle.values()) {
}
for await (let name of directoryHandle.keys()) {
}

在 Web Worker 中操作 OPFS

Web Workers 不會阻塞主執行緒,這意味著您可以在此上下文中執行同步檔案訪問 API。同步 API 更快,因為它們避免了處理 Promise。

您可以透過在常規FileSystemFileHandle 上呼叫FileSystemFileHandle.createSyncAccessHandle() 來同步訪問檔案。

注意:儘管其名稱中包含“Sync”(同步),但 createSyncAccessHandle() 方法本身是非同步的。

js
const opfsRoot = await navigator.storage.getDirectory();
const fileHandle = await opfsRoot.getFileHandle("my-high-speed-file.txt", {
  create: true,
});
const syncAccessHandle = await fileHandle.createSyncAccessHandle();

在返回的FileSystemSyncAccessHandle 上有許多*同步*方法可用:

  • getSize():返回檔案的大小(以位元組為單位)。
  • write():將緩衝區的內容寫入檔案,可以選擇在給定偏移量處寫入,並返回寫入的位元組數。檢查返回的寫入位元組數允許呼叫者檢測和處理錯誤及部分寫入。
  • read():將檔案內容讀取到緩衝區中,可以選擇在給定偏移量處讀取。
  • truncate():將檔案大小調整為給定大小。
  • flush():確保檔案內容包含透過 write() 進行的所有修改。
  • close():關閉訪問控制代碼。

以下是一個使用上述所有方法的示例

js
const opfsRoot = await navigator.storage.getDirectory();
const fileHandle = await opfsRoot.getFileHandle("fast", { create: true });
const accessHandle = await fileHandle.createSyncAccessHandle();

const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();

// Initialize this variable for the size of the file.
let size;
// The current size of the file, initially `0`.
size = accessHandle.getSize();
// Encode content to write to the file.
const content = textEncoder.encode("Some text");
// Write the content at the beginning of the file.
accessHandle.write(content, { at: size });
// Flush the changes.
accessHandle.flush();
// The current size of the file, now `9` (the length of "Some text").
size = accessHandle.getSize();

// Encode more content to write to the file.
const moreContent = textEncoder.encode("More content");
// Write the content at the end of the file.
accessHandle.write(moreContent, { at: size });
// Flush the changes.
accessHandle.flush();
// The current size of the file, now `21` (the length of
// "Some textMore content").
size = accessHandle.getSize();

// Prepare a data view of the length of the file.
const dataView = new DataView(new ArrayBuffer(size));

// Read the entire file into the data view.
accessHandle.read(dataView, { at: 0 });
// Logs `"Some textMore content"`.
console.log(textDecoder.decode(dataView));

// Read starting at offset 9 into the data view.
accessHandle.read(dataView, { at: 9 });
// Logs `"More content"`.
console.log(textDecoder.decode(dataView));

// Truncate the file after 4 bytes.
accessHandle.truncate(4);

瀏覽器相容性

另見