Skip to content

make 流程

make 流程是 webpack 的一个核心流程,其主要分为下面几个步骤

  1. 入口

根据前面加载 SingleEntryPlugin,MultiEntryPlugin两个插件的 apply 时,注册了一个 compiler.hooks.make 的钩子函数

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

  const dep = SingleEntryPlugin.createDependency(entry, name)
  compilation.addEntry(context, dep, name, callback)
})

然后我们看compilation.addEntry()

js
/**
	 *
	 * @param {string} context context path for entry
	 * @param {Dependency} entry entry dependency being created
	 * @param {string} name name of entry
	 * @param {ModuleCallback} callback callback function
	 * @returns {void} returns
	 */
	addEntry(context, entry, name, callback) {
		const slot = {
			name: name,
			// TODO webpack 5 remove `request`
			// 入口文件的路径 地址
			request: null,
			module: null
		};

		// 保存路径地址
		if (entry instanceof ModuleDependency) {
			slot.request = entry.request;
		}

		// TODO webpack 5: merge modules instead when multiple entry modules are supported
		// 在 _preparedEntrypoints 上保存当前入口地址对象
		const idx = this._preparedEntrypoints.findIndex(slot => slot.name === name);
		if (idx >= 0) {
			// Overwrite existing entrypoint
			// 如果找到 当前入口地址 就覆盖
			this._preparedEntrypoints[idx] = slot;
		} else {
			this._preparedEntrypoints.push(slot);
		}
		// 真正的去处理module
		this._addModuleChain(
			context,
			entry,
			module => {
				this.entries.push(module);
			},
			(err, module) => {
				if (err) {
					return callback(err);
				}

				if (module) {
					slot.module = module;
				} else {
					const idx = this._preparedEntrypoints.indexOf(slot);
					if (idx >= 0) {
						this._preparedEntrypoints.splice(idx, 1);
					}
				}
				return callback(null, module);
			}
		);
	}

其主要还是调用 this._addModuleChain() 去处理入口模块

js
	/**
	 *
	 * @param {string} context context string path   项目路径  d:\guzhangh\webpack\webpack\hash
	 * @param {Dependency} dependency dependency used to create Module chain
	 * @param {OnModuleCallback} onModule function invoked on modules creation
	 * @param {ModuleChainCallback} callback callback for when module chain is complete
	 * @returns {void} will throw if dependency instance is not a valid Dependency
	 */
	_addModuleChain(context, dependency, onModule, callback) {
		const start = this.profile && Date.now();
		const currentProfile = this.profile && {};

		const errorAndCallback = this.bail
			? err => {
					callback(err);
			  }
			: err => {
					err.dependencies = [dependency];
					this.errors.push(err);
					callback();
			  };

		if (
			typeof dependency !== "object" ||
			dependency === null ||
			!dependency.constructor
		) {
			throw new Error("Parameter 'dependency' must be a Dependency");
		}
		const Dep = /** @type {DepConstructor} */ (dependency.constructor);
		// 获取当前module类型的工厂方法
		const moduleFactory = this.dependencyFactories.get(Dep);
		if (!moduleFactory) {
			throw new Error(
				`No dependency factory available for this dependency type: ${
					dependency.constructor.name
				}`
			);
		}

		/**
		 *  通过信号机的方式 维持当前处理 module的个数不能超过当前设置的运行最大并行的数目 `new Semaphore(options.parallelism || 100);`
		 *
		 * @type {Object}
		 */
		this.semaphore.acquire(() => {
			// 调用module工厂函数的创建方法
			moduleFactory.create(
				{
					contextInfo: {
						issuer: "",
						compiler: this.compiler.name
					},
					context: context,
					dependencies: [dependency]
				},
				(err, module) => {
					if (err) {
						this.semaphore.release();
						return errorAndCallback(new EntryModuleNotFoundError(err));
					}

					let afterFactory;

					if (currentProfile) {
						afterFactory = Date.now();
						currentProfile.factory = afterFactory - start;
					}
					// 添加当前 Model 主要是缓存
					const addModuleResult = this.addModule(module);
					module = addModuleResult.module;
					// 执行每一个NormalModule 的回调  如入口Module缓存到 this.entry
					onModule(module);

					dependency.module = module;
					module.addReason(null, dependency);

					const afterBuild = () => {
						if (currentProfile) {
							const afterBuilding = Date.now();
							currentProfile.building = afterBuilding - afterFactory;
						}

						if (addModuleResult.dependencies) {
							this.processModuleDependencies(module, err => {
								if (err) return callback(err);
								callback(null, module);
							});
						} else {
							return callback(null, module);
						}
					};

					if (addModuleResult.issuer) {
						if (currentProfile) {
							module.profile = currentProfile;
						}
					}

					if (addModuleResult.build) {
						// 真正的开始 编译module
						this.buildModule(module, false, null, null, err => {
							if (err) {
								this.semaphore.release();
								return errorAndCallback(err);
							}

							if (currentProfile) {
								const afterBuilding = Date.now();
								currentProfile.building = afterBuilding - afterFactory;
							}

							this.semaphore.release();
							afterBuild();
						});
					} else {
						this.semaphore.release();
						this.waitForBuildingFinished(module, afterBuild);
					}
				}
			);
		});
	}

在 _addModuleChain 中主要是做的什么那?

上面我们在 SingleEntryPluginconst dep = SingleEntryPlugin.createDependency(entry, name); 创建了一个 SingleEntryDependency 的实例对象,那么在 const Dep = /** @type {DepConstructor} */ (dependency.constructor);指向的就是我们的 SingleEntryDependency构造函数对象,然后通过 const moduleFactory = this.dependencyFactories.get(Dep); 获取当前SingleEntryDependency对应的 module 工厂实例对象。

我们看一个 this.dependencyFactories this.dependencyFactories 中保存了所有类型的 module 对于的工厂函数实例对象

当我们获取到这个 module 对应的工厂函数以后通过 this.semaphore.acquire(() => {}) 一个信号机的方式去处理这个模块,有关于 Semaphore可以看这一篇 Semaphore,这个主要的作用就是 当我们处理模块的时候可能会同时有非常大的 module 一起执行,机子带不动的,那么这时候就可以通过 Semaphore 去控制同时执行的 module 的个数,当超过允许个数的时候只有通过this.semaphore.release();释放一个机会,才会从队列中执行下一个 module。

然后开始通过 moduleFactory.create()去处理 module,module 的类型不同其工厂函数也不同,我们这边以 NormalModuleFactory为例子。

下面我们看 NormalModuleFactory.create()

js
class NormalModuleFactory extends Tapable {
  constructor(context, resolverFactory, options) {
    super()
    this.hooks = {
      resolver: new SyncWaterfallHook(["resolver"]),
      factory: new SyncWaterfallHook(["factory"]),
      parser: new HookMap(() => new SyncHook(["parser", "parserOptions"])),
    }
    this.hooks.factory.tap("NormalModuleFactory", () => (result, callback) => {
      // 执行当前中 定义的 resolver 函数 其实跟 factory 一样 都是通过钩子函数去放回一个 回调函数
      let resolver = this.hooks.resolver.call(null)

      // Ignored
      if (!resolver) return callback()
      // 执行获取的 resolver 的回调函数 就是下面的 	`this.hooks.resolver.tap("NormalModuleFactory", () => (data, callback) => {}) `
      resolver(result, (err, data) => {
        if (err) return callback(err)

        // Ignored
        if (!data) return callback()

        // direct module
        if (typeof data.source === "function") return callback(null, data)

        this.hooks.afterResolve.callAsync(data, (err, result) => {
          if (err) return callback(err)

          // Ignored
          if (!result) return callback()

          let createdModule = this.hooks.createModule.call(result)
          if (!createdModule) {
            if (!result.request) {
              return callback(new Error("Empty dependency (no request)"))
            }
            createdModule = new NormalModule(result)
          }
          // 执行当前 normalModuleFactory 中创建 module 的钩子函数,传入的是 createdModule(当前默认的创建 NormalModule 实例对象)
          //
          createdModule = this.hooks.module.call(createdModule, result)

          return callback(null, createdModule)
        })
      })
    })
    this.hooks.resolver.tap("NormalModuleFactory", () => (data, callback) => {
      const contextInfo = data.contextInfo
      // 工程路径
      const context = data.context
      // module的路径
      const request = data.request
      // 获取当前Module对于的 loader
      const loaderResolver = this.getResolver("loader")
      // 获取通过的resolver
      const normalResolver = this.getResolver("normal", data.resolveOptions)

      let matchResource = undefined
      let requestWithoutMatchResource = request
      const matchResourceMatch = MATCH_RESOURCE_REGEX.exec(request)
      if (matchResourceMatch) {
        matchResource = matchResourceMatch[1]
        if (/^\.\.?\//.test(matchResource)) {
          matchResource = path.join(context, matchResource)
        }
        requestWithoutMatchResource = request.substr(matchResourceMatch[0].length)
      }

      const noPreAutoLoaders = requestWithoutMatchResource.startsWith("-!")
      const noAutoLoaders = noPreAutoLoaders || requestWithoutMatchResource.startsWith("!")
      const noPrePostAutoLoaders = requestWithoutMatchResource.startsWith("!!")
      let elements = requestWithoutMatchResource.replace(/^-?!+/, "").replace(/!!+/g, "!").split("!")
      let resource = elements.pop()
      elements = elements.map(identToLoaderRequest)

      asyncLib.parallel(
        [
          callback => this.resolveRequestArray(contextInfo, context, elements, loaderResolver, callback),
          callback => {
            if (resource === "" || resource[0] === "?") {
              return callback(null, {
                resource,
              })
            }

            normalResolver.resolve(contextInfo, context, resource, {}, (err, resource, resourceResolveData) => {
              if (err) return callback(err)
              callback(null, {
                resourceResolveData,
                resource,
              })
            })
          },
        ],
        (err, results) => {
          if (err) return callback(err)
          let loaders = results[0]
          const resourceResolveData = results[1].resourceResolveData
          resource = results[1].resource

          // translate option idents
          try {
            for (const item of loaders) {
              if (typeof item.options === "string" && item.options[0] === "?") {
                const ident = item.options.substr(1)
                item.options = this.ruleSet.findOptionsByIdent(ident)
                item.ident = ident
              }
            }
          } catch (e) {
            return callback(e)
          }

          if (resource === false) {
            // ignored
            return callback(
              null,
              new RawModule("/* (ignored) */", `ignored ${context} ${request}`, `${request} (ignored)`)
            )
          }

          const userRequest =
            (matchResource !== undefined ? `${matchResource}!=!` : "") +
            loaders.map(loaderToIdent).concat([resource]).join("!")

          let resourcePath = matchResource !== undefined ? matchResource : resource
          let resourceQuery = ""
          const queryIndex = resourcePath.indexOf("?")
          if (queryIndex >= 0) {
            resourceQuery = resourcePath.substr(queryIndex)
            resourcePath = resourcePath.substr(0, queryIndex)
          }

          // 获取匹配的 rule
          const result = this.ruleSet.exec({
            resource: resourcePath,
            realResource: matchResource !== undefined ? resource.replace(/\?.*/, "") : resourcePath,
            resourceQuery,
            issuer: contextInfo.issuer,
            compiler: contextInfo.compiler,
          })
          const settings = {}
          const useLoadersPost = []
          const useLoaders = []
          const useLoadersPre = []
          for (const r of result) {
            if (r.type === "use") {
              if (r.enforce === "post" && !noPrePostAutoLoaders) {
                useLoadersPost.push(r.value)
              } else if (r.enforce === "pre" && !noPreAutoLoaders && !noPrePostAutoLoaders) {
                useLoadersPre.push(r.value)
              } else if (!r.enforce && !noAutoLoaders && !noPrePostAutoLoaders) {
                useLoaders.push(r.value)
              }
            } else if (
              typeof r.value === "object" &&
              r.value !== null &&
              typeof settings[r.type] === "object" &&
              settings[r.type] !== null
            ) {
              settings[r.type] = cachedMerge(settings[r.type], r.value)
            } else {
              settings[r.type] = r.value
            }
          }
          asyncLib.parallel(
            [
              this.resolveRequestArray.bind(this, contextInfo, this.context, useLoadersPost, loaderResolver),
              this.resolveRequestArray.bind(this, contextInfo, this.context, useLoaders, loaderResolver),
              this.resolveRequestArray.bind(this, contextInfo, this.context, useLoadersPre, loaderResolver),
            ],
            (err, results) => {
              if (err) return callback(err)
              loaders = results[0].concat(loaders, results[1], results[2])
              process.nextTick(() => {
                const type = settings.type
                const resolveOptions = settings.resolve
                callback(null, {
                  context: context,
                  request: loaders.map(loaderToIdent).concat([resource]).join("!"),
                  dependencies: data.dependencies,
                  userRequest,
                  rawRequest: request,
                  loaders,
                  resource,
                  matchResource,
                  resourceResolveData,
                  settings,
                  type,
                  parser: this.getParser(type, settings.parser),
                  generator: this.getGenerator(type, settings.generator),
                  resolveOptions,
                })
              })
            }
          )
        }
      )
    })
  }
  /**
	 * 创建一个当前NormalModuleFactory工厂函数的module
	 * @param  {[type]}   data     [description]
	 * 										- context
	 * 										- contextInfo: {
													 issuer: "",
													 compiler: this.compiler.name
												 },
	 										 	- dependencies: [dependency]
	 * @param  {Function} callback [description]
	 * @return {[type]}            [description]
	 */
  create(data, callback) {
    // 获取当前module的依赖
    const dependencies = data.dependencies
    //
    const cacheEntry = dependencyCache.get(dependencies[0])
    if (cacheEntry) return callback(null, cacheEntry)
    // 工程路径
    const context = data.context || this.context
    const resolveOptions = data.resolveOptions || EMPTY_RESOLVE_OPTIONS
    const request = dependencies[0].request
    const contextInfo = data.contextInfo || {}
    this.hooks.beforeResolve.callAsync(
      {
        contextInfo,
        resolveOptions,
        context,
        request,
        dependencies,
      },
      (err, result) => {
        if (err) return callback(err)

        // Ignored
        if (!result) return callback()

        // 获取当前 NormalModuleFactory 上定义的 factory 的钩子函数  详情在 NormalModuleFactory constructor中定义的一个 回调函数
        const factory = this.hooks.factory.call(null)

        // Ignored
        if (!factory) return callback()

        // 执行当前 NormalModuleFactory 上定义的 factory 的钩子函数
        factory(result, (err, module) => {
          if (err) return callback(err)

          if (module && this.cachePredicate(module)) {
            for (const d of dependencies) {
              dependencyCache.set(d, module)
            }
          }

          callback(null, module)
        })
      }
    )
  }
}

从 NormalModuleFactory.build() 开始其主要执行下面的流程

其起初是一个以下的对象数据

js
let module = {
	contextInfo :{
    compiler:undefined
    issuer:""
  },
  resolveOptions : {},
  // 工程路径
  context : 'd:\guzhangh\webpack\webpack\hash',
  // module的路径
  request : "d:\guzhangh\webpack\webpack\hash\public\js\main.js",
  // dependecies
	dependencies:[SingleEntryDependency]
}

每一个类型的 ModuleFactory 都通过 build 执行以下的流程

  1. resolve

  2. loader

  3. rules

  4. 获取对于的 module

1. resolve

  • 通过 NormalModuleFactory.hooks.beforeResolve​ 执行 beforeResolve 的钩子函数

  • NormalModuleFactory.hooks.factory.call(null); 获取对于的 factory 实例对象

factory()

  • this.hooks.resolver.call(null); 获取当前工厂函数对应的 resolve 解析的钩子函数

其通过 constructor 在创建的时候 this.hooks.resolver.tap("NormalModuleFactory", () => (data, callback) => {})添加了一个 resolver 钩子处理函数

  • 执行 resolver()

  • 获取 loaderResolver、 normalResolver

  • 获取对应的 loader

最后生成一个

js
let obj = {
  // 工程路径
  context: "d:\\guzhangh\\webpack\\webpack\\hash",
  // 文件的路径
  request: "d:\\guzhangh\\webpack\\webpack\\hash\\public\\js\\main.js",
  // 一开始传入的module
  dependencies: [
    {
      module: null,
      weak: false,
      optional: false,
      loc: { name: "main" },
      request: "d:\\guzhangh\\webpack\\webpack\\hash\\public\\js\\main.js",
      userRequest: "d:\\guzhangh\\webpack\\webpack\\hash\\public\\js\\main.js",
    },
  ],
  // 文件的路径
  userRequest: "d:\\guzhangh\\webpack\\webpack\\hash\\public\\js\\main.js",
  // 文件的路径
  rawRequest: "d:\\guzhangh\\webpack\\webpack\\hash\\public\\js\\main.js",
  loaders: [],
  resource: "d:\\guzhangh\\webpack\\webpack\\hash\\public\\js\\main.js",
  // 当前资源对应的 resole信息
  resourceResolveData: {
    context: { issuer: "" },
    path: "d:\\guzhangh\\webpack\\webpack\\hash\\public\\js\\main.js",
    query: "",
    module: false,
    file: false,
    descriptionFilePath: "d:\\guzhangh\\webpack\\webpack\\hash\\package.json",
    // package.json的内容
    descriptionFileData: {},
    descriptionFileRoot: "d:\\guzhangh\\webpack\\webpack\\hash",
    relativePath: "./public/js/main.js",
    __innerRequest_relativePath: "./public/js/main.js",
    __innerRequest: "./public/js/main.js",
  },
  settings: { type: "javascript/auto", resolve: {} },
  // 文件类型
  type: "javascript/auto",
  // 对于的 Parser 实例对象
  parser: {
    _pluginCompat: {},
    comments: undefined,
    hooks: {},
    options: {},
    scope: undefined,
    options: {},
    sourceType: "auto",
  },
  generator: {},
  resolveOptions: {},
}

其中最重要的是通过 loader 获取到当前资源 module 对于的 Parser 实例对象

  • this.hooks.createModule.call(result); createModule 钩子函数去创建上面数据对应的 Module 实例对象

如果没有自定义createModule那么就使用此钩子函数默认的createdModule = new NormalModule(result);去创建对于的 Module 对象

这时候我们再看 module 对象的内容

js
let module = {
  dependencies: [],
  blocks: [],
  variables: [],
  type: "javascript/auto",
  context: "d:\\guzhangh\\webpack\\webpack\\hash\\public\\js",
  debugId: 1000,
  resolveOptions: {},
  factoryMeta: {},
  warnings: [],
  errors: [],
  reasons: [],
  _chunks: { _lastActiveSortFn: null },
  id: null,
  index: null,
  index2: null,
  depth: null,
  issuer: null,
  prefetched: false,
  built: false,
  used: null,
  usedExports: null,
  optimizationBailout: [],
  useSourceMap: false,
  _source: null,
  request: "d:\\guzhangh\\webpack\\webpack\\hash\\public\\js\\main.js",
  userRequest: "d:\\guzhangh\\webpack\\webpack\\hash\\public\\js\\main.js",
  rawRequest: "d:\\guzhangh\\webpack\\webpack\\hash\\public\\js\\main.js",
  binary: false,
  parser: {
    _pluginCompat: {
      _args: ["options"],
      taps: [
        { type: "sync", name: "Parser" },
        { type: "sync", name: "Tapable camelCase", stage: 100 },
        { type: "sync", name: "Tapable this.hooks", stage: 200 },
      ],
      interceptors: [],
    },
    hooks: {},
    options: {},
    sourceType: "auto",
  },
  generator: {},
  resource: "d:\\guzhangh\\webpack\\webpack\\hash\\public\\js\\main.js",
  loaders: [],
  error: null,
  _buildHash: "",
  _cachedSources: {},
  lineToLine: false,
  _lastSuccessfulBuildMeta: {},
}

发现暂时其主要的信息还是 Parser 对象

  • createdModule = this.hooks.module.call(createdModule, result); module 钩子函数去处理 module 对象

一个 module 对象就通过 createModule -> module 两个钩子函数的蹂躏

  • 终于处理好 module 回到NormalModuleFactory.create() 的factory的回调函数中去了

这时候通过

js
if (module && this.cachePredicate(module)) {
  for (const d of dependencies) {
    dependencyCache.set(d, module)
  }
}

缓存当前 module 在对于的 dependencyCache 中

  • 继续返回回调到 moduleFactory.create()的回调中

    • this.addModule(module) 缓存 module

    • onModule(module); 如果是入口函数 就缓存到 this.entries.push(module);

    • module.addReason(null, dependency);

    • this.buildModule()终于开始编译 module

js
 /**
	* Builds the module object
	*
	* @param {Module} module 编译的module对象
	* @param {boolean} optional optional flag
	* @param {Module=} origin origin module this module build was requested from
	* @param {Dependency[]=} dependencies optional dependencies from the module to be built
	* @param {TODO} thisCallback the callback
	* @returns {TODO} returns the callback function with results
	*/
	buildModule(module, optional, origin, dependencies, thisCallback) {
		let callbackList = this._buildingModules.get(module);
		if (callbackList) {
			callbackList.push(thisCallback);
			return;
		}
		this._buildingModules.set(module, (callbackList = [thisCallback]));

		const callback = err => {
			this._buildingModules.delete(module);
			for (const cb of callbackList) {
				cb(err);
			}
		};
		// 调用编译module开始的时候的钩子函数 如
		// - ProgressPlugin  在控制台上输出编译当前module的进度条
		this.hooks.buildModule.call(module);
		// 调用每一个Module的build方法  如NormalModule的build
		module.build(
			this.options,
			this,
			this.resolverFactory.get("normal", module.resolveOptions),
			this.inputFileSystem,
			error => {
				const errors = module.errors;
				for (let indexError = 0; indexError < errors.length; indexError++) {
					const err = errors[indexError];
					err.origin = origin;
					err.dependencies = dependencies;
					if (optional) {
						this.warnings.push(err);
					} else {
						this.errors.push(err);
					}
				}

				const warnings = module.warnings;
				for (
					let indexWarning = 0;
					indexWarning < warnings.length;
					indexWarning++
				) {
					const war = warnings[indexWarning];
					war.origin = origin;
					war.dependencies = dependencies;
					this.warnings.push(war);
				}
				module.dependencies.sort((a, b) => compareLocations(a.loc, b.loc));
				if (error) {
					this.hooks.failedModule.call(module, error);
					return callback(error);
				}
				this.hooks.succeedModule.call(module);
				return callback();
			}
		);
  }

这里面又经历了

  1. this._buildingModules 缓存当前正在编译的 module

  2. this.hooks.buildModule.call(module); 调用 buildModule 钩子函数

在这里面主要有下面几个插件

  • ProgressPlugin 在控制台上输出编译当前 module 的进度条
  1. 调用真正的每一个 Module 对于的 build 方法

这时候我们又回到 NormalModule.js

js
class NormalModule extends Module {
  build(options, compilation, resolver, fs, callback) {
    this.buildTimestamp = Date.now()
    this.built = true
    this._source = null
    this._ast = null
    this._buildHash = ""
    this.error = null
    this.errors.length = 0
    this.warnings.length = 0
    this.buildMeta = {}
    this.buildInfo = {
      cacheable: false,
      fileDependencies: new Set(),
      contextDependencies: new Set(),
    }

    return this.doBuild(options, compilation, resolver, fs, err => {
      this._cachedSources.clear()

      // if we have an error mark module as failed and exit
      if (err) {
        this.markModuleAsErrored(err)
        this._initBuildHash(compilation)
        return callback()
      }

      // check if this module should !not! be parsed.
      // if so, exit here;
      const noParseRule = options.module && options.module.noParse
      if (this.shouldPreventParsing(noParseRule, this.request)) {
        this._initBuildHash(compilation)
        return callback()
      }

      const handleParseError = e => {
        const source = this._source.source()
        const error = new ModuleParseError(this, source, e)
        this.markModuleAsErrored(error)
        this._initBuildHash(compilation)
        return callback()
      }

      const handleParseResult = result => {
        this._lastSuccessfulBuildMeta = this.buildMeta
        this._initBuildHash(compilation)
        return callback()
      }

      try {
        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)
        }
      } catch (e) {
        handleParseError(e)
      }
    })
  }

  doBuild(options, compilation, resolver, fs, callback) {
    const loaderContext = this.createLoaderContext(resolver, options, compilation, fs)

    runLoaders(
      {
        resource: this.resource,
        loaders: this.loaders,
        context: loaderContext,
        readResource: fs.readFile.bind(fs),
      },
      (err, result) => {
        if (result) {
          this.buildInfo.cacheable = result.cacheable
          this.buildInfo.fileDependencies = new Set(result.fileDependencies)
          this.buildInfo.contextDependencies = new Set(result.contextDependencies)
        }

        if (err) {
          if (!(err instanceof Error)) {
            err = new NonErrorEmittedError(err)
          }
          const currentLoader = this.getCurrentLoader(loaderContext)
          const error = new ModuleBuildError(this, err, {
            from: currentLoader && compilation.runtimeTemplate.requestShortener.shorten(currentLoader.loader),
          })
          return callback(error)
        }

        const resourceBuffer = result.resourceBuffer
        const source = result.result[0]
        const sourceMap = result.result.length >= 1 ? result.result[1] : null
        const extraInfo = result.result.length >= 2 ? result.result[2] : null

        if (!Buffer.isBuffer(source) && typeof source !== "string") {
          const currentLoader = this.getCurrentLoader(loaderContext, 0)
          const err = new Error(
            `Final loader (${
              currentLoader ? compilation.runtimeTemplate.requestShortener.shorten(currentLoader.loader) : "unknown"
            }) didn't return a Buffer or String`
          )
          const error = new ModuleBuildError(this, err)
          return callback(error)
        }

        this._source = this.createSource(this.binary ? asBuffer(source) : asString(source), resourceBuffer, sourceMap)
        this._ast =
          typeof extraInfo === "object" && extraInfo !== null && extraInfo.webpackAST !== undefined
            ? extraInfo.webpackAST
            : null
        return callback()
      }
    )
  }
}

发现其主要通过

  1. build,

  2. doBuild 去真正的执行编译流程

  3. runLoaders() 获取 loader 处理后的 String 或者 Buffer

js
// 将前面loader处理后的String或者Buffer内容
// 1. 生成资源文件  包含路径和String类型的内容
// 2. 通过 SourceMapSource() 去生成对应的sourceMap文件
this._source = this.createSource(this.binary ? asBuffer(source) : asString(source), resourceBuffer, sourceMap)

继续回到 doBuild 这时候获取到 module 文件的内容了,那么这才开始真正的开始 parse

js
try {
	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);
	}
}

Parser 的流程

js
class Parser extends Tapable {
  parse(source, initialState) {
    let ast
    let comments
    if (typeof source === "object" && source !== null) {
      ast = source
      comments = source.comments
    } else {
      comments = []
      // 按照类型 将代码字符串转换成AST语法树
      ast = Parser.parse(source, {
        sourceType: this.sourceType,
        onComment: comments,
      })
    }
    // 保存原来的作用域
    const oldScope = this.scope
    const oldState = this.state
    const oldComments = this.comments
    // 设置当前模块的作用域
    this.scope = {
      topLevelScope: true,
      inTry: false,
      inShorthand: false,
      isStrict: false,
      definitions: new StackedSetMap(),
      renames: new StackedSetMap(),
    }
    const state = (this.state = initialState || {})
    this.comments = comments
    // 调用 parser 后的 program 钩子函数
    // - UseStrictPlugin
    //    用来检测文件是否有 use strict,如果有,则增加一个 ConstDependency 依赖。这里估计大家会有一个疑问:文件中已经有了,为什么还有增加一个这样的依赖呢?在 UseStrictPlugin.js 的源码中有一句注释
    //    Remove "use strict" expression. It will be added later by the renderer again. This is necessary in order to not break the strict mode when webpack prepends code.
    //    意识是说,webpack 在处理我们的代码的时候,可能会在开头增加一些代码,这样会导致我们原本写在代码第一行的 "use strict" 不在第一行。所以 UseStrictPlugin 中通过增加 ConstDependency 依赖,来放置一个“占位符”,在最后生成打包文件的时候将其再转为 "use strict"。

    // - HarmonyDetectionParserPlugin
    //   HarmonyDetectionParserPlugin 中,如果代码中有 import 或者 export 或者类型为 javascript/esm,那么会增加了两个依赖:HarmonyCompatibilityDependency, HarmonyInitDependency 依赖。
    // - WebpackDeepScopeAnalysisPlugin
    if (this.hooks.program.call(ast, comments) === undefined) {
      // 处理 use struct 严格模式
      // 检测当前执行块是否有 use strict,并设置 this.scope.isStrict = true
      this.detectStrictMode(ast.body)
      // prewalkStatements 阶段负责处理变量
      // 处理 提前声明的属性
      // 如 import
      this.prewalkStatements(ast.body)
      // 深入到函数的内部处理
      // 1. 对于函数类型的 会通过 walkFunctionDeclaration 然后再次执行 detectStrictMode -> prewalkStatements -> walkStatements
      this.walkStatements(ast.body)
    }
    this.scope = oldScope
    this.state = oldState
    this.comments = oldComments
    return state
  }
}

我们看 Parse 的流程发现其通过 acorn.js 将其按照类型转换成对应的 AST 树,然后

  • this.detectStrictMode(ast.body);
  • this.prewalkStatements(ast.body);
  • this.walkStatements(ast.body);

三步流程深度遍历处理 AST 中的语法,

其中在this.prewalkStatements(ast.body); 通过 case "ImportDeclaration": this.prewalkImportDeclaration(statement); 获取到当前 module 依赖的 Module 的数据,然后回到 doBuild的过程,其通过

js
const handleParseResult = result => {
  this._lastSuccessfulBuildMeta = this.buildMeta
  this._initBuildHash(compilation)
  return callback()
}

生成对应 module 的 Hash

然后回到Compilation.buildModule()

js
// 这个一般有 ProcessPlugin 上面不是在	this.hooks.buildModule.call(module);的时候ProcessPlugin生成当前module的编译进度命令行输出,这时候需要移除此输出
this.hooks.succeedModule.call(module)

回到_addModuleChain()

js
this.buildModule(module, false, null, null, err => {
  if (err) {
    this.semaphore.release()
    return errorAndCallback(err)
  }

  if (currentProfile) {
    const afterBuilding = Date.now()
    currentProfile.building = afterBuilding - afterFactory
  }

  this.semaphore.release()
  afterBuild()
})

其在当前 module 编译完成后 this.semaphore.release();执行了一次信号机的释放方法,那么这时候就会将下一个 Module 的编译流程在从队列中取出加入到下一个 EventLoop 中,

在最后也调用了afterBuild();

js
const afterBuild = () => {
  if (currentProfile) {
    const afterBuilding = Date.now()
    currentProfile.building = afterBuilding - afterFactory
  }

  if (addModuleResult.dependencies) {
    this.processModuleDependencies(module, err => {
      if (err) return callback(err)
      callback(null, module)
    })
  } else {
    return callback(null, module)
  }
}

我们在 Parser 的时候将遇到的 import require 等依赖其他模块的描述符对象添加到 module.dependencies 中了,那么这时候就开始通过**this.processModuleDependencies()**去处理当前 module 的依赖了

js

/**
	 * @param {Module} module to be processed for deps
	 * @param {ModuleCallback} callback callback to be triggered
	 * @returns {void}
	 */
	processModuleDependencies(module, callback) {
		// 保存当前依赖资源
		// Map : {
		// 	key : 依赖资源工厂构造函数 (NormalModuleFactory、ContextModuleFactory、) ,
		// 	value : {
		// 		每一个工厂函数类型的资源文件
		// 	  module./module1.js  : 	 [ dependency  ],
		// 	  module./module3.js  : 	 [ dependency  ],
		//  }
		// }
		const dependencies = new Map();

		const addDependency = dep => {
			// 获取当前依赖资源的标识符
			const resourceIdent = dep.getResourceIdentifier();
			if (resourceIdent) {
				// 获取依赖资源的工厂函数
				const factory = this.dependencyFactories.get(dep.constructor);
				if (factory === undefined) {
					throw new Error(
						`No module factory available for dependency type: ${
							dep.constructor.name
						}`
					);
				}
				let innerMap = dependencies.get(factory);
				if (innerMap === undefined) {
					dependencies.set(factory, (innerMap = new Map()));
				}
				let list = innerMap.get(resourceIdent);
				if (list === undefined) innerMap.set(resourceIdent, (list = []));
				list.push(dep);
			}
		};

		/**
		 * 处理不同形式的 子模块依赖
		 *
		 * @param {[type]} block [description]
		 */
		const addDependenciesBlock = block => {
			// 处理
			if (block.dependencies) {
				iterationOfArrayCallback(block.dependencies, addDependency);
			}
			// 处理 import('loadsh').then( _ => {}) 这种
			if (block.blocks) {
				iterationOfArrayCallback(block.blocks, addDependenciesBlock);
			}
			if (block.variables) {
				iterationBlockVariable(block.variables, addDependency);
			}
		};

		try {
			// 处理当前module中所有的依赖
			addDependenciesBlock(module);
		} catch (e) {
			callback(e);
		}

		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
		);
	}

从上面我们发现其也是通过遍历 module.dependencies,module.blocks,module.variables去处理不同的依赖方式声明的依赖,也是通过 this.dependencyFactories.get(dep.constructor) 每一种依赖的构造函数去获取其对应的 factory,然后最后在 this.addModuleDependencies(),

js
	/**
	 * @param {Module} module module to add deps to
	 * @param {SortedDependency[]} dependencies set of sorted dependencies to iterate through
	 * @param {(boolean|null)=} bail whether to bail or not
	 * @param {TODO} cacheGroup optional cacheGroup
	 * @param {boolean} recursive whether it is recursive traversal
	 * @param {function} callback callback for when dependencies are finished being added
	 * @returns {void}
	 */
	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;

				const errorAndCallback = err => {
					err.origin = module;
					err.dependencies = dependencies;
					this.errors.push(err);
					if (bail) {
						callback(err);
					} else {
						callback();
					}
				};
				const warningAndCallback = err => {
					err.origin = module;
					this.warnings.push(err);
					callback();
				};

				const semaphore = this.semaphore;
				// 还是通过信号机的方式去处理每一个依赖
				semaphore.acquire(() => {
					const factory = item.factory;
					factory.create(
						{
							contextInfo: {
								issuer: module.nameForCondition && module.nameForCondition(),
								compiler: this.compiler.name
							},
							resolveOptions: module.resolveOptions,
							context: module.context,
							dependencies: dependencies
						},
						(err, dependentModule) => {
							let afterFactory;

							const isOptional = () => {
								return dependencies.every(d => d.optional);
							};

							const errorOrWarningAndCallback = err => {
								if (isOptional()) {
									return warningAndCallback(err);
								} else {
									return errorAndCallback(err);
								}
							};

							if (err) {
								semaphore.release();
								return errorOrWarningAndCallback(
									new ModuleNotFoundError(module, err)
								);
							}
							if (!dependentModule) {
								semaphore.release();
								return process.nextTick(callback);
							}
							if (currentProfile) {
								afterFactory = Date.now();
								currentProfile.factory = afterFactory - start;
							}

							const iterationDependencies = depend => {
								for (let index = 0; index < depend.length; index++) {
									const dep = depend[index];
									dep.module = dependentModule;
									dependentModule.addReason(module, dep);
								}
							};

							const addModuleResult = this.addModule(
								dependentModule,
								cacheGroup
							);
							dependentModule = addModuleResult.module;
							iterationDependencies(dependencies);

							const afterBuild = () => {
								if (currentProfile) {
									const afterBuilding = Date.now();
									currentProfile.building = afterBuilding - afterFactory;
								}

								if (recursive && addModuleResult.dependencies) {
									this.processModuleDependencies(dependentModule, callback);
								} else {
									return callback();
								}
							};

							if (addModuleResult.issuer) {
								if (currentProfile) {
									dependentModule.profile = currentProfile;
								}

								dependentModule.issuer = module;
							} else {
								if (this.profile) {
									if (module.profile) {
										const time = Date.now() - start;
										if (
											!module.profile.dependencies ||
											time > module.profile.dependencies
										) {
											module.profile.dependencies = time;
										}
									}
								}
							}

							if (addModuleResult.build) {
								this.buildModule(
									dependentModule,
									isOptional(),
									module,
									dependencies,
									err => {
										if (err) {
											semaphore.release();
											return errorOrWarningAndCallback(err);
										}

										if (currentProfile) {
											const afterBuilding = Date.now();
											currentProfile.building = afterBuilding - afterFactory;
										}

										semaphore.release();
										afterBuild();
									}
								);
							} else {
								semaphore.release();
								this.waitForBuildingFinished(dependentModule, afterBuild);
							}
						}
					);
				});
			},
			err => {
				// In V8, the Error objects keep a reference to the functions on the stack. These warnings &
				// errors are created inside closures that keep a reference to the Compilation, so errors are
				// leaking the Compilation object.

				if (err) {
					// eslint-disable-next-line no-self-assign
					err.stack = err.stack;
					return callback(err);
				}

				return process.nextTick(callback);
			}
		);
	}

然后我们看 addModuleDependencies() 发现其好长好长,但是其逻辑任然是通过 asyncLib 去串行异步处理所有的依赖(通过信号机添加一个 factory.create() -> XXXModuleFactory.create() -> resolve -> loader -> buildModule -> parse -> processModuleDependencies )

总结

从上面我们可以看出 make 的流程主要是以下:

  1. SingleEntryPlugin,MultiEntryPlugin 去处理单入口还是多入口

  2. 实例化 Compilation 并添加 addEntry()

  3. 找到当前 module 对应的 XXXModuleFactory,并通过信号机的方式去调用 XXXModuleFactory.create()

  4. 在 XXXModuleFactory 中经历 resolve 的处理

  5. loader 的处理

  6. rules 的处理

生成一个当前 module 对应的 module 对象

  1. buildModule 的过程

  2. Parser 的过程

  3. 语法解析(找到 module 中的依赖)

  4. processModuleDependencies 处理当前 module 的依赖,然后无限循环上面的 3 - 10 的过程