图片上传姿式以及你不知道的Typed Arrays

在思否答题遇到几个关于图片上传的问题,中间都涉及到ArrayBuffer的概念,心心念念想整理下这方面的知识,也但愿让更多人能有所收获。php

各位看官,一块儿开始吧。html

1. 如何上传文件

前端中上传通常使用FormData建立请求数据,示例以下:前端

var formData = new FormData();

formData.append("username", "Groucho");

// HTML 文件类型input,由用户选择
formData.append("userfile", fileInputElement.files[0]);

// JavaScript file-like 对象
var content = '<a id="a"><b id="b">hey!</b></a>'; // 新文件的正文...
var blob = new Blob([content], { type: "text/xml"});

formData.append("webmasterfile", blob);

var request = new XMLHttpRequest();
request.open("POST", "http://foo.com/submitform.php");
request.send(formData);
复制代码

FormData 对象的字段类型能够是 Blob, File, 或者 string,若是它的字段类型不是Blob也不是File,则会被转换成字符串。es6

咱们经过<input type="input"/>选择图片,把获取到的file放到FormData,再提交到服务器。web

若是上传多个文件,就追加到同一个字段中。json

fileInputElement.files.forEach(file => {
  formData.append('userfile', file);
})
复制代码

其中的file-likenew Blob的示例说明咱们能够构造一个新的文件直接上传。canvas

场景1:剪辑图片上传

咱们经过裁剪库能够获得data url或者canvas数组

cropperjs举例,使用getCroppedCanvas获取到canvas,而后利用自身的toBlob获取到file数据,再经过FormData上传。浏览器

转换的核心代码能够参考下面:缓存

canvas = cropper.getCroppedCanvas({
  width: 160,
  height: 160,
});

initialAvatarURL = avatar.src;
avatar.src = canvas.toDataURL();

// 从canvs获取blob数据
canvas.toBlob(function (blob) {
  var formData = new FormData();
  formData.append('avatar', blob, 'avatar.jpg');
  
  // 接下来能够发起请求了
  makeRequest(formData)
})
复制代码

场景2:base64图片上传

获取到base64形式的图片后,咱们经过下面函数转为blob形式:

function btof(base64Data, fileName) {
  const dataArr = base64Data.split(",");
  const byteString = atob(dataArr[1]);

  const options = {
    type: "image/jpeg",
    endings: "native"
  };
  const u8Arr = new Uint8Array(byteString.length);
  for (let i = 0; i < byteString.length; i++) {
    u8Arr[i] = byteString.charCodeAt(i);
  }
  return new File([u8Arr], fileName + ".jpg", options);
}
复制代码

这样咱们拿到了文件file,而后就能够继续上传了。

场景3:URL图片上传

想要直接用图片URL上传,咱们能够分红两部来作:

  1. 获取base64
  2. 而后转为file

其中关键代码是如何从URL中建立canvas,这里经过建立Image对象,在图片挂载以后,填充到到canvas中。

var img =
  "https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=508387608,2848974022&fm=26&gp=0.jpg"; //imgurl 就是你的图片路径
  
var image = new Image();
image.src = img;
image.setAttribute("crossOrigin", "Anonymous");
image.onload = function() {
  // 第1步:获取base64形式的图片
  var base64 = getBase64Image(image);

  var formData = new FormData(); 

  // 第2步:转换base64到file
  var file = btof(base64, "test");
  formData.append("imageName", file);
};

function getBase64Image(img) {
  var canvas = document.createElement("canvas");
  canvas.width = img.width;
  canvas.height = img.height;
  var ctx = canvas.getContext("2d");
  ctx.drawImage(img, 0, 0, img.width, img.height);
  var ext = img.src.substring(img.src.lastIndexOf(".") + 1).toLowerCase();
  var dataURL = canvas.toDataURL("image/" + ext);

  return dataURL;
}

复制代码

See the Pen url image转为base64 by neo (@ineo6) on CodePen.

2. 思考

虽然前文提到的场景咱们解决了,可是里面包含了这些关键词,不得不让人思考:

  • Blob
  • File
  • Uint8Array
  • ArrayBuffer
  • TypedArray
  • Base64
  • atob,btoa

这些关键词都指向"文件"、"二进制"、"编码",也是咱们平时不太会注意的点。

以前使用到FileBlob时内心也一直有疑惑。

到底这些有什么做用呢?接下来能够看看我整理的这些知识。

3. 概念

3.1 Blob

Blob 对象表示一个不可变、原始数据的类文件对象。

File接口也是基于Blob对象,而且进行扩展支持用户系统的文件格式。

3.1.1 建立Blob对象

要从其余非blob对象和数据构造Blob,就要使用Blob()构造函数:

var debug = {hello: "world"};
var blob = new Blob([JSON.stringify(debug, null, 2)], {type : 'application/json'});
复制代码

3.1.1 读取Blob对象

使用FileReader能够读取Blob对象中的内容。

var reader = new FileReader();
reader.addEventListener("loadend", function() {
   //reader.result 就是内容
   console.log(reader.result)
});
reader.readAsArrayBuffer(blob);
复制代码

3.1.1 Object URLs

Object URLs指的是以blob:开头的地址,能够用来展现图片、文本信息。

这里就有点相似base64图片的展现,因此咱们一样能够用来预览图片。

下面代码片断就是把选中的图片转为Object URLs形式。

function handleFiles(files) {
  if (!files.length) {
    fileList.innerHTML = "<p>No file!</p>";
  } else {
    fileList.innerHTML = "";
    var list = document.createElement("ul");
    fileList.appendChild(list);
    for (var i = 0; i < files.length; i++) {
      var li = document.createElement("li");
      list.appendChild(li);
      
      var img = document.createElement("img");
      // 从文件中建立object url
      img.src = window.URL.createObjectURL(files[i]);
      img.height = 60;
      img.onload = function() {
        // 加载完成后记得释放object url
        window.URL.revokeObjectURL(this.src);
      }
      li.appendChild(img);
      var info = document.createElement("span");
      info.innerHTML = files[i].name + ": " + files[i].size + " bytes";
      li.appendChild(info);
    }
  }
}
复制代码

demo

3.2 Typed Arrays - 类型化数组

类型化数组是一种相似数组的对象,提供了访问原始二进制数据的功能。可是类型化数组和正常数组并非一类的,Array.isArray()调用会返回false

Typed Arrays有两块内容:

  • 缓冲(ArrayBuffer)
  • 视图(TypedArray 和 DataView)

3.2.1 ArrayBuffer

ArrayBuffer 对象用来表示通用的、固定长度的原始二进制数据缓冲区。 ArrayBuffer 不能直接操做,而是要经过TypedArrayDataView对象来操做,它们会将缓冲区中的数据表示为特定的格式,并经过这些格式来读写缓冲区的内容。

ArrayBuffer主要用来高效快速的访问二进制数据,好比 WebGL, Canvas 2D 或者 Web Audio 所使用的数据。

接下来咱们结合TypedArray一块儿理解下。

3.2.2 TypedArray

TypedArray能够在ArrayBuffer对象之上,根据不一样的数据类型创建视图。

// 建立一个8字节的ArrayBuffer
const b = new ArrayBuffer(8);

// 建立一个指向b的Int32视图,开始于字节0,直到缓冲区的末尾
const v1 = new Int32Array(b);

// 建立一个指向b的Uint8视图,开始于字节2,直到缓冲区的末尾
const v2 = new Uint8Array(b, 2);

// 建立一个指向b的Int16视图,开始于字节2,长度为2
const v3 = new Int16Array(b, 2, 2);
复制代码

Int32Array,Uint8Array之类指的就是TypedArrayTypedArray对象描述的是底层二进制数据缓存区的一个相似数组(array-like)的视图。

它有着众多的成员:

Int8Array(); 
Uint8Array(); 
Uint8ClampedArray();
Int16Array(); 
Uint16Array();
Int32Array(); 
Uint32Array(); 
Float32Array(); 
Float64Array();
复制代码

再来看一个小栗子:

var buffer = new ArrayBuffer(2) 
var bytes = new Uint8Array(buffer)

bytes[0] = 65 // ASCII for 'A'
bytes[1] = 66 // ASCII for 'B'

// 查看buffer内容
var blob = new Blob([buffer], {type: 'text/plain'})
var dataUri = window.URL.createObjectURL(blob)
window.open(dataUri) // AB
复制代码

字节序

上面的例子中,咱们先写入'A',再写入'B',固然咱们也能够经过Uint16Array一下写入两个字节。

var buffer = new ArrayBuffer(2) // 两个字节的缓冲
var word = new Uint16Array(buffer) // 以16位整型访问缓冲

// 添加'A'到高位,添加'B'到低位
var value = (65 << 8) + 66
word[0] = value

var blob = new Blob([buffer], {type: 'text/plain'})
var dataUri = window.URL.createObjectURL(blob)
window.open(dataUri) // BA
复制代码

执行这段代码你会发现,为何看到的是"BA"而不是"AB"?

这是由于还有"字节序"的存在,分别是小端字节序和大端字节序。

好比,一个占据四个字节的 16 进制数0x12345678,决定其大小的最重要的字节是“12”,最不重要的是“78”。小端字节序将最不重要的字节排在前面,储存顺序就是78563412;大端字节序则彻底相反,将最重要的字节排在前面,储存顺序就是12345678。

由于浏览器使用的是小端字节序,就致使咱们看到的是"BA"。为了解决字节序不统一的问题,咱们可使用DataView设定字节序。

TypedArray.prototype.buffer

TypedArray实例的buffer属性,返回整段内存区域对应的ArrayBuffer对象。该属性为只读属性。

const a = new Float32Array(64);
const b = new Uint8Array(a.buffer);
复制代码

上面代码的a视图对象和b视图对象,对应同一个ArrayBuffer对象,即同一段内存。

TypedArray.prototype.byteLength,TypedArray.prototype.byteOffset

byteLength属性返回 TypedArray 数组占据的内存长度,单位为字节。byteOffset属性返回 TypedArray 数组从底层ArrayBuffer对象的哪一个字节开始。这两个属性都是只读属性。

const b = new ArrayBuffer(8);

const v1 = new Int32Array(b);
const v2 = new Uint8Array(b, 2);
const v3 = new Int16Array(b, 2, 2);

v1.byteLength // 8
v2.byteLength // 6
v3.byteLength // 4

v1.byteOffset // 0
v2.byteOffset // 2
v3.byteOffset // 2
复制代码
TypedArray.prototype.length

length属性表示 TypedArray 数组含有多少个成员。注意将 length 属性和 byteLength 属性区分,前者是成员长度,后者是字节长度。

const a = new Int16Array(8);

a.length // 8
a.byteLength // 16
复制代码
TypedArray.prototype.set()

TypedArray数组的set方法用于复制数组(普通数组或 TypedArray 数组),也就是将一段内容彻底复制到另外一段内存。

const a = new Uint8Array(8);
const b = new Uint8Array(8);

b.set(a);
复制代码

set方法还能够接受第二个参数,表示从b对象的哪个成员开始复制a对象。

TypedArray.prototype.subarray()

subarray方法是对于 TypedArray 数组的一部分,再创建一个新的视图。

const a = new Uint16Array(8);
const b = a.subarray(2,3);

a.byteLength // 16
b.byteLength // 2
复制代码
TypedArray.prototype.slice()

TypeArray 实例的slice方法,能够返回一个指定位置的新的TypedArray实例。

let ui8 = Uint8Array.of(0, 1, 2);
ui8.slice(-1)
// Uint8Array [ 2 ]
复制代码
TypedArray.of()

TypedArray 数组的全部构造函数,都有一个静态方法of,用于将参数转为一个TypedArray实例。

Float32Array.of(0.151, -8, 3.7)
// Float32Array [ 0.151, -8, 3.7 ]
复制代码

下面三种方法都会生成一样一个 TypedArray 数组。

// 方法一
let tarr = new Uint8Array([1,2,3]);

// 方法二
let tarr = Uint8Array.of(1,2,3);

// 方法三
let tarr = new Uint8Array(3);
tarr[0] = 1;
tarr[1] = 2;
tarr[2] = 3;
复制代码
TypedArray.from()

静态方法from接受一个可遍历的数据结构(好比数组)做为参数,返回一个基于这个结构的TypedArray实例。

Uint16Array.from([0, 1, 2])
// Uint16Array [ 0, 1, 2 ]
复制代码

这个方法还能够将一种TypedArray实例,转为另外一种。

const ui16 = Uint16Array.from(Uint8Array.of(0, 1, 2));
ui16 instanceof Uint16Array // true
复制代码

from方法还能够接受一个函数,做为第二个参数,用来对每一个元素进行遍历,功能相似map方法。

Int8Array.of(127, 126, 125).map(x => 2 * x)
// Int8Array [ -2, -4, -6 ]

Int16Array.from(Int8Array.of(127, 126, 125), x => 2 * x)
// Int16Array [ 254, 252, 250 ]
复制代码

上面的例子中,from方法没有发生溢出,这说明遍历不是针对原来的 8 位整数数组。也就是说,from会将第一个参数指定的 TypedArray 数组,拷贝到另外一段内存之中,处理以后再将结果转成指定的数组格式。

复合视图

因为视图的构造函数能够指定起始位置和长度,因此在同一段内存之中,能够依次存放不一样类型的数据,这叫作“复合视图”。

const buffer = new ArrayBuffer(24);

const idView = new Uint32Array(buffer, 0, 1);
const usernameView = new Uint8Array(buffer, 4, 16);
const amountDueView = new Float32Array(buffer, 20, 1);
复制代码

上面代码将一个 24 字节长度的ArrayBuffer对象,分红三个部分:

  • 字节 0 到字节 3:1 个 32 位无符号整数
  • 字节 4 到字节 19:16 个 8 位整数
  • 字节 20 到字节 23:1 个 32 位浮点数

3.2.3 DataView - 视图

若是一段数据包含多种类型,咱们还可使用DataView视图进行操做。

DataView 视图提供 8 个方法写入内存。

dataview.setXXX(byteOffset, value [, littleEndian])

  • byteOffset 偏移量,单位为字节
  • value 设置的数值
  • littleEndian 传入false或undefined表示使用大端字节序

setInt8:写入 1 个字节的 8 位整数。
setUint8:写入 1 个字节的 8 位无符号整数。
setInt16:写入 2 个字节的 16 位整数。
setUint16:写入 2 个字节的 16 位无符号整数。
setInt32:写入 4 个字节的 32 位整数。
setUint32:写入 4 个字节的 32 位无符号整数。
setFloat32:写入 4 个字节的 32 位浮点数。
setFloat64:写入 8 个字节的 64 位浮点数。
复制代码

相应也有8个方法读取内存:

getInt8:读取 1 个字节,返回一个 8 位整数。
getUint8:读取 1 个字节,返回一个无符号的 8 位整数。
getInt16:读取 2 个字节,返回一个 16 位整数。
getUint16:读取 2 个字节,返回一个无符号的 16 位整数。
getInt32:读取 4 个字节,返回一个 32 位整数。
getUint32:读取 4 个字节,返回一个无符号的 32 位整数。
getFloat32:读取 4 个字节,返回一个 32 位浮点数。
getFloat64:读取 8 个字节,返回一个 64 位浮点数。
复制代码

下面是表格里是BMP文件的头信息:

Byte 描述
2 "BM"标记
4 文件大小
2 保留
2 保留
4 文件头和位图数据之间的偏移量

咱们使用DataView能够这样简单实现:

var buffer = new ArrayBuffer(14)
var view = new DataView(buffer)

view.setUint8(0, 66)     // 写入1字节: 'B'
view.setUint8(1, 67)     // 写入1字节: 'M'
view.setUint32(2, 1234)  // 写入4字节的大小: 1234
view.setUint16(6, 0)     // 写入2字节保留位
view.setUint16(8, 0)     // 写入2字节保留位
view.setUint32(10, 0)    // 写入4字节偏移量
复制代码

里面对应的结构应该是这样的:

Byte  |    0   |    1   |    2   |    3   |    4   |    5   | ... |
Type  |   I8   |   I8   |                I32                | ... |    
Data  |    B   |    M   |00000000|00000000|00000100|11010010| ... |
复制代码

回到前面遇到的"BA"问题,咱们用DataView从新执行下:

var buffer = new ArrayBuffer(2) 
var view = new DataView(buffer)

var value = (65 << 8) + 66
view.setUint16(0, value)

var blob = new Blob([buffer], {type: 'text/plain'})
var dataUri = window.URL.createObjectURL(blob)
window.open(dataUri) // AB
复制代码

这下咱们获得了正确结果"AB",这个也说明DataView默认使用大端字节序。

参考文章

本文同步发表于做者博客: 图片上传姿式以及你不知道的Typed Arrays

相关文章
相关标签/搜索