二进制家族

117 阅读8分钟

Blob、File、ArrayBuffer、Buffer、Base64。

Blob

Blob 即 binary large object,直译就是二进制大对象,可以简单地理解为二进制数据的容器,它的数据可以按文本或二进制的格式进行读取。

在 js 中有两个构造函数 Blob 和 File 来生成 Blob 对象,File 继承了 Blob 并作了相应的扩展以支持用户系统上的文件。

在前端中主要有以下两种方式获取 File 对象:

  • 通过<input />标签选择文件

  • 通过拖曳产生的 DataTransfer 对象

创建 Blob

语法

const blob = new Blob(array, options);

参数

  • array array 为 ArrayBuffer, ArrayBufferView, Blob, String 等对象构成的 Array

  • options

    可选参数,有两个属性:

    • type

      默认值为 "",表示放入到 blob 中的数组内容的 MIME 类型。

    • endings

      默认值为"transparent",用于指定包含行结束符\n 的字符串如何被写入。 有两个值 a) "native",代表行结束符会被更改为适合宿主操作系统文件系统的换行符 b) "transparent",代表会保持 blob 中保存的结束符不变

实例

  • 字符串转 Blob

    const obj = { hello: 'world' };
    const blob = new Blob([JSON.stringify(obj, null, 2)], {
      type: 'application/json',
    });
    
  • ArrayBuffer 转 Blob

    const ab = new ArrayBuffer(16);
    const blob = new Blob([ab]);
    
  • Blob 转 text

    const text = await blob.text();
    
  • Blob 转 ArrayBuffer

    const buffer = await blob.arrayBuffer();
    

Blob 实用功能

  • 生成 URL 供图片使用

    可以通过window.URL.createObjectURL(blob)生成 blob URL(类似blob:https://www.google.com.hk/ad882004-b8b2-4d56-846b-909d19347a81),该 URL 只在浏览器内部有效。

    <html>
      <input type="file" id="image" />
      <img id="img" style="width: 300px; height: 300px" />
      <script>
        document.getElementById('image').addEventListener(
          'change',
          function (e) {
            const file = this.files[0];
            const img = document.getElementById('img');
            const url = window.URL.createObjectURL(file);
            img.src = url;
            img.onload = () => {
              window.URL.revokeObjectURL(url);
            };
          },
          false
        );
      </script>
    </html>
    
  • 生成 URL 供下载

    生成 Blob URL 后赋值给 a.download 属性,然后点击这个链接就可以实现下载了。

    const blob = new Blob(['Hello World']);
    const url = window.URL.createObjectURL(blob);
    
    const a = document.createElement('a');
    a.setAttribute('href', url);
    a.setAttribute('download', 'test.txt');
    a.style.display = 'none';
    document.body.appendChild(a);
    a.click();
    
    setTimeout(() => {
      //revokeObjectURL释放一个之前通过调用 URL.createObjectURL() 创建的 URL 对象
      window.URL.revokeObjectURL(url);
      document.body.removeChild(a);
    });
    
  • Blob 实现文件分片上传

    通过Blob.slice(start,end)可以将大 Blob 分割为多个小 Blob, 然后通过 xhr.send 发送 Blob 对象。

    html:

    <html>
      <body>
        <input type="file" id="input" />
        <script>
          function upload(blob) {
            const xhr = new XMLHttpRequest();
            xhr.open('POST', '/upload', true);
            xhr.setRequestHeader('Content-Type', 'text/plain');
            xhr.send(blob);
          }
    
          document.getElementById('input').addEventListener(
            'change',
            function (e) {
              const blob = this.files[0];
              const CHUNK_SIZE = 10;
              const SIZE = blob.size;
              let start = 0;
              let end = CHUNK_SIZE;
              while (start < SIZE) {
                upload(blob.slice(start, end));
                start = end;
                end = start + CHUNK_SIZE;
              }
            },
            false
          );
        </script>
      </body>
    </html>
    

    上传文件:

    //test.txt
    abcdefghijklmnopqrstuvwxyz
    

    服务端:

    const path = require('path');
    const express = require('express');
    const bodyParser = require('body-parser');
    
    const app = express();
    
    app.use(express.static(path.join(__dirname, '')));
    app.use(bodyParser.text());
    
    app.post('/upload', function (req, res) {
      //输出
      //body abcdefghij
      //body klmnopqrst
      //body uvwxyz
      console.log('body', req.body);
    });
    
    app.listen(8081, function () {
      console.log('listening on port', 8081);
    });
    
  • 本地读取文件内容

    通过 FileReader 可以将 Blob 转换为其他格式的数据。

    • FileReader.readAsText(Blob)

      将 Blob 转化为文本字符串。

    • FileReader.readAsArrayBuffer(Blob)

      将 Blob 转为 ArrayBuffer。

    • FileReader.readAsDataURL()

      将 Blob 转化为 Base64 格式的 Data URL

    const blob = new Blob([1, 2, 3, 4]);
    const reader = new FileReader();
    reader.onload = function (result) {
      console.log(result);
    };
    reader.readAsText(blob);
    reader.readAsArrayBuffer(blob);
    reader.readAsDataURL(blob);
    

    实例:

    <html>
      <body>
        <canvas id="canvas">canvas</canvas>
        <script>
          function convertCanvasToArrayBuffer(canvas) {
            return new Promise((resolve, reject) => {
              const reader = new FileReader();
              reader.addEventListener('loadend', () => {
                resolve(reader.result);
              });
              reader.addEventListener('error', reject);
              canvas.toBlob((blob) => reader.readAsArrayBuffer(blob));
            });
          }
    
          function drawRect(canvas) {
            const ctx = canvas.getContext('2d');
            ctx.beginPath();
            ctx.rect(20, 20, 150, 100);
            ctx.stroke();
          }
    
          window.onload = async () => {
            const canvas = document.getElementById('canvas');
            drawRect(canvas);
            const ab = await convertCanvasToArrayBuffer(canvas);
            console.log('ab', ab);
          };
        </script>
      </body>
    </html>
    

选择文件:

//test.txt
abcdefghijklmnopqrstuvwxyz

输出 abcdefghijklmnopqrstuvwxyz。

可以看到 Blob 主要用于操作文件,缺乏对二进制数据的细粒度控制,而 ArrayBuffer 可以满足这一需求。

ArrayBuffer

ArrayBuffer 就是用来操作二进制数据的接口,表示固定长度的原始二进制数据缓冲区。 创建 ArrayBuffer:

const buffer = new ArrayBuffer(16); // 创建一个长度为16字节的buffer,并用0进行预填充
console.log(buffer.byteLength); // 16

ArrayBuffer 与 Array 有很大的不同:

  • ArrayBuffer 的长度是固定的,而 Array 大小可以自由增减
  • ArrayBuffer 数据存放在栈中(取数据时更快),而 Array 放在堆中
  • ArrayBuffer 没有 push/pop 等方法
  • ArrayBuffer 只能读不能写,写需要借助 TypedArray/DataView

TypedArray

ArrayBuffer 存储了原始的字节序列,这些字节序列表示什么可以通过类似数组的视图 TypedArray 来解释 诸如 Uint8Array、Uint16Array 都属于 TypedArray,但并没有一个叫 TypedArray 的构造器。

  • Uint8Array(8 位无符号整数)

    将 ArrayBuffer 中的每 8 位,也就是一个字节解释为一个数字(8 位,[0-255])。

  • Uint16Array(16 位无符号整数)

    将 ArrayBuffer 中的每 16 位,也就是两个字节解释为一个数字(16 位,[0-65535])。

  • Uint32Array(32 位无符号整数) 将 ArrayBuffer 中的每 32 位,也就是四个字节解释为一个数字(32 位,[0-4294967295])。

  • Float64Array 将 ArrayBuffer 中的每 64 位,也就是八个字节解释为一个浮点数(64 位,[5.0x10^-324-1.8x10^308])。

因此,上述 16 字节 ArrayBuffer 可以解释为不同类型的数据,可以是 16 个“小数字”,或 8 个更大的数字(每个数字 2 个字节),或 4 个更大的数字(每个数字 4 个字节),或 2 个高精度的浮点数(每个数字 8 个字节)。

TypedArray 的几种使用方式如下:

  • new TypedArray(buffer, [byteOffset], [length]);

    • buffer(可选)

      ArrayBuffer,在该 buffer 基础上创建视图。

    • byteOffset(可选)

      buffer 的起始位置,默认为 0。

    • length(可选)

      长度,默认是 buffer 的长度。

    通过这种方式可以选取 buffer 的一部分进行处理。

    const byteLength = 16;
    const buffer = new ArrayBuffer(byteLength); // 创建一个长度为 16字节 的 buffer
    
    const length = (byteLength * 8) / 16;
    const view = new Uint16Array(buffer, 0, length); // 将 buffer 视为一个 16 位整数的序列
    
    console.log(view.length); // 8,它存储了 8 个整数
    
    view[0] = 8; //写入值
    
  • new TypedArray(object)

    当传入一个 object 作为参数时,如同通过 TypedArray.from() 方法一样创建一个新的类型化数组。如果给定的是 Array,或任何类数组对象则会创建一个相同长度的类型化数组,并复制其内容。

    const arr = new Uint8Array([1, 2, 3]);
    console.log(arr.length); // 3,创建了相同长度的类型化数组
    console.log(arr[1]); // 2,用给定值填充了 3 个字节(无符号 8 位整数)
    
  • new TypedArray(typedArray)

    如果给定的是另一个 TypedArray,会创建一个相同长度的类型化数组,并复制其内容,数据在此过程中会被转换为新的类型。

    const a = new Uint16Array([1, 500]);
    const b = new Uint8Array(a);
    console.log(b[0]); // 1
    console.log(b[1]); // 244
    

    Uint16Array 转 Uint8Array 的过程中,无法复制 500 到 8 位字节([0,255]),发生越界,只保留最右边低位的 8 位。500 的二进制表示为 111110100, 取右边 8 位则是 11110100,转换为十进制就是 244。

  • new TypedArray(length)

    创建类型化数组时带上元素的个数。

    const arr = new Uint16Array(2); // 为 2 个整数创建类型化数组
    console.log(Uint16Array.BYTES_PER_ELEMENT); // 每个整数 2 个字节(16位)
    console.log(arr.byteLength); // 4(字节中的大小)
    
  • new TypedArray()

    创建类型化数组可以不带参数,这会创建长度为零的类型化数组。

因为类型化数组是用来处理 ArrayBuffer 的,因此除第一种构造函数外(已提供 ArrayBuffer)都会自动创建 ArrayBuffer。通过 TypedArray 的实例就能访问到:

  • arr.buffer

    引用 ArrayBuffer。

  • arr.byteLength

    ArrayBuffer 的长度。

DataView

DataView 是在 ArrayBuffer 上一种灵活的“未类型化”视图。它允许以任何格式访问任何偏移量的数据。 它与 TypedArray 最大的不同就是 TypedArray 在构造时已经确定了如何去解释 ArrayBuffer,比如 Uint8Array,将 ArrayBuffer 中的每 8 位解释为一个数字,而 DataView 是灵活的可以通过.getUint8(i) 或 .getUint16(i) 之类的方法来解释(以某种方式访问)数据。

语法:

new DataView(buffer, [byteOffset], [byteLength])
  • buffer

    ArrayBuffer。与类型化数组不同,DataView 不会自动创建 buffer。

  • byteOffset

    视图的起始字节位置,默认为 0。

  • byteLength

    视图的字节长度,默认为 buffer 的长度(bytes)。

// 4 个字节的二进制数组,每个都是最大值 255
const buffer = new Uint8Array([1, 2, 255, 255]).buffer;

const dataView = new DataView(buffer);

// 在偏移量为 0 处获取 8 位数字
console.log(dataView.getUint8(0)); // 1
// 在偏移量为 1个字节 处获取 8 位数字
console.log(dataView.getUint8(1)); // 2

// 现在在偏移量为 0 处获取 16 位数字,它由 2 个字节组成,一起解析为 258(1的二进制为00000001,2的二进制00000010,16位就是0000000100000010,转换为十进制就是258)
console.log(dataView.getUint16(0)); // 258
dataView.setUint32(0, 0); //在偏移量为0处设置一个32位的数字0,一共就4个字节,即将所有字节都设为 0

可以看到通过 DataView 可以动态解析 ArrayBuffer,假如 ArrayBuffer 中存储了混合类型的数据,DataView 能够轻松处理。

ArrayBuffer 实例

  • String 转 ArrayBuffer

    const enc = new TextEncoder(); // always utf-8
    const str = 'hello world';
    const unit8Array = enc.encode(str);
    const arrayBuffer = unit8Array.buffer;
    
  • ArrayBuffer 转 String

    const enc = new TextEncoder();
    const str = 'hello world';
    const unit8Array = enc.encode(str);
    const arrayBuffer = unit8Array.buffer;
    const decoded = new TextDecoder('utf-8').decode(arrayBuffer);
    console.log(decoded); //hello world
    
  • 通过 ArrayBuffer 的格式读取 Ajax 请求数据:

    html:

    <html>
      <body>
        <button id="button">通过ajax获取数据</button>
        <script>
          function ajax() {
            const xhr = new XMLHttpRequest();
            xhr.open('GET', '/ajax', true);
            xhr.responseType = 'arraybuffer';
    
            xhr.onload = function () {
              const ab = xhr.response;
    
              console.log('arrayBuffer', ab);
              console.log(
                'arrayBuffer to text',
                new TextDecoder('utf-8').decode(ab)
              ); //hello world
            };
            xhr.send();
          }
    
          document.getElementById('button').addEventListener('click', (e) => {
            ajax();
          });
        </script>
      </body>
    </html>
    

    服务端:

    const path = require('path');
    const express = require('express');
    const bodyParser = require('body-parser');
    
    const app = express();
    
    app.use(express.static(path.join(__dirname, '')));
    app.use(bodyParser.text());
    
    app.get('/ajax', function (req, res) {
      console.log('/ajax');
      res.send('hello world');
    });
    
    app.listen(8081, function () {
      console.log('listening on port', 8081);
    });
    

Buffer

Buffer 是 Node.js 提供的对象,前端没有,但是 webpack5 以前都会生成 Polyfill,以供前端使用。 它一般应用于 IO 操作,例如以 Buffer 方式读写数据:

//Node.js 创建的流都是运作在字符串和 Buffer上
const fs = require('fs');

const inputStream = fs.createReadStream('input.txt'); // 创建可读流
const outputStream = fs.createWriteStream('output.txt'); // 创建可写流

inputStream.pipe(outputStream); // 管道读写

Buffer 也可以处理前端上传到服务器的数据:

html:

<html>
  <body>
    <button id="button">点击上传</button>
    <script>
      function upload() {
        const xhr = new XMLHttpRequest();
        xhr.open('POST', '/upload', true);
        xhr.setRequestHeader('Content-Type', 'text/plain');
        xhr.send('hello world');
      }

      document.getElementById('button').addEventListener('click', (e) => {
        upload();
      });
    </script>
  </body>
</html>

服务端:

const path = require('path');
const express = require('express');

const app = express();

app.use(express.static(path.join(__dirname, '')));

app.post('/upload', function (req, res) {
  const chunks = [];

  req.on('data', (buf) => {
    chunks.push(buf);
  });
  req.on('end', () => {
    let buffer = Buffer.concat(chunks);
    console.log(buffer.toString());
    res.sendStatus(200);
  });
});

app.listen(8081, function () {
  console.log('listening on port', 8081);
});

Buffer 类是 JavaScript 的 Uint8Array 类的子类,因此与 ArrayBuffer 很容易互相转换。

const toArrayBuffer = (buf) => {
  const ab = new ArrayBuffer(buf.length);
  const view = new Uint8Array(ab);
  for (let i = 0; i < buf.length; ++i) {
    view[i] = buf[i];
  }
  return ab;
};

const toBuffer = (ab) => {
  const buf = Buffer.alloc(ab.byteLength);
  const view = new Uint8Array(ab);
  for (let i = 0; i < buf.length; ++i) {
    buf[i] = view[i];
  }
  return buf;
};

Base64

前端有些时候会用到 Base64,且 Base64 也可以同二进制相关的对象进行转换,在这就一并介绍下。 Base64 格式如下: 其中 base64, 后面那一长串的字符串,就是 Base64 编码字符串:

data:image/png;base64,iVBORw0KGgoAAAANSUh...

它是一种用 64 个字符来表示任意二进制数据的方法。 用记事本打开 jpg、pdf 这些文件时,会出现一堆乱码,因为二进制文件包含很多无法显示和打印的字符。所以,要让文本处理软件能处理二进制数据,就需要用可见字符来表示二进制,Base64 是一种最常见的二进制编码方法。

Base64 索引表

选取 64 个字符用来表示二进制:

编码方式

64 个字符,只需要用 6 个二进制位(bit)来表示,而一个标准的字符是用 8 位(bit)来表示,它们的最小公倍数是 24,因此 4 个 Base64 字符可以表示 3 个标准的 ascll 字符。

  • 将 3 个字节作为一组

    3 个字节有 24 位

  • 将这 24 位分为 4 组

    4 组也就代表 4 个 Base64 字符。

  • 在每组的 6 个二进制位前面补两个 00,扩展成 32 个二进制位,即四个字节

  • 每个字节对应一个小于 64 的数字,即为字符编号

  • 根据 Base64 索引表,每个字符编号对应一个字符,就得到了 Base64 编码字符

字符串 you 的编码: 可以看到字符串 you 的 Base64 编码编码结果为 eW91。

如果要编码的二进制数据不是 3 的倍数,先使用 0 字节在末尾补足后,再在编码的末尾加上 1 个或 2 个=号,表示补了多少字节,解码的时候,会自动去掉。 参考下表:

转换

  • 字符串转换为 Base64

    通过 btoa()将字符串或二进制值转换成 Base64 编码字符串。

    btoa('xhm'); //'eGht'
    //于btoa、atob 仅支持对ASCII字符编码,也就是单字节字符,而中文是 2-4 字节的字符。因此,可以先将中文字符转为 utf-8 的编码,将utf-8编码当做字符
    btoa(encodeURIComponent('星魂')); //'JUU2JTk4JTlGJUU5JUFEJTgy'
    

    注意:btoa 方法只能直接处理 ASCII 码的字符,对于非 ASCII 码的字符,则会报错。

  • Base64 转换为字符串

    通过 atob() 对 base64 编码的字符串进行解码。 注意:atob 方法如果传入字符串参数不是有效的 Base64 编码(如非 ASCII 码字符),或者其长度不是 4 的倍数,会报错。

    atob('eGht'); //'xhm'
    decodeURIComponent(atob('JUU2JTk4JTlGJUU5JUFEJTgy')); //'星魂'
    
  • canvas 转换为 Base64

    <html>
      <body>
        <div style="display: flex">
          canvas:<canvas id="canvas">canvas</canvas>
        </div>
    
        <div style="display: flex">get img from canvas:<img id="img" /></div>
    
        <script>
          function drawRect(canvas) {
            const ctx = canvas.getContext('2d');
            ctx.beginPath();
            ctx.rect(20, 20, 150, 100);
            ctx.stroke();
          }
    
          window.onload = () => {
            const canvas = document.getElementById('canvas');
            drawRect(canvas);
            const img = document.getElementById('img');
            img.setAttribute('src', canvas.toDataURL());
          };
        </script>
      </body>
    </html>
    
  • Base64 to blob

    //dataURI: data:image/png;base64,iVBORw0KGgoAAAANSUh...
    const dataURItoBlob = (dataURI) => {
      const mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0]; // mime类型
      const byteString = atob(dataURI.split(',')[1]); //base64 解码
      const arrayBuffer = new ArrayBuffer(byteString.length);
      const intArray = new Uint8Array(arrayBuffer);
    
      for (let i = 0; i < byteString.length; i++) {
        intArray[i] = byteString.charCodeAt(i); //返回字符的 Unicode 编码
      }
      return new Blob([intArray], { type: mimeString });
    };