Skip to content

流程开始

入口

webpack进行打包时,会根据命令找到打包的真实入口:node_modules/webpack/lib/webpack.js文件下的webpack函数,其主要代码如下:

javascript
const webpack = (options, callback) => {
  const create = () => {
    compiler = createCompiler(options);
    return { compiler, watch, watchOptions };
  };
  const { compiler, watch } = create();
  return compiler;
}
const webpack = (options, callback) => {
  const create = () => {
    compiler = createCompiler(options);
    return { compiler, watch, watchOptions };
  };
  const { compiler, watch } = create();
  return compiler;
}

optionswebpack配置文件和命令参数合并后的配置。create方法通过createCompiler创建一个Compiler对象,该对象用于管理整个打包过程的主流程:

javascript
const createCompiler = rawOptions => {
  // ...
  // 1. 创建 Compiler 对象
  const compiler = new Compiler(options.context);
  compiler.options = options;
  // 2. 用户在 webpack 配置中定义的插件进行应用
  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();
  // 3. 内置插件应用
  new WebpackOptionsApply().process(options, compiler);
  compiler.hooks.initialize.call();
  return compiler;
};
const createCompiler = rawOptions => {
  // ...
  // 1. 创建 Compiler 对象
  const compiler = new Compiler(options.context);
  compiler.options = options;
  // 2. 用户在 webpack 配置中定义的插件进行应用
  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();
  // 3. 内置插件应用
  new WebpackOptionsApply().process(options, compiler);
  compiler.hooks.initialize.call();
  return compiler;
};

在获取了Compiler对象后,开始应用用户自定义插件和webpack内置插件。并且此时内置的EntryOptionPlugin监听的entryOptions钩子会被立即调用:

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

EntryOptionPlugin插件会在后续处理entry时起到关键作用,因此需要留意。

run方法

在获取到compiler对象后,会调用其run方法,该方法准确的调用位置在webpack-cli包当中:

javascript
const cli = new WebpackCLI();
await cli.run(args);
const cli = new WebpackCLI();
await cli.run(args);

这里的cli就是返回的compiler对象。在webpack/lib/Compiler.js中找到run方法:

javascript
const run = () => {
  this.hooks.beforeRun.callAsync(this, err => {
    this.hooks.run.callAsync(this, err => {
      this.readRecords(err => {
        this.compile(onCompiled);
      });
    });
  });
};
const run = () => {
  this.hooks.beforeRun.callAsync(this, err => {
    this.hooks.run.callAsync(this, err => {
      this.readRecords(err => {
        this.compile(onCompiled);
      });
    });
  });
};

首先会执行beforeRun/run钩子,这些不是很重要,可以暂且忽略。现在着重看一下compile方法。

compile方法

compile方法精简后的代码如下:

javascript
compile(callback) {
  const params = this.newCompilationParams();
  this.hooks.beforeCompile.callAsync(params, err => {
    this.hooks.compile.call(params);
    const compilation = this.newCompilation(params);
    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);
              });
            });
          });
        });
      });
    });
  });
}
compile(callback) {
  const params = this.newCompilationParams();
  this.hooks.beforeCompile.callAsync(params, err => {
    this.hooks.compile.call(params);
    const compilation = this.newCompilation(params);
    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);
              });
            });
          });
        });
      });
    });
  });
}

这里我们大概可以知道编译的主要流程:

javascript
beforeCompile => compile => make => finishMake => finish => seal => afterCompile => 执行回调
beforeCompile => compile => make => finishMake => finish => seal => afterCompile => 执行回调

其中make阶段是编译阶段,处理依赖和模块关系以及模块的编译。而seal阶段主要是处理modulechunk的关系。这两个阶段都比较重要,后面会单独讲解。现在回到compile方法的第一行代码:

javascript
const params = this.newCompilationParams()

// 实例化两个参数
newCompilationParams() {
  const params = {
    normalModuleFactory: this.createNormalModuleFactory(),
    contextModuleFactory: this.createContextModuleFactory()
  };
  return params;
}
const params = this.newCompilationParams()

// 实例化两个参数
newCompilationParams() {
  const params = {
    normalModuleFactory: this.createNormalModuleFactory(),
    contextModuleFactory: this.createContextModuleFactory()
  };
  return params;
}

该方法返回了normalModuleFactorycontextModuleFactory两个工厂函数。其中normalModuleFactory处理普通模块的创建,如import A from './a.js'。而contextModuleFactory处理如import './a.js'模块的创建。

接下来是实例化一个compilation对象:

javascript
// 实例化 compilation
const compilation = this.newCompilation(params);

newCompilation(params) {
  const compilation = this.createCompilation();
  compilation.name = this.name;
  compilation.records = this.records;
  this.hooks.thisCompilation.call(compilation, params);
  this.hooks.compilation.call(compilation, params);
  return compilation;
}
// 实例化 compilation
const compilation = this.newCompilation(params);

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

此时会触发thisCompilationcompilation两个钩子,用于监听打包过程中处理compilation的事件回调。

onCompiled方法

待编译完成之后,会执行callback,即onCompiled方法,精简后的代码如下:

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

const finalCallback = (err, stats) => {
  this.hooks.afterDone.call(stats);
};
const onCompiled = (err, compilation) => {
  this.emitAssets(compilation, err => {
    this.emitRecords(err => {
      this.hooks.done.callAsync(stats, err => {
        this.cache.storeBuildDependencies(
          compilation.buildDependencies,
          err => {
            return finalCallback(null, stats);
          }
        );
      });
    });
  });
};

const finalCallback = (err, stats) => {
  this.hooks.afterDone.call(stats);
};

该方法的主要任务是通过emitAssets方法将打包后的chunk正确的输出成文件形式:

javascript
emitAssets(compilation, callback) {
  const emitFiles = err => {
    // ...
  };

  this.hooks.emit.callAsync(compilation, err => {
    outputPath = compilation.getPath(this.outputPath, {});
    mkdirp(this.outputFileSystem, outputPath, emitFiles);
  });
}
emitAssets(compilation, callback) {
  const emitFiles = err => {
    // ...
  };

  this.hooks.emit.callAsync(compilation, err => {
    outputPath = compilation.getPath(this.outputPath, {});
    mkdirp(this.outputFileSystem, outputPath, emitFiles);
  });
}

最终调用的主要钩子为:

javascript
emit => done => afterDone
emit => done => afterDone

总结

webpack源码中有几个核心的概念:CompilerCompilation和插件。

Compiler对象主要用于控制打包过程的整个流程,如:

  1. run => 开始启动编译过程
  2. make => 进行编译
  3. seal => 编译完成,形成chunk
  4. emit => 输出文件

Compilation对象代表在这个打包过程中的打包产物,用于记录打包过程中的内容,如dependencies/modules/chunks等等。

基于主打包流程和打包产物。插件则是监听打包过程中的某一个或多个过程,然后对compilation等内容进行加工处理,最终得到打包好的内容。