1. 詳解 Webpack devtools

        共 8113字,需瀏覽 17分鐘

         ·

        2022-10-09 11:49

        最近在開發(fā)一個低代碼平臺,主要用于運營搭建 H5 活動。這中間涉及到第三方組件的開發(fā),而第三方組件想要接入平臺,需要經(jīng)過我們特定的打包工具來build。構(gòu)建之后的組件,會合并成單個的 js 文件,而且代碼會被壓縮會混淆,這個時候如果需要調(diào)試,那就會極其痛苦。想要有一個好的調(diào)試環(huán)境,就要涉及 SourceMap 的輸出,而 Webpack 的 devtools 字段就是用于控制 SourceMap。

        SourceMap 原理

        在詳細解釋 devtools 配置之前,先看看 SourceMap 的原理。SourceMap 的主要作用就是用來還原代碼,將已經(jīng)編譯壓縮的代碼,還原成之前的代碼。

        下圖左邊代碼為 Webpack 打包之前,右邊為打包之后。

        打開 chrome 引入 dist.js ,會發(fā)現(xiàn)瀏覽器會自動將壓縮的代碼進行了還原。

        那這個 SourceMap 到底是怎么將右邊的代碼還原成左邊的樣子的呢。我們先看一下 dist.js.map 的結(jié)構(gòu)。

        {
          // 版本號
          "version"3,
          // 輸出的文件名
          "file""dist.js",
          // 輸出代碼與源代碼的映射關(guān)系
          "mappings""MAAA,IAAMA,EAAM,CACVC,KAAM,KACNC,OAAQ,KAGV,SAASC,IACPH,EAAIE,QAAU,EAGhB,SAASE,IACPJ,EAAIE,QAAU,EACdG,QAAQC,IAAIN,EAAIC,KAAM,OAGxBE,IACAC,IACAA,IACAD,K",
          // 原代碼中的一些變量名
          "names": [
            "dog""name""weight",
            "eat""call""console""log"
          ],
          // 源文件列表
          // 我們打包的時候經(jīng)常是多個js文件合并成一個,所以源文件有多個
          "sources": [
            "webpack:///./src/index.ts"
          ],
          // 源文件內(nèi)容的列表,與sources字段對應(yīng)
          "sourcesContent": [
            "const dog = {\n  name: '旺財',\n  weight: 100\n}\n\nfunction eat() {\n  dog.weight += 1\n}\n\nfunction call() {\n  dog.weight -= 1\n  console.log(dog.name, '汪汪汪')\n}\n\neat()\ncall()\ncall()\neat()"
          ],
        }

        其他字段應(yīng)該都好理解,比較難懂的就是 mappings 字段,看著就像是一堆亂碼。這是一串使用 VLQ 進行編碼的字符串,規(guī)則比較復(fù)雜。我們可以直接在 github 找一個VLQ(https://github.com/Rich-Harris/vlq/blob/master/src/index.js)編碼的庫,對這串字符進行解碼。

        /** @type {Record<string, number>} */
        let char_to_integer = {};

        /** @type {Record<number, string>} */
        let integer_to_char = {};

        'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
         .split('')
         .forEach(function (char, i{
          char_to_integer[char] = i;
          integer_to_char[i] = char;
         });

        /** @param {string} string */
        function decode(string{
         /** @type {number[]} */
         let result = [];

         let shift = 0;
         let value = 0;

         for (let i = 0; i < string.length; i += 1) {
          let integer = char_to_integer[string[i]];

          if (integer === undefined) {
           throw new Error('Invalid character (' + string[i] + ')');
          }

          const has_continuation_bit = integer & 32;

          integer &= 31;
          value += integer << shift;

          if (has_continuation_bit) {
           shift += 5;
          } else {
           const should_negate = value & 1;
           value >>>= 1;

           if (should_negate) {
            result.push(value === 0 ? -0x80000000 : -value);
           } else {
            result.push(value);
           }

           // reset
           value = shift = 0;
          }
         }

         return result;
        }

        mappings 字符串一般通過分號(;)和逗號(,)進行分隔。每個分號分隔的部分對應(yīng)壓縮后代碼的每一行。因為上面打包的代碼經(jīng)過了壓縮,只有一行代碼,所以這個 mappings 中就沒有分號。而通過逗號進行分割的部分表示壓縮后代碼當(dāng)前行的某一列與源代碼的對應(yīng)關(guān)系。

        我們試著通過上面的代碼,對 mappings 的前面一部分進行解碼。

        'MAAA,IAAMA,EAAM,CACVC,KAAM'.split(',').forEach((str) => {
         console.log(decode(str))
        })

        解碼結(jié)果如下:

        6000 ]       // MAAA
        40060 ]    // IAAMA
        2006 ]       // EAAM
        101-101 ]  // CACVC
        5006 ]       // KAAM

        每一串字符都對應(yīng)五個數(shù)字,這個五個數(shù)字分別對應(yīng)下面的含義:

        1. 第一位,表示這個位置壓縮代碼的第幾列(與前面的數(shù)字累加獲取)。

        2. 第二位,表示這個位置屬于sources屬性中的哪一個文件。

        3. 第三位,表示這個位置屬于源碼的第幾行(與前面的數(shù)字累加獲取)。

        4. 第四位,表示這個位置屬于源碼的第幾列(與前面的數(shù)字累加獲?。?/p>

        5. 第五位,表示這個位置屬于names屬性中的哪一個變量。

        那么 MAAA: [ 6, 0, 0, 0 ]: 對應(yīng)的意思就是,壓縮后代碼的第1行的第7列(PS. 計數(shù)都是從0開始,所以數(shù)字6對應(yīng)的應(yīng)該是第7列,后面的數(shù)字同理),對應(yīng)sources中的第1個文件的第1行的第1列。看代碼能看出,就是表示壓縮后的這個 var 聲明,對應(yīng)源碼的 const。

        在看看 IAAMA: [ 4, 0, 0, 6, 0 ],表示壓縮代碼的第11列(這里的4,表示從前面已計算的列向后再數(shù)4列,也就是第11列),對應(yīng)源碼第1行的第7列(這里同理,也是向后數(shù)6列),且對應(yīng) names 屬性的第1個變量名,也就是 "dog"。這里對代碼進行了混淆,所以有個 names 字段專門用來記錄壓縮之前的變量名。

        簡單翻譯一下前面的解碼結(jié)果:

        6000 ] // 壓縮代碼的第7列,對應(yīng)源碼第1行的第1列
        40060 ] // 壓縮代碼的第11列,對應(yīng)源碼第1行的第7列,對應(yīng)names第1個變量("dog")
        2006 ] // 壓縮代碼的第13列,對應(yīng)源碼第1行的第13列
        101-101 ] // 壓縮代碼的第14列,對應(yīng)源碼第2行的第3列,對應(yīng)names第2個變量("name")
        5006 ] // 壓縮代碼的第19列,對應(yīng)源碼第2行的第9列

        可以看到這里面出現(xiàn)了一個負數(shù),這里是因為對應(yīng)關(guān)系從源碼的第1行,跳到了第2行,新的一行列數(shù)應(yīng)該從前面開始計算,而列數(shù)是按照前面的結(jié)果累加的,所以這里要進行列數(shù)的回退,所以出現(xiàn)了一個負數(shù),將列數(shù)進行回退。

        上面是代碼經(jīng)過壓縮處理的情況,如果我們只通過webpack進行打包處理,不進行壓縮,生成的 mappings 如下:

        可以看到,dist.js 前面5行代碼都是 webpack 生成的 runtime,與源代碼無關(guān),所以 mappings 前面有五個分號(;),表示前 5 行與源碼沒有對應(yīng)關(guān)系,后面的 AAAA,IAAMA,GAAG,GAAG; 才是 dist.js 第六行與源碼的對應(yīng)關(guān)系。

        devtools 配置項

        在了解了 SourceMap 的原理后,在看看 devtools 的配置項。如果看 Webpack 的官方文檔,會發(fā)現(xiàn) devtools 的配置項是一個有十幾行的表格,有點唬人,仔細觀察會發(fā)現(xiàn),devtools 配置以 "source-map" 為基礎(chǔ),然后加上各種前綴。

        格式如下:

        [inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map

        不同的配置會生成不同的產(chǎn)物,在 webpack 的 github 倉庫中,有一個專門的demo用于展示不同參數(shù)打包后的產(chǎn)物:https://github.com/webpack/webpack/tree/main/examples/source-map。

        source-map

        先看最基礎(chǔ)的配置(devtools: "source-map"),就是單獨生成一個 .map 文件,然后在打包代碼的最后一行加上一個注釋,寫明生成 SourceMap 的路徑,方便瀏覽器讀取。

        //# sourceMappingURL=SourceMap文件路徑

        inline-source-map

        看名字很容易理解,在前面加上 inline- 屬于內(nèi)聯(lián)的 SourceMap,就是將 SourceMap 的內(nèi)容進行 base64 轉(zhuǎn)義,直接放到打包代碼的最后一行。

        //# sourceMappingURL=data:application/json;charset=utf-8;.......

        eval/eval-source-map

        eval-source-map 會將對應(yīng)模塊的代碼都放到 eval() 中執(zhí)行,如果加上了 //# sourceURL=xxx ,瀏覽器會自動將 eval 中的代碼自動放到 sources 中。

        eval中的代碼在sources中也能看到

        通過 eval 生成代碼的好處,改動了某個模塊,只需要對某個模塊的代碼重新 eval 就可以,可以提升二次編譯的效率。官方文檔也有說明,evalrebuild 的效率基本是最高的。

        cheap-source-map/cheap-module-source-map

        // source-map
        "mappings"";;;;;AAAA,IAAMA,GAGL,GAAG;EACFC,IAAI,EAAE,IADJ;EAEFC,MAAM,EAAE;AAFN,CAHJ;;AAQA,SAASC,GAAT,CAAaD,MAAb,EAA6B;EAC3BF,GAAG,CAACE,MAAJ,IAAcA,MAAd;AACD;;AAED,SAASE,IAAT,GAAgB;EACdJ,GAAG,CAACE,MAAJ,IAAc,CAAd;EACAG,OAAO,CAACC,GAAR,CAAYN,GAAG,CAACC,IAAhB,EAAsB,KAAtB;AACD;;AAEDE,GAAG,CAAC,EAAD,CAAH;AACAC,IAAI;AACJA,IAAI;AACJD,GAAG,CAAC,CAAD,CAAH,C"

        // cheap-source-map
        "mappings"";;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA"

        上面是通過 source-mapcheap-source-map 生成的 mappings 的區(qū)別,可以看到 cheap-source-map 生成的 mappings 精簡了很多。因為 cheap-source-map 去掉了列信息,可以大幅提高 souremap 生成的效率。

        在 webpack 打包的過程中,代碼會經(jīng)過許多 loader 處理,而 loader 處理的過程中,對應(yīng)的代碼映射關(guān)系可能會發(fā)生變化,而 cheap-module-source-map  的作用就是打包后的代碼是與最開始的代碼進行對應(yīng)的,而不是經(jīng)過 loader 處理的代碼。

        我們先寫一段 typescript 代碼,如下:

        const dog: {
          name: string,
          weight: number
        } = {
          name: '旺財',
          weight: 100
        }
        function eat(weight: number{
          dog.weight += weight
        }
        function call({
          dog.weight -= 1
          console.log(`${dog.name}: 汪汪汪`)
        }

        eat(10)
        call()
        call()
        eat(5)

        先看看直接使用 cheap-source-map 還原出的代碼:

        在看看 cheap-module-source-map 進行還原出的代碼:

        hidden-source-map

        source-map 配置一樣,會單獨生成一個 .map 文件,只是打包代碼的最后沒有與之關(guān)聯(lián)的注釋,一般生產(chǎn)發(fā)布的時候,將 .map 文件上傳到報錯平臺(例如:sentry)。另外,如果配置了多個 loader,可以考慮在上線時,將 devtools 配置成 hidden-cheap-module-source-map。

        小結(jié)

        上面介紹了各種配置輸出代碼的特性,每一種都是能排列組合的。比如,在開發(fā)環(huán)境,為了盡可能的看到未經(jīng)過 loader 轉(zhuǎn)化的原代碼,可以配置成 cheap-module-source-map。如果需要進一步提升編譯速度,就可以配置成 eval-cheap-module-source-map。而在發(fā)布上線的時候,就可以將配置調(diào)整成 hidden-cheap-module-source-map

        - END -


        瀏覽 67
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

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

        手機掃一掃分享

        分享
        舉報
          
          

            1. frexx性欧美 | 五月色婷婷超清 | 免费专区 - 色哟哟 | 国产精品熟女一区二区不卡 | 午夜xxx|