跳转到内容

存储 · API

navigator.storage.getDirectory()(OPFS)

发布于

navigator.storage.getDirectory() 解析为源私有文件系统(OPFS)的根 FileSystemDirectoryHandle。OPFS 是受配额管理的文件与目录存储,没有选择器或权限提示把关,用户在文件管理器里也看不到它。在专用 worker 内,FileSystemFileHandle.createSyncAccessHandle() 再加上同步、按偏移寻址的读写,这正是 SQLite 等 Wasm 数据库能在浏览器里实用的原因。

navigator.storage.getDirectory()
fileHandle.createSyncAccessHandle()
fileHandle.createSyncAccessHandle({ mode })

getDirectory() 在窗口和 worker 中都可用(BCD api.StorageManager.getDirectory:Chrome 86、Firefox 111、Safari 15.2)。createSyncAccessHandle() 只存在于专用 worker(Chrome 102、Firefox 111、Safari 15.2),而且名字虽叫 sync,本身返回的是 Promise;真正同步的是返回的 FileSystemSyncAccessHandle 上的方法(read、write、getSize、truncate、flush、close)。OPFS 文件句柄上的异步 createWritable() 到 Safari 26 才加入,所以在 Safari 15.2 到 18 上同步访问句柄是唯一的写入路径。

方法 参数 类型 含义
getDirectory 无 返回该源的 OPFS 根。每个源以及被嵌入源的每个存储分区各有一个根。
createSyncAccessHandle options.mode string,可选 锁模式。"readwrite"(默认):每个文件同时只能有一个句柄。"read-only":可同时打开任意多个句柄,但只能调用 read()、getSize()、close()。"readwrite-unsafe":可同时打开任意多个可写句柄,没有任何协调,一致性由调用方负责。

根下的文件和目录通过 getFileHandle(name, { create }) 与 getDirectoryHandle(name, { create }) 获取,通过 removeEntry(name, { recursive }) 删除。用量计入该源的配额并体现在 navigator.storage.estimate() 中;清除站点数据会删除整棵树。

异常 位置 触发条件
SecurityError DOMException getDirectory() 浏览器无法为该源映射目录,例如不透明源或存储被禁用。
NotFoundError DOMException getFileHandle()、createSyncAccessHandle() 具名条目不存在且未传 create: true,或文件在取得句柄后被删除。
TypeMismatchError DOMException getFileHandle()、getDirectoryHandle() 该名称存在,但是另一种类型的条目。
NoModificationAllowedError DOMException createSyncAccessHandle() 拿不到锁:另一个 "readwrite" 模式的句柄仍打开着,或该文件上有打开的 FileSystemWritableFileStream。常见原因是第二个标签页的 worker 打开了同一个数据库文件。
InvalidStateError DOMException createSyncAccessHandle()、已关闭句柄上的任何方法 句柄不指向 OPFS(来自选择器的句柄),或已经调用过 close()。
NotAllowedError DOMException createSyncAccessHandle() "readwrite" 模式下该句柄的权限状态不是 granted。
QuotaExceededError DOMException write()、truncate() 该源配额已用尽。文件保留失败之前已写入的字节。

write() 返回写入的字节数,但在 flush() 返回之前不保证落盘;在两者之间被终止的 worker 可能丢掉写入的尾部。各异常的完整条件见 FileSystemFileHandle: createSyncAccessHandle() method(developer.mozilla.org)。

worker 示例假定用 new Worker() 创建的专用 worker;共享 worker 和 Service Worker 拿不到同步句柄。

主线程发来一行文本;worker 在页面生命周期内持有一个 "readwrite" 句柄,并在当前大小处追加。按消息开关句柄也可以,但每次都要付出一次加锁往返。

// log-worker.js(专用 worker)
let handle;
self.onmessage = async ({ data: line }) => {
if (!handle) {
const root = await navigator.storage.getDirectory();
const file = await root.getFileHandle('app.log', { create: true });
handle = await file.createSyncAccessHandle();
}
const bytes = new TextEncoder().encode(`${line}\n`);
handle.write(bytes, { at: handle.getSize() });
handle.flush();
};

句柄是独占的,第二个标签页启动同一个 worker 时 createSyncAccessHandle() 会抛出 NoModificationAllowedError;页面应捕获它,然后要么改用 "readwrite-unsafe" 并自己加锁(例如 Web Locks API),要么把写入集中到一个标签页。

API 的异步部分在窗口里可用。getFile() 返回 File,所以普通的 text()、arrayBuffer()、stream() 读取方式都适用。

async function readLog() {
const root = await navigator.storage.getDirectory();
try {
const file = await (await root.getFileHandle('app.log')).getFile();
return await file.text();
} catch (err) {
if (err.name === 'NotFoundError') return ''; // 还没写过日志
throw err;
}
}

来自 OPFS 的 File 是快照:getFile() 兑现之后 worker 追加的字节通过它看不到,要读新内容就重新从句柄取文件。

createSyncAccessHandle 只暴露给专用 worker,在主线程上检测即使在 Chrome 里也是 false。把探测放进 worker 并把结果发回,由页面据此选择存储。

// probe-worker.js(专用 worker)
(async () => {
if (!('storage' in navigator) || typeof navigator.storage.getDirectory !== 'function') {
postMessage('none'); // 没有 OPFS:页面退回 IndexedDB
return;
}
const sync = typeof FileSystemFileHandle !== 'undefined'
&& 'createSyncAccessHandle' in FileSystemFileHandle.prototype;
postMessage(sync ? 'sync' : 'async');
})();

'sync' 表示上面的日志 worker 路径可用;'async' 对应暴露了根但没有同步句柄的引擎,这时数据库仍然更适合放在 IndexedDB。

规范

规范状态
无。