本地测试代码

日常工作中一般都是基于指令进行打包,如npx webpack。但在源码调试中,指令不方便打断点,而且webpack中的Compiler类中存在一个run方法,和指令打包结果输出一致,因此,在调试源码时使用run进行打包。具体代码如下(之后的源码分析都基于该代码):

  1. // 引入webpack模块
  2. let webpack = require('webpack')
  3. /*
  4. * 引入设置的webpack配置项
  5. {
  6. devtool: 'none',
  7. mode: 'development',
  8. context: process.cwd(),
  9. entry: './src/index.js',
  10. output: {
  11. filename: 'index.js',
  12. path: path.resolve('dist')
  13. }
  14. }
  15. */
  16. let options = require('./webpack.config')
  17. let compiler = webpack(options)
  18. // 执行run方法执行编译
  19. compiler.run((err, stats) => {
  20. console.log(err)
  21. console.log(stats.toJson({
  22. entries: true,
  23. chunks: false,
  24. modules: false,
  25. assets: false
  26. }))
  27. })

在开始之前,简单说一下Tapable库,其核心原理是设计模式中的发布订阅模式。

  1. const test = [];
  2. // 可以简单理解成通过 类型a.tap 往 test数组中添加函数
  3. a.tap => test.push(fn)
  4. // 类型a.call 通过循环test中每一项,执行对应的函数
  5. a.call => test.forEach(fn => fn())

从入口开始查找

一般查看源码时,都从package.json中的main入口查找。由此可以得出,打包从lib/webpack.js开始。

  1. {
  2. "name": "webpack",
  3. "version": "4.45.0",
  4. ...,
  5. "main": "lib/webpack.js",
  6. ...
  7. }

粗略阅读lib/webpack.js后,发现导出的是一个webpack对象。该对象主要执行了如下步骤:

  1. 根据传入的配置项,设置webpack默认配置项
  2. 创建compiler对象
  3. 给pulgin插件设置权限
  4. 挂载所有webpack内部插件
  5. 如果我们传入了回调函数,内部会偷偷调用run方法,就和之前写的测试代码中一致
  6. 返回compiler对象
    1. /*
    2. * options: 传入的webpack配置项
    3. * callback: 回调函数
    4. */
    5. const webpack = (options, callback) => {
    6. // 声明compiler
    7. // compiler的主要引擎,记录了完整的上下文信息。在整个生命周期中,只会生成一次
    8. let compiler;
    9. // 如果是配置项类型是数组类型
    10. // false
    11. if (Array.isArray(options)) {
    12. ...
    13. // // 如果是配置项类型是对象类型
    14. } else if (typeof options === "object") {
    15. // 设置默认配置项
    16. options = new WebpackOptionsDefaulter().process(options);
    17. // 设置Compiler类
    18. compiler = new Compiler(options.context);
    19. compiler.options = options;
    20. // 给pulgin插件设置权限
    21. new NodeEnvironmentPlugin({
    22. infrastructureLogging: options.infrastructureLogging
    23. }).apply(compiler);
    24. // 如果存在pulgin,判断是函数类型还是对象
    25. if (options.plugins && Array.isArray(options.plugins)) {
    26. for (const plugin of options.plugins) {
    27. // 如果是函数类型,则将this指向compiler,并传参compiler
    28. if (typeof plugin === "function") {
    29. plugin.call(compiler, compiler);
    30. } else {
    31. // 调用apply方法并传参compiler
    32. // 此处也是为什么,写插件的时候需要提供一个apply方法
    33. plugin.apply(compiler);
    34. }
    35. }
    36. }
    37. // 此处是引入的Tapable
    38. // 执行Tapable中的environment和afterEnvironment钩子函数
    39. compiler.hooks.environment.call();
    40. compiler.hooks.afterEnvironment.call();
    41. // 挂载所有webpack内部插件入口
    42. compiler.options = new WebpackOptionsApply().process(options, compiler);
    43. } else {
    44. throw new Error("Invalid argument: options");
    45. }
    46. if (callback) {
    47. // 不是函数类型报错
    48. if (typeof callback !== "function") {
    49. throw new Error("Invalid argument: callback");
    50. }
    51. // 热更新相关,暂时不看
    52. if (
    53. options.watch === true ||
    54. (Array.isArray(options) && options.some(o => o.watch))
    55. ) {
    56. ...
    57. }
    58. // 内部调用run方法
    59. compiler.run(callback);
    60. }
    61. return compiler;
    62. }
    63. // 导出webpack对象
    64. exports = module.exports = webpack;

01 => webpack默认项

通过实例化WebpackOptionsDefaulter类,设置默认项

  1. class OptionsDefaulter extends OptionsDefaulter {
  2. // 设置默认项
  3. constructor() {
  4. this.set("entry", "./src");
  5. this.set("context", process.cwd());
  6. this.set("target", "web");
  7. this.set("devtool", "make", options =>
  8. options.mode === "development" ? "eval" : false
  9. );
  10. this.set("output.filename", "[name].js");
  11. ...
  12. }
  13. }

02 => 创建compiler对象

通过实例化WebpackOptionsDefaulter类,设置默认项

  1. // Compiler 继承 Tapable 库
  2. class Compiler extends Tapable {
  3. constructor(context) {
  4. super();
  5. // 往this.hooks绑定对应的 Tapable 中的钩子函数
  6. // 后续代码中如果出现 compiler.hooks.xxx ,都是指向的 Tapable 中的钩子
  7. this.hooks = {
  8. done: new AsyncSeriesHook(["stats"]),
  9. beforeRun: new AsyncSeriesHook(["compiler"]),
  10. run: new AsyncSeriesHook(["compiler"]),
  11. emit: new AsyncSeriesHook(["compilation"]),
  12. compile: new SyncHook(["params"]),
  13. make: new AsyncParallelHook(["compilation"]),
  14. ...
  15. }
  16. // 绑定一些属性,方便后续使用
  17. this.name = undefined;
  18. this.outputPath = "";
  19. this.options = {};
  20. this.context = context;
  21. ...
  22. }
  23. }

03 => 给plugin插件设置权限

入口文件中,发现是通过实例化NodeEnvironmentPlugin类并调用内部apply方法给plugin增加功能,内部核心都是NodeJs的fs模块。具体源码如下:

  1. class NodeEnvironmentPlugin {
  2. // 接受传入的参数
  3. constructor(options) {
  4. this.options = options || {};
  5. }
  6. apply(compiler) {
  7. ...
  8. // 内部核心都是fs模块
  9. // 增加修改文件权限
  10. compiler.inputFileSystem = new CachedInputFileSystem(
  11. new NodeJsInputFileSystem(),
  12. 60000
  13. );
  14. // 增加导出文件权限
  15. compiler.outputFileSystem = new NodeOutputFileSystem();
  16. }
  17. }

NodeOutputFileSystem类为例,其源码如下:

  1. const fs = require("fs");
  2. const path = require("path");
  3. const mkdirp = require("mkdirp");
  4. class NodeOutputFileSystem {
  5. constructor() {
  6. this.mkdirp = mkdirp;
  7. this.mkdir = fs.mkdir.bind(fs);
  8. this.rmdir = fs.rmdir.bind(fs);
  9. this.unlink = fs.unlink.bind(fs);
  10. this.writeFile = fs.writeFile.bind(fs);
  11. this.join = path.join.bind(path);
  12. }
  13. }
  14. module.exports = NodeOutputFileSystem;

04 => 挂载内部所有插件

最终找到了EntryOptionPlugin类,这里是打包流程的入口

  1. class WebpackOptionsApply {
  2. process(options, compiler) {
  3. // 设置compiler的属性
  4. compiler.outputPath = options.output.path;
  5. compiler.recordsInputPath = options.recordsInputPath || options.recordsPath;
  6. compiler.recordsOutputPath =
  7. options.recordsOutputPath || options.recordsPath;
  8. compiler.name = options.name;
  9. compiler.dependencies = options.dependencies;
  10. ...
  11. // 这里是一个打包流程的入口
  12. // 实例化EntryOptionPlugin类并调用apply方法
  13. new EntryOptionPlugin().apply(compiler);
  14. // 调用Tapable的entryOption钩子
  15. compiler.hooks.entryOption.call(options.context, options.entry);
  16. ...
  17. }
  18. }