JavaScript 提供了一些 API 來處理文件或原始文件數據,例如:File、Blob、FileReader、ArrayBuffer、base64 等。下面就來看看它們都是如何使用的,它們之間又有何區別和聯繫!
ArrayBuffer
ArrayBuffer 對象用來表示通用的、固定長度的原始二進制數據緩衝區,是內存中一段固定長度的連續數據存儲區的引用,你無法直接操作或修改它,只能通過 DataView 對象或 TypedArrray 對象來訪問。這些對象用於讀取和寫入緩衝區內容。
ArrayBuffer不是一個Array類型,如果想要判斷其類型,可以使用
toString.call(new ArrayBuffer()) === '[object, ArrayBuffer]'
ArrayBuffer 本身就是一個黑盒,不能直接讀寫所存儲的數據,需要藉助以下視圖對象來讀寫:
- TypedArray:用來生成內存的視圖,通過9個構造函數,可以生成9種數據格式的視圖。
- DataViews:用來生成內存的視圖,可以自定義格式和字節序。
TypedArray
首先要弄清楚 TypedArray 的概念, 這是 ES2015(又稱ES6) 中新出的一個接口, 不能直接被實例化, 也就是説如下代碼會報錯。
new TypedArray()
因為這個接口就是一個抽象接口, 就像java中的抽象接口一樣, 是不能被實例化的, 只能實例化實現該接口的子類. Uint8Array 就是實現 TypedArray 接口的一個子類。
就 Nodejs 而言, 可以使用 Buffer 操作二進制數據, 那對前端 JS 而言, 在 TypeArray 出現之前, 是沒有可以直接操作二進制數據的類的, 這也與前端很少需要操作二進制數據相關。
所以 TypeArray 接口的作用是操作二進制數據。
TypeArray 是一個類數組結構, 也就是説數組可以用的函數, 比如 arr[0], slice, copy 等方法, TypeArray 也可以使用。
值編碼
所有的類型化數組都是基於 ArrayBuffer 進行操作的,你可以藉此觀察到每個元素的確切字節表示,因此二進制格式中的數字編碼方式具有重要意義。
- 無符號整數數組(Uint8Array、Uint16Array、Uint32Array 和 BigUint64Array)直接以二進制形式存儲數字。
- 有符號整數數組(Int8Array、Int16Array、Int32Array 和 BigInt64Array)使用二進制補碼存儲數字。
- 浮點數組(Float32Array 和 Float64Array)使用 IEEE 754浮點格式存儲數字。Number 參考文檔中有關於確切格式的更多信息。JavaScript 數字默認使用雙精度浮點格式,這與 Float64Array 相同。Float32Array 將 23(而不是 52)位用於尾數,以及 8(而不是 11)位用於指數。請注意,規範要求所有的 NaN 值使用相同的位編碼,但確切的位模式取決於實現。
- Uint8ClampedArray 是一種特殊情況。它像 Uint8Array 一樣以二進制形式存儲數字,但是當你存儲超出範圍的數字時,它會將數字鉗制(clamp)到 0 到 255 的範圍內,而不是截斷最高有效位。
除了 Int8Array、Unit8Array 和 Uint8ClampedArray 以外的其他類型數組都將每個元素存儲為多個字節。這些字節可以按照從最高有效位到最低有效位(大端序)或從最低有效位到最高有效位(小端序)的順序進行排序。請參閲字節序以瞭解更多。類型化數組始終使用平台的本機字節順序。如果要在緩衝區中寫入和讀取時指定字節順序,應該使用 DataView。
DataView
DataView 視圖是一個可以從二進制 ArrayBuffer 對象中讀寫多種數值類型的底層接口,使用它時,不用考慮不同平台的字節序(endianness)問題。
DataView 訪問器(accessor)提供了對如何訪問數據的明確控制,而不管執行代碼的計算機的字節序如何。
// dataview.setInt16(byteOffset, value [, littleEndian])
const littleEndian = (() => {
const buffer = new ArrayBuffer(2);
new DataView(buffer).setInt16(0, 256, true /* 小端對齊 */);
// Int16Array 使用平台的字節序。
return new Int16Array(buffer)[0] === 256;
})();
console.log(littleEndian); // true 或 false
TypedArrray 與 DataView 區別
TypedArray 視圖的字節順序與底層的計算機體系結構有關。在大多數計算機體系結構中,包括 x86 架構的處理器,字節順序是 Little Endian。因此,當使用 TypedArray 視圖時,它們默認採用 Little Endian 字節順序。
然而,並非所有計算機體系結構都使用 Little Endian 字節順序。例如,某些 ARM 架構的處理器使用的是 Big Endian 字節順序。在這些體系結構上,TypedArray 視圖會自動適應相應的字節順序。
因此,需要注意的是,儘管 TypedArray 視圖在大多數情況下默認採用與機器相關的字節順序(通常是 Little Endian),但具體的字節順序仍取決於底層的計算機體系結構。如果需要確保特定的字節順序,可以使用 DataView 視圖並顯式指定字節順序。
不深入討論二進制數據的工作原理,讓我們看一個簡單的例子:
var buffer = new ArrayBuffer(2) // array buffer for two bytes
var bytes = new Uint8Array(buffer) // views the buffer as an array of 8 bit integers
bytes[0] = 65 // ASCII for 'A'
bytes[1] = 66 // ASCII for 'B'
現在我們可以將其轉換為 Blob 對象,從中創建一個 Data URI,並將其作為一個新的文本文件打開:
var blob = new Blob([buffer], {type: 'text/plain'})
var dataUri = window.URL.createObjectURL(blob)
window.open(dataUri)
這將在一個新的瀏覽器窗口中顯示文本 'AB'。
你可以看到在前面的例子中,我們先寫入了表示 'A' 的字節,然後是表示 'B' 的字節,但我們也可以使用 Uint16Array,將這兩個字節一次性寫入一個 16 位的數字中:
var buffer = new ArrayBuffer(2) // array buffer for two bytes
var word = new Uint16Array(buffer) // views the buffer as an array with a single 16 bit integer
var value = (65 << 8) + 66 // we shift the 'A' into the upper 8 bit and add the 'B' as the lower 8 bit.
word[0] = value // write the 16 bit (2 bytes) into the typed array
// Let's create a text file from them:
var blob = new Blob([buffer], {type: 'text/plain'})
var dataUri = window.URL.createObjectURL(blob)
window.open(dataUri)
但等等?我們看到的是"BA"而不是之前的"AB"!發生了什麼?
讓我們仔細看一下我們寫入數組的值:
65 decimal = 01 00 00 01 binary
66 decimal = 01 00 00 10 binary
// what we did when we wrote into the Uint8Array:
01 00 00 01 01 00 00 10
<bytes[0]-> <bytes[1]->
// what we did when we created the 16-bit number:
var value = (01 00 00 01 00 00 00 00) + 01 00 00 10
= 01 00 00 01 01 00 00 10
你可以看到我們寫入 Uint8Array 和 Uint16Array 的 16 位數值是相同的,那為什麼結果會不同呢?
答案是,一個超過一個字節的值的字節順序取決於系統的字節序(大小端序)。讓我們來驗證一下:
var buffer = new ArrayBuffer(2)
// create two typed arrays that provide a view on the same ArrayBuffer
var word = new Uint16Array(buffer) // this one uses 16 bit numbers
var bytes = new Uint8Array(buffer) // this one uses 8 bit numbers
var value = (65 << 8) + 66
word[0] = (65 << 8) + 66
console.log(bytes) // will output [66, 65]
console.log(word[0] === value) // will output true
當我們查看各個字節時,我們發現 B 的值確實被寫入了緩衝區的第一個字節,而不是 A 的值,但當我們讀回這個 16 位數字時,它是正確的!
這是因為瀏覽器默認使用小端序(Little Endian)的數字。
這是什麼意思?
讓我們假設一個字節可以保存一個單個數字,因此數字123將佔用三個字節:1、2和3。小端序(Little Endian)意味着多字節數值的較低位數字先存儲,因此在內存中它將按照3、2、1的順序存儲。
還有一種大端序(Big Endian)格式,其中字節按照我們預期的順序存儲,從最高位數字開始,所以在內存中它將按照1、2、3的順序存儲。
只要計算機知道數據的存儲方式,它就可以為我們進行轉換,並從內存中得到正確的數字。
讓我們看看另一種讀寫 ArrayBuffer 的方式:DataView,想象一下,您想要編寫一個需要一些文件頭的二進制文件,如下所示:
順便説一下:這是 BMP 文件頭的結構。
除了使用各種類型化數組進行操作,我們還可以使用DataView:
var buffer = new ArrayBuffer(14)
var view = new DataView(buffer)
view.setUint8(0, 66) // Write one byte: 'B'
view.setUint8(1, 67) // Write one byte: 'M'
view.setUint32(2, 1234) // Write four byte: 1234 (rest filled with zeroes)
view.setUint16(6, 0) // Write two bytes: reserved 1
view.setUint16(8, 0) // Write two bytes: reserved 2
view.setUint32(10, 0) // Write four bytes: offset
我們的ArrayBuffer現在包含以下數據:
Byte | 0 | 1 | 2 | 3 | 4 | 5 | ... |
Type | I8 | I8 | I32 | ... |
Data | B | M |00000000|00000000|00000100|11010010| ... |
在上面的示例中,我們使用DataView將兩個Uint8寫入前兩個字節,然後是佔用接下來四個字節的Uint32,依此類推。
很酷。現在讓我們回到我們的簡單文本例子。
我們也可以使用DataView而不是之前使用的Uint16Array來寫入一個Uint16,以保存我們的兩個字符字符串'AB':
var buffer = new ArrayBuffer(2) // array buffer for two bytes
var view = new DataView(buffer)
var value = (65 << 8) + 66 // we shift the 'A' into the upper 8 bit and add the 'B' as the lower 8 bit.
view.setUint16(0, value)
// Let's create a text file from them:
var blob = new Blob([buffer], {type: 'text/plain'})
var dataUri = window.URL.createObjectURL(blob)
window.open(dataUri)
等一下,什麼?我們得到期望的字符串'AB',而不是上次寫入Uint16時得到的'BA'!也許setUint16默認為大端序(Big Endian)?
根據DataView的規範,setUint16方法的定義如下:
DataView.prototype.setUint16 ( byteOffset, value [ , littleEndian ] )
根據規範,如果沒有明確指定littleEndian參數,則默認為false,即使用大端序(Big Endian)。因此,使用DataView的setUint16方法寫入值時,默認情況下采用的是大端序。這就解釋了為什麼使用DataView寫入Uint16時,我們得到了正確的字符串'AB',而不是'BA'。
Blob
Blob 全稱為 binary large object ,即二進制大對象,它是 JavaScript 中的一個對象,表示原始的類似文件的數據。下面是 MDN 中對 Blob 的解釋:
Blob 對象表示一個不可變、原始數據的類文件對象。它的數據可以按文本或二進制的格式進行讀取,也可以轉換成 ReadableStream 來用於數據操作。
實際上,Blob 對象是包含有隻讀原始數據的類文件對象。簡單來説,Blob 對象就是一個不可修改的二進制文件。
可以使用 Blob() 構造函數來創建一個 Blob:
new Blob(array, options);
其有兩個參數:
- array:由 ArrayBuffer、ArrayBufferView、Blob、DOMString 等對象構成的,將會被放進 Blob;
-
options:可選的 BlobPropertyBag 字典,它可能會指定如下兩個屬性
- type:默認值為 "",表示將會被放入到 blob 中的數組內容的 MIME 類型。
- endings:默認值為"transparent",用於指定包含行結束符\n的字符串如何被寫入,不常用。
Blob 有哪些使用場景?
圖片本地預覽
這裏整理 2 種圖片本地預覽的方式:
- 使用 DataURL 方式;
- 使用 Blob URL/Object URL 方式;
<body>
<h1>1.DataURL方式:</h1>
<input type="file" accept="image/*" onchange="selectFileForDataURL(event)">
<img id="output1">
<h1>2.Blob方式:</h1>
<input type="file" accept="image/*" onchange="selectFileForBlob(event)">
<img id="output2">
<script>
// 1.DataURL方式:
async function selectFileForDataURL() {
const reader = new FileReader();
reader.onload = function () {
const output = document.querySelector("#output1")
output.src = reader.result;
}
reader.readAsDataURL(event.target.files[0]);
}
//2.Blob方式:
async function selectFileForBlob(){
const reader = new FileReader();
const output = document.querySelector("#output2");
const imgUrl = window.URL.createObjectURL(event.target.files[0]);
output.src = imgUrl;
reader.onload = function(event){
window.URL.revokeObjectURL(imgUrl);
}
}
</script>
</body>
分片上傳
File對象繼承了Blob對象的所有屬性和方法,可以使用File對象的slice()方法進行文件切片操作,將大文件切割成較小的分片,並逐個上傳這些分片。
// 分片上傳對象
var ChunkUploader = function(file) {
this.file = file;
this.chunkSize = 1024 * 1024; // 每個分片的大小,這裏設置為1MB
this.totalChunks = Math.ceil(file.size / this.chunkSize);
this.currentChunk = 0;
this.uploadedChunks = [];
this.isPaused = false;
};
// 上傳下一個分片
ChunkUploader.prototype.uploadNextChunk = function() {
if (this.currentChunk >= this.totalChunks) {
console.log("文件上傳完成");
return;
}
if (this.isPaused) {
console.log("上傳已暫停");
return;
}
var start = this.currentChunk * this.chunkSize;
var end = Math.min(start + this.chunkSize, this.file.size);
var chunk = this.file.slice(start, end);
var formData = new FormData();
formData.append("file", chunk);
formData.append("chunkIndex", this.currentChunk);
// 發起上傳請求,這裏使用XMLHttpRequest示例
var xhr = new XMLHttpRequest();
xhr.open("POST", "/upload", true);
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
// 上傳成功,記錄已上傳分片信息
this.uploadedChunks.push(this.currentChunk);
// 繼續上傳下一個分片
this.currentChunk++;
this.uploadNextChunk();
} else {
// 上傳失敗,處理錯誤
console.error("上傳失敗:", xhr.status, xhr.statusText);
}
};
xhr.onerror = () => {
console.error("上傳出錯");
};
xhr.send(formData);
};
// 暫停上傳
ChunkUploader.prototype.pauseUpload = function() {
this.isPaused = true;
};
// 繼續上傳
ChunkUploader.prototype.resumeUpload = function() {
this.isPaused = false;
this.uploadNextChunk();
};
// 創建文件上傳實例
var fileInput = document.getElementById("fileInput");
var chunkUploader = new ChunkUploader(fileInput.files[0]);
// 啓動上傳
chunkUploader.uploadNextChunk();
// 暫停上傳
chunkUploader.pauseUpload();
// 繼續上傳
chunkUploader.resumeUpload();
下載數據
要通過HTTP請求獲取文件並下載,您可以使用XMLHttpRequest或Fetch API來執行請求,並使用Blob對象進行文件下載。以下是一個示例代碼,演示如何請求並下載文件:
function downloadFile(url, fileName) {
var xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.responseType = "blob";
xhr.onload = function () {
if (xhr.status === 200) {
var blob = xhr.response;
var a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = fileName;
var event = document.createEvent("MouseEvents");
event.initEvent("click", true, false);
a.dispatchEvent(event);
}
};
xhr.send();
}
function downloadFile(url, fileName) {
fetch(url)
.then(function (response) {
if (response.ok) {
return response.blob();
} else {
throw new Error("File download failed");
}
})
.then(function (blob) {
var a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = fileName;
var event = document.createEvent("MouseEvents");
event.initEvent("click", true, false);
a.dispatchEvent(event);
})
.catch(function (error) {
console.error("File download error:", error);
});
}
圖片壓縮
當我們希望本地圖片在上傳之前,先進行一定壓縮,再提交,從而減少傳輸的數據量。
在前端我們可以使用 Canvas 提供的 toDataURL() 方法來實現,該方法接收 type 和 encoderOptions 兩個可選參數:
- type 表示「圖片格式」,默認為 image/png ;
- encoderOptions 表示「圖片質量」,在指定圖片格式為 image/jpeg 或 image/webp 的情況下,可以從 0 到 1 區間內選擇圖片質量。如果超出取值範圍,將會使用默認值 0.92,其他參數會被忽略。
<body>
<input type="file" accept="image/*" onchange="loadFile(event)" />
<script>
// 將base64轉化為File對象
const base64ToFile = (base64String, fileName, fileType) => {
const byteCharacters = atob(base64String.split(",")[1]);
const byteArrays = [];
for (let i = 0; i < byteCharacters.length; i++) {
byteArrays.push(byteCharacters.charCodeAt(i));
}
const byteArray = new Uint8Array(byteArrays);
return new File([byteArray], fileName, { type: fileType });
};
const compress = (file, maxWidth, maxHeight, quality) => {
return new Promise((resolve, reject) => {
const image = new Image();
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
image.onload = () => {
let width = image.width;
let height = image.height;
// 計算壓縮後的尺寸
if (maxWidth && width > maxWidth) {
height *= maxWidth / width;
width = maxWidth;
}
if (maxHeight && height > maxHeight) {
width *= maxHeight / height;
height = maxHeight;
}
// 設置 Canvas 的尺寸
canvas.width = width;
canvas.height = height;
// 在 Canvas 上繪製壓縮後的圖片
ctx.drawImage(image, 0, 0, width, height);
// 轉換為壓縮後的圖片數據
const compressedDataUrl = canvas.toDataURL("image/jpeg", quality);
// 將base64轉化為File對象
const compressedFile = base64ToFile(
compressedDataUrl,
encodeURIComponent(file.name),
file.type
);
resolve(compressedFile);
};
// 加載圖片
image.src = URL.createObjectURL(file);
});
};
// 通過 AJAX 提交到服務器
const uploadFile = (url, file) => {
let formData = new FormData();
let request = new XMLHttpRequest();
formData.append("image", file);
request.open("POST", url, true);
request.send(formData);
}
const loadFile = (event) => {
const file = event.target.files[0];
const compressedFile = compress(file);
uploadFile("https://httpbin.org/post", compressedFile);
};
</script>
</body>
其實 Canvas 對象除了提供 toDataURL() 方法之外,它還提供了一個 toBlob() 方法,該方法的語法如下:
canvas.toBlob(callback, mimeType, qualityArgument)
和 toDataURL() 方法相比,toBlob() 方法是異步的,因此多了個 callback 參數,這個 callback 回調方法默認的第一個參數就是轉換好的 blob文件信息。
ArrayBuffer 與 Blob
看定義的話,先翻翻 MDN:
ArrayBuffer 對象用來表示通用的、固定長度的原始二進制數據緩衝區。
Blob 對象表示一個不可變、原始數據的類文件對象。它的數據可以按文本或二進制的格式進行讀取,也可以轉換成 ReadableStream 來用於數據操作。
從定義可以知曉,兩者都是對二進制數據進行操作。MDN描述的還比較模糊,《現代 JavaScript 教程》中寫的比較清楚:“基本的二進制對象是 ArrayBuffer —— 對固定長度的連續內存空間的引用”。Blob支持的類型更加複合,既然也是操作二進制數據所以核心也是基於ArrayBuffer,但更主要是對文件進行操作。所以兩者大部分情況下能夠替換使用也就很容易理解了。但還不夠解釋以上其他問題。
繼續查找資料,翻到Chrome設計文檔Chrome's Blob Storage System Design中有對Blob詳細描述。
之前的疑問在這裏就有答案了:
If the in-memory space for blobs is getting full, or a new blob is too large to be in-memory, then the blob system uses the disk. This can either be paging old blobs to disk, or saving the new too-large blob straight to disk.
大意是説,當blob的內存空間佔滿時,或者新創建的blob太大,剩餘的內存空間放不下了,blob會轉存到磁盤中。可以是轉存舊的blob數據,也可以是將新的blob直接存儲到磁盤。
同時還提到了,在使用blob時應該避免快速創建非常多的blob,特別是數據量非常大的,這會導致瀏覽器要將blob寫入到磁盤後才能渲染器才能繼續處理後續數據。這樣也就解釋了為什麼之前blob有出現卡頓的情況。
總結一下差異:
- ArrayBuffer:僅對內存操作,是最基礎的二進制對象。所有的數據都放在內存中,當有大量的ArrayBuffer時等於數據全在內存中,就容易導致瀏覽器標籤頁因內存超過限制而崩潰。
- Blob:blob的數據存儲比較複合,所引用的數據不僅僅在內存中,也可能存在磁盤上。當數據超過一定量時會將數據從內存轉存到磁盤中。這也符合blob的名稱二進制大數據對象(Binary Large Object),對大文件對象有做專門的優化。
綜合看來,如果 Axios 處理文件數據,還是配置 blob 比較適合。
另一個問題,Axios 中為什麼説 blob 僅瀏覽器可用?這個比較容易找到答案,賀師俊在知乎有個回答:
注意,Blob並不像ArrayBuffer是JS語言內置的,而是Web API,Node.js的API裏就沒有Blob。這也是為什麼MDN説「Blobs can represent data that isn't necessarily in a JavaScript-native format」(中文版的翻譯「Blob表示的不一定是JavaScript原生格式的數據」反而比英文原文難理解)。
不看這説明是真不理解 MDN 的那段描述,在《現代 JavaScript 教程》中其實也有提到,但只在 Blob 章節開頭提了 ArrayBuffer 是 ECMA 標準的一部分,沒提説 Blob 是不是,看着也是會覺得有些奇怪。
不過這個回答是2020年的,當時Node還不支持Blob,到Node18版本發佈已經正式支持Blob類型了,詳細的可以看Node官方文檔class-blob中History表,所以現在Node中也是支持Blob了。
Stream
最後是 Stream,先説説 Stream 模式與 Arraybuffer(Node 中對應的是 Buffer)模式應用的差異。
在大文件讀取的場景下,使用 Arraybuffer 會將所有數據全部寫入內存後再處理,文件很大時很可能導致內存爆了。如果使用 Stream 數據依然是存入內存,但存入的數據會立即就開始處理,不必等到所有數據加載完再開始,這樣只需要消耗極小的內存就能完成對文件的處理。
Node 中是有 Stream 模式相關的 API,那瀏覽器呢?也是有的,Chrome 從59版本開始其實是有 Stream API 的,網絡請求需要配合 fetch 使用。
翻閲代碼,可以發現Axios瀏覽器請求還是基於 XMLHttpRequest 的,axios/lib/adapters/xhr.js 源碼中 responseType 數據沒有處理直接傳入 XMLHttpRequest 對象的。
那麼 XMLHttpRequest 的 responseType 是否支持設置為 stream?來看看 WHATWG 對 XMLHttpRequest 支持的類型描述:
enum XMLHttpRequestResponseType {
"",
"arraybuffer",
"blob",
"document",
"json",
"text"
};
可知,XMLHttpRequest 是不支持的。咦?很奇怪,axios 文檔怎麼寫的是支持?
翻了一圈 issue,發現有提到 axios 準備增加一個新的 adapter(使用的是 fetch)來支持 stream。回頭又找了一圈代碼,沒發現有新增的模塊。繼續翻翻 issue 和 discussions,之前的相關信息都已經關閉了,但在Axios next的關聯中有一個相關 issue 還是打開的,相關PR也還未合併,查看代碼版本目前處於 beta5,也有半年沒更新了。
也難怪主版本中沒看到過相關代碼,目前看來相關改動還沒有確定下來。實際測試中 stream 也沒支持成功,流數據返回的話會解析成字符串。如果非常想在 axios 中接收 stream 數據,可以嘗試使用還在測試中的模塊,將 adapter 配置更換一下。
總之目前為止,如果想使用 stream 傳輸數據還是轉向用 fetch 吧。
以下是流式獲取文本的示例:
const resp = await fetch(url);
const reader = resp.body.getReader();
const textDecoder = new TextDecoder();
while(1) {
const { value, done } = await reader.read();
if (done) {
break;
}
console.log(textDecoder.decode(value));
}
Base64
Base64(radix-64)是一種基於64個可打印字符來表示二進制數據的表示方法。由於,所以每6個比特為一個單元,對應某個可打印字符。3個字節相當於24個比特,對應於4個 Base64 單元,即3個字節可由4個可打印字符來表示。這 64 個字符包括大小寫字母(A-Z, a-z)、數字(0-9)以及兩個特殊字符(+ 和 /)。
這意味着 Base64 格式的字符串或文件的尺寸約是原始尺寸的 133%(增加了大約 33%)。如果編碼的數據很少,增加的比例可能會更高。例如:長度為 1 的字符串 "a" 進行 Base64 編碼後是 "YQ==",長度為 4,尺寸增加了 3 倍。
Base64 常用於在通常處理文本數據的場合,表示、傳輸、存儲一些二進制數據,包括 MIME 的電子郵件及 XML 的一些複雜數據。
Base64 編碼在網絡上的一個常見應用是對二進制數據進行編碼,以便將其納入 data: URL 中。
在 Base64 編碼中,每三個字節的二進制數據被編碼為四個字符,如果最後剩下的字節不足三個,則會進行填充。
具體的填充規則如下:
- 如果最後一個分組有一個字節,編碼結果為兩個字符,然後在末尾添加兩個 "="。
- 如果最後一個分組有兩個字節,編碼結果為三個字符,然後在末尾添加一個 "="。
- 如果最後一個分組有三個字節,編碼結果為四個字符,不需要進行填充。
以下是一個示例,將一個字符串 "Hello, World!" 進行 Base64 編碼:
將字符串轉換為對應的二進制數據。例如,使用 UTF-8 編碼將 "Hello, World!" 轉換為字節數組:[72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33]。
將每三個字節的數據分組,並將其轉換為對應的 Base64 字符。對於每個分組,將其轉換為四個字符。
第一個分組:72, 101, 108 → 01001000, 01100101, 01101100 → 010010, 000110, 010101, 101100 → S, G, V, s
第二個分組:108, 111, 44 → 01101100, 01101111, 00101100 → 011011, 000110, 111100, 101100 → b, G, 9, s
第三個分組:32, 87, 111 → 00100000, 01010111, 01101111 → 001000, 000010, 101111, 101100 → I, F, v, s
第四個分組:114, 108, 100 → 01110010, 01101100, 01100100 → 011100, 100110, 110100 → c, m, Q
第五個分組:33 → 00100001 → 001000 010000 → I, Q 最後一個分組只有一個字節,編碼結果為兩個字符,然後在末尾添加兩個 "="。
將每個分組得到的字符連接起來,得到最終的 Base64 編碼字符串:SGVsbG8sIFdvcmxkIQ==
atob 與 btoa
在 JavaScript 中,有兩個函數被分別用來處理解碼和編碼 Base64 字符串:
- btoa():從二進制數據“字符串”創建一個 Base-64 編碼的 ASCII 字符串(“btoa”應讀作“binary to ASCII”)
- atob():解碼通過 Base-64 編碼的字符串數據(“atob”應讀作“ASCII to binary”)
atob() 和 btoa() 使用的算法在 RFC 4648 第四節中給出。
“Unicode 問題”
由於 JavaScript 字符串是 16 位編碼的字符串,在大多數瀏覽器中,在 Unicode 字符串上調用 window.btoa,如果一個字符超過了 8 位 ASCII 編碼字符的範圍,就會引起 Character Out Of Range 異常。
有兩種可能的方法來解決這個問題:
- 第一種是先對整個字符串轉義,然後進行編碼;
- 第二種是將 UTF-16 字符串轉換為 UTF-8 字符數組,然後進行編碼。
方案 1——先轉義字符串
encodeURIComponent 會將字符轉換為 UTF-8 編碼的字節序列
function utf8_to_b64(str) {
return window.btoa(unescape(encodeURIComponent(str)));
}
function b64_to_utf8(str) {
return decodeURIComponent(escape(window.atob(str)));
}
// Usage:
utf8_to_b64("✓ à la mode"); // "4pyTIMOgIGxhIG1vZGU="
b64_to_utf8("4pyTIMOgIGxhIG1vZGU="); // "✓ à la mode"
該方案由 Johan Sundström 提出。
另一個可能的解決方案是不利用現在已經廢棄的 'unescape' 和 'escape' 函數。不過這個方案並沒有對輸入的字符串進行 base64 編碼。注意,utf8_to_b64 和 b64EncodeUnicode 的輸出結果的不同。採用這種方式可能會導致與其他應用程序的互操作性問題。
function b64EncodeUnicode(str) {
return btoa(encodeURIComponent(str));
}
function UnicodeDecodeB64(str) {
return decodeURIComponent(atob(str));
}
b64EncodeUnicode("✓ à la mode"); // "JUUyJTlDJTkzJTIwJUMzJUEwJTIwbGElMjBtb2Rl"
UnicodeDecodeB64("JUUyJTlDJTkzJTIwJUMzJUEwJTIwbGElMjBtb2Rl"); // "✓ à la mode"
方案 2——使用 TypedArray 和 UTF-8 重寫 atob() 和 btoa() 方法
"use strict";
// Array of bytes to Base64 string decoding
function b64ToUint6(nChr) {
return nChr > 64 && nChr < 91
? nChr - 65
: nChr > 96 && nChr < 123
? nChr - 71
: nChr > 47 && nChr < 58
? nChr + 4
: nChr === 43
? 62
: nChr === 47
? 63
: 0;
}
function base64DecToArr(sBase64, nBlocksSize) {
const sB64Enc = sBase64.replace(/[^A-Za-z0-9+/]/g, ""); // Remove any non-base64 characters, such as trailing "=", whitespace, and more.
const nInLen = sB64Enc.length;
const nOutLen = nBlocksSize
? Math.ceil(((nInLen * 3 + 1) >> 2) / nBlocksSize) * nBlocksSize
: (nInLen * 3 + 1) >> 2;
const taBytes = new Uint8Array(nOutLen);
let nMod3;
let nMod4;
let nUint24 = 0;
let nOutIdx = 0;
for (let nInIdx = 0; nInIdx < nInLen; nInIdx++) {
nMod4 = nInIdx & 3;
nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << (6 * (3 - nMod4));
if (nMod4 === 3 || nInLen - nInIdx === 1) {
nMod3 = 0;
while (nMod3 < 3 && nOutIdx < nOutLen) {
taBytes[nOutIdx] = (nUint24 >>> ((16 >>> nMod3) & 24)) & 255;
nMod3++;
nOutIdx++;
}
nUint24 = 0;
}
}
return taBytes;
}
/* Base64 string to array encoding */
function uint6ToB64(nUint6) {
return nUint6 < 26
? nUint6 + 65
: nUint6 < 52
? nUint6 + 71
: nUint6 < 62
? nUint6 - 4
: nUint6 === 62
? 43
: nUint6 === 63
? 47
: 65;
}
function base64EncArr(aBytes) {
let nMod3 = 2;
let sB64Enc = "";
const nLen = aBytes.length;
let nUint24 = 0;
for (let nIdx = 0; nIdx < nLen; nIdx++) {
nMod3 = nIdx % 3;
// To break your base64 into several 80-character lines, add:
// if (nIdx > 0 && ((nIdx * 4) / 3) % 76 === 0) {
// sB64Enc += "\r\n";
// }
nUint24 |= aBytes[nIdx] << ((16 >>> nMod3) & 24);
if (nMod3 === 2 || aBytes.length - nIdx === 1) {
sB64Enc += String.fromCodePoint(
uint6ToB64((nUint24 >>> 18) & 63),
uint6ToB64((nUint24 >>> 12) & 63),
uint6ToB64((nUint24 >>> 6) & 63),
uint6ToB64(nUint24 & 63),
);
nUint24 = 0;
}
}
return (
sB64Enc.substring(0, sB64Enc.length - 2 + nMod3) +
(nMod3 === 2 ? "" : nMod3 === 1 ? "=" : "==")
);
}
/* UTF-8 array to JS string and vice versa */
function UTF8ArrToStr(aBytes) {
let sView = "";
let nPart;
const nLen = aBytes.length;
for (let nIdx = 0; nIdx < nLen; nIdx++) {
nPart = aBytes[nIdx];
sView += String.fromCodePoint(
nPart > 251 && nPart < 254 && nIdx + 5 < nLen /* six bytes */
? /* (nPart - 252 << 30) may be not so safe in ECMAScript! So…: */
(nPart - 252) * 1073741824 +
((aBytes[++nIdx] - 128) << 24) +
((aBytes[++nIdx] - 128) << 18) +
((aBytes[++nIdx] - 128) << 12) +
((aBytes[++nIdx] - 128) << 6) +
aBytes[++nIdx] -
128
: nPart > 247 && nPart < 252 && nIdx + 4 < nLen /* five bytes */
? ((nPart - 248) << 24) +
((aBytes[++nIdx] - 128) << 18) +
((aBytes[++nIdx] - 128) << 12) +
((aBytes[++nIdx] - 128) << 6) +
aBytes[++nIdx] -
128
: nPart > 239 && nPart < 248 && nIdx + 3 < nLen /* four bytes */
? ((nPart - 240) << 18) +
((aBytes[++nIdx] - 128) << 12) +
((aBytes[++nIdx] - 128) << 6) +
aBytes[++nIdx] -
128
: nPart > 223 && nPart < 240 && nIdx + 2 < nLen /* three bytes */
? ((nPart - 224) << 12) +
((aBytes[++nIdx] - 128) << 6) +
aBytes[++nIdx] -
128
: nPart > 191 && nPart < 224 && nIdx + 1 < nLen /* two bytes */
? ((nPart - 192) << 6) + aBytes[++nIdx] - 128
: /* nPart < 127 ? */ /* one byte */
nPart,
);
}
return sView;
}
function strToUTF8Arr(sDOMStr) {
let aBytes;
let nChr;
const nStrLen = sDOMStr.length;
let nArrLen = 0;
/* mapping… */
for (let nMapIdx = 0; nMapIdx < nStrLen; nMapIdx++) {
nChr = sDOMStr.codePointAt(nMapIdx);
if (nChr >= 0x10000) {
nMapIdx++;
}
nArrLen +=
nChr < 0x80
? 1
: nChr < 0x800
? 2
: nChr < 0x10000
? 3
: nChr < 0x200000
? 4
: nChr < 0x4000000
? 5
: 6;
}
aBytes = new Uint8Array(nArrLen);
/* transcription… */
let nIdx = 0;
let nChrIdx = 0;
while (nIdx < nArrLen) {
nChr = sDOMStr.codePointAt(nChrIdx);
if (nChr < 128) {
/* one byte */
aBytes[nIdx++] = nChr;
} else if (nChr < 0x800) {
/* two bytes */
aBytes[nIdx++] = 192 + (nChr >>> 6);
aBytes[nIdx++] = 128 + (nChr & 63);
} else if (nChr < 0x10000) {
/* three bytes */
aBytes[nIdx++] = 224 + (nChr >>> 12);
aBytes[nIdx++] = 128 + ((nChr >>> 6) & 63);
aBytes[nIdx++] = 128 + (nChr & 63);
} else if (nChr < 0x200000) {
/* four bytes */
aBytes[nIdx++] = 240 + (nChr >>> 18);
aBytes[nIdx++] = 128 + ((nChr >>> 12) & 63);
aBytes[nIdx++] = 128 + ((nChr >>> 6) & 63);
aBytes[nIdx++] = 128 + (nChr & 63);
nChrIdx++;
} else if (nChr < 0x4000000) {
/* five bytes */
aBytes[nIdx++] = 248 + (nChr >>> 24);
aBytes[nIdx++] = 128 + ((nChr >>> 18) & 63);
aBytes[nIdx++] = 128 + ((nChr >>> 12) & 63);
aBytes[nIdx++] = 128 + ((nChr >>> 6) & 63);
aBytes[nIdx++] = 128 + (nChr & 63);
nChrIdx++;
} /* if (nChr <= 0x7fffffff) */ else {
/* six bytes */
aBytes[nIdx++] = 252 + (nChr >>> 30);
aBytes[nIdx++] = 128 + ((nChr >>> 24) & 63);
aBytes[nIdx++] = 128 + ((nChr >>> 18) & 63);
aBytes[nIdx++] = 128 + ((nChr >>> 12) & 63);
aBytes[nIdx++] = 128 + ((nChr >>> 6) & 63);
aBytes[nIdx++] = 128 + (nChr & 63);
nChrIdx++;
}
nChrIdx++;
}
return aBytes;
}
測試
/* Tests */
const sMyInput = "Base 64 \u2014 Mozilla Developer Network";
const aMyUTF8Input = strToUTF8Arr(sMyInput);
const sMyBase64 = base64EncArr(aMyUTF8Input);
alert(sMyBase64);
const aMyUTF8Output = base64DecToArr(sMyBase64);
const sMyOutput = UTF8ArrToStr(aMyUTF8Output);
alert(sMyOutput);
Blob URL 和 Data URL
Blob URL 和 Data URL 是兩種不同的 URL 方案,用於在瀏覽器中表示和使用數據。
Blob URL(或稱為 Object URL)是一種特殊的 URL 格式,用於表示 Blob 對象的地址。它通過使用 URL.createObjectURL() 方法生成,該方法接受一個 Blob 或 File 對象作為參數,並返回一個唯一的 URL,該 URL 可以用於引用該 Blob 對象。Blob URL 的格式通常是以 "blob:" 開頭,後面跟隨一個唯一的標識符。
Data URL 是一種用於嵌入數據的 URL 格式,可以直接將數據嵌入到 URL 中。它的格式如下:
data:[<mediatype>][;base64],<data>
其中 <mediatype> 是數據的 MIME 類型,例如 text/plain、image/jpeg 等;;base64 是可選的,表示數據是否使用 Base64 編碼;<data> 是實際的數據內容。
Blob URL 和 Data URL 的區別主要在於數據的來源和用途:
- Blob URL 用於表示 Blob 對象的地址,通常用於在瀏覽器中處理和操作二進制數據,如文件下載、視頻播放、圖像顯示等。它適用於大型數據或二進制數據,因為它僅提供了 Blob 對象的引用,而不需要將整個數據嵌入到 URL 中。
- Data URL 則直接將數據嵌入到 URL 中,適用於小型數據或文本數據,如圖像的 Base64 編碼表示、內聯腳本或樣式表等。它可以簡化資源的引用和傳輸,但對於較大的數據會增加 URL 的長度,可能導致性能下降。
在選擇使用 Blob URL 還是 Data URL 時,需要根據具體的使用場景和數據大小來進行權衡。如果涉及到大型或二進制數據,Blob URL 通常更合適;而對於小型或文本數據,Data URL 可能更方便。
encodeURIComponent
encodeURIComponent() 函數通過將特定字符的每個實例替換成代表字符的 UTF-8 編碼的一個、兩個、三個或四個轉義序列來編碼 URI(只有由兩個“代理”字符組成的字符會被編碼為四個轉義序列)。與 encodeURI() 相比,此函數會編碼更多的字符,包括 URI 語法的一部分。
以下是 encodeURIComponent 的編碼過程:
- 將要編碼的字符串按字符進行遍歷。
-
對於每個字符,判斷是否屬於以下字符集之一:
- 字母(A-Z,a-z)
- 數字(0-9)
- 特殊字符(-,_,.,!,~,*,',(,))
如果字符屬於上述字符集之一,則保持不變。
-
對於不屬於上述字符集的字符:
- 將字符轉換為 UTF-8 編碼的字節序列。
- 將每個字節轉換為兩位十六進制數。
- 在每個十六進制數前添加 "%"。
- 將得到的編碼後的字符串連接起來。
- 返回編碼後的字符串作為結果。
以下是一個示例,將一個字符串 "шеллы" 進行編碼:
console.log(`?x=${encodeURIComponent('шеллы')}`);
// Expected output: "?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B"
以"ш"字符為例,看其是如何被編碼的:
// 獲取字符"ш"的碼點
"ш".charCodeAt() // 1096
// 轉成十六進制
Number(1096).toString(16) // '448'
// 對照以下 Unicode 十六進制轉化 UTF-8 編碼方式表,448 介於 U+0080 到 U+07FF 之間
Unicode符號範圍 UTF-8編碼方式
(十六進制) (二進制)
0000 0000-0000 007F(U+0000 到 U+007F) 0xxxxxxx
0000 0080-0000 07FF(U+0080 到 U+07FF) 110xxxxx 10xxxxxx
0000 0800-0000 FFFF(U+0800 到 U+FFFF) 1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF(U+10000 到 U+10FFFF) 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
// 將其轉化成二進制,然後從 "ш" 的最後一個二進制位開始,依次從後向前填入格式中的x,多出的位補0
Number(1096).toString(2) // '10001001000'
// 填充後得到 UTF-8 編碼方式
11010001 10001000
// 然後,轉成十六進制,每個十六進制數對應四位二進制數
1101(D) 0001(1) 1000(8) 1000(8) -> %D1%88
參考
Base64-MDN
Base64-維基百科
JavaScript中"ArrayBuffer"對象與"Blob"對象到底有什麼區別?
談談JS二進制:File、Blob、FileReader、ArrayBuffer、Base64
axios中responseType配置blob、arraybuffer、stream值有什麼差異