前言本文將詳細(xì)的介紹前端 Base64 編碼知識(shí),,探索起源,讓大家對開發(fā)經(jīng)常用到的 Base64 有個(gè)更全面深入的認(rèn)知,。 大綱
Base64在前端的應(yīng)用Base64編碼,你一定知道的,,先來看看它在前端的一些常見應(yīng)用: Canvas圖片生成canvas的 toDataURL[2]可以把canvas的畫布內(nèi)容轉(zhuǎn)base64編碼格式包含圖片展示的 data URI[3]。 const ctx = canvasEl.getContext('2d'); 你畫我猜,新用戶加入,要獲取當(dāng)前的最新的繪畫界面,,也可以通過Base64格式的消息傳遞,。 文件讀取FileReader的 readAsDataURL[4]可以把上傳的文件轉(zhuǎn)為base64格式的data URI,比較常見的場景是用戶頭像的剪裁和上傳,。
jwtjwt由header, payload,signature三部分組成,前兩個(gè)解碼后,都是可以明文看見的,。拿 國服最強(qiáng)JWT生成Token做登錄校驗(yàn)講解,看完保證你學(xué)會(huì),![5] 里面的token做測試,。 網(wǎng)站圖片和小圖片移動(dòng)端網(wǎng)站圖標(biāo)優(yōu)化<link rel='icon' href='data:,' /> 至于怎么獲得這個(gè)值
小圖片這個(gè)就有很多場景了,,比如img標(biāo)簽,背景圖等 img標(biāo)簽: <img src='data:image/png;base64,iVBORw0KGgoAAAA.......' /> css背景圖:
簡單的數(shù)據(jù)加密當(dāng)然這不是好方法,,但是至少讓你不好解讀,。
SourceMap借用阮大神的一段代碼, 注意mappings字段,這實(shí)際上就是bas64編碼格式的內(nèi)容,,當(dāng)然你直接去解,,是會(huì)失敗的。
具體的實(shí)現(xiàn)請看官方的base64-vlq.js[6]文件,。 混淆加密代碼著名的代碼混淆庫,, javascript-obfuscator[7],其也是有應(yīng)用base64幾碼的,,一起看看選項(xiàng): --string-array-indexes-type '<list>' (comma separated) [hexadecimal-number, hexadecimal-numeric-string] 其他X.509公鑰證書, github SSH key,, mht文件,,郵件附件等等,都有Base64的影子,。 Base64數(shù)據(jù)編碼起源早期郵件傳輸協(xié)議基于 ASCII 文本,,對于諸如圖片、視頻等二進(jìn)制文件處理并不好,。ASCII 主要用于顯示現(xiàn)代英文,,到目前為止只定義了 128 個(gè)字符,包含控制字符和可顯示字符,。為了解決上述問題,,Base64 編碼順勢而生。 Base64是編解碼,,主要的作用不在于安全性,,而在于讓內(nèi)容能在各個(gè)網(wǎng)關(guān)間無錯(cuò)的傳輸,這才是Base64編碼的核心作用,。 除了Base64數(shù)據(jù)編碼,,其實(shí)還有Base32數(shù)據(jù)編碼, Base16數(shù)據(jù)編碼,可以參見 RFC 4648[9],。 Base64編碼64的含義64就是64個(gè)字符的意思,。 base64對照表, 借用 Base64原理[10]的一張圖:
當(dāng)然還有一個(gè)字符 對照表的索引值,,注意一下,,后面的base64編碼和解碼會(huì)用到。 Base64編碼優(yōu)缺點(diǎn)優(yōu)點(diǎn)
缺點(diǎn)
說完優(yōu)缺點(diǎn),,回到正題: 我們今天的重點(diǎn)是 uf8編碼轉(zhuǎn)Base64編碼: 基本流程
在之前要解一下編碼的知識(shí), 了解編碼知識(shí),又要先了解一些計(jì)算機(jī)的基礎(chǔ)知識(shí),。 一些計(jì)算機(jī)和前端基礎(chǔ)知識(shí)比特和字節(jié)比特又叫位,。在計(jì)算機(jī)的世界里,信息的表示方式只有 0 和 1, 其可以表示兩種狀態(tài),。 一個(gè)字節(jié) 所以一個(gè)字節(jié)可以表示 獲得字符的 Unicode碼點(diǎn)String.prototype.charCodeAt[11] 可以獲取字符的碼點(diǎn),,獲取范圍為
進(jìn)制表示
注意 0b11111111 // 255
進(jìn)制轉(zhuǎn)換10進(jìn)制轉(zhuǎn)其他進(jìn)制 100..toString(2) // 1100100 其他進(jìn)制轉(zhuǎn)為10進(jìn)制
這里額外提一下一元操作符號(hào) +'1000' // 1000 位移操作本文只涉及右移操作,就只講右移,,右移相當(dāng)于除以2,,如果是整數(shù),簡單說是去低位,,移動(dòng)幾位去掉幾位,,其實(shí)和10進(jìn)制除以10是一樣的,。
一元 |
Unicode 碼點(diǎn)范圍(十六進(jìn)制) | 十進(jìn)制范圍 | UTF-8 編碼方式(二進(jìn)制) | 字節(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對應(yīng)表[15]這里找,而等于4
的可以去這看看Unicode? 13.0 Versioned Charts Index[16]
下面這些碼點(diǎn)都處于0000 0080 ~ 0000 07FF
, utf-8編碼需要2個(gè)字節(jié)
下面這些碼點(diǎn)都處于0001 0000 ~ 0010 FFFF
, utf-8編碼需要4個(gè)字節(jié)
可能這里光說不好理解,,我們分別以英文字符a
和中文字符掘
來講解一下:
為了驗(yàn)證結(jié)果,,可以去 Convert UTF8 to Binary Bits - Online UTF8 Tools[17]
a
'a'.charCodeAt(0)
等于 97
1
個(gè)字節(jié)97..toString(2)
得到編碼 1100001
0xxxxxxx
進(jìn)行填充,, 最終結(jié)果01100001
掘
'掘'.charCodeAt(0)
等于 25496
3
個(gè)字節(jié)25496..toString(2)
得到編碼 110 001110 011000
1110xxxx 10xxxxxx 10xxxxxx
進(jìn)行填充, 最終結(jié)果如下11100110 10001110 10011000
Convert UTF8 to Binary Bits - Online UTF8 Tools[18]執(zhí)行結(jié)果:完全匹配
基于上面的表格和轉(zhuǎn)換過程,,我們抽象一個(gè)方法,這個(gè)方法在之后的Base64編碼和解碼至關(guān)重要:
先看看功能,,覆蓋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++) {
//獲取麻點(diǎn)
code = str.charCodeAt(n);
if (code < 0x007F) { // 1個(gè)字節(jié)
// 0000 0000 ~ 0000 007F 0 ~ 127 1個(gè)字節(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個(gè)字節(jié)
// 0x0080 的二進(jìn)制為 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個(gè)字節(jié)
// 0x0800的二進(jìn)制為 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個(gè)字節(jié)
// https://www./charts/PDF/Unicode-13.0/U130-2F800.pdf
throw new TypeError('暫不支持碼點(diǎn)大于65535的字符')
}
}
return result;
}
方法中有三個(gè)地方稍微難理解一點(diǎn),,我們一起來解讀一下:
(code >> 6) | 0b11000000
其作用是生成高位二進(jìn)制。
我們以實(shí)際的一個(gè)栗子來講解,,以?
為例,,其碼點(diǎn)為0x633
,在0000 0080 ~ 0000 07FF
之間,,占兩個(gè)字節(jié), 在其二進(jìn)制編碼為11 000110011
,, 其填充格式如下, 低位要用6位,。
110xxxxx 10xxxxxx
為了方便觀察,,我們把 11 000110011
重新調(diào)整一下 11000 110011
。
(code >> 6) 等于 00110011 >> 6,,右移6位,, 直接干掉低6位。為什么是6呢,,因?yàn)榈臀恍枰?位,,右移動(dòng)6位后,剩下的就是用于高位操作的位了,。
11000000
11000 | 110011
--------------
11011000
(code & 0b111111) | 0b10000000
?
為例,,11000 110011
, 填充格式 110xxxxx 10xxxxxx
(code & 0b111111)這步的操作是為了干掉6位以上的高位,,僅僅保留低6位
。一元&
符號(hào),,兩邊都是1的時(shí)候才會(huì)是1,,妙啊。
11000 110011
111111
------------------
110011
接著進(jìn)行 | 0b10000000
, 主要是按照格式10xxxxxx
進(jìn)行位數(shù)填補(bǔ), 讓其滿8位,。
11000 110011
111111 (code & 0b111111)
------------------
110011
10 000000 (code & 0b111111) | 0b10000000
-------------------
10 110011
4
步的值作為索引,,去ASCII碼表找對應(yīng)的值2
步添加字節(jié)數(shù)個(gè)數(shù)的 =
2
添加了2個(gè)字節(jié),,后面是2個(gè)=
以大掘A
為例, 我們通過上面的utf8_to_binary
方法得到utf8的編碼11100110 10001110 10011000 11000001
, 其字節(jié)數(shù)不能被3整除,后面填補(bǔ)
11100110
10001110
10011000
01000001
--------
00000000
00000000
6位一組分為四組,, 高位補(bǔ)0
, 用|
分割一下填補(bǔ)的,。
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 => => =
結(jié)果是:5o6YQQ==
,, 完美。
基于上面的to_binary
方法和base64的轉(zhuǎn)換規(guī)則,,就很簡單啦:
先看看執(zhí)行效果,very good, 和 base64.us[19] 結(jié)果完全一致,。
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萬元獎(jiǎng)池等你挑戰(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;
// 需要填補(bǔ)的=的數(shù)量
let paddingCharLen = len % 24 !== 0 ? (24 - len % 24) / 8 : 0;
//6個(gè)一組
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;
}
至于解碼,,是其逆過程,,留給大家去實(shí)現(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'
其支持到6字節(jié),,但是可讀性并不好,。
雖然有那么多成熟的,但是我們理解和自己實(shí)現(xiàn),,才能更明白Base64的編碼原理,。
借用[你真的了解 Unicode 和 UTF-8 嗎,?[23]]一張圖:
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 中關(guān)于 base64 的一些事[36]
Base64 的原理、實(shí)現(xiàn)及應(yīng)用[37]
圖片與Base64換算關(guān)系[38]
[你真的了解 Unicode 和 UTF-8 嗎,?[39]]
Unicode中UTF-8與UTF-16編碼詳解[40]
Unicode對應(yīng)表[41]
JavaScript Source Map 詳解[42]
|