「多图预警」那些年,被blob虐过的程序猿觉醒了!

8,078 阅读18分钟

前言

本文以图文的方式深入浅出二进制的概念,前面的概念描述较为枯燥,但是非常重要!希望大家耐心往下看,后面有惊喜,定能让您虎躯一震~😉

Blob

Blob表示二进制类型的大对象,通常是影像、声音或多媒体文件,在 javaScript中Blob表示一个不可变、原始数据的类文件对象。

其构造函数如下:

new Blob(blobParts, options);
  • lobParts:数组类型,可以存放任意多个ArrayBuffer, ArrayBufferView, Blob或者DOMString(会编码为UTF-8),将它们连接起来构成Blob对象的数据。
  • options:可选项,用于设置blob对象的属性,可以指定如下两个属性:
    • type:存放到blob中数组内容的MIME类型(默认为"")。
    • endings:用于指定包含行结束符\n的字符串如何被写入。值为native表示行结束符会被更改为适合宿主操作系统文件系统的换行符(默认值为transparent表示会保持blob中保存的结束符不变)
DOMString 是一个UTF-16字符串。由于JavaScript已经使用了这样的字符串,所以DOMString直接映射到 一个String。
ArrayBuffer(二进制数据缓冲区)、ArrayBufferView(二进制数据缓冲区的array-like视图)

示例如下👇

  1. 创建一个包含domstring对象的blob对象
    const blob = new Blob(['<div>john</div>'], { type: 'text/xml' });
    console.log(blob); // Blob {size: 15, type: "text/xml"}
  1. 创建一个包含arraybuffer对象的blob对象
    var abf = new ArrayBuffer(8);
    const blob = new Blob([abf], { type: 'text/plain' });
    console.log(blob); // Blob {size: 8, type: "text/plain"}
  1. 创建一个包含arraybufferview对象的blob对象
    var abf = new ArrayBuffer(8);
    var abv = new Int16Array(abf);
    const blob = new Blob(abv, { type: 'text/plain' });
    console.log(blob); // Blob {size: 4, type: "text/plain"}

属性

Blob对象有两个属性,参见下表👇:

属性名描述
sizeBlob对象中所包含数据的大小。字节为单位。 只读。
type一个字符串,表明该Blob对象所包含数据的MIME类型。如果类型未知,则该值为空字符串。 只读。

方法

  • slice(start:number, end:number, contentType:DOMString):类似于数组的slice方法,将原始Blob对象按照指定范围分割成新的blob对象并返回,可以用作切片上传
    • start:开始索引,默认为0
    • end:结束索引,默认为最后一个索引
    • contentType:新Blob的MIME类型,默认情况下为空字符串
  • stream():返回一个能读取blob内容的ReadableStream
  • text():返回一个Promise对象且包含blob所有内容的UTF-8格式的 USVString
  • arrayBuffer():返回一个Promise 对象且包含blob所有内容的二进制格式的ArrayBuffer

将blob(或者file)二进制文件保存到formData进行网络请求(之后可以获取到图片的imageUrl可以用作图片展示或者后续的通过websocket发送图片地址)

File

File对象是一种特殊的Blob对象,继承了所有Blob的属性和方法,当然同样也可以用作formData二进制文件上传

File的获取:

下面我们分别使用input和拖放方式选择多张图片操作👇:

  • input获取本地文件
  <input type="file" multiple id="f" />
  <script>
    var elem = document.getElementById('f');
    elem.onchange = function (event) {
      var files = event.target.files;
      console.log(files); // [{{name: "1.jpg",lastModified: 1594369580771...},{name:'2.jpg',lastModified: 1596012406708...}]
      var file = files[0];
      console.log(file); // {name: "1.jpg",lastModified: 1594369580771,size: 22344,type: "image/jpeg"...}
      console.log(file instanceof File); //true
      console.log(files instanceof FileList); // true
      
      /* File继承Blob */
      console.log(file.__proto__.__proto__); // Blob {size: 22344, type: ""}
    };
  </script>
  • 拖放获取
    <div id="content" ondrop="drop(event)" ondragover="allowDrop(event);" />
    <script>
      function allowDrop(ev) {
        ev.preventDefault();
      }
      function drop(ev) {
        ev.preventDefault();
        const files = ev.dataTransfer.files;
        console.log(files); // [{{name: "1.jpg",lastModified: 1594369580771...},{name:'2.jpg',lastModified: 1596012406708...}]
        console.log(files instanceof FileList); // true
      }
    </script>
    <style type="text/css">
      #content {
        width: 500px;
        height: 500px;
        border: 1px solid brown;
      }
    </style>
    

为input元素添加multiple属性,允许用户选择多个文件,用户选择的每一个文件都是一个file对象,而FileList对象则是这些file对象的列表,代表用户选择的所有文件,是file对象的集合。

属性

File对象属性,参见下表👇:

属性名描述
lastModified引用文件最后修改日期
name文件名或文件路径
size以字节为单位返回文件的大小
type文件的 MIME 类型

方法

File 对象没有自己的实例方法,由于继承了 Blob 对象,因此可以使用 Blob 的实例方法slice()。

数据缓冲区

XHRFile APICanvas等等各种地方,读取了一大串字节流,如果用JS里的Array去存,又浪费,又低效。在编程中,数据缓冲区(或简称为缓冲区)是物理内存中中操作二进制数据的存储区(比硬盘驱动器访问快),用于在数据从一个位置移动到另一位置时存储临时数据,解释器借助存储二进制数据的内存缓冲区读取行。主内存中有一个正在运行的文件,如果解释器必须返回文件以读取每个位,则执行过程将耗费大量时间。为了防止这种情况,JavaScript使用数据缓冲区,该缓冲区将一些位存储在一起,然后将所有位一起发送给解释器。这样,JavaScript解释器就不必担心从文件数据中检索文件。这种方法节省了执行时间并加快了应用程序的速度。各种缓冲区类对数据执行有效的二进制操作,包括FileBlobArrayBufferArray。选择的方法决定了内存中缓冲区的内部结构。

Buffer

BufferNode.js提供的对象,前端没有。 它一般应用于IO操作,例如接收前端请求数据时候,可以通过Buffer相关的API创建一个专门存放二进制数据的缓存区对接收到的前端数据进行整合,一个Buffer类似于一个整数数组,但它对应于V8堆内存之外的一块原始内存。

ArrayBuffer、ArrayBufferView

ArrayBuffer

ArrayBuffer表示固定长度的二进制数据的原始缓冲区,它的作用是分配一段可以存放数据的连续内存区域,因此对于高密度的访问(如音频数据)操作而言它比JS中的Array速度会快很多,ArrayBuffer存在的意义就是作为数据源提前写入在内存中,因此其长度固定

先大致看下ArrayBuffer的功能:

ArrayBuffer对象的构造函数如下(length表示ArrayBuffer的长度)👇:

ArrayBuffer(length);

Array和ArrayBuffer的区别👇:

ArrayArrayBuffer
可以放数字、字符串、布尔值以及对象和数组等只能存放0和1组成的二进制数据
数据放在堆中数据放在栈中,取数据时更快
可以自由增减只读,初始化后固定大小,无论缓冲区是否为空,只能借助TypedArrays、Dataview写入

属性

ArrayBuffer对象属性,参见下表👇:

属性名描述
byteLength表示ArrayBuffer的大小

方法

  • slice:有两个参数👉begin表示起始,end表示结束点。方法返回一个新的 ArrayBuffer ,它的内容是这个ArrayBuffer的字节副本,从begin(包括),到end(不包括)。

ArrayBuffer不能直接操作,而是要通过TypedArrayDataView对象来操作,它们会将缓冲区中的数据转换为各种数据类型的数组,并通过这些格式来读写缓冲区的内容。👇

ArrayBufferView

由于ArrayBuffer对象不提供任何直接读写内存的方法,而ArrayBufferView对象实际上是建立在ArrayBuffer对象基础上的视图,它指定了原始二进制数据的基本处理单元,通过ArrayBufferView对象来读取ArrayBuffer对象的内容。类型化数组(TypedArrays)和DataView是ArrayBufferView的实例。

TypedArrays

类型化数组(TypedArrays)是JavaScript中新出现的一个概念,专为访问原始的二进制数据而生,本质上,类型化数组和ArrayBuffer是一样的,只不过是他具备读写功能

类型数组的类型有:👇:

名称大小 (以字节为单位)说明
Int8Array18位有符号整数
Uint8Array18位无符号整数
Int16Array216位有符号整数
Uint16Array216位无符号整数
Int32Array432位有符号整数
Uint32Array432位无符号整数
Float32Array432位浮点数
Float64Array864位浮点数

类型转换如图👇: 举一些代码例子展示如何转换:

// 创建一个8字节的ArrayBuffer  
var b = new ArrayBuffer(8);  
  
// 创建一个指向b的视图v1,采用Int32类型,开始于默认的字节索引0,直到缓冲区的末尾  
var v1 = new Int32Array(b);  // Int32Array(2) [0, 0]
v1[0] = 1
console.log(v1); // Int32Array(2) [1, 0]
  
// 创建一个指向b的视图v2,采用Uint8类型,开始于字节索引2,直到缓冲区的末尾  
var v2 = new Uint8Array(b, 2);  // Uint8Array(6) [0, 0, 0, 0, 0, 0]
  
// 创建一个指向b的视图v3,采用Int16类型,开始于字节索引2,长度为2  
var v3 = new Int16Array(b, 2, 2);  // Int16Array(2) [0, 0]

因为普通Javascript数组使用的是Hash查找方式,而类型化数组直接访问固定内存,因此,速度很赞,比传统数组要快!同时,类型化数组天生处理二进制数据,这对于XMLHttpRequestcanvaswebGL等技术有着先天的优势。

TypedArray的应用如何拼接两个音频文件

fetch请求音频资源 -> ArrayBuffer -> TypedArray -> 拼接成一个 TypedArray -> ArrayBuffer -> Blob -> Object URL

DataView

DataView对象可以在ArrayBuffer中的任意位置读取和存储不同类型的二进制数据。

创建DataView的语法如下:

var dataView = new DataView(DataView(buffer, byteOffset[可选], byteLength[可选]);
属性

DataView对象有三个属性,参见下表👇:

属性名描述
buffer表示ArrayBuffer
byteOffset指缓冲区开始处的偏移量
byteLength指缓冲区部分的长度
方法
  • setint8():从DataView起始位置以byte为计数的指定偏移量(byteOffset)处存储一个8-bit数(一个字节)
  • getint8():从DataView起始位置以byte为计数的指定偏移量(byteOffset)处获取一个8-bit数(一个字节)

除此之外还有getInt16, getUint16, getInt32, getUint32... 使用方法一致,这里就不一一例举

用法如下👇:

let buffer = new ArrayBuffer(32);
let dataView = new DataView(buffer,0);
dataView.setInt16(1,56);
dataView.getInt16(1); // 56

FileReader

我们无法直接访问Blob或者文件对象的内容,如果想要读取它们并转化为其他格式的数据,可以借助FileReader对象的API进行操作

  • readAsText(Blob):将Blob转化为文本字符串
  • readAsArrayBuffer(Blob): 将Blob转为ArrayBuffer格式数据
  • readAsDataURL(): 将Blob转化为Base64格式的DataURL

使用分别如下👇:

    const blob = new Blob(['<xml>foo</xml>'], { type: 'text/xml' });
    console.log(blob); // Blob(14) {size: 14, type: "text/xml"}

    const reader = new FileReader();
    reader.onload = () => {
      console.log(reader.result);
    };
    reader.readAsText(blob); // <xml>foo</xml>
    reader.readAsArrayBuffer(blob); // ArrayBuffer(14) {}
    reader.readAsDataURL(blob); // data:text/xml;base64,PHhtbD5mb288L3htbD4

下面我们尝试把一个文件的内容通过字符串的方式读取出来:

<input type="file" id='f' />
<script>
  document.getElementById('f').addEventListener('change', function (e) {
    var file = this.files[0];
    // 首先,需要创建一个FileReader的实例。
    const reader = new FileReader();
    reader.onload = function () {
        // 在加载完成时回调
        const content = reader.result;
        console.log(content);
    }
    reader.readAsText(file); // 将blob转化为文本字符串读取
  }, false);
</script>

读取结果如下👇:

BlobURL

BlobURL(ObjectURL)是一种伪协议,只能由浏览器在内部生成,我们知道script/img/video/iframe等标签的src属性和background的url可以通过url和base64来显示,我们同样可以把blob或者file转换为url生成BlobURL来展示图像,BlobURL允许Blob和File对象用作图像,下载二进制数据链接等的URL源。

图像展示👇:

  <div id="content">
    <input type="file" multiple id="f" />
  </div>
  <script>
    const elem = document.getElementById('f');
    const content = document.getElementById('content');
    
    // 根据不同浏览器封装一个转换BlobUrl的方法:file可以是File对象也可以是Blob对象
    const getObjectURL = (file) => {
      let url;
      if (window.createObjectURL) {
        url = window.createObjectURL(file);
      } else if (window.URL) {
        url = window.URL.createObjectURL(file);
      } else if (window.webkitURL) {
        url = window.webkitURL.createObjectURL(file);
      }
      return url;
    };

    elem.onchange = function (event) {
      const files = event.target.files;
      const file = files[0];
      const img = document.createElement('img');
      img.src = getObjectURL(file);
      content.appendChild(img);
    };
  </script>

我们查看demo页面这个mm图片元素,会发现其URL地址既不是传统HTTP,也不是Base64 URL,而是blob:开头的字符串,可以通过将其放在地址栏中进行检查。

文件下载👇:

<body>
 <button onclick="download()">download.txt</button>

 <script>
      const getObjectURL = (file) => {
        let url;
        if (window.createObjectURL) {
          url = window.createObjectURL(file);
        } else if (window.URL) {
          url = window.URL.createObjectURL(file);
        } else if (window.webkitURL) {
          url = window.webkitURL.createObjectURL(file);
        }
        return url;
      };
      function download() {
        const fileName = 'download.txt';
        const myBlob = new Blob(['johnYu'], { type: 'text/plain' });
        downloadFun(fileName, myBlob);
      }
      function downloadFun(fileName, blob) {
        const link = document.createElement('a');
        link.href = getObjectURL(blob);
        link.download = fileName;
        link.click();
        link.remove();
        URL.revokeObjectURL(link.href);
      }
    </script>
  </body>

点击按钮下载文档,文档内容为:johnYu

这里不调用revokeObjectURL时访问chrome://blob-internals/可以看到当前内部的blob文件列表: 不再使用的BlobUrl后续会自动清除(关闭浏览器也会自动清除),但是最好使用URL.revokeObjectURL(url)手动清除它们:

URL.revokeObjectURL('blob:http://127.0.0.1:5500/d2a9a812-0dbf-41c5-a96b-b6384d33f281');

执行后再次访问chrome://blob-internals/可以看到文件已经被清除

dataURL

dataURL允许内容的创建者将较小的文件嵌入到文档中。与常规的URL使用场合类似

其语法格式格式如下👇:

data:[<mediatype>][;base64],data
  • data:前缀
  • mediatype表明数据类型,是一个MIME类型字符串,如image/jpeg表示一个JPEG图片文件。如果省略,默认值为text/plain;charset=US-ASCII
  • base64:标志位(如果是文本,则可选)
  • data:数据本身

如何获取DataUrl

  1. 上面示例中使用的方法readAsDataURL()就是将Blob转化为Base64格式的DataUrl;
  2. 使用原生Web API编码/解码

Javascript中有两个函数负责编码和解码base64字符串,分别是atob和btoa。两者都只针对Data URL中的data进行处理。

btoa('<xml>foo</xml>') // "PHhtbD5mb288L3htbD4="
atob('PHhtbD5mb288L3htbD4=') // "<xml>foo</xml>"
  • atob(): 负责解码已经使用base64编码了的字符串。
  • btoa(): 将二进制字符串转为base64编码的ASCII字符串。
  1. Canvas的toDataURL方法:

Canvas提供了toDataURL方法,用于获取canvas绘制内容,将其转为base64格式。

  <body>
    <canvas id="canvas" width="200" height="50"></canvas>
    <textarea id="content" style="width: 200px; height: 200px"></textarea>

    <script>
      var canvas = document.getElementById('canvas');
      if (canvas.getContext) {
        var ctx = canvas.getContext('2d');
        // canvas的绘制
        ctx.font = 'Bold 20px Arial';
        ctx.textAlign = 'left';
        ctx.fillStyle = 'purple';
        ctx.fillText('johnYu', 10, 30);
        // 获取 Data URL
        document.getElementById('content').value = canvas.toDataURL();
      }
    </script>
  </body>

如下图所示,文本框中的内容即为canvas中绘制内容的base64格式。

如果我们将前面的返回结果data:text/xml;base64,PHhtbD5mb288L3htbD4=放在浏览器的地址栏中,则可以看到显示的内容。

DataUrl的使用

  1. 由于可以将其用作URL的替代,因此DataURL和BlobUrl一样可以在script/img/video/iframe等标签的src属性和background的url中使用,用法与BlobUrl基本一致,只需要将前面的elem.onchange做如下改造
<body>
    <div id="content">
      <input type="file" multiple id="f" />
    </div>
    <script>
      const elem = document.getElementById('f');
      const content = document.getElementById('content');

      elem.onchange = function (event) {
        const files = event.target.files;
        const file = files[0];
        const img = document.createElement('img');
-        img.src = getObjectURL(file);
+        const reader = new FileReader();
+        reader.onload = function () {
+          img.src = reader.result;
+        };
+        reader.readAsDataURL(file);
        content.appendChild(img);
      };
    </script>
  </body>
  1. 由于数据本身由URL表示,因此可以将其保存在Cookie中传递给服务器。
  2. 当图片的体积太小,占用一个HTTP会话不是很值得时。
  3. 当访问外部资源很麻烦或受限时
  4. DataUrl不会被浏览器缓存,但是小部分会通过css缓存,在下面例子中,DataUrl的使用是完全符合场景的。它避免了让这个小小的背景图片独自产生一次HTTP请求,而且,这个小图片还能同CSS文件一起被浏览器缓存起来,重复使 用,不会每次使用时都加载一次。只要这个图片不是很大,而且不是在CSS文件里反复使用,就可以DataUrl方法呈现图片降低页面的加载时间,改善用户的浏览体验。
 background-image: url("data:image/gif;base64,R0lGODlhAwADAIAAAP///8zMzCH5BAAAAAAALAAAAAADAAMAAAIEBHIJBQA7"); 
  1. 作为下载连接使用
  <script>
    const createDownload = (fileName, content) => {
      const blob = new Blob([content]);
      const reader = new FileReader();
      const link = document.createElement('a');
      link.innerHTML = fileName;
      link.download = fileName;
      reader.onload = () => {
        link.href = reader.result;
        document.getElementsByTagName('body')[0].appendChild(link);
      };
      reader.readAsDataURL(blob);
    };

    createDownload('download.txt', 'johnYu');
  </script>

点击a标签后后下载文本内容为johnYu的txt文件,在下面的BlobURL同样可以实现👇


区别

BlobURL基本用法与DataUrl相同,都可以通过将其放在地址栏中进行检查也可以用作普通URL使用。

但是,存在以下差异。

  1. BlobUrl始终是唯一字符串,即时你每次传递相同的Blob,每次也会生成不同的BlobUrl;DataUrl值跟随blob变化;
  2. 就BlobUrl而言,它并不代表数据本身,数据存储在浏览器中,BlobUrl只是访问它的key。数据会一直有效,直到关闭浏览器或者手动清除。而DataUrl是直接编码的数据本身。因此即使将BlobUrl传递给服务器等也无法访问数据。关闭浏览器后仍然可以在地址栏访问后DataUrl,但是访问不到BlobUrl
  3. BlobUrl的长度一般比较短,但DataUrl因为直接存储图片base64编码后的数据,往往很长(Base64编码的数据体积通常会比二进制格式的图片体积大1/3。),因此当显式大图片时,使用BlobUrl能获取更好的可能性,速度和内存比DataUrl更有效
  4. BlobUrl可以方便的使用XMLHttpRequest获取源数据(xhr.responseType = 'blob')。对于DataUrl,并不是所有浏览器都支持通过XMLHttpRequest获取源数据的
  <body>
    <button onclick="download1()">XMLHttpRequest下载</button>
    <button onclick="download2()">fetch下载</button>
    <img id="img" />
    <script>
      var eleAppend = document.getElementById('forAppend');
      const url = 'https://sf3-ttcdn-tos.pstatp.com/img/user-avatar/9ecb4e119c26e64b8b4ec5258f159b3b~300x300.image';
      const pingan = document.querySelector('#pingan');
      function download1() {
        const xhr = new XMLHttpRequest();
        xhr.open('get', url, true);
        xhr.responseType = 'blob';
        xhr.onload = function () {
          if (this.status == 200) {
            renderImage(this.response);
          }
        };
        xhr.send(null);
      }
      function download2() {
        fetch(url)
          .then((res) => {
            return res.blob();
          })
          .then((myBlob) => {
            renderImage(myBlob);
          });
      }

      function renderImage(blob) {
        window.URL = window.URL || window.webkitURL;
        var img = document.getElementById('img');
        img.onload = function (e) {
          window.URL.revokeObjectURL(img.src); // 清除释放
        };
        img.src = window.URL.createObjectURL(blob);
      }
    </script>
  </body>
  1. BlobUrl除了可以用作图片资源的网络地址,BlobUrl也可以用作其他资源的网络地址,例如html文件、json文件等,为了保证浏览器能正确的解析BlobUrl返回的文件类型,需要在创建Blob对象时指定相应的type
    const createDownload = (fileName, content) => {
      const blob = new Blob([content], { type: 'text/html' });
      const link = document.createElement('a');
      link.innerHTML = fileName;
      link.download = fileName;
      link.href = getObjectURL(blob);
      document.getElementsByTagName('body')[0].appendChild(link);
    };
    createDownload('download.html', '<button>foo</button>');

6. DataUrl不会被浏览器缓存,这意味着每次访问这样页面时都被下载一次。这是一个使用效率方面的问题——尤其当这个图片被整个网站大量使用的时候。但是小部分可以通过css缓存

canvas

Canvas对象元素负责在页面中设定一个区域,然后就可以通过 JavaScript 动态地在这个区域中绘制图形。

方法

  • toDataURL(type, encoderOptions)):以指定格式返回 DataUrl,该方法接收两个可选参数
    • type:表示图片格式,默认为 image/png
    • encoderOptions:表示图片的质量,在指定图片格式为 image/jpeg 或 image/webp 的情况下,可以从 0 到 1 的区间内选择图片的质量。如果超出取值范围,将会使用默认值 0.92,其他参数会被忽略。
  • toBlob(callback, type, encoderOptions):创造Blob对象, 用于展示canvas的图片,默认图片类型是image/png,分辨率是96dpi
    • callback: 参数是blob对象的回调函数
  • getImageData(x,y,width,height):返回 ImageData 对象,该对象拷贝了画布指定矩形的像素数据。
    • x: 开始复制的左上角位置的 x 坐标。
    • y: 开始复制的左上角位置的 y 坐标。
    • width: 将要复制的矩形区域的宽度。
    • height: 将要复制的矩形区域的高度。
  • putImageData(imgData,x,y,dirtyX,dirtyY,dirtyWidth,dirtyHeight):将图像数据(从指定的 ImageData 对象)放回画布上。
    • imgData: 规定要放回画布的 ImageData 对象。
    • x: ImageData 对象左上角的 x 坐标,以像素计。
    • y: ImageData 对象左上角的 y 坐标,以像素计。
    • dirtyX: 可选。水平值(x),以像素计,在画布上放置图像的位置。
    • dirtyY: 可选。水平值(y),以像素计,在画布上放置图像的位置。
    • dirtyWidth: 可选。在画布上绘制图像所使用的宽度。
    • dirtyHeight: 可选。在画布上绘制图像所使用的高度。

应用场景

当我们需要获取到canvas的内容,可以用到toDataURLtoBlob属性(可用于签名,图片剪裁,图片压缩等场景),putImageDatagetImageData可以用于图片灰度或者复制时使用(见后面的使用场景章节👇)

获取内容:

<body>
    <div id="content">
      <button onclick="drawnImg()">绘制图像</button>
      <button onclick="getImg()">获取图像</button>
      <canvas style="border: 1px solid black" id="drawing" width="200" height="200">A drawing of something.</canvas>
      <img src="./timg.jpg" alt="" />
    </div>
    <script>
      var drawing = document.getElementById('drawing');
      var quality = 0.3;
      const imgType = 'image/jpeg';

      var drawnImg = function () {
        if (drawing.getContext) {
          var context = drawing.getContext('2d');
          //取得图像的数据 URI
          var image = document.images[0];
          context.drawImage(image, 20, 20, 100, 100);
        }
      };
      var getImg = async function () {
        const content = getContent('base64');
        console.log(content);
        const content1 = await getContent('file');
        console.log(content1);
      };
      var getContent = function (type) {
        switch (type) {
          case 'base64':
            {
              const imgURL = drawing.toDataURL(imgType, quality);
              return imgURL;
            }
            break;
          case 'file':
            {
              // 转为文件格式
              return new Promise((resolve) => {
                drawing.toBlob(
                  (blob) => {
                    resolve(blob);
                  },
                  imgType,
                  quality
                );
              });
            }
            break;
        }
      };
    </script>
  </body>

关系及转换

字符串 → Uint8Array

    var str = 'ab';
    console.log(Uint8Array.from(str.split(''), (e) => e.charCodeAt(0))); // Uint8Array(2) [97, 98]

Uint8Array → 字符串

    var u8 = Uint8Array.of(97, 98);
    console.log(Array.from(u8, (e) => String.fromCharCode(e)).join('')); // ab

字符串 → DataUrl

    var str = 'ab';
    console.log('data:application/octet-stream;base64,' + btoa(str)); // data:application/octet-stream;base64,YWI=

DataUrl -> 字符串

    var data = 'data:application/octet-stream;base64,YWI=';
    console.log(atob(data.split(',')[1])); // ab

Uint8Array -> ArrayBuffer

    var u8 = Uint8Array.of(1, 2);
    console.log(u8.buffer); // ArrayBuffer(2) {}

ArrayBuffer -> Uint8Array

    var buffer = new ArrayBuffer(2);
    console.log(new Uint8Array(buffer)); // Uint8Array(2) [0, 0]

ArrayBuffer -> DataView

    var buffer = new ArrayBuffer(2);
    var dataView = new DataView(buffer, 0); // DataView(2) {}

DataView -> ArrayBuffer

    console.log(dataView.buffer); // ArrayBuffer(2) {}

ArrayBuffer → Blob

    var buffer = new ArrayBuffer(32);
    var blob = new Blob([buffer]);  // Blob {size: 32, type: ""}

UintXXArray → Blob

    var u8 = Uint8Array.of(97, 32, 72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33);
    var blob = new Blob([u8]);

字符串 → Blob

    var blob = new Blob(['Hello World!'], {type: 'text/plain'}); // Blob {size: 12, type: "text/plain"}

以上都是用new Blob()转blob

DataUrl -> blob

    var data = 'data:application/octet-stream;base64,YWI=';
    function dataURLtoBlob(dataurl) {
      var arr = dataurl.split(','),
        mime = arr[0].match(/:(.*?);/)[1],
        bstr = atob(arr[1]),
        n = bstr.length,
        u8arr = new Uint8Array(n);

      while (n--) {
        u8arr[n] = bstr.charCodeAt(n);
      }
      return new Blob([u8arr], { type: mime });
    }
    console.log(dataURLtoBlob(data)); // Blob {size: 2, type: "application/octet-stream"}

Blob →

需要用到FileReader的Api转换readAsText(Blob)、readAsArrayBuffer(Blob)、readAsDataURL(),但是需要异步执行

    var blob = new Blob(['a Hello world!'], { type: 'text/plain' });
    var reader = new FileReader();
    reader.readAsText(blob, 'utf-8');
    reader.onload = function (e) {
      console.info(reader.result); // a Hello world!
    };
    reader.onerror = function (e) {
      console.error(reader.error);
    };

可以用promise做多次转换

    var blob = new Blob(['a Hello world!'], { type: 'text/plain' });
    function read(blob) {
      var fr = new FileReader();
      var pr = new Promise((resolve, reject) => {
        fr.onload = (eve) => {
          resolve(fr.result);
        };
        fr.onerror = (eve) => {
          reject(fr.error);
        };
      });

      return {
        arrayBuffer() {
          fr.readAsArrayBuffer(blob);
          return pr;
        },
        binaryString() {
          fr.readAsBinaryString(blob);
          return pr;
        },
        dataURL() {
          fr.readAsDataURL(blob);
          return pr;
        },
        text() {
          fr.readAsText(blob);
          return pr;
        },
      };
    }
    var pstr1 = read(blob).binaryString();
    var pstr2 = read(blob)
      .arrayBuffer()
      .then((e) => Array.from(new Uint8Array(e), (e) => String.fromCharCode(e)).join(''));
    Promise.all([pstr1, pstr2]).then((e) => {
      console.log(e[0]); // a Hello world!
      console.log(e[0] === e[1]); // true
    });

应用场景

图像灰度化

这里主要用到canvasimageData的转换

<body>
    <button onclick="drawngray()">黑白图片</button>
    <img src="./syz.jpg" alt="" />
    <canvas id="myCanvas">canvas</canvas>
    <script>
      var drawngray = function () {
        var myCanvas = document.getElementById('myCanvas');
        if (myCanvas.getContext) {
          var context = myCanvas.getContext('2d');
          var image = document.images[0];
          // 动态设置canvas的大小
          myCanvas.width = image.width;
          myCanvas.height = image.height;
          var imageData, data, i, len, average, red, green, blue, alpha;
          //绘制原始图像
          context.drawImage(image, 0, 0);
          //取得图像数据
          imageData = context.getImageData(0, 0, image.width, image.height);
          data = imageData.data;
          for (i = 0, len = data.length; i < len; i += 4) {
            red = data[i];
            green = data[i + 1];
            blue = data[i + 2];
            // alpha = data[i + 3];
            //求得 rgb 平均值
            average = Math.floor((red + green + blue) / 3);
            //设置颜色值,透明度不变
            data[i] = average;
            data[i + 1] = average;
            data[i + 2] = average;
          }

          //回写图像数据并显示结果
          imageData.data = data;
          context.putImageData(imageData, 0, 0);
        }
      };
    </script>
  </body>

除次之外getImageData和putImageData还可以用作cavas图片复制:https://www.w3school.com.cn/tiy/t.asp?f=html5_canvas_getimagedata

图片压缩

在前端要实现图片压缩,我们可以利用 Canvas 对象提供的 toDataURL() 方法

compress.js

const MAX_WIDTH = 800; // 图片最大宽度

function compress(base64, quality, mimeType) {
  let canvas = document.createElement('canvas');
  let img = document.createElement('img');
  img.crossOrigin = 'anonymous';
  return new Promise((resolve, reject) => {
    img.src = base64;
    img.onload = () => {
      let targetWidth, targetHeight;
      if (img.width > MAX_WIDTH) {
        targetWidth = MAX_WIDTH;
        targetHeight = (img.height * MAX_WIDTH) / img.width;
      } else {
        targetWidth = img.width;
        targetHeight = img.height;
      }
      canvas.width = targetWidth;
      canvas.height = targetHeight;
      let ctx = canvas.getContext('2d');
      ctx.clearRect(0, 0, targetWidth, targetHeight); // 清除画布
      ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
      // 通过toDataURL压缩后的base64
      let imageData = canvas.toDataURL(mimeType, quality / 100);
      resolve(imageData);
    };
  });
}

test.html

  <body>
    <input type="file" accept="image/*" onchange="loadFile(event)" />
    <script src="./compress.js"></script>
    <script>
      function dataUrlToBlob(base64) {
        var arr = base64.split(','),
          mime = arr[0].match(/:(.*?);/)[1],
          bstr = atob(arr[1]),
          n = bstr.length,
          u8arr = new Uint8Array(n);

        while (n--) {
          u8arr[n] = bstr.charCodeAt(n);
        }
        return new Blob([u8arr], { type: mime });
      }

      function uploadFile(url, blob) {
        let formData = new FormData();
        let request = new XMLHttpRequest();
        // 封装到FormData中进行文件的上传
        formData.append('image', blob);
        request.open('POST', url, true);
        request.send(formData);
      }

      const loadFile = function (event) {
        const reader = new FileReader();
        reader.onload = async function () {
          let compressedDataURL = await compress(reader.result, 90, 'image/jpeg');
          // 压缩后将base64转为Blob 对象减少传输数据量
          let compressedImageBlob = dataUrlToBlob(compressedDataURL);
          uploadFile('https://httpbin.org/post', compressedImageBlob);
        };
        // 获取用户选取的图片文件,通过FileReader转化成base64
        reader.readAsDataURL(event.target.files[0]);
      };
    </script>
  </body>

分片上传

<body>
    <input type="file" name="file" onchange="selfile();" />

    <script>
      const url = 'https://httpbin.org/post';
      /**
       * @param file 原始文件
       * @param chunkSize 默认每次上传分片大小
       */
      async function chunkedUpload(file, chunkSize = 1024 * 1024 * 5) {
        // 将文件拆分成chunkSize大小的分块,然后每次请求只需要上传这一个部分的分块即可
        for (let start = 0; start < file.size; start += chunkSize) {
          // File对象继承自Blob对象,因此可以使用slice方法对大文件进行切
          const chunk = file.slice(start, start + chunkSize + 1);
          const fd = new FormData();
          fd.append('data', chunk);

          await fetch(url, { method: 'post', body: fd })
            .then((res) => res.text())
            .then((res) => console.log(res)); // 打印上传结果
        }
      }

      function selfile() {
        let file = document.querySelector('[name=file]').files[0];

        // 自定义分片大小
        const LENGTH = 1024 * 1024 * 1;
        chunkedUpload(file, LENGTH);
      }
    </script>
  </body>

服务器接收到这些切片后,再将他们拼接起来就可以了,下面是PHP拼接切片的示例代码:

$filename = './upload/' . $_POST['filename'];//确定上传的文件名
//第一次上传时没有文件,就创建文件,此后上传只需要把数据追加到此文件中
if(!file_exists($filename)){
    move_uploaded_file($_FILES['file']['tmp_name'],$filename);
}else{
    file_put_contents($filename,file_get_contents($_FILES['file']['tmp_name']),FILE_APPEND);
    echo $filename;
}

测试时记得修改nginx的server配置,否则大文件可能会提示413 Request Entity Too Large的错误。

server {
	// ...
	client_max_body_size 50m;
}

参考文章 📜

❤️ 理解DOMString、Document、FormData、Blob、File、ArrayBuffer数据类型

❤️ 聊聊JS的二进制家族:Blob、ArrayBuffer和Buffer

❤️ 你不知道的 Blob

扩展 🏆

如果你觉得本文对你有帮助,可以查看我的其他文章❤️:

👍 vue3实战笔记 | 快速入门🚀

👍 10个简单的技巧让你的 vue.js 代码更优雅🍊

👍 零距离接触websocket🚀

👍 Web开发应了解的5种设计模式

👍 Web开发应该知道的数据结构

👍 如何在JavaScript中获取屏幕,窗口和网页大小