有趣的面试题
一天小🐟面试心脏跳动,前面面试小🐟进行的非常顺利,面试官也是对小🐟非常满意,一时兴起就问:小🐟同学,你知道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。
大家是否好奇过?
- 为什么flv.js能够在浏览器原生不支持的情况下播放 .flv 格式的文件?src 为什么是一段以 blob: 开头的字符串?
- 后端给了个 POST 的下载请求,前端怎么办?
在 Web 开发中,当我们处理文件时(创建,上传,下载),经常会遇到二进制数据。另一个典型的应用场景是图像处理。
这些都可以通过 JavaScript 进行处理,而且二进制操作性能更高。
不过,在 JavaScript 中有很多种二进制数据格式,会有点容易混淆。仅举几个例子:
ArrayBuffer,Uint8Array,DataView,Blob,File及其他。
与其他语言相比,JavaScript 中的二进制数据是以非标准方式实现的。但是,当我们理清楚以后,一切就会变得相当简单了。
ArrayBuffer
基本的二进制对象是ArrayBuffer,是对固定长度的连续内存空间的引用 。 它使用一种类数组结构,用于表示通用的、固定长度的二进制缓冲区。ArrayBuffer 对象与普通 JavaScript 数组不同,它不能直接存储数字、字符串、对象等数据类型,而是只能存储二进制数据。ArrayBuffer 长度在创建时就被确定,不能改变。
ArrayBuffer不是某种东西的数组
让我们先澄清一个可能的误区。ArrayBuffer 与 Array 没有任何共同之处:
- 它的长度是固定的,我们无法增加或减少它的长度。
- 它正好占用了内存中的那么多空间。
- 要访问单个字节,需要另一个“视图”对象,而不是
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 个字节)。
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 到 127 | 1 | 8 位有符号整型(补码) | byte | int8_t |
| Uint8Array | 0 到 255 | 1 | 8 位无符号整型 | octet | uint8_t |
| Uint8ClampedArray | 0 到 255 | 1 | 8 位无符号整型(一定在 0 到 255 之间) | octet | uint8_t |
| Int16Array | -32768 到 32767 | 2 | 16 位有符号整型(补码) | short | int16_t |
| Uint16Array | 0 到 65535 | 2 | 16 位无符号整型 | unsigned short | uint16_t |
| Int32Array | -2147483648 到 2147483647 | 4 | 32 位有符号整型(补码) | long | int32_t |
| Uint32Array | 0 到 4294967295 | 4 | 32 位无符号整型 | unsigned long | uint32_t |
| Float32Array | -3.4E38 到 3.4E38 并且 1.2E-38 是最小的正数 | 4 | 32 位 IEEE 浮点数(7 位有效数字,例如 1.234567) | unrestricted float | float |
| Float64Array | -1.8E308 到 1.8E308 并且 5E-324 是最小的正数 | 8 | 64 位 IEEE 浮点数(16 位有效数字,例如 1.23456789012345) | unrestricted double | double |
| BigInt64Array | -263 到 263 - 1 | 8 | 64 位有符号整型(补码) | bigint | int64_t (signed long long) |
| BigUint64Array | 0 到 264 - 1 | 8 | 64 位无符号整型 | bigint | uint64_t (unsigned long long) |
这些对象都是全局对象,可以通过以下方式创建:
const array = new TypedArray(buffer [, byteOffset, length]);
当你看到new TypedArray之类的内容时,它表示new Int8Array、new Uint8Array及其他中之一。其中,buffer 参数使用一个 ArrayBuffer 对象作为它的元素缓冲区;byteOffset 参数指向缓冲区中开始使用的索引。
该对象不能被直接实例化——试图去使用new构造它将会抛出TypeError。
所有 TypedArray 子类的构造函数只能使用 new 构造。试图在没有 new 的情况下调用,会抛出 TypeError。
越界行为
如果我们尝试将越界值写入类型化数组会出现什么情况?不会报错。但是多余的位被切除。
例如,我们尝试将 256 放入 Uint8Array。256 的二进制格式是 100000000(9 位),但 Uint8Array 每个值只有 8 位,因此可用范围为 0 到 255。
对于更大的数字,仅存储最右边的(低位有效)8 位,其余部分被切除:
Uint8ClampedArray
因此结果是 0。
257 的二进制格式是 100000001(9 位),最右边的 8 位会被存储,因此数组中会有 1:
换句话说,该数字对 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),map,slice,find 和 reduce 等。
但有几件事我们做不了:
- 没有
splice—— 我们无法“删除”一个值,因为类型化数组是缓冲区(buffer)上的视图,并且缓冲区(buffer)是固定的、连续的内存区域。我们所能做的就是分配一个零值。 - 无
concat方法。
还有两种其他方法:
arr.set(fromArr, [offset])从offset(默认为 0)开始,将fromArr中的所有元素复制到arr。arr.subarray([begin, end])创建一个从begin到end(不包括)相同类型的新视图。这类似于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
- 如何使用 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);
- 如何使用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 等。
构造函数
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
- 从 字符串 创建 Blob
- 从类型化数组(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>,对应的示例如下:
浏览器内部为每个通过
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 文件下载的具体示例。
示例
- 文件下载
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();
});
});
- 本地图片预览
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 时,需要注意以下几点:
- 资源释放:使用 Blob URL 访问 Blob 数据时,需要注意资源释放问题。由于 Blob URL 是基于浏览器文件系统机制实现的,因此在使用完毕后应该调用 URL.revokeObjectURL() 方法释放对应的资源,以避免内存泄漏等问题。
- 安全性限制:Blob URL 受到安全性限制,不能跨域访问和传输数据。因此,在使用 Blob URL 时,应该考虑到跨域访问和数据传输等问题,并采取相应的安全措施。
- 兼容性问题:Blob URL 在不同浏览器中的实现方式可能有所差异,因此在使用时需要进行相应的兼容性测试和处理。同时,在一些老旧的浏览器中可能无法支持 Blob URL 特性,需要使用其他方案来实现相应的功能。
除了以上注意事项外,还有一些常见问题需要注意:
- Blob URL 可能会被浏览器缓存,导致数据更新不及时。如果希望每次访问都能够获取最新的数据,可以考虑使用 XMLHttpRequest 等具有缓存控制功能的 API 来实现。
- Blob URL 在一些移动设备上可能存在性能问题,特别是在文件较大时。因此,在设计移动端应用时,应该考虑到性能优化和流量控制等问题。
- Blob URL 的生成需要消耗一定的计算资源,如果频繁生成大量的 Blob URL,可能会影响页面性能。因此,在实际开发中应该设计合理的缓存机制和资源管理策略,避免产生不必要的性能消耗。
Blob转换为Base64
Blob 转换为 Base64 是一种将二进制数据转换为文本形式的方法,它可以对 Blob 对象进行编码,并将编码结果以字符串的形式输出。在 Web 开发中,Blob 转换为 Base64 可以应用于多种场景,例如图片上传、文件传输等。
base64介绍
示例
Blob 转换为 Base64 的原理非常简单,即通过 FileReader API 将 Blob 对象读取为 DataURL 格式的字符串,并从字符串中提取出 Base64 编码的部分。Data URL 是一种包含数据和元数据的通用格式,它以 base64 或 blob 形式表示数据内容。通过调用 FileReader.readAsDataURL() 方法,我们可以将 Blob 对象转换为 Data URL 格式的字符串。然后,我们可以从字符串中提取出 base64 编码的部分,并将其作为最终结果返回。
- 将 字符串 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)
- 将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"));
- 文件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);
}
参考
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(如果失败)。
使用最广泛的事件无疑是 load 和 error。
这是一个读取文件的示例:
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);
};
})
总结
题外知识
-
bit 是 binary digit(二进制数字)的缩写,通常用于描述计算机中最小的数据单元。一个bit可以取0或1两个值,它是计算机中所有信息的基础单位;1 byte = 8 bits。
- 将比特转换为字节:byte = bit / 8
- 将字节转换为比特:bit = byte * 8
-
Endianness(字节序):或字节顺序("Endian"、"endianness" 或 "byte-order"),描述了计算机如何组织字节,组成对应的数字。
- 举个例子,用不同字节序存储数字
0x12345678(即十进制中的 305 419 896): - little-endian:
0x78 0x56 0x34 0x12 - big-endian:
0x12 0x34 0x56 0x78 - mixed-endian(文物,非常罕见):
0x34 0x12 0x78 0x56
- 举个例子,用不同字节序存储数字