I’LL BE HERE

In solitude, where we are least alone.

在浏览器中用 JavaScript 上传文件 (1/3) 前端的代码

一些记录与例子。

  • Part 1/3:前端的代码【这一篇】
  • Part 2/3:multipart/form-data 的 POST 请求
  • Part 3/3:后端的代码(Golang 版)

1 客户端的效果

▶︎ **例子 1-1:**简单的演示。其中,“提交”按钮不能真正提交文件,而只作演示用(因为没有服务器)。

例子 1-1 演示:一次性上传多个文件


◀︎

以下是基本知识点。

2 文件选择元素

▶︎ **例子 2-1(<input type='file'>):**在上面的例子 1-1 中,用户点击“添加更多文件”,就能调出文件选择窗口;这是用 input 实现的。 input 是一种 HTML 元素。当 input 的 type 属性被设置为 file,即:

<!-- 例子 2-1 代码:文件选择元素 -->
<input type='file'>

就可以选择文件了。效果如下:

◀︎

为了后续讨论方便,定义:

  • type 属性为 file 的 input 元素被称为 文件选择元素
  • 点击文件选择元素自带的按钮之后,跳出来的窗口被称为 文件选择窗口

注意:input 是不需要关闭标签的;换言之,

<!-- input 元素的不规范写法 -->
<input type='file'></input> 

这样的写法是不规范的。

2.1 multiple 属性:一次选择多个文件

文件选择元素的一个重要属性是 multiple,它决定了之后跳出的文件选择窗口是否可以选择多个文件。 带 multiple 的文件选择元素可以选择多个文件。下面是例子:

▶︎ 例子 2-2:

<!-- 例子 2-2 代码:可以选择多个文件的文件选择元素 -->
<input type='file' multiple>

效果:

上面这个文件选择元素允许使用者选择多个文件,来试一试吧。 ◀︎

2.2 click 方法:用 JavaScript 调出文件选择窗口

多数时候,出于美观考虑,我们希望用自定义的按钮调出文件选择窗口,也就是自己搞一个按钮,然后一点这个按钮,就可以让我们选择文件。 为了实现这一需求,可以先通过增加 display:none; 样式,把文件选择元素隐藏起来; 然后需要调出文件选择窗口的时候,通过 JavaScript 调用文件选择元素的 click 方法。 以下是例子:

▶︎ 例子 2-3:

<!-- 例子 2-3 代码:用 JavaScript 调出文件选择窗口 -->

<!-- 自定义样式的按钮: -->
<div
	id='elemButton'
	style='cursor:pointer;max-width:15em;border:1px solid black;border-radius:5px;background:#3af;color:#fff;text-align:center;margin:0 auto;padding:5px;'
>
点我选择文件
</div>

<!-- 被隐藏的文件选择元素: -->
<input
	id='elemFileSelector'
	type='file'
	style='display:none;'
	multiple
>

<script>
document.getElementById('elemButton').addEventListener('click', function() {
	// 调用 click 方法,调出文件选择窗口
	document.getElementById('elemFileSelector').click();
});
</script>

效果如下:

点我选择文件

只要点击上面的按钮,就可以调出文件选择窗口了。 ◀︎

2.3 change 事件

当用户通过文件选择元素选中了新的文件时,change 事件会被触发。

有时候,我们希望用户选中文件以后,系统能弹出一个对话框,上面有“您已经选择了文件!”的提示。为了实现这一需求,我们可以把弹出对话框的代码写到一个匿名函数里,再把这个匿名函数通过 addEventListener 绑定为 change 事件的回调函数。

▶︎ 例子 2-4:change 事件演示

<input type='file' id='elemFileSelector'>

<script>
document.getElementById('elemFileSelector').addEventListener('change', function() {
	alert('您已经选择了文件!');
});
</script>

效果如下:

◀︎

2.4 files 属性、FileList 对象、File 对象

选中文件之后,可以用 JavaScript 通过文件选择元素的 files 属性来访问被选中的文件的信息。 以下是一个试验:

试验:通过 files 属性获取选中文件的信息

准备:需要有一个能打开开发者面板的 PC 端浏览器。

  1. 选文件。上面是一个 id 属性为 elemFileSelector2 文件选择元素。首先点击按钮,然后选择 3 个文件。
  2. 然后,打开开发者面板的控制台。
  3. 接着,执行下面这一行代码。这行代码的作用是,将 elemFileSelector2files 属性值打印到控制台上。
console.log(document.getElementById('elemFileSelector2').files);
  1. 最后,观察输出结果。

输出截图如下(会根据浏览器的不同而有样式上的不同):

试验结果截图

截图中的输出结果是一个有着 3 个 File 对象的 FileList 对象。 也就是说,文件选择元素的 files 属性的值,是一个 FileList。

FileList 的访问方法与数组类似,都是通过方括号算符来访问的。 比如说,如果想把首个File对象输出到控制台上,可以这样:

var selectedFiles = document.getElementById('elemFileSelector').files; // 其中,elemFileSelector 为文件选择元素的 id。

// selectedFiles 的类型是 FileList,
// selectedFiles[0] 的类型是 File。
console.log(selectedFiles[0]);

FileList 不是数组,没有 push 和 pop 之类的方法。不过,FileList 也有 length 属性。 也就是说:

console.log(selectedFiles.length);

这行代码可以输出选中了多少文件。

FileList 里面是一堆 File 对象。每个 File 对象中,有这四个属性:

  • lastModified:文件最后一次被修改的时间。是数字。以毫秒为单位的 UNIX 时间戳。
  • name:文件名。
  • size:文件的大小。数字。以字节为单位。
  • type:文件的 MIME 类型。其值由且只由文件后缀决定。比如说,有一个 png 图象文件,但是这个图象文件的后缀是 .txt,那么其对应 File 对象的 type 值就是 text/plain,而不是 image/png

这四个属性都是只读的。

2.5 读取 File 对象的文件内容(略)

上述的四个属性都是用来描述文件的元信息的。文件里面的内容无法光靠这四个属性读取。想要读取 File 对象对应的文件内容,可以这样:

// 假设 fileObject 是一个 File 对象:

fileObject.arrayBuffer().then(function(buf) {
	var byteArray = new Uint8Array(buf);
});

上面的代码构建了一个名为 byteArray、类型为 Uint8Array 的变量。 Uint8Array 为 fileObject 所指的 ArrayBuffer 提供数据视图,允许程序员对那一块 ArrayBuffer 进行读写。(待查) 想要查看 byteArray 里面的内容,可以用 console.log 输出:

fileObject.arrayBuffer().then(function(buf) {
	var byteArray = new Uint8Array(buf);
	console.log(byteArray);
});

3 异步上传文件:XMLHttpRequestFormData

假设现在有一个名为 fileObject 的变量,指向一个 File 对象。通过以下代码,可以把这个对象代表的文件通过 POST 方法上传到 http://target-url.com/

// 方法 1:

var formData = new FormData();
formData.append('fileobject', fileObject); // 将 fileObject 包装进 formData 中。

var xhr = new XMLHttpRequest();
xhr.open('POST', 'http://target-url.com');
xhr.send(formData);

在大多数新版本的浏览器中,也可以直接把 fileObject 作为 xhr.send 的参数:

// 方法 2:

var xhr = new XMLHttpRequest();
xhr.open('POST', 'http://target-url.com/');
xhr.send(fileObject);

这两种方法的一些区别:

方法 1 - xhr.send(<FormData>) 方法 2 - xhr.send(<File>)
请求体 multipart/form-data 普通的 Post Request,文件的内容作为 Request Payload
浏览器支持 Chrome 6+(更广泛的支持) Chrome 22+

具体的区别,将在下一篇(Part 2/3)记录。

旁注:XMLHttpRequestprogress 事件

有时候,我们会想要得知文件上传的进度,比如做一个进度条之类的。这个需求可以通过监听 XMLHttpRequest 的 progress 事件来实现。具体方法见 MDN Doc - Progress Event

4 总结:关键概念与链接