<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          詳解 Webpack devtools

          共 8113字,需瀏覽 17分鐘

           ·

          2022-10-09 11:49

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

          SourceMap 原理

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

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

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

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

          {
            // 版本號(hào)
            "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"
            ],
            // 源文件列表
            // 我們打包的時(shí)候經(jīng)常是多個(gè)js文件合并成一個(gè),所以源文件有多個(gè)
            "sources": [
              "webpack:///./src/index.ts"
            ],
            // 源文件內(nèi)容的列表,與sources字段對應(yīng)
            "sourcesContent": [
              "const dog = {\n  name: '旺財(cái)',\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 進(jìn)行編碼的字符串,規(guī)則比較復(fù)雜。我們可以直接在 github 找一個(gè)VLQ(https://github.com/Rich-Harris/vlq/blob/master/src/index.js)編碼的庫,對這串字符進(jìn)行解碼。

          /** @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 字符串一般通過分號(hào)(;)和逗號(hào)(,)進(jìn)行分隔。每個(gè)分號(hào)分隔的部分對應(yīng)壓縮后代碼的每一行。因?yàn)樯厦娲虬拇a經(jīng)過了壓縮,只有一行代碼,所以這個(gè) mappings 中就沒有分號(hào)。而通過逗號(hào)進(jìn)行分割的部分表示壓縮后代碼當(dāng)前行的某一列與源代碼的對應(yīng)關(guān)系。

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

          '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)五個(gè)數(shù)字,這個(gè)五個(gè)數(shù)字分別對應(yīng)下面的含義:

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

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

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

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

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

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

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

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

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

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

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

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

          devtools 配置項(xiàng)

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

          格式如下:

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

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

          source-map

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

          //# sourceMappingURL=SourceMap文件路徑

          inline-source-map

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

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

          eval/eval-source-map

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

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

          通過 eval 生成代碼的好處,改動(dòng)了某個(gè)模塊,只需要對某個(gè)模塊的代碼重新 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 精簡了很多。因?yàn)?cheap-source-map 去掉了列信息,可以大幅提高 souremap 生成的效率。

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

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

          const dog: {
            name: string,
            weight: number
          } = {
            name: '旺財(cái)',
            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 進(jìn)行還原出的代碼:

          hidden-source-map

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

          小結(jié)

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

          - END -


          瀏覽 67
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  99热6在线观看 | 黄网站视频免费 | 狠狠爱大香蕉 | 国际亚洲中文字幕最新网址 | 小黄片在线视频 |