原文发布在我的 GitHub 欢迎 star 收藏。

webpack模块解析流程
本文源码来源:webpack-4

编译之前

在开始编译流程之前,webpack 会处理用户配置,首先进行校验,通过后与默认配置合并或者是调用相应函数进行处理,输出最终的 options。之后实例化 Compiler,并传入 options,并为注册的 plugins 注入 compiler 实例。如果配置了 watch 选项,则添加监听,在资源变化的时候会重新进行编译流程;否则直接进入编译流程:

const webpack = (options, callback) => {
  // 校验
  validateSchema(webpackOptionsSchema, options);
  /* ... */
  // 处理、合并 options
  options = new WebpackOptionsDefaulter().process(options);
  // 实例化 Compiler
  const compiler = new Compiler(options.context);
  /* ... */
  // 注册插件
  if (Array.isArray(options.plugins)) {
    for (const plugin of options.plugins) {
      if (typeof plugin === "function") {
        plugin.call(compiler, compiler);
      } else {
        plugin.apply(compiler);
      }
    }
  }
  // 开始编译流程
  if (watch) {
    compiler.watch(watchOptions, callback);
  } else {
    compiler.run((err, stats) => {
      compiler.close(err2 => {
        callback(err || err2, stats);
      });
    });
  }
}

module 编译

compiler.run() 的过程中,会调用 compile 方法。在 compile 方法中会实例化 Compilation,模块的编译构建流程都由 Compilation 控制。实例化 Compilation 后紧接着触发 compilationmake 事件,从 make 开始主编译流程。这里有一个很细节的点,其实也与 webpack 的插件机制有关,webpack 的插件机制固然让其有很好的扩展性,但对于阅读源码来说会把人绕晕,众多的钩子,注册钩子函数的代码与触发的地方很难让人找到其联系之处,只能通过编辑器的全局搜索去找到注册的钩子函数。在处理 options 的时候,会调用到 EntryPlugin 插件,其内部会注册 compilationmake 事件:

apply(compiler) {
  compiler.hooks.compilation.tap(
    "EntryPlugin",
    (compilation, { normalModuleFactory }) => {
      // 设置 DependencyFactories,在添加 moduleChain 的时候会用上
      compilation.dependencyFactories.set(
        EntryDependency,
        normalModuleFactory
      );
    }
  );

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

    const dep = EntryPlugin.createDependency(entry, name);
    // 编译文件模块的入口
    compilation.addEntry(context, dep, name, err => {
      callback(err);
    });
  });
}

addEntry

make 事件触发后,会调用 compilation.addEntry 从入口文件开始,根据 dep 类型判断使用哪一种 moduleFactory,并且将入口添加到 _preparedEntrypoints 属性中,其结构如下:

const slot = {
  name: name, // entry 的 name
  request: null, // entry 的 request
  module: null // 对 entry 文件解析成的最终 module,在 afterBuild 中对其赋值
};
// ...
this._preparedEntrypoints.push(slot);

调用 _addModuleChian 将模块加入编译链条,会传入一个 dependency 对象,存有 entrynamerequest 等信息。request 中,存储的是文件的路径信息:

addEntry(context, entry, name, callback) {
  this.hooks.addEntry.call(entry, name);
  // ...
  this._addModuleChain(
    context,
    entry,
    module => {
      this.entries.push(module);
    },
    (err, module) => {
      // ...
    }
  );
}

createModule

_addModuleChain 的回调中会调用到 xxxModuleFactorycreate 方法。以 normalModuleFactorycreate 方法为例,先是触发 beforeResolve 事件,然后触发 normalModuleFactoryfactory 事件,并调用返回的 factory 方法:

create(data, callback) {
  // ...
  this.hooks.beforeResolve.callAsync({ ... },
    (err, result) => {
      // ...
      const factory = this.hooks.factory.call(null);
      // ...
      factory(result, (err, module) => {
        // ...
      });
    }
  );
}

factory 方法中,会触发 resolver 事件,并返回一个函数,通过调用此函数执行文件路径及相关 loader 路径的解析,完成后触发 afterResolve 事件,并在其回调中生成一个 module 实例,并将 resolve 的结果存入其中:

// factory 事件注册
this.hooks.factory.tap("NormalModuleFactory", () => (result, callback) => {
  let resolver = this.hooks.resolver.call(null);
  // ...
  resolver(result, (err, data) => {
    // ...
    this.hooks.afterResolve.callAsync(data, (err, result) => {
      // ...
      let createdModule = this.hooks.createModule.call(result);
      if (!createdModule) {
        if (!result.request) {
          return callback(new Error("Empty dependency (no request)"));
        }
        // normalModule 实例
        createdModule = new NormalModule(result);
      }
      createdModule = this.hooks.module.call(createdModule, result);
      return callback(null, createdModule);
    });
  });
});

// resolve 事件注册
this.hooks.resolver.tap("NormalModuleFactory", () => (data, callback) => {
  // ...
  // loader 和 normal 文件的 resolve 有所区别
  const loaderResolver = this.getResolver("loader");
  const normalResolver = this.getResolver("normal", data.resolveOptions);
  // resolve loader & normal path
});

buildModule

回到 normalModuleFactory.createcallback 中,会依次执行 addModule ---> addReason ---> buildModule ---> afterBuild

  • addModule: 将前面创建的 module 实例添加到全局 Compilation.modules 数组中和 _modules 对象中;
  • addReason: 为该 module 添加 reason,即是哪个 module 依赖了该 module
  • buildModule: 编译 module
  • afterBuild: 处理依赖,递归编译 module

调用 buildModule,最终会走到相应 module 实例的 build 方法中,前面我们的 moduleNormalModule 的实例,所以我们来看看 NormalModulebuild 方法:

// NormalModule 的 build 方法
build(options, compilation, resolver, fs, callback) {
  // ...
  return this.doBuild(options, compilation, resolver, fs, err => {
    // doBuild 回调
    // ...
    try {
      // 进行 AST 的转换
      const result = this.parser.parse(
        // 如果在 runLoaders 的时候已经解析成 AST 则使用 _ast,否则传入 JS 源代码
        this._ast || this._source.source(),
        { current: this, module: this, compilation: compilation, options: options },
        (err, result) => {
          if (err) handleParseError(err);
          else handleParseResult(result);
        }
      );
      if (result !== undefined) handleParseResult(result);
    } catch (e) {
      handleParseError(e);
    }
  });
}
// doBuild 方法
doBuild(options, compilation, resolver, fs, callback) {
  // ...
  runLoaders(
    {
      resource: this.resource,
      loaders: this.loaders,
      context: loaderContext,
      readResource: fs.readFile.bind(fs)
    },
    (err, result) => {
      // ...
      // createSource 将 loaders 处理的结果转换为字符串
      this._source = this.createSource(
        this.binary ? asBuffer(source) : asString(source),
        resourceBuffer,
        sourceMap
      );
      this._sourceSize = null;
      // 如果已经转换为 AST,则存在 _ast 中
      this._ast =
        typeof extraInfo === "object" &&
        extraInfo !== null &&
        extraInfo.webpackAST !== undefined
          ? extraInfo.webpackAST
          : null;
      return callback();
    }
  );
}

处理依赖

duBuild 方法中,会进行 loaders 的转换。在 this.parser.parse 方法中,会将转换后的源码进行 AST 转换,并在 import/export 等地方添加相应的 Dependency,也可以理解为某种占位符,在后续的解析中,会根据相应的模板等进行替换生成最终文件。buildModule 结束后,执行其回调,并调用 afterBuild 方法,在 afterBuild 方法中,调用了 processModuleDependencies 方法处理依赖:

processModuleDependencies(module, callback) {
  const dependencies = new Map();
  // 省略对 dependencies 做的一些处理
  const sortedDependencies = [];
  // 将依赖解析成下面的结构
  for (const pair1 of dependencies) {
    for (const pair2 of pair1[1]) {
      sortedDependencies.push({
        factory: pair1[0],
        dependencies: pair2[1]
      });
    }
  }
  // 添加模块依赖的解析
  this.addModuleDependencies(
    module,
    sortedDependencies,
    this.bail,
    null,
    true,
    callback
  );
}
// addModuleDependencies 遍历依赖,并重新进行 module 的编译
addModuleDependencies(module, dependencies, bail, cacheGroup, recursive, callback) {
  const start = this.profile && Date.now();
  const currentProfile = this.profile && {};

  asyncLib.forEach(
    dependencies,
    (item, callback) => {
      const dependencies = item.dependencies;
      // ...
      semaphore.acquire(() => {
        const factory = item.factory;
        // create
        factory.create(
          {
            // ...
          },
          (err, dependentModule) => {
            // ...
            const iterationDependencies = depend => {
              for (let index = 0; index < depend.length; index++) {
                // ...
                // addReason
                dependentModule.addReason(module, dep);
              }
            };
            // addModule
            const addModuleResult = this.addModule(
              dependentModule,
              cacheGroup
            );
            // ...
            if (addModuleResult.build) {
              // buildModule
              this.buildModule(
                dependentModule,
                isOptional(),
                module,
                dependencies,
                err => {
                  // ...
                  // afterBuild
                  afterBuild();
                }
              );
            }
            // ...
          }
        );
      });
    },
    err => {
      // ...
    }
  );
}

可以看到,在 addModuleDependencies 方法中,对前一个 module 的依赖进行遍历,又重新执行 factory.create ---> addModule ---> buildModule ---> afterBuild,进行编译。当所有的依赖都编译完,便完成了 module 的编译过程。回到 make 事件的回调中,此时所有需要编译的 module 都已经经过上面的步骤处理完毕,接下来会调用 compilation.seal 进行 chunk 图的生成工作:

this.hooks.make.callAsync(compilation, err => {
  if (err) return callback(err);
  compilation.finish(err => {
    if (err) return callback(err);
    // seal 阶段,组合 chunk,生成 chunkGroup
    compilation.seal(err => {
      if (err) return callback(err);
      // 在其回调中触发 afterCompile 事件,编译过程结束
      this.hooks.afterCompile.callAsync(compilation, err => {
        if (err) return callback(err);
        return callback(null, compilation);
      });
    });
  });
});

生成 chunkGroup

未完待续...


hanrenguang
251 声望1 粉丝

« 上一篇
babel7实践