前幾篇文章中菱肖,我們介紹了webpack v4.20.2相關的內(nèi)容酵紫,
但是很多老項目,還在使用webpack 3丑勤,
也要一些常用的代碼庫在webpack 4中是不兼容的华嘹。
例如,extract-text-webpack-plugin法竞,目前仍不兼容webpack 4耙厚,
可以參考github中這個issue,Webpack 4 compatibility岔霸。
而且薛躬,我在學習webpack源碼的過程中,
extract-text-webpack-plugin這個插件秉剑,確實給我造成了不小的困擾泛豪,
它用到了childCompiler這個概念,很值得一看侦鹏。
本文我們自成體系诡曙,來看看webpack 3項目,以及extract-text-webpack-plugin的實現(xiàn)邏輯略水。
一圖勝千言价卤,
1. webpack 3示例應用
1.1 初始化
$ mkdir ~/Test/debug-webpack3
$ cd ~/Test/debug-webpack3
$ npm init -f
1.2 安裝依賴
$ npm i -D \
webpack@3.11.0 \
babel-loader@7.1.3 \
babel-core@6.26.0 \
babel-preset-env@1.6.1 \
extract-text-webpack-plugin@3.0.2 \
css-loader@0.28.10 \
less-loader@4.0.6 \
less@2.7.3
1.3 配置webpack
新建webpack.config.js,
const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = {
entry: {
index: path.resolve(__dirname, 'src/index.js'),
},
output: {
path: path.resolve(__dirname, 'dist/'),
filename: '[name].js',
},
module: {
rules: [
{ test: /\.js$/, use: { loader: 'babel-loader', query: { presets: ['babel-preset-env'] } } },
{
test: /\.less$/,
use: ExtractTextPlugin.extract({
use: [
{ loader: 'css-loader' },
{ loader: 'less-loader' },
],
}),
},
]
},
plugins: [
new ExtractTextPlugin({
filename: '[name].css',
}),
]
};
以上代碼中渊涝,我們使用了extract-text-webpack-plugin慎璧,
(1)對于 .less 文件,使用ExtractTextPlugin.extract
配置loader
(2)在plugins
中跨释,增加了一個ExtractTextPlugin
的實例
注:
雖然我們已經(jīng)為ExtractTextPlugin
實例配置了filename
胸私,
但是extract-text-webpack-plugin仍然需要webpack.config.js導出output.filename
,
所以鳖谈,我們在第10
行output
屬性中增加了filename
字段岁疼。
1.4 添加npm scripts
打開package.json,為scripts
屬性添加一個build
字段缆娃,值為"webpack"
{
...
"scripts": {
...
"build": "webpack"
},
...
}
1.5 新建源碼文件
(1)src/index.js
import './index.less';
alert();
(2)src/index.less
body {
background: gray;
}
1.6 編譯打包
$ npm run build
> debug-webpack3@1.0.0 build ~/Test/debug-webpack3
> webpack
Hash: 1b8999f3bb679ecffd56
Version: webpack 3.11.0
Time: 673ms
Asset Size Chunks Chunk Names
index.js 2.64 kB 0 [emitted] index
index.css 29 bytes 0 [emitted] index
[0] ./src/index.js 49 bytes {0} [built]
[1] ./src/index.less 41 bytes {0} [built]
+ 1 hidden module
Child extract-text-webpack-plugin node_modules/_extract-text-webpack-plugin@3.0.2@extract-text-webpack-plugin/dist node_modules/_css-loader@0.28.10@css-loader/index.js!node_modules/_less-loader@4.0.6@less-loader/dist/cjs.js!src/index.less:
[0] ./node_modules/_css-loader@0.28.10@css-loader!./node_modules/_less-loader@4.0.6@less-loader/dist/cjs.js!./src/index.less 211 bytes {0} [built]
+ 1 hidden module
1.7 查看編譯結果
(1)dist/index.js
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, {
/******/ configurable: false,
/******/ enumerable: true,
/******/ get: getter
/******/ });
/******/ }
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = 0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
__webpack_require__(1);
alert();
/***/ }),
/* 1 */
/***/ (function(module, exports) {
// removed by extract-text-webpack-plugin
/***/ })
/******/ ]);
(2)dist/index.css
body {
background: gray;
}
2. 調(diào)試webpack3
2.1 新建debug.js
const webpack = require('webpack');
const options = require('./config/webpack.config');
const compiler = webpack(options);
compiler.run((...args) => {
console.log(...args);
});
2.2 使用vscode進行調(diào)試
在以上代碼第6
行中捷绒,打個斷點瑰排,保持光標位于該文件中,按F5
暖侨。
然后程序停在了斷點處椭住,
2.3 輕車熟路
前幾篇中,我們已經(jīng)對webpack v4.20.2有了一定的了解字逗,
現(xiàn)在雖然是webpack3(v.3.11.0)京郑,我們還是能夠駕輕就熟。
compiler.run
扳肛,會跳入Compiler.js 第226行的run
方法中傻挂,
run(callback) {
...
this.applyPluginsAsync("before-run", this, err => {
...
this.applyPluginsAsync("run", this, err => {
...
this.readRecords(err => {
...
this.compile(onCompiled);
});
});
});
}
與之前的v4.20.2對比一下, webpack 4.20.2 Compiler.js 第198行挖息,
run(callback) {
...
this.hooks.beforeRun.callAsync(this, err => {
...
this.hooks.run.callAsync(this, err => {
...
this.readRecords(err => {
...
this.compile(onCompiled);
});
});
});
}
我們發(fā)現(xiàn)金拒,webpack3中的this.applyPluginsAsync("before-run", this, err => {
,
剛好對應與webpack4中的this.hooks.beforeRun.callAsync(this, err => {
套腹。
其余幾個hooks調(diào)用也類似绪抛。
下文中,我們?nèi)匀环Q插件中實現(xiàn)的切面為hooks电禀。
所以幢码,我們還是可以按以前的分析,知道compiler.run
調(diào)用了this.compile
尖飞,
于是我們在compile
方法中打一個斷點症副。
compile(callback) {
// 斷點
...
this.applyPluginsAsync("before-compile", params, err => {
...
this.applyPluginsParallel("make", compilation, err => {
...
compilation.seal(err => {
...
this.applyPluginsAsync("after-compile", compilation, err => {
...
});
});
});
});
}
注意斷點的位置,是在compile
方法的入口處政基,
還沒調(diào)用compiler.hooks.make
贞铣,也沒調(diào)用compilation.seal
。
然后沮明,見證奇跡的時候到了辕坝。。
我們按下F5
荐健,讓程序繼續(xù)運行酱畅,
結果程序運行了一會之后,又跑到了現(xiàn)在這個斷點江场。
這真是太奇怪了纺酸。
值得一提的是,run
方法中的this.compile
處如果打一個斷點址否,
我們會發(fā)現(xiàn)this.compile
卻沒有被第二次調(diào)用吁峻。
2.4 調(diào)用堆棧
還好vscode的調(diào)試工具提供了查看調(diào)用堆棧的功能,
我們可以點擊某個棧幀在张,來查看程序的執(zhí)行過程用含。
點擊第二行runAsChild
,我們發(fā)現(xiàn)this.compile
是由runAsChild
調(diào)用的帮匾,
runAsChild
是Compiler
類的實例方法啄骇,位于 Compiler.js 第286行。
runAsChild(callback) {
this.compile((err, compilation) => {
...
});
}
那么runAsChild
是哪里調(diào)用的呢瘟斜?
我們點擊第三行pitch
缸夹,結果runAsChild
是由extract-text-webpack-plugin(v3.0.2)調(diào)用的,
代碼位置在螺句,extract-text-webpack-plugin loader.js 第81行虽惭,
childCompiler.runAsChild((err, entries, compilation) => {
...
}
這下就很清楚了,
extract-text-webpack-plugin創(chuàng)建了一個childCompiler
蛇尚,
然后調(diào)用了這個childCompiler
的runAsChild
方法芽唇,結果導致this.compiler
再次被調(diào)用了。
extract-text-webpack-plugin這樣做取劫,會對我們調(diào)試compiler.hooks.make
和compilation.seal
產(chǎn)生困擾匆笤,
因為this.compile
會觸發(fā)兩次,
結果compiler.hooks.make
和compilation.seal
也會觸發(fā)兩次谱邪。
注:
每次加載一個 .less 文件炮捧,都會新建一個childCompiler
,
因此惦银,如果工程中用到了很多 .less 文件咆课,
this.compile
方法會甚至會觸發(fā)成百上千次。
至于為什么會這樣扯俱,我們繼續(xù)往下看书蚪。
3. extract-text-webpack-plugin
3.1 LOADER_EXECUTION
我們繼續(xù)跟蹤調(diào)用堆棧,點到第四行LOADER_EXECUTION
蘸吓,
這個名字我們似曾相識善炫,是的,我們在第四篇runLoaders一節(jié)介紹過它库继,
它位于 loader-runner LoaderRunner.js 第118行箩艺。
var result = (function LOADER_EXECUTION() {
return fn.apply(context, args);
}());
LOADER_EXECUTION
做的事情是,使用已載入的loader宪萄,來加載相匹配的資源艺谆。
此時,已載入的loader是extract-text-webpack-plugin extract方法返回的loader拜英,
匹配的資源是待載入的less文件静汤。
我們來驗證下這個結論,在LOADER_EXECUTION
函數(shù)中打個斷點,
然后重新啟動調(diào)試虫给。
程序第一次來到這里的時候藤抡,是為了加載src/index.js,
體現(xiàn)在context.resource
字段抹估,
~/Test/debug-webpack3/src/index.js
然后我們按F5
缠黍,等待程序第二次來到這里,
此時药蜻,context.resource
變成了瓷式,
~/Test/debug-webpack3/src/index.less
表示當前正在加載 src/index.less。
3.2 childCompiler
現(xiàn)在我們用單步調(diào)試语泽,進入到fn.apply(context, args)
這個調(diào)用里面贸典。
結果程序跳轉(zhuǎn)到了 extract-text-webpack-plugin loader.js pitch函數(shù)中。
export function pitch(request) {
...
if (...) {
...
} else if (...) {
...
} else if (...) {
...
const childCompiler = this._compilation.createChildCompiler(`extract-text-webpack-plugin ${NS} ${request}`, outputOptions);
...
childCompiler.runAsChild((err, entries, compilation) => {
...
});
}
}
看到了吧踱卵,每一次加載 .less文件廊驼,都會執(zhí)行LOADER_EXECUTION
,
每次執(zhí)行LOADER_EXECUTION
都會調(diào)用pitch
函數(shù)颊埃,
pitch
函數(shù)中每次都會創(chuàng)建一個新的childCompiler
蔬充,然后調(diào)用childCompiler.runAsChild
。
3.3 this._compilation
如果我們想知道this._compilation.createChildCompiler
做了什么事情班利,
就必須知道this._compilation
是怎么來的饥漫,
因此,也就必須搞清楚this
是什么罗标。
this
實際上就是pitch
的上下文庸队,我們需要看pitch
是如何被調(diào)用的,
翻看上文的調(diào)用鏈路闯割,我們知道了彻消,
pitch
是通過fn.apply(context, args)
調(diào)用的,其中fn
的值就是pitch
宙拉。
因此宾尚,pitch
中的this
指向了fn.apply(context, args)
中的context
。
通過查看調(diào)用堆棧谢澈,我們最終定位到煌贴,
這個context
是在 webpack NormalModule.js doBuild中調(diào)用createLoaderContext
創(chuàng)建的,
doBuild(options, compilation, resolver, fs, callback) {
...
const loaderContext = this.createLoaderContext(resolver, options, compilation, fs);
runLoaders({
...
context: loaderContext,
...
}, (err, result) => {
...
});
}
createLoaderContext
是NormalModule
的實例方法锥忿,
它的定義在牛郑,NormalModule.js 第112行,
createLoaderContext(resolver, options, compilation, fs) {
const loaderContext = {
...
_compilation: compilation,
...
};
...
return loaderContext;
}
因此敬鬓,這個_compilation
淹朋,就是doBuild
參數(shù)中的compiation
笙各。
而這個compiler
就是在Compiler.js中第497行,觸發(fā)compiler.hooks.make
之前新建的那個compilation
础芍。
const compilation = this.newCompilation(params);
this.applyPluginsParallel("make", compilation, err => {
...
});
3.4 this._compilation.createChildCompiler
我們就可以去Compilation.js 第1416行中查看createChildCompiler
方法了杈抢,
createChildCompiler(name, outputOptions, plugins) {
...
return this.compiler.createChildCompiler(this, name, idx, outputOptions, plugins);
}
它調(diào)用了compiler.createChildCompiler
,在Compiler.js 第413行者甲,
createChildCompiler(compilation, compilerName, compilerIndex, outputOptions, plugins) {
const childCompiler = new Compiler();
...
for(const name in this._plugins) {
if(["make", "compile", "emit", "after-emit", "invalid", "done", "this-compilation"].indexOf(name) < 0)
childCompiler._plugins[name] = this._plugins[name].slice();
}
...
compilation.applyPlugins("child-compiler", childCompiler, compilerName, compilerIndex);
return childCompiler;
}
它會新建一個Compiler
實例春感,然后把原來父compiler
上的_plugins
都淺拷貝過去。
因此虏缸,以前掛載在compiler
上的hooks同樣也會掛載到childCompiler
上,
只是嫩实,當hooks被調(diào)用時刽辙,才會觸發(fā)回調(diào)。
其中"make", "compile", "emit", "after-emit", "invalid", "done", "this-compilation"
甲献,
這些_plugin
不拷貝宰缤。
假如我們寫了一個這樣的webpack3插件,
(只需將webpack4中插件的寫法從hooks改成plugin即可)
class Plugin {
apply(compiler) {
compiler.plugin('compilation', compilation => {
compilation.plugin('seal', () => {
...
});
});
}
}
則當childCompiler
調(diào)用compiler.hooks.compilation
時晃洒,
以上為父compiler
注冊的事件也會在childCompiler
上觸發(fā)慨灭,
唯一不同是參數(shù)compilation
不同。
所以接下來球及,compilation.plugin('seal', () => {
氧骤,
就為這個新compilation
實現(xiàn)了一個新的hooks.seal
。
3.5 hooks的多次觸發(fā)
我們來看下實際使用這個插件時的日志信息吃引。
(1)新建plugin.js
const log = require('debug')('debug-webpack plugin.js');
class Plugin {
apply(compiler) {
compiler.plugin('compilation', compilation => {
log('in: compilation');
compilation.plugin('seal', () => {
log('in: seal, compilation.entries: %s', compilation.entries.map(({ resource }) => resource).join());
});
});
}
}
module.exports = Plugin;
(2)在webpack.config.js中使用它
...
const Plugin = require('./plugin');
module.exports = {
...
plugins: [
...
new Plugin,
]
};
(3)運行一下
$ DEBUG=debug-wepack* npm run build
> debug-webpack3@1.0.0 build ~/Test/debug-webpack3
> webpack
debug-webpack plugin.js in: compilation +0ms
debug-webpack plugin.js in: seal, compilation.entries: ~/Test/debug-webpack3/src/index.js +600ms
debug-webpack plugin.js in: compilation +7ms
debug-webpack plugin.js in: seal, compilation.entries: ~/Test/debug-webpack3/src/index.less +28ms
Hash: 1b8999f3bb679ecffd56
Version: webpack 3.11.0
Time: 657ms
Asset Size Chunks Chunk Names
index.js 2.64 kB 0 [emitted] index
index.css 29 bytes 0 [emitted] index
[0] ./src/index.js 49 bytes {0} [built]
[1] ./src/index.less 41 bytes {0} [built]
+ 1 hidden module
Child extract-text-webpack-plugin node_modules/_extract-text-webpack-plugin@3.0.2@extract-text-webpack-plugin/dist node_modules/_css-loader@0.28.10@css-loader/index.js!node_modules/_less-loader@4.0.6@less-loader/dist/cjs.js!src/index.less:
[0] ./node_modules/_css-loader@0.28.10@css-loader!./node_modules/_less-loader@4.0.6@less-loader/dist/cjs.js!./src/index.less 211 bytes {0} [built]
+ 1 hidden module
我們看到compilation.hooks.seal
總共觸發(fā)了兩次筹陵,
第一次的entry是~/Test/debug-webpack3/src/index.js,
第二次為~/Test/debug-webpack3/src/index.less镊尺。
第二次 .less 文件觸發(fā)compilation.hooks.seal
的流程如下朦佩,
webpack在加載 .less 文件時,使用了extract-text-webpack-plugin庐氮,
每次加載一個 .less 文件语稠,都會創(chuàng)建一個新的 childCompiler
。
這個childCompiler
會把父compiler
中所有的hooks都拷貝過去弄砍,
然后就調(diào)用了childCompiler.runAsChild
仙畦,它會調(diào)用this.compile
,此時this
是childCompiler
输枯,
然后this.compile
中议泵,會觸發(fā)compiler.hooks.compilation
這個hooks(見Compiler.js 第465行)。
這個hooks是從父compiler
那里拷貝過來的桃熄,
因此就會觸發(fā)我們的插件注冊的那個回調(diào)先口,只是傳入一個新創(chuàng)建的compilation
實例作為參數(shù)型奥。
compiler.plugin('compilation', compilation => {
...
});
接著為這個新compilation
實例,在這個回調(diào)中注冊了compilation.hooks.seal
事件碉京。
然后webpack在對 .less 文件 seal的時候厢汹,觸發(fā)hooks.seal
事件時,就引發(fā)了這個回調(diào)谐宙。
參考
extract-text-webpack-plugin: Webpack 4 compatibility
webpack v3.11.0
webpack v4.20.2
loader-runner v2.3.1