文章目录
- Blob和ArrayBuffer
- 实战一:上传图片预览
- 实战二:以Blob URL加载网络视频
- 前端实现文件下载(a标签实现文件下载 避免直接打开问题)
- 先说结论
- 方案一 a标签+download属性
- 方案二 后端设置下载请求的响应头 Content-Disposition 强制下载
- 方案三 通过接口跨域请求,动态创建a标签,以blob形式下载
- 关于 video 标签 src 带有blob:http的 一些想法
- 1. 分析
- 2. 找真实地址
- 3. 找关联
- FileReader.readAsDataURI 与 URL.createObjectURL(blob) 区别
自从 HTML5提供了video标签,在网页中播放视频已经变成一个非常简单的事,只要一个video标签,src属性设置为视频的地址就完事了。由于src指向真实的视频网络地址,在早期一般网站资源文件不怎么通过referer设置防盗链,当我们拿到视频的地址后可以随意的下载或使用(每次放假回家,就会有亲戚找我帮忙从一些视频网站上下东西)。
目前的云存储服务商大部分都支持referer防盗链。其原理就是在访问资源时,请求头会带上发起请求的页面地址,判断其不存在(表示直接访问图片地址)或不在白名单内,即为盗链。
可是从某个时间开始我们打开调试工具去看各大视频网站的视频src会发现,它们统统变成了这样的形式。
Blob和ArrayBuffer
Blob(Binary Large Object)二进制类型的大对象,其名称来源于SQL数据库,表示一个不可变、原始数据的类文件对象;
在JavaScript中,它不一定非得是大量数据,其也可以表示一个小型文本文件的内容;
Blob是不透明的,只能获取它们的大小、MIME类型以及将它们分割成更小的Blob;
构造函数:
Blob(blobParts [, options]):返回一个Blob对象,其内容由参数中给定的数组串联组成;
参数:
blobParts是一个由ArrayBuffer、ArrayBufferView、Blob、String等对象构成的Array,或者其他类似对象的混合体,它将会被放入Blob;其中,Strings会被编码为UTF-8;options是一个可选的BlobPropertyBag([bɡ])字典,它可能会指定如下两个属性:
type,默认值为 “”,它代表了将会被放入到blob中的数组内容的MIME类型;
endings,默认值为"transparent",用于指定包含行结束符\n的字符串如何被写入,它是以下两个值中的一个:“native”,代表行结束符会被更改为适合宿主操作系统文件系统的换行符,或者 “transparent”,代表会保持blob中保存的结束符不变;
ArrayBuffer()是一个普通的JavaScript构造函数,可用于在内存中分配特定数量的字节空间。
const buf = new ArrayBuffer(16); // 在内存中分配16 字节
alert(buf.byteLength); // 16
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主要用于大量API需要进行二进制数据交换场景,为这些应用提供了通用、高效的数据交换机制,如图:
FileReader.readAsDataURL这个函数是
没有返回值
的,看着似乎跟URL.createObjectURL一样有返回值得,其实只有后者有返回值。
通过这些API,可以获取Blob对象,例如:
message事件从其他窗口或者线程中获取Blob;
可以从客户端数据库中获取Blob;
可以使用XHR2,从Web中下载Blob;
还有File对象,它是Blob的子类;
一旦获取了Blob对象,就可以对其进行很多的操作,如:
可以使用postMessage()方法向其他窗口或Worker发送一个Blob;
可以将Blob存储在客户端数据库中;
可以通过将Blob传递给XHR对象的send()方法,来将该Blob上传到服务端;
可以使用URL.createObjectURL()函数获取一个特殊的blob://URL,该URL代表Blob的内容,然后,将其和DOM或者CSS结合使用;
可以使用FileReader对象来异步地将一个Blob内容抽取成一个字符串或者ArrayBuffer;
可以使用File和FileWriter对象,来实现将一个Blob写入到一个本地文件中;
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 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对象也有可能来自用户的拖放操作;
File接口:
File继承自Blob,是特殊类型
的 Blob,且可以用在任意的Blob类型的context中;如: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>
大文件切块处理的思路
使用 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 方法对大文件进行切割,然后分片进行上传;如下:
<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);
}
}
File对象是特殊类型的Blob,且可以用在任意的Blob类型的context中,比如:FileReader、URL.createObjectURL()、createImageBitmmmap()及XMLHttpRequest.send()都能处理Blob和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对象属性:
error属性:只读,表示在读取文件时发生的错误;
readyState属性:只读,表示FileReader状态,可能的值有:
EMPTY(0):还没有加载任何数据;
LOADING(1):数据正在被加载;
DONE(2):已完成全部的读取请求
result属性:只读,返回文件的内容,其仅在读取操作完成后才有效,并且,数据的格式取决于使用哪个方法来启动读取操作;
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左右;
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中的内容,一旦完成,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);
}
}
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);
});
readAsDataURL(blob):开始读取指定的Blob中的内容,一旦完成,result属性中将包含一个data: URL格式的base64字符串以表示所读取文件的内容;
<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>
data URL由四部分组成:data:[][;base64],
data::前缀;
[]:MIME type,代表数据的类型,比如’image/jpeg’;如果忽略的话,默认是"text/plain;charset=US-ASCII";
[;base64]:可选的base64标识
:数据本身,如果是简单的纯文本,可以直接嵌入文本;如果不是纯文本,上一个参数可以标识为base64,并且嵌入base64编码的二进制数据;
读取多个文件,如:
<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);
}
}
对象URL(Blob URL):
对象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>
实战一:上传图片预览
有时我们通过input上传图片文件之前,会希望可以预览一下图片,这个时候就可以通过前面所学到的东西实现,而且非常简单。
html
<input id="upload" type="file" />
<img id="preview" src="" alt="预览"/>
这样一个图片上传预览就实现了,同样这个方法也适用于上传视频的预览。
实战二:以Blob URL加载网络视频
现在我们有一个网络视频的地址,怎么能将这个视频地址变成Blob URL是形式呢,思路肯定是先要拿到存储这个视频原始数据的Blob对象,但是不同于input上传可以直接拿到File对象,我们只有一个网络地址。
我们知道平时请求接口我们可以使用xhr(jquery里的ajax和axios就是封装的这个)或fetch,请求一个服务端地址可以返回我们相应的数据,那如果我们用xhr或者fetch去请求一个图片或视频地址会返回什么呢?当然是返回图片和视频的数据,只不过要设置正确responseType才能拿到我们想要的格式数据。
function ajax(url, cb) {
const xhr = new XMLHttpRequest();
xhr.open("get", url);
xhr.responseType = "blob"; // ""|"text"-字符串 "blob"-Blob对象 "arraybuffer"-ArrayBuffer对象
xhr.onload = function() {
cb(xhr.response);
};
xhr.send();
}
注意XMLHttpRequest和Fetch API请求会有跨域问题,可以通过跨域资源共享(CORS)解决。
看到responseType可以设置blob和arraybuffer我们应该就有谱了,请求返回一个Blob对象,或者返回ArrayBuffer对象转换成Blob对象,然后通过createObjectURL生成地址赋值给视频的src属性就可以了,这里我们直接请求一个Blob对象。
用调试工具查看视频标签的src属性已经变成一个Blob URL,表面上看起来是不是和各大视频网站形式一致了,但是考虑一个问题,这种形式要等到请求完全部视频数据才能播放,小视频还好说,要是视频资源大一点岂不爆炸,显然各大视频网站不可能这么干。
解决这个问题的方法就是流媒体,其带给我们最直观体验就是使媒体文件可以边下边播(像我这样的90后男性最早体会到流媒体好处的应该是源于那款快子头的播放器),web端如果要使用流媒体,有多个流媒体协议可以供我们选择。
前端实现文件下载(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)识别数据类型,对相应的数据做出相应处理,对于图像文本等浏览器可以直接打开的文件,默认处理方式就是打开,为了避免浏览器直接打开文件我们需要做一些处理;
方案一 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/ 这个网站的视频怎么下载。
1. 分析
以这个网址为例:
美天合集团CFO汪润怡谈制胜新兴市场的战略-高顿公开课
看video标签中的src属性,发现
src=“blob:https://open.gaodun/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/web/static-player/loader.js?13p9Wv580v1a!!fs-3"></script> </div>
看了js的源码,再跟了下debug。
找到了这个网页请求的m3u8地址是这个:https://vod.gaodun/13p9Wv580v1a!!fs/SD/1.m3u8。
直接浏览器访问就可以获取,就可以获取ts文件。ts文件就是一段段的视频,可以下载下来之后拼接成一个完整的文件。
至此,关于video 标签 src 带有blob:http的 抓取的就写完了。但是每个网站的情况都不一样。
这里只是提供一种思路,比如刚开始写的 https://vimeo/ 这个网站就不是js,而是json里边包含的视频地址。
结论就是:
blob:https并不是一种协议,而是html5中blob对象在赋给video标签后生成的一串标记,blob对象对象包含的数据,浏览器内部会解析;
每次调用 URL.createObjectURL() 方法都会生成一个地址,这个地址代表着根据 blob 对象生成的资源入口,而这个资源入口存放于浏览器维护的一个 blob URL store 中
在web容器中的页面代码
浏览器访问后的页面代码
这是因为在浏览器中执行了如下js
FileReader.readAsDataURI 与 URL.createObjectURL(blob) 区别
- 通过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
更多推荐
blob/URL.createObjectURL()/reader.readAsDataURL/文件上传
发布评论