如《Webpack 5 发布,Chrome 86 开始支持本地文件系统》一文所述,Chrome 86 开始,浏览器正式支持操作本地文件。接下来结合最近的使用,分享下用法。
0. 准备工作:理清概念
首先,我们要先搞清楚一些概念。实际上,让浏览器操作本地文件是开发者一直在努力并且不停在探索的方向,所以历史上有很多方案,存在很多名称接近但其实并不一样的 API,大家在学习的时候一定要搞清楚,不要弄混。
最早登场的是 File API,代表功能是 FileReader(参考:《使用 Promise 封装 FileReader》)。这个 API 最大的进步在于,我们可以在浏览器里读取文件,然后操作二进制数据;最后还可以构建内存 URL,并通过 <a download="file.{ext}">
下载到本地。如此,浏览器可以操作几乎所有二进制数据,作为工具平台的价值大大提高。
接下来,激进的 Google Chrome 提出并实现了 File System API。这个 API 试图在浏览器里创建一个独立的文件环境,让开发者可以在里面任意操作文件和目录。如果能做好,这会是一个非常好的实现,可以大大提升浏览器作为系统核心的地位。可惜步子不仅大、而且偏,最终失败。我总结原因有二:
- 一方面,“独立的文件环境”,即无法操作系统本地文件,其实没什么价值,反而增加了用户理解和使用时的心智负担。
- 另外,当时浏览器的其它限制没有突破——没有包管理、没有 babel、甚至没有
Promise
,IE 仍然大量存在,开发难度极大。
所以最终这套方案死得悄无声息。我曾经的博客《HTML5的File API应用》,可能是为数不多的中文分享。
接下来是 Chrome Extension、Chrome App、Chrome OS 里的 File (System) API。这几个产品都是 Google 私有,不用考虑其他浏览器厂商,所以可以放开手脚随便搞。这里大家需要注意的是,因为 Google 的产品策略一向是说关就关,所以大家要留心常看文档,别学了一半 API 没了,比如:Extension 的 chrome.fileSystem
就已经弃用了。
最后,也就是今天的主角,File System Access API。这套方案应该是未来的主角。它提供了比较稳妥的本地文件交互模式,即保证了实用价值,又保障了用户的数据安全,明显是前辈 File System API 的继任者。
它的设计思路也不复杂:
- 要求用户手动选择文件或者目录,以获取文件或目录的控制权限
- 选择文件或目录后,获取到
FileHandle
,后续的操作经由它来进行 FileHandle
是serializable
对象,所以可以通过序列化和反序列化实现跨 session 的存储(即刷新页面后还能用)
好,下面看代码。
1. 读取本地文件
这段代码可以比较完整的演示 window.showOpenFilePicker
API 的用法:
// 使用 `try...catch` 可以捕获用户取消选择时抛出的错误,如果你对错误不在意,不捕获也行
try {
const [handle] = await showOpenFilePicker({
multiple: false, // 只选择一个文件
types: [
{
description: 'Navlang Files',
accept: {
'text/x-navlang': '.nav',
},
},
],
excludeAcceptAllOption: true,
});
} catch (e) {
if (e.message.indexOf('The user aborted a request') === -1) {
console.error(e);
return;
}
}
// 如果没有选择文件,就不需要继续执行了
if (!handle) {
return;
}
// 这里的 options 用来声明对文件的权限,能否写入
const options = {
writable: true,
mode: 'readwrite',
};
// 然后向用户要求权限
if ((await handle.queryPermission(options)) !== 'granted'
&& (await handle.requestPermission(options)) !== 'granted') {
alert('Please grant permissions to read & write this file.');
return;
}
// 前面获取的是 FileHandle,需要转换 File 才能用
const file = await handle.getFile();
// 接下来,`file` 就是普通 File 实例,你想怎么处理都可以,比如,获取文本内容
const code = await file.text();
2. 保存本地文件
前面说过,FileHandle 可以序列化,也即可以进行持久化存储。所以我们只需要把对应的 FileHandle 存下来,然后保存即可。
if (data.file) {
const writable = await data.file.createWritable();
await writable.write(data.code);
await writable.close();
}
如果之前没有获取过 FileHandle,则可以通过 window.showSaveFilePicker
来获取:
try {
const file = await showSaveFilePicker(filePickerOptions);
} catch (e) {
if (e.message.indexOf('The user aborted a request.') === -1) {
console.error(e);
}
return;
}
// 然后接前面的代码
const writable = await file.createWritable();
await writable.write(data.code);
await writable.close();
这个功能现在有一点小问题,不知道是不是 Chrome 实现不太稳定,如果你打开开发者工具,然后钩上“Pause on caught exceptions”,那么保存时会暂停数次,并提示错误。不用理会,直接继续执行即可。我猜测这个过程本来应该由浏览器自动捕获并重试,直到超时保护或者写入成功,但是现在会错误地抛出来。
3. 总结
File System Access API 不仅可以操作文件,还可以操作目录,操作目录的方式和文件相仿,我就不详细举例了,大家可以看下后面的参考链接,或者等我用到目录、踩了坑再来分享。
这个 API 对前端来说意义不小。有了这个功能,Web 可以提供更完整的功能链路,从打开、到编辑、到保存,一套到底。虽然目前只有 Chrome 支持,但还是建议大家尽快把它用起来。
参考链接:
欢迎吐槽,共同进步