前端中的二进制流

吐槽君 分类:javascript

说说二进制流

我们通常将图片/音频等文件用二进制流来表示.

计算机的存储在物理上是都二进制的,所以文本文件与二进制文件的区别并不是物理上的,而是逻辑上的。

可能有人要问了,为什么要用二进制来表示图片呢?我们来看看二进制的优势:

  1. 二进制文件比较节约空间,在储存字符型数据时与普通存储并没有差别。但是在储存数字,特别是实型数字时,二进制更节省空间;
  2. 内存中参加计算的数据都是用二进制无格式储存起来的,因此,使用二进制储存到文件就更快捷。如果储存为文本文件,则需要一个转换的过程。在数据量很大的时候,两者就会有明显的速度差别了;
  3. 涉及到一些比较精确的数据,使用二进制储存不会造成有效位的丢失;

看到这里,相信大家已经对二进制流有了一定的认识。既然提到了存储,补充说明一下图片在后端存储方式:

  • 其一:可以将图片以独立文件的形式存储在服务器的指定文件夹中,再将路径存入数据库字段中;
  • 其二:将图片转换成二进制流,直接存储到数据库的 Image 类型字段中;

base64 编码

我们有时候会在网页中看到这样的代码:

data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAb4AAABYCAYAAACK55LCAAAgAElEQVR4Xuy9aXNc2ZEleN4e+wIgsO8ACXAnk8mUUqoqldQ9Yz3W/an/wsxvnA/TM2NWizKVmczkTgIg9h0IxL6+fez4DbCo7JQqpZLZfGBARjEJBOK95/de9+PHj3toO//7f40x/BpaYGiBoQWGFhha4BOxgDYMfJ/ISg8fc2iBoQWGFhhaQCwwDHzDjTC0wNACQwsMLfBJWWAY
...
 

这就是 base64 格式的数据,代表着一张图片的数据,专业术语叫:Data URI scheme

Base64是一种基于64个可打印字符来表示二进制数据的表示方法。

表示方式:

由于{\displaystyle 2^{6}=64},所以每6个 二进制位(bit) 为一个单元,对应某个可打印字符。3个 字节(byte)有24个bit,对应于4个Base64单元,即3个字节可由4个可打印字符来表示。

编码字符的情况下,由于 Base64 仅可对 ASCII 字符进行编码,也就是单字节字符(可以理解为英文和数字,还有一些简单符号)。若编码中文字符串,则需先转换为 ASCII 字符。

img

在Base64中的可打印字符包括 字母A-Za-z、数字0-9,这样共有62个字符,此外两个可打印符号在不同的系统中而不同。

转换方式:

  • 编码

    const base64 = window.btoa('abc'); // YWJj
     
  • 解码

    const initString = window.atob(base64); // abc
     

但是 btoa、atob 仅支持对 ASCII 字符编码,也就是单字节字符,而我们平时的中文都是 3-4 字节的字符。

因此可以先将中文字符转为 utf-8 的编码,将 utf-8 的编码当做字符,这样就可以对多个单字节字符进行编码。往下看。

encodeURIComponent & decodeURIComponent

chrome 上输入网址 http://zh.wikipedia.org/wiki/春节,然后复制下来,你会发现变成了 https://zh.wikipedia.org/wiki/%E6%98%A5%E8%8A%82

我们知道,"春"和"节"的 utf-8 编码分别是"E6 98 A5"和"E8 8A 82",因此,"%E6%98%A5%E8%8A%82"就是按照顺序,在每个字节前加上 % 而得到的。

这也就是 encodeURL 的功能:将非 ACSII 码的字符进行 utf-8 编码。 encodeURLComponent 与它不同的地方在于,对整个 url 进行编码(http%3A%2F%2Fzh.wikipedia.org%2Fwiki%2F%E6%98%A5%E8%8A%82)。

所以接上文,我们可以对 url 进行编码后,将这个 utf-8 编码作为 ACSII 字符,进行 base64。

比如我们在转换中文的时候:

  • 编码(多字节)

    const base64 = window.btoa(encodeURIComponent('春节')); // JUU2JTk4JUE1JUU4JThBJTgy
     
  • 解码(多字节)

    const initString = decodeURIComponent(window.atob(base64)); // 春节
     

Blob 对象(二进制容器)

Blob (binary large object),二进制大对象,是一个可以存储二进制文件的容器

注意! Blob对象只是二进制数据的容器,本身并不能操作二进制。如要操作请看下文FileReader API。

构建方式:

const blob = new Blob(['something...'])
 

这是我转换的一个 Blob 对象:

Blob {
  name: "图片示例:jartto.png",
  preview: "blob:file:///f3823a2a-2908-44cb-81e2-c19d98abc5d1",
  size: 47396,
  type: "image/png",
}
 

请注意,这里的 preview 属性是可以拿出来做图片预览的。

base64 与 Blob 互转

在日常开发中, 最常见的便是将blobbase64之间相互转换.

// blob to base64
function blobTobase64(blob) {
    const fileReader = new FileReader()
	let base64 = ''
	fileReader.onload = () => {
  		base64 = fileReader.result // 读取base64
	}
	fileReader.readAsDataURL(blob) // 读取blob
}
// base64 to blob
function dataURItoBlob(dataURI) {
  var mimeString = dataURI
    .split(',')[0]
    .split(':')[1]
    .split(';')[0] // mime类型
  var byteString = atob(dataURI.split(',')[1]) //base64 解码
  var arrayBuffer = new ArrayBuffer(byteString.length) //创建ArrayBuffer
  var intArray = new Uint8Array(arrayBuffer) //创建视图
  for (var i = 0; i < byteString.length; i++) {
    intArray[i] = byteString.charCodeAt(i)
  }
  return new Blob([intArray], { type: mimeString }) // 转成 blob
}
 

URL 转 base64

好了,更有意思的事情来了,我提高了要求,只给你一个图片的 URL(本地或者在线地址),是否能将图片编码成流?答案是肯定的,这里我们得请出 canvas 了:

思路很简单,根据图片对象的数据去画 canvas,再利用它来等价表示图片流。

getDataUri(url) {
  return new Promise((resolve, reject) => {
    /* eslint-disable */
    let image = new Image();
    image.onload = function() {
      let canvas = document.createElement('canvas');
      canvas.width = this.naturalWidth;
      canvas.height = this.naturalHeight;
      canvas.getContext('2d').drawImage(this, 0, 0);
      // Data URI
      resolve(canvas.toDataURL('image/png'));
    };
    image.src = url;
    // console.log(image.src);
    image.onerror = () => {
      reject(new Error('图片流异常'));
    };
  });
}
 

你可以像这样调用:

let dataUri = await this.getDataUri(`image/test/jartto.png`);
 

FileReader 是什么?

从上节了解到Blob对象只是二进制数据的容器,本身并不能操作二进制,故本节将对其操作对象FileReader进行介绍。

读取的 File 对象可以是什么呢?

  • 来自用户在一个 <input> 元素上选择文件后返回的 FileList 对象

  • 来自拖放操作生成的 DataTransfer 对象

  • 来自在一个 HTMLCanvasElement 上执行 mozGetAsFile() 方法后返回结果

  • ... ...

官话说了一大堆,其实你只需要知道「它是用来读取文件的」就够了。

参数说明如下:

  • FileReader.error(只读):一个 DOMException,表示在读取文件时发生的错误 ;
  • FileReader.readyState(只读):表示 FileReader 状态的数字。取值如下:
    • EMPTY 0 还没有加载任何数据.
    • LOADING 1 数据正在被加载.
    • DONE 2 已完成全部的读取请求.
  • FileReader.result(只读):文件的内容。该属性仅在读取操作完成后才有效,数据的格式取决于使用哪个方法来启动读取操作。

还有几个事件处理函数:

  • FileReader.onabort:处理 abort 事件。该事件在读取操作被中断时触发。
  • FileReader.onerror:处理 error 事件。该事件在读取操作发生错误时触发。
  • FileReader.onload:处理 load 事件。该事件在读取操作完成时触发。
  • FileReader.onloadstart:处理 loadstart 事件。该事件在读取操作开始时触发。
  • FileReader.onloadend:处理 loadend 事件。该事件在读取操作结束时(要么成功,要么失败)触发。
  • FileReader.onprogress:处理 progress 事件。该事件在读取 Blob 时触发。

可以用的方法如下:

  • FileReader.abort():中止读取操作。在返回时,readyState 属性为 DONE。
  • FileReader.readAsArrayBuffer():开始读取指定的 Blob 中的内容。一旦完成,result 属性将包含一个ArrayBuffer 来表示文件数据;
  • FileReader.readAsBinaryString()(即将弃用):开始读取指定的 Blob 中的内容。一旦完成,result 属性中将包含所读取文件的原始二进制数据。
  • FileReader.readAsDataURL():开始读取指定的 Blob 中的内容。一旦完成,result 属性中将包含一个 data: URL 格式的字符串以表示所读取文件的内容。(base64编码)
  • FileReader.readAsText():开始读取指定的 Blob 中的内容。一旦完成,result 属性中将包含一个字符串以表示所读取的文件内容。

上面的文档描述的很清楚了,但是估计你也没逐条细看,没关系,我们还有示例?

具体的应用场景可能如下:

const blob = new Blob(['你']);

const reader = new FileReader();

reader.readAsArrayBuffer(blob);

reader.onload = (e) => {
  // ArrayBuffer(3) {}
  let result = e.target.result;

  // 这里使用Unit8解读ArrayBuffer
  const v1 = new Uint8Array(result);

  // "你" 这个字符默认转为utf-8编码,占据三个字节;这里为10进制展示,只是为了直观
  console.log(v1) // Unit8Array(3) [228, 189, 160]
}
---------------------------------------------------------------
// 同样我们可以将二进制解读出文本(假设我们拿到了 v1 Unit8二进制数组对象)
const blob = new Blob([v1]);

let reader = new FileReader();

reader.readAsText(blob);

reader.onload = (e) => {
  console.log(e.target.result) // 你
}

 

用法很简单,你唯一需要注意的是一定要在 onload 之后再做操作!

常见问题与补充说明

  1. Failed to execute 'readAsDataURL' on 'FileReader': parameter 1 is not of type 'Blob'.

    出现如上问题,请检查你的 Blob 对象格式是否正确,是否缺少必要项。

  2. 很多第三方应用都需要去除 base64 头信息:

    base64.replace(/^data:image\/(jpeg|png|gif);base64,/,'')
     
  3. FileReader 读取文件后,记得放在 onload 函数内,切记。

  4. 在使用 canvas.getContext('2d').drawImage(this, 0, 0); 过程中,一定要注意这里的 this 指向,否则可能发生异常。

  5. base64Blob,请记得:

    Object.assign(blob,{
      // jartto: 这里一定要处理一下
      preview: URL.createObjectURL(blob),
      name: `图片示例:${index}.png`
    });
     

参考

  1. MDN FileReader
  2. ASCII,Unicode,UTF-8和Base64
  3. Convert Image to Data URI
  4. html base64 img 图片显示
  5. 解决 Javascript 中 atob 方法解码中文字符乱码问题

回复

我来回复
  • 暂无回复内容