从 Web 应用中使用文件

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

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

访问选定的文件

考虑以下 HTML

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

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

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

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

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

在更改事件中访问选定的文件

也可以(但不是必须)通过 change 事件访问 FileList。您需要使用 EventTarget.addEventListener() 添加 change 事件侦听器,如下所示

js
const inputElement = document.getElementById("input");
inputElement.addEventListener("change", handleFiles, false);
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 对象提供了三个属性,这些属性包含有关文件的有用信息。

名称

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

大小

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

类型

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

示例:显示文件大小

以下示例显示了 size 属性的可能用法。

html
<!doctype html>
<html lang="en-US">
  <head>
    <meta charset="UTF-8" />
    <title>File(s) size</title>
  </head>

  <body>
    <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>

    <script>
      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;
        },
        false,
      );
    </script>
  </body>
</html>

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

您可以隐藏看起来不太美观的 <input> 元素,并为您自己的打开文件选择器和显示用户选择的文件的界面提供自己的界面。您可以通过使用 display:none 对输入元素进行样式设置,并在 <input> 元素上调用 click() 方法来实现。

考虑以下 HTML

html
<input
  type="file"
  id="fileElem"
  multiple
  accept="image/*"
  style="display:none" />
<button id="fileSelect" type="button">Select some files</button>

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

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

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

您可以根据需要对 <button> 进行样式设置。

使用标签元素触发隐藏的文件输入元素

要允许在不使用 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 {
  position: absolute !important;
  height: 1px;
  width: 1px;
  overflow: hidden;
  clip: rect(1px, 1px, 1px, 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, false);
dropbox.addEventListener("dragover", dragover, false);
dropbox.addEventListener("drop", drop, false);

在此示例中,我们将 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 (let i = 0; i < files.length; i++) {
    const file = files[i];

    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,该 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/*"
  style="display:none" />
<a href="#" id="fileSelect">Select some files</a>
<div id="fileList">
  <p>No files selected!</p>
</div>

这建立了我们的文件 <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 "#"
  },
  false,
);

fileElem.addEventListener("change", handleFiles, false);

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 (let i = 0; i < this.files.length; i++) {
      const li = document.createElement("li");
      list.appendChild(li);

      const img = document.createElement("img");
      img.src = URL.createObjectURL(this.files[i]);
      img.height = 60;
      img.onload = () => {
        URL.revokeObjectURL(img.src);
      };
      li.appendChild(img);
      const info = document.createElement("span");
      info.textContent = `${this.files[i].name}: ${this.files[i].size} bytes`;
      li.appendChild(info);
    }
  }
}

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

如果传递给 handleFiles()FileList 对象为 null,我们将块的内部 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.revokeObjectURL() 方法并将 img.src 指定的对象 URL 字符串作为参数传入来实现。
    6. 将新的列表项追加到列表中。

以下是上面代码的实时演示

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

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

注意: 通常建议使用 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 (let i = 0; i < imgs.length; i++) {
    new FileUpload(imgs[i], imgs[i].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;

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

  xhr.upload.addEventListener(
    "load",
    (e) => {
      self.ctrl.update(100);
      const canvas = self.ctrl.ctx.canvas;
      canvas.parentNode.removeChild(canvas);
    },
    false,
  );
  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. 通过调用 XMLHttpRequest 函数 overrideMimeType() 设置上传的 MIME 类型。在本例中,我们使用通用 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>
    <script type="application/javascript">
        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);
        }

        window.onload = () => {
            const dropzone = document.getElementById("dropzone");
            dropzone.ondragover = dropzone.ondragenter = (event) => {
                event.stopPropagation();
                event.preventDefault();
            }

            dropzone.ondrop = (event) => {
                event.stopPropagation();
                event.preventDefault();

                const filesArray = event.dataTransfer.files;
                for (let i=0; i<filesArray.length; i++) {
                    sendFile(filesArray[i]);
                }
            }
        }
    </script>
</head>
<body>
    <div>
        <div id="dropzone" style="margin:30px; width:500px; height:300px; border:1px dotted grey;">Drag & drop your file here</div>
    </div>
</body>
</html>

示例:使用对象 URL 显示 PDF

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

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

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

这是 src 属性的更改

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

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

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

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

另请参阅