如何在線上使用 SourceMap
前言
介紹了在線上使用 SourceMap 進行調試的方法和常見問題。常見的使用姿勢是通過瀏覽器的開發者工具進行本地調試,而在線上使用 SourceMap 則需要手動添加 SourceMap 地址。針對線上無法自動加載 SourceMap 的問題,可以嘗試使用瀏覽器插件、Charles 進行轉發或者私有靜態服務託管 SourceMap。今日前端早讀課文章由 ES2049@落風分享。
1. 前言
1.1 常見調試手段
平日開發過程中,大家是如何調試線上問題的呢?採樣後的衆生相如下:
-
酷酷派 :我沒什麼線上問題!!
-
投機派 :通過報錯的特殊標識在源碼中定位
-
學院派 :使用 XSwitch 等代理工具將資源轉發到本地
-
硬核派 :直接看混淆後的代碼一把梭
-
... 等等等
等一下,還有沒有人在用 SourceMap 調試啊?通過 SourceMap,我們可以在瀏覽器內,直接看到源碼,而不是編譯、壓縮、混淆後的部署產物。
1.2 SourceMap 的常規用法
爲了保證後續內容的理解,這裏簡單闡述下常見的使用姿勢。
1.2.1 本地調試
本地啓動項目後,可以在 Source 標籤頁看到三塊面板:
-
File Navigator Pane:文件導航面板
-
Code Editor Pane:代碼編輯面板
-
JavaScript Debugging Pane:JS 調試面板
通過這些面板,我們可以直接找到希望調試的源碼文件,打斷點單步調試,查看上下文信息等。如果你是純粹的 debugger 選手或 console.log 選手,只好表示 understand & respect。
1.2.2 線上排查
線上使用 SourceMap 則困難不少。通常我們的使用方式是:
1\ 發現控制檯中報異常,根據 Chrome 提供的堆棧定位到報錯文件
2\ 在代碼編輯面板中,右鍵 - Reveal in sidebar,在文件樹中定位到部署文件
3\ 右鍵部署文件,點擊 Copy link address,在控制檯中查看資源的地址
4\ 確定構建產物對應的 SourceMap 資源地址(以下爲內部發布平臺示意)
5\ 在代碼編輯面板,右鍵 - Add source map,添加 SourceMap 地址
6\ 從已經映射到源碼的堆棧信息再次進入,即可看到報錯在源碼中的位置
從上面的說明我們可以感受到:
-
Chrome DevTools 很強大
-
手動添加 SourceMap 很麻煩
2. 問題探索
我們需要先將以下問題研究清楚:
-
瀏覽器是如何識別並加載 SourceMap 的?
-
爲什麼本地可以自動加載而線上不可以?
-
如何感知瀏覽器確實加載了 SourceMap?
2.1 瀏覽器是如何識別並加載 SourceMap 的?
如果我們讓構建工具開啓了 SourceMap,例如 Webpack 的 devtools,源碼經過構建過程(編譯、混淆、壓縮等)生成的部署代碼會在底部增加一行註釋,如下圖所示:
sourceMappingURL 告訴我們,當前資源文件 ConsoleSiteList.57ca29c2.chunk.js 對應的 SourceMap 文件的路徑是 ConsoleSiteList.57ca29c2.chunk.js.map。這是相對路徑的寫法,也就是說,在本地啓用的服務中,構建後的 chunk 和對應的 SourceMap 文件的地址分別爲:
-
構建後的 chunk 地址:http://localhost:3000/ConsoleSiteList.57ca29c2.chunk.js
-
對應 SourceMap 地址:http://localhost:3000/ConsoleSiteList.57ca29c2.chunk.js.map
這樣一來,瀏覽器就可以根據 sourceMappingURL 去自動加載 SourceMap,而不用苦哈哈的手動添加。
2.2 爲什麼本地可以自動加載而線上不可以?
一般來講,線上產物中會把 SourceMap 去除,除了爲了加速構建過程,更重要的是避免有開發經驗的人直接在瀏覽器中「閱讀源碼」。除了直接去除,企業內也常常利用內部的存儲能力,將構建好的 SourceMap 資源轉存到其他地方,這樣一來,構建產物中的 sourceMappingURL 將無法正確指向 SourceMap 的資源地址,從而實現與直接去除接近的效果。
這樣一來,在生產環境下 Chrome 根據 sourceMappingURL 相對路徑的寫法就只能尋址到不存在的 404,瀏覽器會加載不到需要的資源。
2.3 如何感知瀏覽器確實加載了 SourceMap?
我們可以打開 DevTools Network 標籤頁,使用過濾器過濾 .map 文件,我們發現什麼都沒有:
難道是 Chrome 加載 SourceMap 不需要通過網絡請求?這顯然不會,如果你有興趣可以查看 issue,這其實是有意爲之。不過我們仍然有其他手段看到 .map
文件的請求,打開 Charles 抓一把(涉及證書安裝等操作本文不再贅述),就可以看到一堆迷途的請求:
你可能會困惑,既然 Chrome 實際發出了請求,Chrome 本身沒提供可以查看的入口嗎?打開 Developer Resources 標籤頁過濾出 SourceMap 相關的請求即可(也可以使用 net-internals):
2.4 總結
瀏覽器根據構建產物中的註釋 sourceMappingURL 嘗試加載 SourceMap 文件,但線上構建時往往會將 SourceMap 刪除(或上傳到了其他地方),因此我們無法直接在線上使用 SourceMap。
3. 解決方案
我們本質論一把:在線上加載到正確的 SourceMap 資源地址。
3.1 嘗試一:基於瀏覽器插件 Redirect
我們可以嘗試使用 XSwitch 等工具重定向一把。假設我們的 SourceMap 資源轉存到了 http://sourcemap.def.alibaba-inc.com 下,規則可以書寫如下:
{
"proxy": [
[
"https://g.alicdn.com/(.*).map",
"https://sourcemap.def.alibaba-inc.com/sourcemap/$1.map",
]
],
}
很快我們就會發現沒有任何卵用。爲了驗證,筆者自行實現了基於 Manifest V3 的瀏覽器插件(使用 chrome.declarativeNetRequestAPI)和基於 Manifest V2 的瀏覽器插件(使用 chrome.webRequestAPI),也同樣沒有卵用。無論是 Charles 還是 Developer Resources 都會告訴你 SourceMap 的加載是 404,不過如果我們轉發 Network 標籤頁中可見的請求是可以生效的。
補充:關於爲什麼如此,目前猜測是因爲 Chrome Extension 無法感知瀏覽器級別的活動(可感知方式)。
很遺憾暫時沒有確鑿的結論(筆者目前還沒有閱讀 Chromium 源碼的功力 )。能夠確定的是:
只有在 DevTools 打開時纔會加載 SourceMap(性能優化 & 用戶並不需要)
DevTools 也是一種擴展,而擴展是無法攔截另一個擴展的請求的(安全性問題)
SourceMap 的加載不能從 Network 中看到而要從 Developer Resources 看到(這也是故意的設計)
基於以上信息,可以理解爲 Chrome Extension 主要還是用於折騰 content 區域,而不是希望你 hack 瀏覽器。
總之很遺憾,我們不能通過 XSwitch 這樣的插件,把 SourceMap 文件的請求地址轉發到正確的位置。
3.2 嘗試二:基於 Charles Map Remote
因爲 Chrome 檢測到 sourceMappingURL 後會實際發起請求,所以使用 Charles 進行轉發是肯定可行的。
系統菜單 - Tools - Map Remote:
配置如下(映射邏輯,支持通配):
重新刷新頁面,從日誌中可以看到轉發生效了。請求的資源首先是 http://g.alicdn.com,然後按 Map Remote 的配置轉發到了 http://sourcemap.def.alibaba-inc.com(這實際是個中間服務),然後再轉發到 SourceMap 資源在 OSS 上的地址。
此時,我們打開 Source 面板,可以在左側文件樹中直接瀏覽源碼。成了!
3.3 嘗試三:私有靜態服務託管 SourceMap
在構建時將 SourceMap 上傳至某個私有的地址(如 CDN 或 OSS),然後利用 Webpack 插件將 sourceMappingURL 改爲該私有地址。這樣開發人員在打開 DevTools 時,Chrome 將根據 sourceMappingURL 直接加載實際的 SourceMap 地址,而外部用戶則完全被隔離。原理展示如下:
這種方式也是可行的。
3.4 嘗試四:從標準中尋找答案
儘管嘗試二可以低成本走通,但畢竟是工具型方案,不是產品化方案。爲了尋找更多可行的方案,筆者開始查找 SourceMap 的標準,不出所料,沒有新鮮事。
我們可以看到以下約定:
-
SourceMap 文件在命名上與源文件保持一致,僅額外多出 .map 後綴
-
添加 Http Header:sourcemap,瀏覽器將識別並加載 SourceMap 文件
-
sourceMappingURL 註釋的優先級比 HttpHeader sourcemap 的優先級高
說試就試,我們以 Manifest V2 爲例實現如下。可以看到,邏輯上就是匹配資源地址,轉換成資源實際的地址,設置爲 Header sourcemap 的值並返回。
const REGEX = /^.*g\.alicdn\.com\/(馬上到!|aimake|muyang-test)\/(.*)\.js.*/;
const TARGET_TPL = 'https://sourcemap.def.alibaba-inc.com/sourcemap/$1/$2.js.map?enableCatchAll=true';
chrome.webRequest.onHeadersReceived.addListener(
function(details) {
if (details.url.match(REGEX)) {
const targetUrl = details.url.replace(REGEX, TARGET_TPL);
const headerSourcemap = { name: "sourcemap", value: targetUrl }
const responseHeaders = details.responseHeaders.concat(headerSourcemap);
return { responseHeaders };
}
return { responseHeaders: details.responseHeaders };
},
// filters
{
urls: ['<all_urls>'],
},
// extraInfoSpec
['blocking', 'responseHeaders', 'extraHeaders']
);
爲了測試效果,我們直接加載已解壓的擴展程序:
然後寫一個簡單的測試資源,這裏我們使用 Rollup 直接打個包。需要說明的是,sourcemap 需指定爲 hidden,其效果等同 Webpack 的 hidden-source-map,此時會構建出 SourceMap 但不會有 sourceMappingURL 的註釋。這樣我們就可以保證只有 Http Header sourcemap 生效。
import nodeResolve from '@rollup/plugin-node-resolve';
// convert CommonJS modules to ES6
import commonjs from '@rollup/plugin-commonjs';
import nodePolyfills from 'rollup-plugin-node-polyfills';
import { babel } from '@rollup/plugin-babel';
import { terser } from 'rollup-plugin-terser';
export default [
{
input: 'src/index.js',
output: [
{
file: 'lib/bundle.js',
name: 'MyBundle',
format: 'umd',
sourcemap: 'hidden',
compact: true, // 開啓壓縮
},
],
plugins: [
nodePolyfills(),
nodeResolve(),
commonjs(),
babel({
babelHelpers: 'runtime',
exclude: '**/node_modules/**',
}),
terser(),
],
},
];
構建測試資源,可以看到在線上生成了 SourceMap 文件:
構建測試頁面,引用上述測試資源,開啓測試插件後,刷新頁面:
我們先來看我們實際加載的的資源的代碼,是編譯後的產物:
然後我們前往 Source 標籤頁,可以看到簡單的幾行源碼,非常舒適:
4. 總結
如果希望使用 SourceMap 的方式調試線上,建議做如下改動:
-
Webpack devtools 配置使用 hidden-source-map,去除 sourceMappingURL 註釋
-
開發瀏覽器插件,支持基於 Header SourceMap 的轉發功能
這樣,我們就能在線上使用 SourceMap 了。
關於本文
作者:@落風
原文:https://zhuanlan.zhihu.com/p/674981525
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/mjvvwyPGjsj8FHRFpH6i_A