【面向面试学习】Webpack插件运行机制

34 阅读5分钟

插件的概念

插件是一个具有apply方法的JavaScript对象,apply方法会被webpack compiler调用,并且在整个编译声明周期都可以访问compiler对象

插件是 webpack 生态的关键部分, 它为社区用户提供了一种强有力的方式来直接触及 webpack 的编译过程(compilation process)。 插件能够 hook 到每一个编译(compilation)中发出的关键事件中。 在编译的每个阶段中,插件都拥有对 compiler 对象的完全访问能力, 并且在合适的时机,还可以访问当前的 compilation 对象

运行机制

插件的运行建立在:基于tapable实现的事件发布订阅以及webpack打包的整个生命周期中对事件的发布

  1. 在webpack.config.js中注册插件
  2. webpack生成compiler对象,调用注册的插件的apply方法,传入compiler对象,完成webapck事件订阅
  3. webpack在打包过程中,在对应的生命周期触发钩子,实现事件的广播

Tapable

  1. SyncHook

    • 同步hook
    • 事件
      • 注册:tap
      • 调用:call
  2. AyncSeriesHook

    • 异步串行hook,不同插件注册在同一hook上的事件串行执行
    • 事件
      • 注册:tap、tapAsync、tapPromise
      • 调用:call、callAsync、promise
  3. AysncParalleHook

    • 异步并行hook,不同插件注册在同一hook上的事件并行执行
    • 事件的注册与调用同 AsyncSeriesHook
  4. 例如下面自定义插件的代码,便可以将自定义事件注册到 webpack打包的生命周期中,当webpack打包过程中,例如在生成资源到output目录前,便会执行 compiler.hooks.emit.callAsync(compilation, err => {}),从而触发插件注册在 emit事件上的回调函数的执行

// HelloPlugin.js
const PluginName = 'HelloPlugin'
module.exports = class HelloPlugin {
  constructor(options) {
    this.options = options;
  }
  apply(compiler) {

    compiler.hooks.entryOption.tap(PluginName, (context, entry) => {
      // console.log('hello-entryOptions: ', context, entry);
    })

    // AsyncSeriesHook 生成资源到 output 目录之前。
    compiler.hooks.emit.tapAsync('ddd', (compilation, cb) => {
      console.log('hello-emit: ', compilation.chunks);
      cb();
    })

    // AsyncSeriesHook 生成资源到 output 目录之后。
    compiler.hooks.afterEmit.tapPromise(PluginName, (compilation) => {
      return new Promise((resolve, reject) => {
        console.log('hello-afterEmit-do');
        resolve()
      })
    })
  }
}

Webpack构建流程

webpack的构建流程中几个重要的阶段

  1. 校验配置文件,初始化本次构建的配置参数
  2. 生成Compiler对象,注册plugin,调用plugin实例的apply,为插件实例传入compiler对象
  3. 进入entryOption阶段:webpack开始读取配置的Entries,递归遍历所有的入口文件
  4. run/watch: 如果运行在watch模式下执行watch方法,否则执行run方法
  5. compilation:创建Compilation对象回调 compilation相关钩子,依次进入每一个入口文件(entry),使用loader对文件进行编译。通过compilation可以读取module的resource(资源路径)、loaders等信息,再将编译好的文件内容使用acorn解析生成AST静态语法树,然后递归重复的执行这个工程,所有模块和依赖分析完成后,执行compilation的seal方法对每个chunk进行整理、优化、封装 _webpack_require_来模拟模块化操作
  6. emit:所有文件的编译及转化都已经完成,包含了最终输出的资源,我们可以在传入事件回调的compilation.assets上拿到所需数据,其中包括即将输出的资源、代码块Chunk等信息
  7. afterEmit:文件已经写入磁盘完成
  8. done:完成编译。

引用自:滴滴云博客,见:blog.didiyun.com/index.php/2…

相关流程代码

webpack.js

初始化配置参数,生成Compiler对象,执行run/watch方法

const webpack = (
    (options, callback) => {
        const create = () => {
            let compiler;
            let watch = false;
            let watchOptions;
            
            // 创建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);
            });
          });
        }
    }
);
// 创建compiler对象
const createCompiler = rawOptions => {
    // ... 初始化配置参数

    const compiler = new Compiler(options.context, options);
    // 调用自定义plugin的apply,将事件钩入生命周期中
    if (Array.isArray(options.plugins)) {
            for (const plugin of options.plugins) {
                    if (typeof plugin === "function") {
                            plugin.call(compiler, compiler);
                    } else {
                            plugin.apply(compiler);
                    }
            }
    }
    applyWebpackOptionsDefaults(options);
    compiler.hooks.environment.call();
    compiler.hooks.afterEnvironment.call();
    new WebpackOptionsApply().process(options, compiler);
    compiler.hooks.initialize.call();
    return compiler;
};

Compiler.js

创建compiler对象,执行 run 方法,创建compilation对象,comiler.hooks.make 触发 EntryPlugin addEntry,处理入口文件

class Compiler {
  constructor(context, options = /** @type {WebpackOptions} */ ({})) {
    // 生命周期钩子
    this.hooks = Object.freeze({
      initialize: new SyncHook([]),
      shouldEmit: new SyncBailHook(["compilation"]),
      done: new AsyncSeriesHook(["stats"]),
      afterDone: new SyncHook(["stats"]),
      beforeRun: new AsyncSeriesHook(["compiler"]),
      run: new AsyncSeriesHook(["compiler"]),
      emit: new AsyncSeriesHook(["compilation"]),
      assetEmitted: new AsyncSeriesHook(["file", "info"]),
      afterEmit: new AsyncSeriesHook(["compilation"]),
      entryOption: new SyncBailHook(["context", "entry"])
      ...
    });
  }
}

Complier 的 run 方法,触发 compiler

run(callback) {
    this.running = true;
    
    const conCompiled = (err, compilation) => {...}

    const run = () => {
      this.hooks.beforeRun.callAsync(this, err => {
        if (err) return finalCallback(err);

        this.hooks.run.callAsync(this, err => {
          if (err) return finalCallback(err);

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

            // 开始 compiler,参数为compiler完成后的调用触发 shouldEmit 、done等hooks
            this.compile(onCompiled);
          });
        });
      });
    };

    run();
  }
  
  const onCompiled = (err, compilation) => {
      if (err) return finalCallback(err);

      if (this.hooks.shouldEmit.call(compilation) === false) {
        compilation.startTime = startTime;
        compilation.endTime = Date.now();
        const stats = new Stats(compilation);
        this.hooks.done.callAsync(stats, err => {
          if (err) return finalCallback(err);
          return finalCallback(null, stats);
        });
        return;
      }

      process.nextTick(() => {
        // 输出文件到构建目录
        this.emitAssets(compilation, err => {
          if (err) return finalCallback(err);

          if (compilation.hooks.needAdditionalPass.call()) {
            compilation.needAdditionalPass = true;
       
            const stats = new Stats(compilation);
            this.hooks.done.callAsync(stats, err => {
              if (err) return finalCallback(err);

              this.hooks.additionalPass.callAsync(err => {
                if (err) return finalCallback(err);
                this.compile(onCompiled);
              });
            });
            return;
          }

          this.emitRecords(err => {
            if (err) return finalCallback(err);
            const stats = new Stats(compilation);
            this.hooks.done.callAsync(stats, err => {
              if (err) return finalCallback(err);
              this.cache.storeBuildDependencies(
                compilation.buildDependencies,
                err => {
                  if (err) return finalCallback(err);
                  return finalCallback(null, stats);
                }
              );
            });
          });
        });
      });
    };

compiler方法,

  1. 创建compilation对象,调用compiler.hooks.compilation,很多内置插件钩入了此阶段,如 EntryPlugin
  2. 调用 make,内置插件 EntryPlugin 调用 addEntry, 解析入口文件,开始进行编译构建
compile(callback) {
    const params = this.newCompilationParams();
    this.hooks.beforeCompile.callAsync(params, err => {
      if (err) return callback(err);
      this.hooks.compile.call(params);
    
      // 创建 compilation对象
      const compilation = this.newCompilation(params);
      
      // 触发 EntryPlugin 的 addEntry 
      this.hooks.make.callAsync(compilation, err => {

        this.hooks.finishMake.callAsync(compilation, err => {

          process.nextTick(() => {
            compilation.finish(err => {

              // 构建结果封装
              compilation.seal(err => {

                this.hooks.afterCompile.callAsync(compilation, err => {

                  return callback(null, compilation);
                });
              });
            });
          });
        });
      });
    });
  }
  
  // 创建 compilation
  newCompilation(params) {
    const compilation = this.createCompilation(params);
    compilation.name = this.name;
    compilation.records = this.records;
    this.hooks.thisCompilation.call(compilation, params);
    this.hooks.compilation.call(compilation, params);
    return compilation;
  }

关键hooks

compiler

hooks介绍类型参数
run执行compiler之前调用AsyncSeriesHookcompiler
compilationcompilation 创建之后执行SyncHookcompilation, compilationParams
emit输出 asset 到 output 目录之前执行AsyncSeriesHookcompilation
done在 compilation 完成时执行AsyncSeriesHookstats

Compilation

hooks介绍类型参数
buildModule在模块构建开始之前触发,可以用来修改模块。SyncHookmodule
sealcompilation 对象停止接收新的模块时触发SyncHookcompilation构建结果封装, 不可再更改,生成资源,这些资源保存在compilation.assets, compilation.chunk
optimize优化阶段开始时触发。SyncHookcompilation

参考文档: