JavaScript二进制深入浅出

1,102 阅读25分钟

有趣的面试题

一天小🐟面试心脏跳动,前面面试小🐟进行的非常顺利,面试官也是对小🐟非常满意,一时兴起就问:小🐟同学,你知道100*100 Canvas占用内存多大?小🐟瞬间慌张支支吾吾也没说出结果,于是面试官说:回去等消息吧!😭😭😭

const btn = document.getElementById('btn');
const canvas = document.getElementById('app') as HTMLCanvasElement;
const ctx = canvas.getContext("2d");
ctx.fillStyle = "rgba(200,2,2, 0.8)";
ctx.fillRect(10, 10, 50, 50);
const getImageData = () => {
  var imgData = ctx.getImageData(10, 10, 1, 1);
  console.log(imgData);
  return imgData;
}
btn.addEventListener('click', () => {
  getImageData()
}, true)

答案揭晓

刚刚从控制台看到这样一个数据结构:

这里只需要关注Uint8ClampedArray数组,也不管这个代表什么意思,它是Uint8开头,了解Go或者C的同学可以联想到这个代表8位无符号整型, *因为计算机存储空间的最小单位是字节(byte),即 8 个比特(bit),它占据一个字节(8 位)的内存空间, 且又是一个Array, 它的长度是4 所以一个像素的 Canvas 占内存是4Byte, 100100 Canvas自然占的内存就是100 * 100 * 4 = 40,000 byte啦,用二进制表示的话就是40,000 * 8 = 320,000 bit。

大家是否好奇过?

  1. 为什么flv.js能够在浏览器原生不支持的情况下播放 .flv 格式的文件?src 为什么是一段以 blob: 开头的字符串?
  2. 后端给了个 POST 的下载请求,前端怎么办?

在 Web 开发中,当我们处理文件时(创建,上传,下载),经常会遇到二进制数据。另一个典型的应用场景是图像处理。

这些都可以通过 JavaScript 进行处理,而且二进制操作性能更高。

不过,在 JavaScript 中有很多种二进制数据格式,会有点容易混淆。仅举几个例子:

  • ArrayBufferUint8ArrayDataViewBlobFile 及其他。

与其他语言相比,JavaScript 中的二进制数据是以非标准方式实现的。但是,当我们理清楚以后,一切就会变得相当简单了。

ArrayBuffer

基本的二进制对象是ArrayBuffer,是对固定长度的连续内存空间的引用 它使用一种类数组结构,用于表示通用的、固定长度的二进制缓冲区。ArrayBuffer 对象与普通 JavaScript 数组不同,它不能直接存储数字、字符串、对象等数据类型,而是只能存储二进制数据。ArrayBuffer 长度在创建时就被确定,不能改变。

ArrayBuffer不是某种东西的数组

让我们先澄清一个可能的误区。ArrayBufferArray 没有任何共同之处:

  • 它的长度是固定的,我们无法增加或减少它的长度。
  • 它正好占用了内存中的那么多空间。
  • 要访问单个字节,需要另一个“视图”对象,而不是 buffer[index]

在创建 ArrayBuffer 时,需要指定缓冲区的大小,单位为字节(byte)。例如,以下代码创建了一个长度为 16 字节的 ArrayBuffer 对象:

const buffer = new ArrayBuffer(16);

它会分配一个 16 字节的连续内存空间,并用 0 进行预填充。

ArrayBuffer 是一个内存区域。它里面存储了什么?无从判断。只是一个原始的字节序列。

如要操作 ArrayBuffer ,我们需要使用“视图”对象。

视图对象本身并不存储任何东西。它是一副“眼镜”,透过它来解释存储在 ArrayBuffer 中的字节。

例如:

  • Uint8Array —— 将 ArrayBuffer 中的每个字节视为 0 到 255 之间的单个数字(每个字节是 8 位,因此只能容纳那么多)。这称为 “8 位无符号整数”。
  • Uint16Array —— 将每 2 个字节视为一个 0 到 65535 之间的整数。这称为 “16 位无符号整数”。
  • Uint32Array —— 将每 4 个字节视为一个 0 到 4294967295 之间的整数。这称为 “32 位无符号整数”。
  • Float64Array —— 将每 8 个字节视为一个 5.0x10(-324)1.8x10(308) 之间的浮点数。

因此,一个 16 字节 ArrayBuffer 中的二进制数据可以解释为 16 个“小数字”,或 8 个更大的数字(每个数字 2 个字节),或 4 个更大的数字(每个数字 4 个字节),或 2 个高精度的浮点数(每个数字 8 个字节)。

image.png ArrayBuffer 是核心对象,是所有的基础,是原始的二进制数据。

但是,如果我们要写入值或遍历它,基本上几乎所有操作 —— 我们必须使用视图(view),例如:

const buffer = new ArrayBuffer(16); // 创建一个长度为 16 的 buffer

const view = new Uint32Array(buffer); // 将 buffer 视为一个 32 位整数的序列

console.log(Uint32Array.BYTES_PER_ELEMENT); // 每个整数 4 个字节

console.log(view.length); // 4,它存储了 4 个整数
console.log(view.byteLength); // 16,字节中的大小

// 让我们写入一个值
view[0] = 123456;

// 遍历值
for(let num of view) {
  console.log(num); // 123456,然后 0,0,0(一共 4 个值)
}

TypedArray

一个 TypedArray 对象描述了底层**二进制数据缓冲区** (ArrayBuffer) 的类数组视图。没有称为 TypedArray 的全局属性,也没有直接可用的 TypedArray 构造函数。

API

TypedArray 是一种基于 ArrayBuffer 的类数组结构,用于存储大量同类型的数据。可以通过 TypedArray 对象来访问 ArrayBuffer中的数据,并将其视为特定类型的数组。JavaScript 提供了以下几种 TypedArray 类型:

类型值范围字节大小描述Web IDL 类型等价的 C 类型
Int8Array-128 到 12718 位有符号整型(补码)byteint8_t
Uint8Array0 到 25518 位无符号整型octetuint8_t
Uint8ClampedArray0 到 25518 位无符号整型(一定在 0 到 255 之间)octetuint8_t
Int16Array-32768 到 32767216 位有符号整型(补码)shortint16_t
Uint16Array0 到 65535216 位无符号整型unsigned shortuint16_t
Int32Array-2147483648 到 2147483647432 位有符号整型(补码)longint32_t
Uint32Array0 到 4294967295432 位无符号整型unsigned longuint32_t
Float32Array-3.4E383.4E38 并且 1.2E-38 是最小的正数432 位 IEEE 浮点数(7 位有效数字,例如 1.234567unrestricted floatfloat
Float64Array-1.8E3081.8E308 并且 5E-324 是最小的正数864 位 IEEE 浮点数(16 位有效数字,例如 1.23456789012345unrestricted doubledouble
BigInt64Array-263 到 263 - 1864 位有符号整型(补码)bigintint64_t (signed long long)
BigUint64Array0 到 264 - 1864 位无符号整型bigintuint64_t (unsigned long long)

这些对象都是全局对象,可以通过以下方式创建:

const array = new TypedArray(buffer [, byteOffset, length]);

当你看到new TypedArray之类的内容时,它表示new Int8Arraynew Uint8Array及其他中之一。其中,buffer 参数使用一个 ArrayBuffer 对象作为它的元素缓冲区;byteOffset 参数指向缓冲区中开始使用的索引。

该对象不能被直接实例化——试图去使用new构造它将会抛出TypeError。

所有 TypedArray 子类的构造函数只能使用 new 构造。试图在没有 new 的情况下调用,会抛出 TypeError。

越界行为

如果我们尝试将越界值写入类型化数组会出现什么情况?不会报错。但是多余的位被切除。

例如,我们尝试将 256 放入 Uint8Array。256 的二进制格式是 100000000(9 位),但 Uint8Array 每个值只有 8 位,因此可用范围为 0 到 255。

对于更大的数字,仅存储最右边的(低位有效)8 位,其余部分被切除:

image.png Uint8ClampedArray

因此结果是 0。

257 的二进制格式是 100000001(9 位),最右边的 8 位会被存储,因此数组中会有 1image.png 换句话说,该数字对 2(8) 取模的结果被保存了下来。 示例如下:

const uint8array = new Uint8Array(16);

const num = 256;
console.log(num.toString(2)); // 100000000(二进制表示)

uint8array[0] = 256;
uint8array[1] = 257;

console.log(uint8array[0]); // 0
console.log(uint8array[1]); // 1

在这方面比较特殊,它的表现不太一样。对于大于 255 的任何数字,它将保存为 255,对于任何负数,它将保存为 0。此行为对于图像处理很有用。

方法

TypedArray 具有常规的 Array 方法,但有个明显的例外。

我们可以遍历(iterate),mapslicefindreduce 等。

但有几件事我们做不了:

  • 没有 splice —— 我们无法“删除”一个值,因为类型化数组是缓冲区(buffer)上的视图,并且缓冲区(buffer)是固定的、连续的内存区域。我们所能做的就是分配一个零值。
  • concat 方法。

还有两种其他方法:

  • arr.set(fromArr, [offset])offset(默认为 0)开始,将 fromArr 中的所有元素复制到 arr
  • arr.subarray([begin, end]) 创建一个从 beginend(不包括)相同类型的新视图。这类似于 slice 方法(同样也支持),但不复制任何内容 —— 只是创建一个新视图,以对给定片段的数据进行操作。

有了这些方法,我们可以复制、混合类型化数组,从现有数组创建新数组等。

// 创建一个长度为 6 的 Int16Array 对象
const int16Array = new Int16Array(6);

// 使用 fill() 方法将所有元素设置为 0
int16Array.fill(0);

// 使用 set() 方法设置前三个元素的值
int16Array.set([1, 2, 3]);

// 使用 subarray() 方法获取第二个到第四个元素组成的新数组
const subArray = int16Array.subarray(1, 4);

// 输出原始数组和子数组的内容
console.log(int16Array); // 输出:Int16Array [ 1, 2, 3, 0, 0, 0 ]
console.log(subArray); // 输出:Int16Array [ 2, 3, 0 ]

// 使用 map() 方法将每个元素乘以 2
const mappedArray = int16Array.map((value) => value * 2);

// 输出原始数组和映射后的数组的内容
console.log(int16Array); // 输出:Int16Array [ 1, 2, 3, 0, 0, 0 ]
console.log(mappedArray); // 输出:Int16Array [ 2, 4, 6, 0, 0, 0 ]

// 使用 reduce() 方法计算所有元素的和
const sum = int16Array.reduce((accumulator, currentValue) => accumulator + currentValue);

// 输出元素的总和
console.log(sum); // 输出:6

DataView

DataView视图是在ArrayBuffer上的一种特殊的超灵活“未类型化”视图。它允许以任何格式访问任何偏移量(offset)的数据。是一个可以从二进制ArrayBuffer对象中读写多种数值类型的底层接口,使用它时,不用考虑不同平台的字节序(endianness)问题。

  • 对于类型化的数组,构造器决定了其格式。整个数组应该是统一的。第 i 个数字是 arr[i]
  • 通过 DataView,我们可以使用 .getUint8(i).getUint16(i) 之类的方法访问数据。我们在调用方法时选择格式,而不是在构造的时候。

API

DataView 是一种用于访问 ArrayBuffer 中数据的接口。它提供了一组方法,用于读写各种数据类型的值,如整数、浮点数、布尔值等。可以通过 DataView 对象的 getInt8()getUint8()getInt16()getUint16() 等方法来读取数据,也可以通过 setInt8()setUint8()setInt16()setUint16() 等方法来写入数据。

以下是一个使用 DataView 对象读取和写入数据的示例:

const bfu = new ArrayBuffer(16);
const view = new DataView(bfu);
// 位置0
view.setInt8(0, 128);
// 位置1
view.setUint8(1, 255);
// 位置2
view.setInt16(2, -32768);
// 位置3
view.setUint16(3, 65535)
// 位置4
view.setUint32(4, 4294967295);
view.setInt32(5, 2147483647);

view.setFloat32(6, Math.PI);
view.setFloat64(7, Math.PI);
const max = 2n ** (64n - 1n) - 1n;
const max1 = 2n ** 64n - 1n;
view.setBigInt64(8, max);
// view.setBigUint64(9, max1);
console.log(view.getInt8(0)); // -128
console.log(view.getUint8(1)); // 255
console.log(view.getInt16(2)); // -32768
console.log(view.getUint16(3)); // 65535
console.log(view.getUint32(4)); // 65535
console.log(view.getInt32(5)); // 2147483647
console.log(view.getFloat32(6)); // 3.1415927
console.log(view.getFloat64(7)); // 2.718281828459045
console.log(view.getBigInt64(8)); // 9223372036854775807n
// console.log(view.getBigUint64(9)); // 18446744073709551615n
// 4 个字节的二进制数组,每个都是最大值 255
const buffer = new Uint8Array([255, 255, 255, 255]).buffer;

const dataView = new DataView(buffer);

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

// 现在在偏移量为 0 处获取 16 位数字,它由 2 个字节组成,一起解析为 65535
console.log(dataView.getUint16(0)); // 65535(最大的 16 位无符号整数)

// 在偏移量为 0 处获取 32 位数字
console.log(dataView.getUint32(0)); // 4294967295(最大的 32 位无符号整数)

dataView.setUint32(0, 0); // 将 4 个字节的数字设为 0,即将所有字节都设为 0

在上述示例中,我们首先创建了一个长度为 16 字节的 ArrayBuffer 对象 buffer,然后使用 DataView 对象 view 访问该对象中的数据。接下来,我们使用 view 对象的一系列方法读写各种数据类型的值,并将其打印到控制台中。

当我们将混合格式的数据存储在同一缓冲区(buffer)中时,DataView 非常有用。例如,当我们存储一个成对序列(16 位整数,32 位浮点数)时,用 DataView 可以轻松访问它们。

需要注意的是,在使用 DataView 对象访问 ArrayBuffer 数据时,需要指定偏移量(offset)和字节顺序(little-endian 或 big-endian)。可以通过传递参数来设置偏移量和字节顺序,例如:

const view = new DataView(buffer, offset, littleEndian);

其中,offset 指定从缓冲区哪个位置开始读取或写入数据,默认为 0;littleEndian 指定字节顺序,为 true 表示使用小端字节序(低位在前),为 false 表示使用大端字节序(高位在前),默认为 false。

demo

  1. 如何使用 ArrayBuffer 修改 Canvas 像素
const canvas = document.getElementById('app');
const ctx = canvas.getContext('2d');

// 创建 ImageData 对象
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

// 创建 ArrayBuffer 对象
const buffer = new ArrayBuffer(imageData.data.length);
const dataView = new DataView(buffer);

// 将像素数据写入 ArrayBuffer 中
let offset = 0;
for (let i = 0; i < imageData.data.length; i += 4) {
  dataView.setUint8(offset++, imageData.data[i]);
  dataView.setUint8(offset++, imageData.data[i + 1]);
  dataView.setUint8(offset++, imageData.data[i + 2]);
  dataView.setUint8(offset++, imageData.data[i + 3]);
}

// 修改第一行像素的颜色为红色
for (let x = 0; x < canvas.width; x++) {
  const index = (x * 4) + 0;
  dataView.setUint8(index, 255);
  dataView.setUint8(index + 1, 0);
  dataView.setUint8(index + 2, 0);
  dataView.setUint8(index + 3, 255);
}

// 将修改后的像素数据写回 ImageData 对象中
offset = 0;
for (let i = 0; i < imageData.data.length; i += 4) {
  imageData.data[i]     = dataView.getUint8(offset++);
  imageData.data[i + 1] = dataView.getUint8(offset++);
  imageData.data[i + 2] = dataView.getUint8(offset++);
  imageData.data[i + 3] = dataView.getUint8(offset++);
}

// 将修改后的 ImageData 对象绘制到 Canvas 上
ctx.putImageData(imageData, 0, 0);
  1. 如何使用ArrayBuffer播放音乐
// 创建 AudioContext 对象和 AudioBufferSourceNode 对象
const audioContext = new AudioContext();
const sourceNode = audioContext.createBufferSource();
fetch('https://content.volccdn.com/obj/volc-content/houtui.flac')
  .then(response => response.arrayBuffer())
  .then(arrayBuffer => {


    // 解码 ArrayBuffer 对象并将其转换为 AudioBuffer 对象
    audioContext.decodeAudioData(arrayBuffer).then(audioBuffer => {
      // 获取左声道的 Float32Array 数组
      const leftChannelData = audioBuffer.getChannelData(0);

      // 创建一个新的 Float32Array 数组,并将左声道的数据复制到其中
      const newLeftChannelData = new Float32Array(leftChannelData.length);
      newLeftChannelData.set(leftChannelData);

      // 将新数组设置为 AudioBuffer 对象的左声道数据
      audioBuffer.copyToChannel(newLeftChannelData, 0);
      console.log(audioBuffer)
      // 设置 AudioBufferSourceNode 对象播放的音频数据
      sourceNode.buffer = audioBuffer;

      // 连接 AudioBufferSourceNode 对象和 AudioContext 对象,并开始播放音频
      sourceNode.connect(audioContext.destination);

    });
  });
const btn = document.getElementById('app')
btn.addEventListener('click', () => {
  sourceNode.start();
}, true)

const btn1 = document.getElementById('stop')
btn1.addEventListener('click', () => {
  sourceNode.stop();
}, true)
const audioPath = 'https://content.volccdn.com/obj/volc-content/houtui.flac';
const audio = document.querySelector('audio');
const btn = document.getElementById('app')
const stop = document.getElementById('stop')
btn.disabled = true
  let source = null
// 发起请求获取音频文件内容,并将其存储为 Blob 对象
fetch(audioPath).then((response) => {
  return response.blob();
}).then((blob) => {
  console.log(blob)
  // 将 Blob 对象转换为 URL,并设置给 audio 元素
  // const objectURL = URL.createObjectURL(blob);
  // audio.src = objectURL;

  // 读取 Blob 对象中的音频数据
  const reader = new FileReader();
  reader.readAsArrayBuffer(blob);

  reader.onload = () => {
    // 使用 AudioContext API 处理音频数据
    const audioContext = new AudioContext();
    audioContext.decodeAudioData(reader.result, (buffer) => {
      source = audioContext.createBufferSource();
      source.buffer = buffer;
      source.connect(audioContext.destination);
      btn.disabled = false
    });
  };
});
btn.addEventListener('click', () => {
  source.start(0);
}, true)
stop.addEventListener('click', () => {
  source.stop()
})

参考

developer.mozilla.org/zh-CN/docs/…

Blob

Blob 对象表示一个不可变、原始数据的类文件对象。它的数据可以按文本或二进制的格式进行读取,也可以转换成 ReadableStream 来用于数据操作。

Blob 表示的不一定是 JavaScript 原生格式的数据。File 接口基于 Blob,继承了 blob 的功能并将其扩展以支持用户系统上的文件。

Blob 是一种 JavaScript 对象,用于表示不可变、原始数据的类文件对象,通常用于处理二进制数据和文本数据。Blob 对象可以直接使用或转换为其他格式,例如 ArrayBuffer、FormData、Blob URL 等。

image.png

构造函数

new Blob(blobParts, options);
  • blobParts 是由Blob/BufferSource/USVString类型的值以任何顺序排列构成的数组。

  • options 可选对象:

    • type —— 默认值为 ""Blob 类型,通常是 MIME 类型,例如 image/png。(需要注意的是:当type会将U+0020到U+007E范围任何字符转化为小写,范围之外的 字符串 将会被设置为空字符串
    • endings —— 默认值为 "transparent",是否转换换行符,使 Blob 对应于当前操作系统的换行符(\r\n\n)。默认为 "transparent"(啥也不做),不过也可以是 "native"会将blobParts 中的任何 USVString 元素中的换行符将被转换为本机格式)。

创建 Blob

  1. 字符串 创建 Blob

码上掘金

  1. 从类型化数组(typed array)和 字符串 创建 Blob

码上掘金

方法

slice方法

slice方法返回一个新的 Blob 对象,包含了源 Blob 对象中指定范围内的数据。从start位置开始,到end结束(不包含end

Blob 对象是不可改变的

我们无法直接在 Blob 中更改数据,但我们可以通过 slice 获得 Blob 的多个部分,从这些部分创建新的 Blob 对象,将它们组成新的 Blob等。

这种行为类似于 JavaScript 字符串:我们无法更改字符串中的字符,但可以生成一个新的改动过的字符串。

var blob = instanceOfBlob.slice([start [, end [, contentType]]]};

stream方法

stream方法返回一个ReadableStream对象,读取它将返回包含在Blob中的数据。

instanceOfBlob.stream()

text方法

text方法返回一个 promise 对象,以 resolve 状态返回一个以文本形式包含 blob 中数据的USVString。并且该数据总是被识别为 UTF-8 格式。

const textPromise = blob.text();
blob.text().then(text => /* 执行的操作…… */);
const text = await blob.text();

text方法总是返回UTF-8 格式 字符串

这与readAsText()的行为不同,以便与Fetch的text()的行为更好地协调。具体来说,该方法将始终使用UTF-8作为编码,而FileReader可以根据blob的类型和传入的编码名称使用不同的编码。

arrayBuffer方法

arrayBuffer方法返回一个Promise对象,包含 blob 中的数据,并在 ArrayBuffer 中以二进制数据的形式呈现。

const bufferPromise = blob.arrayBuffer();
blob.arrayBuffer().then(buffer => /* 处理 ArrayBuffer 数据的代码……*/);
const buffer = await blob.arrayBuffer();

demo

const btn = document.getElementById('btn');
const I = new Uint8Array([73]); 
const Love = new Uint8Array([76, 111, 118, 101]); 
const You = new Uint8Array([89, 111, 117]); 
const emioj = new Uint8Array([40, 42, 46, 43, 41])


btn.addEventListener('click', async () => {
  const strBlob = new Blob([I, ' ', Love, ' ', You, ' ', emioj], {type: 'text/plain'});
  console.log(strBlob)
  // slice方法
  const newStrBlob = strBlob.slice(0, 1, 'text/html')
  // text方法
  const showText = await newStrBlob.text()
  console.log(showText)
  // arrayBuffer方法
  const showTextArrayBuffer = await newStrBlob.arrayBuffer()
  console.log(showTextArrayBuffer)
  // window.open(URL.createObjectURL(newStrBlob))
}, true)
//  Blob.stream() 方法下载并读取服务器端返回的 csv 文件,同时将其中的数据转换为 JSON 格式并进行显示
const csvToJson = (text: string) => {
  const lines = text.split('\n');
  const result = [];
  const headers = lines[0].split(',');

  for (let i = 1; i < lines.length; i++) {
    const obj = {};
    const currentLine = lines[i].split(',');

    for (let j = 0; j < headers.length; j++) {
      obj[headers[j]] = currentLine[j];
    }

    result.push(obj);
  }

  return result;
}
const downloadAndReadCsv = (url:string) => {
  fetch(url)
    .then(response => response.blob())
    .then(blob => {
      const readableStream = blob.stream();
      const reader = readableStream.getReader();
      let text = '';

      reader.read().then(function processText({ done, value }) {
        if (done) {
          console.log(text)
          const json = csvToJson(text);
          console.log(json);
          return;
        }

        text += new TextDecoder().decode(value);
        return reader.read().then(processText);
      });
    })
    .catch(error => console.error(error));
}
downloadAndReadCsv('https://content.volccdn.com/obj/volc-content/test.csv')

应用场景

Blob用作URL

Blob URL(Object URLs)是 W3C File API 中的一个重要特性,它提供了一种将 Blob 对象转换为 URL 地址的方法,从而实现对 Blob 数据进行访问和处理的功能。

协议介绍

Blob URL/Object URL 是一种伪协议,允许 Blob 和 File 对象用作图像,下载二进制数据链接等的 URL 源。Blob URL 的原理非常简单,即通过调用 URL.createObjectURL() 方法将一个 Blob 对象转换为 URL 地址,并返回该地址。生成的 URL 地址具有如下特点:

  • 以 blob: 开头,表示将要访问的资源是一个 Blob 对象;
  • 后面跟随一个唯一的标识符,用于唯一地表示该 Blob 对象;
  • 可以直接作为链接使用,例如设置 a 元素的 href 属性,或者在 JavaScript 中进行网络请求等操作。

其形式为 blob:<origin>/<uuid>,对应的示例如下:

image.png 浏览器内部为每个通过 URL.createObjectURL 生成的 URL 存储了一个 URL → Blob 映射。因此,此类 URL 较短,但可以访问 Blob。生成的 URL 仅在当前文档打开的状态下才有效。它允许引用 <img><a> 中的 Blob,但如果你访问的 Blob URL 不再存在,则会从浏览器中收到 404 错误。

上述的 Blob URL 看似很不错,但实际上它也有副作用。虽然存储了 URL → Blob 的映射,但 Blob 本身仍驻留在内存中,浏览器无法释放它。映射在文档卸载时自动清除,因此 Blob 对象随后被释放。

但是,如果应用程序寿命很长,那不会很快发生。因此,如果我们创建一个 Blob URL,即使不再需要该 Blob,它也会存在内存中。

针对这个问题,我们可以调用 URL.revokeObjectURL(url) 方法,从内部映射中删除引用,从而允许删除 Blob(如果没有其他引用),并释放内存。接下来,我们来看一下 Blob 文件下载的具体示例。

示例
  1. 文件下载
const downloadButton = document.querySelector('#btn');

downloadButton.addEventListener('click', () => {
  fetch('https://content.volccdn.com/obj/volc-content/test.csv')
    .then(response => response.blob())
    .then(blob => {
      const url = URL.createObjectURL(blob);

      const linkElement = document.createElement('a');
      linkElement.href = url;
      linkElement.download = '秘密数据.csv';

      document.body.appendChild(linkElement);

      linkElement.click();
    });
});
  1. 本地图片预览
const fileInput = document.querySelector('input[type="file"]');

fileInput.addEventListener('change', (event) => {
  const file = event.target.files[0];
  const imageUrl = URL.createObjectURL(file);

  const imageElement = document.createElement('img');
  imageElement.src = imageUrl;
  imageElement.width = 200
  imageElement.height = 200
  document.body.appendChild(imageElement);
})
注意事项

在使用 Blob URL 时,需要注意以下几点:

  1. 资源释放:使用 Blob URL 访问 Blob 数据时,需要注意资源释放问题。由于 Blob URL 是基于浏览器文件系统机制实现的,因此在使用完毕后应该调用 URL.revokeObjectURL() 方法释放对应的资源,以避免内存泄漏等问题。
  2. 安全性限制:Blob URL 受到安全性限制,不能跨域访问和传输数据。因此,在使用 Blob URL 时,应该考虑到跨域访问和数据传输等问题,并采取相应的安全措施。
  3. 兼容性问题:Blob URL 在不同浏览器中的实现方式可能有所差异,因此在使用时需要进行相应的兼容性测试和处理。同时,在一些老旧的浏览器中可能无法支持 Blob URL 特性,需要使用其他方案来实现相应的功能。

除了以上注意事项外,还有一些常见问题需要注意:

  1. Blob URL 可能会被浏览器缓存,导致数据更新不及时。如果希望每次访问都能够获取最新的数据,可以考虑使用 XMLHttpRequest 等具有缓存控制功能的 API 来实现。
  2. Blob URL 在一些移动设备上可能存在性能问题,特别是在文件较大时。因此,在设计移动端应用时,应该考虑到性能优化和流量控制等问题。
  3. Blob URL 的生成需要消耗一定的计算资源,如果频繁生成大量的 Blob URL,可能会影响页面性能。因此,在实际开发中应该设计合理的缓存机制和资源管理策略,避免产生不必要的性能消耗。

Blob转换为Base64

Blob 转换为 Base64 是一种将二进制数据转换为文本形式的方法,它可以对 Blob 对象进行编码,并将编码结果以字符串的形式输出。在 Web 开发中,Blob 转换为 Base64 可以应用于多种场景,例如图片上传、文件传输等。

base64介绍

image.png

示例

Blob 转换为 Base64 的原理非常简单,即通过 FileReader API 将 Blob 对象读取为 DataURL 格式的字符串,并从字符串中提取出 Base64 编码的部分。Data URL 是一种包含数据和元数据的通用格式,它以 base64 或 blob 形式表示数据内容。通过调用 FileReader.readAsDataURL() 方法,我们可以将 Blob 对象转换为 Data URL 格式的字符串。然后,我们可以从字符串中提取出 base64 编码的部分,并将其作为最终结果返回。

  1. 字符串 Blob 转换为Base64
const btn = document.getElementById('btn')
const code = document.getElementById('code')
const blob = new Blob(['Hello World'], { type: 'text/plain' });


btn.addEventListener('click', () => {
  const reader = new FileReader();
  reader.readAsDataURL(blob);

  reader.onload = () => {
    const result = reader.result;
    const base64 = result.split(',')[1];

    console.log(base64);
    code.innerText = base64
  }
}, true)
  1. 将file Blob 转换为Base64

目录结构

├── node_modules
├── package.json
├── pnpm-lock.yaml
├── server.ts
└── static
<!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>
  </head>
  <body>
    <label for="file">
      <input type="file" name="file" id="file" />
    </label>
    <script>
      const fileInput = document.getElementById("file");
      fileInput.addEventListener("change", (event) => {
        const file = event.target.files[0];

        const reader = new FileReader();
        reader.readAsDataURL(file);

        reader.onload = () => {
          const result = reader.result;
          const base64 = result.split(",")[1];

          // 发送 base64 编码的图片数据到服务器端
          fetch("/base64Upload", {
            method: "POST",
            body: JSON.stringify({ data: base64, fileName: file.name }),
            headers: { "Content-Type": "application/json" },
          });
        };
      });
    </script>
  </body>
</html>
import Koa from "koa";
import KoaRouter from "koa-router";
import cors from "koa-cors";
import koaStatic from "koa-static";
import koaBody from "koa-body";
import path from "path";
import fs from "fs";
const app = new Koa();
const router = new KoaRouter();
app.use(cors());
app.use(
  koaBody({
    multipart: true,
    formidable: {
      uploadDir: path.join(__dirname, "static"),
      multiples: true,
      keepExtensions: true,
    },
  })
);

router.post("/base64Upload", async (ctx, next) => {
  const req = ctx.request.body;
  const buffer = Buffer.from(req.data, "base64");
  const uploadPath = "./static"; 
  const filePath = `${uploadPath}/${req.fileName}`;
  fs.writeFileSync(`${filePath}`, buffer);
  ctx.body = { code: 200, description: "SUCCESS", url: filePath };
});
app.use(router.routes())
app.use(router.allowedMethods())
app.use(koaStatic(path.join(__dirname, "static")));

app.listen(9898, () => console.log("listen port at 9898"));

  1. 文件base64下载
import Koa from "koa";
import KoaRouter from "koa-router";
import cors from "koa-cors";
import koaStatic from "koa-static";
import koaBody from "koa-body";
import path from "path";
import fs from "fs";
import mime from 'mime'
const app = new Koa();
const router = new KoaRouter();
app.use(cors());
app.use(
  koaBody({
    multipart: true,
    formidable: {
      uploadDir: path.join(__dirname, "static"),
      multiples: true,
      keepExtensions: true,
    },
  })
);

router.get("/base-file", async (ctx) => {
  const base64 = fs.readFileSync(path.join(__dirname, "static/a.jpg"), {
    encoding: "base64",
  });
  let ext = path.extname(path.join(__dirname, "static/a.jpg"));
  ext = ext.split('.').pop()
  ctx.body = {base64, type: mime.getType(path.join(__dirname, "static/a.jpg"))};
});
app.use(router.routes());
app.use(router.allowedMethods());
app.use(koaStatic(path.join(__dirname, "static")));

app.listen(9898, () => console.log("listen port at 9898"));

// 前端
const btn = document.getElementById("btn");
btn.addEventListener(
  "click",
  () => {
    fetch("/base-file")
      .then((response) => {
        return response.json();
      })
      .then((data) => {
        console.log(data);
        const binary = atob(data.base64);

        // 将解码后的二进制数据保存为 Blob 对象并下载
        const len = binary.length;
        const buffer = new ArrayBuffer(len);
        const view = new DataView(buffer);

        for (let i = 0; i < len; i++) {
          view.setUint8(i, binary.charCodeAt(i));
        }

        const blob = new Blob([view], { type: "image/jpg" });
        const linkElement = document.createElement("a");
        linkElement.href = URL.createObjectURL(blob);
        linkElement.download = '小🐟';

        document.body.appendChild(linkElement);

        linkElement.click();
      });
  },
  true
);

文件切分上传

├── package.json
├── pnpm-lock.yaml
├── server.ts
├── static
│   ├── chunk.html
import Koa from "koa";
import KoaRouter from "koa-router";
import cors from "koa-cors";
import koaStatic from "koa-static";
import koaBody from "koa-body";
import path from "path";
import fs from "fs";
const app = new Koa();
const router = new KoaRouter();
app.use(cors());
app.use(
  koaBody({
    multipart: true,
    formidable: {
      uploadDir: path.join(__dirname, "static"),
      multiples: true,
      keepExtensions: true,
    },
  })
);

router.post("/chunk-upload", async (ctx) => {
  const chunk = ctx.request.files?.chunk as any;
  ctx.body = chunk.filepath;
});
router.post("/chunk-merge", async (ctx) => {
  const req = ctx.request.body;
  const pathList = req.pathList as string[];
  console.log(pathList)
  const filename = req.filename;
  let data = [] as any[];
  pathList.forEach((element) => {
    data.push(fs.readFileSync(element));
    fs.unlinkSync(element);
  });
  fs.writeFileSync(
    path.join(__dirname, `static/${filename}`),
    Buffer.concat(data)
  );
  ctx.body = {ok: true}
});
app.use(router.routes());
app.use(router.allowedMethods());
app.use(koaStatic(path.join(__dirname, "static")));

app.listen(9898, () => console.log("listen port at 9898"));
// 前端部分
      const fileInput = document.getElementById("file");
      let pathList = []
      fileInput.addEventListener("change", async (event) => {
        pathList = []
        const file = event.target.files[0];
        const chunkSize = 1024 * 200; // 每个块的大小为 1024

        let start = 0;
        let end = Math.min(chunkSize, file.size);

        while (start < file.size) {
          const chunk = file.slice(start, end); // 获取当前块的数据

          const formData = new FormData();
          formData.append("chunk", chunk);
          formData.append("start", start);
          formData.append("end", end);
          formData.append("filename", file.name);

          await fetch("/chunk-upload", {
            method: "POST",
            body: formData,
          }).then(data => data.text()).then((data) => pathList.push(data));

          start = end;
          end = Math.min(start + chunkSize, file.size);
        }
        await fetch('/chunk-merge', {
            method: 'POST',
            body: JSON.stringify({pathList, filename: '小🐟.jpg'}),
            headers: { "Content-Type": "application/json" }
        })

        console.log("Upload complete!");
      });

文件预览下载

const btn = document.getElementById('btn')
const image = document.getElementById('img') as HTMLImageElement
btn.addEventListener('click', () => {
  fetch('https://img2.baidu.com/it/u=1830236614,3389089205&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500').then((file) => file.blob()).then((data) => {
    console.log(data)
    // 新页面打开
    window.open(URL.createObjectURL(data))
    // 预览
    image.src = URL.createObjectURL(data)
    // 下载
    const link = document.createElement('a')
    link.href = URL.createObjectURL(data)
    link.download = 'a.jpeg'
    link.click()
  })
}, true)

配合canvas进行图片压缩

在 HTML5 中,可以使用 canvas.toBlob() 方法将 Canvas 元素中的图像转换为 Blob 对象。通过设置其第二个参数,我们还可以对生成的 Blob 对象进行压缩。

const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');



const compressBlob = (blob, quality) => {
  return new Promise((resolve) => {
    const reader = new FileReader();
    reader.readAsDataURL(blob);
    reader.onload = () => {
      const img = new Image();
      img.src = reader.result;
      img.onload = () => {
        const canvas = document.createElement('canvas');
        const ctx = canvas.getContext('2d');

        const { width, height } = img;
        canvas.width = width;
        canvas.height = height;

        ctx.drawImage(img, 0, 0, width, height);

        canvas.toBlob((compressedBlob) => {
          resolve(compressedBlob);
        }, 'image/jpeg', quality);
      };
    };
  });
}
const getSize = (url) => {
  const file = new FileReader()
  file.onloadend = (e) => {
    console.log(e.total)
  }
  file.readAsDataURL(url)
}
const image = new Image();
image.src = 'https://webstatic.mihoyo.com/upload/event/2022/07/29/c31dd1d732913e4ab5f3d4f03346a706_9097205533659112586.png';
image.crossOrigin="anonymous"
image.onload = () => {
  console.log('old image size', getSize(new Blob([image.src]) ))
  const { width, height } = image;
  canvas.width = width;
  canvas.height = height;

  ctx.drawImage(image, 0, 0);

  canvas.toBlob(async (blob) => {
    const compressedBlob = await compressBlob(blob, 0.8);
    const compressedImage = new Image();
    compressedImage.crossOrigin="anonymous"
    compressedImage.src = URL.createObjectURL(compressedBlob);
    console.log('new image size', await getSize(new Blob([compressedImage.src]) ))
    document.body.appendChild(compressedImage);
  }, 'image/jpeg', 0.9);
};
扩展知识

在Chrome内核中,toBlob()方法的实现是基于Chromium源代码中的 Skia 图形库。Skia 是一个开源的 2D 图形库,被广泛应用于 Google 的多个产品中,包括Chrome 浏览器、Android 操作系统、Chrome OS等。

具体来说,在Chrome 内核中调用toBlob()方法时,会先将 Canvas 元素中的像素数据传递给 Skia 图形库进行处理,并最终生成一个Bitmap对象。然后根据需要将Bitmap对象转换为PNG或JPEG格式的二进制数据,并封装为Blob对象返回。

下面是Chrome内核中toBlob()方法的核心代码片段:

void PaintCanvas::toBlob(
    ScriptPromiseResolver* resolver,
    const String& type,
    int quality) {
  CanvasResourceProvider* provider = GetResourceProvider();
  SkBitmap snapshot;
  if (!provider->CopyTo(&snapshot))
    return;

  SkAutoTDelete<SkWStream> stream(SkMemoryStream::New());
  SkImageEncoder::EncodeStream(stream, snapshot, SkImageEncoder::TypeForMIMEType(type),
                               quality);

  // Create a blob with the data.
  Vector<uint8_t> contents(SkMemoryStreamGetMemoryBase(stream.get()),
                           SkMemoryStreamGetSize(stream.get()));
  Blob* blob = Blob::Create(contents.data(), contents.size(), type);
  resolver->Resolve(blob);
}

参考

skia.org/docs/

w3c.github.io/FileAPI/#bl…

juejin.cn/post/704450…

File

文件(File)接口提供有关文件的信息,并允许网页中的JavaScript访问其内容。

File对象是特殊类型的Blob,且可以用在任意的Blob类型的 context 中。比如说,FileReader, URL.createObjectURL(), createImageBitmap(), 及XMLHttpRequest.send()都能处理 Blob 和 File。

API

有两种方式可以获取它。

第一种,与 Blob 类似,有一个构造器:

new File(fileParts, fileName, [options])
  • fileParts —— Blob/BufferSource/USVString 类型值的数组。

  • fileName —— 文件名字符串。

  • options —— 可选对象:

    • lastModified —— 最后一次修改的时间戳(整数日期)。

第二种,更常见的是,我们从 <input type="file"> 或拖放或其他浏览器接口来获取文件。在这种情况下,file 将从操作系统(OS)获得 this 信息。

由于 File 是继承自 Blob 的,所以 File 对象具有相同的属性,附加:

  • name —— 文件名,
  • lastModified —— 最后一次修改的时间戳。

这就是我们从 <input type="file"> 中获取 File 对象的方式:


const fileInput = document.getElementById("file");
fileInput.addEventListener("change", (event) => {
  const file = event.target.files[0];
  console.log(`File name: ${file.name}`); // 例如 my.png
  console.log(`Last modified: ${file.lastModified}`); // 例如 1552830408824
})

FileReader

FileReader 是一个对象,其唯一目的是从 Blob(因此也从 File)对象中读取数据。

它使用事件来传递数据,因为从磁盘读取数据可能比较费时间。

构造函数:

let reader = new FileReader(); // 没有参数

主要方法:

  • readAsArrayBuffer(blob) —— 将数据读取为二进制格式的 ArrayBuffer
  • readAsText(blob, [encoding]) —— 将数据读取为给定编码(默认为 utf-8 编码)的文本字符串。
  • readAsDataURL(blob) —— 读取二进制数据,并将其编码为 base64 的 data url。
  • abort() —— 取消操作。

read* 方法的选择,取决于我们喜欢哪种格式,以及如何使用数据。

  • readAsArrayBuffer —— 用于二进制文件,执行低级别的二进制操作。对于诸如切片(slicing)之类的高级别的操作,File 是继承自 Blob 的,所以我们可以直接调用它们,而无需读取。
  • readAsText —— 用于文本文件,当我们想要获取字符串时。
  • readAsDataURL —— 当我们想在 src 中使用此数据,并将其用于 img 或其他标签时。还有一种用于此的读取文件的替代方案:URL.createObjectURL(file)

读取过程中,有以下事件:

  • loadstart —— 开始加载。
  • progress —— 在读取过程中出现。
  • load —— 读取完成,没有 error。
  • abort —— 调用了 abort()
  • error —— 出现 error。
  • loadend —— 读取完成,无论成功还是失败。

读取完成后,我们可以通过以下方式访问读取结果:

  • reader.result 是结果(如果成功)
  • reader.error 是 error(如果失败)。

使用最广泛的事件无疑是 loaderror

这是一个读取文件的示例:

const fileInput = document.getElementById("file");
fileInput.addEventListener("change", (event) => {
  const file = event.target.files[0];

  const reader = new FileReader();

  reader.readAsText(file);

  reader.onload = function () {
    console.log(reader.result);
  };

  reader.onerror = function () {
    console.log(reader.error);
  };
})

总结

image.png

题外知识

  1. bit 是 binary digit(二进制数字)的缩写,通常用于描述计算机中最小的数据单元。一个bit可以取0或1两个值,它是计算机中所有信息的基础单位;1 byte = 8 bits。

    1. 将比特转换为字节:byte = bit / 8
    2. 将字节转换为比特:bit = byte * 8
  2. Endianness(字节序):或字节顺序("Endian"、"endianness" 或 "byte-order"),描述了计算机如何组织字节,组成对应的数字。

    1.   举个例子,用不同字节序存储数字 0x12345678(即十进制中的 305 419 896):
    2. little-endian0x78 0x56 0x34 0x12
    3. big-endian0x12 0x34 0x56 0x78
    4. mixed-endian(文物,非常罕见):0x34 0x12 0x78 0x56