灏天阁

File System Access API 让浏览器拥有操作本地文件的能力

· Yin灏

在早期我们经常听到这样的说法:浏览器是一个沙盒,它不允许我们操作本地文件,但是现在这个说法已经不再适用了,因为我们可以使用 File System Access API 来实现这个功能。

什么是 File System Access API

File System Access API 是一项 Web API,允许 Web 应用程序从用户设备的本地文件系统中读取和写入文件。

它提供了一种简单且安全的方法,让用户在不离开 Web 应用的情况下,从本地文件系统中操作文件。

这项 APIWeb 应用程序提供了更多的灵活性和功能,使其更接近于本地应用程序的体验。

File System Access API 遵循同源策略,只允许 Web 应用程序在具有相同源的文件系统上进行操作。

当用户使用该 API 时,会提示用户授权应用程序访问他们的文件系统。

如果用户授权,则应用程序可以使用该 API 访问用户选择的文件和目录。

使用 File System Access API 可以访问本地文件系统,从而实现一些有用的功能,例如:

  • 将文件从本地文件系统上传到 Web 应用程序;
  • Web 应用程序中的数据写入到本地文件系统中;
  • 在用户的本地文件系统上创建、重命名和删除文件;
  • 读取本地文件系统上的文件内容。

如何使用 File System Access API

我不是很喜欢概念性的东西,上面的内容是网上借鉴的(文化人),我更喜欢直接上代码,所以我们直接上代码。

选择文件

首先我们来看看如何选择文件,这个功能是 File System Access API 中最基础的功能,我们可以通过 showOpenFilePicker 方法来实现。

const fileHandle = await window.showOpenFilePicker();
console.log(fileHandle);

可以看到我们这里使用了async/await语法,这是因为showOpenFilePicker异步方法,它会返回一个Promise对象,我们可以通过await来等待它的结果。

showOpenFilePicker方法会返回一个FileHandle对象,我们可以通过它来获取文件的信息。

我们来看看它最后返回的结果:

image.png

可以看到的是最后的结果是一个数组,这是因为我们可以选择多个文件;

而这个数组的每一项都是一个FileSystemFileHandle对象,我们可以通过它来获取和操作文件。

FileSystemFileHandle

FileSystemFileHandle对象是一个代表文件的对象,它提供了一些方法来获取和操作文件。

FileSystemFileHandle提供了一些方法来获取和操作文件,例如:

  • getFile:返回一个Promise对象,用于获取文件;
  • createSyncAccessHandle:返回一个FileSystemSyncAccessHandle对象,用于同步访问文件;
  • createWritable:返回一个Promise对象,用于创建一个可写流,用于写入文件;

我们来看看如何使用getFile方法来获取文件。

const fileHandle = await window.showOpenFilePicker();
const file = await fileHandle[0].getFile();
console.log(file);

image.png

可以看到,我们通过getFile方法获取到了文件,它返回的是一个File对象,我们可以通过它来获取文件的信息。

都拿到File对象了,后面怎么操作就很熟悉了吧,直接使用FileReader对象来获取文件内容,后面你爱怎么操作就怎么操作。

FileSystemHandle

通过截图我们还看到了有kind属性和name属性,这两个属性是继承自FileSystemHandle对象的。

FileSystemFileHandle继承自FileSystemHandle,它是一个代表文件系统中的文件或目录的对象。

FileSystemHandle提供了一些方法来获取和操作文件系统中的文件或目录,例如:

  • kind:返回一个字符串,用于表示文件或目录;
  • name:返回一个字符串,用于表示文件或目录的名称;
  • isSameEntry:返回一个boolean值,用于表示两个文件或目录是否相同;
  • queryPermission:返回一个Promise对象,用于查询文件或目录的权限;
  • requestPermission:返回一个Promise对象,用于请求文件或目录的权限;
  • remove:返回一个Promise对象,用于删除文件或目录;

我们可以通过kind属性来判断当前的FileSystemHandle对象是文件还是目录。

const fileHandle = await window.showOpenFilePicker();
const file = await fileHandle[0].getFile();
console.log(fileHandle[0].kind);

当然我们使用的是showOpenFilePicker方法,所以它返回的肯定是文件,所以还有一个showDirectoryPicker方法,它可以用来选择目录。

选择目录

选择目录的方法和选择文件的方法是一样的,只是我们需要使用showDirectoryPicker方法。

const directoryHandle = await window.showDirectoryPicker();
console.log(directoryHandle);

image.png

可以看到,我们通过showDirectoryPicker方法获取到了目录,它返回的是一个FileSystemDirectoryHandle对象,我们可以通过它来获取和操作目录。

使用showDirectoryPicker方法时,浏览器会提示用户授权应用程序访问他们的文件系统,请不要拒绝哟。

FileSystemDirectoryHandle

FileSystemDirectoryHandle对象是一个代表文件系统中的目录的对象,它提供了一些方法来获取和操作目录。

FileSystemDirectoryHandle提供的方法就比较多了,例如:

  • entries:返回一个AsyncIterable对象,用于获取目录中的所有文件和目录;
  • keys:返回一个AsyncIterable对象,用于获取目录中的所有文件和目录的名称;
  • values:返回一个AsyncIterable对象,用于获取目录中的所有文件和目录的FileSystemHandle对象;
  • getFileHandle:返回一个Promise对象,用于获取目录中的文件;
  • getDirectoryHandle:返回一个Promise对象,用于获取目录中的目录;
  • removeEntry:返回一个Promise对象,用于删除目录中的文件或目录;
  • resolve:返回一个Promise对象,用于获取目录中的文件或目录;

entrieskeysvalues这三个方法都是用来获取目录中的所有文件和目录的,它们返回的都是一个AsyncIterable对象,我们可以通过for await...of语法来遍历它。

const directoryHandle = await window.showDirectoryPicker();
for await (const [name, handle] of directoryHandle.entries()) {
  if (handle.kind === "file") {
    console.log(name, "file");
  } else {
    console.log(name, "directory");
  }
}

我们可以通过handle.kind来判断当前的FileSystemHandle对象是文件还是目录。

而这里的getFileHandlegetDirectoryHandle就是用来获取目录中的文件和目录的,它们都返回一个Promise对象,我们可以通过await来获取它们。

const directoryHandle = await window.showDirectoryPicker();
for await (const [name, handle] of directoryHandle.entries()) {
  if (handle.kind === "file") {
    const fileHandle = await directoryHandle.getFileHandle(name);
    console.log(fileHandle);
  } else {
    const directoryHandle = await directoryHandle.getDirectoryHandle(name);
    console.log(directoryHandle);
  }
}

这里大家可以自己尝试一下,我就不截图了。

操作文件

上面我们了解到了如何获取文件和目录,那么我们接下来就来看看如何操作文件和目录。

读取文件

读取文件做过文件上传的同学应该都很熟悉了,我们可以使用FileReader对象来读取文件。

const fileHandle = await window.showOpenFilePicker({
  excludeAcceptAllOption: false,
  types: [
    {
      description: "Text files",
      accept: {
        "text/plain": [".txt"],
      },
    },
  ],
});
const file = await fileHandle[0].getFile();
const reader = new FileReader();
reader.onload = () => {
  console.log(reader.result);
};
reader.readAsText(file);

这里我们在使用showOpenFilePicker方法时,我们通过types属性来限制文件的类型,这样用户就只能选择文本文件了。

showOpenFilePicker还有其他的属性,例如:

  • multiple:一个boolean值,默认为false,是否允许用户选择多个文件;
  • excludeAcceptAllOption:一个boolean值,默认为false,是否允许用户选择所有类型的文件(就是选择文件下拉的所有文件选项);
  • types:一个数组,用于限制用户选择的文件类型(就是选择文件的下拉选项);
    • description:一个字符串,用于描述文件类型(就是下拉选项的文字);
    • accept:一个对象,用于描述文件类型(就是控制选择文件的类型,例如image/*表示图片类型),具体可以参考:MIME types

写入文件

写入文件可以使用上面提到的FileSystemFileHandle对象的createWritable方法来创建一个FileSystemWritableFileStream对象,然后通过它来写入文件。

const fileHandle = await window.showSaveFilePicker({
  types: [
    {
      description: "Text files",
      accept: {
        "text/plain": [".txt"],
      },
    },
  ],
});

// 创建一个可写流
const writable = await fileHandle.createWritable();

// 写入数据
await writable.write("Hello World!");

// 关闭流
await writable.close();

这里我们使用showSaveFilePicker方法来创建一个文件,然后通过createWritable方法来创建一个可写流,然后通过write方法来写入数据,最后通过close方法来关闭流。

showSaveFilePicker也是文件选择器的一种,它和showOpenFilePicker的区别在于,showSaveFilePicker是用来创建文件的,而showOpenFilePicker是用来选择文件的。

showSaveFilePicker返回的是新创建的文件的FileSystemFileHandle对象,而showOpenFilePicker返回的是选择的文件的FileSystemFileHandle对象。

注意:操作文件流时,一定要记得关闭流哟,否则会导致文件锁定,无法进行其他操作,做前端的同学可能对这一块并不熟悉,所以特此提醒一下。

操作目录

上面我们已经知道了如何操作文件了,那么接下来我们就来看看如何操作目录。

创建目录

创建目录可以使用FileSystemDirectoryHandle对象的getDirectoryHandle方法来创建一个目录。

const directoryHandle = await window.showDirectoryPicker();
const newDirectoryHandle = await directoryHandle.getDirectoryHandle(
  "new-directory",
  {
    create: true,
  }
);

getDirectoryHandle方法接收两个参数:

  • name:一个字符串,用于指定目录的名称;
  • options:一个对象,用于指定目录的选项(可选);
    • create:一个boolean值,默认为false,是否创建目录;

目前只有create一个选项,如果设置为true,则会创建一个目录,如果设置为false,则会获取一个目录。

如果目录不存在,且createfalse,则会报错。

删除目录

删除目录可以使用FileSystemDirectoryHandle对象的removeEntry方法来删除一个目录。

const directoryHandle = await window.showDirectoryPicker();
await directoryHandle.removeEntry("new-directory");

removeEntry方法接收一个参数,一个字符串,用于指定要删除的目录的名称。

兼容性

截止到现在,showDirectoryPickershowOpenFilePicker这两个方法在Chrome 86版本中已经可以正常使用了,但是在Firefox中还不支持。

下面是来自caniuse的兼容性数据:

image.png

虽然Firefox还不支持,但是在一些实验性的项目上我们可以使用这些API,指定用户使用Chrome浏览器来访问。

总结

本文主要介绍了File System Access API的基本使用,包括如何获取文件和目录,以及如何操作文件和目录。

同时因为有这个API有跨域的问题,所以这次就没办法给大家演示码上掘金的代码了,感兴趣的同学可以将我文中的代码动手尝试一下。

可以发现我这次讲解的并不是很详细,因为这个API还在实验阶段,所以我只是简单的介绍了一下,如果大家想要了解更多的话,可以参考下面的参考资料。

参考资料

- Book Lists -