分类
js

使用 File System Access API 在浏览器里操作本地文件

如《Webpack 5 发布,Chrome 86 开始支持本地文件系统》一文所述,Chrome 86 开始,浏览器正式支持操作本地文件。接下来结合最近的使用,分享下用法。

0. 准备工作:理清概念

首先,我们要先搞清楚一些概念。实际上,让浏览器操作本地文件是开发者一直在努力并且不停在探索的方向,所以历史上有很多方案,存在很多类似但其实并不一样的 API,大家在学习的时候一定要搞清楚,不要弄混。

最早登场的是 File API,代表功能是 FileReader(参考:《使用 Promise 封装 FileReader》)。这个 API 最大的进步在于,我们可以在浏览器里读取和操作二进制文件,然后通过 <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 的继任者。

它的设计思路也不复杂:

  1. 要求用户手动选择文件或者目录,以获取文件或目录的控制权限
  2. 选择文件或目录后,获取到 FileHandle,后续的操作经由它来进行
  3. FileHandleserializable 对象,所以可以通过序列化和反序列化实现跨 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 支持,但还是建议大家尽快把它用起来。


参考链接: