細說 js 壓縮、sourcemap、通過 sourcemap 查找原始報錯信息

細說 js 壓縮、sourcemap、通過 sourcemap 查找原始報錯信息

1. js 壓縮

js 壓縮對前端開發者來說是一門必修課。

一般來說,壓縮 js 主要出於以下兩個目的:

  1. 減小代碼體積,加快前端資源加載速度
  2. 保護源代碼不被別人獲取

壓縮 js 使用的工具庫:

壓縮 js 的主要過程:

  1. 移除無用代碼
  2. 混淆代碼中變量名稱、函數名稱等
  3. 預編譯代碼
  4. 對結構進行扁平化處理

1. 移除無用代碼

去掉所有對解析引擎來說無用的字符,包括空格、註釋、換行、沒有用的變量聲明、函數聲明等。

2. 混淆代碼中變量名稱、函數名稱等

把一些局部變量名稱、函數名稱等用 a, b, ...$1, $2, ..._1, _2, ... 之類的簡略字符進行替換,達到混淆的目的。

源代碼

(function () {
  var hello = 'hi';
  var print = function (str) {
    console.log(str);
  };

  print(hello);
})();

壓縮後的代碼(僅演示混淆功能)

(function () {
  var a = 'hi';
  var b = function (c) {
    console.log(c);
  };

  b(a);
})();

3. 預編譯代碼

把不依賴外部環境的邏輯提前進行運算,並把運算結果替換到相應的源碼處,然後從源碼中移除這段邏輯。

源代碼

(function () {
  var hello = 'hi' + ' everyone, ';
  var count = 3 * 5;

  console.log(hello + count + ' girls');
})();

壓縮後的代碼(僅演示預編譯功能)

(function () {
  var hello = 'hi everyone, ';
  var count = 15;

  console.log(hello + count + ' girls');
})();

4. 對結構進行扁平化處理

對於 js 來說,嵌套越深,執行越慢,對代碼進行扁平化處理也是優化代碼的一種方式。

源代碼

(function () {
  var say = {
    hello: function (str) {
      console.log('hello ' + str);
    }
  };

  say.hello('everyone');
})();

壓縮後的代碼(僅演示扁平化結構功能)

!function(str){console.log("hello "+str)}("everyone");

完整示例

源代碼

(function () {
  var say = {
    hello: function (str) {
      console.log('hello ' + str);
    }
  };

  say.hello('everyone');
})();

壓縮後的代碼

!function(l){console.log("hello "+l)}("50 girls");

2. sourcemap

通常 js 壓縮後只有一行代碼,並且裏面的變量名與函數名等都是混淆了的,這在實際運行中會有一個問題,就是 js 的報錯信息將會失真,無法追蹤到是在源代碼哪一行哪一列報的錯。

sourcemap 便是爲了解決這個問題而生的。

sourcemap 文件就是記錄了從源代碼文件到壓縮文件的一個代碼對應關係記錄表,通過壓縮文件和 sourcemap 文件可以原原本本找出源代碼文件。

查看阮一峯老師的 JavaScript Source Map 詳解 瞭解 sourcemap 的原理與格式。

一般在壓縮 js 的過程中,會生成相應的 sourcemap 文件,並且在壓縮的 js 文件末尾追加 sourcemap 文件的鏈接 //# sourceMappingURL=bundle-file-name.js.map。這樣,瀏覽器在加載這個壓縮 js 的時候,就知道還有一個相應的 sourcemap 文件,也一併加載下來,運行的過程中如果 js 報錯,也會給出相應源代碼的行號與列號,而非壓縮文件的。

比如,對下面的源碼進行壓縮:

(function () {
  var say = {
    hi: function () {
      console.log('hi');
    }
  };

  say.hello();

  return say;
})();

未加 sourcemap 文件時,報錯信息是:

圖片描述

圖片描述

加上 sourcemap 文件時,報錯信息是:

圖片描述

圖片描述

sourcemap 擴展

webpacksourcemap 做了擴展,定義在 devtool 配置項中:

  • eval: 每個模塊都使用 eval() 執行,並且都有 //@ sourceURL,構建很快,但無法正確顯示行號
  • eval-source-map: 每個模塊使用 eval() 執行,並且 source map 轉換爲 DataUrl 後添加到 eval() 中,一般開發模式中使用這種方式
  • cheap-eval-source-map: 類似 eval-source-map,但只映射行,不映射列,並忽略源自 loadersource map,僅顯示轉譯後的代碼
  • cheap-module-eval-source-map: 類似 cheap-eval-source-map,但會保留源自 loadersource map
  • inline-source-map: source map 轉換爲 DataUrl 後添加到 bundle
  • cheap-source-map: 只映射行,不映射列,並忽略源自 loadersource map,僅顯示轉譯後的代碼
  • inline-cheap-source-map: inline-source-mapcheap-source-map 的結合
  • cheap-module-source-map: 類似 cheap-module-eval-source-map,但不使用 eval() 執行
  • inline-cheap-module-source-map: inline-source-mapcheap-module-source-map 的結合
  • source-map: 整個 source map 作爲一個單獨的文件生成,產品環境一般使用這種模式
  • hidden-source-map: 類似 source-map,但不會把 //# sourceMappingURL=bundle-file-name.js.map 追加到壓縮文件後面
  • nosources-source-map: 類似 source-map,但只有堆棧信息,沒有源碼信息

更詳細信息可以參考:

使用建議

對於使用 webpack 來構建項目,建議在開發時使用 eval-source-map,產品環境使用 source-map

因爲用壓縮文件與 sourcemap 文件是可以原原本本的找到源代碼的,所以,爲了保護源代碼,可以這樣隱藏 sourcemap 文件:

  1. web 服務器設置外部不能訪問 sourcemap 文件,只能內部訪問
  2. 直接把 sourcemap 文件存放到其他地方

3. 通過 sourcemap 查找原始報錯信息

一般而言,在產品階段,我們會用 window.onerror 來捕獲 js 報錯,然後上報到服務器,以此來收集用戶使用時發生的 bug

window.onerror = function(message, source, lineno, colno, error) {
  // message: 錯誤信息
  // source: 報錯腳本的 url 地址
  // lineno: 行號
  // colno: 列號
  // error: 錯誤對象
  // 上報必要的信息到服務器
}

但產品環境的代碼都是壓縮的,行號和列號都是失真的,所以就需要用 sourcemap 文件來找到錯誤對應源代碼的行號與列號,以及其他的信息。

使用工具: mozilla/source-map

源代碼

(function () {
  var say = {
    hi: function () {
      console.log('hi');
    }
  };

  say.hello();

  return say;
})();

壓縮後報錯信息

window.onerror = function(message, source, lineno, colno, error) {
  console.log(`message: ${message}`);
  console.log(`source: ${source}`);
  console.log(`lineno: ${lineno}`);
  console.log(`colno: ${colno}`);
  console.log(`error: ${error}`);
}

// message: Uncaught TypeError: e.hello is not a function
// source: url/to/bundle.min.js
// lineno: 1
// colno: 982
// error: TypeError: e.hello is not a function

通過 source-map 查找原始報錯信息

const fs = require('fs');
const SourceMap = require('source-map');

const { readFileSync } = fs;
const { SourceMapConsumer } = SourceMap;

const rawSourceMap = JSON.parse(readFileSync('path/to/js/map/file', 'utf8'));

SourceMapConsumer.with(rawSourceMap, null, consumer => {
  const pos = consumer.originalPositionFor({
    line: 1,
    column: 982
  });

  console.log(pos);
});

查找到的原始信息

{ 
  source: 'path/to/index.js',
  line: 8,
  column: 7,
  name: 'hello' 
}

這樣,便找到了原始報錯信息:

  • 原始報錯文件:path/to/index.js
  • 原始報錯行號:8
  • 原始報錯列號:7
  • 原始對象名稱:hello

如此,便能一下子就找到錯誤在哪裏了。

更多用法,參考 mozilla/source-map

後續

更多博客,查看 https://github.com/senntyou/blogs

作者:深予之 (@senntyou)

版權聲明:自由轉載-非商用-非衍生-保持署名(創意共享3.0許可證

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章