如何在線上使用 SourceMap

前言

介紹了在線上使用 SourceMap 進行調試的方法和常見問題。常見的使用姿勢是通過瀏覽器的開發者工具進行本地調試,而在線上使用 SourceMap 則需要手動添加 SourceMap 地址。針對線上無法自動加載 SourceMap 的問題,可以嘗試使用瀏覽器插件、Charles 進行轉發或者私有靜態服務託管 SourceMap。今日前端早讀課文章由 ES2049@落風分享。

1. 前言

1.1 常見調試手段

平日開發過程中,大家是如何調試線上問題的呢?採樣後的衆生相如下:

等一下,還有沒有人在用 SourceMap 調試啊?通過 SourceMap,我們可以在瀏覽器內,直接看到源碼,而不是編譯、壓縮、混淆後的部署產物。

1.2 SourceMap 的常規用法

爲了保證後續內容的理解,這裏簡單闡述下常見的使用姿勢。

1.2.1 本地調試

本地啓動項目後,可以在 Source 標籤頁看到三塊面板:

通過這些面板,我們可以直接找到希望調試的源碼文件,打斷點單步調試,查看上下文信息等。如果你是純粹的 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\ 從已經映射到源碼的堆棧信息再次進入,即可看到報錯在源碼中的位置

從上面的說明我們可以感受到:

2. 問題探索

我們需要先將以下問題研究清楚:

2.1 瀏覽器是如何識別並加載 SourceMap 的?

如果我們讓構建工具開啓了 SourceMap,例如 Webpack 的 devtools,源碼經過構建過程(編譯、混淆、壓縮等)生成的部署代碼會在底部增加一行註釋,如下圖所示:

sourceMappingURL 告訴我們,當前資源文件 ConsoleSiteList.57ca29c2.chunk.js 對應的 SourceMap 文件的路徑是 ConsoleSiteList.57ca29c2.chunk.js.map。這是相對路徑的寫法,也就是說,在本地啓用的服務中,構建後的 chunk 和對應的 SourceMap 文件的地址分別爲:

這樣一來,瀏覽器就可以根據 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 的標準,不出所料,沒有新鮮事。

我們可以看到以下約定:

說試就試,我們以 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 的方式調試線上,建議做如下改動:

這樣,我們就能在線上使用 SourceMap 了。

關於本文
作者:@落風
原文:https://zhuanlan.zhihu.com/p/674981525

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/mjvvwyPGjsj8FHRFpH6i_A