1. 生成source map的两种配置
1.1 配置devtool
最简单的生成source map的方式是,如下配置webpack.config.js,
const path = require('path');
module.exports = {
devtool: 'source-map',
entry: {
index: path.resolve(__dirname, 'src/index.js'),
},
output: {
devtoolModuleFilenameTemplate: '[resource-path]',
path: path.resolve(__dirname, 'dist/'),
filename: 'index.js',
},
module: {
rules: [
{ test: /\.js$/, use: { loader: 'babel-loader', query: { presets: ['@babel/preset-env'] } } },
]
},
};
其中,devtool默认值为false
,
配置为source-map
表示以独立的文件形式生成source map。
因此,dist/
文件夹下,会产生两个文件,
index.js
index.js.map
index.js文件末尾,webpack会自动添加一行注释,
//# sourceMappingURL=index.js.map
浏览器解析到这里,会自动根据index.js的相对路径,请求map文件并加载它。
1.2 SourceMapDevToolPlugin
除了直接配置devtool
之外,还可以使用webpack官方插件 SourceMapDevToolPlugin,
进行更细粒度的source map配置。
const path = require('path');
const webpack = require('webpack');
module.exports = {
// devtool: 'source-map',
entry: {
index: path.resolve(__dirname, 'src/index.js'),
},
output: {
// devtoolModuleFilenameTemplate: '[resource-path]',
path: path.resolve(__dirname, 'dist/'),
filename: 'index.js',
},
module: {
rules: [
{ test: /\.js$/, use: { loader: 'babel-loader', query: { presets: ['@babel/preset-env'] } } },
]
},
plugins: [
new webpack.SourceMapDevToolPlugin({
filename: '[file].map',
moduleFilenameTemplate: '[resource-path]',
append: '\n//# sourceMappingURL=http://127.0.0.1:8080/dist/[url]'
}),
],
};
以上配置中,我们注释掉了devtool
和devtoolModuleFilenameTemplate
,
将它们配置到了SourceMapDevToolPlugin
中。
这样配置,dist/
目录最后也会生成两个文件,
index.js
index.js.map
由于我们使用了该插件的append
功能,修改了sourceMappingURL
地址,
因此,index.js末尾source map文件的地址就变成了,
//# sourceMappingURL=http://127.0.0.1:8080/dist/index.js.map
注:
这里值得一提的是,同时配置devtool
和SourceMapDevToolPlugin
是不行的,
index.js文件末尾会被添加两行sourceMappingURL
,
//# sourceMappingURL=http://127.0.0.1:8080/dist/index.js.map
//# sourceMappingURL=index.js.map
而且map文件的内容也不正确,是一个空的map文件,
{"version":3,"sources":[],"names":[],"mappings":"","file":"index.js","sourceRoot":""}
2. optimization.minimizer
除了devtool
和SourceMapDevToolPlugin
之外,还有一个地方会影响source map,
那就是webpack支持用户自定义文件压缩方式,
const path = require('path');
const webpack = require('webpack');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
module.exports = {
// devtool: 'source-map',
entry: {
index: path.resolve(__dirname, 'src/index.js'),
},
output: {
// devtoolModuleFilenameTemplate: '[resource-path]',
path: path.resolve(__dirname, 'dist/'),
filename: 'index.js',
},
module: {
rules: [
{ test: /\.js$/, use: { loader: 'babel-loader', query: { presets: ['@babel/preset-env'] } } },
]
},
plugins: [
new webpack.SourceMapDevToolPlugin({
filename: '[file].map',
moduleFilenameTemplate: '[resource-path]',
append: '\n//# sourceMappingURL=http://127.0.0.1:8080/dist/[url]'
}),
],
optimization: {
minimize: true,
minimizer: [
new UglifyJsPlugin({
sourceMap: false,
}),
],
},
};
以上配置中,指定了optimization.minimizer
,同时配置optimization.minimize
为true
,
UglifyJsPlugin
中配置sourceMap
为false
。
这样webpack构建将只生成一个index.js文件了,文件末尾也没有sourceMappingURL
。
3. 代码压缩
现在我们来看一下webpack发生了什么,
我们在项目node_modules,webpack源码 lib/WebpackOptionsApply.js 第L463行打个断点,
if (options.optimization.minimize) {
for (const minimizer of options.optimization.minimizer) {
if (typeof minimizer === "function") {
minimizer.call(compiler, compiler);
} else {
minimizer.apply(compiler);
}
}
}
发现这里是判断了webpack.config.js中是否配置了optimization.minimize
,
然后依次调用了minimizer.apply(compiler)
。
紧接着就跳转到了,uglifyjs-webpack-plugin 第158行,
apply(compiler) {
...
此时,
this.options.sourceMap
为
false
,
而 第191行判断了,
if (this.options.sourceMap && asset.sourceAndMap) {
const { source, map } = asset.sourceAndMap();
input = source;
if (UglifyJsPlugin.isSourceMap(map)) {
inputSourceMap = map;
} else {
inputSourceMap = map;
compilation.warnings.push(
new Error(`${file} contains invalid source map`)
);
}
} else {
input = asset.source();
inputSourceMap = null;
}
如果this.options.sourceMap
为false
,inputSourceMap
就为空。
那么在 minify.js 第172行,
uglifyOptions.sourceMap
就是null
了,
反之,如果配置了optimization.minimizer
的sourceMap
为true
,
则此时,uglifyOptions.sourceMap
就是babel转译资源后的innerSourceMap
了,
可参考UglifyJS2: Minify options
4. 代码生成
以上代码压缩阶段只是生成了一个数据结构,用来存储压缩结果,还没有写入到文件中,
而在目标代码尾部添加sourceMappingURL
,则是在代码生成阶段完成的。
这块代码位于webpack源码lib/SourceMapDevToolPlugin.js 第136行
const task = getTaskForFile(file, chunk, options, compilation);
它为每一个目标文件,看情况创建一个task
,创建了task
的文件在末尾添加sourceMappingURL
。
下面我们来看下,什么时候才会创建task
,getTaskForFile
的定义位于当前文件第25行,
const getTaskForFile = (file, chunk, options, compilation) => {
...
if (asset.sourceAndMap) {
const sourceAndMap = asset.sourceAndMap(options);
sourceMap = sourceAndMap.map;
source = sourceAndMap.source;
} else {
sourceMap = asset.map(options);
source = asset.source();
}
if (sourceMap) {
return {
...
};
}
// 这里隐含了 return undefined;
};
可见,getTaskForFile
会判断if (sourceMap) {
,
如果代码压缩阶段没有生成source map,则getTaskForFile
就会返回undefined
了,即不生成task
。
反之,如果当时生成了source map,就会在第273行,
向源码尾部添加了sourceMappingURL
,
if (currentSourceMappingURLComment !== false) {
assets[file] = compilation.assets[file] = new ConcatSource(
new RawSource(source),
currentSourceMappingURLComment.replace(
/\[url\]/g,
sourceMapUrl
)
);
}
其中,
currentSourceMappingURLComment
正是我们在
SourceMapDevToolPlugin
中配置的
append
值,
\n//# sourceMappingURL=http://127.0.0.1:8080/dist/[url]
参考
webpack 4.29.6
uglifyjs-webpack-plugin 2.1.2