做离线编辑、断点续传、本地缓存媒体时,常会卡在「数据太大,塞进 IndexedDB 别扭,又不想事事打到自己的服务器」。File System API 这一支里,其实并排着两套味道很不一样的能力:一套是弹出系统选择器、读写用户看得见的文件。另一套是源私有的 Origin private file system(OPFS)。混着搜「浏览器文件系统」容易装错门。MDN 标明这类能力跑在安全上下文(HTTPS 等),Worker 里也能用,具体兼容情况看兼容表。
用户可见目录:选择器 + 权限
MDN 在 File System API 总览里写得很清楚:多数和本地文件的交互通过句柄完成,常见入口是 showOpenFilePicker、showDirectoryPicker、showSaveFilePicker。用户选中之后,你才拿到 FileSystemFileHandle / FileSystemDirectoryHandle。写回用户可见磁盘时,安全检查和权限提示更多,MDN 也提到这类写入往往不是简单的原地改,性能在大规模更新(比如改本地库文件)时会吃亏。适合「打开用户自己的文档、另存为、拖拽进来继续编」这种必须让人看见路径的场景。句柄还可以进 IndexedDB 或经 postMessage 传递,方便下次会话接着用。前提仍是用户曾经授权过那条路径。
OPFS:同源私有,用户翻不到
OPFS 是同一套 API 下的存储端点,但私有于页面源,不像常规文件系统那样暴露给用户浏览。MDN 概括几条和「用户可见盘」的差别:
- 受浏览器存储配额约束,和其他按源隔离的存储类似。用量可用
navigator.storage.estimate()查看 - 清站点数据会删掉 OPFS
- 访问其中的文件一般不走那套权限弹窗和面向用户盘的安全检查
- 内容会持久化到磁盘某处,但不要指望在资源管理器里找到一一对应的文件名
入口是 navigator.storage.getDirectory(),返回代表 OPFS 根目录的 FileSystemDirectoryHandle。主线程上继续用异步的 getFileHandle / getDirectoryHandle(可带 { create: true })、createWritable() 写入、remove() / removeEntry() 删除。列目录可以把目录句柄当异步迭代器,for await...of 配 entries() / values() / keys()。需要清空整棵 OPFS 时,MDN 给过对根目录 remove({ recursive: true }) 的写法,上线前先确认那是你想要的「整站私有文件清零」,别和用户文档目录搞混。
需要更高吞吐、按字节读写时,MDN 指向在 Worker 里对文件句柄调用 createSyncAccessHandle(),再用同步的 read / write / flush / truncate / getSize / close。创建句柄的那一步本身仍是异步的,只是拿到句柄之后的读写是同步 API,避免在主线程里用 Promise 把热路径拖慢。
什么时候倾向 OPFS
MDN 列过一类用例:持久化上传队列(先拷进沙箱再分片传)、游戏或媒体资源解包缓存、音视频/图片编辑的本地缓存与原地改元数据、大文件边下边看、邮件附件本地暂存。共同特点是:应用自己管理生命周期,用户不必在访达 / 资源管理器里点开那个缓存目录。若产品语义就是「打开我磁盘上的那份合同」,仍然走选择器,不要硬塞进 OPFS 再假装那是用户文件。
配额、驱逐,别当成无限盘
OPFS 吃的是源的存储配额,不是「另挂了一块无限硬盘」。清理站点数据、用户或浏览器按存储压力淘汰,都可能让你写进去的东西消失。实现上应假定:重要数据要有可重建路径或与服务器的同步策略。estimate() 只是观察,不是担保。本文不写任何绕过存储隔离、跨源偷读的做法。能用的边界以 MDN 和浏览器实现为准。
本地工具型 PWA 若只是草稿和分片缓存,OPFS 往往比把大 Blob 全塞进别的键值存储好推。你是先在主线程异步 API 打通读写,还是一上来就为热路径开 Worker + SyncAccessHandle?