[FE] webpack群侠传(八):childCompiler

前几篇文章中,我们介绍了webpack v4.20.2相关的内容,
但是很多老项目,还在使用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的实现逻辑。
一图胜千言,

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的实例

注:
虽然我们已经为ExtractTextPlugin实例配置了filename
但是extract-text-webpack-plugin仍然需要webpack.config.js导出output.filename
所以,我们在第10output属性中增加了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. 调试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进行调试

在以上代码第6行中,打个断点,保持光标位于该文件中,按F5
然后程序停在了断点处,

2.3 轻车熟路

前几篇中,我们已经对webpack v4.20.2有了一定的了解,
现在虽然是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);
            });
        });
    });
}

我们发现,webpack3中的this.applyPluginsAsync("before-run", this, err => {
刚好对应与webpack4中的this.hooks.beforeRun.callAsync(this, err => {
其余几个hooks调用也类似。

下文中,我们仍然称插件中实现的切面为hooks。
所以,我们还是可以按以前的分析,知道compiler.run调用了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方法的入口处,
还没调用compiler.hooks.make,也没调用compilation.seal

然后,见证奇迹的时候到了。。
我们按下F5,让程序继续运行,
结果程序运行了一会之后,又跑到了现在这个断点

这真是太奇怪了。
值得一提的是,run方法中的this.compile处如果打一个断点,
我们会发现this.compile却没有被第二次调用。

2.4 调用堆栈

还好vscode的调试工具提供了查看调用堆栈的功能,

我们可以点击某个栈帧,来查看程序的执行过程。
点击第二行runAsChild,我们发现this.compile是由runAsChild调用的,
runAsChildCompiler类的实例方法,位于 Compiler.js 第286行

runAsChild(callback) {
    this.compile((err, compilation) => {
        ...
    });
}

那么runAsChild是哪里调用的呢?
我们点击第三行pitch,结果runAsChild是由extract-text-webpack-plugin(v3.0.2)调用的,
代码位置在,extract-text-webpack-plugin loader.js 第81行

childCompiler.runAsChild((err, entries, compilation) => {
    ...
}

这下就很清楚了,
extract-text-webpack-plugin创建了一个childCompiler
然后调用了这个childCompilerrunAsChild方法,结果导致this.compiler再次被调用了。

extract-text-webpack-plugin这样做,会对我们调试compiler.hooks.makecompilation.seal产生困扰,
因为this.compile会触发两次,
结果compiler.hooks.makecompilation.seal也会触发两次。

注:
每次加载一个 .less 文件,都会新建一个childCompiler
因此,如果工程中用到了很多 .less 文件,
this.compile方法会甚至会触发成百上千次

至于为什么会这样,我们继续往下看。

3. extract-text-webpack-plugin

3.1 LOADER_EXECUTION

我们继续跟踪调用堆栈,点到第四行LOADER_EXECUTION
这个名字我们似曾相识,是的,我们在第四篇runLoaders一节介绍过它,
它位于 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 函数中打个断点,
然后重新启动调试。

程序第一次来到这里的时候,是为了加载src/index.js,
体现在context.resource字段,

~/Test/debug-webpack3/src/index.js

然后我们按F5,等待程序第二次来到这里,
此时,context.resource变成了,

~/Test/debug-webpack3/src/index.less

表示当前正在加载 src/index.less。

3.2 childCompiler

现在我们用单步调试,进入到fn.apply(context, args)这个调用里面。
结果程序跳转到了 extract-text-webpack-plugin loader.js pitch函数中。

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文件,都会执行LOADER_EXECUTION
每次执行LOADER_EXECUTION 都会调用pitch函数,
pitch函数中每次都会创建一个新的childCompiler,然后调用childCompiler.runAsChild

3.3 this._compilation

如果我们想知道this._compilation.createChildCompiler 做了什么事情,
就必须知道this._compilation是怎么来的,
因此,也就必须搞清楚this是什么。

this实际上就是pitch的上下文,我们需要看pitch是如何被调用的,
翻看上文的调用链路,我们知道了,
pitch是通过fn.apply(context, args)调用的,其中fn的值就是pitch

因此,pitch中的this指向了fn.apply(context, args)中的context
通过查看调用堆栈,我们最终定位到,
这个context是在 webpack NormalModule.js doBuild中调用createLoaderContext创建的,

doBuild(options, compilation, resolver, fs, callback) {
    ...
    const loaderContext = this.createLoaderContext(resolver, options, compilation, fs);

    runLoaders({
        ...
        context: loaderContext,
        ...
    }, (err, result) => {
        ...
    });
}

createLoaderContextNormalModule的实例方法,
它的定义在,NormalModule.js 第112行

createLoaderContext(resolver, options, compilation, fs) {
    const loaderContext = {
        ...
        _compilation: compilation,
        ...
    };
    ...
    return loaderContext;
}

因此,这个_compilation,就是doBuild参数中的compiation
而这个compiler就是在Compiler.js中第497行,触发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);
}

它调用了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被调用时,才会触发回调。

其中"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调用compiler.hooks.compilation时,
以上为父compiler注册的事件也会在childCompiler上触发,
唯一不同是参数compilation不同。

所以接下来,compilation.plugin('seal', () => {
就为这个新compilation实现了一个新的hooks.seal

3.5 hooks的多次触发

我们来看下实际使用这个插件时的日志信息。

(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总共触发了两次,
第一次的entry是~/Test/debug-webpack3/src/index.js,
第二次为~/Test/debug-webpack3/src/index.less。

第二次 .less 文件触发compilation.hooks.seal的流程如下,
webpack在加载 .less 文件时,使用了extract-text-webpack-plugin,
每次加载一个 .less 文件,都会创建一个新的 childCompiler

这个childCompiler会把父compiler中所有的hooks都拷贝过去,
然后就调用了childCompiler.runAsChild,它会调用this.compile,此时thischildCompiler
然后this.compile中,会触发compiler.hooks.compilation这个hooks(见Compiler.js 第465行)。

这个hooks是从父compiler那里拷贝过来的,
因此就会触发我们的插件注册的那个回调,只是传入一个新创建的compilation实例作为参数。

compiler.plugin('compilation', compilation => {
    ...
});

接着为这个新compilation实例,在这个回调中注册了compilation.hooks.seal事件。
然后webpack在对 .less 文件 seal的时候,触发hooks.seal事件时,就引发了这个回调。


参考

extract-text-webpack-plugin: Webpack 4 compatibility
webpack v3.11.0
webpack v4.20.2
loader-runner v2.3.1

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,688评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,559评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,749评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,581评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,741评论 3 271
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,684评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,122评论 2 292
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,847评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,441评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,939评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,333评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,783评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,275评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,830评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,444评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,553评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,618评论 2 249

推荐阅读更多精彩内容