Автор: Цуй Цзин
существуетПредыдущее поколение модулей 1Мы проанализировали, как webpack находит соответствующий файл в соответствии с конфигурацией записи, и следующим шагом будет преобразование файла в модуль. Этот длительный процесс можно разделить на следующие этапы.
- create: подготовить данные и сгенерировать экземпляр модуля.
- add: информация сохраняется в экземпляре компиляции.
- build: анализировать содержимое файла.
- processDep: обработайте зависимости, проанализированные на шаге 3, и добавьте их в цепочку компиляции.
Простой файл js будет использоваться в качестве примера позже, чтобы увидеть весь основной процесс.
// a.js
export const A = 'a'
// demo.js,webpack 入口文件
import { A } from './a.js'
function test() {
const tmp = 'something'
return tmp + A
}
const r = test()
create
_addModuleChain
После этого идет этап создания файла, который официально входит в ссылку обработки файла. В предыдущем разделе мы кратко упомянули MultipleEntryPlugin:_addModuleChain
То, что выполняется в обратном вызове,moduleFactory.create
. В приведенном выше примере метод create фактически выполняется.nromalModuleFactory.create
метод, основная логика кода выглядит следующим образом:
create(data, callback) {
//...省略部分逻辑
this.hooks.beforeResolve.callAsync(
{
contextInfo,
resolveOptions,
context,
request,
dependencies
},
(err, result) => {
//...
// 触发 normalModuleFactory 中的 factory 事件。
const factory = this.hooks.factory.call(null);
// Ignored
if (!factory) return callback();
factory(result, (err, module) => {
//...
callback(null, module);
});
}
);
}
Посмотрите на внутреннюю логику создания в одиночку:
- Инициировать событие beforeResolve: в событии beforeResolve нет обработки задачи, и напрямую введите функцию обратного вызова.
- Запускает фабричное событие в NormalModuleFactory. В конструкторе NormalModuleFactory есть логика регистрации фабричного события.
- Выполняем фабричный метод (конкретный код находится в конструкторе NormalModuleFactory), основной процесс выглядит следующим образом:
- Этап распознавателя: получите информацию о пути к demo.js, а также пути загрузчика и задействованного загрузчика (подробное описание процесса см. в распознавателе и загрузчике). После выполнения этого шага подготовка к созданию модуля завершена.
- Этап createModule: создание экземпляра модуля и сохранение данных с предыдущего шага в экземпляре.
На данный момент получен экземпляр модуля. Для удобства позже мы будем называть этот экземпляр модуля демо-модулем.
addModule
После получения демонстрационного модуля его необходимо сохранить в глобальный массив Compilation.modules и объект _modules.
В этом процессе к демонстрационному модулю также будет добавлена причина, то есть то, какой модуль зависит от демонстрационного модуля. Поскольку demo.js является файлом входа, причина, естественно, в SingleEntryDependency. А для входных файлов он также будет добавлен в Compilation.entries.
// moduleFactory.create 的 callback 函数
(err, module) => {
//...
let afterFactory;
//...
// addModule 会执行 this._modules.set(identifier, module); 其中 identifier 对于 normalModule 来说就是 module.request,即文件的绝对路径
// 和 this.modules.push(module);
const addModuleResult = this.addModule(module);
module = addModuleResult.module;
// 对于入口文件来说,这里会执行 this.entries.push(module);
onModule(module);
dependency.module = module;
module.addReason(null, dependency);
//... 开始 build 阶段
}
Эту стадию можно рассматривать как стадию добавления, которая сохраняет всю информацию о модуле в компиляции, чтобы ее можно было использовать, когда она будет окончательно упакована в чанк. Затем в этой функции обратного вызова он вызоветthis.buildModule
Войдите в стадию сборки.
build
Демонстрационный модуль является экземпляром NormalModule, поэтому метод module.build, вызываемый в Compilation.buildModule, на самом деле является методом NormalModule.build. Основная логика метода сборки следующая:
// NormalModule.build 方法
build(options, compilation, resolver, fs, callback) {
//...
return this.doBuild(options, compilation, resolver, fs, err => {
//...
try {
// 这里会将 source 转为 AST,分析出所有的依赖
const result = this.parser.parse(/*参数*/);
if (result !== undefined) {
// parse is sync
handleParseResult(result);
}
} catch (e) {
handleParseError(e);
}
})
}
// NormalModule.doBuild 方法
doBuild(options, compilation, resolver, fs, callback) {
//...
// 执行各种 loader
runLoaders(
{
resource: this.resource,
loaders: this.loaders,
context: loaderContext,
readResource: fs.readFile.bind(fs)
},
(err, result) => {
//...
// createSource 会将 runLoader 得到的结果转为字符串以便后续处理
this._source = this.createSource(
this.binary ? asBuffer(source) : asString(source),
resourceBuffer,
sourceMap
);
//...
}
);
}
build разделен на две части: обратные вызовы doBuild и doBuild.
doBuild: получить исходный код
До doBuild мы фактически получали только путь к файлу, но не получали реального содержимого файла, по этой ссылке в методе runLoader doBuild содержимое прочитанного файла будет получено по этому пути, а затем обрабатывается различными загрузчиками. , получаем окончательный результат, эта часть была проанализирована в загрузчике, см.Подробное объяснение четвертого загрузчика вебпака серии 2.
обратный вызов: обработать источник
Источником файла, полученного на предыдущем шаге, является строковая форма demo.js, как из этой строки получить зависимости demo.js? Это требует обработки этой строки,this.parser.parse
метод выполняется.
Далее давайте подробно рассмотрим процесс синтаксического анализа, конкретный код находится в lib/Parser.js. код показывает, как показано ниже:
parse(source, initialState) {
let ast;
let comments;
if (typeof source === "object" && source !== null) {
ast = source;
comments = source.comments;
} else {
comments = [];
ast = Parser.parse(source, {
sourceType: this.sourceType,
onComment: comments
});
}
const oldScope = this.scope;
const oldState = this.state;
const oldComments = this.comments;
// 设置 scope,可以理解为和代码中个作用域是一致的
this.scope = {
topLevelScope: true,
inTry: false,
inShorthand: false,
isStrict: false,
definitions: new StackedSetMap(),
renames: new StackedSetMap()
};
const state = (this.state = initialState || {});
this.comments = comments;
// 遍历 AST,找到所有依赖
if (this.hooks.program.call(ast, comments) === undefined) {
this.detectStrictMode(ast.body);
this.prewalkStatements(ast.body);
this.walkStatements(ast.body);
}
this.scope = oldScope;
this.state = oldState;
this.comments = oldComments;
return state;
}
В методе parse параметр источника может иметь две формы: объект ast или строка. Почему существует объект ast? Чтобы объяснить это, давайте сначала посмотрим, откуда берется источник параметра. Вернитесь к обратному вызову runLoaders и посмотрите
runLoaders({...}, (err, result) => {
//...省略其他内容
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;
//...
this._ast =
typeof extraInfo === "object" &&
extraInfo !== null &&
extraInfo.webpackAST !== undefined
? extraInfo.webpackAST
: null;
})
Результатом runLoader является массив:[source, sourceMap, extraInfo]
, extraInfo.webpackAST будет сохранен в module._ast, если он существует. То есть, помимо возврата исходного кода после обработки загрузчика, он также может возвращать объект AST. Он будет использоваться первым в обратном вызове doBuild.module._ast
.
const result = this.parser.parse(
this._ast || this._source.source(),
//...
)
На данный момент в метод parse передается файл extraInfo.webpackAST, возвращаемый загрузчиком после обработки, а тип — объект AST. Какая польза от этого? Если загрузчик уже выполнил преобразование файла в AST, объект AST сохраняется в файле extraInfo.webpackAST, и его можно повторно использовать непосредственно на этом шаге, чтобы избежать повторного создания AST и повысить производительность.
Вернемся к методу синтаксического анализа темы, если источник является строкой, он будет проходить черезParser.parse
затем преобразуется в AST (тот, который используется в веб-пакете,acorn). Здесь исходный код в demo.js будет разобран на древовидную структуру, примерная структура следующая
Следующим шагом является обход дерева, процесс таков: событие программы -> detectStrictMode -> prewalkStatements -> walkStatements. Этот процесс добавит в модуль множество экземпляров зависимостей. Каждый класс зависимостей будет иметь метод шаблона и сохранять диапазон позиций символов в исходном коде.Когда упакованный файл будет окончательно сгенерирован, содержимое части диапазона будет заменено результатом шаблона. Таким образом, окончательная зависимость не только содержит всю информацию о зависимостях в файле, но также используется для изменения и замены исходного содержимого при окончательном создании упакованного кода.return 'sssss' + A
заменитьreturn 'sssss' + _a_js__WEBPACK_IMPORTED_MODULE_0__["A"]
программа мероприятия
В программном событии запускаются два обратных вызова плагина: HarmonyDetectionParserPlugin и UseStrictPlugin.
HarmonyDetectionParserPlugin, если в коде есть импорт или экспорт или тип javascript/esm, то будут добавлены две зависимости: зависимости HarmonyCompatibilityDependency, HarmonyInitDependency.
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.
Осведомленность заключается в том, что когда веб-пакет обрабатывает наш код, он может добавить некоторый код в начале, что приведет к тому, что код, который мы изначально написали, будет в первой строке кода."use strict"
не в первой строке. Таким образом, UseStrictPlugin помещает «заполнитель», добавляя зависимости ConstDependency, и преобразует его в"use strict"
.
В общем, в программном событии будут добавлены зависимости к демо-модулю по ситуации.
detectStrictMode
Проверьте, есть ли в текущем исполнительном блокеuse strict
, и установитеthis.scope.isStrict = true
prewalkStatements
Предварительная фаза отвечает за обработку переменных. В сочетании с приведенным выше демонстрационным AST давайте посмотрим, как код предварительного обхода обрабатывает переменные.
Сначала введите функцию prewalkStatements, которая вызывает три узла, содержащиеся в первом слое демонстрационного AST соответственно.prewalkStatement
prewalkStatements(statements) {
for (let index = 0, len = statements.length; index < len; index++) {
const statement = statements[index];
this.prewalkStatement(statement);
}
}
Функция prewalkStatement — это гигантский метод переключения, который вызывает разные обработчики в зависимости от оператора.
prewalkStatement(statement) {
switch (statement.type) {
case "BlockStatement":
this.prewalkBlockStatement(statement);
break;
//...
}
}
Тип первого узла — importDeclaration, поэтому он войдет в метод prewalkImportDeclaration.
prewalkImportDeclaration(statement) {
// source 值为 './a.js'
const source = statement.source.value;
this.hooks.import.call(statement, source);
// 如果原始代码为 import x, {y} from './a.js',则 statement.specifiers 包含 x 和 { y } ,也就是我们导入的值
for (const specifier of statement.specifiers) {
const name = specifier.local.name; // 这里是 import { A } from './a.js' 中的 A
// 将 A 写入 renames 和 definitions
this.scope.renames.set(name, null);
this.scope.definitions.add(name);
switch (specifier.type) {
case "ImportDefaultSpecifier":
this.hooks.importSpecifier.call(statement, source, "default", name);
break;
case "ImportSpecifier":
this.hooks.importSpecifier.call(
statement,
source,
specifier.imported.name,
name
);
break;
case "ImportNamespaceSpecifier":
this.hooks.importSpecifier.call(statement, source, null, name);
break;
}
}
}
Задействовано несколько плагинов: Событие импорта вызовет HarmonyImportDependencyParserPlugin, добавляя ConstDependency и HarmonyImportSideEffectDependency.
Событие importSpecifier запускает HarmonyImportDependencyParserPlugin, который установит значение A в переименовании на «импортированная переменная».
parser.hooks.importSpecifier.tap(
"HarmonyImportDependencyParserPlugin",
(statement, source, id, name) => {
// 删除 A
parser.scope.definitions.delete(name);
// 然后将 A 设置为 import var
parser.scope.renames.set(name, "imported var");
if (!parser.state.harmonySpecifier)
parser.state.harmonySpecifier = new Map();
parser.state.harmonySpecifier.set(name, {
source,
id,
sourceOrder: parser.state.lastHarmonyImportOrder
});
return true;
}
);
После завершения первого раздела перейдите ко второму узлу и введите prewalkFunctionDeclaration. Здесь будет обрабатываться только имя функции, а содержимое функции обрабатываться не будет.
prewalkFunctionDeclaration(statement) {
if (statement.id) {
// 将 function 的名字,test 添加到 renames 和 definitions 中
this.scope.renames.set(statement.id.name, null);
this.scope.definitions.add(statement.id.name);
}
}
Остальное здесь не будет представлено по одному, во время процесса prewalkStatements переменные в текущей области видимости будут обработаны и записаны вscope.renames
и добавьте связанные зависимости в оператор импорта.
walkStatements
На предыдущем шаге prewalkStatements отвечает только за обработку переменных в текущей области видимости и не будет углубляться внутрь, если встретит функцию. На шаге обхода он в основном отвечает за проникновение внутрь функции. AST для демонстрации переходит ко второму узлу, FunctionDeclaration.
walkFunctionDeclaration(statement) {
const wasTopLevel = this.scope.topLevelScope;
this.scope.topLevelScope = false;
for (const param of statement.params) this.walkPattern(param);
// inScope 方法会生成一个新的 scope,用于对函数的遍历。在这个新的 scope 中会将函数的参数名 和 this 记录到 renames 中。
this.inScope(statement.params, () => {
if (statement.body.type === "BlockStatement") {
this.detectStrictMode(statement.body.body);
this.prewalkStatement(statement.body);
this.walkStatement(statement.body);
} else {
this.walkExpression(statement.body);
}
});
this.scope.topLevelScope = wasTopLevel;
}
будет вызываться перед обходомinScope
метод, создайте новую область, а затем дляfunction(){}
метод, продолжайте обнаруживатьStrictMode -> prewalkStatement -> walkStatement. Этот процесс похож на обход тела, давайте пропустим его здесь и посмотрим непосредственноreturn temp + A
A в AST — это конечный узел BinaryExpression.right в AST. Поскольку A - это переменная, которую мы ввели, она будет другой, код выглядит следующим образом
walkIdentifier(expression) {
// expression.name = A
if (!this.scope.definitions.has(expression.name)) {
const hook = this.hooks.expression.get(
this.scope.renames.get(expression.name) || expression.name
);
if (hook !== undefined) {
const result = hook.call(expression);
if (result === true) return;
}
}
}
В prewalk есть обработчик для переменной A, сброс которой удалит ее из определений (логика в плагине HarmonyImportDependencyParserPlugin).
// 删除 A
parser.scope.definitions.delete(name);
// 然后将 A 设置为 import var
parser.scope.renames.set(name, "imported var");
Итак, здесь будет введена логика if, и в то же времяthis.scope.renames.get(expression.name)
Результатом этого значения является 'import var'. Также в плагине HarmonyImportDependencyParserPlugin также зарегистрировано событие выражения 'import var':
parser.hooks.expression
.for("imported var")
.tap("HarmonyImportDependencyParserPlugin", expr => {
const name = expr.name;// A
// parser.state.harmonySpecifier 会在 prewalk 阶段写入
const settings = parser.state.harmonySpecifier.get(name);
// 增加一个 HarmonyImportSpecifierDependency 依赖
const dep = new HarmonyImportSpecifierDependency(
settings.source,
parser.state.module,
settings.sourceOrder,
parser.state.harmonyParserScope,
settings.id,
name,
expr.range,
this.strictExportPresence
);
dep.shorthand = parser.scope.inShorthand;
dep.directImport = true;
dep.loc = expr.loc;
parser.state.module.addDependency(dep);
return true;
});
Итак, в методе walkIdentifierthis.hooks.expression.get
Получите крючок этого события и выполните его. После завершения выполнения к модулю будет добавлена зависимость HarmonyImportSpecifierDependency.Также эта зависимость также является заполнителем, который будет добавлен в пакет, когда упакованный файл будет окончательно сгенерирован.return tmp + A
Замените букву А в .
parseСводка
Для части зависимостей всего процесса синтаксического анализа подведем итог:
- Преобразование исходного кода в AST (если исходный текст имеет строковый тип)
- Просмотрите AST и добавьте соответствующие зависимости при встрече с оператором импорта, а также добавьте соответствующие зависимости там, где A (переменная, импортируемая импортом) появляется в коде. (Зависимость «use strict» не имеет ничего общего с основным процессом, сгенерированным нашим модулем, который здесь временно игнорируется)
Все зависимости хранятся в module.dependencies, всего есть следующие 4
HarmonyCompatibilityDependency
HarmonyInitDependency
ConstDependency
HarmonyImportSideEffectDependency
HarmonyImportSpecifierDependency
На этом этап сборки завершен, и мы вернемся к функции обратного вызова module.build. Следующим шагом будет работа с зависимостями
Этап обработки зависимостей
Первое, к чему нужно вернуться, — это обратный вызов module.build, исходный код находится в buildModule Compilation.js. Отсортируйте зависимости в соответствии с порядком появления кода в файле, а затем выполнитеcallback
, продолжайте возвращаться, возвращайтесь кbuildModule
В обратном вызове метода вызывается afterBuild.
const afterBuild = () => {
if (currentProfile) {
const afterBuilding = Date.now();
currentProfile.building = afterBuilding - afterFactory;
}
// 如果有依赖,则进入 processModuleDependencies
if (addModuleResult.dependencies) {
this.processModuleDependencies(module, err => {
if (err) return callback(err);
callback(null, module);
});
} else {
return callback(null, module);
}
};
На данный момент у нас есть 4 зависимости, поэтому мы введем processModuleDependencies.
processModuleDependencies(module, callback) {
const dependencies = new Map();
// 整理 dependency
const addDependency = dep => {
const resourceIdent = dep.getResourceIdentifier();
// 过滤掉没有 ident 的,例如 constDependency 这些只用在最后打包文件生成的依赖
if (resourceIdent) {
// dependencyFactories 中记录了各个 dependency 对应的 ModuleFactory。
// 还记得前一篇文章中介绍的处理入口的 xxxEntryPlugin 吗?
// 在 compilation 事的回调中会执行 `compilation.dependencyFactories.set` 方法。
// 类似的,ImportPlugin,ConstPlugin 等等,也会在 compilation 事件回调中执行 set 操作,
// 将 dependency 与用来处理这个 dependency 的 moduleFactory 对应起来。
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);
}
};
const addDependenciesBlock = block => {
if (block.dependencies) {
iterationOfArrayCallback(block.dependencies, addDependency);
}
if (block.blocks) {
iterationOfArrayCallback(block.blocks, addDependenciesBlock);
}
if (block.variables) {
iterationBlockVariable(block.variables, addDependency);
}
};
try {
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(/*参数*/);
}
Откуда взялся блок, переменная?
На этом этапе зависимости, полученные на этапе сборки, войдут в логику addDependency. Все, что мы получаем в демонстрации, — это зависимости, но, кроме того, есть два типа блоков и переменных.
блочные зависимости
Когда мы используем ленивую загрузку веб-пакетаimport('xx.js').then()
Метод записи на этапе синтаксического анализа будет выполняться при разборе этого предложения.
//...省略其他逻辑
else if (expression.callee.type === "Import") {
result = this.hooks.importCall.call(expression);
//...
}
//...
затем войдетImportParserPlugin
В этом плагине по умолчанию стоит ленивый режим, то есть ленивая загрузка. В этом режимеImportDependenciesBlock
Введите зависимости и добавьте их в module.block.
// ImportParserPlugin
const depBlock = new ImportDependenciesBlock(
param.string,
expr.range,
Object.assign(groupOptions, {
name: chunkName
}),
parser.state.module,
expr.loc,
parser.state.module
);
// parser.state.current 为当前处理的 module
parser.state.current.addBlock(depBlock);
ImportDependenciesBlock — это отдельный чанк, который сам будет иметь зависимости типа зависимости, блока, переменной.
переменные зависят от
Если мы используем встроенные переменные модуля webpack__resourceQuery
, например следующий код
// main.js
require('./a.js?test')
// a.js
const a = __resourceQuery
console.log(a)
В модуле a.js будет module.variables__resourceQuery
. зависимости переменных используются для хранения глобальных переменных в веб-пакете (находятся только временно во время тестирования__resourceQuery
Он будет храниться в переменных), да и вообще редко используется (убрана часть про переменные в последних зависимостях модуля обработки webpack5).
Возвращаясь к нашей демонстрации, некоторые из 4 зависимостей, которые мы получили ранее, используются исключительно как «заполнители» (HarmonyCompatibilityDependency, HarmonyInitDependency, ConstDependency), первый шаг в addDependency.dep.getResourceIdentifier();
Логика отфильтрует эти зависимости, а затем классифицирует оставшиеся зависимости по идентификатору соответствующего модуляFactory и зависимости и, наконец, получит следующую структуру:
dependencies = {
NormalModuleFactory: {
"module./a.js": [
HarmonyImportSideEffectDependency,
HarmonyImportSpecifierDependency
]
}
}
Затем преобразовать в форму массива
sortedDependencies = [
{
factory: NormalModuleFactory,
dependencies: [
HarmonyImportSideEffectDependency,
HarmonyImportSpecifierDependency
]
}
]
Затем в методе addModuleDependencies такая же обработка выполняется для каждого элемента в массиве sortedDependencies, добавляя его в цепочку компиляции. Внимательно посмотрите на код в addModuleDependencies, который обрабатывает зависимости.
// addModuleDependencies
addModuleDependencies(
module,
dependencies,
bail,
cacheGroup,
recursive,
callback
) {
//...
asyncLib.forEach(
dependencies,
(item, callback) => {
const dependencies = item.dependencies;
//...
semaphore.acquire(() => {
const factory = item.factory;
// create 阶段
factory.create(
{/*参数*/},
(err, dependentModule) => {
let afterFactory;
const isOptional = () => {
return dependencies.every(d => d.optional);
};
//...
// addModule 阶段
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;
// 将 module 信息写入依赖中
iterationDependencies(dependencies);
// build 阶段
const afterBuild = () => {
//...
// build 阶段结束后有依赖的话继续处理依赖
if (recursive && addModuleResult.dependencies) {
this.processModuleDependencies(dependentModule, callback);
} else {
return callback();
}
};
//...
if (addModuleResult.build) {
this.buildModule(/*参数*/);
} else {
//...
}
}
);
});
},
err => {
//...
}
);
}
Как видно из приведенного выше кода, снова выполните create->build->add->processDep для всех зависимостей. Таким образом, все наши файлы в конечном итоге будут преобразованы в модули, и мы получим структуру отношений между модулями и зависимостями.
_preparedEntrypoints:
\
module: demo.js module
|\
| HarmonyImportSideEffectDependency
| module: a.js module
\
HarmonyImportSpecifierDependency
module: a.ja module
Эта структура будет передана последующим фрагментам и коду для создания файла пакета. После завершения процесса генерации модуля он в конечном итоге вернется к обратному вызову события make метода компиляции в Compiler.js:
compile(callback) {
const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => {
//...
this.hooks.make.callAsync(compilation, err => {
// 回到这个回调中
if (err) return callback(err);
compilation.finish();
compilation.seal(err => {
if (err) return callback(err);
this.hooks.afterCompile.callAsync(compilation, err => {
if (err) return callback(err);
return callback(null, compilation);
});
});
});
});
В методе обратного вызова запечатывания последний фрагмент будет интегрирован с использованием модуля и информации о зависимостях модулей (конкретный процесс будет представлен в следующей статье «Создание фрагментов серии Webpack»).
Суммировать
На этом процесс генерации модуля закончен, подытожим процесс генерации модуля в целом блок-схемой: