Webpack - Source Map
Webpack 中 配置source map的学习笔记, 收获和总结。
通过构建或者编译之类的操作,我们将开发阶段编写的源代码转换为能够在生产环境中运行的代码,这种进步同时也意味着我们实际运行的代码和我们真正编写的代码之间存在很大的差异。
在这种情况下,如果需要调试我们的应用,或是应用运行的过程中出现意料之外的错误,那我们将无从下手。因为无论是调试还是报错,都是基于构建后的代码进行的,我们只能看到错误信息在构建后代码中具体的位置,却很难直接定位到源代码中对应的位置。
所以我们今天来聊聊如何借助工具解决现代化前端应用的调试问题。
Source Map 简介
Source Map(源代码地图)就是解决此类问题最好的办法,从它的名字就能够看出它的作用:映射转换后的代码与源代码之间的关系。一段转换后的代码,通过转换过程中生成的 Source Map 文件就可以逆向解析得到对应的源代码。
让我们来看一个简单的.map文件:
这是一个 JSON 格式的文件,为了更容易阅读,我提前对该文件进行了格式化。这个 JSON 里面记录的就是转换后和转换前代码之间的映射关系,主要存在以下几个属性:
- ●
version
是指定所使用的 Source Map 标准版本;
●sources
中记录的是转换前的源文件名称,因为有可能出现多个文件打包转换为一个文件的情况,所以这里是一个数组;
●names
是源代码中使用的一些成员名称,我们都知道一般压缩代码时会将我们开发阶段编写的有意义的变量名替换为一些简短的字符,这个属性中记录的就是原始的名称;
●mappings
属性,这个属性最为关键,它是一个叫作 base64-VLQ 编码的字符串,里面记录的信息就是转换后代码中的字符与转换前代码中的字符之间的映射关系.
一般我们会在转换后的代码中通过添加一行注释的方式来去引入 Source Map 文件
这样我们在 Chrome 浏览器中如果打开了开发人员工具,它就会自动请求这个文件,然后根据这个文件的内容逆向解析出来源代码,以便于调试。同时因为有了映射关系,所以代码中如果出现了错误,也就能自动定位找到源代码中的位置了。
案例准备
为了可以更好地对比不同模式的 Source Map 之间的差异,我们创建了一个新项目,同时创建出不同模式下的打包结果,通过具体实验来横向对比它们之间的差异。
代码结构
在这个案例中,项目中只有两个 JS 模块,在 main.js 中,我故意加入了一个运行时错误,具体项目结构和部分代码如下:
text
└─ source-map
├── src
│ ├── utils.js
│ └── main.js
├── package.json
└── webpack.config.js
javascript
// ./src/main.js
import { createElement } from "./util";
const element = createElement();
document.body.appendChild(element);
console.log('main.js running')
// help to debug
console.log1('main.js running')
javascript
// ./src/utils.js
export const createElement = () => {
const element = document.createElement('div');
element.textContent = 'Hello, World!';
return element;
}
依赖安装
bash
npm i webpack webpack-cli html-webpack-plugin babel-loader @babel/core @babel/preset-env -D
npm i serve -g
serve用来启动一个本地的开发服务器,调试我们的代码。
webpack配置
Webpack 的配置文件除了可以导出一个配置对象,还可以导出一个数组,数组中每一个元素就是一个单独的打包配置,那这样就可以在一次打包过程中同时执行多个打包任务。
例如,我们这里导出一个数组,然后在这个数组中添加两个打包配置,它们的 entry 都是 ./src/main.js,不过它们输出的文件名不同,具体代码如下:
javascript
// ./webpack.config.js
module.exports = [
{
entry: './src/main.js',
output: {
filename: 'output1.js'
}
},
{
entry: './src/main.js',
output: {
filename: 'output2.js'
}
}
]
这么配置的话,再次打包就会有两个打包子任务工作,执行npx webpack
我们的 dist 中生成的结果也就是两个文件,具体结果如下:
了解了 Webpack 这种配置用法过后,我们再次回到配置文件中,遍历刚刚定义的数组,为每一个模式单独创建一个打包配置,这样就可以一次性生成所有模式下的不同结果,这比我们一个一个去试验的效率更高,而且对比起来也更明显。
javascript
// ./webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
const allModes = [
'eval',
// 'cheap-eval-source-map', //webpack 5 is not support
// 'cheap-module-eval-source-map', //webpack 5 is not support
'eval-source-map',
'cheap-source-map',
'cheap-module-source-map',
'inline-cheap-source-map',
'inline-cheap-module-source-map',
'source-map',
'inline-source-map',
'hidden-source-map',
'nosources-source-map'
]
module.exports = allModes.map(item => ({
devtool: item,
mode: 'none',
entry: './src/main.js',
output: {
filename: `js/${item}.js`,
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
filename: `${item}.html`
})
]
}))
这里简单解释一下这个配置中的部分配置用意:
- 定义
devtool
属性,它就是当前所遍历的模式名称(devtool); - 将
mode
设置为 none,确保 Webpack 内部不做额外处理; - 设置打包入口和输出文件名称,打包入口都是 ./src/main.js,输出文件名称我们就放在 js 目录中,以模式名称命名
- 为 js 文件配置一个 babel-loader,配置 babel-loader 的目的是稍后能够辨别其中一类模式的差异。
- 配置一个 html-webpack-plugin,也就是为每个打包任务生成一个 HTML 文件,通过前面的内容,我们知道 html-webpack-plugin 可以生成使用打包结果的 HTML,接下来我们就是通过这些 HTML 在浏览器中进行尝试。
配置完成以后,我们再次回到命令行终端运行打包,那此时这个打包过程就自动生成了不同模式下的打包结果,具体结果如下图所示
然后我们执行 npx serve dist
把结果运行起来,打开浏览器,此时我们能够在页面中看到每一个使用不同模式 Source Map 的 HTML 文件,具体如下图
不同模式的对比
eval 模式
首先来看 eval 模式。在去具体了解 Webpack eval 模式的 Source Map 之前,我们需要先了解一下 JavaScript 中 eval 的一些特点。
eval 其实指的是 JavaScript 中的一个函数,可以用来运行字符串中的 JavaScript 代码。例如下面这段代码,字符串中的 console.log("hi~") 就会作为一段 JavaScript 代码被执行:
javascript
const code = 'console.log("hi~")'
eval(code) // 将 code 中的字符串作为 JS 代码执行
我们可以直接在浏览器控制台执行它
其实我们可以通过 sourceURL
来声明这段代码所属文件路径,接下来我们再来尝试在执行的 JavaScript 字符串中添加一个 sourceURL
的声明,具体操作如下:
javascript
const code = 'console.log("hi~") //# sourceURL=./js/evel.js'
eval(code) // 将 code 中的字符串作为 JS 代码执行
再回到我们js文件夹下的的evel.js文件,你会发现每个模块中的代码都被包裹到了一个 eval 函数中,而且每段模块代码的最后都会通过 sourceURL 的方式声明这个模块对应的源文件路径。
回到浏览器,点击evel.html。
但是当你点击控制台中的文件名打开这个文件后,看到的却是打包后的模块代码,而并非我们真正的源代码,具体如下:
综上所述,在 eval 模式下,Webpack 会将每个模块转换后的代码都放到 eval 函数中执行,并且通过 sourceURL 声明对应的文件路径,这样浏览器就能知道某一行代码到底是在源代码的哪个文件中。
因为在 eval 模式下并不会生成 Source Map 文件,所以它的构建速度最快,但是缺点同样明显:它只能定位源代码的文件路径,无法知道具体的行列信息。
eval-source-map
这个模式也是使用 eval 函数执行模块代码,不过这里有所不同的是,eval-source-map 模式除了定位文件,还可以定位具体的行列信息。相比于 eval 模式,它不会生成 .map 文件,而是将 source map 以 data URI 的形式内嵌在打包后的 JS 文件里,你可以在 eval-source-map.js 里看到类似这样的内容:
language
//# sourceMappingURL=data:application/json;charset=utf-8;base64,...
浏览器会自动识别并解析这些内嵌的 source map 数据,所以你在浏览器开发者工具中依然可以看到和调试源文件(如 main.js、util.js),即使没有单独的 .map 文件。
cheap-eval-source-map
和 cheap-module-eval-source-map
在新版webpack5中不再支持,此处不在讲解。
cheap-source-map
在讲解cheap-source-map
前,我先给大家总结一个规律。mode名字里带有evel
的就用到了evel函数。名字中不带 module
的模式,解析出来的源代码是没有经过 Loader 加工的,而名字中不带 module 的模式,解析出来的源代码是经过 Loader 加工后的结果。这也是我们在config文件中加babel loader的原因(区别差异)。
cheap-source-map 可以生产.map文件,同时可以获取报错的代码行信息(不包含列信息,是loader处理后的代码)
可以发现代码已经被处理过了。
cheap-module-source-map
与cheap-source-map
相比,cheap-module-source-map
多了个module,表示它可以获取到loader处理前的源代码。同时只可以获取行信息。
inline-source-map
它跟普通的 source-map 效果相同,只不过这种模式下 Source Map 文件不是以物理文件存在,而是以 data URLs 的方式出现在代码中。我们前面遇到的 eval-source-map 也是这种 inline 的方式
hidden-source-map
在这个模式下,我们在开发工具中看不到 Source Map 的效果,但是它也确实生成了 Source Map 文件,这就跟 jQuery 一样,虽然生成了 Source Map 文件,但是代码中并没有引用对应的 Source Map 文件,开发者可以自己选择使用。
nosources-source-map
在这个模式下,我们能看到错误出现的位置(包含行列位置),但是点进去却看不到源代码。这是为了保护源代码在生产环境中不暴露
总结
本文我们通过一个具体的demo展示如何开启webpack中的Source Map, 以及不同的模式之间的差异。在实际的项目开发以及上线中可以根据不同的实际需求更改配置。
Source Map 并不是 Webpack 特有的功能,它们两者的关系只是:Webpack 支持 Source Map。大多数的构建或者编译工具也都支持 Source Map 特性
