Blob API 学习笔记

640 阅读8分钟

读《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 构造函数

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 对象做上传,来减少传输的数据量。下面会说各种类型之间的转换。

实现效果:

WX20200607-123559@2x

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

WX20200607-125646@2x

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 对象。

二、知识点拓展

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);
  }
})