Blob/FileReader/ArrayBuffer/文件上传/Uint16Array

谈谈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:由 ArrayBufferArrayBufferViewBlobDOMString 等对象构成的,将会被放进 Blob;
  • options:可选的 BlobPropertyBag 字典,它可能会指定如下两个属性
    • type:默认值为 “”,表示将会被放入到 blob 中的数组内容的 MIME 类型。
    • endings:默认值为"transparent",用于指定包含行结束符\n的字符串如何被写入,不常用。

常见的 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方法:
  1. text():返回一个promise且包含blob所有内容的UTF-8格式的字符串;
var blob = new Blob(['大师哥王唯']);
blob.text().then(value => console.log(value))
  1. 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的子类;
  1. 使用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中
如下:

  • 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对象也有可能来自用户的拖放操作;

大文件切块处理的思路

使用 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方法:
  1. abort():中止读取操作;在返回时,readyState属性为DONE;

  2. 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);
    }
  }
  1. 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>
  1. 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);
    };
  }
  1. 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.readAsDataURI 与 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 种类型的视图,每一种视图都是一种构造函数。如下:

元素类型化数组字节描述
Int8Int8Array18 位有符号整数
Uint8Uint8Array18 位无符号整数
Uint8CUint8ClampedArray18 位无符号整数
Int16Int16Array216 位有符号整数
Uint16Uint16Array216 位无符号整数
Int32Int32Array432 位有符号整数
Uint32Uint32Array432 位无符号整数
Float32Float32Array432 位浮点
Float64Float64Array864 位浮点

来看看这些都是什么意思:

  • 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形式下载 具体看下面解析

文件下载通常有以下方式:

  1. 下载 a标签访问文件地址
  2. window.open(‘http://localhost:8087/upload/user.png’) 打开文件地址
  3. 后端提供一个接口 /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.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
在这里插入图片描述

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值