文件系统访问 API 是一个 Web API,允许读取和写入用户本地文件。它开启了构建强大 Web 应用程序的新功能,例如文本编辑器或 IDE、图像编辑工具、改进的导入/导出,所有这些都在前端进行。让我们了解一下如何开始使用此 API。

使用文件系统访问 API 读取文件
在深入研究从用户系统读取文件所需的代码之前,请记住一个重要细节,即 **调用文件系统访问 API 必须由用户手势在安全上下文中执行**。在以下示例中,我们将使用单击事件。
从单个文件读取
从文件读取数据可以在不到 10 行代码中完成。这是一个示例代码片段
let fileHandle;
document.querySelector(".pick-file").onclick = async () => {
[fileHandle] = await window.showOpenFilePicker();
const file = await fileHandle.getFile();
const content = await file.text();
return content;
};
假设我们在 HTML 中有一个带有类 .pick-file
的按钮。单击此按钮时,我们通过调用 window.showOpenFilePicker()
启动文件选择器,并将此查询的结果存储在一个名为 fileHandle
的变量中。
调用 showOpenFilePicker()
返回的是一个 FileSystemFileHandle
对象数组,表示我们选择的每个文件。由于此示例用于单个文件,因此我们解构结果。我稍后将展示如何选择多个文件。
这些对象包含一个 kind
和 name
属性。如果您要使用 console.log(fileHandle)
,您将看到以下对象
FileSystemFileHandle {kind: 'file', name: 'data.txt'}
kind
可以是 file
或 directory
。
然后,我们可以在 fileHandle
上调用 getFile()
方法以获取有关我们文件的信息。调用此方法会返回一个包含一些属性的对象,包括文件上次修改的时间戳、文件名、大小和类型。
最后,我们可以对文件调用 text()
以获取其内容。
从多个文件读取
要从多个文件读取,我们需要将一个 options
对象传递给 showOpenFilePicker()
。
例如
let fileHandles;
const options = {
multiple: true,
};
document.querySelector(".pick-file").onclick = async () => {
fileHandles = await window.showOpenFilePicker(options);
// The rest of the code will be shown below
};
默认情况下,multiple
属性设置为 false
。其他选项可用于指示可以选择的文件类型。
例如,如果我们只想接受 .jpeg
文件,则 options
对象将包含以下内容
const options = {
types: [
{
description: "Images",
accept: {
"image/jpeg": ".jpeg",
},
},
],
excludeAcceptAllOption: true,
};
在此示例中,fileHandles
是一个包含多个文件的数组,因此获取其内容将按以下方式完成
let fileHandles;
const options = {
multiple: true,
};
document.querySelector(".pick-file").onclick = async () => {
fileHandles = await window.showOpenFilePicker(options);
const allContent = await Promise.all(
fileHandles.map(async (fileHandle) => {
const file = await fileHandle.getFile();
const content = await file.text();
return content;
})
);
console.log(allContent);
};
使用文件系统访问 API 写入文件
文件系统访问 API 还允许您将内容写入文件。首先,让我们了解一下如何保存新文件。
写入新文件
写入新文件也可以用非常少的代码完成!
document.querySelector(".save-file").onclick = async () => {
const options = {
types: [
{
description: "Test files",
accept: {
"text/plain": [".txt"],
},
},
],
};
const handle = await window.showSaveFilePicker(options);
const writable = await handle.createWritable();
await writable.write("Hello World");
await writable.close();
return handle;
};
如果我们想象一个带有类 save-file
的第二个按钮,单击时,我们将使用 showSaveFilePicker()
方法打开文件选择器,并传入一个包含要保存的文件类型的 option
对象,这里是一个 .txt
文件。
调用此方法也将像第一部分一样返回一个 FileSystemFileHandle
对象。在此对象上,我们可以调用 createWritable()
方法,该方法将返回一个 FileSystemWritableFileStream
对象。然后,我们可以使用 write()
方法将一些内容写入此流,在其中我们需要传递内容。
最后,我们需要调用 close()
方法关闭文件并完成将内容写入磁盘。
例如,如果您想将一些 HTML 代码写入文件,则只需要更改 options
对象中的内容以接受 "text/html": [".html"]
并将一些 HTML 内容传递给 write()
方法即可。
编辑现有文件
如果您想导入文件并使用文件系统访问 API 编辑它,则示例代码如下所示
let fileHandle;
document.querySelector(".pick-file").onclick = async () => {
[fileHandle] = await window.showOpenFilePicker();
const file = await fileHandle.getFile();
const writable = await fileHandle.createWritable();
await writable.write("This is a new line");
await writable.close();
};
如果您一直在关注本文的其余部分,您可能会认识到我们从 showOpenFilePicker()
和 getFile()
方法开始读取文件,然后使用 createWritable()
、write()
和 close()
写入同一文件。
如果您要导入的文件已经包含内容,则此代码示例将用传递给 write()
方法的新内容替换当前内容。
其他文件系统访问 API 功能
在不赘述细节的情况下,文件系统访问 API 还允许您列出目录中的文件以及删除文件或目录。
读取目录
读取目录可以使用少量代码完成
document.querySelector(".read-dir").onclick = async () => {
const directoryHandle = await window.showDirectoryPicker();
for await (const entry of directoryHandle.values()) {
console.log(entry.kind, entry.name);
}
};
如果我们添加一个带有类 .read-dir
的新按钮,单击时,调用 showDirectoryPicker()
方法将打开文件选择器,并且当您在计算机上选择一个目录时,此代码将列出在该目录中找到的文件。
删除文件
可以使用以下代码示例删除目录中的文件
document.querySelector(".pick-file").onclick = async () => {
const [fileHandle] = await window.showOpenFilePicker();
await fileHandle.remove();
};
如果您想删除文件夹,则只需要对上面的代码示例进行少量更改
document.querySelector(".read-dir").onclick = async () => {
const directoryHandle = await window.showDirectoryPicker();
await directoryHandle.remove();
};
最后,如果您想在选择文件夹时删除特定文件,可以这样编写
// Delete a single file named data.txt in the selected folder
document.querySelector(".pick-folder").onclick = async () => {
const directoryHandle = await window.showDirectoryPicker();
await directoryHandle.removeEntry("data.txt");
};
如果您想删除整个文件夹,则需要以下几行代码
// Recursively delete the folder named "data"
document.querySelector(".pick-folder").onclick = async () => {
const directoryHandle = await window.showDirectoryPicker();
await directoryHandle.removeEntry('data', { recursive: true });
};
文件系统访问 API 浏览器支持
目前,IE 和 Firefox 似乎不支持文件系统访问 API。但是,存在一个名为 browser-fs-access 的 ponyfill。
此浏览器支持数据来自 Caniuse,其中包含更多详细信息。数字表示浏览器在该版本及更高版本中支持该功能。
桌面
Chrome | Firefox | IE | Edge | Safari |
---|---|---|---|---|
130 | 132 | 不支持 | 127 | 试验 |
移动/平板电脑
Android Chrome | Android Firefox | Android | iOS Safari |
---|---|---|---|
127 | 不支持 | 127 | 18.0 |
总结
如果您想尝试文件系统访问 API,请查看由 Google 工程师构建的此实时演示文本编辑器。否则,如果您想了解有关此 API 及其所有功能的更多信息,以下是一些资源
- 文件系统访问 API (W3C 规范)
- 文件系统访问 API (MDN)
- 对比度范围、replaceAll 方法、原生文件系统 API (Šime Vidas)
- 文件系统访问 API:简化对本地文件的访问 (web.dev)
- 使用 browser-fs-access 库读取和写入文件和目录 (web.dev)
- browser-fs-access 仓库 (GitHub)
这非常酷。我期待着看到它成熟!
什么被认为是有效的“用户手势”?我想它必须在某个事件的事件处理程序中(是否有列表?),但仅此而已吗?它必须立即在处理程序中发生吗?必须是同步的吗?
虽然我喜欢这个 API——我甚至在我的这个项目中使用它——但我不喜欢它不稳定的状态:例如,
.delete()
方法相当新,并且在许多 Chromium 分支和派生浏览器(如 Ungoogled Chromium)中不受支持。如果要支持除官方 Chrome 和 Chromium 之外的浏览器,目前删除文件的支持度最高的方法是使用.removeEntry()
。同样的事情也适用于
.move()
:即使它是一个非常有用的功能,它也在规范中添加得比较晚,大多数关于此 API 的页面都没有任何关于它不受良好支持的说明。此外,有一段时间存在.rename()
,但.move()
现在执行相同的功能,并且.rename()
已被移除。由于它的不稳定性,也就不足为奇了,只有 Chromium 实现了它:除了潜在的安全问题之外,其他浏览器如何在 Chrome/Chromium 稳定版中实现这些更改?
我将安全问题放在一边,因为其他浏览器如何改进安全性?Safari 没有自动更新,Firefox 在最近的版本中删除了确认下载的提示(即使出于安全目的曾经有过延迟!)。
对我来说,虽然此 API 使某些安全问题更容易解决(例如,曾经有一种方法可以使用户下载 .lnk 文件,这些文件可以链接到恶意软件,而用户不知道这些文件很危险,使它们看起来像图像文件),但随着时间的推移,这些问题将得到解决,并且好处将超过微不足道的潜在缺点。
另一方面,为什么他们在发布之前不能使这个 API 稳定?为什么他们要创建一个
.rename()
方法并在 Chrome 中发布它,然后在下一个版本中将其移除?为什么他们最近才添加了.delete()
?为什么要重复 DOM 存在的问题?当然,执行el.remove()
比执行el.parentElement.removeChild(el)
更容易,那么.delete()
应该早点出现,而不是通过使.removeEntry()
像.removeChild()
一样工作来重复文件系统 API 中的相同问题。也许,由于潜在的安全问题,只有 Chromium 贡献者希望帮助编写此 API,并且它变得混乱,我不知道。好吧,即使在 Chromium 社区中,它也存在争议:Brave 已经将其移除。顺便说一句,我发现的其他问题:与
<a download>
不同,您不能在没有提示用户的情况下将其下载到下载文件夹,与<a download>
不同;它不会自动清理文件名,而是如果您使用无效名称,它将抛出一个模糊的错误(并且为此使用 npm 库不起作用,因为——我尝试了大多数库——它们不会清理 Chromium 认为不安全的全部字符,例如,由于某些奇怪的原因,~
),当然,错误没有指定哪些字符不允许使用,因此您需要猜测;最后,它在文件路径中没有文件系统限制,如果您尝试写入一个文件,该文件将导致文件路径大于底层文件系统接受的文件路径,它将拒绝并抛出一个模糊的错误,直到我猜出是什么导致它,它才导致一些头痛。另一方面,正如您从我上面链接的项目中看到的那样,它有自己的用途,功能非常强大,有用并且是一个很棒的 API。它支持流文件读取和写入而无需使用 Service Worker hack 的事实非常棒:如果没有它,我的项目需要将文件放入内存,这将使该项目无法处理大型文件(如视频)。我相信它在未来会成为一个很棒且流行的 API,当它变得更加稳定时。
我认为这些简化的代码示例对读者不利。它推广了不检查错误或空返回值的代码。错误处理是使用 API 的重要部分。至少应该提到它。
不知道你需要 ssl 才能运行代码
这如何与包文件(例如 textbundle 文件)或压缩档案(例如 .zip 档案)一起使用?