配置Webpack?SourceMap?實踐教程
Source Map 簡介
Source Map(源代碼地圖)就是解決此類問題最好的辦法,從它的名字就能夠看出它的作用:映射轉(zhuǎn)換后的代碼與源代碼之間的關(guān)系。一段轉(zhuǎn)換后的代碼,通過轉(zhuǎn)換過程中生成的 Source Map 文件就可以逆向解析得到對應(yīng)的源代碼。
目前很多第三方庫在發(fā)布的文件中都會同時提供一個 .map 后綴的 Source Map 文件。例如 jQuery。我們可以打開它的 Source Map 文件看一下,如下圖所示:
這是一個 JSON 格式的文件,為了更容易閱讀,我提前對該文件進(jìn)行了格式化。這個 JSON 里面記錄的就是轉(zhuǎn)換后和轉(zhuǎn)換前代碼之間的映射關(guān)系,主要存在以下幾個屬性:
- version 是指定所使用的 Source Map 標(biāo)準(zhǔn)版本;
- sources 中記錄的是轉(zhuǎn)換前的源文件名稱,因為有可能出現(xiàn)多個文件打包轉(zhuǎn)換為一個文件的情況,所以這里是一個數(shù)組;
- names 是源代碼中使用的一些成員名稱,我們都知道一般壓縮代碼時會將我們開發(fā)階段編寫的有意義的變量名替換為一些簡短的字符,這個屬性中記錄的就是原始的名稱;
- mappings 屬性,這個屬性最為關(guān)鍵,它是一個叫作 base64-VLQ 編碼的字符串,里面記錄的信息就是轉(zhuǎn)換后代碼中的字符與轉(zhuǎn)換前代碼中的字符之間的映射關(guān)系,具體如下圖所示:
一般我們會在轉(zhuǎn)換后的代碼中通過添加一行注釋的方式來去引入 Source Map 文件。不過這個特性只是用于開發(fā)調(diào)試的,所以最新版本的 jQuery 已經(jīng)去除了引入 Source Map 的注釋,我們需要手動添加回來,這里我們在最后一行添加 //# sourceMappingURL=jquery-3.4.1.min.map,具體效果如下:
這樣我們在 Chrome 瀏覽器中如果打開了開發(fā)人員工具,它就會自動請求這個文件,然后根據(jù)這個文件的內(nèi)容逆向解析出來源代碼,以便于調(diào)試。同時因為有了映射關(guān)系,所以代碼中如果出現(xiàn)了錯誤,也就能自動定位找到源代碼中的位置了。
我們回到瀏覽器中,打開開發(fā)人員工具,找到 Source 面板,這里我們就能看到轉(zhuǎn)換前的 jQuery 源代碼了,具體效果如下圖所示:
我們還可以添加一個斷點,然后刷新頁面,進(jìn)行單步調(diào)試,此時調(diào)試過程中使用的就是源代碼而不是壓縮過后的代碼,具體效果如下圖所示:
Webpack 中配置 Source Map
我們使用 Webpack 打包的過程,同樣支持為打包結(jié)果生成對應(yīng)的 Source Map。用法上也很簡單,不過它提供了很多不同模式,導(dǎo)致大部分初學(xué)者操作起來可能會比較懵。那接下來我們就一起研究一下在 Webpack 中如何開啟 Source Map,然后再來了解一下幾種不同的 Source Map 模式之間存在哪些差異。
我們回到配置文件中,這里我們要使用的配置屬性叫作 devtool。這個屬性就是用來配置開發(fā)過程中的輔助工具,也就是與 Source Map 相關(guān)的一些功能。我們可以先將這個屬性設(shè)置為 source-map,具體代碼如下:
// ./webpack.config.js module.exports = { devtool: 'source-map' // source map 設(shè)置 }
然后打開命令行終端,運(yùn)行 Webpack 打包。打包完成過后,我們打開 dist 目錄,此時這個目錄中就會生成我們 bundle.js 的 Source Map 文件,與此同時 bundle.js 中也會通過注釋引入這個 Source Map 文件,具體如下圖所示:
我們再回到命令行,通過 serve 工具把打包結(jié)果運(yùn)行起來,然后打開瀏覽器,再打開開發(fā)人員工具,此時我們就可以直接定位到錯誤所在的位置了。當(dāng)然如果需要調(diào)試,這里也可以直接調(diào)試源代碼。
如果你只是需要使用 Source Map 的話,操作到這里就已經(jīng)實現(xiàn)了。但是只會使用這種最普通的 Source Map 模式還遠(yuǎn)遠(yuǎn)不夠。
為什么這么說呢?
因為現(xiàn)階段 Webpack 支持的 Source Map 模式有很多種。每種模式下所生成的 Source Map 效果和生成速度都不一樣。顯然,效果好的一般生成速度會比較慢,而生成速度快的一般就沒有什么效果。
那具體哪種 Source Map 模式才是最好呢?這里我們還需要繼續(xù)去探索。
Webpack 中的 devtool 配置,除了可以使用 source-map 這個值,它還支持很多其他的選項,具體的我們可以參考文檔中的不同模式的對比表。
上表分別從初次構(gòu)建速度、監(jiān)視模式重新構(gòu)建速度、是否適合生成環(huán)境使用,以及 Source Map 的質(zhì)量,這四個維度去橫向?qū)Ρ攘瞬煌?Source Map 模式之間的差異。
通過表格中四個維度的對比你可能覺得不夠清晰,也不太好理解,所以接下來我們會根據(jù)表格中的介紹,通過實際操作來體會這些模式之間的差異,從而帶你找到適合自己的最佳實踐。
Eval 模式
首先來看 eval 模式。在去具體了解 Webpack eval 模式的 Source Map 之前,我們需要先了解一下 JavaScript 中 eval 的一些特點。
eval 其實指的是 JavaScript 中的一個函數(shù),可以用來運(yùn)行字符串中的 JavaScript 代碼。例如下面這段代碼,字符串中的 console.log("foo~") 就會作為一段 JavaScript 代碼被執(zhí)行:
const code = 'console.log("foo~")' eval(code) // 將 code 中的字符串作為 JS 代碼執(zhí)行
在默認(rèn)情況下,這段代碼運(yùn)行在一個臨時的虛擬機(jī)環(huán)境中,我們在控制臺中就能夠看到:
其實我們可以通過 sourceURL 來聲明這段代碼所屬文件路徑,接下來我們再來嘗試在執(zhí)行的 JavaScript 字符串中添加一個 sourceURL 的聲明,具體操作如下:
具體就是在 eval 函數(shù)執(zhí)行的字符串代碼中添加一個注釋,注釋的格式:# sourceURL=./path/to/file.js,這樣的話這段代碼就會執(zhí)行在指定路徑下。
在了解了 eval 函數(shù)可以通過 sourceURL 指定代碼所屬文件路徑這個特點過后,我們再來嘗試使用這個叫作 eval 模式的 Source Map。
我們回到 Webpack 的配置文件中,將 devtool 屬性設(shè)置為 eval,具體如下:
// ./webpack.config.js module.exports = { devtool: 'eval' }
然后我們回到命令行終端再次運(yùn)行打包,打包過后,找到生成的 bundle.js 文件,你會發(fā)現(xiàn)每個模塊中的代碼都被包裹到了一個 eval 函數(shù)中,而且每段模塊代碼的最后都會通過 sourceURL 的方式聲明這個模塊對應(yīng)的源文件路徑,具體如下:
那此時如果我們回到瀏覽器運(yùn)行這里的 bundle.js,一旦出現(xiàn)錯誤,瀏覽器的控制臺就可以定位到具體是哪個模塊中的代碼,具體效果如下:
但是當(dāng)你點擊控制臺中的文件名打開這個文件后,看到的卻是打包后的模塊代碼,而并非我們真正的源代碼,具體如下:
綜上所述,在 eval 模式下,Webpack 會將每個模塊轉(zhuǎn)換后的代碼都放到 eval 函數(shù)中執(zhí)行,并且通過 sourceURL 聲明對應(yīng)的文件路徑,這樣瀏覽器就能知道某一行代碼到底是在源代碼的哪個文件中。
因為在 eval 模式下并不會生成 Source Map 文件,所以它的構(gòu)建速度最快,但是缺點同樣明顯:它只能定位源代碼的文件路徑,無法知道具體的行列信息。
案例準(zhǔn)備工作
為了可以更好地對比不同模式的 Source Map 之間的差異,這里我們使用一個新項目,同時創(chuàng)建出不同模式下的打包結(jié)果,通過具體實驗來橫向?qū)Ρ人鼈冎g的差異。
在這個案例中,項目中只有兩個 JS 模塊,在 main.js 中,我故意加入了一個運(yùn)行時錯誤,具體項目結(jié)構(gòu)和部分代碼如下:
└─ 07-devtool-diff ├── src │ ├── heading.js │ └── main.js ├── package.json └── webpack.config.js
// ./src/main.js import createHeading from './heading.js' const heading = createHeading() document.body.append(heading) console.log('main.js running') // 運(yùn)行時錯誤 console.log111('main.js running')
然后我們打開 Webpack 的配置文件,在這個文件中定義一個數(shù)組,數(shù)組中每一個成員都是 devtool 配置取值的一種,具體代碼如下:
const allDevtoolModes = [ 'eval', 'cheap-eval-source-map', 'cheap-module-eval-source-map', 'eval-source-map', 'cheap-source-map', 'cheap-module-source-map', 'inline-cheap-source-map', 'inline-cheap-module-source-map', 'source-map', 'inline-source-map', 'hidden-source-map', 'nosources-source-map' ]
在上一課時中我們也提到過,Webpack 的配置文件除了可以導(dǎo)出一個配置對象,還可以導(dǎo)出一個數(shù)組,數(shù)組中每一個元素就是一個單獨(dú)的打包配置,那這樣就可以在一次打包過程中同時執(zhí)行多個打包任務(wù)。
例如,我們這里導(dǎo)出一個數(shù)組,然后在這個數(shù)組中添加兩個打包配置,它們的 entry 都是 src 中的 main.js,不過它們輸出的文件名不同,具體代碼如下:
// ./webpack.config.js module.exports = [ { entry: './src/main.js', output: { filename: 'output1.js' } }, { entry: './src/main.js', output: { filename: 'output2.js' } } ]
這么配置的話,再次打包就會有兩個打包子任務(wù)工作,我們的 dist 中生成的結(jié)果也就是兩個文件,具體結(jié)果如下:
了解了 Webpack 這種配置用法過后,我們再次回到配置文件中,遍歷剛剛定義的數(shù)組,為每一個模式單獨(dú)創(chuàng)建一個打包配置,這樣就可以一次性生成所有模式下的不同結(jié)果,這比我們一個一個去試驗的效率更高,而且對比起來也更明顯。
具體配置代碼如下:
// ./webpack.config.js const HtmlWebpackPlugin = require('html-webpack-plugin') const allModes = [ ‘eval', ‘cheap-eval-source-map', ‘cheap-module-eval-source-map', ‘eval-source-map', ‘cheap-source-map', ‘cheap-module-source-map', ‘inline-cheap-source-map', ‘inline-cheap-module-source-map', ‘source-map', ‘inline-source-map', ‘hidden-source-map', ‘nosources-source-map' ] module.exports = allModes.map(item => ({ devtool: item, mode: ‘none', entry: ‘./src/main.js', output: { filename: js/<span class="hljs-subst">${item}</span>.js }, module: { rules: [ { test: /.jsKaTeX parse error: Expected 'EOF', got '}' at position 342: … } }? ] }, <…{item}.html` }) ] }))
這里簡單解釋一下這個配置中的部分配置用意:
- 定義 devtool 屬性,它就是當(dāng)前所遍歷的模式名稱;
- 將 mode 設(shè)置為 none,確保 Webpack 內(nèi)部不做額外處理;
- 設(shè)置打包入口和輸出文件名稱,打包入口都是 src/main.js,輸出文件名稱我們就放在 js 目錄中,以模式名稱命名,至于為什么放在單獨(dú)目錄中,你可以在接下來的內(nèi)容中找到答案;
- 為 js 文件配置一個 babel-loader,配置 babel-loader 的目的是稍后能夠辨別其中一類模式的差異。
- 配置一個 html-webpack-plugin,也就是為每個打包任務(wù)生成一個 HTML 文件,通過前面的內(nèi)容,我們知道 html-webpack-plugin 可以生成使用打包結(jié)果的 HTML,接下來我們就是通過這些 HTML 在瀏覽器中進(jìn)行嘗試。
配置完成以后,我們再次回到命令行終端運(yùn)行打包,那此時這個打包過程就自動生成了不同模式下的打包結(jié)果,具體結(jié)果如下圖所示:
然后我們通過 serve 把結(jié)果運(yùn)行起來,打開瀏覽器,此時我們能夠在頁面中看到每一個使用不同模式 Source Map 的 HTML 文件,具體如下圖:
那如果剛剛沒有把 JS 文件輸出到單獨(dú)目錄中,這里的文件就會非常多,導(dǎo)致 HTML 文件尋找起來特別麻煩。
不同模式的對比
有了不同模式下生成的結(jié)果過后,我們就可以仔細(xì)去對比不同 Source Map 模式之間的具體差異了。其實也沒必要真的一個一個去看,這里我先帶你看幾個比較典型的模式,然后找出它們的規(guī)律,這樣你就再也不用頭大了。
首先 eval 模式,這個模式剛剛已經(jīng)單獨(dú)看過了,它就是將模塊代碼放到 eval 函數(shù)中執(zhí)行,并且通過 sourceURL 標(biāo)注所屬文件路徑,在這種模式下沒有 Source Map 文件,所以只能定位是哪個文件出錯,具體效果如下圖:
然后我們再來看一個叫作 eval-source-map 的模式,這個模式也是使用 eval 函數(shù)執(zhí)行模塊代碼,不過這里有所不同的是,eval-source-map 模式除了定位文件,還可以定位具體的行列信息。相比于 eval 模式,它能夠生成 Source Map 文件,可以反推出源代碼,具體效果如下:
緊接著我們再來看一個叫作 cheap-eval-source-map 的模式。根據(jù)這個模式的名字就能推斷出一些信息,它就是在 eval-source-map 基礎(chǔ)上添加了一個 cheap,也就是便宜的,或者叫廉價的。用計算機(jī)行業(yè)的常用說法,就是閹割版的 eval-source-map,因為它雖然也生成了 Source Map 文件,但是這種模式下的 Source Map 只能定位到行,而定位不到列,所以在效果上差了一點點,但是構(gòu)建速度會提升很多,具體效果如下圖:
接下來再看一個叫作 cheap-module-eval-source-map 的模式。慢慢地我們就發(fā)現(xiàn) Webpack 中這些模式的名字不是隨意的,好像都有某種規(guī)律。這里就是在 cheap-eval-source-map 的基礎(chǔ)上多了一個 module,具體效果如下圖:
這種模式同樣也只能定位到行,它的特點相比于 cheap-eval-source-map 并不明顯 ,如果你沒有發(fā)現(xiàn)差異,可以再去看看上一種模式,仔細(xì)做一個對比,相信對比之后你會發(fā)現(xiàn),cheap-module-eval-source-map 中定位的源代碼與我們編寫的源代碼是一模一樣的,而 cheap-eval-source-map 模式中定位的源代碼是經(jīng)過 ES6 轉(zhuǎn)換后的結(jié)果,具體對比如下(左圖是 cheap-eval-source-map):
這也是為什么之前我要給 JS 文件配置 Loader 的原因:因為這種名字中帶有 module 的模式,解析出來的源代碼是沒有經(jīng)過 Loader 加工的,而名字中不帶 module 的模式,解析出來的源代碼是經(jīng)過 Loader 加工后的結(jié)果。也就是說如果我們想要還原一模一樣的源代碼,就需要選擇 cheap-module-eval-source-map 模式。
了解了這些過后,你基本上就算通盤了解了 Webpack 中所有 Source Map 模式之間的差異,因為其它的模式無外乎就是這幾個特點的排列組合罷了。
例如,我們再來看一個 cheap-source-map 模式,這個模式的名字中沒有 eval,意味著它沒用 eval 執(zhí)行代碼,而名字中沒有 module,意味著 Source Map 反推出來的是 Loader 處理后的代碼,有 cheap 表示只能定位源代碼的行號。
那以上就是我們在日常開發(fā)過程中經(jīng)常用到的幾種 Source Map 模式,你在嘗試的時候一定要注意:找規(guī)律很重要。
除此之外,還有幾個特殊一點的模式,我們單獨(dú)介紹一下:
- inline-source-map 模式
它跟普通的 source-map 效果相同,只不過這種模式下 Source Map 文件不是以物理文件存在,而是以 data URLs 的方式出現(xiàn)在代碼中。我們前面遇到的 eval-source-map 也是這種 inline 的方式。
- hidden-source-map 模式
在這個模式下,我們在開發(fā)工具中看不到 Source Map 的效果,但是它也確實生成了 Source Map 文件,這就跟 jQuery 一樣,雖然生成了 Source Map 文件,但是代碼中并沒有引用對應(yīng)的 Source Map 文件,開發(fā)者可以自己選擇使用。
- nosources-source-map 模式:
在這個模式下,我們能看到錯誤出現(xiàn)的位置(包含行列位置),但是點進(jìn)去卻看不到源代碼。這是為了保護(hù)源代碼在生產(chǎn)環(huán)境中不暴露。
寫在最后
雖然 Webpack 中支持各種各樣的 Source Map 模式,但一般應(yīng)用開發(fā)時我們只會用到其中的幾種。其實在我們掌握了它們的特點過后,選擇上就沒有什么需要糾結(jié)的地方了。
這里再分享一下我個人開發(fā)時的選擇,供你參考。
首先開發(fā)過程中(開發(fā)環(huán)境),我會選擇 cheap-module-eval-source-map,原因有以下三點:
- 我使用框架的情況會比較多,以 React 和 Vue.js 為例,無論是 JSX 還是 vue 單文件組件,Loader 轉(zhuǎn)換后差別都很大,我需要調(diào)試 Loader 轉(zhuǎn)換前的源代碼。
- 一般情況下,我編寫的代碼每行不會超過 80 個字符,對我而言能夠定位到行到位置就夠了,而且省略列信息還可以提升構(gòu)建速度。
- 雖然在這種模式下啟動打包會比較慢,但大多數(shù)時間內(nèi)我使用的 webpack-dev-server 都是在監(jiān)視模式下重新打包,它重新打包的速度非???。
綜上所述,開發(fā)環(huán)境下我會選擇 cheap-module-eval-source-map。
至于發(fā)布前的打包,也就是生產(chǎn)環(huán)境的打包,我選擇 none,它不會生成 Source Map。原因很簡單:
- 首先,Source Map 會暴露我的源代碼到生產(chǎn)環(huán)境。如果沒有控制 Source Map 文件訪問權(quán)限的話,但凡是有點技術(shù)的人都可以很容易的復(fù)原項目中涉及的絕大多數(shù)源代碼,這非常不合理也不安全,我想很多人可能都忽略了這個問題。
- 其次,調(diào)試應(yīng)該是開發(fā)階段的事情,你應(yīng)該在開發(fā)階段就盡可能找到所有問題和隱患,而不是到了生產(chǎn)環(huán)境中再去全民公測。如果你對自己的代碼實在沒有信心,我建議你選擇 nosources-source-map 模式,這樣出現(xiàn)錯誤可以定位到源碼位置,也不至于暴露源碼。
當(dāng)然這些選擇不是絕對的,我們理解這些模式之間的差異的目的,就是為了可以在不同環(huán)境中快速選擇一個合適的模式,而不是尋求一個通用法則,開發(fā)行業(yè)也根本不會有絕對的通用法則!
除此之外,我還要強(qiáng)調(diào)一點,Source Map 并不是 Webpack 特有的功能,它們兩者的關(guān)系只是:Webpack 支持 Source Map。大多數(shù)的構(gòu)建或者編譯工具也都支持 Source Map。希望你不要把它們二者捆綁到一起,混為一談。
到此這篇關(guān)于配置 Webpack SourceMap 實踐教程的文章就介紹到這了,更多相關(guān)Webpack SourceMap配置內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
javascript隨機(jī)抽取0-100之間不重復(fù)的10個數(shù)
這篇文章主要為大家詳細(xì)介紹了javascript隨機(jī)抽取0-100之間不重復(fù)的10個數(shù),分享了兩種簡單辦法,感興趣的小伙伴們可以參考一下2016-02-02Fastest way to build an HTML string(拼裝html字符串的最快方法)
Fastest way to build an HTML stringPosted in 'Code Snippets, JavaScript' by James on May 29th, 20092011-08-08JavaScript學(xué)習(xí)筆記之創(chuàng)建對象
在JavaScript中對象是一種基本的數(shù)據(jù)類型,在數(shù)據(jù)結(jié)構(gòu)上是一種散列表,可以看作是屬性的無序集合,除了原始值其他一切都是對象。這篇文章主要給大家介紹JavaScript學(xué)習(xí)筆記之創(chuàng)建對象,需要的朋友參考下吧2016-03-03設(shè)為首頁和收藏的Javascript代碼(親測兼容IE,Firefox,chrome等瀏覽器)
這篇文章主要介紹了設(shè)為首頁和收藏的Javascript代碼(親測兼容IE,Firefox,chrome等瀏覽器)。需要的朋友可以過來參考下,希望對大家有所幫助2013-11-11