源私有文件系统

**安全上下文:** 此功能仅在 安全上下文 (HTTPS) 中,在部分或所有 支持的浏览器 中可用。

**注意:** 此功能在 Web 工作线程 中可用。

**源私有文件系统** (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 工作线程中运行,以免阻塞主线程。

总结一下 OPFS 与用户可见文件系统的区别

  • OPFS 受到 浏览器存储配额限制 的约束,就像任何其他来源分区的存储机制一样(例如 IndexedDB API)。您可以通过 navigator.storage.estimate() 访问 OPFS 使用的存储空间量。
  • 清除站点的存储数据会删除 OPFS。
  • 访问 OPFS 中的文件不需要权限提示和安全检查。
  • 浏览器会将 OPFS 的内容持久化到磁盘的某个位置,但您无法期望找到与之一一对应的创建的文件。OPFS 不打算对用户可见。

如何访问 OPFS?

要首先访问 OPFS,您可以调用 navigator.storage.getDirectory() 方法。这将返回对表示 OPFS 根目录的 FileSystemDirectoryHandle 对象的引用。

从主线程操作 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 工作线程操作 OPFS

Web 工作线程不会阻塞主线程,这意味着您可以在这种情况下使用同步文件访问 API。同步 API 速度更快,因为它们避免了处理 Promise 的需要。

您可以通过在常规的 FileSystemFileHandle 上调用 FileSystemFileHandle.createSyncAccessHandle() 来同步访问文件

**注意:** 尽管名称中包含“Sync”,但 createSyncAccessHandle() 方法本身是异步的。

js
const opfsRoot = await navigator.storage.getDirectory();
const fileHandle = await opfsRoot.getFileHandle("my highspeed 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);

另请参阅