手机上编辑文件只能下载副本?Chrome 132 把本地文件句柄搬进了安卓浏览器

用户在手机浏览器里编辑完一份文件,想「保存」,你的 H5 只能把它重新下载一遍——文件堆在 Download 文件夹里,改了三次就有三个副本,根本没有「原地保存」这回事。今年开始这件事在安卓浏览器上彻底变了:Chrome 132 把 File System Access API 的 showOpenFilePicker / showSaveFilePicker 搬上了 Android 和 WebView,网页第一次拿到了真正的本地文件句柄,能读、能改、能写回同一个文件。

先把结论摆出来:如果你在 2026 年做 PWA 或 H5 工具类应用(编辑器、笔记、图片处理、表格),文件读写这件事不用再走「上传 + 下载」这条老路了。 但「能用」和「正确用」中间隔着四道坎,这篇文章一次讲清。

一、先纠正一个流传很广的错误认知

你在网上搜「File System Access API 支持安卓吗」,大概率会看到一句话:「不支持,安卓网页应用要用 TWA 包一层或者退回 OPFS。」这个结论在 2025 年 1 月之前是对的,但现在已经过期了。

实际情况按时间线是这样的:

  • Chrome 86(2020-10):桌面端首发 showOpenFilePicker / showDirectoryPicker / showSaveFilePicker。
  • Chrome 109(2023-01):安卓端先上了 OPFS 那部分(private file system + FileSystemSyncAccessHandle),但不含 picker。
  • Chrome 132(2025-01):picker 三件套正式在 Android 和 WebView 上线,安卓网页第一次能弹系统文件选择器、拿真实文件句柄。
  • Chrome 121:FileSystemFileHandle.createWritable() 加了 mode 参数(exclusive / siloed),createSyncAccessHandle() 加了 readwrite-unsafe。

所以到 2026 年,安卓 Chrome / WebView / 三星浏览器基本都能用了,唯一还缺的是 Safari(桌面 + iOS 都是反对立场,理由是安全)和 Firefox(未实现,立场也是 negative)。这意味着:Chrome 系全通,苹果系全不通。

⚠️ 划重点:这不是一种「所有浏览器都支持」的 Baseline 能力,而是一块「Chromium 专属 + 明确不会被基线化」的地盘。 Web Platform 的官方描述写得很直接——「已经收到至少一个浏览器厂商的负面标准立场,在当前形态下几乎不可能进入 Baseline」。所以别指望它进 Baseline 表,得把它当增强能力来用。

二、到底能解决什么痛点

老方案的三个死结:

  1. 上传是只读的:<input type="file"> 给你一个 Blob,你能读,但写不回去。想保存只能再下载一份。
  2. 下载 = 副本爆炸:用户改 3 次,Download 里躺 3 个同名文件,手机上根本分不清哪个是最新的。
  3. 目录不可选:想批量处理一个文件夹里的图片,网页连入口都没有。

File System Access 的解法是把「句柄」交给网页:

// 打开:弹系统选择器,拿句柄(必须有用户手势触发)
const [fileHandle] = await window.showOpenFilePicker({
  types: [{ description: 'Markdown', accept: { 'text/markdown': ['.md'] } }],
});
const file = await fileHandle.getFile();   // 拿到 File(Blob 子类)
const text = await file.text();            // 读内容

// 保存:写回原文件,不是下载副本
const writable = await fileHandle.createWritable();
await writable.write(newText);
await writable.close();   // close 才真正落盘

关键区别在于 fileHandle 是持久的——同一个会话里再点一次保存,不需要重新弹选择器,直接往原来那个文件写。这就是原生应用「Ctrl+S」的感觉。

还有一个隐藏能力很多人不知道:createWritable() 默认会清空原文件。hivebook 在 Chromium 141 上实测过,一个内容是 “ABCDEFGHIJ” 的文件,调用 createWritable() 再 write(“xy”) 之后,剩下的是 “xy” 而不是 “xyCDEFGHIJ”。要保留原内容得显式传 { keepExistingData: true }。这个坑不踩一次根本不知道。

三、安卓端特有的四道坎

别以为桌面能跑就能原样搬到安卓,下面这四点只有真机才会暴露。

1. Scoped Storage 限制了保存位置。 Android 11+ 的分区存储策略下,showSaveFilePicker 基本只能写进 Downloads 目录,用户没法随便选外置 SD 卡或别的目录。桌面端那种「另存为到任意文件夹」的体验,安卓上是不完整的。设计保存流程时要把这个假设去掉。

2. Incognito 默认被锁。 Chrome 147 起,File System Access 被纳入了隐身模式的「高敏感权限」清单(和 WebUSB、WebHID 同级),无痕窗口下默认禁用,未授权时 showOpenFilePicker 直接抛 NotAllowedError。开发者调试需要自己去 chrome://flags/#file-system-access-api-incognito 手动开。移动端这个 flag 藏得更深,要在地址栏输入 chrome://flags 再搜 “filesystem incognito”。

3. 一定要 HTTPS。 picker 方法在规范里标记为 SecureContext,纯 HTTP 页面上 window.showOpenFilePicker 根本不存在。本地开发用 localhost 没问题,但测试机连内网 IP 调试时就会踩。

4. 权限会过期,要处理拒绝路径。 句柄不是永久授权。用户撤销权限、标签关闭、无痕退出之后句柄即失效,写入前必须用 fileHandle.queryPermission() / requestPermission() 检查,并在用户拒绝时给出优雅降级(回退到下载方案)。

四、OPFS:另一条容易被混淆的路

很多人把 File System Access 和 OPFS 当同一件事,其实目标完全不同:

  • File System Access 的 picker:面向用户可见的真实文件,用户选择、用户授权、用户能看见。
  • OPFS(Origin Private File System):面向网页私有沙箱,用户看不见,也不该看见,作用是高性能存储。

OPFS 在安卓的落地比 picker 更早(Chrome 109)。它的杀手锏是 createSyncAccessHandle()——同步读写,没有每次调用的 Promise 开销,快到能直接在浏览器里跑一个编译成 WASM 的 SQLite。但有个硬约束:它只能在 dedicated Web Worker 里用,规范和 Chromium 实测都确认了——主线程里没有、SharedWorker 里没有、ServiceWorker 里也没有(IDL 写的是 [Exposed=DedicatedWorker])。

OPFS 还有两个容易翻车的点:

  • 文件不会出现在 DevTools 里,Chrome 的 Application 面板根本不列 OPFS 文件,得装 OPFS Explorer 扩展才能看,而且这扩展时灵时不灵。存了调试不出来,很折磨人。
  • 多标签页写同一文件会冲突,没有内置的跨标签协调机制,得自己上 Web Locks API 或 BroadcastChannel。默认的 sync access handle 是独占锁,第二个 handle 直接抛错(Chrome 121 才有 readwrite-unsafe 放宽,且只有 Chrome 有)。

一句话选型:要让用户打开/编辑/保存他自己的文件 → File System Access picker;要给自己存大数据(数据库、缓存、媒体)→ OPFS。

五、还有一个藏在 manifest 里的杀手能力

如果你的目标是让 PWA 看起来像个原生应用,那 File Handling API 值得单独说。

在 manifest.json 里声明:

{
  "file_handlers": [
    {
      "action": "/open-file",
      "accept": { "text/markdown": [".md"] }
    }
  ]
}

装好 PWA 之后,用户在系统文件管理器里双击一个 .md 文件,操作系统会把这个 PWA 列进「用什么打开」的选项里,就像 Word 处理 .docx 那样。打开后通过 launchQueue 拿到文件:

if ('launchQueue' in window) {
  launchQueue.setConsumer(async (launchParams) => {
    for (const fileHandle of launchParams.files) {
      const file = await fileHandle.getFile();
      // fileHandle 是可写的,直接接着 File System Access 那套逻辑
    }
  });
}

但这里有个必须说清的落差:File Handling API 目前只支持 Chromium 桌面系统,不覆盖移动端。 也就是说「让 app 成为文件默认打开方式」这件事在安卓上还做不到。所以移动端实际能依赖的仍然只有 picker 那一套。做兼容设计时,落点要分清。

六、给你一份可落地的接入顺序

不要一上来就全量替换,按这个顺序推最稳:

  1. 先做能力探测,别做 UA 判断。

    const canPickFiles = 'showOpenFilePicker' in window;

    探测不到就走老路:<input type="file"> + 下载。这是渐进增强,不是非黑即白。

  2. 把「上传 + 下载」的入口改成双轨。 支持 picker 的浏览器给「打开文件 / 保存」按钮,不支持的给「选择上传 / 下载副本」。共用一个内部数据处理函数,UI 层分叉。

  3. 每次写盘前检查权限。

    if (await fileHandle.queryPermission({ mode: 'readwrite' }) !== 'granted') {
      if (await fileHandle.requestPermission({ mode: 'readwrite' }) !== 'granted') {
        return fallbackToDownload(content); // 优雅降级
      }
    }
  4. 记住 createWritable() 默认清空,需要追加/局部修改就传 { keepExistingData: true }。

  5. 在大数据场景(本地数据库、离线缓存、大文件)优先用 OPFS + Worker,别硬塞进 IndexedDB 或 localStorage。

把这五步做完,你的 H5 工具在 Chrome 系设备上就真的有了「打开—编辑—原地保存」的闭环,而不再是「上传—下载—再上传」的折磨。

最后

这次变化的本质不是「浏览器变强了」,而是浏览器终于在文件这件事上,从「只读的门」变成了「可写的门把手」。代价是它是一个 Chromium 专属、明确不进 Baseline 的能力,苹果系完全不跟。所以真正的问题不是「要不要用」,而是你有没有一套能让它在缺失时安静降级的架构。

如果你的项目里有「用户反复编辑同一个文件」的场景——笔记、代码片段、表格、图片批处理——这个 API 值得从今天开始接入。先加一行能力探测,再慢慢把主路径搬过去,别等用户把 Download 文件夹堆满第 20 个副本才动手。

评论区

0 条评论

登录后可评论。

阿跨·跨端开发 135 阅读