「前端」從UglifyJSPlugin強制開啟css壓縮探究webpack外掛執行機制

尚妝產品技術刊讀發表於2019-02-14

本文來自尚妝前端團隊南洋

發表於尚妝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了。

引用:

相關文章