浏览器已经能够处理文件和目录很长时间了。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()
的方法。在内部,该库会检测是否支持文件系统访问 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',
});
})();
演示
您可以在 Glitch 上的演示中查看上述代码的实际运作情况。您也可以在该处找到其源代码。出于安全考虑,跨源子框架不允许显示文件选择器,因此无法将该演示嵌入到本文中。
实际使用的 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) {}
),除了 Save 按钮之外,您还可以显示 Save As 按钮。以下屏幕截图显示了 Excalidraw 在 iPhone 和 Chrome 桌面版上的自适应主应用工具栏之间的差异。请注意,iPhone 上缺少 Save As 按钮。
总结
从技术层面讲,在所有现代浏览器中都可以处理系统文件。在支持 File System Access API 的浏览器上,您可以允许真正保存和覆盖(而不仅仅是下载)文件,并允许用户在任何位置创建新文件,从而改善用户体验,同时在不支持 File System Access API 的浏览器上保持功能正常。browser-fs-access 可处理渐进增强的细微之处,并尽可能简化代码,从而让您的工作更轻松。
致谢
本文由 Joe Medley 和 Kayce Basques 审核。 感谢 Excalidraw 的贡献者参与该项目的工作,并审核我的拉取请求。主打图片:Unsplash 上的 Ilya Pavlov。