使用 Web 应用程序中的文件
注意:此功能在 Web Workers 中可用。
使用 File API,Web 内容可以要求用户选择本地文件,然后读取这些文件的内容。此选择可以通过使用 HTML <input type="file"> 元素或通过拖放来完成。
访问选定的文件
考虑此 HTML
<input type="file" id="input" multiple />
File API 使得访问包含表示用户选择文件的 File 对象的 FileList 成为可能。
input 元素的 multiple 属性允许用户选择多个文件。
使用经典 DOM 选择器访问第一个选定的文件
const selectedFile = document.getElementById("input").files[0];
在 change 事件上访问选定的文件
也可以(但不是强制性的)通过 change 事件访问 FileList。你需要使用 EventTarget.addEventListener() 添加 change 事件监听器,如下所示
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 属性的值来确定用户选择了多少个文件
const numFiles = fileList.length;
可以通过将列表作为数组访问来检索单个 File 对象。
File 对象提供了三个属性,其中包含有关文件的有用信息。
示例:显示文件大小
以下示例展示了 size 属性的一种可能用法
<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>
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
<input type="file" id="fileElem" multiple accept="image/*" />
<button id="fileSelect" type="button">Select some files</button>
#fileElem {
display: none;
}
处理 click 事件的代码可能如下所示
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
<input
type="file"
id="fileElem"
multiple
accept="image/*"
class="visually-hidden" />
<label for="fileElem">Select some files</label>
以及此 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 应用程序中。
第一步是建立一个拖放区。你的内容的哪个部分将接受拖放可能会因应用程序的设计而异,但让元素接收拖放事件很简单
let dropbox;
dropbox = document.getElementById("dropbox");
dropbox.addEventListener("dragenter", dragenter);
dropbox.addEventListener("dragover", dragover);
dropbox.addEventListener("drop", drop);
在此示例中,我们将 ID 为 dropbox 的元素转换为我们的拖放区。这是通过添加 dragenter、dragover 和 drop 事件的监听器来完成的。
在我们的案例中,我们实际上不需要对 dragenter 和 dragover 事件做任何事情,因此这些函数都很简单。它们只是停止事件传播并阻止默认操作发生
function dragenter(e) {
e.stopPropagation();
e.preventDefault();
}
function dragover(e) {
e.stopPropagation();
e.preventDefault();
}
真正的魔法发生在 drop() 函数中
function drop(e) {
e.stopPropagation();
e.preventDefault();
const dt = e.dataTransfer;
const files = dt.files;
handleFiles(files);
}
在这里,我们从事件中检索 dataTransfer 字段,从中拉出文件列表,然后将其传递给 handleFiles()。从这一点开始,无论用户是使用 input 元素还是拖放,文件处理都是相同的。
示例:显示用户选择图像的缩略图
假设你正在开发下一个伟大的照片共享网站,并希望在用户实际上传图像之前使用 HTML 显示图像的缩略图预览。你可以像前面讨论的那样建立输入元素或拖放区,并让它们调用一个函数,例如下面的 handleFiles() 函数。
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
const objectURL = window.URL.createObjectURL(fileObj);
对象 URL 是一个标识 File 对象的字符串。每次你调用 URL.createObjectURL() 时,即使你已经为该文件创建了一个对象 URL,也会创建一个唯一的对象 URL。这些都必须释放。虽然它们在文档卸载时会自动释放,但如果你的页面动态使用它们,你应该通过调用 URL.revokeObjectURL() 显式释放它们
URL.revokeObjectURL(objectURL);
示例:使用对象 URL 显示图像
此示例使用对象 URL 显示图像缩略图。此外,它还显示其他文件信息,包括它们的名称和大小。
呈现界面的 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>
#fileElem {
display: none;
}
这建立了我们的文件 <input> 元素以及一个调用文件选择器的链接(因为我们保持文件输入隐藏以防止显示不那么吸引人的用户界面)。这在使用 click() 方法使用隐藏文件输入元素一节中解释,调用文件选择器的方法也是如此。
handleFiles() 方法如下
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 设置为显示“未选择文件!”。否则,我们开始构建文件列表,如下所示
- 创建一个新的无序列表 (
<ul>) 元素。 - 通过调用其
Node.appendChild()方法,将新的列表元素插入到<div>块中。 - 对于
files表示的FileList中的每个File- 创建一个新的列表项 (
<li>) 元素并将其插入到列表中。 - 创建一个新的图像 (
<img>) 元素。 - 使用
URL.createObjectURL()创建 blob URL,将图像的源设置为表示文件的新对象 URL。 - 将图像的高度设置为 60 像素。
- 将新的列表项附加到列表中。
- 创建一个新的列表项 (
这是上述代码的实时演示
请注意,我们不会在图像加载后立即撤销对象 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() 选择用户选择的所有要上传的图像,如下所示
function sendFiles() {
const imgs = document.querySelectorAll(".obj");
for (const img of imgs) {
new FileUpload(img, img.file);
}
}
document.querySelectorAll 获取文档中所有具有 CSS 类 obj 的元素的 NodeList。在我们的例子中,这些将是所有图像缩略图。一旦我们有了这个列表,遍历它并为每个图像创建一个新的 FileUpload 实例就很容易了。每个实例都负责上传相应的图像。
处理文件的上传过程
FileUpload 函数接受两个输入:一个图像元素和从中读取图像数据的文件。
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 来处理数据上传。
在实际传输数据之前,需要执行几个准备步骤
XMLHttpRequest的上传progress监听器设置为使用新的百分比信息更新进度条,以便随着上传的进行,进度条将根据最新信息进行更新。XMLHttpRequest的上传load事件处理程序设置为将进度条进度信息更新为 100%,以确保进度指示器实际达到 100%(以防在过程中出现粒度问题)。然后它会删除进度条,因为它不再需要。这会导致进度条在上传完成后消失。- 通过调用
XMLHttpRequest的open()方法打开上传图像文件的请求,以开始生成 POST 请求。 - 上传的 MIME 类型通过调用
XMLHttpRequest函数overrideMimeType()设置。在这种情况下,我们使用的是通用 MIME 类型;你可能需要或根本不需要设置 MIME 类型,具体取决于你的用例。 FileReader对象用于将文件转换为二进制字符串。- 最后,当内容加载后,调用
XMLHttpRequest函数send()来上传文件的内容。
异步处理文件上传过程
此示例在服务器端使用 PHP,在客户端使用 JavaScript,演示了文件的异步上传。
<?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。
<iframe id="viewer"></iframe>
这是 src 属性的更改
const objURL = URL.createObjectURL(blob);
const iframe = document.getElementById("viewer");
iframe.setAttribute("src", objURL);
// Later:
URL.revokeObjectURL(objURL);
示例:将对象 URL 与其他文件类型一起使用
你可以以相同的方式操作其他格式的文件。以下是如何预览上传的视频
const video = document.getElementById("video");
const objURL = URL.createObjectURL(blob);
video.src = objURL;
video.play();
// Later:
URL.revokeObjectURL(objURL);