细说 js 压缩、sourcemap、通过 sourcemap 查找原始报错信息
1. js
压缩
js
压缩对前端开发者来说是一门必修课。
一般来说,压缩 js
主要出于以下两个目的:
- 减小代码体积,加快前端资源加载速度
- 保护源代码不被别人获取
压缩 js
使用的工具库:
- UglifyJS2: 压缩
es5
- uglify-es: 压缩
es6+
- closure-compiler、closure-compiler-js:
google
的js
压缩、优化工具
压缩 js
的主要过程:
- 移除无用代码
- 混淆代码中变量名称、函数名称等
- 预编译代码
- 对结构进行扁平化处理
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
扩展
webpack 对 sourcemap
做了扩展,定义在 devtool
配置项中:
-
eval
: 每个模块都使用eval()
执行,并且都有//@ sourceURL
,构建很快,但无法正确显示行号 -
eval-source-map
: 每个模块使用eval()
执行,并且source map
转换为DataUrl
后添加到eval()
中,一般开发模式中使用这种方式 -
cheap-eval-source-map
: 类似eval-source-map
,但只映射行,不映射列,并忽略源自loader
的source map
,仅显示转译后的代码 -
cheap-module-eval-source-map
: 类似cheap-eval-source-map
,但会保留源自loader
的source map
-
inline-source-map
:source map
转换为DataUrl
后添加到bundle
中 -
cheap-source-map
: 只映射行,不映射列,并忽略源自loader
的source map
,仅显示转译后的代码 -
inline-cheap-source-map
:inline-source-map
与cheap-source-map
的结合 -
cheap-module-source-map
: 类似cheap-module-eval-source-map
,但不使用eval()
执行 -
inline-cheap-module-source-map
:inline-source-map
与cheap-module-source-map
的结合 -
source-map
: 整个source map
作为一个单独的文件生成,产品环境一般使用这种模式 -
hidden-source-map
: 类似source-map
,但不会把//# sourceMappingURL=bundle-file-name.js.map
追加到压缩文件后面 -
nosources-source-map
: 类似source-map
,但只有堆栈信息,没有源码信息
更详细信息可以参考:
- webpack devtool(英文)
- webpack devtool(中文)
使用建议
对于使用 webpack 来构建项目,建议在开发时使用 eval-source-map
,产品环境使用 source-map
。
因为用压缩文件与 sourcemap
文件是可以原原本本的找到源代码的,所以,为了保护源代码,可以这样隐藏 sourcemap
文件:
- 在
web
服务器设置外部不能访问sourcemap
文件,只能内部访问 - 直接把
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许可证)