源私有文件系统
注意:此功能在 Web Workers 中可用。
源私有文件系统(OPFS)是作为文件系统 API 的一部分提供的存储端点,它仅对页面的源可见,不像常规文件系统那样对用户可见。它提供了对一种特殊文件的访问,这种文件在性能方面经过高度优化,并允许对其内容进行原地写入访问。
使用文件系统访问 API 处理文件
扩展了文件系统 API 的文件系统访问 API,通过选择器方法提供对文件的访问。例如:
Window.showOpenFilePicker()允许用户选择要访问的文件,这将返回一个FileSystemFileHandle对象。- 调用
FileSystemFileHandle.getFile()来访问文件内容,然后使用FileSystemFileHandle.createWritable()/FileSystemWritableFileStream.write()修改内容。 - 使用
FileSystemHandle.requestPermission({mode: 'readwrite'})请求用户保存更改的权限。 - 如果用户接受权限请求,更改将保存回原始文件。
这可行,但有一些限制。这些更改是针对用户可见的文件系统进行的,因此存在许多安全检查(例如,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 } 传递给上述方法会导致在文件或文件夹不存在时创建它们。
// 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");
读取文件
- 调用
FileSystemDirectoryHandle.getFileHandle()返回一个FileSystemFileHandle对象。 - 调用
FileSystemFileHandle.getFile()对象返回一个File对象。这是一种特殊的Blob类型,因此可以像任何其他Blob一样对其进行操作。例如,您可以通过Blob.text()直接访问文本内容。
写入文件
- 调用
FileSystemDirectoryHandle.getFileHandle()返回一个FileSystemFileHandle对象。 - 调用
FileSystemFileHandle.createWritable()返回一个FileSystemWritableFileStream对象,这是一种特殊的WritableStream类型。 - 使用
FileSystemWritableFileStream.write()调用向其中写入内容。 - 使用
WritableStream.close()关闭流。
删除文件或文件夹
您可以对父目录调用FileSystemDirectoryHandle.removeEntry(),并将要删除的项的名称作为参数传递。
directoryHandle.removeEntry("my first nested file");
您还可以对代表要删除的项的FileSystemFileHandle 或FileSystemDirectoryHandle 调用FileSystemHandle.remove()。要删除一个文件夹及其所有子文件夹,请传递 { recursive: true } 选项。
await fileHandle.remove();
await directoryHandle.remove({ recursive: true });
以下提供了一种快速清空整个 OPFS 的方法
await (await navigator.storage.getDirectory()).remove({ recursive: true });
列出文件夹内容
FileSystemDirectoryHandle 是一个异步迭代器。因此,您可以使用 for await...of 循环和标准方法(如entries()、values() 和keys())对其进行迭代。
例如
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() 方法本身是异步的。
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():关闭访问句柄。
以下是一个使用上述所有方法的示例
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);
浏览器兼容性
加载中…