圖片上傳姿式以及你不知道的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,則會被轉換成字符串。

咱們經過<input type="input"/>選擇圖片,把獲取到的file放到FormData,再提交到服務器。es6

若是上傳多個文件,就追加到同一個字段中。web

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

其中的file-likenew Blob的示例說明咱們能夠構造一個新的文件直接上傳。json

場景1:剪輯圖片上傳

咱們經過裁剪庫能夠獲得data url或者canvascanvas

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

<p class="codepen" data-height="355" data-theme-id="0" data-default-tab="js,result" data-user="ineo6" data-slug-hash="MWgpGQZ" data-preview="true" style="height: 355px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="url image轉爲base64">
<span>See the Pen
url image轉爲base64
by neo (@ineo6)
on CodePen.</span>
</p>
<script async src="https://static.codepen.io/ass...;></script>

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

15722463800941.jpg

再來看一個小栗子:

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

wechat-find-me.png

相關文章
相關標籤/搜索