Blob API

读《Blob API》总结-2020.06.06

学习时间:2020.06.07

学习章节:Blob API

一、主要知识点

  • 什么是Blob
  • Blob 构造函数
  • Blob 的属性与方法
  • Blob 的拓展应用
  • Blob 与 ArrayBuffer
  • 各种类型之间的转换

1. 什么是 Blob

Blob(Binary Large Object) Blob 对象表示一个不可变、原始数据的类文件对象。File接口基于Blob,继承了 Blob 的功能并将其扩展使其支持用户系统上的文件。

一个 Blob 对象包含两个属性:size 与 type,如下:

WX20200606-225803@2x

一个 File 对象包含 lastModified、lastModifiedDate、size、type 与 webkitRelativePath 如下:

WX20200606-225711@2x

2. Blob 简介

2.1 构造函数

1
var newBlob = new Blob(array, options);

参数:

  • array:ArrayBuffer,ArrayBufferView,Blob,USVString对象的数组等对象构成的数组,将被放入Blob中。USVString 对象会被编码成 UTF-8 。
  • options:一个可选对象
    • type:它是 MINE type 类型,将会被放到 blob 中,默认是空字符串。
    • endings:默认值为 transparent,用于指定包含结束符 \n 的字符串如何被写入。

示例:

1
2
var aFileParts = ['<a id="a"><b id="b">hey qhw!</b></a>']; // an array consisting of a single DOMString
var oMyBlob = new Blob(aFileParts, {type : 'text/html'}); // the blob

2.2 Blob 的属性

  • Blob.size:Blob 对象中所包含数据的大小(字节)。
  • Blob.type:一个字符串,表明该 Blob 对象所包含数据的 MIME 类型。如果类型未知,则该值为空字符串。

2.3 Blob 的方法

  • Blob.slice([start[, end[, contentType]]]):返回一个新的 Blob 对象,包含了源 Blob 对象中指定范围内的数据。Blob 对象是不可改变的。我们不能直接在一个 Blob 中更改数据,但是我们 可以对一个 Blob 进行分割,从其中创建新的 Blob 对象,将它们混合到一个新的 Blob 中
  • Blob.stream():返回一个能读取blob内容的 ReadableStream
  • Blob.text():返回一个promise且包含blob所有内容的UTF-8格式的 USVString
  • Blob.arrayBuffer():返回一个promise且包含blob所有内容的二进制格式的 ArrayBuffer

3. Blob 的拓展应用

我们经常在上传文件的时候,会得到一个 file 对象,它基于 Blob。所以我们可以把它分块上传。也可以通过一些转换操作,来生成带水印的图片。最后再通过 formData 上传 Blob。

3.1 分块上传

这边使用 koa 作为服务端

服务端部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
const fs = require("fs");
const path = require("path");
const Utils = require("./utils").utils;

const Koa = require("koa");
const router = require("koa-router")();
const bodyParser = require("koa-body");

const app = new Koa();

const uploadDir = 'uploads';

app.use(bodyParser({multipart: true}));
app.use(router.routes());

router.get("/index-upload", function(ctx){//首页
ctx.response.type = 'html';
ctx.response.body = fs.createReadStream('./index-upload.html');
})

router.post('/upload', async function(ctx){//上传
//拿到接口中的数据
let data = ctx.request.body.fields,
currChunk = data.currChunk,
fileMd5Value = data.fileMd5Value,
file = ctx.request.body.files,
folder = path.join('uploads', fileMd5Value);
//判断文件是否存在
let isExist = await Utils.folderIsExist(path.join(__dirname, folder));
if(isExist){//将文件写入fileMd5Value下面的文件夹
let destFile = path.join(__dirname, folder, currChunk),
srcFile = path.join(file.data.path);
await Utils.copyFile(srcFile, destFile).then(() => {
ctx.response.body = 'chunk ' + currChunk + ' upload success!!!'
}, (err) => {
console.error(err);
ctx.response.body = 'chunk ' + currChunk + ' upload failed!!!'
})
}
})

router.get("/mergeChunk", async function(ctx){//合并chunk写成文件
let md5 = ctx.query.md5,
fileName = ctx.query.fileName,
size = ctx.query.size;

await Utils.mergeFiles(path.join(__dirname, uploadDir, md5),
path.join(__dirname, uploadDir),
fileName, size)

ctx.response.body = "success";
})

app.listen(3000);

console.log("the server is listening on port 3000")

前端部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<script src="http://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="https://cdn.bootcss.com/spark-md5/3.0.0/spark-md5.min.js"></script>
<title>HTML5 文件分段上传</title>
</head>

<body>
<form class="form-inline" role="form">
<input type="file" id="fileinput">
<a id="submit">SUBMIT</a>
</form>
<script>
let baseUrl = 'http://localhost:3000';
let chunkSize = 1 * 1024 * 1024
let fileSize = 0
let file = null
let chunks = 0
//点击submit开始上传
$("body").on("click", "#submit", function () {
let files = document.querySelector("#fileinput").files;
if (!files.length) {
alert("当前没有选择文件");
return false;
}
file = files[0];
fileSize = file.size;
startUpload(file);
})

async function startUpload(file) {
//生成文件MD5 等下文件上传完成后的唯一标识,为了做合并使用的
let fileMd5Value = await md5File(file);
//得到上传chunk分块长度
chunks = Math.ceil(fileSize / chunkSize);
for (let i = 0; i < chunks; i++) {
//上传chunk
await uploadChunk(i, fileMd5Value, chunks);
}
// 上传完成后,提交合并分文件请求
mergeChunk(fileMd5Value);
}

//生成文件MD5
function md5File(file) {
return new Promise((resolve, reject) => {
var blobSlice = File.prototype.slice || File.prototype.webkitSlice || File.prototype.mozSlice,
chunkSize = file.size / 100,
chunks = 100,
currentChunk = 0,
spark = new SparkMD5.ArrayBuffer(),
fileReader = new FileReader();
fileReader.onload = function (e) {
spark.append(this.result);
currentChunk++;
if (currentChunk < chunks) {
loadNext();
} else {
resolve(spark.end());
}
}

function loadNext() {
let start = currentChunk * chunkSize,
end = ((start + chunkSize) >= file.size) ? file.size : (start + chunkSize);
fileReader.readAsArrayBuffer(blobSlice.apply(file, [start, end]));
}
loadNext();
})
}

// 上传分块
function uploadChunk(i, fileMd5Value, chunks) {
return new Promise((resolve, reject) => {
let end = (i + 1) * chunkSize >= file.size ? file.size : (i + 1) * chunkSize;
// 构建一个formdata
let form = new FormData()
form.append("data", file.slice(i * chunkSize, end));
form.append("totalChunks", chunks);
form.append("currChunk", i);
form.append("fileMd5Value", fileMd5Value);

let url = `${baseUrl}/upload`;
$.ajax({
url: url,
type: "post",
data: form,
async: true,
processData: false,
contentType: false,
success: function (data) {
console.log(data);
resolve(data);
}
})
})
}

//5. 合并分块
function mergeChunk(fileMd5Value) {
let url = `${baseUrl}/mergeChunk?md5=${fileMd5Value}&fileName=${file.name}&size=${file.size}`;
$.get(url, function (data) {
alert('上传成功');
})
}
</script>
</body>

</html>

3.2 图片添加水印

前端部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>生成水印图片</title>
</head>
<body>
<input type="file" accept="image/*" onchange="change(event)" />
<img id="mask" src="./logo.png" style="display: none;" />
<canvas id="canvas" style="display: none;"></canvas>
<img id="outputImg" />
</body>
<script>
function change(e) {
var canvas = document.getElementById("canvas");
var maskElement = document.getElementById("mask");

var img = new Image(); // img 标签
var URL =
window.URL && window.URL.createObjectURL
? window.URL
: window.webkitURL && window.webkitURL.createObjectURL
? window.webkitURL
: null;
if (!URL) {
throw Error("No createObjectURL function found to create blob url");
}
img.src = URL.createObjectURL(e.target.files[0]); // 水印的 blob URL
img.onload = function() {
render(canvas, maskElement, img);
};
}

function render(canvasElement, maskElement, img) {
var naturalWidth = img.naturalWidth;
var naturalHeight = img.naturalHeight;
canvasElement.width = naturalWidth;
canvasElement.height = naturalHeight;
var ctx = canvasElement.getContext("2d");
ctx.drawImage(img, 0, 0);
for (let i = 0; i < 10; i++) {
for (let j = 0; j < 10; j++) {
ctx.drawImage(
maskElement,
(i * naturalWidth) / 3,
(j * naturalHeight) / 3,
114,
86
);
}
}
var dataURL = canvasElement.toDataURL("image/jpeg");
document.getElementById("outputImg").src = dataURL;
}
</script>
</html>

这边只是将图片转成 Base64 展示,如果需要上传,可以转成 Blob 对象做上传,来减少传输的数据量。下面会说各种类型之间的转换。

实现效果:

WX20200607-123559@2x

3.3 图片压缩

在我们选择本地图片上传之前,我们可以使用 Canvas 来对图片进行压缩。也就是使用添加水印功能中用到的 toDataURL 方法,它接受2个可选参数:

  • type:图片格式,默认为 image/png
  • encoderOptions:图片之类,取值范围为0-1,如果超出范围,默认为0.92

压缩方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const MAX_WIDTH = 600; // 图片最大宽度
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;
let offetX = 0; // 图片偏移值
img.onload = () => {
if (img.width > MAX_WIDTH) {
canvas.width = MAX_WIDTH;
canvas.height = (img.height * MAX_WIDTH) / img.width;
offetX = (img.width - MAX_WIDTH) / 2;
} else {
canvas.width = img.width;
canvas.height = img.height;
}
canvas
.getContext("2d")
.drawImage(img, 0, 0, canvas.width, canvas.height);
let imageData = canvas.toDataURL(mimeType, quality);
resolve(imageData);
};
});
}

我们可以用上面那个生成水印的图片分别测试压缩与未压缩的图片:

1
2
3
4
5
6
...

compress(dataURL, .5, 'image/png').then(res=>{
document.getElementById("outputImg").src = res;
})
...

可以看到设置 encoderOptions 为 .5 的时候,图片大小小了大概 37kb

WX20200607-125646@2x

4. Blob URL/Object URL

在上面水印的例子中我们使用 createObjectURL 方法得到Blob URL

1
blob:http://localhost:8000/a9f10cc1-3a13-470d-bd34-bcccbcee9167

4.1 什么是 Blob URL/Object URL

Blob URL/Object URL 是一种伪协议,允许 Blob 和 File 对象用作图像,下载二进制数据连接等 URL 源,在浏览器中,我们使用 URL.createObjectURL 方法来创建它,该方法接收一个 Blob 对象,并为其创建一个唯一的 URL,形式为 blob:<origin>/<uuid>,就跟上面的例子长的一样。

浏览器内部为每个通过 URL.createObjectURL 方法生成的 URL 存储了 URL -> Blob 的映射,因此,此类 URL 比较短,但可以访问 Blob。

4.2 Blob URL 的副作用

也正是因为 URL.createObjectURL 方法生成的 URL 存储了 URL -> Blob 的映射,Blob 本身驻留在内存中,浏览器无法释放。映射在文档卸载时自动清除,Blob 对象也会被释放。但是如果应用程序寿命较长,那不会很快就被释放。也就说我们创建了 Blob URL,不再需要使用该 Blob 的时候,它也在内存中。

解决方案:我们可以通过 URL.revokeObjectURL(url) 方法,从内部映射中删除引用,从而允许删除 Blob(在没有其他引用的情况下),从而释放内存。

5. Blob 与 ArrayBuffer

ArrayBuffer 对象:用于表示通用的,固定长度的原始二进制数据缓冲区。你不能直接操纵 ArrayBuffer 的内容,而是需要创建一个类型化数组对象或 DataView 对象,该对象以特定格式表示缓冲区,并使用该对象读取和写入缓冲区的内容。

Blob 类型的对象:表示不可变的类似文件对象的原始数据。Blob 表示的不一定是 JavaScript 原生格式的数据。File 接口基于 Blob,继承了Blob 功能并将其扩展为支持用户系统上的文件。

5.1 Blob vs ArrayBuffer

  • 除非你需要使用 ArrayBuffer 提供的写入/编辑的能力,否则 Blob 格式可能是最好的。
  • Blob 对象是不可变的,而 ArrayBuffer 是可以通过 TypedArrays 或 DataView 来操作。
  • ArrayBuffer 是存在内存中的,可以直接操作。而 Blob 可以位于磁盘、高速缓存内存和其他不可用的位置。
  • 虽然 Blob 可以直接作为参数传递给其他函数,比如 window.URL.createObjectURL()。但是,你可能仍需要 FileReader 之类的 File API 才能与 Blob 一起使用。
  • Blob 与 ArrayBuffer 对象之间是可以相互转化的:
    • 使用 FileReader 的 readAsArrayBuffer() 方法,可以把 Blob 对象转换为 ArrayBuffer 对象;
    • 使用 Blob 构造函数,如 new Blob([new Uint8Array(data]);,可以把 ArrayBuffer 对象转换为 Blob 对象。

二、知识点拓展

1. 各种类型之间的转换

可以先了解一下这两个方法

btoa:方法用于创建一个 base-64 编码的字符串。

atob:atob() 方法用于解码使用 base-64 编码的字符串。

1.1 img 转 canvas

1
2
3
4
5
6
7
8
function imgtocanvas(img){
let canvas = document.createElement("canvas");
let ctx = canvas.getContext('2d')
canvas.width = img.width
canvas.height = img.height
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
return canvas
}

1.2 canvas 转 base64

1
canvasElement.toDataURL("image/jpeg");

1.3 DataURL(base64)转blob

1
2
3
4
5
6
7
8
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 });
}

1.4 file(blob)转DataURL(base64)

1
2
3
4
5
6
7
8
9
10
11
12
function filetoblob(file) {
return new Promise((resolve, reject) => {
var reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = function (e) {
resolve(reader.result)
}
reader.onerror = function (e) {
resolve(reader.result)
}
})
}

1.5 blob 转 blob URL

1
2
3
4
5
6
7
var URL =
window.URL && window.URL.createObjectURL
? window.URL
: window.webkitURL && window.webkitURL.createObjectURL
? window.webkitURL
: null;
URL.createObjectURL(blob)

1.6 blob URL 转 blob

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function URLtoblob(){
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.open('GET', input)
xhr.responseType = 'blob'
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.response)
} else {
reject(xhr.statusText)
}
}
xhr.onerror = () => reject(xhr.statusText)
xhr.send()
})

1.7 ArrayBuffer 转 blob

只需将 ArrayBuffer 作为参数传入即可

1
2
const buffer = new ArrayBuffer(16);
const blob = new Blob(buffer);

1.8 blob 转 ArrayBuffer

需要借助 FileReader

1
2
3
4
5
6
7
const blob = new Blob([1, 2, 3, 4, 5]);
const reader = new FIleReader();

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

2. FormData 设置 blob 上传

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var form= new FormData();
form.append("image", blob);
let url = `${baseUrl}/upload`;
$.ajax({
url: url,
type: "post",
data: form,
async: true,
processData: false,
contentType: false,
success: function (data) {
resolve(data);
}
})