[1.3萬字] 玩轉前端二進制

本文阿寶哥將按照如下的流程來介紹前端如何進行圖片處理,而後穿插介紹二進制、Blob、Blob URL、Base6四、Data URL、ArrayBuffer、TypedArray、DataView 和圖片壓縮相關的知識點。javascript

image-process-flow-1.jpeg

閱讀完本文,小夥伴們將能輕鬆看懂如下轉換關係圖:html

binary-object-transfer-flow.jpeg

還在猶豫什麼?跟上阿寶哥的腳步,讓咱們一塊兒來玩轉前端二進制。請小夥伴們原諒阿寶哥的 「自戀」,在後面的示例中,咱們將使用阿寶哥的我的頭像做爲演示素材。前端

abao.png

好的,如今咱們開始來進入第一個環節:選擇本地圖片 -> 圖片預覽java

1、選擇本地圖片 -> 圖片預覽

1.1 FileReader API

在支持 FileReader API 的瀏覽器中,咱們也能夠利用該 API 方便實現圖片本地預覽功能。node

caniuse-file-reader.jpg

(圖片來源:https://caniuse.com/#search=f...git

由上圖可知,該 API 兼容性較好,咱們能夠放心使用。這裏阿寶哥就不展開詳細介紹 FileReader API,咱們直接來看一下利用它如何實現本地圖片預覽,具體代碼以下:github

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>圖片本地預覽示例</title>
  </head>
  <body>
    <h3>阿寶哥:圖片本地預覽示例</h3>
    <input type="file" accept="image/*" onchange="loadFile(event)" />
    <img id="previewContainer" />

    <script>
      const loadFile = function (event) {
        const reader = new FileReader();
        reader.onload = function () {
          const output = document.querySelector("#previewContainer");
          output.src = reader.result;
        };
        reader.readAsDataURL(event.target.files[0]);
      };
    </script>
  </body>
</html>

在以上示例中,咱們爲 file 類型的輸入框綁定 onchange 事件處理函數 loadFile,在該函數中,咱們建立了一個 FileReader 對象併爲該對象綁定 onload 相應的事件處理函數,而後調用 FileReader 對象的 readAsDataURL() 方法,把本地圖片對應的 File 對象轉換爲 Data URL。web

其實對於 FileReader 對象來講,除了支持把 File/Blob 對象轉換爲 Data URL 以外,它還提供了 readAsArrayBuffer()readAsText() 方法,用於把 File/Blob 對象轉換爲其它的數據格式。

當文件讀取完成後,會觸發綁定的 onload 事件處理函數,在該處理函數內部會把獲取 Data URL 數據賦給 img 元素的 src 屬性,從而實現圖片本地預覽。算法

經過使用 Chrome 開發者工具,咱們能夠在 Elements 面板中看到 Data URL 的 「芳容」數據庫

在圖中右側的綠色框中,咱們能夠清楚的看到 img 元素 src 屬性值是一串很是 奇怪 的字符串:

data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAhAAAAIwCAYAAADXrFK...

其實,這串奇怪的字符串被稱爲 Data URL,它由四個部分組成:前綴(data:)、指示數據類型的 MIME 類型、若是非文本則爲可選的 base64 標記、數據自己:

data:[<mediatype>][;base64],<data>

mediatype 是個 MIME 類型的字符串,好比 "image/png" 表示 PNG 圖像文件。若是被省略,則默認值爲 text/plain;charset=US-ASCII。若是數據是文本類型,你能夠直接將文本嵌入(根據文檔類型,使用合適的實體字符或轉義字符)。若是是二進制數據,你能夠將數據進行 base64 編碼以後再進行嵌入。

MIME(Multipurpose Internet Mail Extensions)多用途互聯網郵件擴展類型,是設定某種擴展名的文件用一種應用程序來打開的方式類型,當該擴展名文件被訪問的時候,瀏覽器會自動使用指定應用程序來打開。多用於指定一些客戶端自定義的文件名,以及一些媒體文件打開方式。

常見的 MIME 類型有:超文本標記語言文本 .html text/html、PNG 圖像 .png image/png、普通文本 .txt text/plain 等。

在 Web 項目開發過程當中,爲了減小 HTTP 請求的數量,對應一些較小的圖標,咱們一般會考慮使用 Data URL 的形式內嵌到 HTML 或 CSS 文件中。 但須要注意的是:若是圖片較大,圖片的色彩層次比較豐富,則不適合使用這種方式,由於該圖片通過 base64 編碼後的字符串很是大,會明顯增大 HTML 頁面的大小,從而影響加載速度。

在 Data URL 中,數據是很重要的一部分,它使用 base64 編碼的字符串來表示。所以要掌握 Data URL,咱們還得了解一下 Base64。

1.2 Base64

Base64 是一種基於 64 個可打印字符來表示二進制數據的表示方法。因爲 2⁶ = 64 ,因此每 6 個比特爲一個單元,對應某個可打印字符。3 個字節有 24 個比特,對應於 4 個 base64 單元,即 3 個字節可由 4 個可打印字符來表示。相應的轉換過程以下圖所示:

Base64 經常使用於在處理文本數據的場合,表示、傳輸、存儲一些二進制數據,包括 MIME 的電子郵件及 XML 的一些複雜數據。 在 MIME 格式的電子郵件中,base64 能夠用來將二進制的字節序列數據編碼成 ASCII 字符序列構成的文本。使用時,在傳輸編碼方式中指定 base64。使用的字符包括大小寫拉丁字母各 26 個、數字 10 個、加號 + 和斜槓 /,共 64 個字符,等號 = 用來做爲後綴用途。

Base64 相應的索引表以下:

瞭解完上述的知識,咱們以編碼 Man 爲例,來直觀的感覺一下編碼過程。Man 由 M、a 和 n 這 3 個字符組成,它們對應的 ASCII 碼爲 7七、97 和 110。

接着咱們以每 6 個比特爲一個單元,進行 base64 編碼操做,具體以下圖所示:

由圖可知,Man (3 字節)編碼的結果爲 TWFu(4 字節),很明顯通過 base64 編碼後體積會增長 1/3。Man 這個字符串的長度恰好是 3,咱們能夠用 4 個 base64 單元來表示。但若是待編碼的字符串長度不是 3 的整數倍時,應該如何處理呢?

若是要編碼的字節數不能被 3 整除,最後會多出 1 個或 2 個字節,那麼可使用下面的方法進行處理:先使用 0 字節值在末尾補足,使其可以被 3 整除,而後再進行 base64 的編碼。

以編碼字符 A 爲例,其所佔的字節數爲 1,不能被 3 整除,須要補 2 個字節,具體以下圖所示:

由上圖可知,字符 A 通過 base64 編碼後的結果是 QQ==,該結果後面的兩個 = 表明補足的字節數。而最後個 1 個 base64 字節塊有 4 位是 0 值。

接着咱們來看另外一個示例,假設需編碼的字符串爲 BC,其所佔字節數爲 2,不能被 3 整除,須要補 1 個字節,具體以下圖所示:

由上圖可知,字符串 BC 通過 base64 編碼後的結果是 QkM=,該結果後面的 1 個 = 表明補足的字節數。而最後個 1 個 base64 字節塊有 2 位是 0 值。

在 JavaScript 中,有兩個函數被分別用來處理解碼和編碼 base64 字符串:

  • btoa():該函數可以基於二進制數據 「字符串」 建立一個 base64 編碼的 ASCII 字符串。
  • atob(): 該函數可以解碼經過 base64 編碼的字符串數據。
1.2.1 btoa 使用示例
const name = 'Semlinker';
const encodedName = btoa(name);
console.log(encodedName); // U2VtbGlua2Vy
1.2.2 atob 使用示例
const encodedName = 'U2VtbGlua2Vy';
const name = atob(encodedName);
console.log(name); // Semlinker

對於 atob 和 btoa 這兩個方法來講,其中的 a 表明 ASCII,而 b 表明 Blob,即二進制。所以 atob 表示 ASCII 到二進制,對應的是解碼操做。而 btoa 表示二進制到 ASCII,對應的是編碼操做。在瞭解方法中 a 和 b 分別表明的意義以後,在之後的工做中,咱們就不會用錯了。

相信看到這裏,小夥伴們對 base64 已經有必定的瞭解。須要注意的是 base64 只是一種數據編碼方式,目的是爲了保障數據的安全傳輸。但標準的 base64 編碼無需額外的信息,便可以進行解碼,是徹底可逆的。所以在涉及傳輸私密數據時,並不能直接使用 base64 編碼,而是要使用專門的對稱或非對稱加密算法。

2、網絡下載圖片 -> 圖片預覽

除了能夠從本地獲取圖片以外,咱們也可使用 fetch API 從網絡上獲取圖片,而後在進行圖片預覽。固然對於網絡上可正常訪問的圖片地址,咱們能夠直接把地址賦給 img 元素,並不須要經過 fetch API 繞一大圈。若在顯示圖片時,你須要對圖片進行特殊處理,好比解密圖片數據時,你就能夠考慮在 Web Worker 中使用 fetch API 獲取圖片數據並進行解密操做。

簡單起見,咱們不考慮特殊的場景。首先咱們先來看一下 fetch API 的兼容性:

(圖片來源:https://caniuse.com/#search=f...

而後咱們使用 fetch API 從 Github 上獲取阿寶哥的頭像,具體代碼以下所示:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>獲取遠程圖片預覽示例</title>
  </head>
  <body>
    <h3>阿寶哥:獲取遠程圖片預覽示例</h3>
    <img id="previewContainer" style="width: 50%;"/>

    <script>
      const image = document.querySelector("#previewContainer");
      fetch("https://avatars3.githubusercontent.com/u/4220799")
        .then((response) => response.blob())
        .then((blob) => {
          const objectURL = URL.createObjectURL(blob);
          image.src = objectURL;
        });
    </script>
  </body>
</html>

在以上示例中,咱們經過 fetch API 從 Github 上下載阿寶哥的頭像,當請求成功後,把響應對象(Response)轉換爲 Blob 對象,而後使用 URL.createObjectURL 方法,建立 Object URL 並把它賦給 img 元素的 src 屬性,從而實現圖片的顯示。

經過使用 Chrome 開發者工具,咱們能夠在 Elements 面板中看到 Object URL 的 「芳容」

在圖中右側的綠色框中,咱們能夠清楚的看到 img 元素 src 屬性值是一串很是 特殊 的字符串:

blob:null/ab24c171-1c5f-4de1-a44e-568bc1f77d7b

以上的特殊字符串,咱們稱之爲 Object URL,相比前面介紹的 Data URL,它簡潔不少。接下來咱們來認識一下 Object URL 這種協議。

2.1 Object URL

Object URL 是一種僞協議,也被稱爲 Blob URL。它容許 Blob 或 File 對象用做圖像,下載二進制數據連接等的 URL 源。在瀏覽器中,咱們使用 URL.createObjectURL 方法來建立 Blob URL,該方法接收一個 Blob 對象,併爲其建立一個惟一的 URL,其形式爲 blob:<origin>/<uuid>,對應的示例以下:

blob:https://example.org/40a5fb5a-d56d-4a33-b4e2-0acf6a8e5f641

瀏覽器內部爲每一個經過 URL.createObjectURL 生成的 URL 存儲了一個 URL → Blob 映射。所以,此類 URL 較短,但能夠訪問 Blob。生成的 URL 僅在當前文檔打開的狀態下才有效。但若是你訪問的 Blob URL 再也不存在,則會從瀏覽器中收到 404 錯誤。

上述的 Blob URL 看似很不錯,但實際上它也有反作用。雖然存儲了 URL → Blob 的映射,但 Blob 自己仍駐留在內存中,瀏覽器沒法釋放它。映射在文檔卸載時自動清除,所以 Blob 對象隨後被釋放。可是,若是應用程序壽命很長,那不會很快發生。所以,若是咱們建立一個 Blob URL,即便再也不須要該 Blob,它也會存在內存中。

針對這個問題,咱們能夠調用 URL.revokeObjectURL(url) 方法,從內部映射中刪除引用,從而容許刪除 Blob(若是沒有其餘引用),並釋放內存。

既然講到了 Blob URL,不得不提 Blob。那麼什麼是 Blob 呢?咱們繼續往下看。

2.2 Blob API

Blob(Binary Large Object)表示二進制類型的大對象。在數據庫管理系統中,將二進制數據存儲爲一個單一個體的集合。Blob 一般是影像、聲音或多媒體文件。在 JavaScript 中 Blob 類型的對象表示不可變的相似文件對象的原始數據。 爲了更直觀的感覺 Blob 對象,咱們先來使用 Blob 構造函數,建立一個 myBlob 對象,具體以下圖所示:

如你所見,myBlob 對象含有兩個屬性:size 和 type。其中 size 屬性用於表示數據的大小(以字節爲單位),type 是 MIME 類型的字符串。Blob 表示的不必定是 JavaScript 原生格式的數據。好比 File 接口基於 Blob,繼承了 blob 的功能並將其擴展使其支持用戶系統上的文件。

Blob 由一個可選的字符串 type(一般是 MIME 類型)和 blobParts 組成:

MIME(Multipurpose Internet Mail Extensions)多用途互聯網郵件擴展類型,是設定某種擴展名的文件用一種應用程序來打開的方式類型,當該擴展名文件被訪問的時候,瀏覽器會自動使用指定應用程序來打開。多用於指定一些客戶端自定義的文件名,以及一些媒體文件打開方式。

常見的 MIME 類型有:超文本標記語言文本 .html text/html、PNG圖像 .png image/png、普通文本 .txt text/plain 等。

2.2.1 Blob 構造函數

Blob 構造函數的語法爲:

var aBlob = new Blob(blobParts, options);

相關的參數說明以下:

  • blobParts:它是一個由 ArrayBuffer,ArrayBufferView,Blob,DOMString 等對象構成的數組。DOMStrings 會被編碼爲 UTF-8。
  • options:一個可選的對象,包含如下兩個屬性:

    • type —— 默認值爲 "",它表明了將會被放入到 blob 中的數組內容的 MIME 類型。
    • endings —— 默認值爲 "transparent",用於指定包含行結束符 \n 的字符串如何被寫入。 它是如下兩個值中的一個: "native",表明行結束符會被更改成適合宿主操做系統文件系統的換行符,或者 "transparent",表明會保持 blob 中保存的結束符不變。

示例一:從字符串建立 Blob

let myBlobParts = ['<html><h2>Hello Semlinker</h2></html>']; // an array consisting of a single DOMString
let myBlob = new Blob(myBlobParts, {type : 'text/html', endings: "transparent"}); // the blob

console.log(myBlob.size + " bytes size");
// Output: 37 bytes size
console.log(myBlob.type + " is the type");
// Output: text/html is the type

示例二:從類型化數組和字符串建立 Blob

let hello = new Uint8Array([72, 101, 108, 108, 111]); // 二進制格式的 "hello"
let blob = new Blob([hello, ' ', 'semlinker'], {type: 'text/plain'});

介紹完 Blob 構造函數,接下來咱們來分別介紹 Blob 類的屬性和方法:

2.2.2 Blob 屬性

前面咱們已經知道 Blob 對象包含兩個屬性:

  • size(只讀):表示 Blob 對象中所包含數據的大小(以字節爲單位)。
  • type(只讀):一個字符串,代表該 Blob 對象所包含數據的 MIME 類型。若是類型未知,則該值爲空字符串。
2.2.3 Blob 方法
  • slice([start[, end[, contentType]]]):返回一個新的 Blob 對象,包含了源 Blob 對象中指定範圍內的數據。
  • stream():返回一個能讀取 blob 內容的 ReadableStream
  • text():返回一個 Promise 對象且包含 blob 全部內容的 UTF-8 格式的 USVString
  • arrayBuffer():返回一個 Promise 對象且包含 blob 全部內容的二進制格式的 ArrayBuffer

這裏咱們須要注意的是,Blob 對象是不可改變的。咱們不能直接在一個 Blob 中更改數據,可是咱們能夠對一個 Blob 進行分割,從其中建立新的 Blob 對象,將它們混合到一個新的 Blob 中。這種行爲相似於 JavaScript 字符串:咱們沒法更改字符串中的字符,但能夠建立新的更正後的字符串。

對於 fetch API 的 Response 對象來講,該對象除了提供 blob() 方法以外,還提供了 json()text()formData()arrayBuffer() 等方法,用於把響應轉換爲不一樣的數據格式。

在先後端分離的項目中,你們用得比較多的應該就是 json() 方法,而其它方法可能用得相對比較少。對於前面的示例,咱們把響應對象轉換爲 ArrayBuffer 對象,一樣能夠正常顯示從網絡下載的圖像,具體的代碼以下所示:

<h3>阿寶哥:獲取遠程圖片預覽示例</h3>
<img id="previewContainer" style="width: 50%;"/>

<script>
   const image = document.querySelector("#previewContainer");
   fetch("https://avatars3.githubusercontent.com/u/4220799")
     .then((response) => response.arrayBuffer())
     .then((buffer) => {
        const blob = new Blob([buffer]);
        const objectURL = URL.createObjectURL(blob);
        image.src = objectURL;
   });
</script>

在以上代碼中,咱們先把響應對象轉換爲 ArrayBuffer 對象,而後經過調用 Blob 構造函數,把 ArrayBuffer 對象轉換爲 Blob 對象,再利用 createObjectURL() 方法建立 Object URL,最終實現圖片預覽。

相信有些小夥伴對 ArrayBuffer 還不太熟悉,下面阿寶哥就帶你們一塊兒來揭開它的神祕 「面紗」

2.3 ArrayBuffer 與 TypedArray

2.3.1 ArrayBuffer

ArrayBuffer 對象用來表示通用的、固定長度的原始二進制數據緩衝區。ArrayBuffer 不能直接操做,而是要經過類型數組對象DataView 對象來操做,它們會將緩衝區中的數據表示爲特定的格式,並經過這些格式來讀寫緩衝區的內容。

ArrayBuffer 簡單說是一片內存,可是你不能直接用它。這就比如你在 C 裏面,malloc 一片內存出來,你也會把它轉換成 unsigned_int32 或者 int16 這些你須要的實際類型的數組/指針來用。

這就是 JS 裏的 TypedArray 的做用,那些 Uint32Array 也好,Int16Array 也好,都是給 ArrayBuffer 提供了一個 「View」,MDN 上的原話叫作 「Multiple views on the same data」,對它們進行下標讀寫,最終都會反應到它所創建在的 ArrayBuffer 之上。

來源:https://www.zhihu.com/questio...

語法

new ArrayBuffer(length)
  • 參數:length 表示要建立的 ArrayBuffer 的大小,單位爲字節。
  • 返回值:一個指定大小的 ArrayBuffer 對象,其內容被初始化爲 0。
  • 異常:若是 length 大於 Number.MAX_SAFE_INTEGER(>= 2 ** 53)或爲負數,則拋出一個 RangeError 異常。

示例

下面的例子建立了一個 8 字節的緩衝區,並使用一個 Int32Array 來引用它:

let buffer = new ArrayBuffer(8);
let view   = new Int32Array(buffer);

從 ECMAScript 2015 開始,ArrayBuffer 對象須要用 new 運算符建立。若是調用構造函數時沒有使用 new,將會拋出 TypeError 異常。好比執行該語句 let ab = ArrayBuffer(10) 將會拋出如下異常:

VM109:1 Uncaught TypeError: Constructor ArrayBuffer requires 'new'
    at ArrayBuffer (<anonymous>)
    at <anonymous>:1:10

對於一些經常使用的 Web API,如 FileReader API 和 Fetch API 底層也是支持 ArrayBuffer,這裏咱們以 FileReader API 爲例,看一下如何把 File 對象讀取爲 ArrayBuffer 對象:

const reader = new FileReader();

reader.onload = function(e) {
  let arrayBuffer = reader.result;
}

reader.readAsArrayBuffer(file);
2.3.2 Unit8Array

Uint8Array 數組類型表示一個 8 位無符號整型數組,建立時內容被初始化爲 0。建立完後,能夠以對象的方式或使用數組下標索引的方式引用數組中的元素。

語法

new Uint8Array(); // ES2017 最新語法
new Uint8Array(length); // 建立初始化爲0的,包含length個元素的無符號整型數組
new Uint8Array(typedArray);
new Uint8Array(object);
new Uint8Array(buffer [, byteOffset [, length]]);

示例

// new Uint8Array(length); 
var uint8 = new Uint8Array(2);
uint8[0] = 42;
console.log(uint8[0]); // 42
console.log(uint8.length); // 2
console.log(uint8.BYTES_PER_ELEMENT); // 1

// new TypedArray(object); 
var arr = new Uint8Array([21,31]);
console.log(arr[1]); // 31

// new Uint8Array(typedArray);
var x = new Uint8Array([21, 31]);
var y = new Uint8Array(x);
console.log(y[0]); // 21

// new Uint8Array(buffer [, byteOffset [, length]]);
var buffer = new ArrayBuffer(8);
var z = new Uint8Array(buffer, 1, 4);
2.3.3 ArrayBuffer 與 TypedArray 之間的關係

ArrayBuffer 自己只是一行 0 和 1 串。 ArrayBuffer 不知道該數組中第一個元素和第二個元素之間的分隔位置。

(圖片來源 —— A cartoon intro to ArrayBuffers and SharedArrayBuffers

爲了提供上下文,實際上要將其分解爲多個盒子,咱們須要將其包裝在所謂的視圖中。可使用類型數組添加這些數據視圖,而且你可使用許多不一樣類型的類型數組。

例如,你能夠有一個 Int8 類型的數組,它將把這個數組分紅 8-bit 的字節數組。

(圖片來源 —— A cartoon intro to ArrayBuffers and SharedArrayBuffers

或者你也能夠有一個無符號 Int16 數組,它會把數組分紅 16-bit 的字節數組,而且把它看成無符號整數來處理。

(圖片來源 —— A cartoon intro to ArrayBuffers and SharedArrayBuffers

你甚至能夠在同一基本緩衝區上擁有多個視圖。對於相同的操做,不一樣的視圖會給出不一樣的結果。

例如,若是咱們從這個 ArrayBuffer 的 Int8 視圖中獲取 0 & 1 元素的值(-19 & 100),它將給咱們與 Uint16 視圖中元素 0 (25837)不一樣的值,即便它們包含徹底相同的位。

(圖片來源 —— A cartoon intro to ArrayBuffers and SharedArrayBuffers

這樣,ArrayBuffer 基本上就像原始內存同樣。它模擬了使用 C 之類的語言進行的直接內存訪問。你可能想知道爲何咱們不讓程序直接訪問內存,而是添加了這種抽象層,由於直接訪問內存將致使一些安全漏洞

目前,咱們已經瞭解 Blob 與 ArrayBuffer,那麼它們以前有什麼區別呢?

2.4 Blob vs ArrayBuffer

ArrayBuffer 對象用於表示通用的,固定長度的原始二進制數據緩衝區。你不能直接操縱 ArrayBuffer 的內容,而是須要建立一個類型化數組對象或 DataView 對象,該對象以特定格式表示緩衝區,並使用該對象讀取和寫入緩衝區的內容。

Blob 類型的對象表示不可變的相似文件對象的原始數據。Blob 表示的不必定是 JavaScript 原生格式的數據。File 接口基於 Blob,繼承了Blob 功能並將其擴展爲支持用戶系統上的文件。

2.4.1 Blob 與 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 對象。

爲了便於你們理解轉換過程,阿寶哥簡單舉個相互轉換的示例:

2.4.2 Blob 轉換爲 ArrayBuffer
var blob = new Blob(["\x01\x02\x03\x04"]),
    fileReader = new FileReader(),
    array;

fileReader.onload = function() {
  array = this.result;
  console.log("Array contains", array.byteLength, "bytes.");
};

fileReader.readAsArrayBuffer(blob);
2.4.3 ArrayBuffer 轉 Blob
var array = new Uint8Array([0x01, 0x02, 0x03, 0x04]);
var blob = new Blob([array]);

2.5 DataView 與 ArrayBuffer

DataView 視圖是一個能夠從二進制 ArrayBuffer 對象中讀寫多種數值類型的底層接口,使用它時,不用考慮不一樣平臺的字節序問題。

字節順序,又稱端序或尾序(英語:Endianness),在計算機科學領域中,指存儲器中或在數字通訊鏈路中,組成多字節的字的字節的排列順序。

字節的排列方式有兩個通用規則。例如,一個多位的整數,按照存儲地址從低到高排序的字節中,若是該整數的最低有效字節(相似於最低有效位)在最高有效字節的前面,則稱小端序;反之則稱大端序。在網絡應用中,字節序是一個必須被考慮的因素,由於不一樣機器類型可能採用不一樣標準的字節序,因此均按照網絡標準轉化。

例如假設上述變量 x 類型爲int,位於地址 0x100 處,它的值爲 0x01234567,地址範圍爲 0x100~0x103字節,其內部排列順序依賴於機器的類型。大端法從首位開始將是:0x100: 01, 0x101: 23,..。而小端法將是:0x100: 67, 0x101: 45,..

2.5.1 DataView 構造函數
new DataView(buffer [, byteOffset [, byteLength]])

相關的參數說明以下:

  • buffer:一個已經存在的 ArrayBuffer 或 SharedArrayBuffer 對象,DataView 對象的數據源。
  • byteOffset(可選):此 DataView 對象的第一個字節在 buffer 中的字節偏移。若是未指定,則默認從第一個字節開始。
  • byteLength:此 DataView 對象的字節長度。若是未指定,這個視圖的長度將匹配 buffer 的長度。

DataView 返回值

使用 new 調用 DataView 構造函數後,會返回一個表示指定數據緩存區的新 DataView 對象。你能夠把返回的對象想象成一個二進制字節緩存區 array buffer 的 「解釋器」 —— 它知道如何在讀取或寫入時正確地轉換字節碼。這意味着它能在二進制層面處理整數與浮點轉化、字節順序等其餘有關的細節問題。

DataView 使用示例

const buffer = new ArrayBuffer(16);

// Create a couple of views
const view1 = new DataView(buffer);
const view2 = new DataView(buffer, 12, 4); //from byte 12 for the next 4 bytes
view1.setInt8(12, 42); // put 42 in slot 12

console.log(view2.getInt8(0)); // expected output: 42
2.5.2 DataView 屬性

全部 DataView 實例都繼承自 DataView.prototype,而且容許向 DataView 對象中添加額外屬性。

  • DataView.prototype.buffer(只讀):指向建立 DataView 時設定的 ArrayBuffer 對象;
  • DataView.prototype.byteLength(只讀):表示 ArrayBuffer 或 SharedArrayBuffer 對象的字節長度;
  • DataView.prototype.byteOffset(只讀):表示從 ArrayBuffer 讀取時的偏移字節長度。
2.5.3 DataView 方法

DataView 對象提供了 getInt8()、getUint8()、setInt8() 和 setUint8() 等方法來操做數據。具體每一個方法的使用,咱們就不詳細介紹。這裏咱們來看個簡單的例子:

const buffer = new ArrayBuffer(16);
const view = new DataView(buffer, 0);

view.setInt8(1, 68);
view.getInt8(1); // 68

介紹完 ArrayBuffer、TypedArray 和 DataView 的相關知識,阿寶哥用一張圖來總結一下它們之間的關係。

好的,下面咱們立刻進入下一個環節。

3、圖片灰度化

要對圖片進行灰度化處理,咱們就須要操做圖片像素數據。那麼問題來了,咱們應該如何獲取圖片的像素數據呢?

3.1 getImageData 方法

針對上述問題,咱們能夠利用 CanvasRenderingContext2D 提供的 getImageData 來獲取圖片像素數據,其中 getImageData() 返回一個 ImageData 對象,用來描述 canvas 區域隱含的像素數據,這個區域經過矩形表示,起始點爲(sx, sy)、寬爲 sw、高爲 sh。其中 getImageData 方法的語法以下:

ctx.getImageData(sx, sy, sw, sh);

相應的參數說明以下:

  • sx:將要被提取的圖像數據矩形區域的左上角 x 座標。
  • sy:將要被提取的圖像數據矩形區域的左上角 y 座標。
  • sw:將要被提取的圖像數據矩形區域的寬度。
  • sh:將要被提取的圖像數據矩形區域的高度。

3.2 putImageData 方法

在獲取到圖片的像素數據以後,咱們就能夠對獲取的像素數據進行處理,好比進行灰度化或反色處理。當完成處理後,若要在頁面上顯示處理效果,則咱們須要利用 CanvasRenderingContext2D 提供的另外一個 API —— putImageData

該 API 是 Canvas 2D API 將數據從已有的 ImageData 對象繪製到位圖的方法。 若是提供了一個繪製過的矩形,則只繪製該矩形的像素。此方法不受畫布轉換矩陣的影響。putImageData 方法的語法以下:

void ctx.putImageData(imagedata, dx, dy);
void ctx.putImageData(imagedata, dx, dy, dirtyX, dirtyY, dirtyWidth, dirtyHeight);

相應的參數說明以下:

  • imageData: ImageData ,包含像素值的數組對象。
  • dx:源圖像數據在目標畫布中的位置偏移量(x 軸方向的偏移量)。
  • dy:源圖像數據在目標畫布中的位置偏移量(y 軸方向的偏移量)。
  • dirtyX(可選):在源圖像數據中,矩形區域左上角的位置。默認是整個圖像數據的左上角(x 座標)。
  • dirtyY(可選):在源圖像數據中,矩形區域左上角的位置。默認是整個圖像數據的左上角(y 座標)。
  • dirtyWidth(可選):在源圖像數據中,矩形區域的寬度。默認是圖像數據的寬度。
  • dirtyHeight(可選):在源圖像數據中,矩形區域的高度。默認是圖像數據的高度。

3.3 圖片灰度化處理

介紹完 getImageData()putImageData() 方法,下面咱們來看一下具體如何利用它們實現圖片灰度化:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>獲取遠程圖片並灰度化</title>
  </head>
  <body>
    <h3>阿寶哥:獲取遠程圖片並灰度化示例</h3>
    <div>
      <button id="grayscalebtn">灰度化</button>
      <div style="display: flex;">
        <div style="flex: 50%;">
          <p>預覽容器</p>
          <img
            id="previewContainer"
            width="230"
            height="230"
            style="border: 2px dashed blue;"
          />
        </div>
        <div style="flex: 50%;">
          <p>Canvas容器</p>
          <canvas
            id="canvas"
            width="230"
            height="230"
            style="border: 2px dashed grey;"
          ></canvas>
        </div>
      </div>
    </div>
    <script>
      const image = document.querySelector("#previewContainer");
      const canvas = document.querySelector("#canvas");

      fetch("https://avatars3.githubusercontent.com/u/4220799")
        .then((response) => response.blob())
        .then((blob) => {
          const objectURL = URL.createObjectURL(blob);
          image.src = objectURL;
          image.onload = () => {
            draw();
          };
        });

      function draw() {
        const ctx = canvas.getContext("2d");
        ctx.drawImage(image, 0, 0, 230, 230);
        const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
        const data = imageData.data;

        const grayscale = function () {
          for (let i = 0; i < data.length; i += 4) {
            const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
            data[i] = avg; // red
            data[i + 1] = avg; // green
            data[i + 2] = avg; // blue
          }
          ctx.putImageData(imageData, 0, 0);
        };
        const grayscalebtn = document.querySelector("#grayscalebtn");
        grayscalebtn.addEventListener("click", grayscale);
      }
    </script>
  </body>
</html>

在以上示例中,咱們先從 Github 上下載阿寶哥的頭像,而後先進行本地預覽,接着調用 draw() 方法把獲取的圖像繪製到頁面的 Canvas 容器中,同時爲灰度化按鈕綁定監聽事件。

當用戶點擊灰度化按鈕時,將會觸發灰度化處理函數,在該函數內部會對經過 ctx.getImageData() 方法獲取的圖片像素進行灰度化處理,處理完成後再經過 ctx.putImageData() 方法把處理過的像素數據更新到 Canvas 上。

以上代碼成功運行後,最終的灰度化效果以下圖所示:

abao-avatar-gray-scale.jpg

4、圖片壓縮

在一些場合中,咱們但願在上傳本地圖片時,先對圖片進行必定的壓縮,而後再提交到服務器,從而減小傳輸的數據量。在前端要實現圖片壓縮,咱們能夠利用 Canvas 對象提供的 toDataURL() 方法,該方法接收 typeencoderOptions 兩個可選參數。

其中 type 表示圖片格式,默認爲 image/png。而 encoderOptions 用於表示圖片的質量,在指定圖片格式爲 image/jpegimage/webp 的狀況下,能夠從 0 到 1 的區間內選擇圖片的質量。若是超出取值範圍,將會使用默認值 0.92,其餘參數會被忽略。

下面咱們來看一下如何對前面已進行灰度化處理的圖片進行壓縮。

<button id="compressbtn">圖片壓縮</button>
<div style="display: flex;">
   <div style="flex: 33.3%;">
      <p>預覽容器</p>
      <img id="previewContainer" width="230" height="230"
         style="border: 2px dashed blue;" />
   </div>
   <div style="flex: 33.3%;">
      <p>Canvas容器</p>
      <canvas id="canvas" width="230" height="230"
         style="border: 2px dashed grey;">
      </canvas>
   </div>
   <div style="flex: 33.3%;">
      <p>壓縮預覽容器</p>
      <img id="compressPrevContainer" width="230" height="230"
         style="border: 2px dashed green;" />
   </div>
</div>

<script>
   const compressbtn = document.querySelector("#compressbtn");
   const compressImage = document.querySelector("#compressPrevContainer");
   compressbtn.addEventListener("click", compress);
  
   function compress(quality = 80, mimeType = "image/webp") {
     const imageDataURL = canvas.toDataURL(mimeType, quality / 100);
     compressImage.src = imageDataURL;
   }
</script>

在以上代碼中,咱們設默認的圖片質量是 0.8,而圖片類型是 image/webp 類型。當用戶點擊壓縮按鈕時,則會調用 Canvas 對象的 toDataURL() 方法實現圖片壓縮。 以上代碼成功運行後,最終的處理效果以下圖所示:

其實 Canvas 對象除了提供 toDataURL() 方法以外,它還提供了一個 toBlob() 方法,該方法的語法以下:

canvas.toBlob(callback, mimeType, qualityArgument)

toDataURL() 方法相比,toBlob() 方法是異步的,所以多了個 callback 參數,這個 callback 回調方法默認的第一個參數就是轉換好的 blob文件信息。

5、圖片上傳

在獲取壓縮後圖片對應的 Data URL 數據以後,能夠把該數據直接提交到服務器。針對這種情形,服務端須要作一些相關處理,才能正常保存上傳的圖片,這裏以 Express 爲例,具體處理代碼以下:

const app = require('express')();

app.post('/upload', function(req, res){
    let imgData = req.body.imgData; // 獲取POST請求中的base64圖片數據
    let base64Data = imgData.replace(/^data:image\/\w+;base64,/, "");
    let dataBuffer = Buffer.from(base64Data, 'base64');
    fs.writeFile("abao.png", dataBuffer, function(err) {
        if(err){
          res.send(err);
        }else{
          res.send("圖片上傳成功!");
        }
    });
});

然而對於返回的 Data URL 格式的圖片數據通常都會比較大,爲了進一步減小傳輸的數據量,咱們能夠把它轉換爲 Blob 對象:

function dataUrlToBlob(base64, mimeType) {
  let bytes = window.atob(base64.split(",")[1]);
  let ab = new ArrayBuffer(bytes.length);
  let ia = new Uint8Array(ab);
  for (let i = 0; i < bytes.length; i++) {
    ia[i] = bytes.charCodeAt(i);
  }
  return new Blob([ab], { type: mimeType });
}

在轉換完成後,咱們就能夠壓縮後的圖片對應的 Blob 對象封裝在 FormData 對象中,而後再經過 AJAX 提交到服務器上:

function uploadFile(url, blob) {
  let formData = new FormData();
  let request = new XMLHttpRequest();
  formData.append("imgData", blob);
  request.open("POST", url, true);
  request.send(formData);
}

6、阿寶哥有話說

6.1 如何查看圖片的二進制數據

要查看圖片對應的二進制數據,咱們就須要藉助一些現成的編輯器,好比 Windows 平臺下的 WinHex 或 macOS 平臺下的 Synalyze It! Pro 十六進制編輯器。這裏咱們使用 Synalyze It! Pro 這個編輯器,以十六進制的形式來查看阿寶哥頭像對應的二進制數據。

6.2 如何區分圖片的類型

計算機並非經過圖片的後綴名來區分不一樣的圖片類型,而是經過 「魔數」(Magic Number)來區分。 對於某一些類型的文件,起始的幾個字節內容都是固定的,根據這幾個字節的內容就能夠判斷文件的類型。

常見圖片類型對應的魔數以下表所示:

文件類型 文件後綴 魔數
JPEG jpg/jpeg 0xFFD8FF
PNG png 0x89504E47
GIF gif 0x47494638(GIF8)
BMP bmp 0x424D

這裏咱們以阿寶哥的頭像(abao.png)爲例,驗證一下該圖片的類型是否正確:

在平常開發過程當中,若是遇到檢測圖片類型的場景,咱們能夠直接利用一些現成的第三方庫。好比,你想要判斷一張圖片是否爲 PNG 類型,這時你可使用 is-png 這個庫,它同時支持瀏覽器和 Node.js,使用示例以下:

Node.js

// npm install read-chunk
const readChunk = require('read-chunk'); 
const isPng = require('is-png');
const buffer = readChunk.sync('unicorn.png', 0, 8);

isPng(buffer);
//=> true

Browser

(async () => {
    const response = await fetch('unicorn.png');
    const buffer = await response.arrayBuffer();

    isPng(new Uint8Array(buffer));
    //=> true
})();

6.3 如何獲取圖片的尺寸

圖片的尺寸、位深度、色彩類型和壓縮算法都會存儲在文件的二進制數據中,咱們繼續以阿寶哥的頭像(abao.png)爲例,來了解一下實際的狀況:

528(十進制) => 0x0210(十六進制)

560(十進制)=> 0x0230(十六進制)

所以若是想要獲取圖片的尺寸,咱們就須要依據不一樣的圖片格式對圖片二進制數據進行解析。幸運的是,咱們不須要本身實現該功能,image-size 這個 Node.js 庫已經幫咱們實現了獲取主流圖片類型文件尺寸的功能,使用示例以下:

同步方式

var sizeOf = require('image-size');

var dimensions = sizeOf('images/abao.png');
console.log(dimensions.width, dimensions.height);

異步方式

var sizeOf = require('image-size');

sizeOf('images/abao.png', function (err, dimensions) {
  console.log(dimensions.width, dimensions.height);
});

image-size 這個庫功能仍是蠻強大的,除了支持 PNG 格式以外,還支持 BMP、GIF、ICO、JPEG、SVG 和 WebP 等格式。

6.4 如何解碼 PNG 圖片中的像素數據

相信小夥們平時也聽過圖片解碼、音視頻解碼。解碼 PNG 圖片就是把一張圖片從二進制數據轉換成包含像素數據的 ImageData。前面咱們已經講過,能夠利用 CanvasRenderingContext2D 提供的 getImageData() 方法來獲取圖片像素數據。

那麼 getImageData() 方法內部是如何處理的呢?下面咱們來簡單介紹一下大體流程,這裏咱們以一張 2px * 2px 的圖片爲例,下圖是放大展現的效果:

pixels-large.png

(圖片來源:https://vivaxyblog.github.io/...

一樣,咱們先使用 Synalyze It! Pro 十六進制編輯器打開上面的 2px * 2px 的圖片:

PNG 圖片的像素數據是保存在 IDAT 塊中,除了 IDAT 塊以外,還包含其餘的數據塊,完整的數據塊以下所示:

(圖片來源:https://dev.gameres.com/Progr...

在解析像素數據以前,咱們先了解像素數據是如何編碼的。每行像素都會先通過過濾函數處理,每行像素的過濾函數能夠不一樣。而後全部行的像素數據會通過 deflate 壓縮算法壓縮。這裏阿寶哥使用 pako 這個庫進行解碼操做:

const pako = require("pako");

const compressed = new Uint8Array([120, 156, 99, 16, 96, 216, 0, 0, 0, 228, 0, 193]);
try {
  const result = pako.inflate(compressed);
  console.dir(result);
} catch (err) {
  console.log(err);
}

在以上代碼中,經過調用 pako.inflate() 方法執行解壓操做,最終的解壓後的像素數據以下:

Uint8Array [ 0, 16, 0, 176 ]

獲取解壓的像素數據以後,還要解碼掃描線,而後根據色板中的索引,來還原出圖片的像素信息。這裏阿寶哥就不詳細展開了,感興趣的小夥伴能夠閱讀 一步一步解碼 PNG 圖片 這篇文章。

6.5 如何實現大文件分片上傳

File 對象是特殊類型的 Blob,且能夠用在任意的 Blob 類型的上下文中。因此針對大文件傳輸的場景,咱們可使用 slice 方法對大文件進行切割,而後分片進行上傳,具體示例以下:

const file = new File(["a".repeat(1000000)], "test.txt");

const chunkSize = 40000;
const url = "https://httpbin.org/post";

async function chunkedUpload() {
  for (let start = 0; start < file.size; start += chunkSize) {
      const chunk = file.slice(start, start + chunkSize + 1);
      const fd = new FormData();
      fd.append("data", chunk);

      await fetch(url, { method: "post", body: fd }).then((res) =>
        res.text()
      );
  }
}

6.6 如何實現文件下載

在一些場景中,咱們會經過 Canvas 進行圖片編輯或使用 jsPDFsheetjs 等一些第三方庫進行文檔處理,當文件文件處理完成後,咱們須要把文件下載並保存到本地。針對這些場景,咱們可使用純前端的方案實現文件下載。

「Talk is cheap」,阿寶哥來舉一個簡單的 Blob 文件下載的示例:

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Blob 文件下載示例</title>
  </head>

  <body>
    <button id="downloadBtn">文件下載</button>
    <script src="index.js"></script>
  </body>
</html>

index.js

const download = (fileName, blob) => {
  const link = document.createElement("a");
  link.href = URL.createObjectURL(blob);
  link.download = fileName;
  link.click();
  link.remove();
  URL.revokeObjectURL(link.href);
};

const downloadBtn = document.querySelector("#downloadBtn");
downloadBtn.addEventListener("click", (event) => {
  const fileName = "blob.txt";
  const myBlob = new Blob(["一文完全掌握 Blob Web API"], { type: "text/plain" });
  download(fileName, myBlob);
});

在示例中,咱們經過調用 Blob 的構造函數來建立類型爲 "text/plain" 的 Blob 對象,而後經過動態建立 a 標籤來實現文件的下載。在實際項目開發過程當中,咱們可使用成熟的開源庫,好比 FileSaver.js 來實現文件保存功能。

7、參考資源

相關文章
相關標籤/搜索