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]); // 244Uint16Array 转 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 }); };