[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的实现逻辑。
一图胜千言,

[FE] webpack群侠传(八):childCompiler_第1张图片

1. webpack 3示例应用

1.1 初始化

$ mkdir ~/Test/debug-webpack3
$ cd ~/Test/debug-webpack3
$ npm init -f

1.2 安装依赖

$ npm i -D \
[email protected] \
[email protected] \
[email protected] \
[email protected] \
[email protected] \
[email protected] \
[email protected] \
[email protected]

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

> [email protected] 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/[email protected]@extract-text-webpack-plugin/dist node_modules/[email protected]@css-loader/index.js!node_modules/[email protected]@less-loader/dist/cjs.js!src/index.less:
       [0] ./node_modules/[email protected]@css-loader!./node_modules/[email protected]@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
然后程序停在了断点处,

[FE] webpack群侠传(八):childCompiler_第2张图片

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

[FE] webpack群侠传(八):childCompiler_第3张图片

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

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

2.4 调用堆栈

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

[FE] webpack群侠传(八):childCompiler_第4张图片

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

runAsChild(callback) {
    this.compile((err, compilation) => {
        ...
    });
}
[FE] webpack群侠传(八):childCompiler_第5张图片

那么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

[FE] webpack群侠传(八):childCompiler_第6张图片

我们继续跟踪调用堆栈,点到第四行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

> [email protected] 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/[email protected]@extract-text-webpack-plugin/dist node_modules/[email protected]@css-loader/index.js!node_modules/[email protected]@less-loader/dist/cjs.js!src/index.less:
       [0] ./node_modules/[email protected]@css-loader!./node_modules/[email protected]@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。

[FE] webpack群侠传(八):childCompiler_第7张图片

第二次 .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

你可能感兴趣的:([FE] webpack群侠传(八):childCompiler)