1. 前端Base64編碼知識,一文打盡

        共 17782字,需瀏覽 36分鐘

         ·

        2021-08-22 23:51

        原文: 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做測試。

        image.png

        網(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>

        image.png

        其他

        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]的一張圖:

        1. A-Z 26
        2. a-z 26
        3. 0-9 10
        4. + / 2

        26 + 26 + 10 + 2 = 64

        當然還有一個字符=,這是填充字符,后面會提到,不屬于64里面的范疇。

        對照表的索引值,注意一下,后面的base64編碼和解碼會用到。

        Base64編碼優(yōu)缺點

        優(yōu)點

        1. 可以將二進制數(shù)據(jù)(比如圖片)轉化為可打印字符,方便傳輸數(shù)據(jù)
        2. 對數(shù)據(jù)進行簡單的加密,肉眼是安全的
        3. 如果是在html或者css處理圖片,可以減少http請求

        缺點

        1. 內容編碼后體積變大, 至少1/3
          因為是三字節(jié)變成四個字節(jié),當只有一個字節(jié)的時候,也至少會變成三個字節(jié)。
        2. 編碼和解碼需要額外工作量

        說完優(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

        進制表示

        1. 0b開頭,可以表示二進制

        注意0b10000000= 128 ,0b11000000=92,之后會用到.

        0b11111111 // 255
        0b10000000 // 128 后面會用到
        0b11000000 // 192 后面會用到

        image.png
        1. 0x開頭,可以表示16進制
        0x11111111 // 286331153

        image.png

        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ī)則:

        1. 對于只有一個字節(jié)的符號,字節(jié)的第一位設為0,后面 7 位為這個符號的 Unicode 碼。此時,對于英語字母UTF-8 編碼和 ASCII 碼是相同的。
        2. 對于 n 字節(jié)的符號(n > 1),第一個字節(jié)的前 n 位都設為 1,第 n + 1 位設為0,后面字節(jié)的前兩位一律設為 10。剩下的沒有提及的二進制位,全部為這個符號的 Unicode 碼,如下表所示:
        Unicode 碼點范圍(十六進制)十進制范圍UTF-8 編碼方式(二進制)字節(jié)數(shù)
        0000 0000 ~ 0000 007F0 ~ 1270xxxxxxx1
        0000 0080 ~ 0000 07FF128 ~ 2047110xxxxx 10xxxxxx2
        0000 0800 ~ 0000 FFFF2048 ~ 655351110xxxx 10xxxxxx 10xxxxxx3
        0001 0000 ~ 0010 FFFF65536 ~ 111411111110xxx 10xxxxxx 10xxxxxx 10xxxxxx4

        我們可能沒見過字節(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

        1. 先獲得其碼點,"a".charCodeAt(0) 等于 97
        2. 對照表格, 0~127, 需1個字節(jié)
        3. 97..toString(2) 得到編碼 1100001
        4. 根據(jù)格式0xxxxxxx進行填充, 最終結果
        01100001

        中文字符

        1. 先獲得其碼點,"掘".charCodeAt(0) 等于 25496
        2. 對照表格,2048 ~ 65535 需3個字節(jié)
        3. 25496..toString(2) 得到編碼 110 001110 011000
        4. 根絕格式1110xxxx 10xxxxxx 10xxxxxx進行填充, 最終結果如下
        11100110 10001110 10011000

        Convert UTF8 to Binary Bits - Online UTF8 Tools[18]執(zhí)行結果:完全匹配

        image.png

        抽象把字符轉為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;
        }


        方法中有三個地方稍微難理解一點,我們一起來解讀一下:

        1. 二字節(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      

        1. 二字節(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ī)則

        1. 獲取每個字符的Unicode碼,轉為utf-8編碼
        2. 三個字節(jié)作為一組,一共是24個二進制位
          字節(jié)數(shù)不能被 3 整除,用0字節(jié)值在末尾補足
        3. 按照6個比特位一組分組,前兩位補0,湊齊8位
        4. 計算每個分組的數(shù)值
        5. 以第4步的值作為索引,去ASCII碼表找對應的值
        6. 替換第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)吧。

        其他的成熟方案

        1. 當然是基于已有的 btoaatob,

        但是 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"

        1. MDN的 rewriting `atob()` and `btoa()` using `TypedArray`s and UTF-8[20]

        其支持到6字節(jié),但是可讀性并不好。

        1. 第三方庫 base64-js[21] 與 js-base64[22]都是周下載量過百萬的庫。

        雖然有那么多成熟的,但是我們理解和自己實現(xiàn),才能更明白Base64的編碼原理。

        額外補充一點

        1. 編碼關系圖

        借用[你真的了解 Unicode 和 UTF-8 嗎?[23]]一張圖:

        image.png
        1. 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ā),我想邀請你幫我三個小忙:
        • 點個【在看】,或者分享轉發(fā),讓更多的人也能看到這篇內容
        • 關注公眾號【趣談前端】,定期分享 工程化 可視化 / 低代碼 / 優(yōu)秀開源。




        從零搭建全棧可視化大屏制作平臺V6.Dooring

        從零設計可視化大屏搭建引擎

        Dooring可視化搭建平臺數(shù)據(jù)源設計剖析

        可視化搭建的一些思考和實踐

        基于Koa + React + TS從零開發(fā)全棧文檔編輯器



        點個在看你最好看


        瀏覽 79
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
          
          

            1. 一级大片网址 | 91性中国毛片 潮喷 | 男女操逼免费观看 | 日韩无码啪啪 | 小骚逼操逼 |