背景
前端开发中大多处理的都是文本类型的数据,例如数字、字符串等,但有时候也会接触到二进制的数据处理,比如文件、音频、视频相关的操作。今天的分享就是对二进制数据及其相关操作的整理。
我们先整体看一下有哪些二进制相关的类
我们至上而下,一个一个看,首先是File类。
File
上传文件的需求,大家肯定都遇到过。像Element或者Ant Design都提供了Upload组件去解决文件上传的问题,但其实HTML原生就已经提供了文件上传的能力。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<input type="file" id="fileInput" multiple="multiple" />
<script>
const fileInput = document.getElementById("fileInput");
fileInput.addEventListener("change", (e) => {
const files = e.target.files;
console.log({ files, file: files[0] });
});
</script>
</body>
</html>
只需要把input元素的type设置为file,就可以支持上传了,通过设置multiple属性,可以控制是否能上传多个元素。上传之后,就能拿到e.target.files,这个就是一个File类型的数组,可以看到File类型有这些属性,可以从中获知这个文件的基本信息,比如:
- lastModified:引用文件最后修改日期,为自1970年1月1日0:00以来的毫秒数;
- lastModifiedDate:引用文件的最后修改日期;
- name:引用文件的文件名;
- size:引用文件的文件大小;
- type:文件的媒体类型(MIME);
File本身并没有提供额外的方法去操作它,想了解怎么去操作一个File,就需要了解第二个类Blob,Blob是File的父类,这一点从File的原型上也能看出来。所以我们了解了怎么操作一个Blob对象,也就知道了怎么去操作一个File对象。
Blob
api
Blob的全称是Binary Large Object,表示二进制类型的大对象,名称来源于数据库。Blob 通常是视频、音频或其他多媒体文件。在JavaScript中Blob表示二进制数据,但是不一定是大量数据,几个字符串也可以生成一个Blob对象。
var aBlob = new Blob(blobParts, options);
blob的构造函数有两个参数,
- 第一个参数是blob对象的内容,它是一个数组,数组的内容可以是字符串、Blob、ArrayBuffer等
- 第二个参数是一个对象,可以在这个对象中指定blob的MIME类型,这个值指定了blob对象的媒体类型,这个类型和请求/响应中的Content-Type是一样的。通常包含类型(type)和子类型(subtype)两个部分。
-
- 类型代表数据类型所属的大致分类,例如 video 或 text。
- 子类型标识了 MIME 类型所代表的指定类型的确切数据类型。以 text 类型为例,它的子类型包括:plain(纯文本)、html(HTML 源代码)。
- 像text/plain、text/html、application/json都是我们经常接触到的类型。
我们现在用简单使用字符串去生成一个Blob,去看看Blob具体有什么。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script>
const blob = new Blob(["Hello, world!", "Hello JavaScript"], {
type: "text/plain",
});
console.log({ blob });
</script>
</body>
</html>
可以看到有两个属性:
- size:blob对象的字节数
- type:blob对象的媒体类型
然后它有几个方法:
- slice([start[, end[, contentType]]]):类似数组的slice方法,可以截取这个对象的某一块生成一个新的blob对象
- text():返回一个 Promise 对象且包含 blob 所有内容的 UTF-8 格式的 USVString。像我们刚刚用字符串生成blob,就可以用text去读取它的内容
- arrayBuffer():返回一个 Promise 对象且包含 blob 所有内容的二进制格式的 ArrayBuffer。这个类后面会讲
- stream():返回一个能读取 blob 内容的可读流。
基本的api介绍完之后,我们来看一下Blob有哪些使用场景
应用场景
分片上传
第一个场景是分片上传。大文件传输可能会遇到内存或者性能的限制,我们对大文件进行切割,然后分片进行上传。这里只是一个简化的分片上传的函数,只是用到了blob.slice这一个api,把大文件分割成一个、一个的chunk,然后后台再组合起来。
async function chunkedUpload(file, chunkSize, url) {
for (let start = 0; start < file.size; start += chunkSize) {
const chunk = file.slice(start, start + chunkSize + 1);
const fd = new FormData();
fd.append("data", chunk);
await fetch(url, { method: "post", body: fd }).then((res) => res.text());
}
}
用作URL
Blob的第二个应用场景就是作为URL来使用。这里需要先介绍一下Blob URL这个概念。
Blob URL/Object URL
Blob URL/Object URL 是一种伪协议,在浏览器中我们使用 URL.createObjectURL 方法来创建 Blob URL,该方法接收一个 Blob 对象,并为其创建一个唯一的 URL,其形式为 blob:<origin>/<uuid>,对应的示例如下:
blob:https://example.org/40a5fb5a-d56d-4a33-b4e2-0acf6a8e5f641
浏览器内部为每个创建的URL存储了一个 URL → Blob 映射。因此,此类 URL 较短,但可以访问 Blob。
伪协议,也被称为伪URL协议或者伪URL方案,是一种特殊的URL方案,它并不指向网络上的资源,而是在浏览器中执行特定的操作或访问特定的资源。伪协议的URL通常以冒号结束,没有指定的主机名或路径。
基于这个特性,可以衍生出图片预览和文件下载这两个用处。我们先来看一下图片预览:
图片预览
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<input type="file" class="upload" />
<img src="" alt="" class="img" />
<script>
const input = document.querySelector(".upload");
const img = document.querySelector(".img");
input.addEventListener("change", function (e) {
const file = e.target.files[0];
const blobUrl = URL.createObjectURL(file);
img.src = blobUrl;
});
</script>
</body>
</html>
下载文件
第二个衍生的用处是文件下载。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<button class="button">下载</button>
<script>
function downloadText() {
const blob = new Blob(["Hello, world!"], {
type: "text/plain",
});
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `blob.txt`;
link.click();
// 清理
URL.revokeObjectURL(url);
}
const btn = document.querySelector(".button");
btn.addEventListener("click", downloadText);
</script>
</body>
</html>
Blob有一个副作用,就是Blob URL生成之后,Blob会常驻内存中,直到文档卸载,Blob才会被自动清理,如果应用程序生命周期比较长,会造成内存不必要的浪费。因此如果不需要用了,就应该调用URL.revokeObjectURL手动清理掉。
上面这个场景是前端主动生成一个文件。更多的时候是从接口中下载一个文件,像MES系统经常会返回一个Excel类型的文件流,比如推广链接
const response = await fetch(API_INFO_FEEDS_LINK_EXPORT({}).url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(searchParams.value),
});
if (!response.ok) {
ElMessage.error('请求失败');
return;
}
const blobData = await response.blob();
const blob = new Blob([blobData], {type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'});
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `推广链接-${dayjs().format('YYYYMMDD')}.xlsx`;
// 模拟点击链接,触发下载
link.click();
// 清理
URL.revokeObjectURL(url);
除了blob url的方式可以用作url之外,也可以转成base64来当成url用
转成base64
Base64 是一种编码方案,它可以将二进制数据转换为 ASCII 字符串。
大多数现代浏览器都支持Data URLs这种特殊的URL方案,它允许你直接在 URL 中嵌入数据,可以减少一次网络请求。Data URL最常见的格式就是base64
这里需要引入一工具类FileReader,它可以把blob对象转成base64
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<input type="file" class="upload" />
<img src="" alt="" class="img" />
<script>
const input = document.querySelector(".upload");
const img = document.querySelector(".img");
input.addEventListener("change", function (e) {
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = function (e) {
img.src = e.target.result;
};
reader.readAsDataURL(file);
});
</script>
</body>
</html>
读取文件内容
Blob对象的另一个应用场景是读取文件内容。FileReader的readAsText方法,将blob转成文本
<input type="file" class="file" />
<div class="show"></div>
const input = document.querySelector(".file");
const show = document.querySelector(".show");
input.addEventListener("change", function (e) {
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = function (e) {
const text = e.target.result;
console.log({ text });
show.innerHTML = text;
};
reader.readAsText(file);
});
通过流的形式读取文件内容
- FileReader 会将整个文件加载到内存中,适用于小型文件。对于大型文件,可能会导致内存消耗过大。这个时候就需要引入stream api。
- stream它提供了一种流式处理数据的方式,可以逐块地读取和处理文件内容,而不必将整个文件加载到内存中。流的种类有可读流、可写流、转换流等。使用流的时候还需要考虑流上游生产和下游消费的速度。上游生产速度远大于下游消费速度,会造成缓冲区溢出,淹没下游。生产速度远小于消费速度,这样存在性能的浪费。
<input type="file" class="file" />
这里只介绍怎么读取,怎么解析chunk在此不作介绍。
const input = document.querySelector(".file");
input.addEventListener("change", (e) => {
const file = e.target.files[0];
const reader = file.stream().getReader();
function readNextChunk() {
reader.read().then(({ done, value }) => {
if (done) {
console.log("done");
return;
}
console.log("读取到 " + value.byteLength / 1024 + " kb数据");
// 继续读取下一块数据
readNextChunk();
});
}
// 开始读取第一块数据
readNextChunk();
});
Blob对象就介绍到这里了,它可以用slice整段的裁剪,也可以整体转成一个新对象来使用,但它是不可编辑的,如果不需要编辑内容,使用blob就足够了,如果想对一段二进制数据的内容进行编辑,就需要了解ArrayBuffer这个类。
ArrayBuffer
api
ArrayBuffer 对象用来表示通用的、固定长度的原始二进制数据缓冲区。
这个对象的原始设计目的,与 WebGL 项目有关。所谓 WebGL,就是指浏览器与显卡之间的通信接口,为了满足 JavaScript 与显卡之间大量的、实时的数据交换,它们之间的数据通信必须是二进制的,而不能是传统的文本格式。文本格式传递一个 32 位整数,两端的 JavaScript 脚本与显卡都要进行格式转化,将非常耗时。这时要是存在一种机制,可以像 C 语言那样,直接操作字节,将 4 个字节的 32 位整数,以二进制形式原封不动地送入显卡,脚本的性能就会大幅提升。
我们先简单看一下ArrayBuffer的api
- 构造函数:new ArrayBuffer(bytelength),指定缓冲区的字节数量,初始化内容是0
new ArrayBuffer(length)
除了构造函数之外,fileReader也能把一个blob对象转成ArrayBuffer
const reader = new FileReader();
reader.onload = function(e) {
let arrayBuffer = reader.result;
}
reader.readAsArrayBuffer(file);
- ArrayBuffer.prototype.byteLength,返回字节数量
- ArrayBuffer.prototype.slice(start, end),支持裁剪
const buffer = new ArrayBuffer(16);
console.log({ buffer }); // 16
console.log({ buffer: buffer.slice(0, 8) }); // 8
视图
ArrayBuffer 的内容不能直接操作,只能通过视图对它们进行下标读写,这些改变最终都会反应到它所建立在的 ArrayBuffer 之上。
- TypedArray 视图:固定数据类型的视图。共包括 9 种类型的视图,比如Uint8Array(无符号 8 位整数)数组视图, Int16Array(16 位整数)数组视图, Float32Array(32 位浮点数)数组视图等等。
- DataView 视图:不固定数据类型,可以是上面TypedArray类型的组合。比如第一个字节是 Uint8(无符号 8 位整数)、第二、三个字节是 Int16(16 位整数)、第四个字节开始是 Float32(32 位浮点数)等等,此外还可以自定义字节序。
ArrayBuffer 本身只是一行 0 和 1 串。 ArrayBuffer 不知道该数组中第一个元素和第二个元素之间的分隔位置。
为了操作这段数据,要将其分解为多个盒子,这个盒子我们就可以理解为视图。可以使用TypedArray添加数据视图,并且你可以使用许多不同类型的类型数组。
例如,你可以有一个 Int8 类型的数组,它将把这个数组分成 8-bit 的字节数组。
Uint16Array
TypedArray可以直接通过下标进行读写,支持的api和数组几乎是一样的
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script>
// new Uint8Array(buffer [, byteOffset [, length]]);
var buffer = new ArrayBuffer(8);
var int8Arr = new Uint8Array(buffer, 1, 4);
int8Arr[0] = 42;
console.log({ buffer, bit: int8Arr[0] });
</script>
</body>
</html>
支持的api和数组几乎是一样的
DataView
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script>
var buffer = new ArrayBuffer(8);
var dataview = new DataView(buffer, 1, 4);
dataview.setInt8(0, 42);
console.log(dataview);
</script>
</body>
</html>
应用
直接修改二进制数据具体内容的场景在前端应该比较少见,我们在这只介绍一个图片灰度化的应用场景。这个应用需要使用canvas完成,我们先介绍一些canvas的api。
getImageData:获取画布上的像素数据
const ctx = canvas.getContext("2d");
ctx.getImageData(sx, sy, sw, sh);
相应的参数说明如下:
- sx:将要被提取的图像数据矩形区域的左上角 x 坐标。
- sy:将要被提取的图像数据矩形区域的左上角 y 坐标。
- sw:将要被提取的图像数据矩形区域的宽度。
- sh:将要被提取的图像数据矩形区域的高度。
putImageData:修改画布上的像素数据
void ctx.putImageData(imagedata, dx, dy);
void ctx.putImageData(imagedata, dx, dy, dirtyX, dirtyY, dirtyWidth, dirtyHeight);
- imageData: ImageData ,包含像素值的数组对象。
- dx:源图像数据在目标画布中的位置偏移量(x 轴方向的偏移量)。
- dy:源图像数据在目标画布中的位置偏移量(y 轴方向的偏移量)。
- dirtyX(可选):在源图像数据中,矩形区域左上角的位置。默认是整个图像数据的左上角(x 坐标)。
- dirtyY(可选):在源图像数据中,矩形区域左上角的位置。默认是整个图像数据的左上角(y 坐标)。
- dirtyWidth(可选):在源图像数据中,矩形区域的宽度。默认是图像数据的宽度。
- dirtyHeight(可选):在源图像数据中,矩形区域的高度。默认是图像数据的高度。
灰度化
在ImageData对象的数据属性中,每个像素所占据的信息是按照RGBA顺序排列的,也就是红色(R)、绿色(G)、蓝色(B)和Alpha(A)通道的信息。因此,数组中的每四个连续元素依次表示一个像素的RGBA信息。
将红绿蓝这三个颜色通道的值相加并取平均值,实际上是对彩色信息进行了平均化处理,使得每个像素的颜色趋向于中性灰色。使得图像呈现出灰度效果。
完整代码如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>获取远程图片并灰度化</title>
</head>
<body>
<div>
<button id="grayscalebtn">灰度化</button>
<div style="display: flex">
<div style="flex: 50%">
<p>原图片</p>
<img
id="previewContainer"
width="230"
height="230"
style="border: 2px dashed blue"
/>
</div>
<div style="flex: 50%">
<p>灰度图片</p>
<canvas
id="canvas"
width="230"
height="230"
style="border: 2px dashed grey"
></canvas>
</div>
</div>
</div>
<script>
const image = document.querySelector("#previewContainer");
const canvas = document.querySelector("#canvas");
fetch("https://avatars3.githubusercontent.com/u/4220799")
.then((response) => response.blob())
.then((blob) => {
const objectURL = URL.createObjectURL(blob);
image.src = objectURL;
image.onload = () => {
const ctx = canvas.getContext("2d");
ctx.drawImage(image, 0, 0, 230, 230);
};
const grayscalebtn = document.querySelector("#grayscalebtn");
grayscalebtn.addEventListener("click", grayscale);
});
const grayscale = function () {
const ctx = canvas.getContext("2d");
ctx.drawImage(image, 0, 0, 230, 230);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
console.log({ data });
// 每个像素
for (let i = 0; i < data.length; i += 4) {
const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
data[i] = avg; // red
data[i + 1] = avg; // green
data[i + 2] = avg; // blue
}
ctx.putImageData(imageData, 0, 0);
};
</script>
</body>
</html>