Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
ShowJoy-com opened this issue Jun 26, 2017 · 2 comments

Comments

@ShowJoy-com
Copy link
Owner

ShowJoy-com commented Jun 26, 2017

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

发表于尚妆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 事件,代表模块载入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了。

引用:

@zlxbuzz
Copy link

zlxbuzz commented Jun 27, 2017

这个autoprefixer的问题吧?我采用autoprefixer 6.7.0, browsers: ['last 20 versions'], 没有发现这个问题

@ShowJoy-com
Copy link
Owner Author

@zlxbuzz 差不多是 最根本的原因还是css-loader autoprefixer的问题。但是直接原因正是因为UglifyJsPlugin这个插件开启了minimize,导致css-loader也开启了压缩,然后css-loader会使用cssnano进行压缩,而cssnano会使用到autoprefixer进行无关前缀的清理。这里cssnano如何使用autoprefixer我没有深入进去了,不过猜测应该是使用了默认的browsersList。

新版本的css-loader、autoprefixer已经做了处理不会再有这样的问题了。

而文中也说明了,webpack2的UglifyJsPlugin插件去掉了强制开启minimize,也同样不会有这样的问题了。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants