侧边栏壁纸
博主头像
孔子说JAVA博主等级

成功只是一只沦落在鸡窝里的鹰,成功永远属于自信且有毅力的人!

  • 累计撰写 297 篇文章
  • 累计创建 134 个标签
  • 累计收到 4 条评论

目 录CONTENT

文章目录

利用Blob进行文件分片上传并使用Blob URL(blob:http)下载图片

孔子说JAVA
2022-05-15 / 0 评论 / 0 点赞 / 172 阅读 / 10,527 字 / 正在检测是否收录...

BLOB (binary large object),二进制大对象,是一个可以存储二进制文件的容器。Blob 对象表示一个不可变、原始数据的类文件对象。它的数据可以按文本或二进制的格式进行读取,也可以转换成 ReadableStream 来用于数据操作。Blob 表示的不一定是JavaScript原生格式的数据。File 接口基于Blob,继承了 blob 的功能并将其扩展使其支持用户系统上的文件。

  • blob文档:https://developer.mozilla.org/zh-CN/docs/Web/API/Blob
  • 在计算机中,BLOB常常是数据库中用来存储二进制文件的字段类型。
  • 我们可以使用 Blob 对象隐藏真实的资源路径,在一定程度上可以起到数据的加密性,更多的是为了干扰爬虫。比如日常使用的一些音频,视频,图片,我们都可以使用其 Blob 二进制数据流来表征数据,而非使用 uri,就像经常用到的 image src 的 dataUrl。

1、Blob对象

Blob对象指的是字节序列,并且具有size属性,是字节序列中的字节总数,和一个type属性,它是小写的ASCII编码的字符串表示的媒体类型字节序列。

  • size:以字节数返回字节序列的大小。获取时,符合要求的用户代理必须返回一个FileReader或一个FileReaderSync对象可以读取的总字节数,如果Blob没有要读取的字节,则返回0 。
  • type:小写的ASCII编码字符串表示媒体类型Blob。在获取时,用户代理必须Blob以小写形式返回a类型的ASCII编码字符串,这样当它转换为字节序列时,它是可解析的MIME类型,或者是空字符串(0字节)如果是类型无法确定。

1.1 构造函数

创建blob对象本质上和创建一个其他对象的方式是一样的,都是使用Blob() 的构造函数来进行创建。 构造函数接受两个参数:

  • 第一个参数为一个数据序列,格式可以是ArrayBuffer, ArrayBufferView, Blob, DOMString
  • 第二个参数是一个包含以下两个属性的对象
  • type: MIME的类型,
  • endings: 决定第一个参数的数据格式。默认值为"transparent",用于指定包含行结束符n的字符串如何被写入。 它是以下两个值中的一个: “native”,表示行结束符会被更改为适合宿主操作系统文件系统的换行符; “transparent”,表示会保持blob中保存的结束符不变。
 var data1 = "a";
 var blob1 = new Blob([data1]);
 console.log(blob1); //输出:Blob {size: 1, type: ""}
 
 var debug = {hello: "world"};
 var blob = new Blob([JSON.stringify(debug, null, 2)],{type : 'application/json'});
 console.log(blob) // 输出 Blob(22) {size: 22, type: "application/json"}
 
 // 创建一个8字节的ArrayBuffer,在其上创建一个每个数组元素为2字节的“视图”
 var abf = new ArrayBuffer(8)
 var abv = new Int16Array(abf)
 var bolb_ArrayBuffer = new Blob(abv, {type : 'text/plain'})
 console.log(bolb_ArrayBuffer) //输出 Blob(4) {size: 4, type: "text/plain"}

1.2 slice方法

Blob对象有一个slice方法,返回一个新的 Blob对象,包含了源 Blob对象中指定范围内的数据。

slice(start, end, contentType)
  • start: 可选,代表 Blob 里的下标,表示第一个会被会被拷贝进新的 Blob 的字节的起始位置。如果传入的是一个负数,那么这个偏移量将会从数据的末尾从后到前开始计算。
  • end: 可选,代表的是 Blob 的一个下标,这个下标-1的对应的字节将会是被拷贝进新的Blob 的最后一个字节。如果你传入了一个负数,那么这个偏移量将会从数据的末尾从后到前开始计算。
  • contentType: 可选,给新的 Blob 赋予一个新的文档类型。这将会把它的 type 属性设为被传入的值。它的默认值是一个空的字符串。
var data = "abcdef";
var blob1 = new Blob([data]);
var blob2 = blob1.slice(0,3);

console.log(blob1); //输出:Blob {size: 6, type: ""}
console.log(blob2); //输出:Blob {size: 3, type: ""}

2、利用Blob进行文件分片上传

2.1 分片上传原理

我们在进行文件上传的时候,因为服务器的限制,会限制每一次上传到服务器的文件大小不会很大,这个时候我们就需要把一个需要上传的文件进行切割,然后分别进行上传到服务器。

分片上传需要解决两个问题:

  • 怎么切割?
  • 怎么得知当前传输的进度?

首先解决怎么切割的问题。因为File文件对象是继承于Blob对象的,因此File文件对象也拥有slice这个方法,我们可以使用这个方法将任何一个File文件进行切割。

Blob对象的slice方法可以用于文件分片上传:

  • 分片与并发结合,将一个大文件分割成多块,并发上传,极大地提高大文件的上传速度。
  • 当网络问题导致传输错误时,只需要重传出错分片,而不是整个文件。另外分片传输能够更加实时的跟踪上传进度。

分片上传逻辑如下:

  1. 获取要上传文件的File对象,根据chunk(每片大小)对文件进行分片
  2. 通过post方法轮循上传每片文件,其中url中拼接querystring用于描述当前上传的文件信息;post body中存放本次要上传的二进制数据片段
  3. 接口每次返回offset,用于执行下次上传

2.2 分片上传代码

initUpload();

//初始化上传
function initUpload() {
 var chunk = 100 * 1024; //每片大小
 var input = document.getElementById("file"); //input file
 input.onchange = function (e) {
  var file = this.files[0];
  var query = {};
  var chunks = [];
  if (!!file) {
   var start = 0;
   //文件分片
   for (var i = 0; i < Math.ceil(file.size / chunk); i++) {
    var end = start + chunk;
    chunks[i] = file.slice(start , end);
    start = end;
   }
   
   // 采用post方法上传文件
   // url query上拼接以下参数,用于记录上传偏移
   // post body中存放本次要上传的二进制数据
   query = {
    fileSize: file.size,
    dataSize: chunk,
    nextOffset: 0
   }

   upload(chunks, query, successPerUpload);
  }
 }
}

// 执行上传
function upload(chunks, query, cb) {
 var queryStr = Object.getOwnPropertyNames(query).map(key => {
  return key + "=" + query[key];
 }).join("&");
 var xhr = new XMLHttpRequest();
 xhr.open("POST", "http://xxxx/opload?" + queryStr);
 xhr.overrideMimeType("application/octet-stream");
 
 //获取post body中二进制数据
 var index = Math.floor(query.nextOffset / query.dataSize);
 getFileBinary(chunks[index], function (binary) {
  if (xhr.sendAsBinary) {
   xhr.sendAsBinary(binary);
  } else {
   xhr.send(binary);
  }

 });

 xhr.onreadystatechange = function (e) {
  if (xhr.readyState === 4) {
   if (xhr.status === 200) {
    var resp = JSON.parse(xhr.responseText);
    // 接口返回nextoffset
    // resp = {
    //  isFinish:false,
    //  offset:100*1024
    // }
    if (typeof cb === "function") {
     cb.call(this, resp, chunks, query)
    }
   }
  }
 }
}

// 每片上传成功后执行
function successPerUpload(resp, chunks, query) {
 if (resp.isFinish === true) {
  alert("上传成功");
 } else {
  //未上传完毕
  query.offset = resp.offset;
  upload(chunks, query, successPerUpload);
 }
}

// 获取文件二进制数据
function getFileBinary(file, cb) {
 var reader = new FileReader();
 reader.readAsArrayBuffer(file);
 reader.onload = function (e) {
  if (typeof cb === "function") {
   cb.call(this, this.result);
  }
 }
}

2.3 分片上传完整示例

改代码中还有我们常说的断点续传功能,说下简单思路:

  1. 拿到文件,保存文件唯一标识,如md5;
  2. 切割文件,分段上传;
  3. 每次上传一段,根据唯一性标识判断文件上传进度,直到文件的全部片段上传完毕。
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>upload</title>
    <script src="https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script>
</head>
<body>
    <input type="file" name="file" id="file">
    <button id="upload" onClick="upload()">upload</button>
    <script type="text/javascript">
        var bytesPerPiece = 1024 * 1024; // 每个文件切片大小定为1MB .
        var totalPieces;
        //发送请求
        function upload() {
            var blob = document.getElementById("file").files[0];
            var start = 0;
            var end;
            var index = 0;
            var filesize = blob.size;
            var filename = blob.name;

            //计算文件切片总数
            totalPieces = Math.ceil(filesize / bytesPerPiece);
            while(start < filesize) {
                end = start + bytesPerPiece;
                if(end > filesize) {
                    end = filesize;
                } // 匹配最后一个分片的情况

                var chunk = blob.slice(start,end);//切割文件    
                var sliceIndex= blob.name + index;
                var formData = new FormData();
                formData.append("file", chunk, sliceIndex);
                $.ajax({
                    url: 'http://localhost:9999/test.php',
                    type: 'POST',
                    cache: false,
                    data: formData,
                    processData: false,
                    contentType: false,
                }).done(function(res){ 

                }).fail(function(res) {

                });
                start = end;
                index++;
            }
        }
    </script>
</body>
</html>

3、使用Blob实现文件下载

3.1 Blob URL

blob协议的url使用时就像平时使用的url一样,可以作为图片请求地址,也可以作为文件请求地址。格式如下:

blob:http://XXX

image-1652181064757

生产中是很少会将一个资源以二进制流的方式返回给前端进行 Blob“加密”的,因为服务端需要将相应的资源打开读取,会消耗对应的内存,比如一个视频如果50M,那我们服务端返回其二进制数据时,就要消耗 50M 的服务器内存将文件载入,然后返回给前端,这代价略大,而且网络传输相应速度也不允许,如果返回个 500M 的二进制数据,估计就会有很大的卡顿了。

要使用 blob 来表征数据资源,需做到以下两点:

  1. 服务端返回的为资源的二进制数据
  2. 前端接收到二进制数据后,使用 URL.createObjectURL(blobData) 方法将服务端返回的二进制数据转换为 blob 的 url 资源挂载到相应的资源对象。

1)new Blob

new Blob() 存放二进制资源

2)URL.createObjectURL(blob)

URL.createObjectURL(blob) 会将 Blob存放的二进制资源转为url地址,这个URL的生命周期仅存在于它被创建的这个文档里,新的对象URL指向执行的File对象或者是Blob对象。

语法:

objectURL = URL.createObjectURL(blob || file);

参数:

  • File对象或者Blob对象

File对象,就是一个文件,比如我用input type="file"标签来上传文件,那么里面的每个文件都是一个File对象.

Blob对象,就是二进制数据,比如通过new Blob()创建的对象就是Blob对象.又比如,在XMLHttpRequest里,如果指定responseType为blob,那么得到的返回值也是一个blob对象.

注意点:

  • 每次调用createObjectURL的时候,一个新的URL对象就被创建了.即使你已经为同一个文件创建过一个URL. 如果你不再需要这个对象,要释放它,需要使用URL.revokeObjectURL()方法. 当页面被关闭,浏览器会自动释放它,但是为了最佳性能和内存使用,当确保不再用得到它的时候,就应该释放它.

3)URL.revokeObjectURL(url)

URL.revokeObjectURL(url) 释放一个通过URL.createObjectURL()创建的对象URL,当你要已经用过了这个对象URL,然后要让浏览器知道这个URL已经不再需要指向对应的文件的时候,就需要调用这个方法。

  • 具体的意思就是说,一个对象URL,使用这个url是可以访问到指定的文件的,但是我可能只需要访问一次,一旦已经访问到了,这个对象URL就不再需要了,就需要被释放掉,被释放掉以后这个对象URL就不再指向指定的文件了.

比如一张图片,我创建了一个对象URL,然后通过这个对象URL,我页面里加载了这张图,既然已经被加载,并且不需要再次加载这张图,那我就把这个对象URL释放,然后这个URL就不再指向这张图了.

语法:

window.URL.revokeObjectURL(objectURL);

参数:

  • objectURL 是一个通过URL.createObjectURL()方法创建的对象URL.

以上两个方法不支持低版本的浏览器。

3.2 Blob下载

大家都知道 a 标签是超链接,点击会跳转至 href 指定的地址。当我们给 a 标签加上 download 属性后,则会去 href 指定的url下载文件(这里一定是同源的)。Blob下载思路:

  1. 首先通过 document.createElement('a') 创建一个a标签,用 link 来接受
  2. 通过 link.setAttribute('download', 'filename.html') 或者直接用 link.download = 'filename.html' 去给 a 标签添加 download 属性
  3. 通过 window.URL.createObjectURL 将 blob二进制资源 转为 url地址并赋给link.href
  4. 然后执行 link.click() 即可下载

3.2.1 下载示例1

let html = `
  <div>
    <div>
      <p style="color: red;font-size: 20px;">张三</p>
    </div>
  </div>
`
const link = document.createElement('a')
link.setAttribute('download', 'zhangsan.html')
// link.download = 'zhangsan.html'
link.href = window.URL.createObjectURL(new Blob([html], { type: 'text/html' }))
link.click()

这段代码即可直接将 html 判断下载,下载的文件名称就是 zhangsan.html

3.2.2 下载示例2

<body>
  <input type="file" id="fileInput">
  <script>
    window.onload = function () {
      fileInput.onchange = function (e) {
        console.log(e.target.files[0])
        const link = document.createElement('a')
        link.href = window.URL.createObjectURL(e.target.files[0])
        // link.setAttribute('download', '1111.html')
        link.download = '1111.html'
        link.click()
      }
    }
  </script>
</body>

这个demo是 咱们去本地选中一个html文件,通过 e.target.files[0] 拿到的就是继承自 Blob 的二进制对象(所以不需要 new Blob()), 即可下载该文件 。

3.2.3 示例3-项目中的导出功能方法封装

export function download (url, params) {
  $http({
    method: 'get',
    url: url,
    params: params,
    responseType: 'blob' //arraybuffer
  }).then(response => {
    if (!response) {
      return
    }
    let link = document.createElement('a')
    link.href = window.URL.createObjectURL(new Blob([response.data]))
    link.target = '_blank'
    let filename = response.headers['content-disposition']
    link.download = decodeURI(filename)  // 下载的文件名称
    document.body.appendChild(link)  // 添加创建的 a 标签 dom节点
    link.click()  // 下载
    document.body.removeChild(link)  // 移除节点
  }).catch(error => {})
}

上面有用到decodeURI, 这里就简单介绍一下 decodeURL 和 encodeURI

  1. encodeURI() 函数可把字符串作为 URI 进行编码
  2. decodeURI() 函数可对 encodeURI() 函数编码过的 URI 进行解码

例子:

let a = '李四'
encodeURI(a)  //'%E6%9D%8E%E5%9B%9B'
let a = encodeURI('李四') // '%E6%9D%8E%E5%9B%9B'
decodeURI(a)  // '李四'

3.2.4 下载文件示例4

下面是一个下载文件的示例,直接调用即可实现文件下载

// file是要下载的文件(blob对象)
downloadHandler: function (file, fileName) {
 let link = document.createElement('a')
 link.href = window.URL.createObjectURL(file)
 link.download = fileName
 link.click()
 window.URL.revokeObjectURL(link.href)
 if (navigator.userAgent.indexOf('Firefox') > -1) {
 const a = document.createElement('a')
 a.addEventListener('click', function (e) {
  a.download = fileName
  a.href = URL.createObjectURL(file)
 })
 let e = document.createEvent('MouseEvents')
 e.initEvent('click', false, false)
 a.dispatchEv 
 ent(e)
 }
}

在从后台获取的数据接口中把返回类型设置为blob

var x = new XMLHttpRequest();
x.responseType = 'blob';  // 返回一个blob对象 

3.2.5 下载文件示例5,释放对象URL

通过ajax获取一张图片,显示在页面里。该例子在createObjectURL获取到对象资源后使用revokeObjectURL释放了该对象URL。

  1. 指定返回的数据格式为blob二进制数据.
  2. 通过返回的图片二进制数据来创建一个对象URL.
  3. 当图片加载完成后释放对象URL.

html:

<body>
    <button id="getPic">获取图片的Blob数据</button>
</body>

js:

//获取图片Blob数据
      document.getElementById('getPic').onclick = function(e){
        $.ajax({
          type:'GET',
          url:'img.png',
          resDataType:'blob',
          imgType:'png',
          success:function(resText,resXML){
            var img = document.createElement('img');
            var objectUrl = window.URL.createObjectURL(resText);
            img.src = objectUrl;
            img.onload = function(){
              window.URL.revokeObjectURL(objectUrl);
            };
            document.body.appendChild(img);
          },
          fail:function(err){
            console.log(err)
          }
        });
        e.preventDefault();
      }

4、Blob URL 和 Data URL的区别

Blob URL:

image-1652178039330

Data URL:

image-1652178056808

  1. Blob URL的长度一般比较短,但Data URL因为直接存储图片base64编码后的数据,往往很长,如上图所示,浏览器在显示Data URL时使用了省略号(…)。当显式大图片时,使用Blob URL能获取更好的可能性。
  2. Blob URL可以方便的使用XMLHttpRequest获取源数据,比如设置XMLHttpRequest返回的数据类型为blob
  3. Blob URL 只能在当前应用内部使用,把Blob URL复制到浏览器的地址栏中,是无法获取数据的。Data URL相比之下,就有很好的移植性,可以在任意浏览器中使用。
0

评论区