使用 browser-fs-access 库读取和写入文件和目录

很长时间以来,浏览器就一直能够处理文件和目录。 File API 提供在 Web 应用中表示文件对象的功能, 还可以以编程方式选择它们并访问其数据。 但是,当你仔细看时,闪闪发光的就都不是黄金了。

处理文件的传统方式

打开文件

作为开发者,您可以通过 <input type="file"> 元素。 打开文件的最简单形式类似于以下代码示例。 input 对象为您提供了 FileList, 在下面的例子中,它只包含 FileFile 是一种特定类型的 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 完全没问题 它尚未广泛使用

<ph type="x-smartling-placeholder">
</ph> File System Access API 的浏览器支持表。所有浏览器都会标记为“不支持”或“在旗帜后面”。 <ph type="x-smartling-placeholder">
</ph> 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 库

这三种方法使用起来很直观。 您可以指定应用接受的 mimeTypesextensions 文件,并设置 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 库

业余时间里,我会为 可安装的 PWA 名为 Excalidraw 一款白板工具,可让您以手绘的方式轻松绘制图表。 它具有完全的响应能力,在从小手机到大屏幕的电脑等各种设备上均可使用。 这意味着它需要处理各种平台上的文件 它们是否支持 File System Access API。 因此,该库非常适合使用 browser-fs-access 库。

比方说,我可以在 iPhone 上画画 保存(从技术层面来讲:下载它,因为 Safari 不支持 File System Access API) 复制到我的 iPhone 的“Downloads”(下载)文件夹中,在我的桌面上打开该文件(从手机传输过来后), 修改文件,用我的更改覆盖它,甚至将其另存为新文件。

<ph type="x-smartling-placeholder">
</ph> iPhone 上的 Excalidraw 图。
在不支持 File System Access API 但可将文件保存(下载)到“下载内容”文件夹中的 iPhone 开始 Excalidraw 绘图。
。 <ph type="x-smartling-placeholder">
</ph> 桌面版 Chrome 中经过修改的 Excalidraw 绘图。
在支持 File System Access API 的桌面设备上打开和修改 Excalidraw 绘图,以便通过该 API 访问文件。
。 <ph type="x-smartling-placeholder">
</ph> 用所做修改覆盖原始文件。
使用对原始 Excalidraw 绘图文件的修改覆盖原始文件。浏览器会显示一个对话框,询问我是否可以正常下载。
。 <ph type="x-smartling-placeholder">
</ph> 正在将修改保存到新的 Excalidraw 绘图文件。
将修改保存到新的 Excalidraw 文件。原始文件保持不变。

真实代码示例

下面,您可以看到一个在 Excalidraw 中使用的 browser-fs-access 功能的实际示例。 此摘录来自 /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 上是如何缺少另存为按钮的。

<ph type="x-smartling-placeholder">
</ph> iPhone 上的 Excalidraw 应用工具栏,仅显示一个“保存”按钮。
iPhone 上的 Excalidraw 应用工具栏,只有一个保存按钮。
。 <ph type="x-smartling-placeholder">
</ph> Chrome 桌面上的 Excalidraw 应用工具栏带有“保存”和“另存为”按钮。
Chrome 上的 Excalidraw 应用工具栏,其中包含保存和聚焦的另存为按钮。

总结

从技术层面来讲,所有现代浏览器都支持使用系统文件。 在支持 File System Access API 的浏览器上,您可以允许 真正实现保存和覆盖(不只是下载)文件和 让用户可以在任意位置创建新文件 同时仍能在不支持 File System Access API 的浏览器上正常运行。 browser-fs-access 功能: 采用渐进式增强的细微之处,并尽可能简化代码。

致谢

本文由 Joe MedleyKayce Basques。 感谢 Excalidraw 的贡献者 以及查看我的拉取请求 主打图片,作者 Ilya Pavlov 在 Un 创立的节目中。