在 web 应用程序中使用文件
备注: 此特性在 Web Worker 中可用。
通过使用文件 API,web 内容可以要求用户选择本地文件,然后读取这些文件的内容。这种选择可以通过使用 HTML <input type="file">
元素或通过拖放来完成。
访问被选择的文件
通过 change 事件访问被选择的文件
可以(但不是强制的)通过 change
事件访问 FileList
。你需要使用 EventTarget.addEventListener()
添加 change
事件的处理器,像这样:
const inputElement = document.getElementById("input");
inputElement.addEventListener("change", handleFiles, false);
function handleFiles() {
const fileList = this.files; /* 现在你可以处理文件列表 */
}
获取被选择文件的信息
示例:显示文件大小
下面的例子展示了 size
属性的一种可能用法:
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>文件大小</title>
</head>
<body>
<form name="uploadForm">
<div>
<input id="uploadInput" type="file" multiple />
<label for="fileNum">选择的文件数量:</label>
<output id="fileNum">0</output>;
<label for="fileSize">总大小:</label>
<output id="fileSize">0</output>
</div>
<div><input type="submit" value="上传文件" /></div>
</form>
<script>
const uploadInput = document.getElementById("uploadInput");
uploadInput.addEventListener(
"change",
() => {
// 计算总大小
let numberOfBytes = 0;
for (const file of uploadInput.files) {
numberOfBytes += file.size;
}
// 近似到最接近的前缀单位
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} 字节`
: `${approx.toFixed(3)} ${
units[exponent]
}(${numberOfBytes} 字节)`;
document.getElementById("fileNum").textContent =
uploadInput.files.length;
document.getElementById("fileSize").textContent = output;
},
false,
);
</script>
</body>
</html>
通过 click() 方法使用隐藏的文件 input 元素
你可以隐藏公认难看的文件 <input>
元素并显示你自己的界面来打开文件选择器,然后显示哪个或哪些文件被用户选中了。你可以通过给 input 元素添加 display:none
的样式,再调用 <input>
元素的 click()
方法来实现。
考虑这段 HTML:
<input
type="file"
id="fileElem"
multiple
accept="image/*"
style="display:none" />
<button id="fileSelect" type="button">选择一些文件</button>
处理 click
事件的代码类似于这样:
const fileSelect = document.getElementById("fileSelect");
const fileElem = document.getElementById("fileElem");
fileSelect.addEventListener(
"click",
(e) => {
if (fileElem) {
fileElem.click();
}
},
false,
);
你可以给这个用来打开文件选择器的 <button>
元素添加任何你想要的样式。
使用 label 元素来触发一个隐藏的文件 input 元素
为了允许在不使用 JavaScript(click() 方法)的情况下打开文件选择器,可以使用 <label>
元素。注意,在这种情况下,input 元素不能用 display: none
来隐藏(也不能用 visibility: hidden
),否则标签就不具有键盘无障碍性。请使用 visually-hidden 技术代替。
考虑这段 HTML:
<input
type="file"
id="fileElem"
multiple
accept="image/*"
class="visually-hidden" />
<label for="fileElem">选择一些文件</label>
和这段 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()
,你也可以给 label 元素添加你想要的样式。你需要在其 label 上提供隐藏 input 字段的焦点状态的视觉提示,比如上面用的轮廓,或者背景颜色或边框阴影。(截至编写时为止,Firefox 不显示 <input type="file">
元素的视觉提示。)
使用拖放来选择文件
你还可以让用户将文件拖拽到你的 web 应用中。
第一步是创建一个 drop 区域。虽然你网页内容的哪部分接受拖放取决于你的应用设计,但是使一个元素接收 drop 事件是很容易的。
let dropbox;
dropbox = document.getElementById("dropbox");
dropbox.addEventListener("dragenter", dragenter, false);
dropbox.addEventListener("dragover", dragover, false);
dropbox.addEventListener("drop", drop, false);
在这个例子中,我们将 ID 为 dropbox
的元素变为了我们的 drop 区域。这是通过给元素添加 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 来展示用户在实际上传之前的图片的缩略图。你可以像我们之前讨论的那样创建自己的 input 元素或者 drop 区域,然后对它们使用一个回调函数,比如下面的 handleFiles()
函数。
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); // 假设“preview”就是用来显示内容的 div
const reader = new FileReader();
reader.onload = (e) => {
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
}
这里我们循环处理用户选择的文件,看每个文件的 type
属性是不是以“image/
”开头。对每个文件而言,如果它是图片,我们就创建一个新的 img
元素。可以使用 CSS 来创建一个漂亮的边框或阴影来显示图片的具体大小,在这儿就不具体做了。
为了在 DOM 树中更容易地找到他们,每个图片元素都被添加了一个名为 obj
的 CSS 类。我们还给每个图片添加了 file
属性以指定其 File
属性;这样做可以让我们拿到稍后需要实际上传的图片。我们在预览页中使用 Node.appendChild()
来添加新的缩略图。
接下来,我们创建了 FileReader
来处理异步的图片加载并把它赋给 img
元素。在创建一个新的 FileReader
对象后,我们设定了它的 onload
函数,然后调用 readAsDataURL()
函数开始后台读取文件。当整个图片文件的内容都被全部加载完后,它们被转换成了一个被传递到 onload
回调函数的 data:
URL。我们对这个程序的实现是将 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 都必须被释放。虽然它们会在文档卸载时自动释放,但如果你的页面动态地使用它们,你应该通过调用 URL.revokeObjectURL()
明确地释放它们:
URL.revokeObjectURL(objectURL);
示例:使用对象 URL 来显示图片
这个示例使用对象 URL 来显示图片缩略图。另外,示例也会显示文件名和文件大小等其他文件信息。
呈现界面的 HTML 看起来像这样:
<input
type="file"
id="fileElem"
multiple
accept="image/*"
style="display:none" />
<a href="#" id="fileSelect">选择一些文件</a>
<div id="fileList">
<p>没有选择任何文件!</p>
</div>
这就建立了我们的文件 <input>
元素,以及调用文件选取器的链接(因为我们把文件 input 隐藏起来,以防止显示那个不那么吸引人的用户界面)。这在使用 click() 方法使用隐藏的文件 input 元素一节中有所说明,调用文件选取器的方法也是如此。
handleFiles()
方法如下:
const fileSelect = document.getElementById("fileSelect"),
fileElem = document.getElementById("fileElem"),
fileList = document.getElementById("fileList");
fileSelect.addEventListener(
"click",
(e) => {
if (fileElem) {
fileElem.click();
}
e.preventDefault(); // 避免导航至“#”
},
false,
);
fileElem.addEventListener("change", handleFiles, false);
function handleFiles() {
fileList.textContent = "";
if (!this.files.length) {
const p = document.createElement("p");
p.textContent = "没有选择任何文件!";
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} 字节`;
li.appendChild(info);
}
}
}
首先,获取 ID 为 fileList
的 <div>
。这个区块里我们会插入我们的文件列表,包括缩略图。
如果传入 handleFiles()
的 FileList
对象值为 null
时,我们只要简单将这块的内部 HTML 为显示“没有选择任何文件!”。否则,我们就需要像下面这样编写我们的文件列表:
-
创建一个无序列表(
<ul>
)元素。 -
通过调用列表的
Node.appendChild()
方法来将新的列表元素插入到<div>
块。 -
遍历
files
代表的文件集合FileList
中的每个File
:- 创建一个新的列表项(
<li>
)元素并插入到列表中。 - 创建一个新的图片(
<img>
)元素。 - 设置图片的源为一个新的指代文件的对象 URL,使用
URL.createObjectURL()
来创建 blob URL。 - 设置图片的高度为 60 像素。
- 设置图片的 load 事件处理器来释放对象 URL,当图片加载完成之后对象 URL 就不再需要了。这个可以通过调用
URL.revokeObjectURL()
方法并且传递img.src
中的对象 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 (let i = 0; i < imgs.length; i++) {
new FileUpload(imgs[i], imgs[i].file);
}
}
document.querySelectorAll
获取了文档中所有 CSS 类为 obj
的元素的 NodeList
,命名为 imgs
。在我们的例子中,这些是包含所有图像缩略图的列表。有了这个列表,遍历并为每一项创建一个新的 FileUpload
实例就很简单了。每个实例都可以处理相应文件的上传。
处理文件的上传过程
FileUpload
函数接受两个输入:一个 image 元素和一个包含图像数据的文件。
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
来处理上传数据。
实际传输数据前,采取了几道准备步骤:
XMLHttpRequest
的progress
监听器被设为将加载指示器更新为新的百分比信息,这样随着上传进行,指示器会显示最新的信息。XMLHttpRequest
的load
监听器被设为将加载指示器的进度信息更新为 100%,以保证进度显示确实达到了 100%(以防在上传过程中出现粒度误差)。然后它移除了已经不再需要的加载指示器。这样上传一完成指示器就会消失。- 上传图像文件的请求,是由调用
XMLHttpRequest
的open()
函数发送 POST 请求完成的。 - 上传的 MIME 类型是通过调用
XMLHttpRequest
的overrideMimeType()
函数来设置的。这个例子中,我们使用通用 MIME 类型。是否需要设置 MIME 类型取决于你的具体使用情况。 FileReader
对象用于将文件转换为二进制字符串。- 最后,当内容被加载时,会调用
XMLHttpRequest
的send()
函数来上传文件内容。
异步处理文件上传
这个例子演示了如何异步上传文件,在服务器端使用了 PHP,在客户端使用了 JavaScript。
<?php
if (isset($_FILES['myFile'])) {
// 示例:
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); // 处理响应
}
};
fd.append('myFile', file);
// 初始化 multipart/form-data 上传
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;">将文件拖放到这里</div>
</div>
</body>
</html>
示例:用对象 URL 显示 PDF
对象 URL 可以用于图像之外的其他东西!它可以用于显示嵌入的 PDF 文件或任何其他浏览器能显示的资源。
在 Firefox 中,要让 PDF 嵌入式地显示在 iframe 中(而不是作为下载的文件弹出),必须将 pdfjs.disabled
设为 false
。
<iframe id="viewer"></iframe>
这是 src
属性的改动:
const obj_url = URL.createObjectURL(blob);
const iframe = document.getElementById("viewer");
iframe.setAttribute("src", obj_url);
URL.revokeObjectURL(obj_url);
示例:将对象 URL 用于其他文件类型
你可以用同样方式操作其他格式的文件。这是预览上传的视频的方法:
const video = document.getElementById("video");
const obj_url = URL.createObjectURL(blob);
video.src = obj_url;
video.play();
URL.revokeObjectURL(obj_url);