source map入门案例

1,203 阅读5分钟

三年前,我收到了一个学校项目里的 xx.js 文件,只有一行代码,对于当时在严格要求自己代码格式规范的我来说,这简直不能忍。于是乎,傻傻地花了 20 分钟时间,一行行,一个个代码块手动更正,but 还是看不懂......直到最后被告知这是个压缩文件!

生产环境 .min.js

吃过没文化的亏,现在精通 xxx、xx 构建工具的我,直接把锅甩给了它们,谁让它们把源码 uglify,还压缩成一行的 😖

不过面对现实,生产环境确实可以优化:

  1. 代码压缩。减小文件大小
  2. 混淆加密。减少源码暴露风险
  3. 编译。生成浏览器可用代码,比如 es6->es5、ts->js

一时压缩一时爽,线上调试却头疼。控制台显示的日志是这样的:

SyntaxError: aaa
    at http://localhost/js/index-7590496c88b19c44e9ed.js:1:4880
    at u (http://localhost/js/common-3ee4144b260a56b17990.js:1:17621)

这完全找不到源代码哪行出错了,即使记录了错误日志也没有任何意义,只有将两个文件中代码位置对应起来,根据这层关系,才能定位到错误所在。

Source Map 就是解决这一问题的技术,各种编译工具在过程中生成 .map 文件,以 json 形式存储着源码的对应关系。

Source Map 格式

目前有很多工具都能生成 source map,比如 uglify-js、gulp、webpack 等等。工具不一样,生成的格式却是一样的,遵循第 3 版的提案:

{
    "version": 3,
    "sourceRoot": "源文件地址前缀",
    "sources": ["源文件地址"],
    "mappings": "VLQ编码,VLQ编码;VLQ编码",
    "file": "文件名",
    "names": ["变量名和属性名"]
}

最关键的就是 mappings 了,每个用 , 分隔开的 VLQ 编码都记录着代码的对应关系,而每个 ; 分隔开则表示着压缩文件的第几行。

VLQ 编码

Variable-Lenght Quantity,可变长量,可以用任意编码(进制)表示一个任意大的整数。

如果用 8 位二进制,VLQ 的结构大致是这样的:

# 八位 VLQ 表示法

        Continuation
        |             Sign
        |             |
        V             V
        7 6 5 4 3 2 1 0
--------------------------------
        1 2 3 4 5 6 7 8
  • 最高位(Continuation)表示是否可连续。0 表示是最后一个 VLQ 编码,1 表示后面还有
  • 最低位(Sign)可用于标志正负数。source map 中没有,略过...

因为第 1 位是连续位,所以转成 VLQ 时应该按照每块 7 位分隔,然后在每块第一位填充连续位。例如:

        00101000 10001111
--------------------------------
       00 1010001 0001111
--------------------------------
        11010001 00001111
        |        |
        |        V
        V        Continuation
        Continuation

最后一块填充 0,其余都填充 1

source map 采用了 Base64 编码,因其范围为 [0-63],只需要 6 位二进制即可。

Base64 VLQ

目前使用最多的是 mozilla/source-map,可查看其 base64-vlq.js 实现。

举个栗子,把 1234 转化成 base64 vlq:

# 六位 VLQ

        010011 010010              1234的二进制
--------------------------------
       01 00110 10010
--------------------------------
 100001 100110 010010              转为base64就是:hmS
 |      |      |
 |      |      V
 |      V      Continuation
 V      Continuation
 Continuation

mappings 位置对应

每个 VLQ 编码都记录着五个位置:

  1. 第一位,代表着压缩文件第几列
  2. 第二位,代表着 sources 数组 index
  3. 第三位,代表着源码第几行
  4. 第四位,代表着源码第几列
  5. 第五位,代表着 names 数组 index

好像少了压缩文件的第几行,其实它之前就被 ; 定义了。

一般来说,第五位如果对应不上 names,那它就没啥用。举个栗子:

        901250
--------------------------------
        9|0|12|5|0                 分段表示【重要】
--------------------------------
        9, index.js, 12, 5, 0
        |     |      |   |  |
        |     |      |   |  V
        |     |      |   V  names第0个
        |     |      V   源码第5列
        |     V      源码第12行
        V     sources第0个:index.js
        压缩代码第9列

如何把一个任意大的数字分段表示?恰巧 VLQ 编码的连续位功能可以实现!!

案例

道理大家都懂,关键还得在浏览器里调试。首先在 F12 调试工具中开启:

  1. Enable JavaScript source maps
  2. Enable CSS source maps

uglify-js

uglify-js 是一个被广泛使用压缩 es5 代码的工具,利用它可以生成 source map。

可以使用 uglify-es 来压缩 es6,但作者已经长时间不维护了,社区基本都转用 terser,它保留了和 uglify-js 一样的 API 和 cli 参数。

uglifyjs
    --compress
    --mangle
    # 开启 source map
    --source-map
    --output dist/index.js
    -- example01-uglifyjs/index.js

dist/index.js 末尾添加一行注释,使浏览器调试工具启用:

//# sourceMappingURL=index.js.map

更正 index.js.map 中的 sourceRoot,正确定位到源文件:

{"sourceRoot":"../","sources":["example01-uglifyjs/index.js"]}

切换到 Sources 面板可以看到:

source-map-uglifyjs

webpack

前端生产工具 webpack 集成了众多功能,包括生成 source map。

可以在生产环境中使用:

module.exports = {
    mode: 'production',
    devtool: 'source-map',
};

source-map-webpack

甚至连样式都能 inspect,开启配置生成 *.css.map

module.exports = {
    module: {
        rules: [
            MiniCssExtractPlugin.loader,
            { use: 'css-loader', options: { sourceMap: true } },
            { use: 'less-loader', options: { sourceMap: true } },
        ],
    },
};

source-map-webpack-css

压缩时需要额外的配置:

module.exports = {
    optimization: {
        minimizer: [
            new TerserWebpackPlugin({ sourceMap: true }),
            new OptimizeCSSAssetsPlugin({
                cssProcessorOptions: {
                    // 参考 postcss source map 配置
                    // https://github.com/postcss/postcss/blob/master/docs/source-maps.md
                    map: true,
                },
            }),
        ],
    },
};

线上 production

线上报错日志,根据生成的 source map 文件定位到源码位置。利用现有工具 mozilla/source-map 直接开搞:

const { SourceMapConsumer } = require('source-map');

// 解析线上错误日志,拿到报错的文件名、行号、列号
const { filename, line, column } = parse(error);
// 读取服务器上的 source map 文件
const rawSourceMap = JSON.parse(fs.readFileSync(filename + '.map'));

SourceMapConsumer.with(rawSourceMap, null, (consumer) => {
    const {
        name, // names 数组中的变量 or 属性名
        source, // sources 数组中文件名
        line, // 源代码列号
        column, // 源代码行号
    } = consumer.originalPositionFor({ line, column });
    // 根据文件名找到 sourceContent 中对应文件源代码
    const content = consumer.sourceContentFor(source);

    // 源代码内容以及报错位置均解析,
    // 可以人类化展示
});

效果图:

source-map-production

参考链接

  1. JavaScript Source Map 详解
  2. debug 工具 —— source-map
  3. Source Maps 101
  4. Source Map Revision 3 Proposal
  5. LitileXueZha/examples-source-map: 一些关于 source map 的例子