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/ 文件夾下,會產(chǎn)生兩個文件,
index.js
index.js.map
index.js文件末尾,webpack會自動添加一行注釋,
//# sourceMappingURL=index.js.map
瀏覽器解析到這里,會自動根據(jù)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文件的內(nèi)容也不正確,是一個空的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構(gòu)建將只生成一個index.js文件了,文件末尾也沒有sourceMappingURL。
3. 代碼壓縮
現(xiàn)在我們來看一下webpack發(fā)生了什么,
我們在項目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);
}
}
}
發(fā)現(xiàn)這里是判斷了webpack.config.js中是否配置了optimization.minimize,
然后依次調(diào)用了minimizer.apply(compiler)。
緊接著就跳轉(zhuǎn)到了,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轉(zhuǎn)譯資源后的innerSourceMap了,
可參考UglifyJS2: Minify options

4. 代碼生成
以上代碼壓縮階段只是生成了一個數(shù)據(jù)結(jié)構(gòu),用來存儲壓縮結(jié)果,還沒有寫入到文件中,
而在目標代碼尾部添加sourceMappingURL,則是在代碼生成階段完成的。
這塊代碼位于webpack源碼lib/SourceMapDevToolPlugin.js 第136行
const task = getTaskForFile(file, chunk, options, compilation);
它為每一個目標文件,看情況創(chuàng)建一個task,創(chuàng)建了task的文件在末尾添加sourceMappingURL。
下面我們來看下,什么時候才會創(chuàng)建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]