[FE] SourceMapDevToolPlugin 和 optimization.minimizer

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]'
        }),
    ],
};

以上配置中,我们注释掉了devtooldevtoolModuleFilenameTemplate
将它们配置到了SourceMapDevToolPlugin中。

这样配置,dist/目录最后也会生成两个文件,

index.js
index.js.map

由于我们使用了该插件的append功能,修改了sourceMappingURL地址,
因此,index.js末尾source map文件的地址就变成了,

//# sourceMappingURL=http://127.0.0.1:8080/dist/index.js.map

注:
这里值得一提的是,同时配置devtoolSourceMapDevToolPlugin是不行的,
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

除了devtoolSourceMapDevToolPlugin之外,还有一个地方会影响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.minimizetrue
UglifyJsPlugin中配置sourceMapfalse

这样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.sourceMapfalse
第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.sourceMapfalseinputSourceMap就为空。
那么在 minify.js 第172行
uglifyOptions.sourceMap就是null了,

反之,如果配置了optimization.minimizersourceMaptrue
则此时,uglifyOptions.sourceMap就是babel转译资源后的innerSourceMap了,
可参考UglifyJS2: Minify options

4. 代码生成

以上代码压缩阶段只是生成了一个数据结构,用来存储压缩结果,还没有写入到文件中,
而在目标代码尾部添加sourceMappingURL,则是在代码生成阶段完成的。

这块代码位于webpack源码lib/SourceMapDevToolPlugin.js 第136行

const task = getTaskForFile(file, chunk, options, compilation);

它为每一个目标文件,看情况创建一个task,创建了task的文件在末尾添加sourceMappingURL

下面我们来看下,什么时候才会创建taskgetTaskForFile的定义位于当前文件第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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 197,966评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,170评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 144,909评论 0 327
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,959评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,851评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,583评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,956评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,590评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,878评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,892评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,719评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,501评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,957评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,124评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,440评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,003评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,211评论 2 339

推荐阅读更多精彩内容

  • GitChat技术杂谈 前言 本文较长,为了节省你的阅读时间,在文前列写作思路如下: 什么是 webpack,它要...
    萧玄辞阅读 12,655评论 7 110
  • 1. 新建一个文件夹,命名为 webpack-cli , webpack-cli 就是你的项目名,项目名建议使用小...
    鲁大师666阅读 1,449评论 1 3
  • publicPath指定了一个在浏览器中被引用的URL地址。 对于使用 和 加载器,当文件路径不同于他们的本地磁盘...
    飞呀飞哥阅读 1,686评论 0 0
  • 前端将大型项目分成一个个单独的模块,一般封装好的每个模块都会实现一个目的明确的完成的功能。如何处理这些模块以及模块...
    pixels阅读 3,404评论 1 14
  • 什么都可以原谅,唯独背叛不能原谅。
    可乐_2c88阅读 195评论 0 0