「前端」从UglifyJSPlugin强制开启css压缩探究webpack插件运行机制

本文来自尚妆前端团队南洋

发表于尚妆github博客,欢迎订阅!

注:本文查看的源码是webpack1.x版本,2.x版本已经不存在这个问题,查看描述

webpack1.x时代讨论地比较热烈的一个话题,就是UglifyJsPlugin插件为什么会对其他loader造成影响。我这里有个曾经遇到的问题,可以查看我为此编写的一个demo,有兴趣可以clone试验一下这个问题。

postcss-loader、autoprefixer处理后的css如下,在开发环境一切ok:

p {
  display: -webkit-box;
  display: -webkit-flex;
  display: -ms-flexbox;
  display: flex;
  -webkit-box-pack: center;
  -webkit-justify-content: center;
      -ms-flex-pack: center;
          justify-content: center;
}

可是用线上环境UglifyJsPlugin进行打包后,最后的css被剔除了很多-webkit-前缀:

p{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}

这样的最终css在ios8以下版本是不兼容的,解决办法我也写在了demo中,大家可以试验一下。

{test: /\.less$/,   loader: 'style-loader!css-loader?minimize&-autoprefixer!postcss-loader!less-loader'},

通过给css-loader添加-autoprefixer参数来告诉css-loader,虽然你被某股不知名的力量强制进行压缩了,但是在压缩的时候关闭掉autoprefixer这个功能,不要强制删除某些你觉得不重要的前缀。

文章最前面的webpack issue也提到了,这股不知名的力量其实就是UglifyJsPlugin插件。我们先来看一下这个插件的一段核心源码。

compilation.plugin("normal-module-loader",  function(context) {
    context.minimize = true;
});

这块代码先不用理解什么意思,但是minimize字段很明确地告诉大家,某个上下文context的minimize字段被设置成true了。至于这个上下文context是哪个上下文,下文会解释道。

对webpack运行原理不清楚的同学肯定会跟我有一样的疑惑,webpack中的插件(plugin),加载器(loader)到底是怎样的运行机制?插件在什么情况下会影响到loader的工作?以及插件除了影响到loader,还能影响什么?能否影响最后的打包输出?

加载器(loader)的作用很明显,负责处理各种类型的模块,比如png /vue/jsx/css/less等等各种后缀类型,用相应的loader就能识别并进行转换。转换好的文件内容才能被webpack运行时读懂。

插件(plugin),官网的解释非常简单

插件目的在于解决 loader 无法实现的其他事。

比方说,css-loader识别并转换完对应的css模块,babel-loader识别并转换完对应的js,他们的工作就结束了,现在我想把css内容从js里抽离出来变成单独一个css文件,这个工作就只能交给插件来做了。

而插件又是如何识别.css模块成功被css-loader转换这个关键事件节点的?

// 命名函数
function MyExampleWebpackPlugin() {

};

// 在它的 prototype 上定义一个 `apply` 方法。
MyExampleWebpackPlugin.prototype.apply = function(compiler) {
  // 指定挂载的webpack事件钩子。
  compiler.plugin('webpacksEventHook', function(compilation /* 处理webpack内部实例的特定数据。*/, callback) {
    console.log("This is an example plugin!!!");
    // 功能完成后调用webpack提供的回调。
    callback();
  });
};

这是官网提供的插件编写例子,先撇开公共的代码部分我们看以下核心代码:

// 指定挂载的webpack事件钩子。
compiler.plugin('webpacksEventHook', function(compilation /* 处理webpack内部实例的特定数据。*/) {
    console.log("This is an example plugin!!!");
  });

我们看到webpacksEventHookwebpack事件钩子,用plugin方法注册到了compiler对象上,compiler是webpack非常核心的对象,稍后会介绍。

这里的webpacksEventHook事件钩子的种类可以看webpack官网

webpack开放了非常丰富的事件钩子,供开发者们在插件中进行注册。而这些注册完的事件由webpack的compiler对象在对应的节点进行调用。

插件何时以及如何作用于webpack的构建过程,注册事件钩子由compiler(以及下文提到的compilation)进行统一分配调用就是答案。

再看一个相对较复杂的插件编写方式:

function HelloCompilationPlugin(options) {}

HelloCompilationPlugin.prototype.apply = function(compiler) {

  // 设置回调来访问编译对象:
  compiler.plugin("compilation", function(compilation) {

    // 现在设置回调来访问编译中的步骤:
    compilation.plugin("optimize", function() {
      console.log("Assets are being optimized.");
    });
  });
};

module.exports = HelloCompilationPlugin;

抽离核心代码:

// 设置回调来访问编译对象:
  compiler.plugin("compilation", function(compilation) {

    // 现在设置回调来访问编译中的步骤:
    compilation.plugin("optimize", function() {
      console.log("Assets are being optimized.");
    });
  });

compiler对象注册方法的回调返回了一个compilation对象,这个对象也能进行事件注册,但两者的事件钩子是有区别的。具体的事件钩子查看compilation对象和compiler对象构成了webpack最核心的两个对象,几乎所有的构建编译逻辑都由这两个对象完成。

我们看下两个对象在编写插件的时候可以进行事件钩子注册的几个重要事件。

  • 「after-plugins」 compiler对象加载完所有插件。
  • 「compile」 compiler对象开始编译。
  • 「compilation」compiler对象构建出compilation对象。
  • 「make」 compiler对象开始在入门点进行模块分析以及依赖分析。在这个节点注册事件,插件可以手动添加入口文件,webpack会将配置文件中的入口和这里添加的入口一同进行打包流程。
  • 「build-module」 compilation对象开始构建模块。这个时间点模块还没开始构建,入口点已经被分析完,依赖已经分析完。
  • 「normal-module-loader」 compilation对象对每个模块构建并载入loader信息。这个节点在每个模块载入loader信息触发。
  • 「seal」 compilation对象开始封装构建结果
  • 「after-compile」 compiler对象完成构建任务
  • 「emit」 compiler对象开始把chunk输出
  • 「after-emit」 compiler对象完成chunk输出

以上列出的只是部分比较关键的节点,这些节点事件都能在插件中进行注册。注册完后只需等待webpack运行时在对应的节点进行调用,就能完成插件想做的事情。

那么compilercompilation是如何完成编译构建的?其实看了事件钩子罗列大概就对webpack的构建流程有点眉目了,我们顺着事件钩子来大致理一理webpack的工作方式。


    // 构建出compiler对象
    compiler = webpack(options)
    // 在webpack调用过程中,完成了所有必要插件的调用
    // 此时所有插件注册的事件钩子都已经准备完毕,等待被调用
    compiler.options = new WebpackOptionsApply().process(options, compiler);
    
    // 调用插件中的 after-plugins 事件
    compiler.applyPlugins("after-plugins", compiler);
    // 这里涉及很多节点
    // compiler调用compile方法 
    // 此时调用插件中的 compile 事件
    // 构建 compilation 对象
    // 此时调用插件中的 compilation 事件
    // 此时调用插件中的 make 事件
    Compiler.prototype.compile = function(callback) {
        var params = this.newCompilationParams();
        this.applyPlugins("compile", params);
    
        var compilation = this.newCompilation(params);
    
        this.applyPluginsParallel("make", compilation, function(err) {}
    // make事件之后 compilation调用buildModule方法开始构建模块
    // 此时调用插件的 build-module 事件
    // 然后 module 实例会调用build方法
    // 中间略过模块构建的步骤
    // 此时调用插件的 normal-module-loader 事件,代表模块构建完成
    Compilation.prototype.buildModule = function(module, thisCallback) {
        this.applyPlugins("build-module", module);
        ...
        module.build(this.options, this, this.resolvers.normal, this.inputFileSystem, function(err) {}
    // 模块全部构建完成后 compilation开始封装模块
    // 此时调用插件的 seal 事件
    // 完成seal后调用插件的 after-compile 事件
compilation.seal(function(err) 
    this.applyPluginsAsync("after-compile", compilation, function(err) {
    });
}.bind(this));
    // 模块封装好后compilation会调用emitAssets方法将模块打包成chunk输出
    // 此时调用插件的 emit 事件
Compiler.prototype.emitAssets = function(compilation, callback) {
    this.applyPluginsAsync("emit", compilation, function(err) {
    }.bind(this));
}

至此就粗略地完成了整个webpack的编译构建过程,现在再回头看UglifyJsPlugin插件。其在插件中对js的压缩注册了optimize-chunk-assets事件,查阅文档可知这个事件模块封装成chunk触发,所以在最后的阶段对js进行压缩是最好的选择。

还有一个事件就是开头提到的

compilation.plugin("normal-module-loader",  function(context) {
    context.minimize = true;
});

normal-module-loader这个事件在模块开始构建并载入了loader时触发,这段代码的意思就是当模块载入对应的loader时,直接将loader的上下文环境中的minimize字段设置成true,而这个字段在css-loaderpostcss-loader中设置成true会开启优化模式,所以会对代码进行压缩。

而webpack2.x在迁移方案中官方明确说明去掉了UglifyJsPlugin强制开启其他loader优化模式的说明,在webpack2.x源码中UglifyJsPlugin插件已经没有注册normal-module-loader了。

引用:

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

推荐阅读更多精彩内容