webpack源码解析(一)compiler对象的构建过程

817 阅读8分钟

阅读源码的目的

为了有能力自己自定义编写自己的webpack插件(ps:为了写个自定义插件还需要阅读源代码--,给你一个赞👍,写的很好,下次别写了)

源码流程

执行webpack(options)返回一个compiler 首先看看compiler是如何生成的

const webpack = /** @type {WebpackFunctionSingle & WebpackFunctionMulti} */ (
	/**
	 * @param {WebpackOptions | (ReadonlyArray<WebpackOptions> & MultiCompilerOptions)} options options
	 * @param {Callback<Stats> & Callback<MultiStats>=} callback callback
	 * @returns {Compiler | MultiCompiler}
	 */
	(options, callback) => {
                        ...
			const { compiler, watch } = create();
			...
			return compiler;
		}
	}
);

const create = () => {
			...
			const webpackOptions = /** @type {WebpackOptions} */ (options);
				/** @type {Compiler} */
			compiler = createCompiler(webpackOptions);
			watch = webpackOptions.watch;
			watchOptions = webpackOptions.watchOptions || {};
			return { compiler, watch, watchOptions };
		};

const createCompiler = rawOptions => {
        //配置默认属性
	const options = getNormalizedWebpackOptions(rawOptions);
	applyWebpackOptionsBaseDefaults(options);
        //创建compiler
	const compiler = new Compiler(options.context, options);
        /*
        新增NodeEnvironmentPlugin插件:
        给compiler绑定infrastructureLogger、
        inputFileSystem(fileSystem引用graceful-fs 然后把其中一些方法进行缓存并且暴露出来 比如lstat、stata、readDir、readFile)、
        outputFileSystem = intermediateFileSystem = graceful-fs、
        watchFileSystem = new NodeWatchFileSystem(inputFileSystem)、其中watcher属性新建Watchpack实例 与监听文件相关 稍后再讲
        然后在compiler.hooks.beforeRun.taps上增加事件
        */
	new NodeEnvironmentPlugin({
		infrastructureLogging: options.infrastructureLogging
	}).apply(compiler);
        
        //遍历plugins 执行plugin的apply方法 传递compiler
	if (Array.isArray(options.plugins)) {
		for (const plugin of options.plugins) {
			if (typeof plugin === "function") {
				plugin.call(compiler, compiler);
			} else {
				plugin.apply(compiler);
			}
		}
	}
        //继续配置一些默认属性 如entry、output、resolve
	applyWebpackOptionsDefaults(options);
        //这时可以知道webpack首先执行的是compiler的environment事件钩子 此钩子在webpack执行初始化配置时启用 此时taps是空 没有添加事件执行
	compiler.hooks.environment.call();
        //afterEnvironment钩子在environment钩子后直接调用 此时taps是空 没有添加事件执行
	compiler.hooks.afterEnvironment.call();
        //默认会有许多plugin注册进插件中 另外介绍
	new WebpackOptionsApply().process(options, compiler);
        //compiler.hooks.initialize钩子事件触发
        //如果配置了HtmlWebpackPlugin会在此钩子上绑定一个事件 调用
	compiler.hooks.initialize.call();
	return compiler;
};

createCompiler首先会创建compiler实例、新增NodeEnvironmentPlugin插件,在beforeRun钩子中增加事件、循环调用plugins的apply方法注册插件、配置初始化参数..

WebpackOptionApply类

默认初始化plugin的类

class WebpackOptionsApply extends OptionsApply {
	constructor() {
		super();
	}

	/**
	 * @param {WebpackOptions} options options object
	 * @param {Compiler} compiler compiler object
	 * @returns {WebpackOptions} options object
	 */
	process(options, compiler) {
                //options属性给到compiler
		compiler.outputPath = options.output.path;
		compiler.recordsInputPath = options.recordsInputPath || null;
		compiler.recordsOutputPath = options.recordsOutputPath || null;
		compiler.name = options.name;
                
                //注册初始plugin到compiler.hooks上
		...
                if (options.output.clean) {
			const CleanPlugin = require("./CleanPlugin");
			new CleanPlugin(
				options.output.clean === true ? {} : options.output.clean
			).apply(compiler);
		}
                ...
                 //增加完EntryOptionPlugin到compiler.hooks.entryOption
                new EntryOptionPlugin().apply(compiler);
               
                //之后立刻执行entryOption钩子
                //EntryOptionPlugin处理完会引入EntryPlugin 遍历每个入口 每个入口都会往compiler.hook.compilation和make中加入事件
		compiler.hooks.entryOption.call(options.context, options.entry);
                
                //初始化内部插件集合 有空自己研究 一大堆内部插件
		...
		new CommonJsPlugin().apply(compiler);
		new LoaderPlugin({}).apply(compiler);
		...
                //执行afterPlugins钩子 默认空
		compiler.hooks.afterPlugins.call(compiler);
		if (!compiler.inputFileSystem) {
			throw new Error("No input filesystem provided");
		}
                //compiler.resolverFactory.hooks有两个hooks
                //往compiler.resolverFactory.hooks.resolveOptions._map(Map对象).normal.taps增加WebpackOptionsApply事件
		compiler.resolverFactory.hooks.resolveOptions
			.for("normal")
			.tap("WebpackOptionsApply", resolveOptions => {
				resolveOptions = cleverMerge(options.resolve, resolveOptions);
				resolveOptions.fileSystem = compiler.inputFileSystem;
				return resolveOptions;
			});
                //往compiler.resolverFactory.hooks.resolveOptions._map(Map对象).context.taps增加WebpackOptionsApply事件
		compiler.resolverFactory.hooks.resolveOptions
			.for("context")
			.tap("WebpackOptionsApply", resolveOptions => {
				resolveOptions = cleverMerge(options.resolve, resolveOptions);
				resolveOptions.fileSystem = compiler.inputFileSystem;
				resolveOptions.resolveToContext = true;
				return resolveOptions;
			});
                //往compiler.resolverFactory.hooks.resolveOptions._map(Map对象).loader.taps增加WebpackOptionsApply事件        
		compiler.resolverFactory.hooks.resolveOptions
			.for("loader")
			.tap("WebpackOptionsApply", resolveOptions => {
				resolveOptions = cleverMerge(options.resolveLoader, resolveOptions);
				resolveOptions.fileSystem = compiler.inputFileSystem;
				return resolveOptions;
			});
                //compiler.hooks.afterResolvers钩子事件触发 默认为空
		compiler.hooks.afterResolvers.call(compiler);
		return options;
	}
}

初始的一些plugin为:

  • ExternalsPlugin 注册到compiler.hooks.compile,与externals有关
  • ChunkPrefetchPreloadPlugin 注册到compiler.hooks.compilation
  • ArrayPushCallbackChunkFormatPlugin 注册到compiler.hooks.thisCompilation,与output.chunkformat == 'array-push'有关
  • ModuleInfoHeaderPlugin 与output.pathinfo有关,输出额外注释到bundle中,注册到compiler.hooks.compilation
  • CleanPlugin 用来清除output输出的,注册到compiler.hooks.emit
  • EvalDevToolModulePlugin devtool相关,注册到compiler.hooks.compilation
  • JavascriptModulesPlugin 注册到compiler.hooks.compilation
  • JsonModulesPlugin 注册到compiler.hooks.compilation
  • AssetModulesPlugin 注册到compiler.hooks.compilation
  • EntryOptionPlugin 注册到compiler.hooks.entryOption...

Compiler介绍

Compiler 模块是 webpack 的主要引擎,它通过 CLI 或者 Node API 传递的所有选项创建出一个 compilation 实例。 它扩展(extends)自 Tapable 类,用来注册和调用插件。 大多数面向用户的插件会首先在 Compiler 上注册。

class Compiler {
	/**
	 * @param {string} context the compilation path
	 * @param {WebpackOptions} options options
	 */
	constructor(context, options = /** @type {WebpackOptions} */ ({})) {
                //初始化hook
		this.hooks = Object.freeze({
			/** @type {SyncHook<[]>} */
			initialize: new SyncHook([]),

			/** @type {SyncBailHook<[Compilation], boolean>} */
			shouldEmit: new SyncBailHook(["compilation"]),
			/** @type {AsyncSeriesHook<[Stats]>} */
			done: new AsyncSeriesHook(["stats"]),
			/** @type {SyncHook<[Stats]>} */
			afterDone: new SyncHook(["stats"]),
			/** @type {AsyncSeriesHook<[]>} */
			additionalPass: new AsyncSeriesHook([]),
			/** @type {AsyncSeriesHook<[Compiler]>} */
			beforeRun: new AsyncSeriesHook(["compiler"]),
			/** @type {AsyncSeriesHook<[Compiler]>} */
			run: new AsyncSeriesHook(["compiler"]),
			/** @type {AsyncSeriesHook<[Compilation]>} */
			emit: new AsyncSeriesHook(["compilation"]),
			/** @type {AsyncSeriesHook<[string, AssetEmittedInfo]>} */
			assetEmitted: new AsyncSeriesHook(["file", "info"]),
			/** @type {AsyncSeriesHook<[Compilation]>} */
			afterEmit: new AsyncSeriesHook(["compilation"]),

			/** @type {SyncHook<[Compilation, CompilationParams]>} */
			thisCompilation: new SyncHook(["compilation", "params"]),
			/** @type {SyncHook<[Compilation, CompilationParams]>} */
			compilation: new SyncHook(["compilation", "params"]),
			/** @type {SyncHook<[NormalModuleFactory]>} */
			normalModuleFactory: new SyncHook(["normalModuleFactory"]),
			/** @type {SyncHook<[ContextModuleFactory]>}  */
			contextModuleFactory: new SyncHook(["contextModuleFactory"]),

			/** @type {AsyncSeriesHook<[CompilationParams]>} */
			beforeCompile: new AsyncSeriesHook(["params"]),
			/** @type {SyncHook<[CompilationParams]>} */
			compile: new SyncHook(["params"]),
			/** @type {AsyncParallelHook<[Compilation]>} */
			make: new AsyncParallelHook(["compilation"]),
			/** @type {AsyncParallelHook<[Compilation]>} */
			finishMake: new AsyncSeriesHook(["compilation"]),
			/** @type {AsyncSeriesHook<[Compilation]>} */
			afterCompile: new AsyncSeriesHook(["compilation"]),

			/** @type {AsyncSeriesHook<[]>} */
			readRecords: new AsyncSeriesHook([]),
			/** @type {AsyncSeriesHook<[]>} */
			emitRecords: new AsyncSeriesHook([]),

			/** @type {AsyncSeriesHook<[Compiler]>} */
			watchRun: new AsyncSeriesHook(["compiler"]),
			/** @type {SyncHook<[Error]>} */
			failed: new SyncHook(["error"]),
			/** @type {SyncHook<[string | null, number]>} */
			invalid: new SyncHook(["filename", "changeTime"]),
			/** @type {SyncHook<[]>} */
			watchClose: new SyncHook([]),
			/** @type {AsyncSeriesHook<[]>} */
			shutdown: new AsyncSeriesHook([]),

			/** @type {SyncBailHook<[string, string, any[]], true>} */
			infrastructureLog: new SyncBailHook(["origin", "type", "args"]),

			// TODO the following hooks are weirdly located here
			// TODO move them for webpack 5
			/** @type {SyncHook<[]>} */
			environment: new SyncHook([]),
			/** @type {SyncHook<[]>} */
			afterEnvironment: new SyncHook([]),
			/** @type {SyncHook<[Compiler]>} */
			afterPlugins: new SyncHook(["compiler"]),
			/** @type {SyncHook<[Compiler]>} */
			afterResolvers: new SyncHook(["compiler"]),
			/** @type {SyncBailHook<[string, Entry], boolean>} */
			entryOption: new SyncBailHook(["context", "entry"])
		});
                //compiler.webpack中带有webpack暴露出的方法、变量
                //const webpack = require("./");引入自身
		this.webpack = webpack;

		/** @type {string=} */
		this.name = undefined;
		/** @type {Compilation=} */
		this.parentCompilation = undefined;
		/** @type {Compiler} */
		this.root = this;
		/** @type {string} */
		this.outputPath = "";
		/** @type {Watching} */
		this.watching = undefined;

		/** @type {OutputFileSystem} */
		this.outputFileSystem = null;
		/** @type {IntermediateFileSystem} */
		this.intermediateFileSystem = null;
		/** @type {InputFileSystem} */
		this.inputFileSystem = null;
		/** @type {WatchFileSystem} */
		this.watchFileSystem = null;

		/** @type {string|null} */
		this.recordsInputPath = null;
		/** @type {string|null} */
		this.recordsOutputPath = null;
		this.records = {};
		/** @type {Set<string | RegExp>} */
		this.managedPaths = new Set();
		/** @type {Set<string | RegExp>} */
		this.immutablePaths = new Set();

		/** @type {ReadonlySet<string>} */
		this.modifiedFiles = undefined;
		/** @type {ReadonlySet<string>} */
		this.removedFiles = undefined;
		/** @type {ReadonlyMap<string, FileSystemInfoEntry | "ignore" | null>} */
		this.fileTimestamps = undefined;
		/** @type {ReadonlyMap<string, FileSystemInfoEntry | "ignore" | null>} */
		this.contextTimestamps = undefined;
		/** @type {number} */
		this.fsStartTime = undefined;

		/** @type {ResolverFactory} */
		this.resolverFactory = new ResolverFactory();

		this.infrastructureLogger = undefined;

		this.options = options;

		this.context = context;

		this.requestShortener = new RequestShortener(context, this.root);

		this.cache = new Cache();

		/** @type {Map<Module, { buildInfo: object, references: WeakMap<Dependency, Module>, memCache: WeakTupleMap }> | undefined} */
		this.moduleMemCaches = undefined;

		this.compilerPath = "";

		/** @type {boolean} */
		this.running = false;

		/** @type {boolean} */
		this.idle = false;

		/** @type {boolean} */
		this.watchMode = false;

		this._backCompat = this.options.experiments.backCompat !== false;

		/** @type {Compilation} */
		this._lastCompilation = undefined;
		/** @type {NormalModuleFactory} */
		this._lastNormalModuleFactory = undefined;

		/** @private @type {WeakMap<Source, { sizeOnlySource: SizeOnlySource, writtenTo: Map<string, number> }>} */
		this._assetEmittingSourceCache = new WeakMap();
		/** @private @type {Map<string, number>} */
		this._assetEmittingWrittenFiles = new Map();
		/** @private @type {Set<string>} */
		this._assetEmittingPreviousFiles = new Set();
	**}**

compiler hook的执行顺序:

  1. environment 准备环境

SyncHook

在编译器准备环境时调用,时机就在配置文件中初始化插件之后,在createCompiler函数中,生成compiler实例,配置完初始化插件参数后调用。

  1. afterEnvironment 环境设置完成后

SyncHook

当编译器环境设置完成后,在 environment hook 后直接调用。在createCompiler函数中。

  1. entryOption 处理entry入口参数

SyncBailHook

  • 回调参数:contextentry

在createCompiler函数,WebpackOptionsApply.process(options, compiler)初始化默认插件的函数中调用,添加完EntryOptionPlugin到此钩子后调用,传入context跟options.entry选项,此时taps中有EntryOptionPlugin插件事件,处理事件,结果就是根据entry每个入口往compiler.hooks.compiliation跟make中各加入一个事件

class EntryOptionPlugin {
	/**
	 * @param {Compiler} compiler the compiler instance one is tapping into
	 * @returns {void}
	 */
	apply(compiler) {
		compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => {
			EntryOptionPlugin.applyEntryOption(compiler, context, entry);
			return true;
		});
	}

	/**
	 * @param {Compiler} compiler the compiler
	 * @param {string} context context directory
	 * @param {Entry} entry request
	 * @returns {void}
	 */
	static applyEntryOption(compiler, context, entry) {
		if (typeof entry === "function") {
			const DynamicEntryPlugin = require("./DynamicEntryPlugin");
			new DynamicEntryPlugin(context, entry).apply(compiler);
		} else {
			const EntryPlugin = require("./EntryPlugin");
			for (const name of Object.keys(entry)) {
				const desc = entry[name];
				const options = EntryOptionPlugin.entryDescriptionToOptions(
					compiler,
					name,
					desc
				);
				for (const entry of desc.import) {
					new EntryPlugin(context, entry, options).apply(compiler);
				}
			}
		}
	}
        
        /**
	 * @param {Compiler} compiler the compiler
	 * @param {string} name entry name
	 * @param {EntryDescription} desc entry description
	 * @returns {EntryOptions} options for the entry
	 */
	static entryDescriptionToOptions(compiler, name, desc) {
		/** @type {EntryOptions} */
		const options = {
			name,
			filename: desc.filename,
			runtime: desc.runtime,
			layer: desc.layer,
			dependOn: desc.dependOn,
			baseUri: desc.baseUri,
			publicPath: desc.publicPath,
			chunkLoading: desc.chunkLoading,
			asyncChunks: desc.asyncChunks,
			wasmLoading: desc.wasmLoading,
			library: desc.library
		};
		if (desc.layer !== undefined && !compiler.options.experiments.layers) {
			throw new Error(
				"'entryOptions.layer' is only allowed when 'experiments.layers' is enabled"
			);
		}
		if (desc.chunkLoading) {
			const EnableChunkLoadingPlugin = require("./javascript/EnableChunkLoadingPlugin");
			EnableChunkLoadingPlugin.checkEnabled(compiler, desc.chunkLoading);
		}
		if (desc.wasmLoading) {
			const EnableWasmLoadingPlugin = require("./wasm/EnableWasmLoadingPlugin");
			EnableWasmLoadingPlugin.checkEnabled(compiler, desc.wasmLoading);
		}
		if (desc.library) {
			const EnableLibraryPlugin = require("./library/EnableLibraryPlugin");
			EnableLibraryPlugin.checkEnabled(compiler, desc.library.type);
		}
		return options;
	}
}
  1. afterPlugins 初始化内部插件集合后

SyncHook

  • 回调参数:compiler

在createCompiler函数,WebpackOptionsApply.process(options, compiler)初始化默认插件的函数中调用,在初始化内部插件集合完成设置之后调用。

  1. afterResolvers 设置完compiler.resolveFactory.hooks.resolveOptions钩子函数后触发

SyncHook

在createCompiler函数,WebpackOptionsApply.process(options, compiler)初始化默认插件的函数中调用

  • 回调参数:compiler
  1. initialize 当compiler对象被初始化plugins、hooks后调用

SyncHook

在createCompiler函数,WebpackOptionsApply.process(options, compiler)初始化默认插件的函数后调用。如果配置了HtmlWebpackPlugin,会在此时调用配置在此钩子下的函数:

class HtmlWebpackPlugin {
  /**
   * @param {HtmlWebpackOptions} [options]
   */
  constructor (options) {
    /** @type {HtmlWebpackOptions} */
    this.userOptions = options || {};
    this.version = HtmlWebpackPlugin.version;
  }

  apply (compiler) {
    // Wait for configuration preset plugions to apply all configure webpack defaults
    compiler.hooks.initialize.tap('HtmlWebpackPlugin', () => {
      //配置的参数与默认参数合并
      const userOptions = this.userOptions;

      // Default options
      /** @type {ProcessedHtmlWebpackOptions} */
      const defaultOptions = {
        template: 'auto',
        templateContent: false,
        templateParameters: templateParametersGenerator,
        filename: 'index.html',
        publicPath: userOptions.publicPath === undefined ? 'auto' : userOptions.publicPath,
        hash: false,
        inject: userOptions.scriptLoading === 'blocking' ? 'body' : 'head',
        scriptLoading: 'defer',
        compile: true,
        favicon: false,
        minify: 'auto',
        cache: true,
        showErrors: true,
        chunks: 'all',
        excludeChunks: [],
        chunksSortMode: 'auto',
        meta: {},
        base: false,
        title: 'Webpack App',
        xhtml: false
      };

      /** @type {ProcessedHtmlWebpackOptions} */
      const options = Object.assign(defaultOptions, userOptions);
      this.options = options;

      // Assert correct option spelling
      ...

      // Default metaOptions if no template is provided
      ...

      // 遍历entry 根据每个key生成数组 数组元素是option 调用hookIntoCompiler
      const userOptionFilename = userOptions.filename || defaultOptions.filename;
      const filenameFunction = typeof userOptionFilename === 'function'
        ? userOptionFilename
        // Replace '[name]' with entry name
        : (entryName) => userOptionFilename.replace(/\[name\]/g, entryName);

      /** output filenames for the given entry names */
      const entryNames = Object.keys(compiler.options.entry);
      const outputFileNames = new Set((entryNames.length ? entryNames : ['main']).map(filenameFunction));

      /** Option for every entry point */
      const entryOptions = Array.from(outputFileNames).map((filename) => ({
        ...options,
        filename
      }));

      //调用hookIntoCompiler函数 主要是处理一下参数的一些初始化操作 给compiler.hooks.thisCompilation添加上事件
      entryOptions.forEach((instanceOptions) => {
        hookIntoCompiler(compiler, instanceOptions, this);
      });
    });
  }
  
 function hookIntoCompiler (compiler, options, plugin) {
  const webpack = compiler.webpack;
  // Instance variables to keep caching information
  // for multiple builds
  let assetJson;
  /**
   * store the previous generated asset to emit them even if the content did not change
   * to support watch mode for third party plugins like the clean-webpack-plugin or the compression plugin
   * @type {Array<{html: string, name: string}>}
   */
  let previousEmittedAssets = [];
  //将options.template转化成 ..dirname/html-webpack-plugin/lib/loader.js!...dirname/template.ejs(你的template文件)的形式
  options.template = getFullTemplatePath(options.template, compiler.context);

  // 使用缓存 将key用compiler映射为PersistentChildCompilerSingletonPlugin
  //调用PersistentChildCompilerSingletonPlugin.apply方法 为compiler.hooks.make添加一个事件
  const childCompilerPlugin = new CachedChildCompilation(compiler);
  
  //如果没有templateContent说明template是一个文件
  if (!options.templateContent) {
    //调用PersistentChildCompilerSingletonPlugin.addEntry(entry)
    //给PersistentChildCompilerSingletonPlugin实例.compilationState.entries数组添加上上面的template request路径
    childCompilerPlugin.addEntry(options.template);
  }

  //判断options.filname是不是相对路径那种 是就转化为相对于ouput.path的路径
  const filename = options.filename;
  if (path.resolve(filename) === path.normalize(filename)) {
    const outputPath = /** @type {string} - Once initialized the path is always a string */(compiler.options.output.path);
    options.filename = path.relative(outputPath, filename);
  }

  //检查options.minify是否启用以及当前是否为正式环境 是就启用压缩
  const isProductionLikeMode = compiler.options.mode === 'production' || !compiler.options.mode;

  const minify = options.minify;
  if (minify === true || (minify === 'auto' && isProductionLikeMode)) {
    /** @type { import('html-minifier-terser').Options } */
    options.minify = {
      // https://www.npmjs.com/package/html-minifier-terser#options-quick-reference
      collapseWhitespace: true,
      keepClosingSlash: true,
      removeComments: true,
      removeRedundantAttributes: true,
      removeScriptTypeAttributes: true,
      removeStyleLinkTypeAttributes: true,
      useShortDoctype: true
    };
  }
  //给compiler.hooks.thisCompilation添加上事件
  compiler.hooks.thisCompilation.tap('HtmlWebpackPlugin',
    /**
       * Hook into the webpack compilation
       * @param {WebpackCompilation} compilation
      */
    (compilation) => {
          ...
        });
    });

总结

compiler总的来说就是webpack的一个构建器,开发者可以通过往compiler.hook上绑定各生命周期的钩子来影响webpack的构建过程。创建compiler的主要流程有:

  1. 创建compiler实例
  2. 新增NodeEnvironmentPlugin插件,在beforeRun钩子中增加事件
  3. 循环调用plugins的apply方法注册插件
  4. 配置一些默认属性 如entry、output、resolve
  5. 执行compiler.hooks.environment事件钩子、afterEnvironment钩子在environment钩子后直接调用
  6. 配置许多默认plugin注册进插件中
  7. compiler.hooks.initialize钩子事件触发
  8. 返回compiler