Webpack编译流程详解

459 阅读4分钟

1. webpack 运行流程图

avatar 跟着流程图走, 弄懂编译流程不用愁~

2. 研究 webpack 源码的好处

  • 理解 webpack 的运行流程
  • 理解 webpack 插件
  • 学习 webpack 中的技巧和思想

3. webpack 插件

一个简单的插件

    class TestPlugin {
      apply(compiler) {
        console.log("compiler", compiler);
        compiler.hooks.compilation.tap("TestPlugin", function() {
          console.log("compilation", compilation);
        });
      }
    }
    module.exports = TestPlugin;
  • 写插件的关键: 了解 compile, compilation 具体是做什么的, 了解这两个函数的参数,方法, 运行机制,才能写好插件

4. webpack 入口

  • webpack 从入口开始, 输入的是什么, 输出的又是什么呢?

    • 输入: 配置文件+ 源代码
    • 输出: 打包后的文件
  • webpack 的入口是一个 webpack 的函数, 下面我们来看看这个函数具体做了什么, 主要流程如下:

    webpack = options => {
      // 1. 整合options
      // options就是配置, 包括自己的配置加webpack的默认配置, 指导webpack后续的运行
      // 2. 实例化compile
      const compile = new Compile(options.context);
      // 3. 实例化所有的插件, 调用他们的apply方法

      // 4. 返回compile 示例
      return compile;
    };
  • 以下模拟 webpack-cli 中的代码, 相当于在命令行输入 webpack
    // webpack函数核心是生成compile函数, 在外部执行它
    const options = require("./webpack.config.js"); // 所有配置文件
    const compile = webpack(options); // 执行webpack函数
    compile.run(); // 执行
  • webpack 函数第三步的代码示例
    // 插件是否被实例化,是可以进行控制的, 见以下代码
    // WebpackOptionsApply.js
    // options.optimization.removeAvailableModules为true, 才会实例化插件
    if (options.optimization.removeAvailableModules) {
      const RemoveParentModulesPlugin = require("./optimize/RemoveParentModulesPlugin");
      new RemoveParentModulesPlugin().apply(compiler);
    }
    // 对于某些插件, 是必须要进行实例化的
    // 如NodeEnvironmentPlugin, 在实例化compile后, 会立即执行new NodeEnvironmentPlugin(compile).run()
    // 以下为本插件的代码:

    class NodeEnvironmentPlugin {
      apply(compile) {
        compiler.inputFileSystem = new CachedInputFileSystem(
          new NodeJsInputFileSystem(),
          60000
        );
        const inputFileSystem = compiler.inputFileSystem;
        compiler.outputFileSystem = new NodeOutputFileSystem();
        compiler.watchFileSystem = new NodeWatchFileSystem(
          compiler.inputFileSystem
        );
        compiler.hooks.beforeRun.tap("NodeEnvironmentPlugin", compiler => {
          if (compiler.inputFileSystem === inputFileSystem)
            inputFileSystem.purge();
        });
      }
    }
    // 本插件赋予了compile对象文件存储的能力, 这是必须插件
对于webpack插件分为两种, 一种是必须的, 一种是优化插件

5. compile

从 webpack 函数中, 我们知道了实例化 compile 对象后, 会执行 compile.run 方法, 以下我们来看看 compile 函数

   // 先看compile中的run方式
    // compile.js
    class compile extends Tapable {
      constructor(context) {
        // 注册钩子
        this.hooks = {
          ...beforeRun,
          run
        };
      }
      run() {
        this.hooks.beforeRun.callAsync(this, err => {
          this.hooks.run.callAsync(this, err => {
            this.compile(onCompiled); // 传入onCompiled回调
          });
        });
      }
    }
执行步骤  beforeRun(此钩子在上例NodeEnvironmentPlugin注册) => run => 调用compile函数, 传入onCompiled回调
    // 以下为compolie函数示例

    class compile extends Tapable {
      createCompilation() {
        return new Compilation(this); // 实例化Compilation
      }
      newCompilation(params) {
        const compilation = this.createCompilation();
        this.hooks.thisCompilation.call(compilation, params); // thisCompilation钩子
        this.hooks.compilation.call(compilation, params); // thisCompilation钩子
        return compilation;
      }

      compile(callback) {
        this.hooks.beforeCompile.callAsync(params, err => {
          this.hooks.compile.call(params);
          const compilation = new newCompilation(params);
          this.hooks.make.callAsync(compilation, err => {
            compilation.finish(err => {
              compilation.seal(err => {
                this.hooks.afterCompile.callAsync(compilation, err => {
                  return callback(null, compilation);
                });
              });
            });
          });
        });
      }
    }
执行步骤: beforeCompile钩子 => compile钩子 => 实例化compilation(1. 调用thisCompilation => 2. compilation) => make钩子(执行完成后, 模块转化完成)
=> finish => seal封装 => afterCompile钩子(编译结束)
    const onCompiled = (err, compilation) => {
      if (this.hooks.shouldEmit.call(compilation) === false) {
        this.hooks.done.callAsync(status, err => {});
        return;
      }
      this.emitAssets(compilation, err => {
        this.hooks.done.callAsync(status, err => {});
        return;
      });
    };

// 步骤: shouldEmit钩子 ? 成功调用emitAssets => done钩子, 完成一次输出 : 编译不成功调用done钩子
  • compile 总结
    • compile 的调用栈如下: run => compile => onCompiled
    • run 函数中触发的钩子:beforeRun,run
    • compile 函数中触发的钩子:beforeCompile,compile,thisCompilation,compilation,make,afterCompile
    • onCompiled 函数中触发的钩子: should-emit,emit,done

6. compilation

  • 对象职责: 构建模块和 chunk, 并利用插件优化构建过程
  • 首先来理解 compilation 对象, moduleFactory, module 三者之间的关系吧

avatar

compilation 编译模块的入口

要从 compiler 的 make 钩子看起,从上面的 compile 的方法内看到,实例化 compilation 对象后,并没有对它做什么操作,而是直接调用了 make 钩子,在钩子挂载的入口相关的插件中,操作了 compilation,我们来看一下:

    class SingleEntryPlugin {
      constructor(context, entry, name) {
        this.context = context;
        this.entry = entry;
        this.name = name;
      }
      apply(compiler) {
        compiler.hooks.compilation.tap(
          "SingleEntryPlugin",
          (compilation, { normalModuleFactory }) => {
            compilation.dependencyFactories.set(
              SingleEntryDependency,
              normalModuleFactory
            );
          }
        );

        compiler.hooks.make.tapAsync(
          "SingleEntryPlugin",
          (compilation, callback) => {
            const { entry, name, context } = this;

            const dep = SingleEntryPlugin.createDependency(entry, name);
            compilation.addEntry(context, dep, name, callback);
          }
        );
      }

      static createDependency(entry, name) {
        const dep = new SingleEntryDependency(entry);
        dep.loc = { name };
        return dep;
      }
    }

这里使用 SingleEntryPlugin 作为例子,配置单入口时会使用此插件,插件往 make 钩子会挂载了回调函数。它不但挂载了 make 钩子,还挂载了 compilation 钩子,这个钩子先于 make 钩子调用,为 compilation 对象的 dependencyFactories 中添加了值,这值是一个 key-value 对,key 是 SingleEntryDependency,值是 normalModuleFactory,normalModuleFactory 就是一种 modulefactory,我们后面在构建模块中用到。

compilation 编译步骤如下:

执行 make 钩子 => 执行 compilation 下的 addEntry 方法 => _addModuleChain (根据依赖, 递归所有模块, 拿到所有的模块链) => compilation.seal 方法, seal 方法下调用各种钩子, 生成 chunk 对象 => 控制权给到 compile, 执行回调 onCompiled => 调用 done 钩子, 一次构建完成

7. 修改代码后再次编译

当我们修改业务代码再次编译时, webpack-cli 会再次调用 compile.run(), 此时不会再次调用 webpack 函数

avatar