2019 年,一个名为「Native File System API」的提案出现在 WICG(Web Incubator Community Group)的讨论区。它的目标很直接:让 Web 应用能够像原生应用一样读写用户的本地文件。三年后,Chrome 86 正式发布了这项功能,更名为 File System Access API。而在大洋彼岸,Mozilla 的工程师在 GitHub issue 中留下了五个字的评价:「harmful」(有害)。

浏览器与本地文件系统之间的那堵墙,正在被悄然拆除。

从下载到编辑:Web 文件操作的二十年困境

在 File System Access API 出现之前,Web 应用处理本地文件的方式始终停留在「下载」层面。

传统模式下,用户选择文件通过 <input type="file"> 元素完成。这个元素返回一个 FileList,其中的 File 对象是一个特殊的 Blob,可以通过 FileReader 读取内容。但读取之后呢?修改后的数据只能通过创建 <a download> 元素,设置 blob: URL 后触发下载。用户得到的是一个新文件副本,永远保存在系统的 Downloads 文件夹中。

// 传统保存文件的方式:只能下载副本
const saveFile = async (blob) => {
  const a = document.createElement("a");
  a.download = "my-file.txt";
  a.href = URL.createObjectURL(blob);
  a.click();
  setTimeout(() => URL.revokeObjectURL(a.href), 30 * 1000);
};

这种「打开→编辑→下载副本」的工作流与用户对桌面应用的认知完全相悖。用户习惯了「打开→编辑→保存」的循环,文件应该被原地更新,而不是被复制到某个角落。

问题的本质在于浏览器的安全模型。自 Netscape 时代起,浏览器就被设计为严格隔离 Web 内容与用户系统。任何网页都不应该能够随意读写用户的硬盘——这是 Web 安全的基石。但这种保护也限制了 Web 应用的能力边界,使得像在线 IDE、图像编辑器这类需要频繁操作本地文件的应用难以提供与原生应用媲美的体验。

File System Access API:一次审慎的突破

File System Access API 并不是简单地打开权限大门,而是设计了一套精细的授权机制。

API 的入口点是三个方法:showOpenFilePicker()showSaveFilePicker()showDirectoryPicker()。每个方法都必须由用户手势(如点击按钮)触发,在安全上下文(HTTPS)中执行。

// 用户选择文件后,获得 FileSystemFileHandle
let fileHandle;
butOpenFile.addEventListener('click', async () => {
  [fileHandle] = await window.showOpenFilePicker();
  const file = await fileHandle.getFile();
  const contents = await file.text();
  textArea.value = contents;
});

showOpenFilePicker() 返回的是一个 FileSystemFileHandle 对象,而不是文件内容本身。这个句柄可以被保存在 IndexedDB 中,在后续会话中重新使用。真正读取文件内容需要调用 handle.getFile(),返回一个标准的 File 对象。

写入文件则需要创建一个可写流:

async function writeFile(fileHandle, contents) {
  const writable = await fileHandle.createWritable();
  await writable.write(contents);
  await writable.close();
}

关键的设计细节在于:createWritable() 调用时,浏览器会检查是否已获得写入权限。如果没有,会弹出权限请求对话框。这个请求必须由用户手势触发,而且只有在调用 createWritable() 时才会出现——这意味着读取权限和写入权限是分离的。

图片来源: Chrome Developers - File System Access API

安全模型:每一层都是一道防线

File System Access API 的安全设计体现了「纵深防御」的理念。

第一层:用户手势

所有文件选择器方法都需要由用户手势触发。不能在页面加载时自动弹出文件选择器,不能在定时器回调中触发,不能在 fetch 的 Promise 回调中触发。这是为了防止恶意网站在用户毫无防备时诱导其授权。

Chrome 团队在实现中发现了一个常见陷阱:开发者往往希望在调用 showSaveFilePicker() 之前先处理数据,以避免用户等待。但这会导致用户手势上下文丢失,API 调用失败。正确的做法是先获取文件句柄,再开始数据处理。

第二层:文件选择器

用户必须通过系统的文件选择器明确选择要访问的文件或目录。网页无法枚举文件系统,无法猜测路径,无法静默访问任何文件。用户的选择是唯一的授权来源。

第三层:受限路径

浏览器会阻止用户选择某些敏感路径。Windows 系统目录(如 C:\Windows)、macOS Library 文件夹等被列为禁止区域。即使用户试图选择这些位置,浏览器也会显示警告并拒绝访问。

第四层:权限提示

当 Web 应用试图修改一个已存在的文件时(而不是保存新文件),浏览器会弹出额外的权限确认对话框。这个对话框明确告知用户:网站想要修改您选择的文件。

图片来源: Chrome Developers - File System Access API

第五层:可见性指示

一旦用户授予了文件访问权限,浏览器地址栏会显示一个文件夹图标。点击图标可以查看当前网站有权访问的所有文件,用户可以随时撤销这些权限。

第六层:会话隔离

权限默认不会跨会话持久化。当所有同源标签页关闭后,权限自动失效。下次访问时需要重新授权。不过,Chrome 122 引入了持久权限功能,用户可以选择「始终允许」,但这需要明确的用户选择。

Mozilla 的反对立场:安全哲学的分歧

Mozilla 对 File System Access API 的态度经历了从「defer」(推迟)到「harmful」(有害)的转变。

在 GitHub 的 standards-positions 仓库中,Mozilla 工程师详细阐述了他们的顾虑:

  1. 用户欺骗风险:普通用户可能不理解授予一个网站文件访问权限的后果。恶意网站可以通过社会工程学手段诱导用户授权访问敏感文件。

  2. 攻击面扩大:传统的 Web 攻击只需要攻破浏览器沙箱。如果网站获得了文件系统访问权限,攻击者可能同时获得对用户文件的直接访问。

  3. 隐私泄露:即使用户只授权访问单个文件,攻击者仍可能通过分析文件元数据、访问时间等信息获取额外隐私。

Mozilla 的替代建议是:Web 应用应该通过操作系统提供的文件关联机制(如桌面应用的「打开方式」菜单)来处理文件,而不是主动请求文件访问权限。

这种分歧反映了两种安全哲学的碰撞。Chrome 团队认为,只要提供足够的透明度和控制,用户应该有权选择是否信任一个 Web 应用。Mozilla 则认为,普通用户无法做出明智的安全决策,浏览器应该充当更严格的守护者。

有趣的是,Mozilla 对 File System Access API 的子集——Origin Private File System(OPFS)——持支持态度。OPFS 是一个完全隔离的虚拟文件系统,不涉及用户可见文件,因此不引发同样的安全顾虑。

Origin Private File System:另一个世界

OPFS 是 File System Access API 中最容易被忽视,却最具实用价值的一部分。

与用户可见文件系统不同,OPFS 是一个完全虚拟的文件系统,与浏览器源(origin)绑定。用户无法通过操作系统的文件管理器访问这些文件,甚至无法确定这些文件在物理磁盘上的具体位置。

const opfsRoot = await navigator.storage.getDirectory();
const fileHandle = await opfsRoot.getFileHandle('my-data', { create: true });
const directoryHandle = await opfsRoot.getDirectoryHandle('cache', { create: true });

OPFS 的设计初衷是为 WebAssembly 应用提供高性能文件 I/O。传统的 Web 存储方案(如 IndexedDB)虽然功能强大,但对于需要字节级随机访问的场景效率较低。SQLite 编译到 WebAssembly 后,需要一个文件系统后端来存储数据库文件——OPFS 正是为这种场景量身打造的。

主线程 vs Web Worker:两种访问模式

OPFS 提供两种访问模式:主线程的异步模式和 Web Worker 的同步模式。

主线程模式下,使用 createWritable() 创建异步写入流:

const writable = await fileHandle.createWritable();
await writable.write(contents);
await writable.close();

在 Web Worker 中,可以使用 createSyncAccessHandle() 获得同步访问句柄:

const accessHandle = await fileHandle.createSyncAccessHandle();
const writtenBytes = accessHandle.write(buffer);
const readBytes = accessHandle.read(buffer, { at: 1 });
accessHandle.flush();
accessHandle.close();

同步模式的设计是基于一个现实:许多编译到 WebAssembly 的原生代码使用同步文件 I/O。强制这些代码使用异步 API 会导致严重的性能开销和代码复杂度。

图片来源: web.dev - Origin Private File System

OPFS 与 IndexedDB 的性能对比

RxDB 团队进行了一系列基准测试,对比了 OPFS、IndexedDB、LocalStorage 等多种浏览器存储方案。结果令人印象深刻:

操作类型 OPFS (Worker 同步) IndexedDB LocalStorage
初始化时间 26.8ms 46ms 0ms
单次写入延迟 1.54ms 0.17ms 0.017ms
单次读取延迟 1.41ms 0.1ms 0.0052ms
批量写入(200条) 104ms 13.41ms 5.79ms
批量读取(100条) 25.61ms 4.99ms 0.39ms

数据来源: RxDB - LocalStorage vs IndexedDB vs OPFS

单看延迟数据,LocalStorage 似乎最快,但这是有代价的:LocalStorage 是同步 API,会阻塞主线程,对于大量数据处理并不适用。

OPFS 在 Web Worker 中的同步模式展现出独特的优势:初始化快、适合流式处理、不阻塞主线程。对于需要处理大量二进制数据的应用(如 SQLite、图像处理),OPFS 是当前最佳选择。

生产环境中的实践

VSCode.dev:让浏览器成为开发环境

VSCode.dev 是最早采用 File System Access API 的大型项目之一。它需要处理的核心问题是:如何让一个在浏览器中运行的代码编辑器,能够像桌面应用一样打开、编辑、保存本地项目文件夹。

实现路径很清晰:

  1. 用户点击「打开文件夹」,调用 showDirectoryPicker()
  2. 获得目录句柄后,递归枚举所有文件和子目录
  3. 用户编辑文件后,通过句柄的 createWritable() 写回修改
  4. 目录句柄被保存到 IndexedDB,下次访问时尝试恢复权限

VSCode.dev 的实现展示了一个关键模式:文件句柄可以被序列化存储。这意味着用户的工作状态可以在会话间保持,而不需要每次都重新选择文件夹。

Photopea:从浏览器到系统文件关联

Photopea 是一个在线图像编辑器,支持 PSD、XCF、Sketch 等 20 多种格式。它的 File System Access API 使用方式更进一步,结合了 File Handling API。

File Handling API 允许 PWA 注册为特定文件类型的处理器。在 Web App Manifest 中声明:

{
  "file_handlers": [
    { "action": "/", "accept": { "image/psd": [".psd"] } },
    { "action": "/", "accept": { "image/jpeg": [".jpeg", ".jpg"] } }
  ]
}

安装 PWA 后,用户可以在操作系统的「打开方式」菜单中选择 Photopea。当用户双击一个 PSD 文件并选择 Photopea 时,浏览器会启动 PWA 并通过 launchQueue 传递文件句柄:

window.launchQueue.setConsumer((launchParams) => {
  const files = launchParams.files;
  files.forEach(async (handle) => {
    const file = await handle.getFile();
    // 处理文件...
  });
});

这种集成方式让 Web 应用真正融入了操作系统的文件处理流程,模糊了 Web 与原生应用的边界。

图片来源: Chrome Developers - How Photopea uses File Handling API

Excalidraw:渐进式增强的典范

Excalidraw 是一个开源的白板绘图工具,它采取了更务实的策略:使用 browser-fs-access 库实现渐进式增强。

browser-fs-access 是一个「ponyfill」(而非 polyfill)——它在支持 File System Access API 的浏览器上使用原生 API,在不支持的浏览器上回退到传统的文件上传/下载方式。

import { fileOpen, fileSave } from 'browser-fs-access';

// 打开文件
const blob = await fileOpen({
  mimeTypes: ['image/*'],
  multiple: true
});

// 保存文件
await fileSave(blob, {
  fileName: 'drawing.excalidraw',
  extensions: ['.excalidraw']
});

这种策略的巧妙之处在于:同一份代码,在 Chrome 上提供「真正的保存」体验,在 Safari 上回退到「下载副本」。用户界面需要相应调整——Excalidraw 在支持 File System Access API 的浏览器上显示「保存」和「另存为」两个按钮,在不支持的浏览器上只显示「保存」(实际是下载)。

浏览器兼容性:分裂的现实

截至 2025 年,File System Access API 的浏览器支持情况如下:

浏览器 用户可见文件系统 OPFS 持久权限
Chrome 86+ ✅ (122+)
Edge 86+
Safari
Firefox
Brave ⚠️ (需启用 flag)

数据来源: Can I use - Native File System API

这种分裂状况意味着:

  1. 如果你只需要 OPFS:所有主流浏览器都支持,可以放心使用。

  2. 如果你需要用户可见文件系统:必须提供回退方案。对于注重兼容性的项目,browser-fs-access 是一个可靠的选择。

  3. Safari 和 Firefox 用户:目前无法享受「原地保存」的体验,只能使用传统的下载方式。

Safari 在 2022 年实现了 OPFS,但明确表示不会支持用户可见文件系统的访问。Firefox 的立场类似——他们支持 OPFS 作为高性能存储后端,但反对让网页直接访问用户文件。

最佳实践:从 API 到工程

1. 永远检查 API 支持

if ('showOpenFilePicker' in window) {
  // 使用 File System Access API
} else {
  // 回退到 <input type="file">
}

2. 处理权限拒绝

权限请求可能被用户拒绝,必须妥善处理异常:

try {
  const writable = await fileHandle.createWritable();
  // ...
} catch (err) {
  if (err.name === 'NotAllowedError') {
    // 用户拒绝了权限请求
    showFallbackSaveDialog();
  }
}

3. 句柄持久化与验证

句柄可以存储在 IndexedDB 中,但在使用前必须验证权限:

const savedHandle = await getHandleFromIndexedDB();
const permission = await savedHandle.queryPermission({ mode: 'readwrite' });
if (permission !== 'granted') {
  const requestResult = await savedHandle.requestPermission({ mode: 'readwrite' });
  if (requestResult !== 'granted') {
    // 无法获得权限,需要重新选择文件
  }
}

4. OPFS 的 Worker 模式

对于性能敏感的应用,将 OPFS 操作放在 Web Worker 中:

// main.js
const worker = new Worker('fs-worker.js');
worker.postMessage({ type: 'write', data: largeBuffer });

// fs-worker.js
self.onmessage = async (e) => {
  if (e.data.type === 'write') {
    const handle = await opfsRoot.getFileHandle('data', { create: true });
    const accessHandle = await handle.createSyncAccessHandle();
    accessHandle.write(e.data.data);
    accessHandle.close();
  }
};

5. 调试 OPFS

Chrome 提供了 OPFS Explorer 扩展,可以在 DevTools 中查看 OPFS 内容。对于开发调试非常有帮助。

技术演进的方向

File System Access API 还在持续演进中。最近的进展包括:

持久权限(Chrome 122+):用户可以选择「始终允许」,避免每次会话都重新授权。这需要网站被安装为 PWA。

move() 方法:文件和目录的移动/重命名功能正在逐步实现。目前在 OPFS 中已可用。

File Handling API 增强:允许 PWA 更深度地集成到操作系统的文件处理流程中。

与此同时,标准化进程也在推进。File System API 已经成为 WHATWG 的 Living Standard,这意味着它不再是某个浏览器的私有功能,而是 Web 平台的正式组成部分。

结语

File System Access API 代表了 Web 平台演进的一个关键节点。它试图在两个看似矛盾的目标之间找到平衡:让 Web 应用拥有与原生应用媲美的能力,同时保持 Web 安全模型的完整性。

Chrome 团队选择了一条「用户选择」的路径:通过透明的授权流程和可见的权限指示,让用户决定信任哪些应用。Mozilla 则坚持更保守的立场,认为这种能力会带来不可接受的风险。

这种分歧是健康的。Web 标准的形成从来不是一帆风顺的,需要不同观点的碰撞。而 OPFS 作为双方都能接受的中间地带,正在成为现代 Web 应用的关键基础设施——从 SQLite-WASM 到 RxDB,从图像处理到数据科学,OPFS 正在重新定义浏览器可以承载的工作负载类型。

对于开发者而言,File System Access API 提供了一个选择。不是每个应用都需要访问本地文件系统,但对于那些需要的应用——代码编辑器、图像处理工具、文档编辑器——这项 API 打开了通往「Web 即原生」的大门。关键是理解它的边界,准备好回退方案,并尊重用户的选择权。


参考资料

  1. File System Access API - W3C WICG
  2. File System Standard - WHATWG
  3. File System API - MDN Web Docs
  4. The File System Access API: simplifying access to local files - Chrome Developers
  5. The origin private file system - web.dev
  6. Mozilla Standards Positions: File System Access API
  7. LocalStorage vs. IndexedDB vs. OPFS - RxDB
  8. How Photopea uses the File Handling API - Chrome Developers
  9. The browser-fs-access library - Excalidraw
  10. The File System API with OPFS - WebKit Blog
  11. Persistent permissions for the File System Access API - Chrome Developers
  12. Can I use: Native File System API