前端Base64編碼知識,一文打盡
原文: https://juejin.cn/post/6989391487200919566 作者: 云的世界 掘金專欄: 前端基礎進階
健康滿分
關注并將「趣談前端」設為星標
每天定時分享技術干貨/優(yōu)秀開源/技術思維
前言
本文將詳細的介紹前端 Base64 編碼知識,探索起源,讓大家對開發(fā)經(jīng)常用到的 Base64 有個更全面深入的認知。
大綱
Base64在前端的應用 Base64數(shù)據(jù)編碼起源 Base64編碼64的含義 Base64編碼優(yōu)缺點 一些計算機和前端基礎知識 ASCII碼, Unicode , UTF-8 Base64編碼和解碼 其他的成熟方案
Base64在前端的應用
Base64編碼,你一定知道的,先來看看它在前端的一些常見應用:
當然絕部分場景都是基于Data URLs[1]
Canvas圖片生成
canvas的 toDataURL[2]可以把canvas的畫布內容轉base64編碼格式包含圖片展示的 data URI[3]。
const ctx = canvasEl.getContext("2d");
// ...... other code
const dataUrl = canvasEl.toDataURL();
// data:image/png;base64,iVBORw0KGgoAAAANSUhE.........
你畫我猜,新用戶加入,要獲取當前的最新的繪畫界面,也可以通過Base64格式的消息傳遞。
文件讀取
FileReader的 readAsDataURL[4]可以把上傳的文件轉為base64格式的data URI,比較常見的場景是用戶頭像的剪裁和上傳。
function readAsDataURL() {
const fileEl = document.getElementById("inputFile");
return new Promise((resolve, reject) => {
const fd = new FileReader();
fd.readAsDataURL(fileEl.files[0]);
fd.onload = function () {
resolve(fd.result);
// data:image/png;base64,iVBORw0KGgoAAAA.......
}
fd.onerror = reject;
});
}
jwt
jwt由header, payload,signature三部分組成,前兩個解碼后,都是可以明文看見的。拿 國服最強JWT生成Token做登錄校驗講解,看完保證你學會![5] 里面的token做測試。

網(wǎng)站圖片和小圖片
移動端網(wǎng)站圖標優(yōu)化
<link rel="icon" href="data:," />
<link rel="icon" href="data:;base64,=" />
至于怎么獲得這個值data:,的:
<canvas height="0" width="0" id="canvas"></canvas>
<script>
const canvasEl = document.getElementById("canvas");
const ctx = canvasEl.getContext("2d");
dataUrl = canvasEl.toDataURL();
console.log(dataUrl); // data:,
</script>
小圖片
這個就有很多場景了,比如img標簽,背景圖等
img標簽:
<img src="data:image/png;base64,iVBORw0KGgoAAAA......." />
css背景圖:
.bg{
background: url(data:image/png;base64,iVBORw0KGgoAAAA.......)
}
簡單的數(shù)據(jù)加密
當然這不是好方法,但是至少讓你不好解讀。
const username = document.getElementById("username").vlaue;
const password = document.getElementById("password").vlaue;
const secureKey = "%%S%$%DS)_sdsdj_66";
const sPass = utf8_to_base64(password + secureKey);
doLogin({
username,
password: sPass
})
SourceMap
借用阮大神的一段代碼, 注意mappings字段,這實際上就是bas64編碼格式的內容,當然你直接去解,是會失敗的。
{
version : 3,
file: "out.js",
sourceRoot : "",
sources: ["foo.js", "bar.js"],
names: ["src", "maps", "are", "fun"],
mappings: "AAgBC,SAAQ,CAAEA"
}
具體的實現(xiàn)請看官方的base64-vlq.js[6]文件。
混淆加密代碼
著名的代碼混淆庫, javascript-obfuscator[7],其也是有應用base64幾碼的,一起看看選項:
webpack-obfuscator[8]也是基于其封裝的。
--string-array-indexes-type '<list>' (comma separated) [hexadecimal-number, hexadecimal-numeric-string]
--string-array-encoding '<list>' (comma separated) [none, base64, rc4]
--string-array-index-shift <boolean>
--string-array-wrappers-count <number>
--string-array-wrappers-chained-calls <boolean>

其他
X.509公鑰證書, github SSH key, mht文件,郵件附件等等,都有Base64的影子。
Base64數(shù)據(jù)編碼起源
早期郵件傳輸協(xié)議基于 ASCII 文本,對于諸如圖片、視頻等二進制文件處理并不好。ASCII 主要用于顯示現(xiàn)代英文,到目前為止只定義了 128 個字符,包含控制字符和可顯示字符。為了解決上述問題,Base64 編碼順勢而生。
Base64是編解碼,主要的作用不在于安全性,而在于讓內容能在各個網(wǎng)關間無錯的傳輸,這才是Base64編碼的核心作用。
除了Base64數(shù)據(jù)編碼,其實還有Base32數(shù)據(jù)編碼, Base16數(shù)據(jù)編碼,可以參見 RFC 4648[9]。
Base64編碼64的含義
64就是64個字符的意思。
base64對照表, 借用 Base64原理[10]的一張圖:
A-Z26a-z260-910+ /2
26 + 26 + 10 + 2 = 64
當然還有一個字符=,這是填充字符,后面會提到,不屬于64里面的范疇。
對照表的索引值,注意一下,后面的base64編碼和解碼會用到。
Base64編碼優(yōu)缺點
優(yōu)點
可以將二進制數(shù)據(jù)(比如圖片)轉化為可打印字符,方便傳輸數(shù)據(jù) 對數(shù)據(jù)進行簡單的加密,肉眼是安全的 如果是在html或者css處理圖片,可以減少http請求
缺點
內容編碼后體積變大, 至少1/3
因為是三字節(jié)變成四個字節(jié),當只有一個字節(jié)的時候,也至少會變成三個字節(jié)。編碼和解碼需要額外工作量
說完優(yōu)缺點,回到正題:
我們今天的重點是 uf8編碼轉Base64編碼:
基本流程
char => 碼點 => utf-8編碼 => base64編碼
在之前要解一下編碼的知識, 了解編碼知識,又要先了解一些計算機的基礎知識。
一些計算機和前端基礎知識
比特和字節(jié)
比特又叫位。在計算機的世界里,信息的表示方式只有 0 和 1, 其可以表示兩種狀態(tài)。
一位二進制可以表示兩狀態(tài), N位可以表示2^N種狀態(tài)。
一個字節(jié)(Byte)有8位(Bit)
所以一個字節(jié)可以表示 2^8 = 256種狀態(tài);
獲得字符的 Unicode碼點
String.prototype.charCodeAt[11] 可以獲取字符的碼點,獲取范圍為0 ~ 65535。這個地方注意一下,關系到后面的utf-8字節(jié)數(shù)。
"a".charCodeAt(0) // 97
"中".charCodeAt(0) // 20013
進制表示
0b開頭,可以表示二進制
注意0b10000000= 128 ,0b11000000=92,之后會用到.
0b11111111 // 255
0b10000000 // 128 后面會用到
0b11000000 // 192 后面會用到

0x開頭,可以表示16進制
0x11111111 // 286331153

0o開頭可以表示8進制,就不多說了,本來不會涉及。
進制轉換
10進制轉其他進制
Number.prototype.toString(radix)[12]可以把十進制轉為其他進制。
100..toString(2) // 1100100
100..toString(16) // 64, 也等于 ox64
其他進制轉為10進制
parseInt(string, radix)[13]可以把其他進制,轉為10進制。
parseInt("10000000", 2) // 128
parseInt("10",16) // 16
這里額外提一下一元操作符號+可以把字符串轉為數(shù)字,后面也會用到,之前提到的0b,0o,0x這里都會生效。
+"1000" // 1000
+"0b10000000" // 128
+"0o10" // 8
+"0x10" // 16
位移操作
本文只涉及右移操作,就只講右移,右移相當于除以2,如果是整數(shù),簡單說是去低位,移動幾位去掉幾位,其實和10進制除以10是一樣的。
64 >> 2 = 16 我們一起看一下過程
0 1 0 0 0 0 0 0 64
-------------------
0 1 0 0 0 0 | 0 0 16
一元 & 操作和 一元|操作
一元&
當兩者皆為1的時候,值為1。 本文的作用可用來去高位, 具體看代碼。3553 & 36 = 0b110111100001 & 0b111111 = 100001
因為高位缺失,不可能都為1,故均為0, 而低位相當于復制一遍而已。
110111 100001
111111
------------
000000 100001
一元|
當任意一個為1,就輸出為1. 本文用來填補0。比如,把3補成8位二進制3 | 256 = 11 | 100000000 = 100000011
100000011.substring(1)是不是就等于8位二進制呢00000011
具備了這些基本知識,我們就開始先了解編碼相關的知識。
ASCII碼, Unicode , UTF-8
ASCII碼
ASCII碼第一位始終是0, 那么實際可以表示的狀態(tài)是 2^7 = 128種狀態(tài)。
ASCII 主要用于顯示現(xiàn)代英文,到目前為止只定義了 128 個字符,包含控制字符和可顯示字符。
0~31 之間的ASCII碼常用于控制像打印機一樣的外圍設備 32~127 之間的ASCII碼表示的符號,在我們的鍵盤上都可以被找到
完整的 ASCII碼對應表,可以參見 基本ASCII碼和擴展ASCII碼[14]
接下來是Unicode和UTF-8編碼,請先記住這個重要的知識:
Unicode: 字符集 UTF-8: 編碼規(guī)則
Unicode
Unicode 為世界上所有字符都分配了一個唯一的編號(碼點),這個編號范圍從 0x000000 到 0x10FFFF (十六進制),有 100 多萬,每個字符都有一個唯一的 Unicode 編號,這個編號一般寫成 16 進制,在前面加上 U+。例如:掘的 Unicode 是U+6398。
U+0000到U+FFFF
最前面的65536個字符位,它的碼點范圍是從0一直到216-1。所有最常見的字符都放在這里。
U+010000一直到U+10FFFF
剩下的字符都放著這里,碼點范圍從U+010000一直到U+10FFFF。
Unicode有平面的概念,這里就不拓展了。
Unicode只規(guī)定了每個字符的碼點,到底用什么樣的字節(jié)序表示這個碼點,就涉及到編碼方法。
UTF-8
UTF-8 是互聯(lián)網(wǎng)使用最多的一種 Unicode 的實現(xiàn)方式。還有 UTF-16(字符用兩個字節(jié)或四個字節(jié)表示)和 UTF-32(字符用四個字節(jié)表示)等實現(xiàn)方式。
UTF-8 是它是一種變長的編碼方式, 使用的字節(jié)個數(shù)從 1 到 4 個不等,最新的應該不止4個, 這個1-4不等,是后面編碼和解碼的關鍵。
UTF-8的編碼規(guī)則:
對于只有一個字節(jié)的符號,字節(jié)的第一位設為 0,后面 7 位為這個符號的 Unicode 碼。此時,對于英語字母UTF-8 編碼和 ASCII 碼是相同的。對于 n字節(jié)的符號(n > 1),第一個字節(jié)的前n位都設為1,第n + 1位設為0,后面字節(jié)的前兩位一律設為10。剩下的沒有提及的二進制位,全部為這個符號的 Unicode 碼,如下表所示:
| Unicode 碼點范圍(十六進制) | 十進制范圍 | UTF-8 編碼方式(二進制) | 字節(jié)數(shù) |
|---|---|---|---|
0000 0000 ~ 0000 007F | 0 ~ 127 | 0xxxxxxx | 1 |
0000 0080 ~ 0000 07FF | 128 ~ 2047 | 110xxxxx 10xxxxxx | 2 |
0000 0800 ~ 0000 FFFF | 2048 ~ 65535 | 1110xxxx 10xxxxxx 10xxxxxx | 3 |
0001 0000 ~ 0010 FFFF | 65536 ~ 1114111 | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx | 4 |
我們可能沒見過字節(jié)數(shù)為2或者為4的字符, 字節(jié)數(shù)為2的可以去Unicode對應表[15]這里找,而等于4的可以去這看看Unicode? 13.0 Versioned Charts Index[16]
下面這些碼點都處于0000 0080 ~ 0000 07FF, utf-8編碼需要2個字節(jié)
下面這些碼點都處于0001 0000 ~ 0010 FFFF, utf-8編碼需要4個字節(jié)
可能這里光說不好理解,我們分別以英文字符a和中文字符掘來講解一下:
為了驗證結果,可以去 Convert UTF8 to Binary Bits - Online UTF8 Tools[17]
英文字符a
先獲得其碼點, "a".charCodeAt(0)等于97對照表格, 0~127, 需 1個字節(jié)97..toString(2)得到編碼1100001根據(jù)格式 0xxxxxxx進行填充, 最終結果
01100001
中文字符掘
先獲得其碼點, "掘".charCodeAt(0)等于25496對照表格,2048 ~ 65535 需 3個字節(jié)25496..toString(2)得到編碼110 001110 011000根絕格式 1110xxxx 10xxxxxx 10xxxxxx進行填充, 最終結果如下
11100110 10001110 10011000
Convert UTF8 to Binary Bits - Online UTF8 Tools[18]執(zhí)行結果:完全匹配

抽象把字符轉為utf8格式二進制的方法
基于上面的表格和轉換過程,我們抽象一個方法,這個方法在之后的Base64編碼和解碼至關重要:
先看看功能,覆蓋utf8編碼1-3字節(jié)范圍
console.log(to_binary("A")) // 11100001
console.log(to_binary("?")) // 1101100010110011
console.log(to_binary("掘")) // 111001101000111010011000
方法如下
function to_binary(str) {
const string = str.replace(/\r\n/g, "\n");
let result = "";
let code;
for (var n = 0; n < string.length; n++) {
//獲取麻點
code = str.charCodeAt(n);
if (code < 0x007F) { // 1個字節(jié)
// 0000 0000 ~ 0000 007F 0 ~ 127 1個字節(jié)
// (code | 0b100000000).toString(2).slice(1)
result += (code).toString(2).padStart(8, '0');
} else if ((code > 0x0080) && (code < 0x07FF)) {
// 0000 0080 ~ 0000 07FF 128 ~ 2047 2個字節(jié)
// 0x0080 的二進制為 10000000 ,8位,所以大于0x0080的,至少有8位
// 格式 110xxxxx 10xxxxxx
// 高位 110xxxxx
result += ((code >> 6) | 0b11000000).toString(2);
// 低位 10xxxxxx
result += ((code & 0b111111) | 0b10000000).toString(2);
} else if (code > 0x0800 && code < 0xFFFF) {
// 0000 0800 ~ 0000 FFFF 2048 ~ 65535 3個字節(jié)
// 0x0800的二進制為 1000 00000000,12位,所以大于0x0800的,至少有12位
// 格式 1110xxxx 10xxxxxx 10xxxxxx
// 最高位 1110xxxx
result += ((code >> 12) | 0b11100000).toString(2);
// 第二位 10xxxxxx
result += (((code >> 6) & 0b111111) | 0b10000000).toString(2);
// 第三位 10xxxxxx
result += ((code & 0b111111) | 0b10000000).toString(2);
} else {
// 0001 0000 ~ 0010 FFFF 65536 ~ 1114111 4個字節(jié)
// https://www.unicode.org/charts/PDF/Unicode-13.0/U130-2F800.pdf
throw new TypeError("暫不支持碼點大于65535的字符")
}
}
return result;
}
方法中有三個地方稍微難理解一點,我們一起來解讀一下:
二字節(jié) (code >> 6) | 0b11000000
其作用是生成高位二進制。
我們以實際的一個栗子來講解,以?為例,其碼點為0x633,在0000 0080 ~ 0000 07FF之間,占兩個字節(jié), 在其二進制編碼為11 000110011 , 其填充格式如下, 低位要用6位。
110xxxxx 10xxxxxx
為了方便觀察,我們把 11 000110011 重新調整一下 11000 110011。
(code >> 6) 等于 00110011 >> 6,右移6位, 直接干掉低6位。為什么是6呢,因為低位需要6位,右移動6位后,剩下的就是用于高位操作的位了。
11000000
11000 | 110011
--------------
11011000
二字節(jié) (code & 0b111111) | 0b10000000
作用,用于生成低位二進制。以?為例,11000 110011, 填充格式
110xxxxx 10xxxxxx
(code & 0b111111)這步的操作是為了干掉6位以上的高位,僅僅保留低6位。一元&符號,兩邊都是1的時候才會是1,妙啊。
11000 110011
111111
------------------
110011
接著進行 | 0b10000000, 主要是按照格式10xxxxxx進行位數(shù)填補, 讓其滿8位。
11000 110011
111111 (code & 0b111111)
------------------
110011
10 000000 (code & 0b111111) | 0b10000000
-------------------
10 110011
Base64編碼和解碼
utf-8轉Base64編碼規(guī)則
獲取每個字符的Unicode碼,轉為utf-8編碼 三個字節(jié)作為一組,一共是24個二進制位
字節(jié)數(shù)不能被 3 整除,用0字節(jié)值在末尾補足按照6個比特位一組分組,前兩位補0,湊齊8位 計算每個分組的數(shù)值 以第 4步的值作為索引,去ASCII碼表找對應的值替換第 2步添加字節(jié)數(shù)個數(shù)的=
比如第2添加了2個字節(jié),后面是2個=
以大掘A為例, 我們通過上面的utf8_to_binary方法得到utf8的編碼11100110 10001110 10011000 11000001, 其字節(jié)數(shù)不能被3整除,后面填補
11100110
10001110
10011000
01000001
--------
00000000
00000000
6位一組分為四組, 高位補0, 用| 分割一下填補的。
00 | 111001 => 57 => 5
00 | 101000 => 40 => o
00 | 111010 => 58 => 6
00 | 011000 => 24 => Y
00 | 110000 => 16 => Q
00 | 010000 => 16 => Q
00 | 000000 => => =
00 | 000000 => => =
結果是:5o6YQQ==, 完美。
utf-8轉Base64編碼規(guī)則代碼實現(xiàn)
基于上面的to_binary方法和base64的轉換規(guī)則,就很簡單啦:
先看看執(zhí)行效果,very good, 和 base64.us[19] 結果完全一致。
console.log(utf8_to_base64("a")); // YQ==
console.log(utf8_to_base64("?")); // yII=
console.log(utf8_to_base64("中國人")); // 5Lit5Zu95Lq6
console.log(utf8_to_base64("Coding Writing 好文召集令|后端、大前端雙賽道投稿,2萬元獎池等你挑戰(zhàn)!"));
//Q29kaW5nIFdyaXRpbmcg5aW95paH5Y+s6ZuG5Luk772c5ZCO56uv44CB5aSn5YmN56uv5Y+M6LWb6YGT5oqV56i/77yMMuS4h+WFg+WlluaxoOetieS9oOaMkeaImO+8gQ==
完整代碼如下:
const BASE64_CHARTS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
function utf8_to_base64(str: string) {
let binaryStr = to_binary(str);
const len = binaryStr.length;
// 需要填補的=的數(shù)量
let paddingCharLen = len % 24 !== 0 ? (24 - len % 24) / 8 : 0;
//6個一組
const groups = [];
for (let i = 0; i < binaryStr.length; i += 6) {
let g = binaryStr.slice(i, i + 6);
if (g.length < 6) {
g = g.padEnd(6, "0");
}
groups.push(g);
}
// 求值
let base64Str = groups.reduce((b64str, cur) => {
b64str += BASE64_CHARTS[+`0b${cur}`]
return b64str
}, "");
// 填充=
if (paddingCharLen > 0) {
base64Str += paddingCharLen > 1 ? "==" : "=";
}
return base64Str;
}
至于解碼,是其逆過程,留給大家去實現(xiàn)吧。
其他的成熟方案
當然是基于已有的 btoa和atob,
但是 unescape是不被推薦使用的方法
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"
MDN的 rewriting `atob()` and `btoa()` using `TypedArray`s and UTF-8[20]
其支持到6字節(jié),但是可讀性并不好。
第三方庫 base64-js[21] 與 js-base64[22]都是周下載量過百萬的庫。
雖然有那么多成熟的,但是我們理解和自己實現(xiàn),才能更明白Base64的編碼原理。
額外補充一點
編碼關系圖
借用[你真的了解 Unicode 和 UTF-8 嗎?[23]]一張圖:

DOMString[24] 是 utf-16編碼
引用
Version-Specific Charts[25]
Unicode13.0.0[26]
Unicode? 13.0 Versioned Charts Index[27]
RFC 4648 | The Base16, Base32, and Base64 Data Encodings[28]
Base64 encoding and decoding[29]
字符編碼筆記:ASCII,Unicode 和 UTF-8[30]
Unicode與JavaScript詳解[31]
Base64 編碼入門教程[32] Base64原理[33]
詳解base64原理[34]
一文讀懂base64編碼[35]
JS 中關于 base64 的一些事[36]
Base64 的原理、實現(xiàn)及應用[37]
圖片與Base64換算關系[38]
[你真的了解 Unicode 和 UTF-8 嗎?[39]]
Unicode中UTF-8與UTF-16編碼詳解[40]
Unicode對應表[41]
JavaScript Source Map 詳解[42]
?? 看完三件事
點個【在看】,或者分享轉發(fā),讓更多的人也能看到這篇內容 關注公眾號【趣談前端】,定期分享 工程化 / 可視化 / 低代碼 / 優(yōu)秀開源。

Dooring可視化搭建平臺數(shù)據(jù)源設計剖析
基于Koa + React + TS從零開發(fā)全棧文檔編輯器
點個在看你最好看
