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

成都創(chuàng)新互聯(lián)自2013年創(chuàng)立以來,先為瓊海等服務(wù)建站,瓊海等地企業(yè),進(jìn)行企業(yè)商務(wù)咨詢服務(wù)。為瓊海企業(yè)網(wǎng)站制作PC+手機(jī)+微官網(wǎng)三網(wǎng)同步一站式服務(wù)解決您的所有建站問題。
SourceMap 原理
在詳細(xì)解釋 devtools 配置之前,先看看 SourceMap 的原理。SourceMap 的主要作用就是用來還原代碼,將已經(jīng)編譯壓縮的代碼,還原成之前的代碼。
下圖左邊代碼為 Webpack 打包之前,右邊為打包之后。
打開 chrome 引入 dist.js ,會發(fā)現(xiàn)瀏覽器會自動將壓縮的代碼進(jì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 進(jìn)行編碼的字符串,規(guī)則比較復(fù)雜。我們可以直接在 github 找一個VLQ(https://github.com/Rich-Harris/vlq/blob/master/src/index.js)編碼的庫,對這串字符進(jìn)行解碼。
/** @type {Record} */
let char_to_integer = {};
/** @type {Record} */
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 字符串一般通過分號(;?)和逗號(,)進(jìn)行分隔。每個分號分隔的部分對應(yīng)壓縮后代碼的每一行。因?yàn)樯厦娲虬拇a經(jīng)過了壓縮,只有一行代碼,所以這個 mappings 中就沒有分號。而通過逗號進(jìn)行分割的部分表示壓縮后代碼當(dāng)前行的某一列與源代碼的對應(yīng)關(guān)系。
我們試著通過上面的代碼,對 mappings 的前面一部分進(jìn)行解碼。
'MAAA,IAAMA,EAAM,CACVC,KAAM'.split(',').forEach((str) => {
console.log(decode(str))
})解碼結(jié)果如下:
[ 6, 0, 0, 0 ] // MAAA
[ 4, 0, 0, 6, 0 ] // IAAMA
[ 2, 0, 0, 6 ] // EAAM
[ 1, 0, 1, -10, 1 ] // CACVC
[ 5, 0, 0, 6 ] // KAAM
每一串字符都對應(yīng)五個數(shù)字,這個五個數(shù)字分別對應(yīng)下面的含義:
第一位,表示這個位置壓縮代碼的第幾列(與前面的數(shù)字累加獲?。?/p>
第二位,表示這個位置屬于sources屬性中的哪一個文件。
第三位,表示這個位置屬于源碼的第幾行(與前面的數(shù)字累加獲?。?。
第四位,表示這個位置屬于源碼的第幾列(與前面的數(shù)字累加獲取)。
第五位,表示這個位置屬于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"。這里對代碼進(jìn)行了混淆,所以有個 names 字段專門用來記錄壓縮之前的變量名。
簡單翻譯一下前面的解碼結(jié)果:
[ 6, 0, 0, 0 ] // 壓縮代碼的第7列,對應(yīng)源碼第1行的第1列
[ 4, 0, 0, 6, 0 ] // 壓縮代碼的第11列,對應(yīng)源碼第1行的第7列,對應(yīng)names第1個變量("dog")
[ 2, 0, 0, 6 ] // 壓縮代碼的第13列,對應(yīng)源碼第1行的第13列
[ 1, 0, 1, -10, 1 ] // 壓縮代碼的第14列,對應(yīng)源碼第2行的第3列,對應(yīng)names第2個變量("name")
[ 5, 0, 0, 6 ] // 壓縮代碼的第19列,對應(yīng)源碼第2行的第9列
可以看到這里面出現(xiàn)了一個負(fù)數(shù),這里是因?yàn)閷?yīng)關(guān)系從源碼的第1行,跳到了第2行,新的一行列數(shù)應(yīng)該從前面開始計算,而列數(shù)是按照前面的結(jié)果累加的,所以這里要進(jìn)行列數(shù)的回退,所以出現(xiàn)了一個負(fù)數(shù),將列數(shù)進(jìn)行回退。
上面是代碼經(jīng)過壓縮處理的情況,如果我們只通過webpack進(jìn)行打包處理,不進(jìn)行壓縮,生成的 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 配置項(xiàng)
在了解了 SourceMap 的原理后,在看看 devtools 的配置項(xiàng)。如果看 Webpack 的官方文檔,會發(fā)現(xiàn) devtools? 的配置項(xiàng)是一個有十幾行的表格,有點(diǎn)唬人,仔細(xì)觀察會發(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"?),就是單獨(dú)生成一個 .map 文件,然后在打包代碼的最后一行加上一個注釋,寫明生成 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 會將對應(yīng)模塊的代碼都放到 eval()? 中執(zhí)行,如果加上了 //# sourceURL=xxx ,瀏覽器會自動將 eval 中的代碼自動放到 sources 中。
eval中的代碼在sources中也能看到
通過 eval 生成代碼的好處,改動了某個模塊,只需要對某個模塊的代碼重新 eval 就可以,可以提升二次編譯的效率。官方文檔也有說明,eval? 的 rebuild 的效率基本是最高的。
cheap-source-map/cheap-module-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-map? 和 cheap-source-map? 生成的 mappings 的區(qū)別,可以看到 cheap-source-map? 生成的 mappings 精簡了很多。因?yàn)?nbsp;cheap-source-map 去掉了列信息,可以大幅提高 souremap 生成的效率。
在 webpack 打包的過程中,代碼會經(jīng)過許多 loader 處理,而 loader 處理的過程中,對應(yīng)的代碼映射關(guān)系可能會發(fā)生變化,而 cheap-module-source-map 的作用就是打包后的代碼是與最開始的代碼進(jìn)行對應(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 進(jìn)行還原出的代碼:
hidden-source-map
與 source-map? 配置一樣,會單獨(dú)生成一個 .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?。如果需要進(jìn)一步提升編譯速度,就可以配置成 eval-cheap-module-source-map?。而在發(fā)布上線的時候,就可以將配置調(diào)整成 hidden-cheap-module-source-map。
網(wǎng)頁名稱:詳解Webpackdevtools
網(wǎng)址分享:http://www.dlmjj.cn/article/djhspph.html


咨詢
建站咨詢
