Седьмая серия webpack — генерация файлов

исходный код Webpack

Автор: Цуй Цзин,Сяо Лэй

После предыдущих статей мы представили, как webpack начинается с записи файла конфигурации, преобразует каждый файл во внутренний модуль, а затем интегрирует модули в куски один за другим. В этой статье давайте рассмотрим последний шаг — как чанки преобразуются в окончательные js-файлы.

Общий процесс

В прошлой статье в основном разбирались, как в начале фазы уплотнения, как webpack организует модули с зависимостями в один чанк. Теперь продолжаем смотреть на этап уплотнения, часть после того, как сгенерирован чанк, мы начинаем сoptimizeTree.callAsyncСмотреть

seal(callback) {
	// 优化 dependence 的 hook
	// 生成 chunk
   // 优化 modules 的 hook,提供给插件修改 modules 的能力
   // 优化 chunk 的 hook,提供给插件修改 chunk 的能力

	this.hooks.optimizeTree.callAsync(this.chunks, this.modules, err => {
		//... 优化 chunk 和 module
		//... record 为记录相关的,不是主流程,这里先忽略
		//... 优化顺序
		
		// 生成 module id
		this.hooks.beforeModuleIds.call(this.modules);
		this.hooks.moduleIds.call(this.modules);
		this.applyModuleIds();
		//... optimize

       // 排序
		this.sortItemsWithModuleIds();

       // 生成 chunk id
       //...
		this.hooks.optimizeChunkOrder.call(this.chunks);
		this.hooks.beforeChunkIds.call(this.chunks);
		this.applyChunkIds();
		//... optimize
		
		// 排序
		this.sortItemsWithChunkIds();
       //...省略 recode 相关代码
		// 生成 hash
		this.hooks.beforeHash.call();
		this.createHash();
		this.hooks.afterHash.call();
		//...
		// 生成最终输出静态文件的内容
		this.hooks.beforeModuleAssets.call();
		this.createModuleAssets();
		if (this.hooks.shouldGenerateChunkAssets.call() !== false) {
			this.hooks.beforeChunkAssets.call();
			this.createChunkAssets();
		}
		this.hooks.additionalChunkAssets.call(this.chunks);
		this.summarizeDependencies();
		//...
		// 增加 webpack 需要的额外代码
		this.hooks.additionalAssets.callAsync(err => {
		  //...
		});
	});
}

В приведенном выше коде в порядке сверху вниз основной процесс опыта выглядит следующим образом:

总流程图

Основные этапы: генерация moduleId, генерация chunkId, генерация хэша, а затем генерация содержимого конечного выходного файла.В то же время между каждым этапом выявляются хуки, предоставляющие плагинам возможность модифицировать. Далее давайте рассмотрим основную логику: генерация идентификатора, генерация хэша, генерация содержимого файла.

генерация идентификатора

webpack генерирует идентификаторы для модуля и чанка соответственно, которые в основном имеют ту же логику. Мы начинаем сmodule idВ качестве примера рассмотрим процесс генерации id (логика генерации id для модуля в webpack находится вapplyModuleIdsметод), код выглядит следующим образом

applyModuleIds() {
	const unusedIds = [];
	let nextFreeModuleId = 0;
	const usedIds = new Set();
	if (this.usedModuleIds) {
		for (const id of this.usedModuleIds) {
			usedIds.add(id);
		}
	}
	const modules1 = this.modules;
	for (let indexModule1 = 0; indexModule1 < modules1.length; indexModule1++) {
		const module1 = modules1[indexModule1];
		if (module1.id !== null) {
			usedIds.add(module1.id);
		}
	}
	if (usedIds.size > 0) {
		let usedIdMax = -1;
		for (const usedIdKey of usedIds) {
			if (typeof usedIdKey !== "number") {
				continue;
			}
			usedIdMax = Math.max(usedIdMax, usedIdKey);
		}
		let lengthFreeModules = (nextFreeModuleId = usedIdMax + 1);
		while (lengthFreeModules--) {
			if (!usedIds.has(lengthFreeModules)) {
				unusedIds.push(lengthFreeModules);
			}
		}
	}
	
	// 为 module 设置 id
	const modules2 = this.modules;
	for (let indexModule2 = 0; indexModule2 < modules2.length; indexModule2++) {
		const module2 = modules2[indexModule2];
		if (module2.id === null) {
			if (unusedIds.length > 0) module2.id = unusedIds.pop();
			else module2.id = nextFreeModuleId++;
		}
	}
}

Видно, что процесс установки идентификатора в основном делится на два этапа:

  • Найдите неиспользуемый в настоящее время идентификатор и самый большой идентификатор, который был использован. Например: если уже используемый идентификатор[3, 6, 7 ,8], Затем, после первого шага в лечении,nextFreeModuleId = 9, unusedIds = [0, 1, 2, 4, 5].

  • Установите идентификатор для модуля без идентификатора. При установке идентификатора сначала используется значение в unusedIds.

При установке id есть суждениеmodule2.id === null, то есть, если значение id было установлено до этого шага, здесь оно будет проигнорировано. Перед установкой id срабатывают два хука:

this.hooks.beforeModuleIds.call(this.modules);
this.hooks.moduleIds.call(this.modules);

В этих двух хуках мы можем управлять модулем и устанавливать свой собственный идентификатор. Внутри веб-пакета NamedModulesPlugin зарегистрирован вbeforeModuleIdsНа хуке установите относительный путь модуля к id. В среде разработки нам удобно отлаживать и анализировать код, вебпак будет использовать этот плагин по умолчанию.

После установки id модули в this.modules и модули в чанках сортируются по id. Он также сортирует причину и используемый экспорт в модуле.

chunk idЛогика генерации аналогична идентификатору модуля, аналогично, после установки идентификатора, сортировка по идентификатору.

hash

Когда webpack создает окончательный файл, мы часто устанавливаем имя файла[name].[hash].jsрежиме, добавляя хеш-значение к имени файла. Интуитивно хеш-значение здесь связано с содержимым файла, но откуда оно берется? Ответ кроется в Compilation.jscreateHashВ методе:

createHash() {
	const outputOptions = this.outputOptions;
	const hashFunction = outputOptions.hashFunction;
	const hashDigest = outputOptions.hashDigest;
	const hashDigestLength = outputOptions.hashDigestLength;
	const hash = createHash(hashFunction);
	//... update hash
	// module hash
	const modules = this.modules;
	for (let i = 0; i < modules.length; i++) {
		const module = modules[i];
		const moduleHash = createHash(hashFunction);
		module.updateHash(moduleHash);
		module.hash = moduleHash.digest(hashDigest);
		module.renderedHash = module.hash.substr(0, hashDigestLength);
	}
	// clone needed as sort below is inplace mutation
	const chunks = this.chunks.slice();
	/**
	 * sort here will bring all "falsy" values to the beginning
	 * this is needed as the "hasRuntime()" chunks are dependent on the
	 * hashes of the non-runtime chunks.
	 */
	chunks.sort((a, b) => {
		const aEntry = a.hasRuntime();
		const bEntry = b.hasRuntime();
		if (aEntry && !bEntry) return 1;
		if (!aEntry && bEntry) return -1;
		return byId(a, b);
	});
	// chunck hash
	for (let i = 0; i < chunks.length; i++) {
		const chunk = chunks[i];
		const chunkHash = createHash(hashFunction);
		if (outputOptions.hashSalt) chunkHash.update(outputOptions.hashSalt);
		chunk.updateHash(chunkHash);
		const template = chunk.hasRuntime()
			? this.mainTemplate
			: this.chunkTemplate;
		template.updateHashForChunk(chunkHash, chunk);
		this.hooks.chunkHash.call(chunk, chunkHash);
		chunk.hash = chunkHash.digest(hashDigest);
		hash.update(chunk.hash);
		chunk.renderedHash = chunk.hash.substr(0, hashDigestLength);
		this.hooks.contentHash.call(chunk);
	}
	this.fullHash = hash.digest(hashDigest);
	this.hash = this.fullHash.substr(0, hashDigestLength);
}

Основная структура фактически состоит из двух частей:

  • Сгенерировать хеш для модуля
  • Сгенерировать хеш для чанка

Нижний уровень расчета хеш-значения в webpack — это уровень в Node.js.crypto, в основном, используя два метода:

  • hash.updateЕго можно просто рассматривать как добавление исходного контента, используемого для генерации хэша (далее — источник хэша).
  • digestМетод используется для получения окончательного значения хеш-функции.

Давайте сначала посмотрим на процесс генерации хэша модуля.

module hash

Логика кода, сгенерированная хэшем модуля, выглядит следующим образом:

createHash() {
   //...省略其他逻辑
	const modules = this.modules;
	for (let i = 0; i < modules.length; i++) {
		const module = modules[i];
		const moduleHash = createHash(hashFunction);
		module.updateHash(moduleHash);
		module.hash = moduleHash.digest(hashDigest);
		module.renderedHash = module.hash.substr(0, hashDigestLength);
	}
	//...省略其他逻辑
}

среди ключевыхupdateHashМетод инкапсулирован в реализацию каждого класса модуля, и взаимосвязь вызова выглядит следующим образом:

module-hash结构

Как видно из рисунка выше, содержимое хэша модуля включает в себя:

  • Каждый модуль имеет свою уникальную информацию, которую нужно записать в хеш

    Для NormalModule этот метод, в частности:

    updateHash(hash) {
    	this.updateHashWithSource(hash);
    	this.updateHashWithMeta(hash);
    	super.updateHash(hash);
    }
    

    То есть он будет содержать метаданные buildMeta исходного контента и сгенерированного файла.

  • идентификатор модуля и используемая информация об экспорте

  • опираясь на информацию

    Конкретная информация о каждой зависимости должна быть записана в источник хэша, который определен в xxxDependency.js.updateHashметод решить. Например следующий код

     // 打包的入口 main.js
      import { A } from './a.js'
      import B from './b.js'
      import 'test-module'
      
      console.log(A)
      B()
    

    После преобразования в модуль (пожалуйста, вспомните процесс генерации модуляПятое поколение модулей webpack серии 2), триimportполучит триHarmonyImportSideEffectDependency. Возьмите эту зависимость в качестве примера и взгляните на процесс записи информации о зависимости в исходном содержимом хеш-контента, как показано на следующем рисунке.

    dep-hash继承

    Как видно из рисунка выше, зависимый модуль повлияет на хеш текущего модуля.Если мы изменим порядок или другие операции вызовут изменение id зависимого модуля, хеш, полученный текущим модулем, также изменится. Таким образом, хеш-контент модуля включает не только исходный код, но и контент, связанный с его упаковкой и построением. Поскольку, когда мы изменим соответствующую конфигурацию веб-пакета, окончательный код, вероятно, изменится. Запись этих конфигураций, которые повлияют на генерацию окончательного кода, в буфер, который генерирует хэш, может гарантировать, что когда мы только изменим конфигурацию упаковки веб-пакета, например, вы также может получить имя файла с другим значением хеш-функции, изменив способ генерации идентификатора модуля.

chunck hash

Перед генерацией хэша чанка сначала будет сортироваться чанк (зачем сортировать, давайте сначала поставим этот вопрос, а потом ответим на него после того, как посмотрим генерацию чанка). Генерация хэша фрагмента, первый шагchunk.updateHash(chunkHash);, конкретный код выглядит следующим образом (находится в Chunck.js):

updateHash(hash) {
	hash.update(`${this.id} `);
	hash.update(this.ids ? this.ids.join(",") : "");
	hash.update(`${this.name || ""} `);
	for (const m of this._modules) {
		hash.update(m.hash);
	}
}

Эта часть логики очень проста, запишите идентификатор, идентификаторы, имя и хэш-информацию всех модулей, которые он содержит. Затем напишите информацию о шаблоне, которая генерирует фрагмент:template.updateHashForChunk(chunkHash, chunk). webpack делит шаблоны на два типа: mainTemplate в итоге сгенерирует код, содержащий runtime и chunkTemplate, то есть мы находимся впервая статьяШаблон кода фрагмента, загруженный через webpackJsonp, как показано здесь.

В основном мы смотрим на mainTemplateupdateHashForChunkметод

updateHashForChunk(hash, chunk) {
	this.updateHash(hash);
	this.hooks.hashForChunk.call(hash, chunk);
}
updateHash(hash) {
	hash.update("maintemplate");
	hash.update("3");
	hash.update(this.outputOptions.publicPath + "");
	this.hooks.hash.call(hash);
}

Здесь будет записан тип шаблона «maintemplate» и настроенный нами publicPath. Затем сработавшее хеш-событие и событие hashForChunk будут записывать выходные данные некоторых файлов. Например: метод jsonp, используемый для загрузки чанков, реализован через JsonpMainTemplatePlugin. В хеш-хуках будет срабатывать его обратный вызов, и в хеш будет записана соответствующая информация jsonp, такая как: имя функции обратного вызова jsonp и т. д. После сохранения соответствующей информации в хэш-буфере вызовите метод дайджеста для генерации окончательного хэша, а затем извлеките из него нужную длину, и получится хэш чанка.

В общем, хэш чанка зависит от хэшей всех модулей внутри него, а также от различной информации, связанной с выходным чанком, которую мы настраиваем. Подобно хэшу модуля, это гарантирует, что при изменении соответствующей конфигурации веб-пакета мы получим другое значение хеш-функции после изменения кода.

Есть еще вопрос остался, почему вам нужно отсортировать при генерировании хэша?

существуетupdateHashForChunkВо время процесса плагин TemplatePathPlugin будет запущен в хуке hashForChunk и выполнит следующую логику.

// TemplatePathPlugin.js
mainTemplate.hooks.hashForChunk.tap(
	"TemplatedPathPlugin",
	(hash, chunk) => {
		const outputOptions = mainTemplate.outputOptions;
		const chunkFilename =
			outputOptions.chunkFilename || outputOptions.filename;
		// 文件名带 chunkhash 
		if (REGEXP_CHUNKHASH_FOR_TEST.test(chunkFilename))
			hash.update(JSON.stringify(chunk.getChunkMaps(true).hash));
		
		// 文件名带 contenthash
		if (REGEXP_CONTENTHASH_FOR_TEST.test(chunkFilename)) {
			hash.update(
				JSON.stringify(
					chunk.getChunkMaps(true).contentHash.javascript || {}
				)
			);
		}
		// 文件名带 name
		if (REGEXP_NAME_FOR_TEST.test(chunkFilename))
			hash.update(JSON.stringify(chunk.getChunkMaps(true).name));
	}
);

Если мы зададим имя выходного файла с помощью chunkhash в webpack.config.js, например:filename: [name].[chunkhash].js, будет искать хэш всех текущих чанков и получит следующую структуру:

{
  hash: { // chunkHashMap
    0: 'chunk 0 的 hash',
    ...
  },
  name: nameHashMap,
  contentHash: { // chunkContentHashMap
    javascript: {
      0: 'chunk 0 的 contentHash',
      ...
    }
  }
}

Затем преобразуйте хэш-содержимое приведенного выше результата в строку и запишите ее в хеш-буфер. Таким образом, для чанков со средой выполнения этот шаг зависит от хеш-значения всех чанков без времени выполнения. Следовательно, перед вычислением хэша чанка будет выполняться логика сортировки. Если подумать немного дальше, зачем полагаться на хэш-значение фрагментов без времени выполнения? Для чанков, которые нужно загружать асинхронно (т.е. чанки без времени выполнения), они будут загружаться через теги скрипта при использовании.На данный момент src — это его имя файла, поэтому имя этого файла нужно сохранить в чанке содержащий время выполнения. Когда имя файла содержит хеш-значение, содержимое файла чанка, содержащего среду выполнения, будет отличаться из-за разных хеш-значений других чанков, поэтому сгенерированное хеш-значение также должно измениться соответствующим образом.

create assets

После создания хеш-значения вызывается метод createChunkAssets для определения соответствующего текстового содержимого в каждом выводимом фрагменте.

// Compilation.js

class Compilation extends Tapable {
	...
	createChunkAssets() {
		for (let i = 0; i < this.chunks.length; i++) {
			const chunk = this.chunks[i]
			try {
				const template = chunk.hasRuntime()
					? this.mainTemplate
					: this.chunkTemplate;
				const manifest = template.getRenderManifest({
					chunk,
					hash: this.hash, // 这次 compilation 的 hash 值
					fullHash: this.fullHash, // 这次 compilation 未被截断的 hash 值
					outputOptions,
					moduleTemplates: this.moduleTemplates,
					dependencyTemplates: this.dependencyTemplates
				}); // [{ render(), filenameTemplate, pathOptions, identifier, hash }]
				for (const fileManifest of manifest) {
					...
					source = fileManifeset.render() // 渲染生成每个 chunk 最终输出的代码
					...
					this.assets[file] = source;
					...
				}
			}
			....
		}
	}
	...
}

Основные шаги:

  1. Получить соответствующий шаблон рендеринга

Внутри метода createChunkAssets блок, который необходимо вывести, будет пройден, и шаблон рендеринга (mainTemplate/chunkTemplate), который будет использоваться, определяется в зависимости от того, содержит ли блок код времени выполнения веб-пакета. mainTemplate в основном используется для рендеринга и генерации кода фрагмента, включая загрузку среды выполнения веб-пакета, а chunkTemplate в основном используется для рендеринга кода обычных фрагментов.

  1. Затем получите контент, необходимый для рендеринга, через getRenderManifest.

mainTemplate и chunkTemplate имеют собственные методы getRenderManifest, в которых генерируется вся информация, необходимая коду рендеринга, включая формат имени файла, соответствующую функцию рендеринга, хеш-значение и т. д.

  1. Выполните render(), чтобы получить окончательный код.
  2. Получите путь к файлу и сохраните его в активах.

Давайте сначала посмотрим, как фрагмент, содержащий код среды выполнения веб-пакета, выводит окончательное текстовое содержимое фрагмента.

mainTemplate отображает фрагмент, содержащий загрузочный код среды выполнения веб-пакета.

Используемый в этом случае mainTemplate вызывает метод getRenderManifest экземпляра для получения массива конфигурации манифеста, каждый из которых содержит следующие поля:

// MainTemplate.js
class MainTemplate extends Tapable {
	...
	getRenderManifest(options) {
		const result = [];

		this.hooks.renderManifest.call(result, options);

		return result;
	}
	...
}

Далее будет оцениваться, выводился ли фрагмент ранее (выходной фрагмент будет кэширован). Если нет, то будет вызван метод рендеринга для завершения вывода текста этого чанка, а именно:compilation.mainTemplate.renderметод.

// MainTemplate.js

module.exports = class MainTemplate extends Tapable {
	...
	constructor() {
		// 注册 render 钩子函数
		this.hooks.render.tap(
			"MainTemplate",
			(bootstrapSource, chunk, hash, moduleTemplate, dependencyTemplates) => {
				const source = new ConcatSource();
				source.add("/******/ (function(modules) { // webpackBootstrap\n");
				source.add(new PrefixSource("/******/", bootstrapSource));
				source.add("/******/ })\n");
				source.add(
					"/************************************************************************/\n"
				);
				source.add("/******/ (");
				source.add(
					// 调用 modules 钩子函数,用以渲染 runtime chunk 当中所需要被渲染的 module
					this.hooks.modules.call(
						new RawSource(""),
						chunk,
						hash,
						moduleTemplate,
						dependencyTemplates
					)
				);
				source.add(")");
				return source;
			}
		);
	}
  ...
  /**
	 * @param {string} hash hash to be used for render call
	 * @param {Chunk} chunk Chunk instance
	 * @param {ModuleTemplate} moduleTemplate ModuleTemplate instance for render
	 * @param {Map<Function, DependencyTemplate>} dependencyTemplates dependency templates
	 * @returns {ConcatSource} the newly generated source from rendering
	 */
	render(hash, chunk, moduleTemplate, dependencyTemplates) {
		// 生成 webpack runtime bootstrap 代码
		const buf = this.renderBootstrap(
			hash,
			chunk,
			moduleTemplate,
			dependencyTemplates
		);
		// 调用 render 钩子函数
		let source = this.hooks.render.call(
			new OriginalSource(
				Template.prefix(buf, " \t") + "\n",
				"webpack/bootstrap"
			),
			chunk,
			hash,
			moduleTemplate,
			dependencyTemplates
		);
		if (chunk.hasEntryModule()) {
			source = this.hooks.renderWithEntry.call(source, chunk, hash);
		}
		if (!source) {
			throw new Error(
				"Compiler error: MainTemplate plugin 'render' should return something"
			);
		}
		chunk.rendered = true;
		return new ConcatSource(source, ";");
	}
  ...
}

Внутри этого метода сначала вызывается метод renderBootstrap для завершения объединения загрузочного кода среды выполнения веб-пакета, а затем вызывается обработчик рендеринга, зарегистрированный в конструкторе MainTemplate. Мы можем увидеть внутреннюю часть этого хука, в основном, чтобы завершить слой упаковки вне кода начальной загрузки среды выполнения, а затем вызвать хук модулей, чтобы начать генерацию модулей, которые необходимо отобразить в этом фрагменте времени выполнения (в частности, как каждый модуль завершает работу). сплайсинг и рендеринг кода я расскажу позже в работе). После вызова хука рендеринга получается фрагмент кода, содержащий загрузочный код среды выполнения веб-пакета, и, наконец, возвращается экземпляр типа ConcatSource. Упростите его примерно следующим образом:

chunk代码生成逻辑

Окончательный код будет храниться в дочерних элементах класса ConcatSource, а окончательный код каждого модуля — в классе ReplaceSource, который содержит массив замен, в котором хранятся операции по преобразованию исходного кода, каждого элемента массива. структура следующая:

	[替换源码的起始位置,替换源码的终止位置,替换的最终内容,优先级]

То есть в процессе рендеринга исходная строка фактически не изменяется, но содержимое, которое нужно изменить, сохраняется в массиве, и окончательный код генерируется в соответствии с этим массивом и исходным кодом, когда статический файл окончательно выход. Это гарантирует, что в течение всего процесса мы можем задним числом вносить эти изменения в исходный код, а в некоторых хуках мы можем гибко изменять эти операции.

runtime chunk

Конфигурация веб-пакета предоставляет параметр конфигурации оптимизации кода: следует ли разделить фрагмент времени выполнения на фрагмент и вывести его в окончательный файл. Это также определяет, что в конечном итоге включается, когда обработчик рендеринга генерирует код фрагмента времени выполнения. Во-первых, давайте взглянем на соответствующую информацию о конфигурации:

// webpack.config.js
module.exports = {
	...
	optimization: {
		runtimeChunk: {
			name: 'bundle'
		}
	}
	...
}

Настроив поле оптимизации, можно инициировать события, связанные с регистрацией плагина RuntimeChunkPlugin.

module.exports = class RuntimeChunkPlugin {
	constructor(options) {
		this.options = Object.assign(
			{
				name: entrypoint => `runtime~${entrypoint.name}`
			},
			options
		);
	}

	apply(compiler) {
		compiler.hooks.thisCompilation.tap("RuntimeChunkPlugin", compilation => {
			// 在 seal 阶段,生成最终的 chunk graph 后触发这个钩子函数,用以生成新的 runtime chunk
			compilation.hooks.optimizeChunksAdvanced.tap("RuntimeChunkPlugin", () => {
				// 遍历所有的 entrypoints(chunkGroup)
				for (const entrypoint of compilation.entrypoints.values()) {
					// 获取每个 entrypoints 的 runtimeChunk(chunk)
					const chunk = entrypoint.getRuntimeChunk();
					// 最终需要生成的 runtimeChunk 的文件名
					let name = this.options.name;
					if (typeof name === "function") {
						name = name(entrypoint);
					}
					if (
						chunk.getNumberOfModules() > 0 ||
						!chunk.preventIntegration ||
						chunk.name !== name
					) {
						// 新建一个 runtime 的 chunk,在 compilation.chunks 中也会新增这一个 chunk。
						// 这样在最终生成的 chunk 当中会包含一个 runtime chunk
						const newChunk = compilation.addChunk(name);
						newChunk.preventIntegration = true;
						// 将这个新的 chunk 添加至 entrypoint(chunk) 当中,那么 entrypoint 也就多了一个新的 chunk
						entrypoint.unshiftChunk(newChunk);
						newChunk.addGroup(entrypoint);
						// 将这个新生成的 chunk 设置为这个 entrypoint 的 runtimeChunk
						entrypoint.setRuntimeChunk(newChunk);
					}
				}
			});
		});
	}
};

Таким образом, загрузчик среды выполнения веб-пакета отдельно извлекается в фрагмент для вывода через плагин RuntimeChunkPlugin. В конце концов, этот фрагмент времени выполнения содержит только код, связанный с загрузкой веб-пакета, и не будет содержать код другого модуля, который необходимо вывести. Конечно, если вы не хотите отделять фрагмент времени выполнения, то эта часть кода времени выполнения в конечном итоге будет упакована в фрагмент, содержащий фрагмент времени выполнения.Конечное содержимое выходного файла этого фрагмента должно содержать не только различные модули. от которого зависит блок.Окончательный код также должен включать загрузочный код веб-пакета.

var window = window || {}

// webpackBootstrap
(function(modules) {
	// 包含了 webpack bootstrap 的代码
})([
/* 0 */   // module 0 的最终代码
(function(module, __webpack_exports__, __webpack_require__) {

}),
/* 1 */   // module 1 的最终代码
(function(module, __webpack_exports__, __webpack_require__) {

})
])

module.exports = window['webpackJsonp']

Выше говорилось об использовании MainTemplate для рендеринга и завершения фрагмента времени выполнения.

chunkTemplate отображает обычный код фрагмента

Далее давайте посмотрим, как фрагменты, не содержащие код среды выполнения веб-пакета (использующие chunkTemplate для рендеринга шаблонов), выводят окончательный контент.

Сначала вызовите метод getRenderManifest, предоставленный в классе ChunkTemplate, чтобы получить содержимое, связанное с манифестом фрагмента.

// ChunkTemplate.js
class ChunkTemplate {
	...
	getRenderManifest(options) {
		const result = []

		// 触发 ChunkTemplate renderManifest 钩子函数
		this.hooks.renderManifest.call(result, options)

		return result
	}
	...
}

// JavascriptModulesPlugin.js
class JavascriptModulesPlugin {
	apply(compiler) {
		compiler.hooks.compilation.tap('JavascriptModulesPlugin', (compilation, { normalModuleFactory }) => {
			...
			// ChunkTemplate hooks.manifest 钩子函数
			compilation.chunkTemplate.hooks.renderManifest.tap('JavascriptModulesPlugin', (result, options) => {
				...
				result.push({
					render: () =>
						// 每个 chunk 代码的生成即调用 JavascriptModulesPlugin 提供的 renderJavascript 方法来进行生成
						this.renderJavascript(
							compilation.chunkTemplate, // chunk模板
							chunk, // 需要生成的 chunk 实例
							moduleTemplates.javascript, // 模块类型
							dependencyTemplates // 不同依赖所对应的渲染模板
						),
					filenameTemplate,
					pathOptions: {
						chunk,
						contentHashType: 'javascript'
					},
					identifier: `chunk${chunk.id}`,
					hash: chunk.hash
				})
				...
			})
			...
		})
	}

	renderJavascript(chunkTemplate, chunk, moduleTemplate, dependencyTemplates) {
		const moduleSources = Template.renderChunkModules(
			chunk,
			m => typeof m.source === "function",
			moduleTemplate,
			dependencyTemplates
		)
		const core = chunkTemplate.hooks.modules.call(
			moduleSources,
			chunk,
			moduleTemplate,
			dependencyTemplates
		)
		let source = chunkTemplate.hooks.render.call(
			core,
			chunk,
			moduleTemplate,
			dependencyTemplates
		)
		if (chunk.hasEntryModule()) {
			source = chunkTemplate.hooks.renderWithEntry.call(source, chunk)
		}
		chunk.rendered = true
		return new ConcatSource(source, ";")
	}
}

Таким образом, элемент конфигурации манифеста фрагмента для рендеринга получается путем запуска хука renderManifest. Основное отличие от массива манифеста, полученного MainTemplate, — это функция рендеринга.То, что вы можете видеть здесь, — это функция рендеринга, предоставляемая подключаемым модулем JavascriptModulesPlugin, вызываемым для рендеринга каждого фрагмента.

После получения элементов конфигурации манифеста, необходимых для рендеринга чанка, вызывается функция рендеринга, чтобы начать рендеринг конечного выходного содержимого чанка, что соответствует методу renderJavascript в JavascriptModulesPlugin.

emit-assets-chunk

  1. Template.renderChunkModules Получите все модули, от которых зависит каждый фрагмент, и, наконец, необходимо отобразить код.
  2. chunkTemplate.hooks.modules запускает хук hooks.modules, чтобы внести большинство изменений в чанк, прежде чем окончательно сгенерировать код фрагмента.
  3. chunkTemplate.hooks.render Когда два вышеуказанных шага выполнены, вызовите функцию-ловушку hooks.render, чтобы завершить окончательный рендеринг чанка, то есть добавить функцию обертывания во внешний слой.

renderChunkModules — генерирует код для каждого модуля

В обзоре веб-пакета мы представили общую структуру кода после упаковки веб-пакета:

(function(modules){
  ...(webpack的函数)
  return __webpack_require__(__webpack_require__.s = "./demo01/main.js");
})(
 {
   "./a.js": (function(){...}),
   "./b.js": (function(){...}),
   "./main.js": (function(){...}),
 }
)

Функция немедленного исполнения, параметром функции является объект (иногда массив), состоящий из различных модулей. Параметры функции здесь получаются функцией renderChunkModules: получаем код каждого модуля через метод moduleTemplate.render, а затем инкапсулируем его в виде массива:[/*module a.js*/, /*module b.js*/]Или в виде объекта:{'a.js':function, 'b.js': function}, который добавляется как параметр к немедленно выполняемой функции. Код метода renderChunkModules выглядит следующим образом:

class Template {
	static renderChunkModules(
		chunk,
		filterFn,
		moduleTemplate,
		dependencyTemplates,
		prefix = ""
	) {
		const source = new ConcatSource();
		const modules = chunk.getModules().filter(filterFn); // 获取这个 chunk 所依赖的模块
		let removedModules;
		if (chunk instanceof HotUpdateChunk) {
			removedModules = chunk.removedModules;
		}
		// 如果这个 chunk 没有依赖的模块,且 removedModules 不存在,那么立即返回,代码不再继续向下执行
		if (
			modules.length === 0 &&
			(!removedModules || removedModules.length === 0)
		) {
			source.add("[]");
			return source;
		}
		// 遍历所有依赖的 module,每个 module 通过使用 moduleTemplate.render 方法进行渲染得到最终这个 module 需要输出的内容
		/** @type {{id: string|number, source: Source|string}[]} */
		const allModules = modules.map(module => {
			return {
				id: module.id, // 每个 module 的 id
				source: moduleTemplate.render(module, dependencyTemplates, { // 渲染每个 module
					chunk
				})
			};
		});
		// 判断这个 chunk 所依赖的 module 的 id 是否存在边界值,如果存在边界值,那么这些 modules 将会放置于一个以边界数组最大最小值作为索引的数组当中;
		// 如果没有边界值,那么 modules 将会被放置于一个以 module.id 作为 key,module 实际渲染内容作为 value 的对象当中
		const bounds = Template.getModulesArrayBounds(allModules);
		if (bounds) {
			// Render a spare array
			const minId = bounds[0];
			const maxId = bounds[1];
			if (minId !== 0) {
				source.add(`Array(${minId}).concat(`);
			}
			source.add("[\n");
			/** @type {Map<string|number, {id: string|number, source: Source|string}>} */
			const modules = new Map();
			for (const module of allModules) {
				modules.set(module.id, module);
			}
			for (let idx = minId; idx <= maxId; idx++) {
				const module = modules.get(idx);
				if (idx !== minId) {
					source.add(",\n");
				}
				source.add(`/* ${idx} */`);
				if (module) {
					source.add("\n");
					source.add(module.source); // 添加每个 module 最终输出的代码
				}
			}
			source.add("\n" + prefix + "]");
			if (minId !== 0) {
				source.add(")");
			}
		} else {
			// Render an object
			source.add("{\n");
			allModules.sort(stringifyIdSortPredicate).forEach((module, idx) => {
				if (idx !== 0) {
					source.add(",\n");
				}
				source.add(`\n/***/ ${JSON.stringify(module.id)}:\n`);
				source.add(module.source);
			});
			source.add(`\n\n${prefix}}`);
		}

		return source
	}
}

Давайте посмотрим, как отрендерить код сплайсинга для каждого зависимого модуля в процессе рендеринга чанка, то есть в методе renderChunkModules, предоставленном в классе Template, в процессе обхода всех зависимых модулей в чанке вызовите moduleTemplate.render Завершите рендеринг кода и склейку каждого модуля.

Во-первых, давайте взглянем на следующие три шаблона, относящиеся к коду модуля вывода:

  • RuntimeTemplate

    Как следует из названия, этот класс шаблона в основном предоставляет методы вывода кода, связанные со средой выполнения модуля.Например, если ваш модуль использует тип esModule, экспортируемый модуль кода будет содержать__esModuleфлаг, и внешние модули, импортированные с помощью синтаксиса импорта, будут проходить/* harmony import */Аннотация для идентификации.

  • dependencyTemplates

    Массив шаблонов dependencyTemplates в основном сохраняет шаблоны разных зависимостей каждого модуля, при выводе конечного кода замена кода шаблона будет производиться через dependencyTemplates.

  • ModuleTemplate

    Класс шаблона ModuleTemplate в основном предоставляет метод рендеринга для внешнего мира.Вызов метода рендеринга для экземпляра moduleTemplate завершает рендеринг кода каждого модуля, который также является методом входа для каждого модуля для вывода окончательного кода.

Теперь начнем с шаблона ModuleTemplate:

// ModuleTemplate.js
module.exports = class ModuleTemplate extends Tapable {
	constructor(runtimeTemplate, type) {
		this.runtimeTemplate = runtimeTemplate
		this.type = type
		this.hooks = {
			content: new SyncWaterfallHook([]),
			module: new SyncWaterfallHook([]),
			render: new SyncWaterfallHook([]),
			package: new SyncWaterfallHook([]),
			hash: new SyncHook([])
		}
	}

	render(module, dependencyTemplates, options) {
		try {
			// replaceSource
			const moduleSource = module.source(
				dependencyTemplates,
				this.runtimeTemplate,
				this.type
			);
			const moduleSourcePostContent = this.hooks.content.call(
				moduleSource,
				module,
				options,
				dependencyTemplates
			);
			const moduleSourcePostModule = this.hooks.module.call(
				moduleSourcePostContent,
				module,
				options,
				dependencyTemplates
			);
			// 添加编译 module 外层包裹的函数
			const moduleSourcePostRender = this.hooks.render.call(
				moduleSourcePostModule,
				module,
				options,
				dependencyTemplates
			);
			return this.hooks.package.call(
				moduleSourcePostRender,
				module,
				options,
				dependencyTemplates
			);
		} catch (e) {
			e.message = `${module.identifier()}\n${e.message}`;
			throw e;
		}
	}
}

emit-assets-module

  1. Сначала вызовите метод module.source, передав ему dependencyTemplates, runtimeTemplate и тип типа рендеринга (по умолчанию — javascript). После выполнения метода module.source он вернет класс ReplaceSource, который содержит исходный код и массив замены. Массив замены хранит операции обработки исходного кода.

  2. FunctionModuleTemplatePlugin будет вызываться на этапе рендеринга, инкапсулируя код, который мы написали в файле, как функцию.

    children:[
      '/***/ (function(module, __webpack_exports__, __webpack_require__) {↵↵'
      '"use strict";↵'
      CachedSource // 1,2 步骤中得到的结果
      '↵↵/***/ })'
    ]
    
  3. Последний пакет, запускающий хук пакета. На этом этапе FunctionModuleTemplatePlugin добавит некоторые комментарии к нашему окончательному коду, чтобы помочь нам взглянуть на код.

исходник - преобразование кодаТеперь мы собираемся погрузиться в module.source, исходный метод, определенный для каждого модуля:

// NormalModule.js
class NormalModule extends Module {
	...
	source(dependencyTemplates, runtimeTemplate, type = "javascript") {
		const hashDigest = this.getHashDigest(dependencyTemplates);
		const cacheEntry = this._cachedSources.get(type);
		if (cacheEntry !== undefined && cacheEntry.hash === hashDigest) {
			// We can reuse the cached source
			return cacheEntry.source;
		}
		// JavascriptGenerator
		const source = this.generator.generate(
			this,
			dependencyTemplates, // 依赖的模板
			runtimeTemplate,
			type
		);

		const cachedSource = new CachedSource(source);
		this._cachedSources.set(type, {
			source: cachedSource,
			hash: hashDigest
		});
		return cachedSource;
	}
	...
}

Мы видим, что метод generate.generate вызывается внутри метода module.source, так откуда же берется этот генератор? По сути, процесс создания NormalModule через NormalModuleFactory завершает создание генератора, который используется для генерации финального визуализированного кода javascript каждого модуля.

getGenerator

Таким образом, код выполнения generate.generate в module.source находится в JavascriptGenerator.js.

// JavascriptGenerator.js
class JavascriptGenerator {
	generate(module, dependencyTemplates, runtimeTemplate) {
		const originalSource = module.originalSource(); // 获取这个 module 的 originSource
		if (!originalSource) {
			return new RawSource("throw new Error('No source available');");
		}
		
		// 创建一个 ReplaceSource 类型的 source 实例
		const source = new ReplaceSource(originalSource);

		this.sourceBlock(
			module,
			module,
			[],
			dependencyTemplates,
			source,
			runtimeTemplate
		);

		return source;
	}

	sourceBlock(
		module,
		block,
		availableVars,
		dependencyTemplates,
		source,
		runtimeTemplate
	) {
		// 处理这个 module 的 dependency 的渲染模板内容
		for (const dependency of block.dependencies) {
			this.sourceDependency(
				dependency,
				dependencyTemplates,
				source,
				runtimeTemplate
			);
		}

		...

		for (const childBlock of block.blocks) {
			this.sourceBlock(
				module,
				childBlock,
				availableVars.concat(vars),
				dependencyTemplates,
				source,
				runtimeTemplate
			);
		}
	}

	// 获取对应的 template 方法并执行,完成依赖的渲染工作
	sourceDependency(dependency, dependencyTemplates, source, runtimeTemplate) {
		const template = dependencyTemplates.get(dependency.constructor);
		if (!template) {
			throw new Error(
				"No template for dependency: " + dependency.constructor.name
			);
		}
		template.apply(dependency, source, runtimeTemplate, dependencyTemplates);
	}
}

Основной поток метода генерации, предоставляемого JavascriptGenerator, выглядит следующим образом:

  1. 生成一个 ReplaceSource 对象,并将源码保存到对象中。 This object will also contain an array of replacements and the source method (the source method will be called when the final code is generated, and the corresponding position code in the source _source will be replaced according to the content of the replacements to obtain the final код).
  2. В соответствии с каждой зависимостью исходный код обрабатывается соответствующим образом, что может заключаться в замене некоторого кода или вставке некоторого кода. Эти операции по преобразованию исходного кода будут храниться в массиве замен ReplaceSource. (Подробнее см.dependencyTemplatesЯ не буду обсуждать это здесь)
  3. Обработка переменных, специфичных для веб-пакета
  4. Если есть блок, делаем 1-4 обработки для каждого блока (при использовании асинхронной загрузки в блок будет помещено содержимое соответствующего импорта).

Большинство операций по преобразованию исходного кода выполняется на втором шаге, описанном выше, который заключается в вызове метода apply шаблона dependencyTemplate, соответствующего каждой зависимости. Все классы xxDependency в webpack будут иметь статический метод Template, который является методом для генерации окончательного кода, соответствующего зависимости (см.dependencyTemplates).

Давайте воспользуемся следующим простым примером, чтобы подробно рассмотреть процесс преобразования исходного кода. Демо выглядит следующим образом

// 打包的入口 main.js
import { A } from './a.js'
console.log(A)

// a.ja
export const A = 'a'
export const B = 'B'

В предыдущих статьях мы представили, что после процесса создания файла в веб-пакете будут получены модули всех файлов, а импорт/экспорт в каждом файле будет преобразован в зависимость. следующим образом

module中包含dep

Итак, модуль main.js при выполнении метода generatefor (const dependency of block.dependencies)На этом этапе вы столкнетесь с 5 типами зависимостей, один за другим.

HarmonyCompatibilityDependencyЕго код шаблона выглядит следующим образом:

HarmonyCompatibilityDependency.Template = class HarmonyExportDependencyTemplate {
	apply(dep, source, runtime) {
		const usedExports = dep.originModule.usedExports;
		if (usedExports !== false && !Array.isArray(usedExports)) {
			const content = runtime.defineEsModuleFlagStatement({
				exportsArgument: dep.originModule.exportsArgument
			});
			source.insert(-10, content);
		}
	}
};

здесьusedExportПеременная хранит в модуле экспорты, которые используются другими модулями. Он особенный для модуля входа каждого чанка, этому значению сразу будет присвоено значение true, а дляexport defaultмодуль оператора, это значение равноdefault, в обоих случаях здесь генерируется следующий код:

__webpack_require__.r(__webpack_exports__);

__webpack_require__.rметод будет__webpack_exports__объект добавляет один__esModuleатрибут, чтобы идентифицировать его как модуль es. webpack преобразует экспорты, представленные в нашем коде, в атрибуты module.exports.__esModuleИдентифицирует модуль, когда мы передаемimport x from 'xx'При введении xmodule.exports.defaultСодержимое , в противном случае оно будет рассматриваться как спецификация модуля CommonJs, и вводится весь module.exports.

HarmonyInitDependencyНаряду с HarmonyCompatibilityDependency зависимостью является HarmonyInitDependency. Этот метод шаблона будет проходить по всем зависимостям в модуле, и если зависимость имеет гармониюInit, она будет выполнена.

for (const dependency of module.dependencies) {
	const template = dependencyTemplates.get(dependency.constructor);
	if (
		template &&
		typeof template.harmonyInit === "function" &&
		typeof template.getHarmonyInitOrder === "function"
	) {
	//...
	}
}

Здесь необходимо объяснить метод гармонииInit: когда мы используем импорт и экспорт в исходном коде, веб-пакету необходимо вставить некоторый целевой код в начало нашего исходного кода, чтобы обработать логику этих ссылок, что можно рассматривать как инициализацию. код. Например, мы часто видим в коде, сгенерированном webpack

__webpack_exports__["default"] = /*...*/
或者
__webpack_require__.d(__webpack_exports__, "A", function() { return A; });

Логика генерации этих кодов хранится в методе HarmonyInit соответствующей зависимости и будет выполняться во время процесса HarmonyInitDependency. На этом этапе вы также лучше поймете причину, о которой мы упоминали, когда говорили о фазе синтаксического анализа генерации модуля: если мы обнаружим импорт или экспорт в исходном коде, будет добавлена ​​зависимость HarmonyInitDependency.

HarmonyImportSideEffectDependency и HarmonyImportSpecifierDependency в main.jsharmonyInitметод, будет вызываться здесь, сгенерируйте следующий код соответственно

"/* harmony import */ var _a_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);↵" // 对应:import { A } from './a.js'

ConstDepedency и HarmonyImportSideEffectDependency import { A } from './a.js'Например:

  • ConstDenpendency заменит это предложение пустой строкой.
  • HarmonyImportSideEffectDependency здесь не имеет реального эффекта Таким образом, роль этих двух зависимостей вместе состоит в том, чтобы фактически преобразовать это предложение в/* harmony import */ var _a_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);

HarmonyImportSpecifierDependencyобработкаconsole.log(A)Эта зависимость добавляется, когда A in , и здесь генерируется следующее имя переменной:

_a_js__WEBPACK_IMPORTED_MODULE_0__[/* A */ "a"]

И замените A в исходном коде этим именем и, наконец, сопоставьте A с переменной A, представленной в a.js.

Когда все зависимости main.js будут обработаны, будут получены следующие данные

//ReplaceSource
replacements:[
  [-10, -11, "__webpack_require__.r(__webpack_exports__);↵", 0],
  [-1, -2, "/* harmony import */ var _a_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);↵", 1],
  [0, 25, "", 2], // 0-25 对应源码:import { A } from './a.js'
  [39, 39, "_a_js__WEBPACK_IMPORTED_MODULE_0__[/* A */ "a"]", 7], // 84-84 对应源码:console.log(A) 中的 A
]

Сравните исходный код и замените код в соответствующем месте исходного кода содержимым в ReplaceSource:

__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _a_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./a.js */ "./demo01/a.js");

console.log(_a_js__WEBPACK_IMPORTED_MODULE_0__["A"])

После обработки веб-пакета наш main.js станет кодом выше.

Давайте посмотрим на это сноваa.js:

В a.js есть 4 типа зависимостей: HarmonyCompatibilityDependency, HarmonyInitDependency, HarmonyExportHeaderDependency, HarmonyExportSpecifierDependency.

HarmonyInitDependencyЭта зависимость была введена в main.js ранее, и она будет проходить через все зависимости. В этом процессе код a.jsexport const Aа такжеexport const Bв соответствующем HarmonyExportSpecifierDependencytemplate.harmonyInitМетод будет выполняться в это время, и тогда будут получены следующие два предложения

/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return A; });
/* unused harmony export B */

так что в финальном кодеconst AОн прописан в экспорте модуля, соответствующего a.js. а такжеconst BПоскольку на него не ссылается другой код, он будет обнаружен логикой встряхивания дерева веб-пакета, которая здесь просто преобразована в комментарий.

HarmonyExportHeaderDependency

Его обработка исходного кода очень проста, код выглядит следующим образом:

HarmonyExportHeaderDependency.Template = class HarmonyExportDependencyTemplate {
	apply(dep, source) {
		const content = "";
		const replaceUntil = dep.range
			? dep.range[0] - 1
			: dep.rangeStatement[1] - 1;
		source.replace(dep.rangeStatement[0], replaceUntil, content);
	}
};

Так как в логике HarmonyInitDependency завершена обработка экспортных переменных, здесь будетexport const A = 'a'а такжеexport const B = 'b'в предложенииexportЗамените пустой строкой.

HarmonyExportSpecifierDependencyTemplate.apply сама по себе является пустой функцией, поэтому эта зависимость в основном играет роль в HarmonyInitDependency.

После обработки всех зависимостей в a.js вы получите один из следующих результатов:

// ReplaceSource
children:[
	[-1, -2, "/* harmony export (binding) */ __webpack_require__…", "a", function() { return A; });↵", 0],
	[-1, -2, "/* unused harmony export B */↵", 1],
	[0, 6, "", 2], // 0-6 对应源码:'export '
	[21, 27, "", 3], // 21-27 对应源码:'export '
]

Точно так же, если вы замените код в соответствующем месте исходного кода a.js, исходный код a.js станет следующим:

__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "A", function() { return A; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "B", function() { return B; });
const A = 'a'
const B = 'B'

generate.render Этот шаг завершает преобразование исходного содержимого, а затем возвращается к методу рендеринга ModuleTemplate.js, чтобы продолжить интеграцию независимых модулей в окончательную исполняемую функцию.

content, module, render, package — пакет кодаПосле завершения generate.render далее запускаются две функции ловушек, hooks.content и hooks.module, которые в основном используются для завершения работы по обработке кода после замены зависимого кода для модуля. код модуля, зарегистрировав соответствующие хуки.Поскольку код, полученный в это время, еще не обернул код среды выполнения веб-пакета во внешний слой, поэтому эти две функции хуков наиболее подходят для модификации кода модуля.

Когда выполняются два вышеуказанных хука, срабатывает хук hooks.render:

// FunctionModuleTemplatePlugin.js
class FunctionModuleTemplatePlugin {
	apply(moduleTemplate) {
		moduleTemplate.hooks.render.tap(
			"FunctionModuleTemplatePlugin",
			(moduleSource, module) => {
				const source = new ConcatSource();
				const args = [module.moduleArgument]; // module
				// TODO remove HACK checking type for javascript
				if (module.type && module.type.startsWith("javascript")) {
					args.push(module.exportsArgument); // __webpack_exports__
					if (module.hasDependencies(d => d.requireWebpackRequire !== false)) {
						// 判断这个模块内部是否使用了被引入的其他模块,如果有的话,那么就需要加入 __webpack_require__
						args.push("__webpack_require__");  // __webpack_require__
					}
				} else if (module.type && module.type.startsWith("json")) {
					// no additional arguments needed
				} else {
					args.push(module.exportsArgument, "__webpack_require__");
				}
				source.add("/***/ (function(" + args.join(", ") + ") {\n\n");
				if (module.buildInfo.strict) source.add('"use strict";\n'); // harmony module 会使用 use strict; 严格模式
				// 将 moduleSource 代码包裹至这个函数当中
				source.add(moduleSource);
				source.add("\n\n/***/ })");
				return source;
			}
		)
	}
}

Основная задача этой функции хука - завершить обертку кода модуля, которая была завершена выше.Содержимое обертки в основном представляет собой набор системы загрузки модулей самого веб-пакета, включая импорт, экспорт и т. Д. Окончательный сгенерированный форма кода каждого модуля:

/***/ (function(module, __webpack_exports__, __webpack_require__) {

// module 最终生成的代码被包裹在这个函数内部
// __webpack_exports__ / __webpack_require__ 相关的功能可以阅读 webpack runtime bootstrap 代码去了解

/***/ })

Когда срабатывает хук hooks.render и завершается пакет кода модуля, срабатывает хук hooks.package, который в основном используется для добавления комментариев к коду модуля.FunctionModuleTemplatePlugin.js.

На этом рендеринг кода модуля завершается, и, наконец, здесь генерируется код каждого модуля в каждом фрагменте.

После того, как код модуля сгенерирован, он возвращается к вышеуказанномуJavascriptModulePlugin.renderJavascriptВ методе продолжается процесс генерации конечного кода каждого чанка.


Интеграция в исполняемые функцииЗатем запускается функция-ловушка chunkTemplate.hooks.modules.Если вам нужно изменить код фрагмента, вы можете зарегистрировать функцию-ловушку hooks.modules через плагин, чтобы завершить соответствующую работу. После срабатывания этого хука продолжайте запускать хук-функцию chunkTemplate.hooks.render вJsonpChunkTemplatePluginСоответствующая функция хука зарегистрирована в этом плагине:

class JsonpChunkTemplatePlugin {
	/**
	 * @param {ChunkTemplate} chunkTemplate the chunk template
	 * @returns {void}
	 */
	apply(chunkTemplate) {
		chunkTemplate.hooks.render.tap(
			"JsonpChunkTemplatePlugin",
			(modules, chunk) => {
				const jsonpFunction = chunkTemplate.outputOptions.jsonpFunction;
				const globalObject = chunkTemplate.outputOptions.globalObject;
				const source = new ConcatSource();
				const prefetchChunks = chunk.getChildIdsByOrders().prefetch;
				source.add(
					`(${globalObject}[${JSON.stringify(
						jsonpFunction
					)}] = ${globalObject}[${JSON.stringify(
						jsonpFunction
					)}] || []).push([${JSON.stringify(chunk.ids)},`
				);
				source.add(modules);
				const entries = getEntryInfo(chunk);
				if (entries.length > 0) {
					source.add(`,${JSON.stringify(entries)}`);
				} else if (prefetchChunks && prefetchChunks.length) {
					source.add(`,0`);
				}

				if (prefetchChunks && prefetchChunks.length) {
					source.add(`,${JSON.stringify(prefetchChunks)}`);
				}
				source.add("])");
				return source;
			}
		)
	}
}

Основная задача этой функции-хука — снова упаковать и собрать код всех отображаемых модулей в этом чанке, чтобы сгенерировать окончательный код этого чанка, то есть тот код, который в конечном итоге будет записан в файл. С этим связан плагин JsonpTemplatePlugin, который регистрирует функцию-ловушку из chunkTemplate.hooks.render внутри и завершает перенос кода чанка в эту функцию. Давайте посмотрим на пример фрагмента кода, сгенерированного после обработки этой функцией-ловушкой:

// a.js
import { add } from './add.js'

add(1, 2)


-------
// 在 webpack config 配置环节将 webpack runtime bootstrap 代码单独打包成一个 chunk,那么最终 a.js 所在的 chunk输出的代码是:

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[1],[
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
			// module id 为0的 module 输出代码,即 a.js 最终输出的代码
/***/ }),
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
			// module id 为1的 module 输出代码,即 add.js 最终输出的代码
/***/ })
],[[0,0]]]);

На данный момент процесс метода renderJavascript был отсортирован, что также является потоком обработки окончательного вывода кода фрагмента начальной загрузки, не связанного с выполнением.

Выше приведен анализ процесса генерации кода чанка, а именно createChunkAssets, Когда этот процесс будет завершен, все чанки, которые необходимо сгенерировать в файлы, в конечном итоге будут сохранены в структуре ключ/значение компиляции:

compilation.assets = {
	[输出文件路径名]: ConcatSource(最终 chunk 输出的代码)
}

Затем оптимизируйте активы, хранящиеся в содержимом, и предоставьте разработчикам некоторые хуки для выполнения связанных операций с этими активами. крючки можно найти в деталяхЧто есть в документах webpack об оптимизации активов.

вывод статических файлов

После прохождения всех вышеперечисленных этапов вся итоговая информация о коде была сохранена в активах компиляции. Затем фрагменты кода будут объединены, а результат ReplaceSource, полученный с помощью generate.generate на предыдущем шаге, будет проходить операции замены в соответствии с порядком заменяемого исходного кода (если одно и то же место, в соответствии с приоритетом последнего параметра в замене ), чтобы заменить исходный код один за другим, а затем кодировать окончательный код. Некоторые оптимизации можно настроить в конфигурации веб-пакета, например, сжатие, поэтому некоторые оптимизации будут выполнены после получения кода. Когда работа по оптимизации, связанная с ресурсами активов, завершена, фаза уплотнения заканчивается. В это время выполните функцию уплотнения, чтобы получить обратный вызов и войти в последующий процесс веб-пакета. Дополнительные сведения см. в описании метода run, предоставляемого объектом компилятора. Содержимое этого метода обратного вызова будет выполнено в методеcompile.emitAssets:

// Compiler.js
class Compiler extends Tapable {
	...
	emitAssets(compilation, callback) {
		let outputPath;
		const emitFiles = err => {
			if (err) return callback(err);

			asyncLib.forEach(
				compilation.assets,
				(source, file, callback) => {
					let targetFile = file;
					const queryStringIdx = targetFile.indexOf("?");
					if (queryStringIdx >= 0) {
						targetFile = targetFile.substr(0, queryStringIdx);
					}

					const writeOut = err => {
						if (err) return callback(err);
						const targetPath = this.outputFileSystem.join(
							outputPath,
							targetFile
						);
						if (source.existsAt === targetPath) {
							source.emitted = false;
							return callback();
						}
						let content = source.source();

						if (!Buffer.isBuffer(content)) {
							content = Buffer.from(content, "utf8");
						}

						source.existsAt = targetPath;
						source.emitted = true;
						this.outputFileSystem.writeFile(targetPath, content, callback);
					};

					if (targetFile.match(/\/|\\/)) {
						const dir = path.dirname(targetFile);
						this.outputFileSystem.mkdirp(
							this.outputFileSystem.join(outputPath, dir),
							writeOut
						);
					} else {
						writeOut();
					}
				},
				err => {
					if (err) return callback(err);

					this.hooks.afterEmit.callAsync(compilation, err => {
						if (err) return callback(err);

						return callback();
					});
				}
			);
		};

		this.hooks.emit.callAsync(compilation, err => {
			if (err) return callback(err);
			outputPath = compilation.getPath(this.outputPath);
			this.outputFileSystem.mkdirp(outputPath, emitFiles);
		});
	}
	...
}

В этом методе сначала запускается хук-функция hooks.emit, и вот-вот начнется процесс записи файлов. Затем создайте целевую выходную папку и выполните метод emitFiles для вывода ресурсов активов, сохраненных в памяти, в целевую папку, тем самым завершив запись кода фрагмента, сохраненного в памяти, в окончательный файл. Окончательная блок-схема исходящих ресурсов для вывода окончательного файла фрагмента выглядит следующим образом:

emit-assets-main-process