手机上编辑文件只能下载副本?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 表,得把它当增强能力来用。
二、到底能解决什么痛点
老方案的三个死结:
- 上传是只读的:
<input type="file">给你一个 Blob,你能读,但写不回去。想保存只能再下载一份。 - 下载 = 副本爆炸:用户改 3 次,Download 里躺 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 那一套。做兼容设计时,落点要分清。
六、给你一份可落地的接入顺序
不要一上来就全量替换,按这个顺序推最稳:
-
先做能力探测,别做 UA 判断。
const canPickFiles = 'showOpenFilePicker' in window;探测不到就走老路:
<input type="file">+ 下载。这是渐进增强,不是非黑即白。 -
把「上传 + 下载」的入口改成双轨。 支持 picker 的浏览器给「打开文件 / 保存」按钮,不支持的给「选择上传 / 下载副本」。共用一个内部数据处理函数,UI 层分叉。
-
每次写盘前检查权限。
if (await fileHandle.queryPermission({ mode: 'readwrite' }) !== 'granted') { if (await fileHandle.requestPermission({ mode: 'readwrite' }) !== 'granted') { return fallbackToDownload(content); // 优雅降级 } } -
记住 createWritable() 默认清空,需要追加/局部修改就传
{ keepExistingData: true }。 -
在大数据场景(本地数据库、离线缓存、大文件)优先用 OPFS + Worker,别硬塞进 IndexedDB 或 localStorage。
把这五步做完,你的 H5 工具在 Chrome 系设备上就真的有了「打开—编辑—原地保存」的闭环,而不再是「上传—下载—再上传」的折磨。
最后
这次变化的本质不是「浏览器变强了」,而是浏览器终于在文件这件事上,从「只读的门」变成了「可写的门把手」。代价是它是一个 Chromium 专属、明确不进 Baseline 的能力,苹果系完全不跟。所以真正的问题不是「要不要用」,而是你有没有一套能让它在缺失时安静降级的架构。
如果你的项目里有「用户反复编辑同一个文件」的场景——笔记、代码片段、表格、图片批处理——这个 API 值得从今天开始接入。先加一行能力探测,再慢慢把主路径搬过去,别等用户把 Download 文件夹堆满第 20 个副本才动手。
评论区
登录后可评论。