本文同时发表于尚妆github博客,欢迎订阅!javascript
注:本文查看的源码是webpack1.x版本,2.x版本已经不存在这个问题,查看描述。css
webpack1.x时代讨论地比较热烈的一个话题,就是UglifyJsPlugin
插件为何会对其余loader形成影响。我这里有个曾经遇到的问题,能够查看我为此编写的一个demo,有兴趣能够clone试验一下这个问题。html
由postcss-loader、autoprefixer
处理后的css以下,在开发环境一切ok:vue
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-前缀:java
p{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}
这样的最终css在ios8如下版本是不兼容的,解决办法我也写在了demo中,你们能够试验一下。webpack
{test: /\.less$/, loader: 'style-loader!css-loader?minimize&-autoprefixer!postcss-loader!less-loader'},
经过给css-loader添加-autoprefixer
参数来告诉css-loader,虽然你被某股不知名的力量强制进行压缩了,可是在压缩的时候关闭掉autoprefixer
这个功能,不要强制删除某些你以为不重要的前缀。ios
文章最前面的webpack issue也提到了,这股不知名的力量其实就是UglifyJsPlugin
插件。咱们先来看一下这个插件的一段核心源码。git
compilation.plugin("normal-module-loader", function(context) { context.minimize = true; });
这块代码先不用理解什么意思,可是minimize字段很明确地告诉你们,某个上下文context
的minimize字段被设置成true了。至于这个上下文context
是哪一个上下文,下文会解释道。github
对webpack运行原理不清楚的同窗确定会跟我有同样的疑惑,webpack中的插件(plugin),加载器(loader)究竟是怎样的运行机制?插件在什么状况下会影响到loader的工做?以及插件除了影响到loader,还能影响什么?可否影响最后的打包输出?web
加载器(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!!!"); });
咱们看到webpacksEventHook
webpack事件钩子,用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运行时在对应的节点进行调用,就能完成插件想作的事情。
那么compiler
和compilation
是如何完成编译构建的?其实看了事件钩子罗列大概就对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 事件,表明模块载入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-loader
和postcss-loader
中设置成true会开启优化模式,因此会对代码进行压缩。
而webpack2.x在迁移方案中官方明确说明去掉了UglifyJsPlugin
强制开启其余loader优化模式的说明,在webpack2.x源码中UglifyJsPlugin
插件已经没有注册normal-module-loader
了。
引用: