предисловие
Webpack — это мощный инструмент для создания пакетов с гибким и богатым механизмом подключаемых модулей.В Интернете есть бесконечное количество технических документов о том, как использовать webpack и анализировать принципы работы webpack. Недавно, в процессе изучения вебпака, я записал его и выложил, надеюсь он вам немного поможет. В этой статье в основном обсуждаются основные моменты, выполняемые в процессе сборки webpack. (Мы изучаем только общий процесс исследования и строительства, и игнорируем детали 🙈)
Известно, что исходный код Webpack представляет собой архитектуру подключаемых модулей, и многие функции реализуются через множество встроенных подключаемых модулей. Webpack написал для этой цели систему плагинов, называемуюTapable
В основном он предоставляет функции регистрации и вызова плагинов.
Прежде чем мы будем учиться вместе, я надеюсь, выtapable
Я понимаю~
отладка
Самый прямой способ прочитать исходный код — отладить ключевой код через точки останова в хроме, мы можем использоватьnode-inspector
Сделайте этот отладчик.
"scripts": {
"build": "webpack --config webpack.prod.js",
"debug": "node --inspect-brk ./node_modules/webpack/bin/webpack.js --inline --progress",
},
Выполнить сборку npm run && npm run debug
// 入口文件
import { helloWorld } from './helloworld.js';
document.write(helloWorld());
// helloworld.js
export function helloWorld() {
return 'bts';
}
// webpack.prod.js
module.exports = {
entry: {
index: './src/index.js',
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name]_[chunkhash:8].js'
},
module: {
rules: [
{
test: /.js$/,
use: 'babel-loader',
},
]
},
};
Базовая архитектура
Для начала разберем основной процесс вебпака в целом в целом, а потом представим его подробно.
На блок-схеме показаны некоторые основные задачи, кратко поясняющие, что они делают:- пройти через
yargs
Разобратьconfig
а такжеshell
элементы конфигурации в -
webpack
Процесс инициализации, в первую очередь, будет основываться на первом шагеoptions
генерироватьcompiler
объект, затем инициализируйтеwebpack
встроенные плагины иoptions
настроить -
run
Представляет начало компиляции, будет строитьcompilation
Объект, используемый для хранения всех данных для этого процесса компиляции. -
make
Выполнить реальный процесс компиляции и сборки, начиная с входного файла, построения модуля, до конца создания всего модуля. -
seal
генерироватьchunks
,правильноchunks
Выполните серию операций оптимизации и сгенерируйте код для вывода. -
seal
после окончания,Compilation
Здесь также завершается вся работа экземпляра, что означает завершение процесса сборки. -
emit
После срабатывания,webpack
пройдетcompilation.assets
, создайте все файлы, затем активируйте точку задачиdone
, чтобы завершить процесс сборки
процесс сборки
При изучении других технических блогов есть анализ основного процесса, аналогичный приведенному выше, и я понимаю логическое обоснование, но не могу убедить себя, не прерывая деталей. Ниже приведены подробные действия некоторых точек задач.Рекомендуется заинтересованным друзьям поиграть еще в несколько отладчиков.
Настоятельно рекомендуется использовать отладчик в функции обратного вызова каждого важного хука, иначе он может уйти далеко.
этап подготовки вебпака
запись запуска веб-пакета, webpack-cli/bin/cli.js
const webpack = require("webpack");
// 使用yargs来解析命令行参数并合并配置文件中的参数(options),
// 然后调用lib/webpack.js实例化compile 并返回
let compiler;
try {
compiler = webpack(options);
} catch (err) {}
// lib/webpack.js
const webpack = (options, callback) => {
// 首先会检查配置参数是否合法
// 创建Compiler
let compiler;
compiler = new Compiler(options.context);
compiler.options = new WebpackOptionsApply().process(options, compiler);
...
if (options.watch === true || ..) {
...
return compiler.watch(watchOptions, callback);
}
compiler.run(callback);
}
Создать компилятор
созданныйcompiler
объект,compiler
можно понимать какwebpack
Скомпилированный диспетчерский центр, который является экземпляром компилятора,compiler
Объект записывает полныйwebpack
экологическая информация, вwebpack
в каждом процессе,compiler
будет сгенерирован только один раз.
class Compiler extends Tapable {
constructor(context) {
super();
this.hooks = {
beforeCompile: new AsyncSeriesHook(["params"]),
compile: new SyncHook(["params"]),
afterCompile: new AsyncSeriesHook(["compilation"]),
make: new AsyncParallelHook(["compilation"]),
entryOption: new SyncBailHook(["context", "entry"])
// 定义了很多不同类型的钩子
};
// ...
}
}
можно увидетьCompiler
Объект наследуется отTapable
, во время инициализации определяется множество хуков.
Инициализировать плагин по умолчанию и конфигурацию параметров
WebpackOptionsApply
Класс зарегистрирует соответствующие плагины в соответствии с конфигурацией, один из которых является более важным плагином.
new EntryOptionPlugin().apply(compiler);
compiler.hooks.entryOption.call(options.context, options.entry);
Плагин EntryOptionPlugin подписывается на хук entryOption компилятора и полагается на плагин SingleEntryPlugin.
module.exports = class EntryOptionPlugin {
apply(compiler) {
compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => {
return new SingleEntryPlugin(context, item, name);
});
}
};
SingleEntryPlugin
подписался в плагинеcompiler
изmake
Подцепите и дождитесь выполнения в обратном вызовеaddEntry
, но в это времяmake
Крюк еще не сработал
apply(compiler) {
compiler.plugin("compilation", (compilation, params) => {
const normalModuleFactory = params.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);
// 正式进入构建阶段
compilation.addEntry(context, dep, name, callback);
}
);
}
run
инициализацияcompiler
после, согласноoptions
изwatch
Определите, следует ли начинатьwatch
, если начатьwatch
вызовcompiler.watch
для мониторинга файлов сборки, в противном случае запуститеcompiler.run
чтобы создать файл,compiler.run
На этот раз это метод входа в нашу компиляцию, что означает, что мы собираемся начать компиляцию.
этап компиляции сборки
передачаcompiler.run
способ начать сборку
run(callback) {
const onCompiled = (err, compilation) => {
this.hooks.done.callAsync(stats, err => {
return finalCallback(null, stats);
});
};
// 执行订阅了compiler.beforeRun钩子插件的回调
this.hooks.beforeRun.callAsync(this, err => {
// 执行订阅了compiler.run钩子插件的回调
this.hooks.run.callAsync(this, err => {
this.compile(onCompiled);
});
});
}
compiler.compile
Начните фактически выполнять наш процесс сборки, основной код выглядит следующим образом.
compile(callback) {
// 实例化核心工厂对象
const params = this.newCompilationParams();
// 执行订阅了compiler.beforeCompile钩子插件的回调
this.hooks.beforeCompile.callAsync(params, err => {
// 执行订阅了compiler.compile钩子插件的回调
this.hooks.compile.call(params);
// 创建此次编译的Compilation对象
const compilation = this.newCompilation(params);
// 执行订阅了compiler.make钩子插件的回调
this.hooks.make.callAsync(compilation, err => {
compilation.finish(err => {
compilation.seal(err => {
this.hooks.afterCompile.callAsync(compilation, err => {
return callback(null, compilation);
});
})
})
})
})
}
существуетcompile
сцена,Compiler
Объект начнет создавать два основных фабричных объекта, а именноNormalModuleFactory
а такжеContextModuleFactory
. Фабричные объекты, как следует из названия, используются для создания экземпляров, которые затем используются для созданияmodule
экземпляр, в том числеNormalModule
так же какContextModule
пример.
Compilation
Создайте эту компиляциюCompilation
Object, основной код выглядит следующим образом:
newCompilation(params) {
// 实例化Compilation对象
const compilation = new Compilation(this);
this.hooks.thisCompilation.call(compilation, params);
// 调用this.hooks.compilation通知感兴趣的插件
this.hooks.compilation.call(compilation, params);
return compilation;
}
Compilation
Объект является основным и наиболее важным объектом в последующем процессе строительства, он содержит все данные в процессе строительства. То есть один процесс сборки соответствует одномуCompilation
пример. при созданииCompilation
Хук сработает, когда экземплярcompilaiion
а такжеthisCompilation
.
В объекте компиляции:
- modules записывает все проанализированные модули
- чанки записывает все чанки
- assets записывает все файлы, которые должны быть сгенерированы
Вышеупомянутые три свойства были включеныCompilation
Большая часть информации в объекте, но в настоящее время есть только общее понятие, особенноmodules
Не очень понятно, что представляет собой каждый экземпляр модуля в . Не беспокойтесь об этом, в конце концов, в это времяCompilation
Объект только что создан.
make
когдаCompilation
После создания экземпляраwebpack
Подготовительный этап пройден, начинается следующий этапmodules
стадия генерации.
this.hooks.make.callAsync()
оформить подпискуmake
Функция обратного вызова подключенного плагина. Вернемся к сказанному выше, во время инициализации плагина по умолчанию (класс WebpackOptionsApply)SingleEntryPlugin
подписался в плагинеcompiler
изmake
Подцепите и дождитесь выполнения в обратном вызовеcompilation.addEntry
метод.
генерировать модули
compilation.addEntry
метод вызовет первую партиюmodule
, то есть имеемentry
Входной файл настроен вindex.js
. глубокоmodules
Перед процессом сборки модуля мы сначалаmodule
иметь представление о концепции.
modules
После создания объекта зависимости (Dependency) соответствующим объектом-фабрикой (Factory) может быть сгенерирован соответствующий экземпляр модуля (Module).
Dependency
, который можно понимать как объект зависимости, который не был разрешен в экземпляр модуля. Например, входной модуль в конфигурации или другие модули, от которых зависит модуль, сначала сгенерируютDependency
объект. каждыйDependency
Там будут соответствующие фабричные объекты, такие как код нашего отладчика на этот раз, входной файлindex.js
сначала сгенерироватьSingleEntryDependency
, соответствующий фабричный объектNormalModuleFactory
. (упомянутый ранееSingleEntryPlugin
В плагине есть код, сомневающиеся студенты могут его дождаться)
// 创建单入口依赖
const dep = SingleEntryPlugin.createDependency(entry, name);
// 正式进入构建阶段
compilation.addEntry(context, dep, name, callback);
SingleEntryPlugin
подписка на плагинmake
событие, зависимость с одной записью, которая будет создана, передается вcompilation.addEntry
метод,addEntry
основное исполнение_addModuleChain()
_addModuleChain
_addModuleChain(context, dependency, onModule, callback) {
...
// 根据依赖查找对应的工厂函数
const Dep = /** @type {DepConstructor} */ (dependency.constructor);
const moduleFactory = this.dependencyFactories.get(Dep);
// 调用工厂函数NormalModuleFactory的create来生成一个空的NormalModule对象
moduleFactory.create({
dependencies: [dependency]
...
}, (err, module) => {
...
const afterBuild = () => {
this.processModuleDependencies(module, err => {
if (err) return callback(err);
callback(null, module);
});
};
this.buildModule(module, false, null, null, err => {
...
afterBuild();
})
})
}
_addModuleChain
получить параметры вdependency
Зависимость входящей записи, используйте соответствующую фабричную функциюNormalModuleFactory.create
метод генерирует пустойmodule
объект, который будет помещен в callbackmodule
депозитcompilation.modules
объект иdependencies.module
В объекте, потому что это входной файл, он также будет храниться вcompilation.entries
середина. затем выполнитьbuildModule
Войдите в реальный процесс создания содержимого модуля.
buildModule
buildModule
Метод в основном выполняетmodule.build()
, что соответствуетNormalModule.build()
// NormalModule.js
build(options, compilation, resolver, fs, callback) {
return this.doBuild(options, compilation, resolver, fs, err => {
...
// 一会儿讲
}
}
Первый взглядdoBuild
что сделал
doBuild(options, compilation, resolver, fs, callback) {
...
runLoaders(
{
resource: this.resource, // /src/index.js
loaders: this.loaders, // `babel-loader`
context: loaderContext,
readResource: fs.readFile.bind(fs)
},
(err, result) => {
...
const source = result.result[0];
this._source = this.createSource(
this.binary ? asBuffer(source) : asString(source),
resourceBuffer,
sourceMap
);
}
)
}
Одним словом,doBuild
называется соответствующимloaders
, чтобы преобразовать наш модуль в стандартный модуль JS. Здесь используйтеbabel-loader
компилироватьindex.js
,source
то естьbabel-loader
скомпилированный код.
// source
"debugger; import { helloWorld } from './helloworld.js';document.write(helloWorld());”
В то же время он также будет генерироватьthis._source
объект, сname
а такжеvalue
два поля,name
наш путь к файлу,value
Это скомпилированный JS-код. Исходный код модуля в конечном итоге сохраняется в_source
свойства, вы можете пройти_source.source()
получить. вернуться к только чтоNormalModule
серединаbuild
метод
build(options, compilation, resolver, fs, callback) {
...
return this.doBuild(options, compilation, resolver, fs, err => {
const result = this.parser.parse(
this._source.source(),
{
current: this,
module: this,
compilation: compilation,
options: options
},
(err, result) => {
}
);
}
}
проходить черезdoBuild
После этого любой из наших модулей превращался в стандартные JS-модули. Далее стоит позвонитьParser.parse
метод парсинга JS в AST.
// Parser.js
const acorn = require("acorn");
const acornParser = acorn.Parser;
static parse(code, options) {
...
let ast = acornParser.parse(code, parserOptions);
return ast;
}
Сгенерированные результаты AST следующие:
Самая большая функция парсинга в AST — это сбор зависимостей модулей.Webpack будет проходить по объектам AST и выполнять соответствующие функции при столкновении с различными типами узлов. например отладочный кодimport { helloWorld } from './helloworld.js'
илиconst xxx = require('XXX')
Оператор импорта модуля, WebPack будет записывать эти зависимости и записано в массиве Module.dependonds. Здесь процесс разрешения является полным модулем ввода, модуль Parsed мы заинтересованы, вы можете распечатать взгляд, здесь я только снял массив модулей.После завершения синтаксического анализа каждого модуля он будет запускатьCompilation
Пример хука SuccessModule объекта, подпишитесь на этот хук, чтобы получить объект модуля, который только что был проанализирован.
Затем webpack будет проходить по массиву module.dependencies, рекурсивно анализировать его зависимые модули для генерации модулей, и, наконец, мы получим все модули, от которых зависит проект. Логика обхода находится вafterBuild()
-> processModuleDependencies()
-> addModuleDependencies()
-> factory.create()
.
make
Это конец фазы, и следующий триггер будетcompilation.seal
метод входа в следующую стадию.
генерировать куски
compilation.seal
Метод в основном генерируетchunks
,правильноchunks
Выполняется ряд операций оптимизации и генерируется код для вывода.webpack
серединаchunk
, который можно понимать как настроенный вentry
модули в , или динамически импортированные модули.
chunk
Главное имущество внутри_modules
, который записывает все включенные объекты модуля. Таким образом, чтобы создатьchunk
, мы должны сначала найти всеmodules
. Ниже приводится краткое описание процесса генерации чанка:
- первый
entry
соответствующий каждомуmodule
оба генерируют новыйchunk
- траверс
module.dependencies
, и добавьте модули, от которых он зависит, в фрагмент, созданный на предыдущем шаге. - Если модуль импортируется динамически, создайте для него новый блок, а затем просмотрите зависимости
На следующем рисунке показаны фрагменты this.chunk, сгенерированные нашей демонстрацией на этот раз.В _modules есть два модуля, а именно модуль индекса входа, который зависит от модуля helloworld.
Во время и после процесса генерации чанков webpack выполнит серию операций по оптимизации чанков и модулей.Большинство операций по оптимизации выполняются различными плагинами. видимыйcompilation.seal
метода, существует много кода, который выполняется хуками.
this.hooks.optimizeModulesBasic.call(this.modules);
this.hooks.optimizeModules.call(this.modules);
this.hooks.optimizeModulesAdvanced.call(this.modules);
this.hooks.optimizeChunksBasic.call(this.chunks, this.chunkGroups);
this.hooks.optimizeChunks.call(this.chunks, this.chunkGroups);
this.hooks.optimizeChunksAdvanced.call(this.chunks, this.chunkGroups);
...
Например, подключаемый модуль SplitChunksPlugin подписывается на хук optimizeChunksAdvanced компиляции. На данный момент наши модули и чанки сгенерированы, и пришло время сгенерировать файлы.
makefile
Сначала нужно сгенерировать окончательный код, в основном вcompilation.seal
называется вcompilation.createChunkAssets
метод.
for (let i = 0; i < this.chunks.length; i++) {
const chunk = this.chunks[i];
const template = chunk.hasRuntime()
? this.mainTemplate
: this.chunkTemplate;
const manifest = template.getRenderManifest({
...
})
...
for (const fileManifest of manifest) {
source = fileManifest.render();
}
...
this.emitAsset(file, source, assetInfo);
}
createChunkAssets
метод будет проходитьchunks
, чтобы отобразить сгенерированный код для каждого фрагмента. фактически,compilation
Когда создается экземпляр объекта, одновременно создаются три объекта, а именноMainTemplate
, ChunkTemplate
а такжеModuleTemplate
. Эти три объекта используются для рендерингаchunk
, чтобы получить окончательный шаблон кода. Разница между ними в том, что,MainTemplate
Используется для рендеринга фрагмента записи,ChunkTemplate
Используется для рендеринга не входных фрагментов,ModuleTemplate
Используется для рендеринга модулей в кусках.
здесь,MainTemplate
а такжеChunkTemplate
изrender
методы используются для генерации различного «кода-оболочки»,MainTemplate
соответствующая записьchunk
должен иметьwebpack
Код запуска, поэтому будет несколько объявлений функций и запусков. А в коде обёртки код каждого модуля пропускается черезModuleTemplate
для рендеринга, но также генерирует только «код-оболочку» для инкапсуляции реального кода модуля, а реальный код модуля передается через экземпляр модуляsource
способ обеспечить. Это может быть не совсем понятно Просто взгляните на код в окончательном сгенерированном файле:
emitAsset
существоватьcompilation.assets
середина. Когда все фрагменты отрендерены, активы являются окончательным списком файлов, которые нужно сгенерировать. Слишком далеко,compilation
изseal
Окончание метода также означаетcompilation
На этом вся работа экземпляра закончилась, а значит процесс сборки закончился, остался только шаг генерации файла.
emit
существуетCompiler
Прежде чем начать генерировать файлы, хукиemit
будет выполнен. Это наш последний шанс изменить окончательный файл. После того, как он сгенерирован, наш файл не может быть изменен.
this.hooks.emit.callAsync(compilation, err => {
if (err) return callback(err);
outputPath = compilation.getPath(this.outputPath);
this.outputFileSystem.mkdirp(outputPath, emitFiles);
});
webpack будет напрямую проходить черезcompile.assets для создания всех файлов, а затем запускает хук, сделанный для завершения процесса сборки.
Суммировать
Мы прошли процесс построения ядра webpack, надеюсь, после прочтения статьи всем будет полезно понять принцип работы webpack~
Код этой статьи был отредактирован и изменен для лучшего понимания. Способность ограничена.Если есть какое-то неправильное место, пожалуйста, поправьте меня и обменяйтесь и учитесь вместе.