FileAPI
介绍
HTML5 为我们提供了 File API 相关规范。主要涉及 File 接口 和 FileReader 对象 。
本文整理了兼容性检测、文件选择、属性读取、文件读取、进度监控、大文件分片上传以及拖拽上传等开发中常见的前端文件操作。
准备工作
首先,我们的 File 来自于 <input> 标签中选中的文件列表。所以,准备如下的 HTML 代码:
<input type="file" id="files" multiple /> | |
<div id="list"></div> | |
<div id="images"></div> | |
<!-- File API相关操作写在了script.js中 --> | |
<script src="./script.js"></script> |
检测兼容性
File 对象是特殊类型的 Blob。在 script 入口处,应该检测当前浏览器是否支持 File API:
if (!(window.File && window.FileReader && window.FileList && window.Blob)) { | |
throw new Error("当前浏览器对FileAPI的支持不完善"); | |
} |
监听文件选择
对于 type 为 file 类型的 <input> 标签,在选择文件的时候,会触发 change 事件。用户选中的文件信息也会传入回调函数的第一个参数中。
function handleFileSelect(event) { | |
const { files } = event.target; | |
if (!files.length) { | |
console.log("没有选择文件"); | |
return; | |
} | |
console.log("选中的文件信息是:", files); | |
} | |
document | |
.querySelector("#files") | |
.addEventListener("change", handleFileSelect, false); |
文件属性-File
event.target.files 是一个 FileList 对象,它是一个由 File 对象组成的列表。
每个 File 对象,保存着选中的对应文件的属性。常用的用:
- name:文件名
- type:文件类型
- size:文件大小
下面,通过 type 属性,过滤掉非图片类型的文件,只展示图片类型文件的信息:
function handleFileSelect(event) { | |
const { files } = event.target; | |
if (!files.length) { | |
console.log("没有选择文件"); | |
return; | |
} | |
const innerHTML = []; | |
const reImage = /image.*/; | |
for (let file of files) { | |
if (!reImage.test(file.type)) { | |
continue; | |
} | |
innerHTML.push( | |
` | |
<li> | |
<strong>${file.name}</strong> | |
(${file.type || "n/a"}) - | |
${file.size} bytes | |
</li> | |
` | |
); | |
} | |
document.querySelector("#list").innerHTML = `<ul>${innerHTML.join("")}</ul>`; | |
} |
FileReader
读取文件-FileReader
还是以图片读取为例,读取并且显示所有的图片类型文件。
文件读取需要使用 FileReader 对象,它常用 3 个回调方法:
- onload: 文件读取完成
- onloadstart:文件上传开始
- onprogress : 文件上传中触发
和Image类似,在读取文件之前,需要先绑定事件处理。它读取操作有:readAsArrayBuffer、readAsDataURL、readAsBinaryString、readAsText。传入的参数就是 File 对象。
那么这几个方法有什么区别呢?不同的读取方式,回调事件onload接受到的event.target.result不相同。比如,readAsDataURL 读取的话,result 是一个图片的 url。
下面就是读取图片文件,然后展示的一个例子:
function handleFileSelect(event) { | |
let { files } = event.target; | |
if (!files.length) { | |
return; | |
} | |
let vm = document.createDocumentFragment(), | |
re = /image.*/, | |
loaded = 0, // 完成加载的图片数量 | |
total = 0; // 总共图片数量// 统计image文件数量for (let file of files) { | |
re.test(file.type) && total++; | |
} | |
// onloadstart回调const handleLoadStart = (ev, file) => | |
console.log(`>>> Start load ${file.name}`); | |
// onload回调const handleOnload = (ev, file) => { | |
console.log(`<<< End load ${file.name}`); | |
const img = document.createElement("img"); | |
img.height = 250; | |
img.width = 250; | |
img.src = ev.target.result; | |
vm.appendChild(img); | |
// 完成加载后,将其放入dom元素中if (++loaded === total) { | |
document.querySelector("#images").appendChild(vm); | |
} | |
}; | |
for (let file of files) { | |
if (!re.test(file.type)) { | |
continue; | |
} | |
const reader = new FileReader(); | |
reader.onloadstart = ev => handleLoadStart(ev, file); | |
reader.onload = ev => handleOnload(ev, file); | |
// 读取文件对象 | |
reader.readAsDataURL(file); | |
} | |
} | |
document | |
.querySelector("#files") | |
.addEventListener("change", handleFileSelect, false); |
监控读取进度
在监控读取进度的时候,主要是处理 FileReader 对象上的 onprogress 事件。
下面的例子,请打开一个较大的文件来查看效果(否则一下就读取完了):
function handleFileSelect(event) { | |
let { files } = event.target; | |
if (!files.length) { | |
return; | |
} | |
const handleLoadStart = (ev, file) => | |
console.log(`>>> Start load ${file.name}`); | |
const handleProgress = (ev, file) => { | |
if (!ev.lengthComputable) { | |
return; | |
} | |
// 计算进度,并且以百分比形式展示const percent = Math.round((ev.loaded / ev.total) * 100); | |
console.log(`<<< Loding ${file.name}, progress is ${percent}%`); | |
}; | |
for (let file of files) { | |
const reader = new FileReader(); | |
reader.onloadstart = ev => handleLoadStart(ev, file); | |
reader.onprogress = ev => handleProgress(ev, file); | |
reader.readAsArrayBuffer(file); | |
} | |
} | |
document | |
.querySelector("#files") | |
.addEventListener("change", handleFileSelect, false); |
slice
大文件分片读取
在对于超大文件,一般采用分片上传的思路解决。文章开头有讲到,File 是 Blob 的一个特例。而 Blob 上有一个 slice (
https://developer.mozilla.org/zh-CN/docs/Web/API/Blob/slice)方法,通过它,前端就可以实现分片读取大文件的操作。
为了方便说明,请先准备好一个 txt 文件,文件内容就是:hello world。
示例代码如下,代码中只读取前 5 个字节,由于每个英文字母占 1 个字节,所以打印结果应该是“hello”。
function handleFileSelect(event) { | |
let { files } = event.target; | |
if (!files.length) { | |
return; | |
} | |
// 为了方便说明,这里仅仅读取第一个文件const file = files[0]; | |
// 读取前5个字节的内容const blob = file.slice(0, 5); | |
const reader = new FileReader(); | |
// 控制台输出结果应该是:hello | |
reader.onload = ev => console.log(ev.target.result); | |
reader.readAsText(blob); | |
} | |
document | |
.querySelector("#files") | |
.addEventListener("change", handleFileSelect, false); |
拖拽上传
和前面所述的 File API 相关是完全一样的。唯一需要特殊处理的是文件对象的获取入口改变了。对于 <input> 标签,监听 onchange 事件,FileList 存放在 event.target.files 中;对于拖拽操作,FileList 存放在拖拽事件的回调函数参数里,通过 event.dataTransfer.files 访问即可。
需要修改一下 html 代码:
<head><meta charset="UTF-8"><style> | |
#container { | |
width: 300px; | |
height: 300px; | |
border: 3px dotted red; | |
} | |
</style> | |
</head> | |
<body><div id="container"></div><script src="./script.js"></script> | |
</body> | |
</html> |
脚本文件的代码如下:
function handleDropover(event) { | |
event.stopPropagation(); | |
event.preventDefault(); | |
} | |
function handleDrop(event) { | |
event.stopPropagation(); | |
event.preventDefault(); | |
/***** 访问拖拽文件 *****/ | |
const files = event.dataTransfer.files; | |
console.log(files); | |
/**********/ | |
} | |
const target = document.querySelector("#container"); | |
target.addEventListener("dragover", handleDropover); | |
target.addEventListener("drop", handleDrop); |
后端相关
后端相关超出了本文的讨论范围,可以参考这篇文章(
https://github.com/purplebamboo/blog/issues/17)。
版权
作者:心谭
链接:
https://juejin.im/post/5d35af63e51d454fa33b199e
著作权归作者所有。
关注
感谢阅读,如果这篇文章对你又帮助,记得 点赞 ,收藏,转发 哟。
期待下次与你相遇 :)