當前,前端對二進位制資料有許多的API可以使用,這豐富了前端對檔案資料的處理能力,有了這些能力,就能夠對圖片等檔案的資料進行各種處理。
本文將著重介紹一些前端二進位制資料處理相關的API知識,如Blob、File、FileReader、ArrayBuffer、TypeArray、DataView等等。
在介紹各種API之前,我們需要先了解下和位元組有關的知識。
我們知道,計算機是二進位制的世界,而位元組(byte)是計算機技術中關於二進位制資料的一種基本單位,1位元組有8個二進位制位,即8位元(bit)。
位元又叫位,一位二進位制資料要麼是0、要麼是1,只有兩種狀態,所以1位元有2種狀態。
1位元組有8位元,即8個二進位制位,那就能表示 2**8 = 256
種狀態,取值從 00000000 到 11111111。
位元組作為基本單位,在很多地方都被使用,如字元編碼知識,見前文前端需要了解的編碼知識。
二進位制資料在儲存的時候,以位元組為單位,這裡還涉及到一個關於位元組序的知識。
位元組序描述的是計算機如何儲存位元組。
因為我們知道,記憶體儲存都有索引地址,每個位元組對應一個索引地址。一個位元組儲存8位元二進位制,即0到255之間,但需要儲存大於255的數值的時候,就需要多個位元組,多個位元組就涉及到排序問題。
所以位元組序就是:當需要多個位元組表示一個值的時候,這多個位元組使用什麼樣的排序方式在記憶體中進行儲存。
而排序方式主要是兩種:大端儲存(big-endian)和小端儲存(little-endian)。
大端儲存又稱大位元組序、高位元組序,方式是低位位元組排在記憶體中的高地址端,高位元組位排放在記憶體中的低地址端。圖片檔案 png、jpg都是這種方式。
小端儲存又稱為小位元組序、低位元組序,方式是低位位元組排在記憶體中的低地址端,高位位元組排在記憶體中的高地址端。圖片檔案gif是小端序。
當我們使用不同的位元組序儲存數位 0x12345678
(這裡是16進位製表示,對應的十進位制:305419896。進位制相關知識可見前文Javascript中的進位制和進位制轉換:
大端儲存在記憶體中的儲存地址:
小端儲存在記憶體中的儲存地址:
這裡數位位元組的高-低位是從左到右,最高位是 12
,最低位是 78
;而記憶體中儲存時從左到右是低地址——高地址。
所以在大端序中高位位元組的 12
在記憶體最左邊的低地址位,而低位元組位 78
則在記憶體最右邊的高地址位;而小端序則正好相反。
從視覺習慣上,大端儲存似乎更順眼,但無論哪種方式,計算的結果都是一樣的,只是在計算的時候需要處理這個排序方式,下文會涉及到。
Blob,即 Binary large Object,本質上是一個二進位制物件,該物件表示的是一個不可變、原始資料的類檔案物件。
它的不可變,代表它是唯讀的,不可被改變。
Blob物件的建構函式語法:new Blob(array, options)
。
引數array:是一個資料陣列,可以是多種物件的資料,包含 ArrayBuffer、Blob、String 等等。
引數options:可選物件,指定兩個屬性:
type
表示Blob物件資料的MIME型別;
endings
指定包含行結束符\n的字串如何寫入。
我們可以使用建構函式直接建立一個新的 Blob 物件:
const blob = new Blob(['123456789'], {type : 'text/plain'});
新建立的物件範例,結構如下:
從以上範例,我們就可以看到Blob物件的方法和屬性:
contentType
可以為新Blob物件指定自己的MIME型別可以針對上面的 blob
範例進行操作:
blob.slice(0, 3).text().then(res => {
console.log(res)
})
// 結果:123
以上程式碼,使用slice()方法獲取原blob的前三位的資料,生成新的Blob範例後,通過text()方法列印出文字內容。
下面可以看看Blob在介面請求中的應用,Fetch API中的 Response
物件,擁有一個blob方法,能夠得到Blob物件。
const imgRequst = new Request('11.jpg')
fetch(imgRequst).then((response) => {
return response.blob()
}).then((mBlob) => {
console.log(mBlob)
})
通過以上程式碼,請求一個jpg圖片檔案,響應物件通過 blob()
方法轉為Blob物件:
File物件繼承了Blob物件,是一種特殊型別的Blob,它擴充套件了對系統檔案的支援能力。
File提供檔案資訊,並能夠在javascript中進行存取,一般在使用 <input>
標籤選擇檔案時返回,因為 <input>
標籤允許選擇多個檔案,這裡返回的是檔案列表 files
。
除了 <input>
標籤以外,還有兩種方式返回File物件:
DataTransfer
物件。FileSystemFileHandle
物件的 getFile()
方法。File的建構函式:new File(bits, name[, options])
。
有三個引數:
下面程式碼,通過 <input>
標籤讀取檔案:
<input id="input-file" type="file" accept="image/*" />
document.getElementById('input-file').onchange = (e) => {
const file = e.target.files[0]
console.log(file)
// ...
}
這是一個簡單的圖片上傳,獲取到的file範例,控制檯列印出來:
通過上圖(chrome瀏覽器下),可以看到File繼承了Blob的素有屬性和方法:
File繼承自Blob,都是唯讀物件,除了使用slice分片以外,並沒有其他操作能力,所以如果對它們進行處理需要藉助其他的API。
主要用於操作Blob的API有:FileReader、URL.createObjectURL()、createImageBitmap()和XMLHttpRequest.send()。下面將介紹這幾種方式。
Blob和File都是
WebAPI
,是由瀏覽器環境提供的,而上面提到這四種物件也同樣是WebAPI。
FileReader是用於非同步讀取檔案型別(或原始資料緩衝區)的內容,指定Blob或File物件為需要讀取的檔案資料。
FileReader 不能在檔案系統中用路徑名的方式讀取檔案。
建構函式:new FileReader()
。
如果對檔案處理功能開發較多,對FileReader物件應該較熟,我們先看一個範例:
document.getElementById('input-file').onchange = (e) => {
const file = e.target.files[0]
const reader = new FileReader()
reader.onload = async (event) => {
const img = new Image()
img.src = event.target.result
}
reader.readAsDataURL(file)
}
以上程式碼,就是很常用的,使用FileReader讀取一個圖片檔案的Base64資料,然後使用圖片物件載入。Base64知識,可參考前文深入理解Base64編碼字串。
這段程式碼也涉及到FileReader對像的屬性、事件、方法。
常數名 | 值 | 狀態描述 |
---|---|---|
EMPTY | 0 | 沒有載入 |
LOADING | 1 | 正在載入 |
DONE | 2 | 已完成全部讀取 |
URL是瀏覽器環境提供的,用於處理url連結的一個介面物件。可以通過它,解析、構造、規範和編碼各種url連結。
而URL提供的一個靜態方法 createObjectURL()
,可以用來處理Blob和File檔案物件。
先看一個例子:
document.getElementById('input-file').onchange = (e) => {
const file = e.target.files[0]
const url = URL.createObjectURL(file)
const img = new Image()
img.onload = () => {
document.body.append(img)
}
img.src = url
}
頁面展示:
這段程式碼就實現了上傳圖片,通過 URL.createObjectURL
讀取後生成一個本地對映的url,再使用Image物件載入圖片。
通過檢視頁面元素,可以看到新新增的圖片元素,它的src是一個類似連結的字串:blob:http://localhost:8088/29c8f4a5-9b47-436f-8983-03643c917f1c
,通過這個字串,圖片就能載入顯示出來。
再來看 createObjectURL()
,它返回一個包含給定的Blob或File物件的url,就可以當做檔案資源被載入。而這個url的生命週期和它的視窗同步,視窗關閉這個url就自動釋放了。
這個url就是被稱為偽協定的Objct URL。
Object URL 又被稱為Blob URL,一般使用Blob或File物件生成,通過 URL.createObjectURL()
方法建立一個唯一的URL。
Object URL的格式為:blob:origin/唯一標識(uuid)
。
上面生成的URL字串就符合這個格式:blob:http://localhost:8088/29c8f4a5-9b47-436f-8983-03643c917f1c
。
http://localhost:8088/
,如果直接開啟本地html檔案,則origin為null。29c8f4a5-9b47-436f-8983-03643c917f1c
。瀏覽器內部會為生成Object URL保持一個 URL
到 Blob
的對映,Blob是留存在記憶體中,瀏覽器只有在解除安裝當前視窗檔案時才會釋放。
如果要手動釋放,則需要URL的另外一個靜態方法:URL.revokeObjectURL()
,它用於銷燬之前建立的URL範例,在合適的時機呼叫即可銷燬Object URL。
URL.revokeObjectURL(url)
XMLHttpRequest.send(body):用於在XHR的HTTP請求中,傳送資料體。
這裡的body引數,可以是多種資料型別,包括Blob物件。
const xhr = new XMLHttpRequest()
xhr.send(new Blob())
createImageBitmap(): 主要處理圖片資源,接受不同的圖片資源物件為引數,並生成一個ImageBitmap物件。
這些引數就就可以是Blob和File物件。
ImageBitmap表示可以繪製在canvas上的點陣圖影象。
createImageBitmap(file).then(imageBitmap => {
const canvas = document.createElement('canvas')
canvas.width = imageBitmap.width
canvas.height = imageBitmap.height
const ctx = canvas.getContext('2d')
ctx.drawImage(imageBitmap, 0, 0)
document.body.append(canvas)
})
如上程式碼,即可讀取圖片檔案,使用canvas繪製。
ArrayBuffer 物件表示通用的、固定長度的原始二進位制緩衝區,它是一個位元組陣列,但不能直接操作它的內容,而需要通過其他方式(如TypeArray或DataView等)進行處理。
建構函式:new ArrayBuffer(length)
,返回一個指定大小的ArrayBuffer物件。
引數length:要建立的 ArrayBuffer 的位元組大小。大於Number.MAX_SAFE_INTEGER(>= 2 ** 53)或為負數,則丟擲一個RangeError異常。
下面我們先使用前面介紹的 FileReader
讀取一個檔案的ArrayBuffer內容:
document.getElementById('input-file').onchange = (e) => {
const file = e.target.files[0]
const reader = new FileReader()
reader.onload = async (event) => {
console.log(event.target.result)
}
reader.readAsArrayBuffer(file)
}
控制檯紀錄檔列印輸出:
從上圖,可以看到ArrayBuffer的範例屬性和方法:
ArrayBuffer還有靜態屬性和方法:
由於我們無法直接操作ArrayBuffer,所以需要使用其他物件來處理,下面將介紹其中兩種。
TypeArray,即型別化陣列,它描述了二進位制資料緩衝區的一個類陣列。TypeArray本身不是一個可用的物件,只是一個輔助的資料型別,作為所有型別陣列的構造原型,真正可用的型別陣列包含了多種,如Int8Array、Uint8Array等。
常用的型別陣列如下表所示:
物件 | 元素所佔位元組數 | 取值範圍 | 描述 |
---|---|---|---|
Int8Array | 1 | -128 - 127 | 8 位有符號整型陣列 |
Uint8Array | 1 | 0 - 255 | 8 位無符號整型陣列 |
Uint8ClampedArray | 1 | 0 - 255 | 8 位無符號整型固定陣列 |
Int16Array | 2 | -32768 - 32767 | 16 位有符號整型陣列 |
Uint16Array | 2 | 0 - 65535 | 16 位無符號整型陣列 |
Int32Array | 4 | -2147483648 - 2147483647 | 32 位有符號整型陣列 |
Uint32Array | 4 | 0 - 4294967295 | 32 位無符號整型陣列 |
Float32Array | 4 | 1.2×10**-38 to 3.4×10**38 |
32 位浮點數型陣列 |
Float64Array | 8 | 5.0×10**-324 to 1.8×10**308 |
64 位浮點數型陣列 |
BigInt64Array | 8 | -2**63 to 2**63-1 |
64 位有符號數型陣列 |
BigUint64Array | 8 | 0 to 2**64-1 |
64 位無符號整型陣列 |
型別化陣列與普通資料也較相似,同樣擁有一系列的方法和屬性,但不支援 push
、pop
、shift
、unshift
、splice
等可以改變原陣列的增刪改方法。
型別化陣列由於定義了資料型別,則各元素必須是同型別的資料,不能像普通資料那樣元素可以是不同型別;當元素資料型別固定統一時,處理效率更優。
各型別陣列在建構函式、屬性、方法等語法上相同,下面就以 Uint8Array
為例。
Uint8Array建構函式:
new Uint8Array()
new Uint8Array(length)
new Uint8Array(typedArray)
new Uint8Array(object)
new Uint8Array(buffer [, byteOffset [, length]])
length 引數的最大取值
8 位類陣列是 2145386496
16 位類陣列是 1072693248
32 位類陣列是 536346624
32 位類陣列是 268173312
Uint8Array
,Uint32Array型別返回 Uint32Array
等等介紹完靜態屬性和方法,下面通過一個範例,來檢視下Uint8Array的範例屬性和方法,程式碼如下。
const reader = new FileReader()
reader.onload = async (event) => {
const aBuffer = event.target.result
const uint8Array = new Uint8Array(aBuffer)
console.log(uint8Array)
}
reader.readAsArrayBuffer(file)
以上程式碼,直接讀取檔案的ArrayBuffer資料,然後通過 Uint8Array
建構函式,得到Uint8Array範例,控制檯檢視:
通過載入一張png圖片,得到它的Uint8Array陣列資料,可以看到型別陣列大部分的屬性和方法都和普通陣列類似,除了前文提到的增刪改陣列的方法以外。因此,對型別陣列使用下標、迴圈等等方式進行讀取,和普通函數沒什麼兩樣。
而型別陣列也自己的特殊屬性(都唯讀)和方法,如下:
要了解常見型別陣列間的關係,我們先看下面這張圖:
圖上所示,是一張png圖片的ArrayBuffer資料,可以看到,ArrayBuffer的位元組長度屬性預設取8位元整型陣列的長度,即與Int8Array和Uint8Array的長度一致。
而Int8Array的長度29848,正好是Int16Array的長度14924的兩倍,是Int32Array的長度7462的四倍,可知,這裡就是對位元組的合併計算:
型別陣列通過陣列的方式對ArrayBuffer的內容進行讀取操作,可以方便我們處理檔案的二進位制資料。
但使用型別陣列的時候,碰到多位元組的資料時,需要考慮位元組序的問題。
下面,我們以讀取小端儲存的GIF圖片為例。
GIF圖片的Uint8Array陣列資料中,寬高資料的儲存就是使用了兩個位元組,第7-8位元儲存圖片的寬度,9-10位儲存圖片的高度。
我們載入的GIF圖片寬高皆為600,需要處理位元組序,程式碼如下:
const uint8Array = new Uint8Array(aBuffer)
let bufferIndex = 6
// 獲取GIF寬度的兩個位元組的值
const width1 = uint8Array[bufferIndex]
// width1 結果:88
const width2 = uint8Array[bufferIndex + 1]
// width2 結果:2
// 得到各自的16進位制資料
const width1hex = width1.toString(16)
const width2hex = width2.toString(16)
// 轉換成實際的寬度大小,注意這裡把兩個位元組的順序做了調整,符合小端序
const width = parseInt(width2hex + width1hex, 16)
// width 結果:600
使用小端序處理後,寬度結果等於600,符合圖片實際寬度。
自己手動處理位元組序會稍顯麻煩,如果不想手動去處理位元組序的問題,可以使用另外一個物件:DataView
。
DataView 是一個從 ArrayBuffer
中讀取多種型別數值並且不用考慮位元組序的介面物件。它的使用簡單方便,擁有一系列的 get-
和 set-
實體方法運算元據。
DataView的建構函式:new DataView(buffer [, byteOffset [, byteLength]])
。
引數:
DataView不用考慮位元組序,同樣是讀取GIF的寬度時,程式碼可簡化:
const fileDataView = new DataView(arrBuffer)
let bufferIndex = 6
const width = fileDataView.getUint16(bufferIndex, true)
// 結果:600
bufferIndex += 2
const height = fileDataView.getUint16(bufferIndex, true)
// 結果:600
以上程式碼,很方便就得到GIF圖片的寬高資料(600),因為使用了 DataView
和它的 getUint16
方法,不需要手動處理位元組序。
getUint16
方法有兩個引數:第一個引數代表位元組索引;第二參數列示位元組序,預設大端序,為true則是小端序,GIF是小端,所以上面程式碼為true。
除了getUint16
以外,DataView
還有十多個類似的實體方法。
get系列方法通過位元組偏移索引獲取對應的數值,其中多位元組的資料,需要兩個引數:
名稱 | 引數 | 描述 |
---|---|---|
getInt8 | (byteOffset) | 有符號 8-bit 整數(1個位元組) |
getUint8 | (byteOffset) | 無符號 8-bit 整數(1個位元組) |
getInt16 | (byteOffset [, littleEndian]) | 16-bit數(短整型,2個位元組) |
getUint16 | (byteOffset [, littleEndian]) | 16-bit數(無符號短整型,2個位元組) |
getInt32 | (byteOffset [, littleEndian]) | 32-bit數(長整型,4個位元組) |
getUint32 | (byteOffset [, littleEndian]) | 32-bit數(無符號長整型,4個位元組) |
getFloat32 | (byteOffset [, littleEndian]) | 32-bit浮點數(單精度浮點數,4個位元組) |
getFloat64 | (byteOffset [, littleEndian]) | 64-bit數(雙精度浮點型,8個位元組) |
getBigInt64 | (byteOffset [, littleEndian]) | 帶符號的64位元整數(long long型別)值 |
getBigUint64 | (byteOffset [, littleEndian]) | 無符號的64位元整數(unsigned long long型別)值 |
set系列方法是和get方法對應的,處理相應位元組偏移索引位置的數值,引數如下:
名稱 | 引數 | 描述 |
---|---|---|
setInt8 | (byteOffset, value) | 8-bit數(一個位元組) |
setUint8 | (byteOffset, value) | 8-bit數(無符號位元組) |
setInt16 | (byteOffset, value [, littleEndian]) | 16-bit數(短整型) |
setUint16 | (byteOffset, value [, littleEndian]) | 16-bit數(無符號短整型) |
setInt32 | (byteOffset, value [, littleEndian]) | 32-bit數(長整型) |
setUint32 | (byteOffset, value [, littleEndian]) | 32-bit數(無符號長整型) |
setFloat32 | (byteOffset, value [, littleEndian]) | 32-bit數(浮點型) |
setFloat64 | (byteOffset, value [, littleEndian]) | 64-bit數(雙精度浮點型) |
setBigInt64 | (byteOffset, value [, littleEndian]) | 帶符號的64位元整數(long long型別)值 |
setBigUint64 | (byteOffset, value [, littleEndian]) | 無符號的64位元整數(unsigned long long型別)值 |
對於Blob和ArrayBuffer兩個物件,我們可以稍做總結:
Blob和ArrayBuffer之間的轉換:
如下程式碼:
const aBuffer = new ArrayBuffer(4)
// 使用Blob建構函式
const blob = new Blob([aBuffer])
// Blob的arrayBuffer()方法(promise)
blob.arrayBuffer()
// FileReader
const reader = new FileReader()
reader.readAsArrayBuffer(blob)