整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

「HTML5 进阶」FileAPI 文件操作实战,内

「HTML5 进阶」FileAPI 文件操作实战,内附详细案例,建议收藏

ileAPI

介绍

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的支持不完善");
}

监听文件选择

对于 typefile 类型的 <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类似,在读取文件之前,需要先绑定事件处理。它读取操作有:readAsArrayBufferreadAsDataURLreadAsBinaryStringreadAsText。传入的参数就是 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

大文件分片读取

在对于超大文件,一般采用分片上传的思路解决。文章开头有讲到,FileBlob 的一个特例。而 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 代码:

<!DOCTYPE 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

著作权归作者所有。

关注

感谢阅读,如果这篇文章对你又帮助,记得 点赞收藏转发 哟。

期待下次与你相遇 :)

tml5文件分割上传解决方案

html5提供的文件API中可以轻松的对文件进行分割切片, 然后通过javascript异步处理向服务器传输数据, 突破对大文件上传的限制, 同时异步处理在一定程度上也提高了文件上传的效率。用户体验上也优于前述方案。

index.html

<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title>大文件上传实例</title>
<script type="text/javascript">
    const BYTES_PER_CHUNK=1024 * 1024; // 每个文件切片大小定为1MB .
    var slices;
    var totalSlices;
    //发送请求
    function sendRequest() {
        var blob=document.getElementById('file').files[0];
        var start=0;
        var end;
        var index=0;
        // 计算文件切片总数
        slices=Math.ceil(blob.size / BYTES_PER_CHUNK);
        totalSlices=slices;
        while(start < blob.size) {
        end=start + BYTES_PER_CHUNK;
        if(end > blob.size) {
            end=blob.size;
        }
        uploadFile(blob, index, start, end);
        start=end;
        index++;
        }
    }
    //上传文件
    function uploadFile(blob, index, start, end) {
        var xhr;
        var fd;
        var chunk;
        xhr=new XMLHttpRequest();
        xhr.onreadystatechange=function() {
        if(xhr.readyState==4) {
        if(xhr.responseText) {
            alert(xhr.responseText);
        }
        slices--;
        // 如果所有文件切片都成功发送,发送文件合并请求。
        if(slices==0) {
            mergeFile(blob);
            alert('文件上传完毕');
        }
        }
        };
        chunk=blob.slice(start,end);//切割文件
        //构造form数据
        fd=new FormData();
        fd.append("file", chunk);
        fd.append("name", blob.name);
        fd.append("index", index);
        xhr.open("POST", "upload.php", true);
        //设置二进制文边界件头
        xhr.setRequestHeader("X_Requested_With", location.href.split("/")[3].replace(/[^a-z]+/g, '$'));
        xhr.send(fd);
    }
    function mergeFile(blob) {
        var xhr;
        var fd;
        xhr=new XMLHttpRequest();
        fd=new FormData();
        fd.append("name", blob.name);
        fd.append("index", totalSlices);
        xhr.open("POST", "merge.php", true);
        xhr.setRequestHeader("X_Requested_With", location.href.split("/")[3].replace(/[^a-z]+/g, '$'));
        xhr.send(fd);
    }
</script>
</head>
<body>
<input type="file" id="file"/>
<button onclick="sendRequest()">上传</button>
</body>
</html>

upload.php

<?php
//省略了文件接收判断isset部分
//当前目录下建立一个uploads文件夹
//接收文件名时进行转码,防止中文乱码。
$target="uploads/" .iconv("utf-8","gbk",$_POST["name"]) . '-' . $_POST['index'];
move_uploaded_file($_FILES['file']['tmp_name'], $target);
// Might execute too quickly.
sleep(1);
?>

merge.php

<?php
//文件合并
$target="uploads/" .iconv("utf-8","gbk",$_POST["name"]);
$dst=fopen($target, 'wb');
for($i=0; $i < $_POST['index']; $i++) {
$slice=$target . '-' . $i;
$src=fopen($slice, 'rb');
stream_copy_to_stream($src, $dst);
fclose($src);
unlink($slice);
}
fclose($dst);

关键函数stream_copy_to_stream()

int stream_copy_to_stream ( resource $source , resource $dest [, int $maxlength=-1 [, int $offset=0 ]] )

<?php
$src=fopen('http://www.example.com', 'r');
$dest1=fopen('first1k.txt', 'w');
$dest2=fopen('remainder.txt', 'w');
echo stream_copy_to_stream($src, $dest1, 1024) . " bytes copied to first1k.txt\n";
echo stream_copy_to_stream($src, $dest2) . " bytes copied to remainder.txt\n";
?>
var blob=document.getElementById('file').files[0];
console.dir(blob);

相关的属性如下:

lastModified: 1511081596000

lastModifiedDate: Sun Nov 19 2017 16:53:16 GMT+0800 (中国标准时间) {}

name: "IMG_20171119_165316.jpg"

size: 4383101

type: "image/jpeg"

slice: ? slice() 用于切割文件

HTML5 中提供的文件API在前端中有着丰富的应用,上传、下载、读取内容等在日常的交互中很常见。而且在各个浏览器的兼容也比较好,包括移动端,除了 IE 只支持 IE10 以上的版本。想要更好地掌握好操作文件的功能,先要熟悉每个API。

FileList 对象和 file 对象

HTML 中的 input[type="file"] 标签有个 multiple 属性,允许用户选择多个文件,FileList对象则就是表示用户选择的文件列表。这个列表中的每一个文件,就是一个 file 对象。

file 对象的属性:

  • name : 文件名,不包含路径。
  • type : 文件类型。图片类型的文件都会以 image/ 开头,可以由此来限制只允许上传图片。
  • size : 文件大小。可以根据文件大小来进行其他操作。
  • lastModified : 文件最后修改的时间。
<input type="file" id="files" multiple>
<script>
 var elem=document.getElementById('files');
 elem.onchange=function (event) {
 var files=event.target.files;
 for (var i=0; i < files.length; i++) {
 // 文件类型为 image 并且文件大小小于 200kb
 if(files[i].type.indexOf('image/') !==-1 && files[i].size < 204800){
 console.log(files[i].name);
 }
 }
 }
</script>

input 中有个 accept 属性,可以用来规定能够通过文件上传进行提交的文件类型。

accept="image/*" 可以用来限制只允许上传图像格式。但是在 Webkit 浏览器下却出现了响应滞慢的问题,要等上好几秒才弹出文件选择框。

解决方法就是将 * 通配符改为指定的 MIME 类型。

<input type="file" accept="image/gif,image/jpeg,image/jpg,image/png">

Blob 对象

Blob 对象相当于一个容器,可以用于存放二进制数据。它有两个属性,size 属性表示字节长度,type 属性表示 MIME 类型。

如何创建

Blob 对象可以使用 Blob() 构造函数来创建。

var blob=new Blob(['hello'], {type:"text/plain"});

Blob 构造函数中的第一个参数是一个数组,可以存放 ArrayBuffer对象、ArrayBufferView 对象、Blob对象和字符串。

Blob 对象可以通过 slice() 方法来返回一个新的 Blob 对象。

var newblob=blob.slice(0,5, {type:"text/plain"});

slice() 方法使用三个参数,均为可选。第一个参数代表要从Blob对象中的二进制数据的起始位置开始复制,第二个参数代表复制的结束位置,第三个参数为 Blob 对象的 MIME 类型。

canvas.toBlob() 也可以创建 Blob 对象。toBlob() 使用三个参数,第一个为回调函数,第二个为图片类型,默认为 image/png,第三个为图片质量,值在0到1之间。

var canvas=document.getElementById('canvas');
canvas.toBlob(function(blob){ console.log(blob); }, "image/jpeg", 0.5);

下载文件

Blod 对象可以通过 window.URL 对象生成一个网络地址,结合 a 标签的 download 属性来实现下载文件功能。

比如把 canvas 下载为一个图片文件。

var canvas=document.getElementById('canvas');
canvas.toBlob(function(blob){
 // 使用 createObjectURL 生成地址,格式为 blob:null/fd95b806-db11-4f98-b2ce-5eb16b38ba36
 var url=URL.createObjectURL(blob);
 var a=document.createElement('a');
 a.download='canvas';
 a.href=url;
 // 模拟a标签点击进行下载
 a.click();
 // 下载后告诉浏览器不再需要保持这个文件的引用了
 URL.revokeObjectURL(url);
});

也可以将字符串保存为一个文本文件,方法类似。

FileReader 对象

FileReader 对象主要用来把文件读入内存,并且读取文件中的数据。通过构造函数创建一个 FileReader 对象

var reader=new FileReader();

该对象有以下方法:

  • abort:中断读取操作。
  • readAsArrayBuffer:读取文件内容到ArrayBuffer对象中。
  • readAsBinaryString:将文件读取为二进制数据。
  • readAsDataURL:将文件读取为data: URL格式的字符串。
  • readAsText:将文件读取为文本。

上传图片预览

在常见的应用就是在客户端上传图片之后通过 readAsDataURL() 来显示图片。

<input type="file" id="files" accept="image/jpeg,image/jpg,image/png">
<img src="blank.gif" id="preview">
<script>
 var elem=document.getElementById('files'),
 img=document.getElementById('preview');
 elem.onchange=function () {
 var files=elem.files,
 reader=new FileReader();
 if(files && files[0]){
 reader.onload=function (ev) {
 img.src=ev.target.result;
 }
 reader.readAsDataURL(files[0]);
 }
 }
</script>

但是在一些手机上竖着拍照上传照片时会有bug,会发现照片倒了,包括三星和iPhone。。。解决方案这里不做讲解,有兴趣可以查看:移动端图片上传旋转、压缩的解决方案

数据备份与恢复

FileReader 对象的 readAsText() 可以读取文件的文本,结合 Blob 对象下载文件的功能,那就可以实现将数据导出文件备份到本地,当数据要恢复时,通过 input 把备份文件上传,使用 readAsText() 读取文本,恢复数据。

代码跟上面功能类似,这里不重复,具体的应用可以参考:notepad

Base64 编码

在 HTML5 中新增了 atob 和 btoa 方法来支持 Base64 编码。它们的命名也很简单,b to a 和 a to b,即代表着编码和解码。

var a="https://lin-xin.github.io";
var b=btoa(a);
var c=atob(b);
console.log(a); // https://lin-xin.github.io
console.log(b); // aHR0cHM6Ly9saW4teGluLmdpdGh1Yi5pbw==console.log(c); // https://lin-xin.github.io

btoa 方法对字符串 a 进行编码,不会改变 a 的值,返回一个编码后的值。atob 方法对编码后的字符串进行解码。

但是参数中带中文,已经超出了8位ASCII编码的字符范围,浏览器就会报错。所以需要先对中文进行 encodeURIComponent 编码处理。

var a="哈喽 世界";
var b=btoa(encodeURIComponent(a));
var c=decodeURIComponent(atob(b));
console.log(b); // JUU1JTkzJTg4JUU1JTk2JUJEJTIwJUU0JUI4JTk2JUU3JTk1JThD
console.log(c); // 哈喽 世界

https://zhuanlan.zhihu.com/p/27677175