一文了解source-map
背景
作为一个开发工程师——无论是什么开发,要求开发环境最不可少的一点功能就是——debug
功能。当我们通过webpack
将我们的源码打包成了 bundle.js
。试想:实际上客户端(浏览器)读取的是打包后的bundle.js
,那么当浏览器执行代码报错的时候,报错的信息自然也是bundle
的内容。我们如何将报错信息(bundle
错误的语句及其所在行列)映射到源码上?为了解决这个问题,google 提出了sourcemap
的想法,并在chorme上最先支持sourcemap
的使用。sourcemap
可以帮我们直接定位到编译前代码的特定位置。
webpack
已经内置了sourcemap
的功能,我们只需要通过简单的配置,将可以开启它。
module.exports = { // 开启 source map // 生产环境一般不开启 sourcemap devtool: 'source-map', }
除source-map
外,还可以基于我们的需求设置其他值,webpack
——devtool
官网一共提供了多种Sourcemap
模式:[官网链接](Devtool | webpack 中文文档 (docschina.org))
这么多种配置项其实只是五个关键字 eval
、source-map
、cheap
、module
和inline
的组合罢了,下面列出它们的意义:
「关键字」 | 「含义」 | 特点 |
source-map | 产生.map 文件 | 定位信息最全,但也.map 文件最大,效率最低 |
eval | 使用 eval 包裹模块代码 | 利用字符串可缓存从而提效 |
cheap | 不包含列信息,也不包含 loader 的 sourcemap | 精准度降低换取文件内容的缩小 |
module | 包含 loader 的 sourcemap(比如 jsx to js ,babel 的 sourcemap),否则无法定义源文件 | 解决对于使用cheap 配置项导致无法定位到 loader 处理前的源代码问题 |
inline | 将.map 作为 DataURI 嵌入,不单独生成.map 文件 | 减少文件数 |
举例
为了方便演示,这里的源代码只包含了一行代码:
console.log('hello webpack');
source-map
module.exports = { // 开启 source map // 生产环境一般不开启 sourcemap devtool: 'source-map', }
当我们执行打包命令之后,我们发现bundle
的最后一行总是会多出一个注释,指向打包出的bundle.map.js
(sourcemap
文件)。sourcemap
文件用来描述 源码文件和bundle
文件的代码位置映射关系。基于它,我们将bundle
文件的错误信息映射到源码文件上。
console.log("hello webpack"); //# sourceMappingURL=main.built.js.map
接下来我们看看sourcemap
文件包含了一些什么信息:
{ "version": 3, "file": "main.built.js", "mappings": "AAAAA,QAAQC,IAAI", "sources": ["webpack://webpack-demo/./src/index.js"], "sourcesContent": ["console.log('hello webpack');"], "names": ["console", "log"], "sourceRoot": "" }
上面可以看到,sourcemap
其实就是就是一段维护了前后代码映射关系的json
描述文件,包含了以下一些信息:
version
:sourcemap
版本(现在都是v3)file
:转换后的文件名。sourceRoot
:转换前的文件所在的目录。如果与转换前的文件在同一目录,该项为空。sources
:转换前的文件。该项是一个数组,表示可能存在多个文件合并。names
:转换前的所有变量名和属性名。mappings
:记录位置信息的字符串。mappings
信息是关键,它使用Base64 VLQ
编码,包含了源代码与生成代码的位置映射信息。mappings
的编码原理详解可见:http://www.ruanyifeng.com/blog/2013/01/javascript_source_map.html,这里就不再详述。
eval
module.exports = { // 开启 source map // 生产环境一般不开启 sourcemap devtool: 'source-map', }
输出的 bundle
文件如下:
(() => { var __webpack_modules__ = { 138: () => { eval("console.log('hello webpack');\n\n//# sourceURL=webpack://webpack-demo/./src/index.js?") } }, __webpack_exports__ = {}; __webpack_modules__[138]() })();
每个module
会封装到 eval
里包裹起来执行,并且会在末尾追加注释//@ sourceURL
。只是它映射的是转换后的代码,而不是映射到原始代码。
eval-source-map
module.exports = { // 开启 source map // 生产环境一般不开启 sourcemap devtool: 'eval-source-map', }
输出的 bundle
文件如下:
(() => { var __webpack_modules__ = { 138: () => { eval("console.log('hello webpack');//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMTM4LmpzIiwibWFwcGluZ3MiOiJBQUFBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vd2VicGFjay1kZW1vLy4vc3JjL2luZGV4LmpzP2I2MzUiXSwic291cmNlc0NvbnRlbnQiOlsiY29uc29sZS5sb2coJ2hlbGxvIHdlYnBhY2snKTsiXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///138\n") } }, __webpack_exports__ = {}; __webpack_modules__[138]() })();
可以看到,加了eval
的配置生成的sourcemap
会作为DataURI
嵌入,不单独生成.map
文件。
inline-source-map
module.exports = { // 开启 source map // 生产环境一般不开启 sourcemap devtool: 'inline-source-map', }
输出的 bundle
文件如下:
console.log("hello webpack"); //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWFpbi5idWlsdC5qcyIsIm1hcHBpbmdzIjoiQUFBQUEsUUFBUUMsSUFBSSIsInNvdXJjZXMiOlsid2VicGFjazovL3dlYnBhY2stZGVtby8uL3NyYy9pbmRleC5qcyJdLCJzb3VyY2VzQ29udGVudCI6WyJjb25zb2xlLmxvZygnaGVsbG8gd2VicGFjaycpOyJdLCJuYW1lcyI6WyJjb25zb2xlIiwibG9nIl0sInNvdXJjZVJvb3QiOiIifQ==
可以看出它将map
作为DataURI
嵌入,不单独生成.map
文件。
cheap-source-map
module.exports = { // 开启 source map // 生产环境一般不开启 sourcemap devtool: 'cheap-source-map', }
接下来我们看看sourcemap
文件包含了一些什么信息:
{ "version": 3, "file": "main.built.js", "mappings": "AAAA", "sources": ["webpack://webpack-demo/./src/index.js"], "sourcesContent": ["console.log('hello webpack');"], "names": [], "sourceRoot": "" }
可以看到比之前source-map
的,mappings
字段短了很多,实际上是因为它没有生成「列映射」(column mapping),只是「映射行数」。
cheap-module-source-map
Webpack
会利用loader
将所有非js
模块转化为webpack
可处理的js
模块,而增加上面的cheap
配置后也不会有loader
模块之间对应的sourceMap
。
什么是模块之间的sourceMap
呢?比如jsx
文件会经历loader
处理成js
文件再混淆压缩, 如果没有loader
之间的sourcemap
,那么在debug
的时候定义到上图中的压缩前的js
处,而不能追踪到jsx
中。
所以为了映射到loader
处理前的代码,我们一般也会加上module
配置。
总结
开发环境
在开发环境中,我们希望速度快,调试更友好。
以此这里推荐配置:eval-cheap-module-souce-map
生产环境
生产环境我们一般不会开启sourcemap
功能,主要有两点原因:
- 通过
bundle
和sourcemap
文件,可以反编译出源码————也就是说,线上产物有soucemap
文件的话,就意味着有暴漏源码的风险。 - 我们可以观察到,
sourcemap
文件的体积相对比较巨大,这跟我们生产环境的追求不同(生产环境追求更小更轻量的bundle
)。