浏览器长期以来一直能够处理文件和目录。 File API 提供了一些功能,用于在 Web 应用中表示文件对象,以及以编程方式选择这些对象并访问其数据。 不过,一旦您仔细观察,就会发现闪闪发光的并不都是黄金。
处理文件的传统方式
打开文件
作为开发者,您可以通过 <input type="file">
元素打开和读取文件。在最简单的情况下,打开文件可能类似于以下代码示例。
input
对象会为您提供 FileList
,在下面的示例中,该对象仅包含一个 File
。File
是一种特定的 Blob
,可用于任何可以使用 Blob 的上下文。
const openFile = async () => {
return new Promise((resolve) => {
const input = document.createElement('input');
input.type = 'file';
input.addEventListener('change', () => {
resolve(input.files[0]);
});
input.click();
});
};
打开目录
对于打开文件夹(或目录),您可以设置 <input webkitdirectory>
属性。除此之外,其他一切都与上述相同。
尽管 webkitdirectory
的名称带有供应商前缀,但它不仅可在 Chromium 和 WebKit 浏览器中使用,还可在基于旧版 EdgeHTML 的 Edge 以及 Firefox 中使用。
保存(更确切地说:下载)文件
对于保存文件,传统上,您只能下载文件,这得益于 <a download>
属性。给定一个 Blob,您可以将锚点的 href
属性设置为一个 blob:
网址,该网址可通过 URL.createObjectURL()
方法获取。
const saveFile = async (blob) => {
const a = document.createElement('a');
a.download = 'my-file.txt';
a.href = URL.createObjectURL(blob);
a.addEventListener('click', (e) => {
setTimeout(() => URL.revokeObjectURL(a.href), 30 * 1000);
});
a.click();
};
问题
下载方法的巨大缺点是无法实现经典的打开→修改→保存流程,也就是说,无法覆盖原始文件。相反,每当您“保存”时,您最终都会在操作系统的默认“下载内容”文件夹中获得原始文件的新副本。
File System Access API
借助 File System Access API,打开和保存操作都变得简单得多。它还支持真正保存,也就是说,您不仅可以选择文件的保存位置,还可以覆盖现有文件。
打开文件
借助 File System Access API,只需调用一次 window.showOpenFilePicker()
方法即可打开文件。此调用会返回一个文件句柄,您可以通过 getFile()
方法从中获取实际的 File
。
const openFile = async () => {
try {
// Always returns an array.
const [handle] = await window.showOpenFilePicker();
return handle.getFile();
} catch (err) {
console.error(err.name, err.message);
}
};
打开目录
通过调用 window.showDirectoryPicker()
打开一个目录,使目录可在文件对话框中选择。
保存文件
保存文件同样非常简单。
从文件句柄中,您可以通过 createWritable()
创建可写入的流,然后通过调用该流的 write()
方法写入 Blob 数据,最后通过调用该流的 close()
方法关闭该流。
const saveFile = async (blob) => {
try {
const handle = await window.showSaveFilePicker({
types: [{
accept: {
// Omitted
},
}],
});
const writable = await handle.createWritable();
await writable.write(blob);
await writable.close();
return handle;
} catch (err) {
console.error(err.name, err.message);
}
};
隆重推出 browser-fs-access
尽管 File System Access API 非常出色,但尚未广泛提供。

因此,我认为 File System Access API 是一种渐进式增强功能。 因此,我希望在浏览器支持时使用它,否则使用传统方法;同时,绝不会因不必要地下载不受支持的 JavaScript 代码而惩罚用户。 browser-fs-access 库就是我针对这一挑战给出的答案。
设计理念
由于 File System Access API 未来仍可能会发生变化,因此 browser-fs-access API 不是根据它建模的。也就是说,该库不是 polyfill,而是 ponyfill。您可以(静态或动态)专门导入所需的功能,以尽可能缩小应用的大小。
可用的方法包括 fileOpen()
、directoryOpen()
和 fileSave()
,这些方法的名称都非常贴切。
在内部,该库会检测是否支持 File System Access API,然后导入相应的代码路径。
使用 browser-fs-access 库
这三种方法直观易用。
您可以指定应用接受的 mimeTypes
或文件 extensions
,并设置 multiple
标志以允许或禁止选择多个文件或目录。如需了解完整详情,请参阅 browser-fs-access API 文档。
以下代码示例展示了如何打开和保存图片文件。
// The imported methods will use the File
// System Access API or a fallback implementation.
import {
fileOpen,
directoryOpen,
fileSave,
} from 'https://unpkg.com/browser-fs-access';
(async () => {
// Open an image file.
const blob = await fileOpen({
mimeTypes: ['image/*'],
});
// Open multiple image files.
const blobs = await fileOpen({
mimeTypes: ['image/*'],
multiple: true,
});
// Open all files in a directory,
// recursively including subdirectories.
const blobsInDirectory = await directoryOpen({
recursive: true
});
// Save a file.
await fileSave(blob, {
fileName: 'Untitled.png',
});
})();
演示
您可以在 GitHub 上的演示中看到上述代码的实际效果。 其源代码同样可在该处获取。出于安全考虑,不允许跨源子框架显示文件选择器,因此无法将演示嵌入到本文中。
实际应用中的 browser-fs-access 库
在空闲时间,我会为一款名为 Excalidraw 的可安装 PWA 贡献一点力量。这款白板工具可让您轻松绘制具有手绘感的图表。 它具有完全的自适应性,可在各种设备上顺畅运行,从小型手机到大屏电脑,不一而足。 这意味着,无论各种平台是否支持 File System Access API,它都需要处理这些平台上的文件。因此,该功能非常适合使用 browser-fs-access 库。
例如,我可以在 iPhone 上开始绘制,将其保存(从技术上讲,由于 Safari 不支持 File System Access API,因此是下载)到 iPhone 的“下载内容”文件夹,在桌面设备上打开该文件(从手机转移后),修改该文件,然后用我的更改覆盖该文件,甚至将其另存为新文件。




实际代码示例
下面展示了 browser-fs-access 在 Excalidraw 中的实际使用示例。此摘录摘自 /src/data/json.ts
。特别值得注意的是,saveAsJSON()
方法如何将文件句柄或 null
传递给 browser-fs-access 的 fileSave()
方法,这会导致在提供句柄时覆盖文件,否则保存到新文件。
export const saveAsJSON = async (
elements: readonly ExcalidrawElement[],
appState: AppState,
fileHandle: any,
) => {
const serialized = serializeAsJSON(elements, appState);
const blob = new Blob([serialized], {
type: "application/json",
});
const name = `${appState.name}.excalidraw`;
(window as any).handle = await fileSave(
blob,
{
fileName: name,
description: "Excalidraw file",
extensions: ["excalidraw"],
},
fileHandle || null,
);
};
export const loadFromJSON = async () => {
const blob = await fileOpen({
description: "Excalidraw files",
extensions: ["json", "excalidraw"],
mimeTypes: ["application/json"],
});
return loadFromBlob(blob);
};
界面注意事项
无论是在 Excalidraw 中还是在您的应用中,界面都应适应浏览器的支持情况。如果支持 File System Access API (if ('showOpenFilePicker' in window) {}
),您可以显示另存为按钮,以及保存按钮。
以下屏幕截图显示了 iPhone 和 Chrome 桌面设备上 Excalidraw 的自适应主应用工具栏之间的区别。
请注意,在 iPhone 上,“另存为”按钮会消失。


总结
从技术上讲,处理系统文件可在所有现代浏览器上进行。 在支持 File System Access API 的浏览器上,您可以允许真正保存和覆盖(而不仅仅是下载)文件,并允许用户在任何位置创建新文件,从而提升用户体验,同时在不支持 File System Access API 的浏览器上保持功能正常运行。 browser-fs-access 通过处理渐进增强的细微之处,让您的生活更轻松,并使您的代码尽可能简单。
致谢
本文由 Joe Medley 和 Kayce Basques 审核。 感谢 Excalidraw 的贡献者为该项目所做的工作,以及对我的拉取请求的审核。 主打图片,由 Unsplash 用户 Ilya Pavlov 提供。