webpack学习笔记

78 阅读6分钟

调试工具:Chrome devtools

package.json的scripts添加配置 "debug": "node --inspect-brk ./node_modules/webpack-cli/bin/cli.js --config ./config/webpack.prod.js" 在终端输入npm run debug之后,进入调试模式,此时断点在webpck文件的第一行。

首先会判断webpack-cli是否已安装,未安装的话,则安装,已安装,直接进入runCli 具体代码如下:

image.png

runCli的代码如下:

const runCli = cli => {
    const path = require("path");
    const pkgPath = require.resolve(`${cli.package}/package.json`);
    // eslint-disable-next-line node/no-missing-require
    const pkg = require(pkgPath);
    // eslint-disable-next-line node/no-missing-require
    
    // 加载webpack-cli的bin文件,即webpack-cli/bin/cli.js
    require(path.resolve(path.dirname(pkgPath), pkg.bin[cli.binName]));
};

进入webpack-cli的bin文件,看看会执行什么 内容很简单:

#!/usr/bin/env node

"use strict";

const importLocal = require("import-local");
const runCLI = require("../lib/bootstrap");

if (!process.env.WEBPACK_CLI_SKIP_IMPORT_LOCAL) {
  // Prefer the local installation of `webpack-cli`
  if (importLocal(__filename)) {
    return;
  }
}

process.title = "webpack";

// 最重要的是这个
runCLI(process.argv);

进入bootstrap文件,文件内容如下:

const WebpackCLI = require("./webpack-cli");

const runCLI = async (args) => {
  // Create a new instance of the CLI object
  const cli = new WebpackCLI();

  try {
    await cli.run(args);
  } catch (error) {
    cli.logger.error(error);
    process.exit(2);
  }
};

module.exports = runCLI;

重点是const cli = new WebpackCLI();cli.run(args);(文件路径:lib/webpack-cli.js)

WebpackCLI构造函数定义了如下方法:

  • colors--为控制台输出添加颜色
  • logger--根据不同类型的输出信息,调用不同console方法
  • program--解析用户输入命令。

接下来直接看cli.run方法(webpck-cli/lib/webpack-cli.js文件)

这个run方法比较长,看起来应该是在解析命令吧,然后经过漫长的解析后(这个过程不了解),解析到当前命令为build,接下来执行run方法中的第1039行代码loadCommandByName,在里面会先执行

this.webpack = await this.loadWebpack()

await this.runWebpack(options, isWatchCommandUsed)

接下来,分别介绍loadWebpackthis.runWebpack


this.loadWebpack方法(webpack-cli/lib/webpack-cli.js)

 async loadWebpack(handleError = true) {
    return this.tryRequireThenImport(WEBPACK_PACKAGE, handleError);
  }

tryRequireThenImport内部会执行require("webpack"),即加载webpack模块,那么其实this.webpack即为加载的webpack模块。根据webpack的package.json的描述信息知,webpack的入口文件为webpack/lib/index.js.

注意看lib/index.js文件,该文件会懒加载lib/webpack.js,只有在真正执行this.webpack()时,才会加载webpack.js文件


this.runWebpack (webpack-cli/lib/webpack-cli.js)

runWebpack在这里会调用this.createCompiler方法,返回结果为我们经常说的贯穿整个webpack生命周期的compiler(好像不对),接下来进入createCompiler内部。

createCompiler内部 (webpack-cli/lib/webpack-cli.js)

image.png

  1. 先拿到运行webpack需要的配置信息
  2. 运行this.webpack,执行webpack/lib/webpack.js文件第108行代码

进入lib/webpack.js内部,这里不截出全部代码,只把关键截取出来

// compiler的创建方法
// create也只截取部分代码
const create = () => {
    // 省略代码
    /** @type {MultiCompiler|Compiler} */
    let compiler;
    let watch = false;
    /** @type {WatchOptions|WatchOptions[]} */
    let watchOptions;
    if (Array.isArray(options)) {
        // 省略代码
    } else {
        const webpackOptions = /** @type {WebpackOptions} */ (options);
        /** @type {Compiler} */
        compiler = createCompiler(webpackOptions);
        watch = webpackOptions.watch;
        watchOptions = webpackOptions.watchOptions || {};
    }
    return { compiler, watch, watchOptions };
};

const { compiler, watch, watchOptions } = create();
if (watch) {
    compiler.watch(watchOptions, callback);
} else {
    // 编译关键
    compiler.run((err, stats) => {
        compiler.close(err2 => {
            callback(err || err2, stats);
        });
    });
}
return compiler;

从以上代码看出,关键点是compiler = createCompiler(webpackOptions);compiler.run(),接下来继续分析以上两个方法。

createCompiler内部(webpack/lib/webpack.js)

const createCompiler = rawOptions => {
    // 将用户传入配置信息与默认信息组合
    const options = getNormalizedWebpackOptions(rawOptions);
    applyWebpackOptionsBaseDefaults(options);
    // Complier: lib/Compiler.js
    const compiler = new Compiler(options.context, options);
    new NodeEnvironmentPlugin({
            infrastructureLogging: options.infrastructureLogging
    }).apply(compiler);
    if (Array.isArray(options.plugins)) {
        // 遍历插件,并执行每个插件的apply或者call方法,compiler为入参
        for (const plugin of options.plugins) {
            if (typeof plugin === "function") {
                plugin.call(compiler, compiler);
            } else {
                plugin.apply(compiler);
            }
        }
    }
    applyWebpackOptionsDefaults(options);
    // 执行compiler的钩子函数
    compiler.hooks.environment.call();
    compiler.hooks.afterEnvironment.call();
    // 注意这里很重要
    new WebpackOptionsApply().process(options, compiler);
    compiler.hooks.initialize.call();
    return compiler;
};

createCompiler主要做了

  1. new了一个Compiler对象
  2. plugin.apply注册了所有的插件
  3. 调用了environmentafterEnvironment环境hook
  4. 调用new WebpackOptionsApply().process将配置属性转为plugin注册
  5. 返回compiler

继续分析上面函数调用的new WebpackOptionsApply().process(options, compiler);(lib/WebpackOptionsApply.js),内部实现有点长(大概就是根据外部配置执行一些webpack内部定义的插件),就不具体看了,先关注这两行代码

new EntryOptionPlugin().apply(compiler);
compiler.hooks.entryOption.call(options.context, options.entry);

由下面分析可知,在注册了entryOption钩子之后,然后又立马触发该钩子,然后在钩子内部回调用,为每个entry注册compilation钩子和make钩子,

进入EntryOptionPlugin函数具体看一下代码。

EntryOptionPlugin(lib/EntryOptionPlugin.js)

apply(compiler) {
    compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => {
        EntryOptionPlugin.applyEntryOption(compiler, context, entry);
        return true;
    });
}

static applyEntryOption(compiler, context, entry) {
    if (typeof entry === "function") {
        // 代码省略
    } else {
        const EntryPlugin = require("./EntryPlugin");
        for (const name of Object.keys(entry)) {
            const desc = entry[name];
            const options = EntryOptionPlugin.entryDescriptionToOptions(
                compiler,
                name,
                desc
            );
            for (const entry of desc.import) {
                new EntryPlugin(context, entry, options).apply(compiler);
            }
        }
    }
}

接下来看EntryPlugin

apply(compiler) {
    // 注册compilation钩子
    compiler.hooks.compilation.tap(
        "EntryPlugin",
        (compilation, { normalModuleFactory }) => {
            compilation.dependencyFactories.set(
                EntryDependency,
                normalModuleFactory
            );
      }
    );

    const { entry, options, context } = this;
    const dep = EntryPlugin.createDependency(entry, options);
    // 注册make钩子
    compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
       compilation.addEntry(context, dep, options, err => {
            callback(err);
       });
    });
}

compiler.run内部(lib/Compiler 424行)

const run = () => {
    // 运行beforeRun钩子
    this.hooks.beforeRun.callAsync(this, err => {
        if (err) return finalCallback(err);

        // 运行run钩子
        this.hooks.run.callAsync(this, err => {
            if (err) return finalCallback(err);

            this.readRecords(err => {
                if (err) return finalCallback(err);

                this.compile(onCompiled);
            });
        });
    });
};

先不管其他钩子的执行情况,先看this.compile(onCompiled)

compile(callback) {
  const params = this.newCompilationParams();
  // 触发beforeCompile钩子
  this.hooks.beforeCompile.callAsync(params, err => {
    if (err) return callback(err);

    // 触发compile钩子
    this.hooks.compile.call(params);

    // 出现compilation了
    const compilation = this.newCompilation(params);

    const logger = compilation.getLogger("webpack.Compiler");

    logger.time("make hook");
    
    // 触发make钩子
    this.hooks.make.callAsync(compilation, err => {
      logger.timeEnd("make hook");
      if (err) return callback(err);

      logger.time("finish make hook");
      
      // 触发finishMake钩子
      this.hooks.finishMake.callAsync(compilation, err => {
        logger.timeEnd("finish make hook");
        if (err) return callback(err);

        process.nextTick(() => {
          logger.time("finish compilation");
          compilation.finish(err => {
            logger.timeEnd("finish compilation");
            if (err) return callback(err);

            logger.time("seal compilation");
            compilation.seal(err => {
              logger.timeEnd("seal compilation");
              if (err) return callback(err);

              logger.time("afterCompile hook");
              
              this.hooks.afterCompile.callAsync(compilation, err => {
                logger.timeEnd("afterCompile hook");
                if (err) return callback(err);

                return callback(null, compilation);
              });
            });
          });
        });
      });
    });
  });
}

从上面代码this.compile可以看出,在触发beforeCompilecompile钩子之后,会触发make钩子。从EntryPlugin的分析中得知,注册了make钩子。下面具体来看看make钩子里面做了什么事。

image.png 那接下来主要看看compilation.addentry做了什么

webpack/lib/Compilation.js

image.png image.png 在执行compilation.addentry这里主要做了

  1. 执行_addEntryItem,用于添加入口的item
  2. 执行addModuleTree
  3. addModuleTree中,执行handleModuleCreation
  4. handleModuleCreation中执行factorizeModule,添加hooks到factorizeQueue队列中
  5. factorizeModule回调用执行addModule,添加module模块到addModuleQueue队列中
  6. addModule中执行AsyncQueue.js中的add,在add方法中执行root._ensureProcessing,再执行this._startProcessing(entry),再执行this._processor(entry.item, callbak(略));(在构造函数中,processor会转成_processor)
  7. buildQueue有一个属性processor值为this._buildModule.bind(this),再执行this._processor,即执行this._buildModule
  8. _buildModule中执行module.needBuild判断模块是否需要重新构建
  9. module.needBuild回调中执行module.build
  10. 最后会在webpack/lib/NormalModule.js中执行build方法,开始构建模块

module的build阶段

上一节中提到,module最后在webpack/lib/NormalModule.js中执行build方法,开始构建模块,那我们具体看看build到底做了哪些事情

webpack/lib/NormalModule.js

  1. 执行build,调用_doBuild image.png
  2. 执行_doBuild

image.png 这里的const { getContext, runLoaders } = require("loader-runner");,在runLoaders的回调中执行processResult

  1. 执行processResult

image.pngprocessResult中会执行callback,而这个callback即为调用_doBuild传入的callBack

  1. 执行_doBuildcallback

image.png

这个parse来自node_modules/webpack/lib/javascript/JavascriptParser.js内的parse.这个parse其实是用到了acorn这个库来解析javascript

  1. _doBuildcallback中调用handleParseResult,在handleParseResult中又调用handleBuildDone,在handleBuildDone里面又调用了传入build里的回调

image.pngbuild执行完后,又会执行传入_buildModule的回调,即为: (node_modules/webpack/lib/util/AsyncQueue.js) image.png

  1. _buildModule的回调全部执行完后,hooks.make执行完成,于是接下来会执行webpack/lib/Compiler.jscompilation.finishcompilation.seal方法 image.png