转载自 那些年,被blob虐过的程序猿觉醒了
前言
本文介绍前端如何进行图片处理,然后穿插介绍二进制、Blob、Blob URL、Base64、Data URL、ArrayBuffer、TypedArray、DataView 和图片压缩相关的知识点。
Blob
Blob
表示二进制类型的大对象,通常是影像、声音或多媒体文件,在 javaScript中Blob表示一个不可变、原始数据的类文件对象。
其构造函数如下:
new Blob(blobParts, options);
-
lobParts
:数组类型,可以存放任意多个ArrayBuffer, ArrayBufferView, Blob_或者_DOMString(会编码为UTF-8),将它们连接起来构成Blob对象的数据。 -
options
:可选项,用于设置blob对象的属性,可以指定如下两个属性: - type:存放到blob中数组内容的MIME类型(默认为"")。
- endings:用于指定包含行结束符\n的字符串如何被写入。值为native表示行结束符会被更改为适合宿主操作系统文件系统的换行符(默认值为transparent表示会保持blob中保存的结束符不变)
DOMString 是一个UTF-16字符串。由于JavaScript已经使用了这样的字符串,所以DOMString直接映射到 一个String。
ArrayBuffer(二进制数据缓冲区)、ArrayBufferView(二进制数据缓冲区的array-like视图)
属性
Blob对象有两个属性,参见下表:
属性名 | 描述 |
---|---|
size | Blob对象中所包含数据的大小。字节为单位。只读。 |
type | 一个字符串,表明该Blob对象所包含数据的MIME类型。如果类型未知,则该值为空字符串。只读。 |
方法
-
slice(start:number, end:number, contentType:DOMString)
:
类似于数组的slice方法,将原始Blob对象按照指定范围分割成新的blob对象并返回,可以用作切片上传- start:开始索引,默认为0
- end:结束索引,默认为最后一个索引
- contentType:新Blob的MIME类型,默认情况下为空字符串
-
stream()
:返回一个能读取blob内容的ReadableStream。 -
text()
:返回一个Promise对象且包含blob所有内容的UTF-8格式的 USVString。 -
arrayBuffer()
:返回一个Promise 对象且包含blob所有内容的二进制格式的ArrayBuffer。
将blob(或者file)二进制文件保存到 formData
进行网络请求(之后可以获取到图片的imageUrl可以用作图片展示或者后续的通过websocket
发送图片地址)
示例如下
- 创建一个包含domstring对象的blob对象
const blob = new Blob(['john'], { type: 'text/xml' });
console.log(blob); // Blob {size: 15, type: "text/xml"}
- 创建一个包含arraybuffer对象的blob对象
var abf = new ArrayBuffer(8);
const blob = new Blob([abf], { type: 'text/plain' });
console.log(blob); // Blob {size: 8, type: "text/plain"}
- 创建一个包含arraybufferview对象的blob对象
var abf = new ArrayBuffer(8);
var abv = new Int16Array(abf);
const blob = new Blob(abv, { type: 'text/plain' });
console.log(blob); // Blob {size: 4, type: "text/plain"}
File
File
对象是一种特殊的Blob对象,继承了所有Blob的属性和方法,当然同样也可以用作formData
二进制文件上传
属性
File对象属性,参见下表:
属性名 | 描述 |
---|---|
lastModified | 引用文件最后修改日期 |
name | 文件名或文件路径 |
size | 以字节为单位返回文件的大小 |
type | 文件的 MIME 类型 |
方法
File 对象没有自己的实例方法,由于继承了 Blob 对象,因此可以使用 Blob 的实例方法 slice()。
File的获取:
下面我们分别使用input和拖放方式选择多张图片操作:
- input获取本地文件
为input元素添加multiple属性,允许用户选择多个文件,用户选择的每一个文件都是一个file对象,而FileList对象则是这些file对象的列表,代表用户选择的所有文件,是file对象的集合。
- 拖放获取
数据缓冲区
从
XHR
、File API
、Canvas
等等各种地方,读取了一大串字节流,如果用JS里的Array去存,又浪费,又低效。在编程中,数据缓冲区
(或简称为缓冲区)是物理内存中中操作二进制数据的存储区(比硬盘驱动器访问快),用于在数据从一个位置移动到另一位置时存储临时数据,解释器
借助存储二进制数据的内存缓冲区读取行。主内存中有一个正在运行的文件,如果解释器必须返回文件以读取每个位,则执行过程将耗费大量时间。为了防止这种情况,JavaScript使用数据缓冲区,该缓冲区将一些位存储在一起,然后将所有位一起发送给解释器。这样,JavaScript解释器就不必担心从文件数据中检索文件。这种方法节省了执行时间并加快了应用程序的速度。各种缓冲区类对数据执行有效的二进制操作,包括File
,Blob
,ArrayBuffer
和Array
。选择的方法决定了内存中缓冲区的内部结构。
Buffer
Buffer
是Node.js
提供的对象,前端没有。它一般应用于IO操作
,例如接收前端请求数据时候,可以通过Buffer相关的API创建一个专门存放二进制数据的缓存区对接收到的前端数据进行整合,一个Buffer类似于一个整数数组,但它对应于V8
堆内存之外的一块原始内存。
ArrayBuffer、ArrayBufferView
ArrayBuffer
ArrayBuffer
表示固定长度
的二进制数据的原始缓冲区,它的作用是分配一段可以存放数据的连续内存区域,因此对于高密度的访问(如音频数据)操作而言它比JS中的Array速度会快很多,ArrayBuffer存在的意义就是作为数据源提前写入在内存中,因此其长度固定
先大致看下ArrayBuffer的功能:
ArrayBuffer对象的构造函数如下(length表示ArrayBuffer的长度):
ArrayBuffer(length)
Array和ArrayBuffer的区别:
Array | ArrayBuffer |
---|---|
可以放数字、字符串、布尔值以及对象和数组等 | 只能存放0和1组成的二进制数据 |
数据放在堆中 | 数据放在栈中,取数据时更快 |
可以自由增减 | 只读,初始化后固定大小,无论缓冲区是否为空,只能借助TypedArrays、Dataview写入 |
属性
ArrayBuffer对象属性,参见下表:
属性名 | 描述 |
---|---|
byteLength | 表示ArrayBuffer的大小 |
方法
-
slice
:有两个参数begin表示起始,end表示结束点。方法返回一个新的 ArrayBuffer ,它的内容是这个ArrayBuffer的字节副本,从begin(包括),到end(不包括)。
ArrayBuffer不能直接操作,而是要通过 TypedArray
或 DataView
对象来操作,它们会将缓冲区中的数据转换为各种数据类型的数组,并通过这些格式来读写缓冲区的内容。
ArrayBufferView
由于ArrayBuffer对象不提供任何直接读写内存的方法,而
ArrayBufferView
对象实际上是建立在ArrayBuffer对象基础上的视图
,它指定了原始二进制数据
的基本处理单元,通过ArrayBufferView对象来读取ArrayBuffer对象的内容。类型化数组(TypedArrays)和DataView是ArrayBufferView的实例。
TypedArrays
类型化数组(TypedArrays
)是JavaScript中新出现的一个概念,专为访问原始的二进制数据而生。
本质上,类型化数组和ArrayBuffer是一样的,只不过是他具备读写功能。
类型数组的类型有::
名称 | 大小 (以字节为单位) | 说明 |
---|---|---|
Int8Array | 1 | 8位有符号整数 |
Uint8Array | 1 | 8位无符号整数 |
Int16Array | 2 | 16位有符号整数 |
Uint16Array | 2 | 16位无符号整数 |
Int32Array | 4 | 32位有符号整数 |
Uint32Array | 4 | 32位无符号整数 |
Float32Array | 4 | 32位浮点数 |
Float64Array | 8 | 64位浮点数 |
类型转换如图:
举一些代码例子展示如何转换:
// 创建一个指向b的视图v1,采用Int32类型,开始于默认的字节索引0,直到缓冲区的末尾
var v1 = new Int32Array(b); // Int32Array(2) [0, 0]
v1[0] = 1
console.log(v1); // Int32Array(2) [1, 0]
// 创建一个指向b的视图v2,采用Uint8类型,开始于字节索引2,直到缓冲区的末尾
var v2 = new Uint8Array(b, 2); // Uint8Array(6) [0, 0, 0, 0, 0, 0]
// 创建一个指向b的视图v3,采用Int16类型,开始于字节索引2,长度为2
var v3 = new Int16Array(b, 2, 2); // Int16Array(2) [0, 0]`
因为普通Javascript数组使用的是Hash查找方式,而类型化数组直接访问固定内存,因此,速度很赞,比传统数组要快!同时,类型化数组天生处理二进制数据,这对于XMLHttpRequest
、canvas
、webGL
等技术有着先天的优势。
TypedArray的应用如何拼接两个音频文件?
fetch请求音频资源 -> ArrayBuffer -> TypedArray -> 拼接成一个 TypedArray -> ArrayBuffer -> Blob -> Object URL
DataView
DataView
对象可以在ArrayBuffer中的任意位置读取和存储不同类型的二进制数据。
创建DataView的语法如下:
var dataView = new DataView(DataView(buffer, byteOffset[可选], byteLength[可选])
属性
DataView对象有三个属性,参见下表:
属性名 | 描述 |
---|---|
buffer | 表示ArrayBuffer |
byteOffset | 指缓冲区开始处的偏移量 |
byteLength | 指缓冲区部分的长度 |
方法
-
setint8()
:从DataView起始位置以byte为计数的指定偏移量(byteOffset)处存储一个8-bit数(一个字节) -
getint8()
:从DataView起始位置以byte为计数的指定偏移量(byteOffset)处获取一个8-bit数(一个字节)
除此之外还有getInt16, getUint16, getInt32, getUint32... 使用方法一致,这里就不一一例举
用法如下:
let buffer = new ArrayBuffer(32);
let dataView = new DataView(buffer,0);
dataView.setInt16(1,56);
dataView.getInt16(1); // 56
FileReader
我们无法直接访问Blob或者文件对象的内容,如果想要读取它们并转化为其他格式的数据,可以借助
FileReader
对象的API进行操作
-
readAsText(Blob)
:将Blob转化为文本字符串 -
readAsArrayBuffer(Blob)
:将Blob转为ArrayBuffer格式数据 -
readAsDataURL()
: 将Blob转化为Base64格式的DataURL
使用分别如下:
const blob = new Blob(['foo '], { type: 'text/xml' });
console.log(blob); // Blob(14) {size: 14, type: "text/xml"}
const reader = new FileReader();
reader.onload = () => {
console.log(reader.result);
};
reader.readAsText(blob); // foo
reader.readAsArrayBuffer(blob); // ArrayBuffer(14) {}
reader.readAsDataURL(blob); // data:text/xml;base64,PHhtbD5mb288L3htbD4`
下面我们尝试把一个文件的内容通过字符串的方式读取出来:
读取结果如下:
BlobURL
BlobURL
(ObjectURL)是一种伪协议
,只能由浏览器在内部生成,我们知道script/img/video/iframe
等标签的src属性和background的url可以通过url和base64来显示,我们同样可以把blob或者file转换为url生成BlobURL来展示图像,BlobURL允许Blob和File对象用作图像,下载二进制数据链接等的URL源。
图像展示:
我们查看demo页面这个mm图片元素,会发现其URL地址既不是传统HTTP,也不是Base64 URL,而是blob:开头的字符串,可以通过将其放在地址栏中进行检查。
文件下载:
点击按钮下载文档,文档内容为:johnYu
这里不调用revokeObjectURL
时访问chrome://blob-internals/
可以看到当前内部的blob文件列表:
不再使用的BlobUrl后续会自动清除(关闭浏览器也会自动清除),但是最好使用URL.revokeObjectURL(url)
手动清除它们:
URL.revokeObjectURL('blob:http://127.0.0.1:5500/d2a9a812-0dbf-41c5-a96b-b6384d33f281')
执行后再次访问chrome://blob-internals/
可以看到文件已经被清除
dataURL
dataURL
允许内容的创建者将较小的文件嵌入到文档中。与常规的URL使用场合类似
其语法格式格式如下:
data:[][;base64],data
-
data
:前缀 -
mediatype
表明数据类型,是一个MIME类型字符串,如image/jpeg表示一个JPEG图片文件。如果省略,默认值为text/plain;charset=US-ASCII。 -
base64
:标志位(如果是文本,则可选) -
data
:数据本身
如何获取DataUrl
- 上面示例中使用的方法readAsDataURL()就是将Blob转化为Base64格式的DataUrl;
- 使用原生
Web API
编码/解码
Javascript中有两个函数负责编码和解码base64字符串,分别是atob和btoa。
两者都只针对Data URL中的data进行处理。
- atob(): 负责解码已经使用base64编码了的字符串。
btoa('hello base64') // PHhtbD5mb288L3htbD4=
- btoa(): 将二进制字符串转为base64编码的
ASCII
字符串。atob('PHhtbD5mb288L3htbD4=') //
foo
- Canvas的toDataURL方法:
Canvas提供了toDataURL方法,用于获取canvas绘制内容,将其转为base64格式
如下图所示,文本框中的内容即为canvas中绘制内容的base64格式。
如果我们将前面的返回结果data:text/xml;base64,PHhtbD5mb288L3htbD4=
放在浏览器的地址栏中,则可以看到显示的内容:
DataUrl的使用
- 由于可以将其用作URL的替代,因此DataURL和BlobUrl一样可以在script/img/video/iframe等标签的src属性和background的url中使用,用法与BlobUrl基本一致,只需要将前面的
elem.onchange
做如下改造
由于数据本身由URL表示,因此可以将其保存在Cookie中传递给服务器。
当图片的体积太小,占用一个HTTP会话不是很值得时。
当访问外部资源很麻烦或受限时。
DataUrl不会被浏览器缓存,但是小部分会通过css缓存,在下面例子中,DataUrl的使用是完全符合场景的。它避免了让这个小小的背景图片独自产生一次HTTP请求,而且,这个小图片还能同CSS文件一起被浏览器缓存起来,重复使 用,不会每次使用时都加载一次。只要这个图片不是很大,而且不是在CSS文件里反复使用,就可以DataUrl方法呈现图片降低页面的加载时间,改善用户的浏览体验。
background-image: url("data:image/gif;base64,R0lGODlhAwADAIAAAP///8zMzCH5BAAAAAAALAAAAAADAAMAAAIEBHIJBQA7")
- 作为下载连接使用。
点击a标签后后下载文本内容为johnYu的txt文件,在下面的BlobURL同样可以实现
区别
BlobURL 基本用法与 DataUrl 相同,都可以通过将其放在地址栏中进行检查也可以用作普通URL使用。
但也存在以下差异:
BlobUrl始终是唯一字符串,即时你每次传递相同的Blob,每次也会生成不同的BlobUrl;DataUrl值跟随blob变化;
就BlobUrl而言,它并不代表数据本身,数据存储在浏览器中,BlobUrl只是访问它的key。数据会一直有效,直到关闭浏览器或者手动清除。而DataUrl是直接编码的数据本身。因此即使将BlobUrl传递给服务器等也无法访问数据。关闭浏览器后仍然可以在地址栏访问后DataUrl,但是访问不到BlobUrl
BlobUrl的长度一般比较短,但DataUrl因为直接存储图片base64编码后的数据,往往很长(Base64编码的数据体积通常会比二进制格式的图片体积大1/3。),因此当显式大图片时,使用BlobUrl能获取更好的可能性,速度和内存比DataUrl更有效
BlobUrl可以方便的使用XMLHttpRequest获取源数据(xhr.responseType = 'blob')。对于DataUrl,并不是所有浏览器都支持通过XMLHttpRequest获取源数据的
- BlobUrl除了可以用作图片资源的网络地址,BlobUrl也可以用作其他资源的网络地址,例如html文件、json文件等,为了保证浏览器能正确的解析BlobUrl返回的文件类型,需要在创建Blob对象时指定相应的type
const createDownload = (fileName, content) => {
const blob = new Blob([content], { type: 'text/html' });
const link = document.createElement('a');
link.innerHTML = fileName;
link.download = fileName;
link.href = getObjectURL(blob);
document.getElementsByTagName('body')[0].appendChild(link);
};
createDownload('download.html', '')
-
DataUrl不会被浏览器缓存,这意味着每次访问这样页面时都被下载一次。这是一个使用效率方面的问题——尤其当这个图片被整个网站大量使用的时候。但是小部分可以通过css缓存
canvas
Canvas
对象元素负责在页面中设定一个区域,然后就可以通过 JavaScript 动态地在这个区域中绘制图形。
方法
-
toDataURL(type, encoderOptions))
:以指定格式返回 DataUrl,该方法接收两个可选参数- type:表示图片格式,默认为 image/png
- encoderOptions:表示图片的质量,在指定图片格式为 image/jpeg 或 image/webp 的情况下,可以从 0 到 1 的区间内选择图片的质量。如果超出取值范围,将会使用默认值 0.92,其他参数会被忽略。
-
toBlob(callback, type, encoderOptions)
:创造Blob对象, 用于展示canvas的图片,默认图片类型是image/png,分辨率是96dpi- callback: 参数是blob对象的回调函数
-
getImageData(x,y,width,height)
:返回 ImageData 对象,该对象拷贝了画布指定矩形的像素数据。- x: 开始复制的左上角位置的 x 坐标。
- y: 开始复制的左上角位置的 y 坐标。
- width: 将要复制的矩形区域的宽度。
- height: 将要复制的矩形区域的高度。
-
putImageData(imgData,x,y,dirtyX,dirtyY,dirtyWidth,dirtyHeight)
:将图像数据(从指定的 ImageData 对象)放回画布上。- imgData: 规定要放回画布的 ImageData 对象。
- x: ImageData 对象左上角的 x 坐标,以像素计。
- y: ImageData 对象左上角的 y 坐标,以像素计。
- dirtyX: 可选。水平值(x),以像素计,在画布上放置图像的位置。
- dirtyY: 可选。水平值(y),以像素计,在画布上放置图像的位置。
- dirtyWidth: 可选。在画布上绘制图像所使用的宽度。
- dirtyHeight: 可选。在画布上绘制图像所使用的高度。
应用场景
当我们需要获取到canvas的内容,可以用到toDataURL
和toBlob
属性(可用于签名,图片剪裁,图片压缩等场景),putImageData
、getImageData
可以用于图片灰度或者复制时使用(见后面的使用场景章节)
获取内容:
关系及转换
`
字符串 → Uint8Array
var str = 'ab';
console.log(Uint8Array.from(str.split(''), (e) => e.charCodeAt(0))); // Uint8Array(2) [97, 98]
Uint8Array → 字符串
var u8 = Uint8Array.of(97, 98);
console.log(Array.from(u8, (e) => String.fromCharCode(e)).join('')); // ab
字符串 → DataUrl
var str = 'ab';
console.log('data:application/octet-stream;base64,' + btoa(str)); // data:application/octet-stream;base64,YWI=
DataUrl -> 字符串
var data = 'data:application/octet-stream;base64,YWI=';
console.log(atob(data.split(',')[1])); // ab
Uint8Array -> ArrayBuffer
var u8 = Uint8Array.of(1, 2);
console.log(u8.buffer); // ArrayBuffer(2) {}
ArrayBuffer -> Uint8Array
var buffer = new ArrayBuffer(2);
console.log(new Uint8Array(buffer)); // Uint8Array(2) [0, 0]
ArrayBuffer -> DataView
var buffer = new ArrayBuffer(2);
var dataView = new DataView(buffer, 0); // DataView(2) {}
DataView -> ArrayBuffer
console.log(dataView.buffer); // ArrayBuffer(2) {}
ArrayBuffer → Blob
var buffer = new ArrayBuffer(32);
var blob = new Blob([buffer]); // Blob {size: 32, type: ""}
UintXXArray → Blob
var u8 = Uint8Array.of(97, 32, 72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33);
var blob = new Blob([u8])
##### 字符串 → Blob
var blob = new Blob(['Hello World!'], {type: 'text/plain'});
// Blob {size: 12, type: "text/plain"}`
以上都是用new Blob()转blob
DataUrl -> blob
var data = 'data:application/octet-stream;base64,YWI=';
function dataURLtoBlob(dataurl) {
var arr = dataurl.split(','),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new Blob([u8arr], { type: mime });
}
console.log(dataURLtoBlob(data)); // Blob {size: 2, type: "application/octet-stream"}
Blob →
需要用到 FileReader 的 Api 转换 readAsText(Blob)、readAsArrayBuffer(Blob)、readAsDataURL() 但是需要异步执行
var blob = new Blob(['a Hello world!'], { type: 'text/plain' });
var reader = new FileReader();
reader.readAsText(blob, 'utf-8');
reader.onload = function (e) {
console.info(reader.result); // a Hello world!
};
reader.onerror = function (e) {
console.error(reader.error);
}
可以用promise做多次转换
var blob = new Blob(['a Hello world!'], { type: 'text/plain' });
function read(blob) {
var fr = new FileReader();
var pr = new Promise((resolve, reject) => {
fr.onload = (eve) => {
resolve(fr.result);
};
fr.onerror = (eve) => {
reject(fr.error);
};
});
return {
arrayBuffer() {
fr.readAsArrayBuffer(blob);
return pr;
},
binaryString() {
fr.readAsBinaryString(blob);
return pr;
},
dataURL() {
fr.readAsDataURL(blob);
return pr;
},
text() {
fr.readAsText(blob);
return pr;
},
};
}
var pstr1 = read(blob).binaryString();
var pstr2 = read(blob)
.arrayBuffer()
.then((e) => Array.from(new Uint8Array(e), (e) => String.fromCharCode(e)).join(''));
Promise.all([pstr1, pstr2]).then((e) => {
console.log(e[0]); // a Hello world!
console.log(e[0] === e[1]); // true
});
应用场景
图像灰度化
这里主要用到canvas
和imageData
的转换
除次之外 getImageData
和putImageData
还可以用作 cavas 图片复制: https://www.w3school.com.cn/tiy/t.asp?f=html5_canvas_getimagedata
图片压缩
在前端要实现图片压缩,我们可以利用 Canvas 对象提供的 toDataURL()
方法
compress.js
const MAX_WIDTH = 800; // 图片最大宽度
function compress(base64, quality, mimeType) {
let canvas = document.createElement('canvas');
let img = document.createElement('img');
img.crossOrigin = 'anonymous';
return new Promise((resolve, reject) => {
img.src = base64;
img.onload = () => {
let targetWidth, targetHeight;
if (img.width > MAX_WIDTH) {
targetWidth = MAX_WIDTH;
targetHeight = (img.height * MAX_WIDTH) / img.width;
} else {
targetWidth = img.width;
targetHeight = img.height;
}
canvas.width = targetWidth;
canvas.height = targetHeight;
let ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, targetWidth, targetHeight); // 清除画布
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
// 通过toDataURL压缩后的base64
let imageData = canvas.toDataURL(mimeType, quality / 100);
resolve(imageData);
};
});
}
test.html
分片上传
服务器接收到这些切片后,再将他们拼接起来就可以了,下面是PHP拼接切片的示例代码:
$filename = './upload/' . $_POST['filename'];//确定上传的文件名
//第一次上传时没有文件,就创建文件,此后上传只需要把数据追加到此文件中
if(!file_exists($filename)){
move_uploaded_file($_FILES['file']['tmp_name'],$filename);
}else{
file_put_contents($filename,file_get_contents($_FILES['file']['tmp_name']),FILE_APPEND);
echo $filename;
}
测试时记得修改 nginx 的 server 配置,否则大文件可能会提示 413 Request Entity Too Large
的错误。
server {
// ...
client_max_body_size 50m;
}
文件下载
Blob 文件下载示例