Пятое поколение модулей серии webpack 1

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

Автор: CUI Jing

введение

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

Обзор

запись записи веб-пакета, параметр записи представляет собой строку с одной записью, массив с одной записью, объект с несколькими записями или динамическую функцию, независимо от того, что это такое, он будет вызыватьсяcompilation.addEntryметод, этот метод будет выполняться_addModuleChain, добавьте файл записи в очередь, которую необходимо скомпилировать. Затем файлы в очереди обрабатываются один за другим, а файлы в файлеimportВведен другими документами будетaddModuleDependenciesДобавьте в очередь компиляции. Наконец, когда содержимое очереди компиляции обработано, преобразование файла в модуль завершено.

总览

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

Управление очередью компиляции — семафор

_addModuleChain 和 addModuleDependenciesфункция вызоветthis.semaphore.acquireКонкретная реализация этой функции находится вlib/util/Semaphore.jsв файле. Посмотрите на конкретную реализацию

class Semaphore {
	constructor(available) {
	   // available 为最大的并发数量
		this.available = available;
		this.waiters = [];
		this._continue = this._continue.bind(this);
	}

	acquire(callback) {
		if (this.available > 0) {
			this.available--;
			callback();
		} else {
			this.waiters.push(callback);
		}
	}

	release() {
		this.available++;
		if (this.waiters.length > 0) {
			process.nextTick(this._continue);
		}
	}

	_continue() {
		if (this.available > 0) {
			if (this.waiters.length > 0) {
				this.available--;
				const callback = this.waiters.pop();
				callback();
			}
		}
	}
}

Есть только два метода воздействия на внешний мир:

  1. Приобретение: Подать заявку на обработку ресурсов.Если есть свободные ресурсы (т.е. параллельные номера), обработка будет выполнена немедленно, а свободные ресурсы будут уменьшены на 1, в противном случае они будут сохранены в очереди ожидания.
  2. Release: освободить ресурс. При приобретении будет вызываться метод обратного вызова, где нужно использовать release для освобождения ресурсов и прибавлять 1 к простаивающим ресурсам. При этом он проверит, есть ли еще контент для обработки, и если да, то продолжит обработку

Этот класс Semaphore заимствует концепцию Semaphore (семафора), который управляет использованием ресурсов в многопоточной среде. Количество параллелизма определяется доступным, так каково значение по умолчанию? существуетCompilation.jsможно найти в

this.semaphore = new Semaphore(options.parallelism || 100);

Число параллелизма по умолчанию — 100. Обратите внимание, что параллелизм, упомянутый здесь, — это только параллелизм в дизайне кода, его не следует путать с однопоточной функцией js. В целом процесс компиляции выглядит следующим образом

编译队列控制_new

От входа до _addModuleChain

официальный сайт вебпакаРуководство по настройкеЗапись может иметь следующие формы:

  • строка: строка, например.
{
  entry: './demo.js'
}
  • [строка]: массив строкового типа, например.
{
  entry: ['./demo1.js', './demo2.js']
}
  • объекты, такие как
{
  entry: {
    app: './demo.js'
  }
}
  • функция, динамическая возвращаемая запись, например
{
  entry: () => './demo.js'
}
// 或者
{
  entry: () => new Promise((resolve) => resolve('./demo.js'))
}

Где они обрабатываются? В стартовом файле webpack.js webpack параметры будут обрабатываться в первую очередь следующим образом:

compiler.options = new WebpackOptionsApply().process(options, compiler);

существуетprocessв процессеentryконфигурация для обработки

// WebpackOptionsApply.js 文件中
new EntryOptionPlugin().apply(compiler);
compiler.hooks.entryOption.call(options.context, options.entry);

Первый взглядEntryOptionsPluginЧто вы наделали

const SingleEntryPlugin = require("./SingleEntryPlugin");
const MultiEntryPlugin = require("./MultiEntryPlugin");
const DynamicEntryPlugin = require("./DynamicEntryPlugin");

const itemToPlugin = (context, item, name) => {
	if (Array.isArray(item)) {
		return new MultiEntryPlugin(context, item, name);
	}
	return new SingleEntryPlugin(context, item, name);
};

module.exports = class EntryOptionPlugin {
	apply(compiler) {
		compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => {
		   // string 类型则为 new SingleEntryPlugin
		   // array 类型则为 new MultiEntryPlugin
			if (typeof entry === "string" || Array.isArray(entry)) {
				itemToPlugin(context, entry, "main").apply(compiler);
			} else if (typeof entry === "object") {
			    // 对于 object 类型,遍历其中每一项
				for (const name of Object.keys(entry)) {
					itemToPlugin(context, entry[name], name).apply(compiler);
				}
			} else if (typeof entry === "function") {
			    // function 类型则为 DynamicEntryPlugin
				new DynamicEntryPlugin(context, entry).apply(compiler);
			}
			return true;
		});
	}
};

существуетEntryOptionsPluginзарегистрирован вentryOptionФункция обработчика событий, согласноentryРазличные типы значений (каждый элемент/функция в строке/массиве/объекте) создают экземпляры и выполняют разныеEntryPlugin: соответствующая строкаSingleEntryPlugin; массив соответствуетMultiEntryPlugin; функция соответствуетDynamicEntryPlugin. Для типа объекта пройдитесь по каждому ключу, обработайте каждый ключ как запись и выберите SingleEntryPlugin или MultiEntryPlugin в соответствии с типом строки/массива. Ниже мы в основном анализируем: SingleEntryPlugin, MultiEntryPlugin, DynamicEntryPlugin

Сравнивая эти три плагина по горизонтали, все они делают две вещи:

  1. Зарегистрирован обратный вызов события компиляции (это событие будет запущено перед событием make ниже), установленное на этапе компиляции.dependencyFactories
compiler.hooks.compilation.tap('xxEntryPlugin', (compilation, { normalModuleFactory }) => {
  //...
  compilation.dependencyFactories.set(...)
})
  1. Зарегистрируйте обратный вызов события make, вызовите метод addEntry на этапе make, а затем введите_addModuleChainВойдите в формальную фазу компиляции.
compiler.hooks.make.tapAsync('xxEntryPlugin',(compilation, callback) => {
  // ...
  compilation.addEntry(...)
})

В сочетании с процессом упаковки webpack давайте начнем с метода компиляции в Compiler.js и посмотрим, что делают событие компиляции и обратный вызов события make.

addEntry总流程

xxxEntryPlugin вызывается в событии компиляции для установкиcompilation.dependencyFactories, гарантированно следовать_addModuleChainНа этапе обратного вызова соответствующую зависимость можно получить по зависимостиmoduleFactory.

В обратном вызове события make зависимости генерируются в соответствии с различными конфигурациями записи, а затем вызываются.addEntryи передать зависимость.

существует_addModuleChainВ соответствии с различными типами зависимостей в обратном вызове, а затем выполнитьmultiModuleFactory.createилиnormalModuleFacotry.create.

Зависимости постоянно упоминаются в приведенных выше шагах, и различные зависимости появятся в следующих статьях. Видно, что зависимость — очень важная вещь в webpack, в папке webpack/lib/dependencies вы увидите различные зависимости. Структура отношений между зависимостью и модулем выглядит следующим образом:

module: {
  denpendencies: [
    dependency: {
      //...
      module: // 依赖的 module,也可能为 null
    }
  ]
}
}

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

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

SingleEntryPlugin

Логика SingleEntryPlugin проста: свяжите SingleEntryDependency с normalModuleFactory, чтобы последующие методы создания выполнялисьnormalModuleFactory.createметод.

apply(compiler) {
	compiler.hooks.compilation.tap(
		"SingleEntryPlugin",
		(compilation, { normalModuleFactory }) => {
		   // SingleEntryDependency 对应的是 normalModuleFactory
			compilation.dependencyFactories.set(
				SingleEntryDependency,
				normalModuleFactory
			);
		}
	);

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

			const dep = SingleEntryPlugin.createDependency(entry, name);
			// dep 的 constructor 为 SingleEntryDependency
			compilation.addEntry(context, dep, name, callback);
		}
	);
}

static createDependency(entry, name) {
	const dep = new SingleEntryDependency(entry);
	dep.loc = name;
	return dep;
}

MultiEntryPlugin

По сравнению с SingleEntryPlugin выше,

  1. В компиляции у dependencyFactories установлено два соответствующих значения
MultiEntryDependency: multiModuleFactory
SingleEntryDependency: normalModuleFactory
  1. createDependency: рассматривать каждое значение в записи как SingleEntryDependency.
static createDependency(entries, name) {
	return new MultiEntryDependency(
		entries.map((e, idx) => {
			const dep = new SingleEntryDependency(e);
			// Because entrypoints are not dependencies found in an
			// existing module, we give it a synthetic id
			dep.loc = `${name}:${100000 + idx}`;
			return dep;
		}),
		name
	);
}

3.multiModuleFactory.create

На втором шаге, поMultiEntryPlugin.createDependencyСгенерированный dep имеет следующую структуру:

{
  dependencies:[]
  module: MultiModule
  //...
}

зависимости — это массив, содержащий несколько зависимостей SingleEntryDependencies. Это dep будет передано в качестве параметра методу multiModuleFactory.create, который является data.dependencies[0] в следующем коде.

// multiModuleFactory.create
create(data, callback) {
	const dependency = data.dependencies[0];
	callback(
		null,
		new MultiModule(data.context, dependency.dependencies, dependency.name)
	);
}

Новый MultiModule создается при создании, и метод сборки в MultiModule будет выполняться в обратном вызове,

build(options, compilation, resolver, fs, callback) {
	this.built = true; // 标记编译已经完成
	this.buildMeta = {};
	this.buildInfo = {};
	return callback();
}

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

В общем, для записи нескольких файлов можно просто понять, что webpack сначала преобразует запись в следующий вид:

import './demo1.js'
import './demo2.js'

Затем обработайте его.

DynamicEntryPlugin

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

compiler.hooks.make.tapAsync(
	"DynamicEntryPlugin",
	(compilation, callback) => {
		const addEntry = (entry, name) => {
			const dep = DynamicEntryPlugin.createDependency(entry, name);
			return new Promise((resolve, reject) => {
				compilation.addEntry(this.context, dep, name, err => {
					if (err) return reject(err);
					resolve();
				});
			});
		};
		Promise.resolve(this.entry()).then(entry => {
			if (typeof entry === "string" || Array.isArray(entry)) {
				addEntry(entry, "main").then(() => callback(), callback);
			} else if (typeof entry === "object") {
				Promise.all(
					Object.keys(entry).map(name => {
						return addEntry(entry[name], name);
					})
				).then(() => callback(), callback);
			}
		});
	}
);

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

После того, как запись найдена, файл преобразуется в модуль. Следующийстатья, процесс конвертации в модуль будет подробно описан.