sourcemap解决压缩代码的噩梦

年前最后缕一缕sourcemap,虽然一直在用。

前言

现在绝大多数的前端项目,css及js都做了代码的压缩或打包处理,这样不但减小了代码体积、减少了请求数,也使得代码混淆提升了安全性。与此相伴的问题随之而来,随着项目的增大,压缩打包后的代码变得越来越难以debug。这也是sourcemap首要解决的问题。

1 概念

SourceMap 是一个存储源代码与编译代码对应位置映射的信息文件。这意味着你可以在优化压缩代码后找到转换前所对应的位置。

其兼容情况如下:
source map兼容性

概括来说,得在Chrome及Firefox使。

2 语法

2.1 代码注释

在优化(压缩)后的文件的底部添加特殊注释,向支持sourcemap的浏览器提供源映射。
格式:

1
//# sourceMappingURL=<url>

其中,<url>指向一个source map文件的一个相对(于请求的URL)或者一个绝对的URL。

2.2 语法(请求头):

在压缩文件的响应中发送HTTP header来指定源映射。
格式:

1
2
SourceMap: <url>
X-SourceMap: <url> (deprecated)

如:

1
SourceMap: /path/to/file.js.map

3 源映射文件

源映射文件包含一个JSON对象,其中包含有关映射本身和原始文件的信息。

如:

1
2
3
4
5
6
7
8
9
10
11
12
{
version: 3,
file: "script.js.map",
sources: [
"app.jsx",
"util.js",
"asset.jsx"
],
sourceRoot: "/",
names: ["React", "ReactRouterDOM", "ReactDOM", "HelloWorld"],
mappings: "yEAAAA,EAAAC,QAAAC,wBCAAF,..."
}

其中:

  • version- 此属性指示文件所遵循的源映射规范的版本。
  • file - 源映射文件的名称。
  • sources - 原始源文件的URL数组。
  • sourceRoot- (可选)sources将解析数组中所有文件的URL 。
  • names - 包含源文件中所有变量和函数名称的数组。
  • mappings- 包含实际代码映射的Base64 VLQ字符串。

映射关系正确的话,可在Chrome见源码:
Chrome

  • 开发者模式下设置里找到 Sources 栏,勾选上允许 JS SourceMap 与 css SourceMap (默认应该是选上的)

然后就可以尽情得开始debug调试和查看源码逻辑了。

3.1 mappings属性

sourcemap的核心对应关系在mapping属性里,它是一个Base64 VLQ字符串,可以分成三层。

  • 第一层是行对应,以分号(;)表示,每个分号对应转换后源码的一行。所以,第一个分号前的内容,就对应源码的第一行,以此类推。
  • 第二层是位置对应,以逗号(,)表示,每个逗号对应转换后源码的一个位置。所以,第一个逗号前的内容,就对应该行源码的第一个位置,以此类推。
  • 第三层是位置转换,以VLQ编码表示,代表该位置对应的转换前的源码位置。

3.2 mappings属性位置对应的原理

如,假定mappings属性的内容如下:

1
2
3
4
5
{
...
mapping: "AAAAA,BBBBB;CCCCC"
...
}

表示,转换后的源码分成两行,第一行有两个位置,第二行有一个位置。

每个位置使用五位,表示五个字段。

  • 第一位,表示这个位置在(转换后的代码的)的第几列。
  • 第二位,表示这个位置属于sources属性中的哪一个文件。
  • 第三位,表示这个位置属于转换前代码的第几行。
  • 第四位,表示这个位置属于转换前代码的第几列。
  • 第五位,表示这个位置属于names属性中的哪一个变量。

如果某个位置是AAAAA,由于A在VLQ编码中表示0,因此这个位置的五个位实际上都是0。

  • 所有的值都是以0作为基数的;第五位不是必需的;每一位都采用VLQ编码表示;由于VLQ编码是变长的,所以每一位可以由多个字符构成。

关于VLQ在此便不过多介绍,有兴趣可点击了解wiki-VLQ

4 项目使用

4.1 webpack

1
2
3
4
// webpack.config.js
export.default = {
devtool: 'source-map'
}

其中

  • eval 每个模块都使用 eval() 执行,并且都有 //@ sourceURL。此选项会相当快地构建。主要缺点是,由于会映射到转换后的代码,而不是映射到原始代码,所以不能正确的显示行数。
  • inline-source-map - SourceMap 转换为 DataUrl 后添加到 bundle 中。
  • eval-source-map - 每个模块使用 eval() 执行,并且 SourceMap 转换为 DataUrl 后添加到 eval() 中。初始化 SourceMap 时比较慢,但是会在重构建时提供很快的速度,并且生成实际的文件。行数能够正确映射,因为会映射到原始代码中。
  • cheap-module-eval-source-map - 就像 eval-source-map,每个模块使用 eval() 执行,并且 SourceMap 转换为 DataUrl 后添加到 eval() 中。”低开销”是因为它没有生成列映射(column map),只是映射行数。
  • source-map - 生成完整的 SourceMap,输出为独立文件。由于在 bundle 中添加了引用注释,所以开发工具知道在哪里去找到 SourceMap。
  • hidden-source-map - 和 source-map 相同,但是没有在 bundle 中添加引用注释。如果你只想要 SourceMap 映射错误报告中的错误堆栈跟踪信息,但不希望将 SourceMap 暴露给浏览器开发工具。
  • cheap-source-map - 不带列映射(column-map)的 SourceMap,忽略加载的 Source Map。
  • cheap-module-source-map - 不带列映射(column-map)的 SourceMap,将加载的 Source Map 简化为每行单独映射。
  • nosources-source-map - 创建一个没有 sourcesContent 的 SourceMap。它可以用来映射客户端(译者注:指浏览器)上的堆栈跟踪,而不会暴露所有的源码。

4.2 gulp

1
2
3
4
5
6
7
const sourcemaps = require('gulp-sourcemaps');

gulp.task('javascript', function() {
gulp.src('src/**/*.js')
.pipe(sourcemaps.write()) //输出 .map 文件
.pipe(gulp.dest('dist'));
});

5 生成工具

参考: