阅读 7

JavaScript 之二进制家族

概述

  • Blob:前端的一个专门用于支持文件操作的二进制对象
  • ArrayBuffer: 前端的一个通用的二进制缓冲区,类似数组,但在 API 和特性上却有众多不同。
  • Buffer:node.js 的一个二进制缓冲区,常用来处理 I/O 操作。

Blob

Blob 全称:Binary Large Object (二进制大型对象)。

Blob 对象表示一个二进制文件的数据内容,通常用来读写文件,比如一个图片文件的内容就可以通过 Blob 对象读写。

与 ArrayBuffer 区别:

  • Blob 用于操作二进制文件
  • ArrayBuffer 用于操作内存

对象使用

浏览器原生提供 Blob() 构造函数,用来生成实例。

const blob = new Blob(array [, options]);
复制代码
  • array 数组,必填,成员是字符串或二进制对象,表示新生成的Blob实例对象的内容; 成员可以是一个由 ArrayBuffer , ArrayBufferView , Blob , DOMString 等对象构成的 Array ,或者其他类似对象的混合体,它将会被放进 Blob。DOMStrings会被编码为UTF-8。
  • options,可选,是一个配置对象,这里介绍常用的属性 type,表示数据的 MIME 类型,默认空字符串;options 目前可能有两个属性: type 和 endings。 endings 用于指定包含行结束符 \n 的字符串如何被写入,默认值 transparent。它只有这两个值:native (代表行结束符会被更改为适合宿主操作系统文件系统的换行符)和 transparent (代表会保持blob中保存的结束符不变)。

实例属性和方法

Blob 具有两个实例属性:

  • size:文件的大小,单位为字节。

  • type:文件的 MIME 类型。如果类型无法确定,则返回空字符串。

const leoHtmlFragment = ['<a id="a"><b id="b">hey leo!</b></a>']; // 一个包含 DOMString 的数组
const leoBlob = new Blob(leoHtmlFragment, {type : 'text/html'});   // 得到 blob

leoBlob.size; // 38
leoBlob.type; // "text/html"

复制代码

Blob 实例方法:

  • slice:方法用于创建一个包含源 Blob 的指定字节范围内的数据的新 Blob 对象。
const newBlob = oldBlob.slice([start [, end [, contentType]]])
复制代码
  • start,可选,起始的字节位置,默认 0;

  • end,可选,结束的字节位置,默认 size 属性的值,不包含该位置;

  • contentType,可选,新实例的数据类型(默认为空字符串);

Blob 实战

通过window.URL.createObjectURL方法可以把一个blob转化为一个Blob URL,并且用做文件下载或者图片显示的链接。

Blob URL所实现的下载或者显示等功能,仅仅可以在单个浏览器内部进行。而不能在服务器上进行存储,亦或者说它没有在服务器端存储的意义。

下面是一个Blob的例子,可以看到它很短

blob:d3958f5c-0777-0845-9dcf-2cb28783acaf
复制代码

和冗长的Base64格式的Data URL相比,Blob URL的长度显然不能够存储足够的信息,这也就意味着它只是类似于一个浏览器内部的“引用“。从这个角度看,Blob URL是一个浏览器自行制定的一个伪协议

获取文件信息

在 JS 中,有两个构造函数 File 和 Blob, 而 File 继承了所有 Blob 的属性。

所以在我们看来,File 对象可以看作一种特殊的 Blob 对象。

文件选择器·<input type="file">用来让用户选取文件。出于安全考虑,浏览器不允许脚本自行设置这个控件的 value 属性,即文件必须是用户手动选取的,不能是脚本指定的。一旦用户选好了文件,脚本就可以读取这个文件。

文件选择器返回一个 FileList 对象,该对象是个类数组对象,每个成员都是一个 File 实例对象。File 实例对象是一个特殊的 Blob 实例,增加了 namelastModifiedDate 属性。

也包括拖放 API 的 dataTransfer.files 返回的也是一个 FileList 对象,成员也是 File 实例对象。

// HTML 代码如下
// <input type="file" accept="image/*" multiple onchange="fileinfo(this.files)"/>

function fileinfo(files) {
  for (let i = 0; i < files.length; i++) {
    let f = files[i];
    console.log(
      f.name, // 文件名,不含路径
      f.size, // 文件大小,Blob 实例属性
      f.type, // 文件类型,Blob 实例属性
      f.lastModifiedDate // 文件的最后修改时间
    );
  }
}

复制代码

下载文件

我们可以通过 window.URL.createObjectURL,接收一个 Blob(File)对象,将其转化为 Blob URL,然后赋给 a.download 属性,然后在页面上点击这个链接就可以实现下载了:

download属性不兼容IE, 对IE可通过window.navigator.msSaveBlob方法或其他进行优化

<!-- html部分 -->
<a id="h">点此进行下载</a>
<!-- js部分 -->
<script>
  var blob = new Blob(["Hello World"]);
  var url = window.URL.createObjectURL(blob);
  var a = document.getElementById("h");
  a.download = "helloworld.txt";
  a.href = url;
</script> 
复制代码

在 AJAX 请求中,指定 responseType 属性为 blob ,就可以获得一个 Blob 对象。

unction getBlob(url, callback) {
  const xhr = new XMLHttpRequest();
  xhr.open('GET', url);
  xhr.responseType = 'blob';
  xhr.onload = function () {
    // xhr.response 拿到的就是一个 Blob 对象。
    callback(xhr.response);
  }
  xhr.send(null);
}

复制代码

使用 axios 下载文件:

比较适合有 token 鉴权的下载

  1. 设置 responseType 为 blob
return axios({
    url: 'xxx',
    method: 'get',
    responseType: 'blob'
  })
复制代码
  1. 在拦截器里直接返回 data
// response interceptor
service.interceptors.response.use((res) => {

  if (res.status === 200) {

    // 如果返回的是二进制 blob  对象,直接返回 data
    if ( res.config.responseType === 'blob' ) {
      return res.data;
    }
    .....
  }
}

// 如果需要文件名时,提取文件名
const fileName = response.headers['content-disposition'].match(
/filename=(.*)/
)[1]
// 将二进制流转为blob
const blob = new Blob([response.data], { type: 'application/octet-stream' })
复制代码
  1. 设置 a 标签 href 和 download 属性
// 请求返回的 res 直接就是 blob 对象
const url = window.URL.createObjectURL(res);
const download = 'template.excel';
复制代码

如果没有 a 标签,需模拟创建 a 标签:

let blob = new Blob([res.data]);

// 兼容 IE
if (window.navigator.msSaveOrOpenBlob) {
  navigator.msSaveBlob(blob, file.fileName);
} else {
  let link = document.createElement("a");
  let evt = document.createEvent("HTMLEvents");
  evt.initEvent("click", false, false);
  link.href = URL.createObjectURL(blob); 
  link.download = file.fileName;
  link.style.display = "none";
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link); // 下载完成移除元素
  window.URL.revokeObjectURL(link.href);
}

复制代码

如果是不需要 token 鉴权等,可直接通过 URL 下载

let url = `${baseUrl}${api.DownOriginFile}${qs.stringify(queryParam)}`;
 window.open(url);
复制代码

或者直接创建 a 标签跳转:

const link = document.createElement('a')
const evt = document.createEvent('HTMLEvents')
evt.initEvent('click', false, false)
link.href = `${baseUrl}${api.DownOriginFile}${qs.stringify(queryParam)}`;
link.target = '_blank';
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link) // 下载完成移除元素
window.URL.revokeObjectURL(link.href);
// URL.revokeObjectURL() 静态方法用来释放一个之前已经存在的、通过调用 URL.createObjectURL() 创建的 URL 对象。当你结束使用某个 URL 对象之后,应该通过调用这个方法来让浏览器知道不用在内存中继续保留对这个文件的引用了。
复制代码

生成 url

浏览器允许使用 URL.createObjectURL() 方法,针对 Blob 对象生成一个临时URL,以便于某些 API 使用。

这个 URL 以 blob:// 开头,表明对应一个 Blob 对象,协议头后面是一个识别符,用来唯一对应内存里面的 Blob 对象。这一点与 data://URL(URL 包含实际数据)和 file://URL(本地文件系统里面的文件)都不一样。

<!-- html部分 -->
<input type="file" id='f' />
<img id='img' style="width: 200px;height:200px;" />
<!-- js部分 -->
<script>
  document.getElementById('f').addEventListener('change', function (e) {
    var file = this.files[0];
    const img = document.getElementById('img');
    const url = window.URL.createObjectURL(file);
    img.src = url;
    img.onload = function () {
        // 释放一个之前通过调用 URL.createObjectURL创建的 URL 对象
        window.URL.revokeObjectURL(url);
    }
  }, false);
</script>
复制代码
const droptarget = document.getElementById('droptarget');

droptarget.ondrop = function (e) {
  const files = e.dataTransfer.files;
  for (let i = 0; i < files.length; i++) {
    let type = files[i].type;
    if (type.substring(0,6) !== 'image/')
      continue;
    let img = document.createElement('img');
    img.src = URL.createObjectURL(files[i]);
    img.onload = function () {
      this.width = 100;
      document.body.appendChild(this);
      URL.revokeObjectURL(this.src);
    }
  }
}
复制代码

文件分片上传

  • 通过Blob.slice(start,end)可以分割大Blob为多个小Blob
  • xhr.send是可以直接发送Blob对象的
<!-- html部分 -->
<input type="file" id='f' />
<!-- js部分 -->
<script>
function upload(blob) {
    var xhr = new XMLHttpRequest();
    xhr.open('POST', '/ajax', true);
    xhr.setRequestHeader('Content-Type', 'text/plain')
    xhr.send(blob);
}

document.getElementById('f').addEventListener('change', function (e) {
    var blob = this.files[0];
    const CHUNK_SIZE = 20; .
    const SIZE = blob.size;
    var start = 0;
    var end = CHUNK_SIZE;
    while (start < SIZE) {
        upload(blob.slice(start, end));
        start = end;
        end = start + CHUNK_SIZE;
    }
}, false);
</script>

复制代码
// Node端(Koa)
app.use(async (ctx, next) => {
    await next();
    if (ctx.path === '/ajax') {
        const req = ctx.req;
        const body = await parse(req);
        ctx.status = 200;
        console.log(body);
        console.log('---------------');
    }
});
复制代码

读取文件

如果想要读取Blob或者文件对象并转化为其他格式的数据,可以借助FileReader对象的API进行操作:

  • FileReader.readAsText(Blob):将Blob转化为文本字符串,需要指定文本编码,默认为 UTF-8。
  • FileReader.readAsArrayBuffer(Blob): 将Blob转为ArrayBuffer格式数据
  • FileReader.readAsDataURL(): 将Blob转化为Base64格式的Data URL
  • FileReader.readAsBinaryString():返回原始的二进制字符串。
<input type="file" id='f' />
<script>
  document.getElementById('f').addEventListener('change', function (e) {
    var file = this.files[0];
    const reader = new FileReader();
    reader.onload = function () {
        const content = reader.result;
        console.log(content);
    }
    reader.readAsText(file);
  }, false);
</script>
复制代码

上面介绍了Blob的用法,我们不难发现,Blob是针对文件的,或者可以说它就是一个文件对象,同时呢我们发现Blob欠缺对二进制数据的细节操作能力,比如如果如果要具体修改某一部分的二进制数据,Blob显然就不够用了,而这种细粒度的功能则可以由下面介绍的ArrayBuffer来完成。

ArrayBuffer

ArrayBuffer 对象是 ES6 才纳入正式 ECMAScript 规范,是 JavaScript 操作二进制数据的一个接口。ArrayBuffer 对象是以数组的语法处理二进制数据,也称二进制数组。

ArrayBuffer 对象代表储存二进制数据的一段内存,它不能直接读写,只能通过视图(TypedArray 视图和 DataView 视图)来读写,视图的作用是以指定格式解读二进制数据。

关于 TypedArray 视图和 DataView 视图 ,可以查看阮一峰老师《ECMAScript 6 入门 ArrayBuffer》 章节的介绍。

对象使用

浏览器原生提供 ArrayBuffer() 构造函数,用来生成实例。

  • 参数:整数,表示二进制数据占用的字节长度。
  • 返回值:一个指定大小的 ArrayBuffer 对象,其内容被初始化为 0。
const buffer = new ArrayBuffer(32); // 表示实例对象 buffer 占用 32 个字节。
复制代码

实例属性和方法

ArrayBuffer 对象有实例属性 byteLength ,表示当前实例占用的内存字节长度(单位字节),一单创建就不可变更(只读):

const buffer = new ArrayBuffer(32);
buffer.byteLength; // 32
复制代码

ArrayBuffer 对象有实例方法 slice(),用来复制一部分内存。

参数如下:

  • start,整数类型,表示开始复制的位置。默认从 0 开始。

  • end,整数类型,表示结束复制的位置(不包括结束的位置)。如果省略,则表示复制到结束。

const buffer = new ArrayBuffer(32);
const buffer2 = buffer.slice(0);
复制代码

ArrayBuffer 实战

通过 ArrayBuffer 的格式读取本地数据

下面是 FileReader.readAsArrayBuffer() 方法的例子,用于读取二进制文件:

// HTML 代码如下
// <input type="file" onchange="typefile(this.files[0])"></input>
function typefile(file) {
  // 文件开头的四个字节,生成一个 Blob 对象
  let slice = file.slice(0, 4);
  let reader = new FileReader();
  // 读取这四个字节
  reader.readAsArrayBuffer(slice);
  reader.onload = function (e) {
    let buffer = reader.result;
    console.log(buffer);
    // 将这四个字节的内容,视作一个32位整数
    let view = new DataView(buffer);
    let magic = view.getUint32(0, false);
    // 根据文件的前四个字节,判断它的类型
    switch(magic) {
      case 0x89504E47: file.verified_type = 'image/png'; break;
      case 0x47494638: file.verified_type = 'image/gif'; break;
      case 0x25504446: file.verified_type = 'application/pdf'; break;
      case 0x504b0304: file.verified_type = 'application/zip'; break;
    }
    console.log(file.name, file.verified_type);
  };
}
复制代码

通过 ArrayBuffer 的格式读取 Ajax 请求数据

const xhr = new XMLHttpRequest();
xhr.open("GET", "ajax", true);
xhr.responseType = "arraybuffer";
xhr.onload = function () {
    console.log(xhr.response)
}
xhr.send();
复制代码

通过 TypeArray 对 ArrayBuffer 进行写操作

const typedArray1 = new Int8Array(8);
typedArray1[0] = 32;

const typedArray2 = new Int8Array(typedArray1);
typedArray2[1] = 42;

console.log(typedArray1);
//  output: Int8Array [32, 0, 0, 0, 0, 0, 0, 0]

console.log(typedArray2);
//  output: Int8Array [32, 42, 0, 0, 0, 0, 0, 0]
复制代码

通过 DataView 对 ArrayBuffer 进行写操作

const buffer = new ArrayBuffer(16);
const view = new DataView(buffer);
view.setInt8(2, 42);
console.log(view.getInt8(2));
// 输出: 42
复制代码

Buffer

Buffer是Node.js提供的对象,前端没有。 它一般应用于IO操作,例如接收前端请求数据时候,可以通过以下的Buffer的API对接收到的前端数据进行整合

// Node端(Koa)
const app = new Koa();
app.use(async (ctx, next) => {
    if (ctx.path === '/ajax') {
        const chunks = [];
        const req = ctx.req;
        req.on('data', buf => {
            chunks.push(buf);
        })
        req.on('end', () => {
            let buffer = Buffer.concat(chunks);
            console.log(buffer.toString())
        })
    }
});
app.listen(3000)

// 前端
const xhr = new XMLHttpRequest();
xhr.open("POST", "ajax", true);
xhr.setRequestHeader('Content-Type', 'text/plain')
xhr.send("asdasdsadfsdfsadasdas");

// Node端输出
asdasdsadfsdfsadasdas
复制代码

参考

前端知乎系列】ArrayBuffer 与 Blob对象

聊聊JS的二进制家族:Blob、ArrayBuffer和Buffer

《ECMAScript 6 入门 ArrayBuffer》

关注下面的标签,发现更多相似文章
评论