读《Blob API》总结
一、主要知识点
- 什么是Blob
- Blob 构造函数
- Blob 的属性与方法
- Blob 的拓展应用
- Blob 与 ArrayBuffer
- 各种类型之间的转换
1. 什么是 Blob
Blob(Binary Large Object) Blob 对象表示一个不可变、原始数据的类文件对象。File接口基于Blob,继承了 Blob 的功能并将其扩展使其支持用户系统上的文件。
一个 Blob 对象包含两个属性:size 与 type,如下:

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

2. Blob 简介
2.1 构造函数
var newBlob = new Blob(array, options);
参数:
- array:ArrayBuffer,ArrayBufferView,Blob,USVString对象的数组等对象构成的数组,将被放入Blob中。USVString 对象会被编码成 UTF-8 。
- options:一个可选对象
- type:它是 MINE type 类型,将会被放到 blob 中,默认是空字符串。
- endings:默认值为 transparent,用于指定包含结束符
\n的字符串如何被写入。
示例:
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 作为服务端
服务端部分:
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")
前端部分:
<!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 图片添加水印
前端部分:
<!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 对象做上传,来减少传输的数据量。下面会说各种类型之间的转换。
实现效果:

3.3 图片压缩
在我们选择本地图片上传之前,我们可以使用 Canvas 来对图片进行压缩。也就是使用添加水印功能中用到的 toDataURL 方法,它接受2个可选参数:
- type:图片格式,默认为
image/png - encoderOptions:图片之类,取值范围为0-1,如果超出范围,默认为
0.92
压缩方法:
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);
};
});
}
我们可以用上面那个生成水印的图片分别测试压缩与未压缩的图片:
...
compress(dataURL, .5, 'image/png').then(res=>{
document.getElementById("outputImg").src = res;
})
...
可以看到设置 encoderOptions 为 .5 的时候,图片大小小了大概 37kb

4. Blob URL/Object URL
在上面水印的例子中我们使用 createObjectURL 方法得到Blob URL
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 对象。
- 使用 FileReader 的
二、知识点拓展
1. 各种类型之间的转换
可以先了解一下这两个方法
btoa:方法用于创建一个 base-64 编码的字符串。
atob:atob() 方法用于解码使用 base-64 编码的字符串。
1.1 img 转 canvas
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
canvasElement.toDataURL("image/jpeg");
1.3 DataURL(base64)转blob
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)
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
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
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 作为参数传入即可
const buffer = new ArrayBuffer(16);
const blob = new Blob(buffer);
1.8 blob 转 ArrayBuffer
需要借助 FileReader
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 上传
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);
}
})