從UglifyJSPlugin強制開啓css壓縮探究webpack插件運行機制

2018.3.1更:javascript

有贊·微商城部門招前端啦,最近的前端hc有十多個,跪求大佬扔簡歷,我直接進行內推實時反饋進度,有興趣的郵件 lvdada#youzan.com,或直接微信勾搭我 wsldd225 瞭解跟多css

有贊開源組件庫·zanUIhtml


注:本文查看的源碼是webpack1.x版本,2.x版本已經不存在這個問題,查看描述前端

webpack1.x時代討論地比較熱烈的一個話題,就是UglifyJsPlugin插件爲何會對其餘loader形成影響。我這裏有個曾經遇到的問題,能夠查看我爲此編寫的一個demo,有興趣能夠clone試驗一下這個問題。vue

postcss-loader、autoprefixer處理後的css以下,在開發環境一切ok:java

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-前綴:webpack

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

這樣的最終css在ios8如下版本是不兼容的,解決辦法我也寫在了demo中,你們能夠試驗一下。ios

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

經過給css-loader添加-autoprefixer參數來告訴css-loader,雖然你被某股不知名的力量強制進行壓縮了,可是在壓縮的時候關閉掉autoprefixer這個功能,不要強制刪除某些你以爲不重要的前綴。git

文章最前面的webpack issue也提到了,這股不知名的力量其實就是UglifyJsPlugin插件。咱們先來看一下這個插件的一段核心源碼。github

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了。

引用:

  • http://taobaofed.org/blog/2016/09/09/webpack-flow/
  • https://github.com/webpack-contrib/css-loader/tree/v0.19.0
  • https://github.com/postcss/autoprefixer/issues/660
  • https://doc.webpack-china.org/guides/migrating/
  • https://webpack.github.io/docs/plugins.html#the-compiler-instance
  • https://github.com/webpack/webpack/issues/283
相關文章
相關標籤/搜索