01 => 调用NormalModule.build方法,开始编译

  • 设置一些属性以便后续使用,如_ast 、 _source 等
  • 返回doBuild方法
    1. class NormalModule extends Module {
    2. ...
    3. // option => this.option
    4. // compilation => Compilation对象
    5. // resolver =>
    6. // fs => 修改文件权限
    7. // callback => 回调函数
    8. build(options, compilation, resolver, fs, callback) {
    9. // 设置属性
    10. this.built = true;
    11. this._source = null; // 某个模块对应的源代码
    12. this._sourceSize = null;
    13. this._ast = null; // 源代码对应的ast树
    14. this._buildHash = "";
    15. this.error = null; // error信息
    16. this.errors.length = 0;
    17. this.warnings.length = 0;
    18. this.buildMeta = {};
    19. this.buildInfo = { // 打包后信息
    20. cacheable: false,
    21. fileDependencies: new Set(),
    22. contextDependencies: new Set(),
    23. assets: undefined,
    24. assetsInfo: undefined
    25. };
    26. // 返回doBuild方法并传入对应参数
    27. // 具体代码在03中,优先看doBuild执行了什么
    28. return this.doBuild(options, compilation, resolver, fs, err => {
    29. ...
    30. })
    31. }
    32. }

    02 => 处理loader,返回文件对应的源代码

    最终触发callback回调,回到doBuild的回调中
    1. doBuild(options, compilation, resolver, fs, callback) {
    2. // 根据传入的loader信息,创建loaderContext
    3. const loaderContext = this.createLoaderContext(
    4. resolver,
    5. options,
    6. compilation,
    7. fs
    8. );
    9. // 调用runLoaders函数
    10. // resource => 读取的绝对路径
    11. // loaders => 传入的loader,为数组
    12. // context => loaderContext上下文对象
    13. // readResource => 读文件函数
    14. runLoaders(
    15. {
    16. resource: this.resource,
    17. loaders: this.loaders,
    18. context: loaderContext,
    19. readResource: fs.readFile.bind(fs)
    20. },
    21. // result 中有一个result属性,为Buffer流
    22. (err, result) => {
    23. // 设置缓存,并设置文件依赖,传入的入口有几个需要build的文件
    24. if (result) {
    25. this.buildInfo.cacheable = result.cacheable;
    26. this.buildInfo.fileDependencies = new Set(result.fileDependencies);
    27. this.buildInfo.contextDependencies = new Set(
    28. result.contextDependencies
    29. );
    30. }
    31. // 设置一些属性,与流程无关
    32. ...
    33. /*
    34. * 返回一个对象
    35. * {
    36. * _name: 文件对应的绝对路径
    37. * _value: 文件的源代码
    38. * }
    39. */
    40. this._source = this.createSource(
    41. // asBuffer => 将Buffer转化成utf-8形式,此时返回的就是我们的源代码
    42. // asBuffer => return Buffer.from(source, "utf-8")
    43. this.binary ? asBuffer(source) : asString(source),
    44. resourceBuffer,
    45. sourceMap
    46. );
    47. // 调用回调函数
    48. return callback();
    49. })
    50. )
    51. }

03 => doBuild内部代码

执行回调后,走到05的位置



this.doBuild(options, compilation, resolver, fs, err => {
  // 清空缓存中对应的
    this._cachedSources.clear();
  ...
  // handleParseResult 函数
  // 执行callback函数,走到了05中
  const handleParseResult = result => {
    this._lastSuccessfulBuildMeta = this.buildMeta;
    this._initBuildHash(compilation);
    return callback();
  };
  // 返回处理后的结果
  // this.parser => 之前绑定的Parser类
  const result = this.parser.parse(
    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) {
    // parse is sync
    handleParseResult(result);
  }
})

04 => 处理AST

class Parser extends Tapable {
    // source => 文件源代码
  // initialState => 03中传入的属性
  parse(source, initialState) {
    let ast;
    let comments;
    // false
    if (typeof source === "object" && source !== null) {
      ast = source;
      comments = source.comments;
      // true
    } else {
      comments = [];
      // 
      ast = Parser.parse(source, {
        sourceType: this.sourceType,
        onComment: comments
      });
    }
    const state = (this.state = initialState || {});
    // 与流程无关,暂不关心
    ...
    return state
  }
  // 返回语法树
  static parse(code, options) {
    // "auto"
      const type = options ? options.sourceType : "module";
    // 设置parserOptions
    const parserOptions = Object.assign(
            Object.create(null),
            defaultParserOptions,
            options
        );
    // type === "auto",true
        if (type === "auto") {
            parserOptions.sourceType = "module";
        } else if (parserOptions.sourceType === "script") {
            parserOptions.allowReturnOutsideFunction = true;
        }
    let ast;
    // 引入 acorn 包,解析出对应的ast树
    // const acorn = require("acorn");
    // const acornParser = acorn.Parser;
    ast = acornParser.parse(code, parserOptions);
    // 错误处理,暂不考虑
    ...
    return ast;
  }
}

05 => module.build回调

模块构建成功后,调用回调函数,进入到buildModule的回调中

module.build(
  this.options,
  this,
  this.resolverFactory.get("normal", module.resolveOptions),
  this.inputFileSystem,
  error => {
      // error相关处理,与流程无关,暂不关心
    ...
    // 调用succeedModule钩子,说明模块打包已经成功
    this.hooks.succeedModule.call(module);
    // 调用回调函数
    return callback();
  }
)

06 => this.buildModule回调

通过回调函数,判断当前模块是否有依赖模块,如果需要的话递归加载

const afterBuild = () => {
  // 判断当前是否存在依赖项,如果存在需要递归加载
  if (addModuleResult.dependencies) {
    this.processModuleDependencies(module, err => {
      if (err) return callback(err);
      callback(null, module);
    });
  } else {
    return callback(null, module);
  }
};

this.buildModule(module, false, null, null, err => {
  // 错误处理,暂不关心
  ...
  afterBuild();
});

07 => 递归加载对应的子模块

  • 重新执行factory.create()操作,和 webpack源码02 中的 05调用compiler.hooks.make,开始编译 大体逻辑是一致的,此处就不详细描述了
  • 执行callback
  • 最终会返回到compilation.seal函数上 ```javascript // this.processModuleDependencies processModuleDependencies(module, callback) { … // 进入addModuleDependencies方法 this.addModuleDependencies(
          module,
          sortedDependencies,
          this.bail,
          null,
          true,
          callback
    
    ); }

// this.addModuleDependencies addModuleDependencies( module, dependencies, bail, cacheGroup, recursive, callback ) { // asyncLib => neo-async库 // const asyncLib = require(“neo-async”); asyncLib.forEach( dependencies, (item, callback) => { const dependencies = item.dependencies; … const factory = item.factory; // 重新创建模块,进行递归处理 factory.create( … ) }, err => { if (err) { … } // 执行callback回调函数 return process.nextTick(callback); } ) }


<a name="yzwTk"></a>
### 08 => 处理Chunk

- 内部会执行许多的 Tapable钩子
- 处理Chunk,核心是创建模块并加载已有模块,同时保存模块信息
- 会进入创建优化后的文件
```javascript
seal(callback) {
  // 执行对应钩子
    this.hooks.seal.call();
  this.hooks.afterOptimizeDependencies.call(this.modules);
    this.hooks.beforeChunks.call();
  ...
  // 因为我们是单入口,所以this._preparedEntrypoints只会存在一项
  for (const preparedEntrypoint of this._preparedEntrypoints) {
      const module = preparedEntrypoint.module;
    const name = preparedEntrypoint.name;
    // 返回的是new Chunk(name)
    const chunk = this.addChunk(name);
    const entrypoint = new Entrypoint(name);
    ...
    // 设置一些属性
    this.namedChunkGroups.set(name, entrypoint);
    this.entrypoints.set(name, entrypoint);
    this.chunkGroups.push(entrypoint);
    GraphHelpers.connectChunkGroupAndChunk(entrypoint, chunk);
    GraphHelpers.connectChunkAndModule(chunk, module);
    chunk.entryModule = module;
    chunk.name = name;
    this.assignDepth(module);
  }
  // 构建Chunk
  // 核心:创建模块加载已有模块的内容,同时记录模块信息 
  buildChunkGraph(
    this,
    /** @type {Entrypoint[]} */ (this.chunkGroups.slice())
  );
  // 调用Tapable钩子
  this.hooks.afterChunks.call(this.chunks);
  this.hooks.optimize.call();
  this.hooks.afterOptimizeModules.call(this.modules);
  this.hooks.afterOptimizeChunks.call(this.chunks, this.chunkGroups);
  // 到此处,整个流程基本都完成,就剩构建文件了
  // 会调用许多的Tapable钩子
  this.hooks.optimizeTree.callAsync(this.chunks, this.modules, err => {
      this.hooks.afterOptimizeTree.call(this.chunks, this.modules);
    this.hooks.afterOptimizeChunkModules.call(this.chunks, this.modules);
        this.hooks.reviveModules.call(this.modules, this.records);
    this.hooks.optimizeModuleOrder.call(this.modules);
    this.hooks.advancedOptimizeModuleOrder.call(this.modules);
    this.hooks.beforeModuleIds.call(this.modules);
    this.hooks.moduleIds.call(this.modules);
    // 会调用许多Tapable钩子
    ...
    if (this.hooks.shouldGenerateChunkAssets.call() !== false) {
      this.hooks.beforeChunkAssets.call();
        // 创建优化后的文件
      this.createChunkAssets();
    }
    ...
  })
}

09 => 处理资源,将其转化成webpack输出后的代码

class Compilation extends Tapable {
  createChunkAssets() {
    // 获取传入的output路径
    const outputOptions = this.outputOptions;
    // 缓存source
    const cachedSourceMap = new Map();
    // 因目前都是同步,所以chunks只有一项
    for (let i = 0; i < this.chunks.length; i++) {
      const chunk = this.chunks[i];
      chunk.files = [];
      let source; // 打包后的资源
      let file;
      let filenameTemplate;
      // this.mainTemplate => 实例化后的MainTemplate类
      const template = chunk.hasRuntime()
        ? this.mainTemplate
        : this.chunkTemplate;
      // 最终返回[{ render(), filenameTemplate, pathOptions, identifier, hash }]
      const manifest = template.getRenderManifest({
        chunk,
        hash: this.hash,
        fullHash: this.fullHash,
        outputOptions,
        moduleTemplates: this.moduleTemplates,
        dependencyTemplates: this.dependencyTemplates
      });
      // 此时无缓存
      // false
      if (
        this.cache &&
        this.cache[cacheName] &&
        this.cache[cacheName].hash === usedHash
      ) {
        source = this.cache[cacheName].source;
      } else {
        // 设置source
        // 会调用MainTemplate类中的renderBootstrap方法
        source = fileManifest.render();
        ...
      }
      ...
      // 将source保存到内存中暂存
      this.emitAsset(file, source, assetInfo);
      // 添加文件对应的缓存
      chunk.files.push(file);
      this.hooks.chunkAsset.call(chunk, file);
      alreadyWrittenFiles.set(file, {
        hash: usedHash,
        source,
        chunk
      });
    }
    emitAsset(file, source, assetInfo = {}) {
      ...
      this.assets[file] = source;
      this.assetsInfo.set(file, assetInfo);
    }
  }
}

// 转换源代码为webpack输出的代码
class MainTemplate extends Tapable {
  // 用字符串拼出来对应的source
  // 返回优化后的source
    renderBootstrap(hash, chunk, moduleTemplate, dependencyTemplates) {
      const buf = [];
        buf.push(
            this.hooks.bootstrap.call(
                "",
                chunk,
                hash,
                moduleTemplate,
                dependencyTemplates
            )
        );
    buf.push(this.hooks.localVars.call("", chunk, hash));
        buf.push("");
        buf.push("// The require function");
        buf.push(`function ${this.requireFn}(moduleId) {`);
        buf.push(Template.indent(this.hooks.require.call("", chunk, hash)));
        buf.push("}");
        buf.push("");
        buf.push(
            Template.asString(this.hooks.requireExtensions.call("", chunk, hash))
        );
    buf.push("");
        buf.push(Template.asString(this.hooks.beforeStartup.call("", chunk, hash)));
        const afterStartupCode = Template.asString(
            this.hooks.afterStartup.call("", chunk, hash)
        );
    ...
    return buf;
  }
}

10 => 根据优化后的代码,产出对应的文件

  • seal 函数执行完毕后,会进行到对应的回调函数,最终会返回到一开始的 run函数(兜兜转转又回到了起点)
  • 通过this.outputFileSystem.writeFile创建文件
  • 至此,webpack流程告一段落了~
    class Compiler extends Tapable {
    run(callback) {
      ...
      // onCompiled就是编译完成后的回调函数
      const onCompiled = (err, compilation) => {
        // 异常直接返回
        if (err) return finalCallback(err);
        // 通过 this.emitAssets ,产出dist目录
        this.emitAssets(compilation, err => {
          // 内部做的是一些记录的事情,与流程无关,暂不关心
          ...
        })
      }
      ...
    }
    // 产出文件
    emitAssets(compilation, callback) {
        let outputPath;
      // 目录创建成功后的回调
      const emitFiles = err => {
          if (err) return callback(err);
        ...
        // 通过路径,创建对应的文件
        this.outputFileSystem.writeFile(targetPath, content, err => {
            ...
        })
      }
      // 调用Tapable的emit钩子
      this.hooks.emit.callAsync(compilation, err => {
              if (err) return callback(err);
        // 获取输出的目录,为绝对路径
              outputPath = compilation.getPath(this.outputPath);
        // 在一开始的时候绑定的,赋予创建文件的功能
              this.outputFileSystem.mkdirp(outputPath, emitFiles);
          });
    }
    }