三年前,我收到了一个学校项目里的
xx.js文件,只有一行代码,对于当时在严格要求自己代码格式规范的我来说,这简直不能忍。于是乎,傻傻地花了 20 分钟时间,一行行,一个个代码块手动更正,but 还是看不懂......直到最后被告知这是个压缩文件!
生产环境 .min.js
吃过没文化的亏,现在精通 xxx、xx 构建工具的我,直接把锅甩给了它们,谁让它们把源码 uglify,还压缩成一行的 😖
不过面对现实,生产环境确实可以优化:
- 代码压缩。减小文件大小
- 混淆加密。减少源码暴露风险
- 编译。生成浏览器可用代码,比如 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 编码都记录着五个位置:
- 第一位,代表着压缩文件第几列
- 第二位,代表着
sources数组 index - 第三位,代表着源码第几行
- 第四位,代表着源码第几列
- 第五位,代表着
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 调试工具中开启:
- Enable JavaScript source maps
- 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 面板可以看到:
webpack
前端生产工具 webpack 集成了众多功能,包括生成 source map。
可以在生产环境中使用:
module.exports = {
mode: 'production',
devtool: 'source-map',
};
甚至连样式都能 inspect,开启配置生成 *.css.map:
module.exports = {
module: {
rules: [
MiniCssExtractPlugin.loader,
{ use: 'css-loader', options: { sourceMap: true } },
{ use: 'less-loader', options: { sourceMap: true } },
],
},
};
压缩时需要额外的配置:
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);
// 源代码内容以及报错位置均解析,
// 可以人类化展示
});
效果图: