本文來自尚妝前端團(tuán)隊南洋
發(fā)表于尚妝github博客,歡迎訂閱!
注:本文查看的源碼是webpack1.x版本,2.x版本已經(jīng)不存在這個問題,查看描述。
webpack1.x時代討論地比較熱烈的一個話題,就是UglifyJsPlugin插件為什么會對其他loader造成影響。我這里有個曾經(jīng)遇到的問題,可以查看我為此編寫的一個demo,有興趣可以clone試驗(yàn)一下這個問題。
由postcss-loader、autoprefixer處理后的css如下,在開發(fā)環(huán)境一切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;
}
可是用線上環(huán)境UglifyJsPlugin進(jìn)行打包后,最后的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中,大家可以試驗(yàn)一下。
{test: /\.less$/, loader: 'style-loader!css-loader?minimize&-autoprefixer!postcss-loader!less-loader'},
通過給css-loader添加-autoprefixer參數(shù)來告訴css-loader,雖然你被某股不知名的力量強(qiáng)制進(jìn)行壓縮了,但是在壓縮的時候關(guān)閉掉autoprefixer這個功能,不要強(qiáng)制刪除某些你覺得不重要的前綴。
文章最前面的webpack issue也提到了,這股不知名的力量其實(shí)就是UglifyJsPlugin插件。我們先來看一下這個插件的一段核心源碼。
compilation.plugin("normal-module-loader", function(context) {
context.minimize = true;
});
這塊代碼先不用理解什么意思,但是minimize字段很明確地告訴大家,某個上下文context的minimize字段被設(shè)置成true了。至于這個上下文context是哪個上下文,下文會解釋道。
對webpack運(yùn)行原理不清楚的同學(xué)肯定會跟我有一樣的疑惑,webpack中的插件(plugin),加載器(loader)到底是怎樣的運(yùn)行機(jī)制?插件在什么情況下會影響到loader的工作?以及插件除了影響到loader,還能影響什么?能否影響最后的打包輸出?
加載器(loader)的作用很明顯,負(fù)責(zé)處理各種類型的模塊,比如png /vue/jsx/css/less等等各種后綴類型,用相應(yīng)的loader就能識別并進(jìn)行轉(zhuǎn)換。轉(zhuǎn)換好的文件內(nèi)容才能被webpack運(yùn)行時讀懂。
插件(plugin),官網(wǎng)的解釋非常簡單
插件目的在于解決 loader 無法實(shí)現(xiàn)的其他事。
比方說,css-loader識別并轉(zhuǎn)換完對應(yīng)的css模塊,babel-loader識別并轉(zhuǎn)換完對應(yīng)的js,他們的工作就結(jié)束了,現(xiàn)在我想把css內(nèi)容從js里抽離出來變成單獨(dú)一個css文件,這個工作就只能交給插件來做了。
而插件又是如何識別.css模塊成功被css-loader轉(zhuǎn)換這個關(guān)鍵事件節(jié)點(diǎn)的?
// 命名函數(shù)
function MyExampleWebpackPlugin() {
};
// 在它的 prototype 上定義一個 `apply` 方法。
MyExampleWebpackPlugin.prototype.apply = function(compiler) {
// 指定掛載的webpack事件鉤子。
compiler.plugin('webpacksEventHook', function(compilation /* 處理webpack內(nèi)部實(shí)例的特定數(shù)據(jù)。*/, callback) {
console.log("This is an example plugin!!!");
// 功能完成后調(diào)用webpack提供的回調(diào)。
callback();
});
};
這是官網(wǎng)提供的插件編寫例子,先撇開公共的代碼部分我們看以下核心代碼:
// 指定掛載的webpack事件鉤子。
compiler.plugin('webpacksEventHook', function(compilation /* 處理webpack內(nèi)部實(shí)例的特定數(shù)據(jù)。*/) {
console.log("This is an example plugin!!!");
});
我們看到webpacksEventHookwebpack事件鉤子,用plugin方法注冊到了compiler對象上,compiler是webpack非常核心的對象,稍后會介紹。
這里的webpacksEventHook事件鉤子的種類可以看webpack官網(wǎng)
webpack開放了非常豐富的事件鉤子,供開發(fā)者們在插件中進(jìn)行注冊。而這些注冊完的事件由webpack的compiler對象在對應(yīng)的節(jié)點(diǎn)進(jìn)行調(diào)用。
插件何時以及如何作用于webpack的構(gòu)建過程,注冊事件鉤子由compiler(以及下文提到的compilation)進(jìn)行統(tǒng)一分配調(diào)用就是答案。
再看一個相對較復(fù)雜的插件編寫方式:
function HelloCompilationPlugin(options) {}
HelloCompilationPlugin.prototype.apply = function(compiler) {
// 設(shè)置回調(diào)來訪問編譯對象:
compiler.plugin("compilation", function(compilation) {
// 現(xiàn)在設(shè)置回調(diào)來訪問編譯中的步驟:
compilation.plugin("optimize", function() {
console.log("Assets are being optimized.");
});
});
};
module.exports = HelloCompilationPlugin;
抽離核心代碼:
// 設(shè)置回調(diào)來訪問編譯對象:
compiler.plugin("compilation", function(compilation) {
// 現(xiàn)在設(shè)置回調(diào)來訪問編譯中的步驟:
compilation.plugin("optimize", function() {
console.log("Assets are being optimized.");
});
});
compiler對象注冊方法的回調(diào)返回了一個compilation對象,這個對象也能進(jìn)行事件注冊,但兩者的事件鉤子是有區(qū)別的。具體的事件鉤子查看。compilation對象和compiler對象構(gòu)成了webpack最核心的兩個對象,幾乎所有的構(gòu)建編譯邏輯都由這兩個對象完成。
我們看下兩個對象在編寫插件的時候可以進(jìn)行事件鉤子注冊的幾個重要事件。
- 「after-plugins」 compiler對象加載完所有插件。
- 「compile」 compiler對象開始編譯。
- 「compilation」compiler對象構(gòu)建出compilation對象。
- 「make」 compiler對象開始在入門點(diǎn)進(jìn)行模塊分析以及依賴分析。在這個節(jié)點(diǎn)注冊事件,插件可以手動添加入口文件,webpack會將配置文件中的入口和這里添加的入口一同進(jìn)行打包流程。
- 「build-module」 compilation對象開始構(gòu)建模塊。這個時間點(diǎn)模塊還沒開始構(gòu)建,入口點(diǎn)已經(jīng)被分析完,依賴已經(jīng)分析完。
- 「normal-module-loader」 compilation對象對每個模塊構(gòu)建并載入loader信息。這個節(jié)點(diǎn)在每個模塊載入loader信息觸發(fā)。
- 「seal」 compilation對象開始封裝構(gòu)建結(jié)果
- 「after-compile」 compiler對象完成構(gòu)建任務(wù)
- 「emit」 compiler對象開始把chunk輸出
- 「after-emit」 compiler對象完成chunk輸出
以上列出的只是部分比較關(guān)鍵的節(jié)點(diǎn),這些節(jié)點(diǎn)事件都能在插件中進(jìn)行注冊。注冊完后只需等待webpack運(yùn)行時在對應(yīng)的節(jié)點(diǎn)進(jìn)行調(diào)用,就能完成插件想做的事情。
那么compiler和compilation是如何完成編譯構(gòu)建的?其實(shí)看了事件鉤子羅列大概就對webpack的構(gòu)建流程有點(diǎn)眉目了,我們順著事件鉤子來大致理一理webpack的工作方式。
// 構(gòu)建出compiler對象
compiler = webpack(options)
// 在webpack調(diào)用過程中,完成了所有必要插件的調(diào)用
// 此時所有插件注冊的事件鉤子都已經(jīng)準(zhǔn)備完畢,等待被調(diào)用
compiler.options = new WebpackOptionsApply().process(options, compiler);
// 調(diào)用插件中的 after-plugins 事件
compiler.applyPlugins("after-plugins", compiler);
// 這里涉及很多節(jié)點(diǎn)
// compiler調(diào)用compile方法
// 此時調(diào)用插件中的 compile 事件
// 構(gòu)建 compilation 對象
// 此時調(diào)用插件中的 compilation 事件
// 此時調(diào)用插件中的 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調(diào)用buildModule方法開始構(gòu)建模塊
// 此時調(diào)用插件的 build-module 事件
// 然后 module 實(shí)例會調(diào)用build方法
// 中間略過模塊構(gòu)建的步驟
// 此時調(diào)用插件的 normal-module-loader 事件,代表模塊構(gòu)建完成
Compilation.prototype.buildModule = function(module, thisCallback) {
this.applyPlugins("build-module", module);
...
module.build(this.options, this, this.resolvers.normal, this.inputFileSystem, function(err) {}
// 模塊全部構(gòu)建完成后 compilation開始封裝模塊
// 此時調(diào)用插件的 seal 事件
// 完成seal后調(diào)用插件的 after-compile 事件
compilation.seal(function(err)
this.applyPluginsAsync("after-compile", compilation, function(err) {
});
}.bind(this));
// 模塊封裝好后compilation會調(diào)用emitAssets方法將模塊打包成chunk輸出
// 此時調(diào)用插件的 emit 事件
Compiler.prototype.emitAssets = function(compilation, callback) {
this.applyPluginsAsync("emit", compilation, function(err) {
}.bind(this));
}
至此就粗略地完成了整個webpack的編譯構(gòu)建過程,現(xiàn)在再回頭看UglifyJsPlugin插件。其在插件中對js的壓縮注冊了optimize-chunk-assets事件,查閱文檔可知這個事件模塊封裝成chunk觸發(fā),所以在最后的階段對js進(jìn)行壓縮是最好的選擇。
還有一個事件就是開頭提到的
compilation.plugin("normal-module-loader", function(context) {
context.minimize = true;
});
normal-module-loader這個事件在模塊開始構(gòu)建并載入了loader時觸發(fā),這段代碼的意思就是當(dāng)模塊載入對應(yīng)的loader時,直接將loader的上下文環(huán)境中的minimize字段設(shè)置成true,而這個字段在css-loader和postcss-loader中設(shè)置成true會開啟優(yōu)化模式,所以會對代碼進(jìn)行壓縮。
而webpack2.x在遷移方案中官方明確說明去掉了UglifyJsPlugin強(qiáng)制開啟其他loader優(yōu)化模式的說明,在webpack2.x源碼中UglifyJsPlugin插件已經(jīng)沒有注冊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