在 JavaScript 中读取文件

在用户的本地设备上选择文件并与之交互是 Web 最常用的功能之一。它允许用户选择文件并将其上传到服务器,例如,在共享照片或提交税务文件时。它还允许站点读取和操作它们,而无需通过网络传输数据。本页面介绍了如何使用 JavaScript 与文件进行交互。

现代文件系统访问 API

文件系统访问 API 提供了一种从用户本地系统上的文件和目录中读取数据以及向其中写入数据的方法。它在大多数基于 Chromium 的浏览器(如 Chrome 和 Edge)中可用。要了解更多信息,请参阅文件系统访问 API

由于文件系统访问 API 与并非所有浏览器都兼容,因此我们建议使用 browser-fs-access,这是一个辅助库,它在新 API 可用的地方使用它,并在不可用时回退到旧方法。

以经典方式处理文件

本指南介绍了如何使用旧版 JavaScript 方法与文件进行交互。

选择文件

选择文件主要有两种方法:使用 HTML input 元素和使用拖放区域

HTML input 元素

用户选择文件最简单的方法是使用 <input type="file"> 元素,该元素在所有主流浏览器中都受支持。单击时,它允许用户使用其操作系统的内置文件选择 UI 选择一个文件或多个文件(如果包含 multiple 属性)。当用户完成选择一个或多个文件后,元素的 change 事件会触发。您可以从 event.target.files 访问文件列表,这是一个 FileList 对象。FileList 中的每个项目都是一个 File 对象。

<!-- The `multiple` attribute lets users select multiple files. -->
<input type="file" id="file-selector" multiple>
<script>
  const fileSelector = document.getElementById('file-selector');
  fileSelector.addEventListener('change', (event) => {
    const fileList = event.target.files;
    console.log(fileList);
  });
</script>

以下示例允许用户使用其操作系统的内置文件选择 UI 选择多个文件,然后将每个选定的文件记录到控制台。

限制用户可以选择的文件类型

在某些情况下,您可能希望限制用户可以选择的文件类型。例如,图像编辑应用应仅接受图像,而不接受文本文件。要设置文件类型限制,请向 input 元素添加 accept 属性,以指定接受的文件类型

<input type="file" id="file-selector" accept=".jpg, .jpeg, .png">

自定义拖放

在某些浏览器中,<input type="file"> 元素也是一个放置目标,允许用户将文件拖放到您的应用中。但是,此放置目标很小,可能难以使用。相反,在使用 <input type="file"> 元素提供核心功能后,您可以提供一个大型自定义拖放表面。

选择您的放置区域

您的放置表面取决于应用程序的设计。您可能只希望窗口的一部分作为放置表面,但您可以使用整个窗口。

A screenshot of Squoosh, an image compression web app.
Squoosh 将整个窗口设为放置区域。

图像压缩应用 Squoosh 允许用户将图像拖动到窗口中的任何位置,然后点击选择图像以调用 <input type="file"> 元素。无论您选择什么作为放置区域,请确保向用户明确他们可以将文件拖到该表面上。

定义放置区域

要启用元素作为拖放区域,请为两个事件创建侦听器:dragoverdropdragover 事件更新浏览器 UI,以直观地指示拖放操作正在创建文件的副本。drop 事件在用户将文件放到表面上后触发。与 input 元素一样,您可以从 event.dataTransfer.files 访问文件列表,这是一个 FileList 对象。FileList 中的每个项目都是一个 File 对象。

const dropArea = document.getElementById('drop-area');

dropArea.addEventListener('dragover', (event) => {
  event.stopPropagation();
  event.preventDefault();
  // Style the drag-and-drop as a "copy file" operation.
  event.dataTransfer.dropEffect = 'copy';
});

dropArea.addEventListener('drop', (event) => {
  event.stopPropagation();
  event.preventDefault();
  const fileList = event.dataTransfer.files;
  console.log(fileList);
});

event.stopPropagation()event.preventDefault() 停止浏览器的默认行为,并让您的代码改为运行。如果没有它们,浏览器会导航离开您的页面,并打开用户放到浏览器窗口中的文件。

有关实时演示,请参阅自定义拖放

目录呢?

遗憾的是,没有访问目录的好方法,使用 JavaScript 无法实现。

<input type="file"> 元素上的 webkitdirectory 属性允许用户选择一个或多个目录。除了 Android 版 Firefox 和 iOS 版 Safari 之外,大多数主流浏览器都支持它

如果启用了拖放,用户可能会尝试将目录拖到放置区域。当 drop 事件触发时,它会包含目录的 File 对象,但不提供对目录中任何文件的访问权限。

读取文件元数据

File 对象包含有关文件的元数据。大多数浏览器提供文件名、文件大小和 MIME 类型,但根据平台的不同,不同的浏览器可能会提供不同或附加的信息。

function getMetadataForFileList(fileList) {
  for (const file of fileList) {
    // Not supported in Safari for iOS.
    const name = file.name ? file.name : 'NOT SUPPORTED';
    // Not supported in Firefox for Android or Opera for Android.
    const type = file.type ? file.type : 'NOT SUPPORTED';
    // Unknown cross-browser support.
    const size = file.size ? file.size : 'NOT SUPPORTED';
    console.log({file, name, type, size});
  }
}

您可以在 input-type-file 演示中看到此操作。

读取文件内容

使用 FileReaderFile 对象的内容读取到内存中。您可以告知 FileReader 将文件读取为数组缓冲区数据 URL文本

function readImage(file) {
  // Check if the file is an image.
  if (file.type && !file.type.startsWith('image/')) {
    console.log('File is not an image.', file.type, file);
    return;
  }

  const reader = new FileReader();
  reader.addEventListener('load', (event) => {
    img.src = event.target.result;
  });
  reader.readAsDataURL(file);
}

此示例读取用户提供的 File,然后将其转换为数据 URL,并使用该数据 URL 在 img 元素中显示图像。要了解如何验证用户是否选择了图像文件,请参阅 read-image-file 演示。

监控文件读取的进度

读取大型文件时,提供一些 UX 来告知用户读取进度可能会很有帮助。为此,请使用 FileReader 提供的 progress 事件。progress 事件具有两个属性:loaded(已读取的量)和 total(要读取的量)。

function readFile(file) {
  const reader = new FileReader();
  reader.addEventListener('load', (event) => {
    const result = event.target.result;
    // Do something with result
  });

  reader.addEventListener('progress', (event) => {
    if (event.loaded && event.total) {
      const percent = (event.loaded / event.total) * 100;
      console.log(`Progress: ${Math.round(percent)}`);
    }
  });
  reader.readAsDataURL(file);
}

英雄图片由 Vincent Botta 提供,来自 Unsplash