当前位置: 首页 > news >正文

「前端」从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了。

引用:

  • http://taobaofed.org/blog/201...

  • https://github.com/webpack-co...

  • https://github.com/postcss/au...

  • https://doc.webpack-china.org...

  • https://webpack.github.io/doc...

  • https://github.com/webpack/we...

相关文章:

  • bzoj2038: [2009国家集训队]小Z的袜子(hose)
  • C++语言基础(10)-虚继承
  • css等高布局技巧
  • union、union all的用法和区别
  • 数据库写操作弃用“SELECT ... FOR UPDATE”解决方案
  • android preference page
  • 在Windows操作系统中,如何终止占有的8080端口的tomcat进程
  • C/C++程序员必须熟练应用的开源项目
  • 创建一个Struts2项目maven 方式
  • mysql 如何把查询到的结果插入到另一个表中
  • How to convert XML String into XML document
  • OA系统:OA的易用性是OA软件商立足根本
  • 寻找适合并行编程模型的中间件
  • 智慧城市:大连社会治理创新“中山模式”
  • 转型太慢药丸?西数欲举债180亿美元竞购闪迪!
  • 【许晓笛】 EOS 智能合约案例解析(3)
  • ECMAScript 6 学习之路 ( 四 ) String 字符串扩展
  • express.js的介绍及使用
  • idea + plantuml 画流程图
  • JavaScript设计模式系列一:工厂模式
  • JDK 6和JDK 7中的substring()方法
  • LeetCode18.四数之和 JavaScript
  • PHP 小技巧
  • React-flux杂记
  • tweak 支持第三方库
  • 番外篇1:在Windows环境下安装JDK
  • 翻译--Thinking in React
  • 前端学习笔记之观察者模式
  • 想使用 MongoDB ,你应该了解这8个方面!
  • 移动互联网+智能运营体系搭建=你家有金矿啊!
  • 原生js练习题---第五课
  • MyCAT水平分库
  • ​渐进式Web应用PWA的未来
  • #mysql 8.0 踩坑日记
  • ( 10 )MySQL中的外键
  • (非本人原创)我们工作到底是为了什么?​——HP大中华区总裁孙振耀退休感言(r4笔记第60天)...
  • (附源码)spring boot智能服药提醒app 毕业设计 102151
  • (十六)一篇文章学会Java的常用API
  • (一)u-boot-nand.bin的下载
  • (原創) 如何刪除Windows Live Writer留在本機的文章? (Web) (Windows Live Writer)
  • (转)如何上传第三方jar包至Maven私服让maven项目可以使用第三方jar包
  • *(长期更新)软考网络工程师学习笔记——Section 22 无线局域网
  • .NET HttpWebRequest、WebClient、HttpClient
  • .NET 中创建支持集合初始化器的类型
  • .NET 中小心嵌套等待的 Task,它可能会耗尽你线程池的现有资源,出现类似死锁的情况
  • .NET 中选择合适的文件打开模式(CreateNew, Create, Open, OpenOrCreate, Truncate, Append)
  • .netcore 如何获取系统中所有session_如何把百度推广中获取的线索(基木鱼,电话,百度商桥等)同步到企业微信或者企业CRM等企业营销系统中...
  • .net生成的类,跨工程调用显示注释
  • // an array of int
  • @html.ActionLink的几种参数格式
  • @Validated和@Valid校验参数区别
  • [20171113]修改表结构删除列相关问题4.txt
  • [android学习笔记]学习jni编程
  • [bzoj4010][HNOI2015]菜肴制作_贪心_拓扑排序
  • [CSS]CSS 的背景