文章目录
谈谈JS二进制:File、Blob、FileReader、ArrayBuffer、Base64
Blob、File、ArrayBuffer、TypedArray、DataView究竟应该如何应用
Blob
Blob 全称为 binary large object ,即二进制大对象,它是 JavaScript 中的一个对象,表示原始的类似文件的数据。下面是 MDN 中对 Blob 的解释:
Blob 对象表示一个不可变、原始数据的类文件对象。它的数据可以按文本或二进制的格式进行读取,也可以转换成 ReadableStream 来用于数据操作。
blob的创建
可以使用 Blob() 构造函数来创建一个 Blob:
new Blob(array, options);
其有两个参数:
- array:由
ArrayBuffer
、ArrayBufferView
、Blob
、DOMString
等对象构成的,将会被放进 Blob; - options:可选的 BlobPropertyBag 字典,它可能会指定如下两个属性
- type:将会被存储到 blob 中的数据的 MIME 类型。默认值是空字符(“”)。
- endings:默认值为"transparent",用于指定包含行结束符\n的字符串如何被写入,不常用。
type 常用
new Blob([_path], { type: 'application/pdf' }) pdf
let blob = new Blob([export_data], { type: 'application/vnd.ms-excel;charset=utf-8'}); fexcel
URL.createObjectURL(blob)
常见的 MIME 类型如下:
var blob = new Blob();
var blob = new Blob([]); console.log(blob);
var buffer = new ArrayBuffer(32);
var blob = new Blob([buffer]); console.log(blob);
var int8 = new Int8Array(10);
var blob = new Blob([int8]); console.log(blob);
var blob = new Blob(['大师哥王唯']); console.log(blob);
var newBlob = new Blob([blob]);
var aFileParts = ['<a id="a"><b>Web前端开发</b></a>'];
var blob = new Blob(aFileParts, { type: 'text/html' });
console.log(blob); // Blob
还可以通过其他对象创建Blob对象,如:
var person = { username: "王唯", sex: true, age: 18 };
var blob = new Blob([JSON.stringify(person, null, 2)], { type: 'application/json' }); console.log(blob);
Blob表示的不一定是JavaScript原生格式的数据,也可能是File对象,File接口基于Blob,继承了Blob的功能并将其扩展使其支持用户系统上的文件;
<input type="file" id="myfile">
window.onload = function () {
var myfile = document.getElementById("myfile");
myfile.onchange = function (event) {
var file = event.target.files[0];
console.log(file);
console.log(file instanceof Blob); // true
}
}
Blob属性:
size:只读,Blob对象中所包含数据的大小(字节);
type:只读,一个字符串,表明该Blob对象所包含数据的MIME类型;如果类型未知,则该值为空字符串;
Blob方法:
- text():返回一个promise且包含blob所有内容的UTF-8格式的字符串;
var blob = new Blob(['大师哥王唯']);
blob.text().then(value => console.log(value))
- arrayBuffer():返回一个promise且包含blob所有内容的二进制格式的ArrayBuffer;
var buffer = new ArrayBuffer(32);
var blob = new Blob([buffer]);
blob.arrayBuffer().then(buffer => console.log(buffer)); // ArrayBuffer(32)
Blob 分片
除了使用Blob()构造函数来创建blob 对象之外,还可以从 blob 对象中创建blob,也就是将 blob 对象切片。Blob 对象内置了 slice() 方法用来将 blob 对象分片,其语法如下:
const blob = instanceOfBlob.slice([start [, end [, contentType]]]};
其有三个参数:
- start:设置切片的起点,即切片开始位置。默认值为 0,这意味着切片应该从第一个字节开始;
- end:设置切片的结束点,会对该位置之前的数据进行切片。默认值为blob.size;
- contentType:设置新 blob 的 MIME 类型。如果省略 type,则默认为 blob 的原始值。
下面来看例子:
const iframe = document.getElementsByTagName("iframe")[0];
const blob = new Blob(["Hello World"], {type: "text/plain"});
const subBlob = blob.slice(0, 5);
iframe.src = URL.createObjectURL(subBlob);
此时页面会显示"Hello"。
blob和其他类型转换图
Blob主要用于大量API需要进行二进制数据交换场景,为这些应用提供了通用、高效的数据交换机制,如图:
获取Blob对象的方法
- message事件从其他窗口或者线程中获取Blob;
- 可以从客户端数据库中获取Blob;
- 使用XHR2,从Web中下载Blob;
- fetch请求响应对象的 blob()方法;
- File对象,它是Blob的子类;
- 使用xhr获取blob
const url = src; // 替换为实际文件的URL
// 使用XMLHttpRequest发送GET请求
const xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = 'blob'; // 设置响应类型为二进制数据
xhr.onload = function () {
if (xhr.status === 200) {
const blob = new Blob([xhr.response]);
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = name; // 替换为要保存的文件名
link.click();
URL.revokeObjectURL(link.href);
}
};
xhr.send();
一旦获取了Blob对象,就可以对其进行很多的操作,如:
可以使用postMessage()方法向其他窗口或Worker发送一个Blob;
可以将Blob存储在客户端数据库中;
可以通过将Blob传递给XHR对象的send()方法,来将该Blob上传到服务端;
可以使用URL.createObjectURL()函数获取一个特殊的blob://URL,该URL代表Blob的内容,然后,将其和DOM或者CSS结合使用;
可以使用FileReader对象来异步地将一个Blob内容抽取成一个字符串或者ArrayBuffer;
可以使用File和FileWriter对象,来实现将一个Blob写入到一个本地文件中;
base64ToBlob(urlData, type) {
let arr = urlData.split(',')
let mime = arr[0].match(/:(.*?);/)[1] || type
// 去掉url的头,并转化为byte
let bytes = window.atob(arr[1])
// 处理异常,将ascii码小于0的转换为大于0
let ab = new ArrayBuffer(bytes.length)
// 生成视图(直接针对内存):8位无符号整数,长度1个字节
let ia = new Uint8Array(ab)
for (let i = 0; i < bytes.length; i++) {
ia[i] = bytes.charCodeAt(i)
}
return new Blob([ab], {
type: mime,
})
},
XMLHttpRequest中的Blob:
在HTML 5中,可以通过XML HttpRequest对象的send方法向服务器端发送Blob对象,因为所有File对象(代表一个文件)都是一个Blob对象,所以同样可以通过发送Blob对象的方式来上传文件,如:
//向服务器发送blob对象
function uploadDocument() {
var bb = new Blob([document.documentElement.outerHTML], { type: "text/html" });
var xhr = new XMLHttpRequest();
xhr.open('POST', 'sendblob.php?fileName=' + getFileName());
var progressBar = document.getElementById('progress');
xhr.upload.onprogress = function (e) {
if (e.lengthComputable) {
progressBar.value = (e.loaded / e.total) * 100;
console.log("上传成功");
}
}
console.log(bb); xhr.send(bb);
}
// 获取当前页面文件的文件名
function getFileName() {
var url = window.location.href;
var pos = url.lastIndexOf("\\");
if (pos == -1) {
// pos==-1表示为本地文件
pos = url.lastIndexOf("/");
// 本地文件路径分割符为"/"
var fileName = url.substring(pos + 1);
// 从url中获得文件名
return fileName
}
var btnUpload = document.getElementById("btnUpload");
btnUpload.addEventListener("click", uploadDocument);
XHR2中的responseType可以指定为blob,以便于从服务器接收Blob数据,如:
// 以Blob的形式获取URL指定的内容,并将其传递给指定的回调函数
function getBlobData(url, callback) {
var xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.responseType = "blob";
xhr.onload = function () {
callback(xhr.response);
};
xhr.send(null);
}
// 如果要下载的数据量很大,想要显示一条进度条,可以使用onprogress事件
function doBlobHandler(blob) {
// console.log(blob);
//Blob
var url = URL.createObjectURL(blob);
var img = document.createElement("img");
img.src = url; document.body.appendChild(img);
}
var btnGetBlob = document.getElementById("btnGetBlob");
btnGetBlob.addEventListener("click", function () {
getBlobData("getBlob.php", doBlobHandler)
})
File对象接口
File继承自Blob,是特殊类型
的 Blob,且可以用在任意的Blob类型的context中;
js的blob文件对象无法直接设置extName等文件信息,MultipartFile file 接收其实时File对象。或者使用FormData对象的第三个参数,设置文件名
let formData = new FormData();
formData.append('file', blob, '123.pdf');
如下:
- FileReader
- URL.createObjectURL()
- createImageBitmap()及XMLHttpRequest.send()都能处理Blob和File
- File构造函数:File(bits, name[, options])
对象参数:
bits:一个包含ArrayBuffer、ArrayBufferView、Blob,或者DOMString对象的Array,或者任何这些对象的组合;即为UTF-8编码的文件内容;
name:USVString,表示文件名称,或者文件路径;
options:可选,一个选项对象,包含文件的可选属性;可用的选项如下:
type:DOMString,表示将要放到文件中的内容的MIME类型,默认值为 “”;
lastModified:数值,表示文件最后修改时间的 Unix 时间戳(毫秒),默认值为 Date.now();
var file = new File(["zeronetwork"], "myfiles/demo.txt",{type:"text/plain",lastModified: 1654300000000});
File对象属性:
lastModified属性:只读,返回当前File对象所引用文件的最后修改时间,自UNIX时间起始以来的毫秒数;
lastModifiedDate属性:只读,返回当前File对象所引用文件最后修改时间的Date对象;
name:只读,返回当前File对象所引用的本地文件名字,但由于安全原因,返回的值并不包含文件路径;
webkitRelativePath非标准属性:只读,返回File相关的path或URL;
size:以字节为单位返回文件的大小;
type属性,只读,返回文件的MIME Type;
console.log(file.name); // example.txt
console.log(file.lastModified); // 1649726357207// Tue Apr 12 2022 09:19:17 GMT+0800
console.log(file.lastModifiedDate);
console.log(file.size); // 15
console.log(file.type); // text/plain
console.log(file.webkitRelativePath); // ""
对于type属性,浏览器不会实际读取文件的字节流,来判断它的媒体类型;它只基于文件扩展名;而且, type属性仅仅对常见文件类型可靠,如图像、文档、音频和视频;不常见的文件扩展名会返回空字符串;
示例:显示选择文件信息
<div><input type="file" id="myFiles" name="myFiles" multiple><br />共选择 <span id="fileNum">0</span> 个文件,共 <span
id="fileSize">0</span></div>
<script>
window.onload = function () {
var myFiles = document.getElementById("myFiles");
myFiles.onchange = function (event) {
var nBytes = 0,
oFiles = event.target.files,
nFiles = oFiles.length;
for (var i = 0; i < nFiles; i++) {
nBytes += oFiles[i].size;
}
var sOutput = nBytes + " bytes";
var aMultiples = ["KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"];
for (var nMultiple = 0, nApprox = nBytes / 1024; nApprox > 1;
nApprox /= 1024, nMultiple++) {
sOutput = nApprox.toFixed(3) + " " + aMultiples[nMultiple] + " (" + nBytes + " bytes)";
}
document.getElementById("fileNum").innerHTML = nFiles; document.getElementById("fileSize").innerHTML = sOutput;
}
}
</script>
File API:
通常情况下,File对象是来自用户在一个元素上选择文件后files属性返回的FileList集合(或者说是一个类数组)对象,如:
<input type="file" id="myfile" multiple /><input type="button" value="获取文件" id="btn">
var myfile = document.getElementById("myfile");
console.log(myfile.files);
// FileList {length: 0}
var btn = document.getElementById("btn");
btn.addEventListener("click", function (event) {
console.log(myfile.files);
// FileList {length: 3}
});
FileList:
通常一个FileList对象来自于一个HTML的元素的files属性,它是HTML5新增的集合属性,其中包含了一组File对象,每个File对象对应着用户所选择的文件;如果元素具有multiple属性,用户可以选择多个文件,否则,该FileList只能包含一个文件;
var file = myfile.files[0];console.log(file); // File
FileList对象具有length属性,其返回列表中的文件数量;
for (var i = 0, len = myfile.files.length;
i < len; i++) {
var file = myfile.files[i];
console.log(file);
}
FileList对象还有个item(index)方法,其根据index索引值,返回FileList对象中对应的File对象;如:
var file = myfile.files[i];
// 或者
var file = myfile.files.item(i);
该FileList对象也有可能来自用户的拖放操作;
将Blob转换为File
// 假设你已经有了一个Blob对象
let blob = new Blob(['Hello World'], { type: 'text/plain' });
// 将Blob转换为File,设置文件名为example.txt
let file = new File([blob], 'example.txt');
// 现在file对象有一个name属性,你可以使用它
console.log(file.name); // 输出 "example.txt"
大文件切块处理的思路
使用 FileReader.readAsArrayBuffer() 读取文件,在读取成功后 result 属性中将包含一个 ArrayBuffer 对象以表示所读取文件的数据。
对读取到的 ArrayBuffer 对象进行拆分,放到一个数组中
使用 Promise.all 来顺序的处理拆分后的数组(保证顺序传输)
使用 Uint8Array 将 ArrayBuffer 拆后后的数组生成 二进制字节数组
最后将二进制字节数组转为 base64 编码的字符串
传输的是 base64 编码的字符串,到达目的地后,需要使用 base64 解码,才能得到原始的二进制字节信息。
有一个配置项:
小于 20 MB 算小文件,直接整个处理;
大于 20 MB 算大文件,进行切块处理;
大文件切块后,将依次按照切块的顺序上传
注意事项:最后从浏览器传输文件数据的时候,传输的是 base64 编码的字符串,因此传送到目的地后,需要 base64 解码后再进行操作
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
* {
padding: 0;
margin: 0;
}
body {
width: 100vw;
height: 100vh;
}
.drag-area-container {
width: 800px;
height: 800px;
border: 1px solid #0f0;
}
.highlight {
border: 10px solid #0f0;
}
</style>
</head>
<body>
<div class="drag-area-container">
</div>
<script>
(function() {
// 禁止浏览器的拖拽默认事件
var globalDragFile = function() {
let globalDragArea = document.querySelector('body')
globalDragArea.addEventListener('dragover',function(e){
e.preventDefault()
})
globalDragArea.addEventListener('drop', function(e) {
e.preventDefault()
})
}
// 识别指定区域的拖动效果
var localDragFile = function() {
let localDragArea = document.querySelector('.drag-area-container')
localDragArea.addEventListener('dragenter',function(e){
// 拖动文件到指定区域时,添加高亮
localDragArea.classList.add('highlight')
e.preventDefault()
})
localDragArea.addEventListener('dragover',function(e){
e.preventDefault()
})
localDragArea.addEventListener('drop', function(e) {
e.preventDefault()
// 去除高亮
localDragArea.classList.remove('highlight')
var file = e.dataTransfer.files[0]
console.log("file = ", file);
uploadFile(file)
})
}
// 处理文件 && 上传文件
var uploadFile = function(file) {
// 使用 FileReader 读取文件的数据
var reader = new FileReader()
reader.onloadend = function() {
var file_result = this.result // ArrayBuffer 数据对象
var file_length = file_result.byteLength
// 小于 20 MB 为小文件,则整个读取并上传
// 大于 20 MB 为大文件,则需要将它切成小块,分别上传
var step = 1024 * 1024 * 20
if (file_length < step) {
console.log("小文件,直接整个上传 ");
handleSmallFile(file_result)
} else {
console.log("大文件,切块分别上传 ");
var block_arr = splitBigFile(file_result, file_length, step)
handleBigFile(block_arr).then(function(results) {
console.log("大文件,切块上传成功 result = ", results)
})
}
}
reader.readAsArrayBuffer(file)
/*
readAsArrayBuffer() // 读取完成,result 属性中保存的将是被读取文件的 ArrayBuffer 数据对象。
readAsBinaryString() // 读取完成,result 属性中将包含所读取文件的原始二进制数据。
readAsDataURL() // 读取完成,result 属性中将包含一个 data: URL 格式的 Base64 字符串以表示所读取文件的内容。
readAsText() // 读取完成,result 属性中将包含一个字符串以表示所读取的文件内容。
*/
}
var handleSmallFile = function(file_result) {
// 先读取到 ArrayBuffer,再获取 ArrayBuffer 的 Uint8Array 字节数组形式,最后用 base64 编码字节数组用于传输。
var unit8_data = new Uint8Array(file_result) // 提取二进制字节数组,使用 Uint8Array 表示
var base64_data = binary2base64(unit8_data) // base64 编码
console.log("==== handle upload start ====");
console.log("data = ", base64_data);
console.log("==== handle upload end ====");
}
// 根据指定的 step 大小,切出来指定的 step 大小的块
var splitBigFile = function(file, file_length, step) {
var step_times = Math.ceil(file_length / step)
var start = 0
var block_arr = []
for (i = 0; i < step_times; i++) {
var block = file.slice(start, start + step)
start = start + step
block_arr.push(block)
}
return block_arr
}
var handleBigFile = async function(big_files) {
return Promise.all([].map.call(big_files, function(file, index) {
return new Promise(function(resolve, reject) {
// 先读取到 ArrayBuffer,再获取 ArrayBuffer 的 Uint8Array 字节数组形式,最后用 base64 编码字节数组用于传输。
var view = new Uint8Array(file) // 提取二进制字节数组,使用 Uint8Array 表示
var base64_data = binary2base64(view) // base64 编码
console.log("==== handle upload start ====");
console.log("block index = ", index);
console.log("data = ", base64_data);
console.log("==== handle upload end ====");
resolve("Promise file")
})
})).then(function(results) {
return results;
})
}
// 二进制字节数组转 base64 编码的字符串
var binary2base64 = function(bi) {
let str = '';
for (let i = 0, len = bi.length; i < len; i++) {
str += String.fromCharCode(bi[i]);
}
return btoa(str);
}
var __main = function() {
// 禁止浏览器的拖拽默认事件
globalDragFile()
// 识别指定区域的拖动效果
localDragFile()
}
__main()
})()
</script>
</body>
</html>
File类本身没有定义任何方法,但是它从Blob类继承了slice方法,针对大文件传输的场景,我们可以使用 slice 方法对大文件进行切割(上面的大文件分割是使用FileReader 把file转换为Buffer Array然后分割),然后分片进行上传;如下:
<input type="file" name="file" id="file"><button id="upload" onClick="upload()">上传</button>
var chunkSize = 1 * 1024 * 1024;
// 每个文件切片大小定为1MBvar totalChunk;
//发送请求
function upload() {
var file = document.getElementById("file").files[0];
var start = 0; var end; var index = 0;
var filesize = file.size; var filename = file.name;
//计算文件切片总数
totalChunk = Math.ceil(filesize / chunkSize);
console.log(totalChunk);
while (start < filesize) {
end = start + chunkSize;
// 匹配最后一个分片的情况
if (end > filesize) {
end = filesize;
}
var chunk = file.slice(start, end);
//切割文件// console.log(chunk);
// 值形式如:mytxt.txt0、mytxt.txt1...
var sliceIndex = file.name + "__" + index;
var formData = new FormData();
formData.append("file", chunk, sliceIndex);
// 如果是最后一个分片,服务端可以合并文件
if (end == filesize) {
formData.append("filename", file.name);
formData.append("totalchunk", totalChunk);
formData.append("done", true);
}
var xhr = new XMLHttpRequest();
xhr.open("POST", "postfile.php");
xhr.onload = function () {
console.log(xhr.response);
if (xhr.response && xhr.response.status == 4) {
console.log(xhr.response.filename + "上传成功")
}
}
xhr.responseType = "json";
xhr.send(formData); start = end; index++
}
}
带进度条的上传大文件:
<div id="progress">
<div id="finish" style="width: 0%;"></div>
</div>
<div><input type="file" name="file" id="upfile"><input type="button" value="停止" id="stop"></div>
#progress {
width: 300px;
height: 20px;
background-color: #f7f7f7;
margin-bottom: 20px;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
border-radius: 4px;
background-image: linear-gradient(to bottom, #f5f5f5, #f9f9f9);
}
#finish {
background-color: #149bdf;
background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
background-size: 40px 40px;
he
var upfile = document.getElementById("upfile");
var stopBtn = document.getElementById('stop');
var upload = new Upload(); upfile.onchange = function () {
upload.addFileAndSend(this);
}
stopBtn.onclick = function () {
this.value = "停止中"; upload.stop();
this.value = "已停止";
}
function Upload() {
var xhr = new XMLHttpRequest(); var LENGTH = 1 * 1024 * 1024;
var start = 0; var end = start + LENGTH; var file; var blob; var blobNum = 1;
var totalBlob = 0; var isStop = 0; var md5filename = '';
//对外方法,传入文件对象
this.addFileAndSend = function (that) {
file = that.files[0];
totalBlob = Math.ceil(file.size / LENGTH);
blob = cutFile(file); sendFile(blob, file);
blobNum += 1;
}
//停止文件上传
this.stop = function () {
xhr.abort(); isStop = 1;
}
// 重新开始
this.restart = function () {
sendFile(blob, file);
is_stop = 0;
}
//文件分片
function cutFile(file) {
var fileBlob = file.slice(start, end);
start = end; end = start + LENGTH; return fileBlob;
};
//发送文件
function sendFile(blob, file) {
var formData = new FormData();
formData.append('file', blob);
formData.append('blobNum', blobNum);
formData.append('totalBlob', totalBlob);
formData.append('fileName', file.name);
// formData.append('md5_file_name',md5filename);
xhr.open('POST', './uploadfile.php', false);
// 此处使用同步的
xhr.onreadystatechange = function () {
var per; var progressObj = document.getElementById('finish');
if (totalBlob == 1) { per = '100%'; }
else {
per = Math.min(100, (blobNum / totalBlob) * 100) + '%';
}
progressObj.style.width = per;
var t = setTimeout(function () {
if (start < file.size && isStop === 0) {
blob = cutFile(file);
sendFile(blob, file); blobNum++; console.log(start + ":" + blobNum + "/" + totalBlob);
} else {
clearTimeout(t); console.log("上传成功");
}
}, 1000);
}
xhr.send(formData);
}
}
读取多个文件,如:
<input type="file" id="myfile" multiple>
<div id="preview">
function readAndPreview(file) {
var preview = document.querySelector('#preview');
// 确保 `file.name` 符合我们要求的扩展名
if (/\.(jpe?g|png|gif)$/i.test(file.name)) {
var reader = new FileReader();
reader.addEventListener("load", function () {
var image = new Image();
image.height = 100;
image.title = file.name;
image.src = this.result;
preview.appendChild(image);
}, false);
reader.readAsDataURL(file);
}
}
window.onload = function () {
var myfile = document.getElementById("myfile");
myfile.onchange = function (event) {
var files = event.target.files;
if (files) {
[].forEach.call(files, readAndPreview);
}
}
}
例子:显示缩略图
function handleFiles(files) {
var dropbox = document.getElementById("dropbox");
for (var i = 0, len = files.length; i < len; i++) {
var file = files[i];
var imageType = /^image\//;
if (!imageType.test(file.type)) continue;
var img = document.createElement("img");
img.classList.add("img");
img.file = file; dropbox.appendChild(img);
// 此时,img没有src,即还不是真正的图片//
console.log(img);
var reader = new FileReader();
reader.onload = (function (aImg) {
return function (e) {
aImg.src = e.target.result;
};
})(img); reader.readAsDataURL(file);
}
}
FileReader:
FileReader对象允许JavaScript异步读取存储在用户计算机上的文件(或原始数据缓存区)的内容,使用File或Blob对象指定要读取的文件或数据;
FileReader经常被用于Web Worker中;
构造函数:FileReader():返回一个FileReader对象,没有参数;
var reader = new FileReader();console.log(reader); // FileReader
如:
var myfile = document.getElementById("myfile");
myfile.onchange = function () {
var reader = new FileReader();
reader.onload = function (event) {
console.log(event.target.result);
};
reader.readAsText(event.target.files[0]);
}
FileReader和其他类型转换图
FileReader对象属性:
- error属性:只读,表示在读取文件时发生的错误;
- result属性:只读,返回文件的内容,其仅在读取操作完成后才有效,并且,数据的格式取决于用哪个方法来启动读取操作;
- readyState属性:只读,表示FileReader状态,可能的值有:
- EMPTY(0):还没有加载任何数据;
- LOADING(1):数据正在被加载;
- DONE(2):已完成全部的读取请求
myfile.onchange = function () {
var reader = new FileReader(); reader.onload = function (event) {
console.log(reader.error); //
nullconsole.log(reader.readyState);
// 2
console.log(event.target.result);
// content
}; console.log(reader.readyState);
//0
reader.readAsText(event.target.files[0]);
}
FileReader事件:
- onload:处理load事件,即在读取操作完成时触发;
- onloadstart:在读取操作开始时触发;
- onloadend:在读取操作结束时触发;
- onprogress:在读取Blob时反复触发,大概间隔50ms左右(可以通过 progress 事件来监控文件的读取进度);
- onprogress 事件提供了两个属性:loaded(已读取量)和total(需读取总量)。
- onerror:处理error事件,即在读取操作发生错误时触发;
- onabort:处理abort事件,即在读取操作被中断时触发;
function handler(event){
console.log(event);}
reader.onabort = handler;
reader.onerror = handler;
reader.onload = handler;
reader.onloadstart = handler;
reader.onloadend = handler;
reader.onprogress = handler;
如:
<input type="file" id="myfile" multiple />
<div id="output"></div><progress id="progress" max="100"></progress>
<script>
var myfile = document.getElementById("myfile");
myfile.onchange = function (evt) {
var info = "",
output = document.getElementById("output"),
progress = document.getElementById("progress"),
files = evt.target.files, type = "default",
reader = new FileReader();
if (/image/.test(files[0].type)) {
reader.readAsDataURL(files[0]);
type = "image";
}
else {
reader.readAsText(files[0]); type = "text";
} reader.onerror = function () {
output.innerHTML = "不能读取文件,错误码是:" + reader.error.code;
};
reader.onprogress = function (event) {
if (event.lengthComputable) progress.value = Math.round(event.loaded / event.total * 100);
}
reader.onload = function (event) {
switch (type) {
case "image": var img = document.createElement("img");
img.src = event.target.result; output.appendChild(img);
break;
case "text": output.appendChild(document.createTextNode(event.target.result));
break;
default: output.innerHTML = "其它内容...";
break;
}
};
}
FileReader方法:
-
abort():中止读取操作;在返回时,readyState属性为DONE;
-
readAsText(blob[, encoding]):以纯文本形式读取指定的blob中的内容,一旦完成,FileReader的result属性中将包含一个字符串以表示所读取的文件内容;可选的参数encoding表示编码类型,如缺省,则默认为“utf-8”类型。
//创建一个以二进制数据存储的html文件
const text = "<div>hello world</div>";
const blob = new Blob([text], { type: "text/html" }); // Blob {size: 22, type: "text/html"}
//以文本读取
const textReader = new FileReader();
textReader.readAsText(blob);
textReader.onload = function() {
console.log(textReader.result); // <div>hello world</div>
};
//以ArrayBuffer形式读取
const bufReader = new FileReader();
bufReader.readAsArrayBuffer(blob);
bufReader.onload = function() {
console.log(new Uint8Array(bufReader.result)); // Uint8Array(22) [60, 100, 105, 118, 62, 104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 60, 47, 100, 105, 118, 62]
};
读取部分内容:
myfile.onchange = function (evt) {
var file = evt.target.files[0];
if (file) {
var blob = file.slice(0, 20);
var reader = new FileReader();
reader.onload = function (event) {
console.log(event.target.result);
}
reader.readAsText(blob);
}
}
- readAsDataURL(blob):开始读取指定的Blob中的内容,一旦完成,result属性中将包含一个data: URL格式的base64字符串以表示所读取文件的内容;
使用上述readAsText例子读取文本文件时,就是比较正常的。如果读取二进制文件,比如png格式的图片,往往会产生乱码,如下:
那该如何处理这种二进制数据呢?readAsDataURL() 是一个不错的选择,它可以将读取的文件的内容转换为 base64 数据的 URL 表示。这样,就可以直接将 URL 用在需要源链接的地方,比如 img 标签的 src 属性。
<input type="file" id="myfile">
<br>
<img src="" height="200" alt="图片预览">
<script>
window.onload = function () {
var myfile = document.getElementById("myfile");
myfile.onchange = function (event) {
var preview = document.querySelector('img');
var file = event.target.files[0];
var reader = new FileReader();
reader.addEventListener("load", function (e) {
preview.src = e.target.result;
}, false);
if (file) {
reader.readAsDataURL(file);
}
}
}
</script>
- readAsArrayBuffer(blob):开始读取参数blob指定的Blob或File对象中的内容,一旦完成,result属性中保存的将是被读取文件的ArrayBuffer数据对象;
<input type="file" id="myfile">
window.onload = function () {
var myfile = document.getElementById("myfile");
myfile.onchange = function (event) {
var file = event.target.files[0];
var reader = new FileReader();
reader.addEventListener("loadend", function (e) {
console.log(e.target.result);
//ArrayBuffer
}, false);
if (file) {
reader.readAsArrayBuffer(file);
}
}
}
获取文件类型:以高位优先读取文件的前4个字节:
/*获取文件类型:以高位优先读取文件的前4个字节:*/
// 检测指定的blob的前4个字节
// 读出来的内容就是文件的类型,可以将其设置成blob的属性
function typefile(file) {
var slice = file.slice(0, 4);
// 只读取文件起始部分
var reader = new FileReader();
reader.readAsArrayBuffer(slice);
reader.onload = function (e) {
var buffer = reader.result;
var view = new DataView(buffer);
var magic = view.getUint32(0, false);
//高位优先,读取4个字节
console.log(magic);
//2303741511
switch (magic) {
//检测文件类型
case 0x89504E47:
file.verified_type = "image/png";
break;
case 0x47494638:
file.verified_type = "image/gif";
break;
case 0x25504446:
file.verified_type = "application/pdf"; break;
case 0x504b0304:
file.verified_type = "application/zip";
break;
}
console.log(file.name, file.verified_type);
};
}
- readAsBinaryString(blob):开始读取指定的Blob中的内容,一旦完成,result属性中将包含所读取文件的原始二进制数据,字符串中每个字符表示一个字节,已被废弃,使用readAsArrayBuffer()代替;
var canvas = document.createElement('canvas');
var width = 200, height = 200; canvas.width = width; canvas.height = height;
document.body.appendChild(canvas);
var ctx = canvas.getContext('2d');
ctx.strokeStyle = '#090';
ctx.beginPath();
ctx.arc(width / 2, height / 2, width / 2 - width / 10, 0, Math.PI * 2);
ctx.stroke();
canvas.toBlob(function (blob) {
var reader = new FileReader();
reader.onloadend = function (ev) {
console.log(ev.target.result);
}
reader.readAsBinaryString(blob);
});
FileReader.readAsDataURL与 URL.createObjectURL(blob)
FileReader.readAsDataURL这个函数是
没有返回值
的,看着似乎跟URL.createObjectURL一样有返回值得,其实只有后者有返回值。
- 通过FileReader.readAsDataURL(file)可以获取一段data:base64的字符串
- 通过URL.createObjectURL(blob)可以获取当前文件的一个内存URL
- 执行时机
- createObjectURL是同步执行(立即的)
- FileReader.readAsDataURL是异步执行(过一段时间)
- 内存使用
- createObjectURL返回一段带hash的url,并且一直存储在内存中,直到document触发了unload事件(例如:document close)或者执行revokeObjectURL来释放。
- FileReader.readAsDataURL则返回包含很多字符的base64,并会比blob url消耗更多内存,但是在不用的时候会自动从内存中清除(通过垃圾回收机制)
- 优劣对比
- 使用createObjectURL可以节省性能并更快速,只不过需要在不使用的情况下手动释放内存
- 如果不太在意设备性能问题,并想获取图片的base64,则推荐使用FileReader.readAsDataURL
ArrayBuffer
ArrayBuffer 对象用来表示通用的、固定长度的原始二进制数据缓冲区。ArrayBuffer 的内容不能直接操作,只能通过 DataView 对象或 TypedArrray 对象来访问。这些对象用于读取和写入缓冲区内容。
ArrayBuffer 本身就是一个黑盒,不能直接读写所存储的数据,需要借助以下视图对象来读写:
- TypedArray:用来生成内存的视图,通过9个构造函数,可以生成9种数据格式的视图。
- DataViews:用来生成内存的视图,可以自定义格式和字节序。
TypedArray视图和 DataView视图的区别
主要是字节序,前者的数组成员都是同一个
数据类型,后者的数组成员可以是不同
的数据类型。
那 ArrayBuffer 与 Blob 有啥区别呢?根据 ArrayBuffer 和 Blob 的特性,Blob 作为一个整体文件,适合用于传输
;当需要对二进制数据
进行操作时(比如要修改某一段数据时),就可以使用 ArrayBuffer。
下面来看看 ArrayBuffer 有哪些常用的方法和属性。
ArrayBuffer 的创建
ArrayBuffer 可以通过以下方式生成:
new ArrayBuffer(bytelength)
ArrayBuffer()构造函数可以分配指定字节数量的缓冲区,其参数和返回值如下:
- 参数:它接受一个参数,即 bytelength,表示要创建数组缓冲区的大小(以字节为单位。);
- 返回值:返回一个新的指定大小的ArrayBuffer对象,内容初始化为0。
属性
- byteLength
ArrayBuffer 实例上有一个 byteLength 属性,它是一个只读属性,表示 ArrayBuffer 的 byte 的大小,在 ArrayBuffer 构造完成时生成,不可改变。来看例子:
const buffer = new ArrayBuffer(16);
console.log(buffer.byteLength); // 16
方法
- slice(begin,end):截取 ArrayBuffer 实例,它返回一个新的 ArrayBuffer
- isView : 如果参数是 ArrayBuffer 的视图实例则返回 true,例如类型数组对象或 DataView 对象;否则返回 false
slice方法
const buffer = new ArrayBuffer(16);
console.log(buffer.slice(0, 8)); // 16
这里会从 buffer 对象上将前8个字节生成一个新的ArrayBuffer对象。这个方法实际上有两步操作,首先会分配一段指定长度的内存,然后拷贝原来ArrayBuffer对象的置顶部分。
isView方法
简单来说,这个方法就是用来判断参数是否是 TypedArray 实例或者 DataView 实例:
const buffer = new ArrayBuffer(16);
ArrayBuffer.isView(buffer) // false
const view = new Uint32Array(buffer);
ArrayBuffer.isView(view) // true
TypedArray
TypedArray 对象一共提供 9 种类型的视图,每一种视图都是一种构造函数。如下:
元素 | 类型化数组 | 字节 | 描述 |
---|---|---|---|
Int8 | Int8Array | 1 | 8 位有符号整数 |
Uint8 | Uint8Array | 1 | 8 位无符号整数 |
Uint8C | Uint8ClampedArray | 1 | 8 位无符号整数 |
Int16 | Int16Array | 2 | 16 位有符号整数 |
Uint16 | Uint16Array | 2 | 16 位无符号整数 |
Int32 | Int32Array | 4 | 32 位有符号整数 |
Uint32 | Uint32Array | 4 | 32 位无符号整数 |
Float32 | Float32Array | 4 | 32 位浮点 |
Float64 | Float64Array | 8 | 64 位浮点 |
来看看这些都是什么意思:
- Uint8Array: 将 ArrayBuffer 中的
每个字节
视为一个整数,可能的值从 0 到 255 (一个字节等于 8 位)。 这样的值称为“8 位无符号整数”。 - Uint16Array:将 ArrayBuffer 中任意两个字节视为一个整数,可能的值从 0 到 65535。 这样的值称为“16 位无符号整数”。
- Uint32Array:将 ArrayBuffer 中任何四个字节视为一个整数,可能值从 0 到 4294967295,这样的值称为“32 位无符号整数”。
这些构造函数生成的对象统称为 TypedArray 对象。它们和正常的数组很类似,都有length
属性,都能用索引获取数组元素,所有数组的方法都可以在类型化数组上面使用。
那类型化数组和数组有什么区别呢?
- 类型化数组的元素都是连续的,不会为空;
- 类型化数组的所有成员的类型和格式相同;
- 类型化数组元素默认值为 0;
- 类型化数组本质上只是一个视图层,不会存储数据,数据都存储在更底层的 ArrayBuffer 对象中。
下面来看看 TypedArray 都有哪些常用的方法和属性。
new TypedArray的创建
TypedArray 的语法如下(TypedArray只是一个概念,实际使用的是那9个对象):
new Int8Array(length);
new Int8Array(typedArray);
new Int8Array(object);
new Int8Array(buffer [, byteOffset [, length]]);
可以看到,TypedArray 有多种用法,下面来分别看一下。
- TypedArray(length) :通过分配指定长度内容进行分配
let view = new Int8Array(16);
view[0] = 10;
view[10] = 6;
console.log(view);
输出结果如下:
这里就生成了一个 16个元素的 Int8Array 数组,除了手动赋值的元素,其他元素的初始值都是 0。
- TypedArray(typeArray) :接收一个视图实例作为参数
const view = new Int8Array(new Uint8Array(6));
view[0] = 10;
view[3] = 6;
console.log(view);
输出结果如下:
- TypedArray(object) :参数可以是一个普通数组
const view = new Int8Array([1, 2, 3, 4, 5]);
view[0] = 10;
view[3] = 6;
console.log(view);
输出结果如下:
需要注意,TypedArray视图会开辟一段新的内存,不会在原数组上建立内存。当然,这里创建的类型化数组也能转换回普通数组:
Array.prototype.slice.call(view); // [10, 2, 3, 6, 5]
- TypeArray(buffer [, byteOffset [, length]]) :
这种方式有三个参数,其中第一个参数是一个ArrayBuffer对象;第二个参数是视图开始的字节序号,默认从0开始,可选;第三个参数是视图包含的数据个数,默认直到本段内存区域结束。
const buffer = new ArrayBuffer(8);
const view1 = new Int32Array(buffer);
const view2 = new Int32Array(buffer, 4);
console.log(view1, view2);
输出结果如下:
BYTES_PER_ELEMENT属性
每种视图的构造函数都有一个 BYTES_PER_ELEMENT
属性,表示这种数据类型占据的字节数:
Int8Array.BYTES_PER_ELEMENT // 1
Uint8Array.BYTES_PER_ELEMENT // 1
Int16Array.BYTES_PER_ELEMENT // 2
Uint16Array.BYTES_PER_ELEMENT // 2
Int32Array.BYTES_PER_ELEMENT // 4
Uint32Array.BYTES_PER_ELEMENT // 4
Float32Array.BYTES_PER_ELEMENT // 4
Float64Array.BYTES_PER_ELEMENT // 8
BYTES_PER_ELEMENT
属性也可以在类型化数组的实例上获取:
const buffer = new ArrayBuffer(16);
const view = new Uint32Array(buffer);
console.log(Uint32Array.BYTES_PER_ELEMENT); // 4
TypedArray.prototype.buffer
TypedArray 实例的 buffer 属性会返回内存中对应的 ArrayBuffer对象,只读属性。
const a = new Uint32Array(8);
const b = new Int32Array(a.buffer);
console.log(a, b);
输出结果如下:
TypedArray.prototype.slice()
TypeArray 实例的 slice方法可以返回一个指定位置的新的 TypedArray实例。
const view = new Int16Array(8);
console.log(view.slice(0 ,5));
输出结果如下:
byteLength 和 length
byteLength
:返回 TypedArray 占据的内存长度,单位为字节;length
:返回 TypedArray 元素个数;
const view = new Int16Array(8);
view.length; // 8
view.byteLength; // 16
DataView
说完 ArrayBuffer,下面来看看另一种操作 ArrayBuffer 的方式:DataView。DataView 视图是一个可以从 二进制 ArrayBuffer 对象中读写多种数值类型的底层接口,使用它时,不用考虑不同平台的字节序问题。
DataView视图提供更多操作选项,而且支持设定字节序。本来,在设计目的上,ArrayBuffer对象的各种TypedArray视图,是用来向网卡、声卡之类的本机设备传送数据,所以使用本机的字节序就可以了;而DataView视图的设计目的,是用来处理网络设备传来的数据,所以大端字节序或小端字节序是可以自行设定的。
new DataView 的创建
DataView视图可以通过构造函数来创建,它的参数是一个ArrayBuffer对象,生成视图。其语法如下:
new DataView(buffer [, byteOffset [, byteLength]])
其有三个参数:
buffer
:一个已经存在的 ArrayBuffer 对象,DataView 对象的数据源。byteOffset
:可选,此 DataView 对象的第一个字节在 buffer 中的字节偏移。如果未指定,则默认从第一个字节开始。byteLength
:可选,此 DataView 对象的字节长度。如果未指定,这个视图的长度将匹配 buffer 的长度。
来看一个例子:
const buffer = new ArrayBuffer(16);
const view = new DataView(buffer);
console.log(view);
打印结果如下:
buffer、byteLength、byteOffset
DataView实例有以下常用属性:
buffer
:返回对应的ArrayBuffer对象;byteLength
:返回占据的内存字节长度;byteOffset
:返回当前视图从对应的ArrayBuffer对象的哪个字节开始。
const buffer = new ArrayBuffer(16);
const view = new DataView(buffer);
view.buffer;
view.byteLength;
view.byteOffset;
打印结果如下:
读取内存
DataView 实例提供了以下方法来读取内存,它们的参数都是一个字节序号,表示开始读取的字节位置:
- getInt8:读取1个字节,返回一个8位整数。
- getUint8:读取1个字节,返回一个无符号的8位整数。
- getInt16:读取2个字节,返回一个16位整数。
- getUint16:读取2个字节,返回一个无符号的16位整数。
- getInt32:读取4个字节,返回一个32位整数。
- getUint32:读取4个字节,返回一个无符号的32位整数。
- getFloat32:读取4个字节,返回一个32位浮点数。
- getFloat64:读取8个字节,返回一个64位浮点数。
下面来看一个例子:
const buffer = new ArrayBuffer(24);
const view = new DataView(buffer);
// 从第1个字节读取一个8位无符号整数
const view1 = view.getUint8(0);
// 从第2个字节读取一个16位无符号整数
const view2 = view.getUint16(1);
// 从第4个字节读取一个16位无符号整数
const view3 = view.getUint16(3);
写入内存
DataView 实例提供了以下方法来写入内存,它们都接受两个参数,第一个参数表示开始写入数据的字节序号,第二个参数为写入的数据:
- setInt8:写入1个字节的8位整数。
- setUint8:写入1个字节的8位无符号整数。
- setInt16:写入2个字节的16位整数。
- setUint16:写入2个字节的16位无符号整数。
- setInt32:写入4个字节的32位整数。
- setUint32:写入4个字节的32位无符号整数。
- setFloat32:写入4个字节的32位浮点数。
- setFloat64:写入8个字节的64位浮点数。
格式转化
看完这些基本的概念,下面就来看看常用格式之间是如何转换的。
(1)ArrayBuffer → blob
const blob = new Blob([new Uint8Array(buffer, byteOffset, length)]);
const binary2base64 = function (bi) {
let str = '';
for (let i = 0, len = bi.length; i < len; i++) {
str += String.fromCharCode(bi[i]);
}
return btoa(str);
};
arraybuffer = reader.readAsArrayBuffer(file);
// 通过slice截取获取一部分arraybuffer 获得 sliceArraybuffer
var unit8_data = new Uint8Array(sliceArraybuffer);
var base64_data = binary2base64(unit8_data); // base64 编码
// 和上面的区别就是给每一个Uint8Array的每一位都调用了String.fromCharCode方法
(2)ArrayBuffer → base64
const base64 = btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer)));
(3)base64 → blob
const base64toBlob = (base64Data, contentType, sliceSize) => {
const byteCharacters = atob(base64Data);
const byteArrays = [];
for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
const slice = byteCharacters.slice(offset, offset + sliceSize);
const byteNumbers = new Array(slice.length);
for (let i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}
const blob = new Blob(byteArrays, {type: contentType});
return blob;
}
(4)blob → ArrayBuffer
function blobToArrayBuffer(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject;
reader.readAsArrayBuffer(blob);
});
}
(5)blob → base64
function blobToBase64(blob) {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.readAsDataURL(blob);
});
}
(6)blob → Object URL
const objectUrl = URL.createObjectURL(blob);
blob的理解
对象URL(Blob URL):
自从HTML5提供了video标签,在网页中播放视频已经变成一个非常简单的事,只要一个video标签,src属性设置为视频的地址就完事了。由于src指向真实的视频网络地址,在早期一般网站资源文件不怎么通过referer设置防盗链,当我们拿到视频的地址后可以随意的下载或使用(每次放假回家,就会有亲戚找我帮忙从一些视频网站上下东西)。
目前的云存储服务商大部分都支持referer防盗链。其原理就是在访问资源时,请求头会带上发起请求的页面地址,判断其不存在(表示直接访问图片地址)或不在白名单内,即为盗链。
可是从某个时间开始我们打开调试工具去看各大视频网站的视频src会发现,它们统统变成了这样的形式。
对象URL也被称为blob URL,指的是引用保存在File或Blob中数据的URL;使用它就可以不必把文件内容读取到JavaScript中而直接使用文件内容;为此,只要在需要文件内容的地方提供对象URL即可;
对象URL主要使用URL类的createobjectURL()和revokeObjectURL()两个静态方法实现;
createObjectURL(object):返回一个DOMString,包含了一个对象URL,该URL可用于指定源object的内容,如包含一个唯一的blob链接;
参数object用于创建URL的File、Blob对象或者MediaSource对象;
使用此方法,可以创建用于引用任何数据的简单URL字符串,也可以引用一个包括用户本地文件的File对象,如:
var myfile = document.getElementById("myfile");
myfile.onchange = function (event) {
var file = event.target.files[0];
var objURL = URL.createObjectURL(file);
// blob:http://127.0.0.1:5500/acc683a4-c890-4399-850b-375ebb714f2
cconsole.log(objURL);
};
这个对象URL是一个标识File对象的字符串,其以“blob://”开始,紧跟着是一小串文本字符串,该字符串用不透明的唯一标识符来标识Blob,指向一块内存地址;
即使对一个已创建了对象URL的文件再次创建一个对象URL,该URL都不相同,如:
// ...var objURL1 = URL.createObjectURL(file);
// blob:
http://127.0.0.1:5500/51a3ee15-d8de-4c57-908f-751baf363bc6
console.log(objURL1);
blob://URL模式被显式的设计成像一个简化的http://URL那样工作,当请求一个blob://URL的时候,浏览器也会像访问HTTP服务器那样做出响应;如果请求的Blob URL已经失效,浏览器必须返回一个404无法找到的状态码;如果请求的Blob URL来自另外的源,那么浏览器必须返回403禁止访问的状态码;
Blob URL只允许通过GET请求获取,并且一旦获取成功,浏览器必须返回一个HTTP 200 OK的状态码,同时响应的Content-Type头信息为该Blob的type属性值,如:“image/png“;
因为这个字符串是一个URL,所以在DOM中也可以使用,如:
myfile.onchange = function (event) {
var file = event.target.files[0];
var objURL = URL.createObjectURL(file);
var img = document.createElement("img");
img.src = objURL; document.body.appendChild(img);
};
revokeObjectURL(objectURL)方法:
在每次调用createObjectURL()方法时,都会创建一个新的URL对象,当不再需要这些 URL 对象时,每个对象必须通过调用revokeObjectURL()方法来释放;虽然关闭浏览器,会自动释放它们,但是为了获得最佳性能和内存使用状况,应该在安全的时机主动释放掉它们;
参数objectURL是一个DOMString,表示通过调用createObjectURL()方法产生的URL对象,如:
// ...URL.revokeObjectURL(objURL);
示例:使用对象URL来显示多张图片
<input type="file" id="myfile" multiple accept="image/*" style="display: none;" />
<a href="javascript:void(0)" id="fileSelect">选择文件</a>
<div id="fileList">
<p>没有选择的文件</p>
</div>
<script>
window.onload = function () {
window.URL = window.URL || window.webkitURL;
var fileSelect = document.getElementById("fileSelect"),
myfile = document.getElementById("myfile"),
fileList = document.getElementById("fileList");
fileSelect.addEventListener("click", function (e) {
if (myfile) myfile.click();
e.preventDefault();
});
myfile.onchange = function (event) {
var files = event.target.files;
if (!files.length) fileList.innerHTML = "<p>没有选择任何文件</p>";
else {
fileList.innerHTML = "";
var list = document.createElement("ul");
fileList.appendChild(list);
for (var i = 0, len = files.length; i < len; i++) {
var li = document.createElement("li");
list.appendChild(li);
var img = document.createElement("img");
img.src = URL.createObjectURL(files[i]);
img.height = 80; img.onload = function () {
// 当图片加载完成之后对象URL就不再需要了
URL.revokeObjectURL(this.src);
};
li.appendChild(img);
var info = document.createElement("span");
info.innerHTML = files[i].name + ":" + files[i].size + "bytes";
li.appendChild(info);
}
}
}
};
</script>
例子:用对象URL显示PDF,对象URL可以用于image之外的其它东西!它可以用于显示嵌入的PDF文件或任何其它浏览器能显示的资源;如:
<input type="file" id="myfile" accept="application/PDF">
<iframe id="viewer"></iframe>
<script>
window.onload = function () {
var myfile = document.getElementById("myfile");
myfile.onchange = function (event) {
var file = event.target.files[0];
if (file) {
var objURL = URL.createObjectURL(file);
var iframe = document.getElementById("viewer");
iframe.setAttribute('src', objURL);
URL.revokeObjectURL(objURL);
}
};
}
</script>
示例:使用Blob对象存储下载数据:从互联网上下载的数据可以存储到Blob对象中,特别是在一些需要鉴权的接口中,可以使用Ajax请求,将鉴权信息附在请求里,下载得到blob对象,然后将blob作为url使用;或者在前端直接通过构建Blob对象进行前端文件下载,如:
<ul id="filelist"></ul>
<script>
window.onload = function () {
var xhr = new XMLHttpRequest();
xhr.open("GET", "blob.php?action=getall&id=1&key=1234");
xhr.onload = function () {
var filelist = document.getElementById("filelist");
var files = xhr.response;
for (var i = 0, len = files.length; i < len; i++) {
var file = files[i];
var li = document.createElement("li");
li.innerHTML = file.filename + "(" + file.filesize + ")";
var btn = document.createElement("input");
btn.type = "button";
btn.value = "下载";
btn.dataset['fileid'] = file.id;
btn.dataset['filename'] = file.filename;
btn.onclick = downloadHandle; li.appendChild(btn); filelist.appendChild(li);
}
};
xhr.responseType = "json";
xhr.withCredentials = true;
xhr.send(null);
}
function downloadHandle(event) {
console.log(event.target.dataset.fileid);
var fileid = event.target.dataset.fileid;
var xhr = new XMLHttpRequest();
// xhr.open("GET", "images/1.jpg");
xhr.open("GET", "blob.php?action=download&id=" + fileid);
xhr.onload = function () {
// console.log(xhr.response);
var url = URL.createObjectURL(xhr.response);
var a = document.createElement("a");
a.setAttribute('download', event.target.dataset.filename);
a.href = url; a.click();
};
xhr.responseType = "blob";
xhr.withCredentials = true;
xhr.send(null);
}
</script>
前端实现文件下载
a标签实现文件下载 避免直接打开问题
先说结论
- 所有情况通用的方式: 后端设置下载请求的响应头 Content-Disposition: attachment; filename=“filename.jpg”
- attachment 表示让浏览器强制下载
- filename 用于设置下载弹出框里预填的文件名
- 非跨域情况下 给a标签加上 download 属性,如
download 里写文件名 注意后缀 (值非必填) - 通过请求解决跨域问题 动态创建a标签通过blob形式下载 具体看下面解析
文件下载通常有以下方式:
- 下载 a标签访问文件地址
- window.open(‘http://localhost:8087/upload/user.png’) 打开文件地址
- 后端提供一个接口 /api/download 通过接口返回文件流
浏览器通过请求头Content-Type中的MIME类型(媒体类型,通常称为 Multipurpose Internet Mail Extensions 或 MIME 类型,如 :image/jpeg application/pdf)识别数据类型,对相应的数据做出相应处理,对于图像文本等浏览器可以直接打开的文件,默认处理方式就是打开,为了避免浏览器直接打开文件我们需要做一些处理;
实现pdf打印
const printPdf = (res) => {
if (!res) return;
fetch(
'https:/localhost:21000/api/filFileDesc/downloadFile/330182000016fds20240422152203001?a=a.jpg'
)
.then((response) => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.blob(); // 获取二进制数据
})
.then((blobData) => {
// 替换这里的 PDF_URL 为你要打印的 PDF 文件链接
const PDF_URL = URL.createObjectURL(blobData);
var iframe = document.createElement('iframe');
iframe.setAttribute('id', 'printPDF');
iframe.setAttribute('name', 'printPDF'); // 不可少
iframe.style.frameborder = 'no';
iframe.style.display = 'none';
iframe.style.pageBreakBefore = 'always'; // 打印保留分页
iframe.src = PDF_URL;
document.body.appendChild(iframe);
setTimeout(() => {
document.getElementById('printPDF').contentWindow.print();
}, 1000);
window.URL.revokeObjectURL(iframe.src);
});
};
方案一 a标签+download属性
当url是同源(同域名、同协议、同端口号)时,这种情况用 a标签加download属性的方式即可,download属性指示浏览器该下载而不是打开该文件,同时该属性值即下载时的文件名;
a标签中download属性可以更改下载文件的文件名。但是如果是跨域的话,download属性就会失效。
解决方案:
//onclick 事件
<a @click="downloadFile(fileUrl,fileName)">下载文件</a>
downloadFile(url, fileName) {
var x = new XMLHttpRequest();
x.open("GET", url, true);
x.responseType = 'blob';
x.onload=function(e) {
//会创建一个 DOMString,其中包含一个表示参数中给出的对象的URL。这个 URL 的生命周期和创建它的窗口中的 document 绑定。这个新的URL 对象表示指定的 File 对象或 Blob 对象。
var url = window.URL.createObjectURL(x.response)
var a = document.createElement('a');
a.href = url
a.download = fileName;
a.click()
}
x.send();
},
方案二 后端设置下载请求的响应头 Content-Disposition 强制下载
这是最通用的一种方式 不受跨域和请求方式的影响
Content-Disposition: attachment; filename=“filename.jpg”
想使用window.open实现强制下载的可以用这种方式
在常规的 HTTP 应答中,该响应头的值表示对响应内容的展现形式
inline 表示将响应内容作为页面的一部分进行展示
attachment 表示将响应内容作为附件下载,大多数浏览器会呈现一个“保存为”的对话框
filename(可选) 指定为保存框中预填的文件名
方案三 通过接口跨域请求,动态创建a标签,以blob形式下载
当接口请求的跨域问题已经解决时(如Nginx方式),可以直接通过请求的方式拿到文件流,将文件流转为blob格式,再通过a标签的download属性下载
// 用fetch发送请求
fetch('/upload/user.png').then((res) => {
res.blob().then((blob) => {
const blobUrl = window.URL.createObjectURL(blob);
// 这里的文件名根据实际情况从响应头或者url里获取
const filename = 'user.jpg';
const a = document.createElement('a');
a.href = blobUrl;
a.download = filename;;
a.click();
window.URL.revokeObjectURL(blobUrl);
});
});
上面通过原生fetch请求,动态生成一个a标签实现文件下载
res.blob() 该方法是Fetch API的response对象方法,该方法将后端返回的文件流转换为返回blob的Promise;blob(Binary Large Object)是一个二进制类型的对象,记录了原始数据信息
URL.createObjectURL(blob) 该方法的返回值可以理解为一个 指向传入参数对象的url 可以通过该url访问 参数传入的对象
该方法需要注意的是,即便传入同一个对象作为参数,每次返回的url对象都是不同的
该url对象保存在内存中,只有在当前文档(document)被卸载时才会被清除,因此为了更好的性能,需要通过URL.revokeObjectURL(blobUrl) 主动释放
当我们使用 Fetch API 获取到后端返回的字节流一般都会通过 “res.blob()” 转换成 blob 对象再进一步处理(Fetch API),那么问题来了 --- ”res.blob()” 又做了什么?
怎么下载后端返回的 zip 文件?
这个问题还是比较好解决的,从后端返回的字节数据,都可以通过调用 response 对象的 blob() 方法来将它转换成返回 blob 的 Promise。
那 blob 对象又是什么?我们可以把它当作原始数据对象,它保存着从后端返回的原始数据以及相关信息(比如字节数以及类型)。我们可以通过它获得 Base64 URL 或者 blob URL。然后通过 a 标签的 download 属性来设置文件名,将获得的 URL 赋给 a 标签的 href 属性就大功告成了,当然别忘了调用 click() 开始下载。
示例代码如下:
fetch('example.zip')
.then(res => res.blob())
.then(blob => {
// 通过 blob 对象获取对应的 url
const url = URL.createObjectURL(blob)
let a = document.createElement('a')
a.download = 'example.zip'
a.href = url
document.body.appendChild(a)
a.click()
a.remove() // document.body.removeChild(a)
})
.catch(err => {
console.error(err)
})
每次调用 res.blob() 方法都会执行 “consume body” 动作,“consume body” 的流程大概是这样的:
获取字节流的读取器
通过读取器读取所有的数据
把数据包装成 blob 对象并返回
既然 res.blob() 可以处理字节流,那么我们能不能自己去处理后端返回的字节流,毕竟我们有时候也会遇到这种情况吧(没有的事,吃饱了撑着)。
fetch('example.zip')
.then((res) => {
// 获取读取器
const reader = res.body.getReader()
const type = res.headers.get('Content-Type')
const data = []
return new Promise((resolve) => {
// 读取所有数据
function push() {
reader.read().then(({done, value}) => {
data.push(value)
if (done) {
// 包装成 blob 对象并返回
resolve(new Blob(data, { type }))
} else {
push()
}
})
}
push()
})
})
.then(blob => {
const url = URL.createObjectURL(blob)
let a = document.createElement('a')
a.download = 'example.zip'
a.href = url
document.body.appendChild(a)
a.click()
a.remove()
})
xhr 也可以实现,axios也差不多,网上都有
const xhr = new XMLHttpRequest();
xhr.open('GET', '/upload/user.png', true);
xhr.responseType = 'blob';
xhr.onload = function() {
if (this.status === 200) {
const fileName = 'test.jpg';
const blob = new Blob([this.response]);
const blobUrl = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = blobUrl;
a.download = fileName;
a.click();
window.URL.revokeObjectURL(blobUrl);
}
};
xhr.send();
注意
- 当用方案一二实现时,下载文件名优先取的是Content-Disposition的filename而不是download,而通过blob的形式,文件名优先取的download属性,如果都没有设置则取的url最后一节;
- 当通过接口的形式fetch(‘/upload/downloadfile’)访问文件,又想保留浏览器的预览效果时,可以仅设置Content-Disposition的filename以指定预览时下载的文件名,否则浏览器会默认取url最后一节,即downloadfile为文件名,导致下载的文件无后缀无法打开
- window.open() 和 a标签 执行的是打开链接的操作,类似于将地址直接输入到浏览器中,相当于从一个域跳到另一个域,因此window.open(‘http://xxx’)可以访问而不会报跨域错误;而fetch/xhr 仅是从当前域发送请求,因此fetch(‘http://xxx’)会报跨域错误
- 浏览器取下载时文件名的优先级是Content-Disposition: filename=“文件名.png” 优先于 优先于 url最后一节 http://localhost:8087/upload/文件名.png
关于 video 标签 src 带有blob:http的 一些想法
之前玩爬虫的时候,看到过video标签中src属性引入的blob:http:xxxx,当时没找到解决思路,今天又遇到类似问题,就试着找了一下。
这是有人问过 https://vimeo.com/ 这个网站的视频怎么下载。
1. 分析
以这个网址为例:
美天合集团CFO汪润怡谈制胜新兴市场的战略-高顿公开课
看video标签中的src属性,发现
src=“blob:https://open.gaodun.com/b9d3366f-87ef-4328-9d97-31110de519a1”
复制这个地址去浏览器什么也找不到。
2. 找真实地址
不管上面的问题。先去看一下视频到底从哪来的。以谷歌浏览器为例,选择XHR,发现加载了m3u8文件。
m3u8是一种视频格式,看response中返回的.ts文件,直接复制ts文件的路径打开,就是视频片段。
到这,文件其实已经找到了。但是video中的blob:https://xxxx是什么呢,是怎么找到的文件。
简单来说就是视频对象做了个标记,src指向的是标记。
3. 找关联
当我对着源码和请求的response对照的时候,发现播放器周围的html标签都是后生成的,找到了一个比较“可疑”的js文件。
发现播放器代码附近的:
<div class="playDiv" id="divid"> <script type="text/javascript" src="https://s.gaodun.com/web/static-player/loader.js?13p9Wv580v1a!!fs-3"></script> </div>
看了js的源码,再跟了下debug。
找到了这个网页请求的m3u8地址是这个:https://vod.gaodun.com/13p9Wv580v1a!!fs/SD/1.m3u8。
直接浏览器访问就可以获取,就可以获取ts文件。ts文件就是一段段的视频,可以下载下来之后拼接成一个完整的文件。
至此,关于video 标签 src 带有blob:http的 抓取的就写完了。但是每个网站的情况都不一样。
这里只是提供一种思路,比如刚开始写的 https://vimeo.com/ 这个网站就不是js,而是json里边包含的视频地址。
结论就是:
blob:https并不是一种协议,而是html5中blob对象在赋给video标签后生成的一串标记,blob对象对象包含的数据,浏览器内部会解析;
每次调用 URL.createObjectURL() 方法都会生成一个地址,这个地址代表着根据 blob 对象生成的资源入口,而这个资源入口存放于浏览器维护的一个 blob URL store 中
在web容器中的页面代码
浏览器访问后的页面代码
这是因为在浏览器中执行了如下js