使用 Web 应用程序中的文件

注意:此功能在 Web Workers 中可用。

使用 File API,Web 内容可以要求用户选择本地文件,然后读取这些文件的内容。此选择可以通过使用 HTML <input type="file"> 元素或通过拖放来完成。

访问选定的文件

考虑此 HTML

html
<input type="file" id="input" multiple />

File API 使得访问包含表示用户选择文件的 File 对象的 FileList 成为可能。

input 元素的 multiple 属性允许用户选择多个文件。

使用经典 DOM 选择器访问第一个选定的文件

js
const selectedFile = document.getElementById("input").files[0];

在 change 事件上访问选定的文件

也可以(但不是强制性的)通过 change 事件访问 FileList。你需要使用 EventTarget.addEventListener() 添加 change 事件监听器,如下所示

js
const inputElement = document.getElementById("input");
inputElement.addEventListener("change", handleFiles);
function handleFiles() {
  const fileList = this.files; /* now you can work with the file list */
}

获取有关选定文件信息

DOM 提供的 FileList 对象列出了用户选择的所有文件,每个文件都指定为 File 对象。你可以通过检查文件列表的 length 属性的值来确定用户选择了多少个文件

js
const numFiles = fileList.length;

可以通过将列表作为数组访问来检索单个 File 对象。

File 对象提供了三个属性,其中包含有关文件的有用信息。

name

文件的名称作为只读字符串。这只是文件名,不包含任何路径信息。

size

文件大小(以字节为单位),作为只读 64 位整数。

type

文件的 MIME 类型作为只读字符串,如果无法确定类型,则为 ""

示例:显示文件大小

以下示例展示了 size 属性的一种可能用法

html
<form name="uploadForm">
  <div>
    <input id="uploadInput" type="file" multiple />
    <label for="fileNum">Selected files:</label>
    <output id="fileNum">0</output>;
    <label for="fileSize">Total size:</label>
    <output id="fileSize">0</output>
  </div>
  <div><input type="submit" value="Send file" /></div>
</form>
js
const uploadInput = document.getElementById("uploadInput");
uploadInput.addEventListener("change", () => {
  // Calculate total size
  let numberOfBytes = 0;
  for (const file of uploadInput.files) {
    numberOfBytes += file.size;
  }

  // Approximate to the closest prefixed unit
  const units = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"];
  const exponent = Math.min(
    Math.floor(Math.log(numberOfBytes) / Math.log(1024)),
    units.length - 1,
  );
  const approx = numberOfBytes / 1024 ** exponent;
  const output =
    exponent === 0
      ? `${numberOfBytes} bytes`
      : `${approx.toFixed(3)} ${units[exponent]} (${numberOfBytes} bytes)`;

  document.getElementById("fileNum").textContent = uploadInput.files.length;
  document.getElementById("fileSize").textContent = output;
});

使用 click() 方法使用隐藏文件输入元素

你可以隐藏公认的难看的 <input> 文件元素,并提供自己的界面来打开文件选择器并显示用户选择的文件。你可以通过将输入元素样式设置为 display:none 并调用 <input> 元素的 click() 方法来完成此操作。

考虑此 HTML

html
<input type="file" id="fileElem" multiple accept="image/*" />
<button id="fileSelect" type="button">Select some files</button>
css
#fileElem {
  display: none;
}

处理 click 事件的代码可能如下所示

js
const fileSelect = document.getElementById("fileSelect");
const fileElem = document.getElementById("fileElem");

fileSelect.addEventListener("click", (e) => {
  if (fileElem) {
    fileElem.click();
  }
});

你可以根据需要设置 <button> 的样式。

使用 label 元素触发隐藏文件输入元素

为了在不使用 JavaScript(click() 方法)的情况下打开文件选择器,可以使用 <label> 元素。请注意,在这种情况下,输入元素不能使用 display: none(或 visibility: hidden)隐藏,否则标签将无法通过键盘访问。请改用视觉隐藏技术

考虑此 HTML

html
<input
  type="file"
  id="fileElem"
  multiple
  accept="image/*"
  class="visually-hidden" />
<label for="fileElem">Select some files</label>

以及此 CSS

css
.visually-hidden {
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  height: 1px;
  overflow: hidden;
  position: absolute;
  white-space: nowrap;
  width: 1px;
}

input.visually-hidden:is(:focus, :focus-within) + label {
  outline: thin dotted;
}

无需添加 JavaScript 代码来调用 fileElem.click()。在这种情况下,你也可以根据需要设置标签元素的样式。你需要为隐藏输入字段在其标签上的焦点状态提供视觉提示,无论是如上所示的轮廓,还是背景色或盒阴影。(截至撰写本文时,Firefox 不显示 <input type="file"> 元素的此视觉提示。)

使用拖放选择文件

你还可以让用户将文件拖放到你的 Web 应用程序中。

第一步是建立一个拖放区。你的内容的哪个部分将接受拖放可能会因应用程序的设计而异,但让元素接收拖放事件很简单

js
let dropbox;

dropbox = document.getElementById("dropbox");
dropbox.addEventListener("dragenter", dragenter);
dropbox.addEventListener("dragover", dragover);
dropbox.addEventListener("drop", drop);

在此示例中,我们将 ID 为 dropbox 的元素转换为我们的拖放区。这是通过添加 dragenterdragoverdrop 事件的监听器来完成的。

在我们的案例中,我们实际上不需要对 dragenterdragover 事件做任何事情,因此这些函数都很简单。它们只是停止事件传播并阻止默认操作发生

js
function dragenter(e) {
  e.stopPropagation();
  e.preventDefault();
}

function dragover(e) {
  e.stopPropagation();
  e.preventDefault();
}

真正的魔法发生在 drop() 函数中

js
function drop(e) {
  e.stopPropagation();
  e.preventDefault();

  const dt = e.dataTransfer;
  const files = dt.files;

  handleFiles(files);
}

在这里,我们从事件中检索 dataTransfer 字段,从中拉出文件列表,然后将其传递给 handleFiles()。从这一点开始,无论用户是使用 input 元素还是拖放,文件处理都是相同的。

示例:显示用户选择图像的缩略图

假设你正在开发下一个伟大的照片共享网站,并希望在用户实际上传图像之前使用 HTML 显示图像的缩略图预览。你可以像前面讨论的那样建立输入元素或拖放区,并让它们调用一个函数,例如下面的 handleFiles() 函数。

js
function handleFiles(files) {
  for (const file of files) {
    if (!file.type.startsWith("image/")) {
      continue;
    }

    const img = document.createElement("img");
    img.classList.add("obj");
    img.file = file;
    preview.appendChild(img); // Assuming that "preview" is the div output where the content will be displayed.

    const reader = new FileReader();
    reader.onload = (e) => {
      img.src = e.target.result;
    };
    reader.readAsDataURL(file);
  }
}

在这里,我们处理用户选择的文件的循环会查看每个文件的 type 属性,以查看其 MIME 类型是否以 image/ 开头。对于每个图像文件,我们都会创建一个新的 img 元素。CSS 可用于建立任何漂亮的边框或阴影并指定图像的大小,因此无需在此处完成。

每个图像都添加了 CSS 类 obj,使其易于在 DOM 树中查找。我们还为每个图像添加了一个 file 属性,指定图像的 File;这将使我们以后能够获取图像进行实际上传。我们使用 Node.appendChild() 将新的缩略图添加到文档的预览区域。

接下来,我们建立 FileReader 以异步加载图像并将其附加到 img 元素。创建新的 FileReader 对象后,我们设置其 onload 函数,然后调用 readAsDataURL() 在后台开始读取操作。当图像文件的全部内容加载后,它们将转换为 data: URL,并将其传递给 onload 回调。我们对该例程的实现将 img 元素的 src 属性设置为加载的图像,从而导致图像出现在用户屏幕上的缩略图中。

使用对象 URL

DOM URL.createObjectURL()URL.revokeObjectURL() 方法允许你创建简单的 URL 字符串,这些字符串可用于引用任何可以使用 DOM File 对象引用的数据,包括用户计算机上的本地文件。

当你有想要从 HTML 通过 URL 引用的 File 对象时,你可以像这样为其创建一个对象 URL

js
const objectURL = window.URL.createObjectURL(fileObj);

对象 URL 是一个标识 File 对象的字符串。每次你调用 URL.createObjectURL() 时,即使你已经为该文件创建了一个对象 URL,也会创建一个唯一的对象 URL。这些都必须释放。虽然它们在文档卸载时会自动释放,但如果你的页面动态使用它们,你应该通过调用 URL.revokeObjectURL() 显式释放它们

js
URL.revokeObjectURL(objectURL);

示例:使用对象 URL 显示图像

此示例使用对象 URL 显示图像缩略图。此外,它还显示其他文件信息,包括它们的名称和大小。

呈现界面的 HTML 如下所示

html
<input type="file" id="fileElem" multiple accept="image/*" />
<a href="#" id="fileSelect">Select some files</a>
<div id="fileList">
  <p>No files selected!</p>
</div>
css
#fileElem {
  display: none;
}

这建立了我们的文件 <input> 元素以及一个调用文件选择器的链接(因为我们保持文件输入隐藏以防止显示不那么吸引人的用户界面)。这在使用 click() 方法使用隐藏文件输入元素一节中解释,调用文件选择器的方法也是如此。

handleFiles() 方法如下

js
const fileSelect = document.getElementById("fileSelect"),
  fileElem = document.getElementById("fileElem"),
  fileList = document.getElementById("fileList");

fileSelect.addEventListener("click", (e) => {
  if (fileElem) {
    fileElem.click();
  }
  e.preventDefault(); // prevent navigation to "#"
});

fileElem.addEventListener("change", handleFiles);

function handleFiles() {
  fileList.textContent = "";
  if (!this.files.length) {
    const p = document.createElement("p");
    p.textContent = "No files selected!";
    fileList.appendChild(p);
  } else {
    const list = document.createElement("ul");
    fileList.appendChild(list);
    for (const file of this.files) {
      const li = document.createElement("li");
      list.appendChild(li);

      const img = document.createElement("img");
      img.src = URL.createObjectURL(file);
      img.height = 60;
      li.appendChild(img);
      const info = document.createElement("span");
      info.textContent = `${file.name}: ${file.size} bytes`;
      li.appendChild(info);
    }
  }
}

这首先获取 ID 为 fileList<div> 的 URL。这是我们将插入文件列表(包括缩略图)的块。

如果传递给 handleFiles()FileList 对象为空,我们则将该块的内部 HTML 设置为显示“未选择文件!”。否则,我们开始构建文件列表,如下所示

  1. 创建一个新的无序列表 (<ul>) 元素。
  2. 通过调用其 Node.appendChild() 方法,将新的列表元素插入到 <div> 块中。
  3. 对于 files 表示的 FileList 中的每个 File
    1. 创建一个新的列表项 (<li>) 元素并将其插入到列表中。
    2. 创建一个新的图像 (<img>) 元素。
    3. 使用 URL.createObjectURL() 创建 blob URL,将图像的源设置为表示文件的新对象 URL。
    4. 将图像的高度设置为 60 像素。
    5. 将新的列表项附加到列表中。

这是上述代码的实时演示

请注意,我们不会在图像加载后立即撤销对象 URL,因为这样做会使图像无法进行用户交互(例如右键单击保存图像或在新选项卡中打开它)。对于长期运行的应用程序,当不再需要对象 URL 时(例如当图像从 DOM 中删除时),应通过调用 URL.revokeObjectURL() 方法并传入对象 URL 字符串来显式撤销对象 URL 以释放内存。

示例:上传用户选择的文件

此示例展示了如何让用户将文件(例如使用上一个示例选择的图像)上传到服务器。

注意: 通常,使用 Fetch API 而不是 XMLHttpRequest 发送 HTTP 请求更好。但是,在这种情况下,我们想向用户显示上传进度,而 Fetch API 仍不支持此功能,因此该示例使用 XMLHttpRequest

使用 Fetch API 跟踪进度通知标准化的工作在 https://github.com/whatwg/fetch/issues/607

创建上传任务

继续上一个示例中构建缩略图的代码,回想一下每个缩略图图像都在 CSS 类 obj 中,并在 file 属性中附加了相应的 File。这允许我们使用 Document.querySelectorAll() 选择用户选择的所有要上传的图像,如下所示

js
function sendFiles() {
  const imgs = document.querySelectorAll(".obj");

  for (const img of imgs) {
    new FileUpload(img, img.file);
  }
}

document.querySelectorAll 获取文档中所有具有 CSS 类 obj 的元素的 NodeList。在我们的例子中,这些将是所有图像缩略图。一旦我们有了这个列表,遍历它并为每个图像创建一个新的 FileUpload 实例就很容易了。每个实例都负责上传相应的图像。

处理文件的上传过程

FileUpload 函数接受两个输入:一个图像元素和从中读取图像数据的文件。

js
function FileUpload(img, file) {
  const reader = new FileReader();
  this.ctrl = createThrobber(img);
  const xhr = new XMLHttpRequest();
  this.xhr = xhr;

  this.xhr.upload.addEventListener("progress", (e) => {
    if (e.lengthComputable) {
      const percentage = Math.round((e.loaded * 100) / e.total);
      this.ctrl.update(percentage);
    }
  });

  xhr.upload.addEventListener("load", (e) => {
    this.ctrl.update(100);
    const canvas = this.ctrl.ctx.canvas;
    canvas.parentNode.removeChild(canvas);
  });
  xhr.open(
    "POST",
    "https://demos.hacks.mozilla.org/paul/demos/resources/webservices/devnull.php",
  );
  xhr.overrideMimeType("text/plain; charset=x-user-defined-binary");
  reader.onload = (evt) => {
    xhr.send(evt.target.result);
  };
  reader.readAsBinaryString(file);
}

function createThrobber(img) {
  const throbberWidth = 64;
  const throbberHeight = 6;
  const throbber = document.createElement("canvas");
  throbber.classList.add("upload-progress");
  throbber.setAttribute("width", throbberWidth);
  throbber.setAttribute("height", throbberHeight);
  img.parentNode.appendChild(throbber);
  throbber.ctx = throbber.getContext("2d");
  throbber.ctx.fillStyle = "orange";
  throbber.update = (percent) => {
    throbber.ctx.fillRect(
      0,
      0,
      (throbberWidth * percent) / 100,
      throbberHeight,
    );
    if (percent === 100) {
      throbber.ctx.fillStyle = "green";
    }
  };
  throbber.update(0);
  return throbber;
}

上面显示的 FileUpload() 函数创建一个进度条,用于显示进度信息,然后创建一个 XMLHttpRequest 来处理数据上传。

在实际传输数据之前,需要执行几个准备步骤

  1. XMLHttpRequest 的上传 progress 监听器设置为使用新的百分比信息更新进度条,以便随着上传的进行,进度条将根据最新信息进行更新。
  2. XMLHttpRequest 的上传 load 事件处理程序设置为将进度条进度信息更新为 100%,以确保进度指示器实际达到 100%(以防在过程中出现粒度问题)。然后它会删除进度条,因为它不再需要。这会导致进度条在上传完成后消失。
  3. 通过调用 XMLHttpRequestopen() 方法打开上传图像文件的请求,以开始生成 POST 请求。
  4. 上传的 MIME 类型通过调用 XMLHttpRequest 函数 overrideMimeType() 设置。在这种情况下,我们使用的是通用 MIME 类型;你可能需要或根本不需要设置 MIME 类型,具体取决于你的用例。
  5. FileReader 对象用于将文件转换为二进制字符串。
  6. 最后,当内容加载后,调用 XMLHttpRequest 函数 send() 来上传文件的内容。

异步处理文件上传过程

此示例在服务器端使用 PHP,在客户端使用 JavaScript,演示了文件的异步上传。

php
<?php
if (isset($_FILES["myFile"])) {
  // Example:
  move_uploaded_file($_FILES["myFile"]["tmp_name"], "uploads/" . $_FILES["myFile"]["name"]);
  exit;
}
?><!doctype html>
<html lang="en-US">
  <head>
    <meta charset="UTF-8" />
    <title>dnd binary upload</title>
  </head>
  <body>
    <div>
      <div
        id="dropzone"
        style="margin:30px; width:500px; height:300px; border:1px dotted grey;">
        Drag & drop your file here
      </div>
    </div>
    <script>
      function sendFile(file) {
        const uri = "/index.php";
        const xhr = new XMLHttpRequest();
        const fd = new FormData();

        xhr.open("POST", uri, true);
        xhr.onreadystatechange = () => {
          if (xhr.readyState === 4 && xhr.status === 200) {
            alert(xhr.responseText); // handle response.
          }
        };
        fd.append("myFile", file);
        // Initiate a multipart/form-data upload
        xhr.send(fd);
      }

      const dropzone = document.getElementById("dropzone");
      dropzone.addEventListener("dragover", (event) => {
        event.stopPropagation();
        event.preventDefault();
      });

      dropzone.addEventListener("drop", (event) => {
        event.preventDefault();

        const filesArray = event.dataTransfer.files;
        for (let i = 0; i < filesArray.length; i++) {
          sendFile(filesArray[i]);
        }
      });
    </script>
  </body>
</html>

示例:使用对象 URL 显示 PDF

对象 URL 不仅可以用于图像!它们还可以用于显示嵌入式 PDF 文件或浏览器可以显示的任何其他资源。

在 Firefox 中,要使 PDF 嵌入在 iframe 中(而不是作为下载文件建议),必须将首选项 pdfjs.disabled 设置为 false

html
<iframe id="viewer"></iframe>

这是 src 属性的更改

js
const objURL = URL.createObjectURL(blob);
const iframe = document.getElementById("viewer");
iframe.setAttribute("src", objURL);

// Later:
URL.revokeObjectURL(objURL);

示例:将对象 URL 与其他文件类型一起使用

你可以以相同的方式操作其他格式的文件。以下是如何预览上传的视频

js
const video = document.getElementById("video");
const objURL = URL.createObjectURL(blob);
video.src = objURL;
video.play();

// Later:
URL.revokeObjectURL(objURL);

另见