«Сделай что-нибудь сложное», чтобы подсмотреть принцип Webpack4.x из исходного кода

внешний интерфейс Webpack
«Сделай что-нибудь сложное», чтобы подсмотреть принцип Webpack4.x из исходного кода

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

«Принципы» Радарио

Видимость: 🌟🌟🌟🌟🌟

Вкус: армейский горячий горшок

Время приготовления: 15 мин.



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

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

Статья слишком длинная, не забудьте поставить лайк и добавить в избранное~单押X2

Суть веб-пакета

В чем суть Webpack? Некоторые учащиеся, возможно, уже знают, что

Webpack本质上一种基于事件流的编程范例,其实就是一系列的插件运行。

Webpack в основном используетCompilerа такжеCompilationДва класса для управления всем жизненным циклом Webpack. Все они наследуют Tapabel и используют Tapabel для регистрации событий, которые должен инициировать каждый процесс в жизненном цикле.

Tapabel

TapabelЭто библиотека, похожая на EventEmitter Node.js, которая в основном управляет публикацией и подпиской на функции-ловушки и является важным помощником в системе подключаемых модулей Webpack.

Хуки и примеры предоставлены Tapabel

Библиотека Tapable предоставляет множество хуков для монтирования плагинов.

const {
    SyncHook,                   // 同步钩子
    SyncBailHook,               // 同步熔断钩子
    SyncWaterfallHook,          // 同步流水钩子
    SyncLoopHook,               // 同步循环钩子
    AsyncParalleHook,           // 异步并发钩子
    AsyncParallelBailHook,      // 异步并发熔断钩子
    AsyncSeriesHook,            // 异步串行钩子
    AsyncSeriesBailHook,        // 异步串行熔断钩子
    AsyncSeriesWaterfallHook     // 异步串行流水钩子
} = require("tapable");

Табпак предоставляет同步&异步Способ привязывания крючка, способ следующий:

Async Sync
Привязка: tapAsync/tapPromise/tap Привязка: нажмите
Выполнить: callAsync/обещание Выполнить: позвонить

Простой пример Tabpack

const demohook = new SyncHook(["arg1", "arg2", "arg3"]);
// 绑定事件到webpack事件流
demohook.tap("hook1",(arg1, arg2, arg3) => console.log(arg1, arg2, arg3)) // 1 2 3
// 执行绑定的事件
demohook.call(1,2,3)

Интерпретация исходного кода

Входной файл инициализированного Webpack

Отслеживая источник, первым делом нужно найти входной файл Webpack.

При запуске Webpack из командной строки npm позволит инструментам командной строки перейти в каталог node_modules.bin.

Затем поищите наличие файлов webpack.sh или webpack.cmd, если они есть, выполните их, если их нет, то выдаст ошибку.

Фактический входной файл:node_modules/webpack/bin/webpack.js, давайте взглянем на основные функции внутри.

// node_modules/webpack/bin/webpack.js
// 正常执行返回
process.exitCode = 0;    
// 运行某个命令                               
const runCommand = (command, args) => {...}
// 判断某个包是否安装
const isInstalled = packageName => {...}
// webpack可用的CLI:webpacl-cli和webpack-command
const CLIs = {...}
// 判断是否两个CLI是否安装了
const installedClis = CLIs.filter(cli=>cli.installed);
// 根据安装数量进行处理
if (installedClis.length === 0) {...} else if 
 (installedClis.length === 1) {...} else {...}

После запуска Webpack в итоге находитwebpack-cli /webpack-commandпакет npm и выполняет CLI.

webpack-cli

После выяснения файла входа, который запускает Webpack, давайте обратим внимание на webpack-cli и посмотрим, что он делает.

  • Введите yargs для настройки командной строки
  • Анализ параметров командной строки, преобразование каждого параметра и формирование элементов конфигурации компиляции
  • Ссылка на веб-пакет, компиляция и сборка в соответствии с элементами конфигурации

webpack-cliКоманды, которые не нужно компилировать, обрабатываются.

// node_modules/webpack-cli/bin/cli.js
const {NON_COMPILATION_ARGS} = require("./utils/constants");
const NON_COMPILATION_CMD = process.argv.find(arg => {
    if (arg === "serve") {
        global.process.argv = global.process.argv.filter(a => a !== "serve");
        process.argv = global.process.argv;
    }
    return NON_COMPILATION_ARGS.find(a => a === arg);
});
if (NON_COMPILATION_CMD) {
    return require("./utils/prompt-command")(NON_COMPILATION_CMD,...process.argv);
}

webpack-cliПредоставленные команды, которые не требуют компиляции, следующие.

// node_modules/webpack-cli/bin/untils/constants.js
const NON_COMPILATION_ARGS = [
    "init",                 // 创建一份webpack配置文件
    "migrate",              // 进行webpack版本迁移
    "add",                  // 往webpack配置文件中增加属性
    "remove",               // 往webpack配置文件中删除属性
    "serve",                // 运行webpack-serve
    "generate-loader",      // 生成webpack loader代码
    "generate-plugin",      // 生成webpack plugin代码
    "info"                  // 返回与本地环境相关的一些信息
];

webpack-cli использует набор инструментов командной строки yargs.

// node_modules/webpack-cli/bin/config/config-yargs.js
const {
    CONFIG_GROUP,
    BASIC_GROUP,
    MODULE_GROUP,
    OUTPUT_GROUP,
    ADVANCED_GROUP,
    RESOLVE_GROUP,
    OPTIMIZE_GROUP,
    DISPLAY_GROUP
} = GROUPS;

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

Помимо этого, вернемся кnode_modules/webpack/lib/webpack.jsДавайте посмотрим, какие приготовления сделал Webpack.

// node_modules/webpack/lib/webpack.js
const webpack = (options, callback) => {
    ...
    options = new WebpackOptionsDefaulter().process(options);
    compiler = new Compiler(options.context);
    new NodeEnvironmentPlugin().apply(compiler);
    ...
    compiler.options = new WebpackOptionsApply().process(options, compiler);
    ...
    webpack.WebpackOptionsDefaulter = WebpackOptionsDefaulter;
    webpack.WebpackOptionsApply = WebpackOptionsApply;
    ...
    webpack.NodeEnvironmentPlugin = NodeEnvironmentPlugin;
}

Функция WebpackOptionsDefaulter состоит в том, чтобы установить некоторые параметры по умолчанию (много кода, который не будет опубликован, вы можете проверить это самостоятельно).node_modules/webpack/lib/WebpackOptionsDefaulter.js).

// node_modules/webpack/lib/node/NodeEnvironmentPlugin.js
class NodeEnvironmentPlugin {
  apply(compiler) {
      ...		
      compiler.hooks.beforeRun.tap("NodeEnvironmentPlugin", compiler => {
	  if (compiler.inputFileSystem === inputFileSystem) inputFileSystem.purge();
      });
  }
}

Из приведенного выше кода мы можем знать, чтоNodeEnvironmentPluginПлагин прослушивает хук beforeRun, и его роль заключается в очистке кеша.

WebpackOptionsApply

WebpackOptionsApplyПреобразует все параметры параметров конфигурации во внутренние плагины веб-пакета.

Использовать список плагинов по умолчанию

  • output.library -> LibraryTemplatePlugin
  • externals -> ExternalsPlugin
  • devtool -> EvalDevtoolModulePlugin, SourceMapDevToolPlugin
  • AMDPlugin, CommonJsPlugin
  • RemoveEmptyChunksPlugin
// node_modules/webpack/lib/WebpackOptionsApply.js
new EntryOptionPlugin().apply(compiler);
compiler.hooks.entryOption.call(options.context, options.entry);

На самом деле плагины со временем станутcompilerэкземпляр на объекте.

EntryOptionPlugin

Далее вводимEntryOptionPluginплагин, чтобы увидеть, что он делает.

// node_modules/webpack/lib/EntryOptionPlugin.js
module.exports = class EntryOptionPlugin {
    apply(compiler) {
        compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => {
	    if (typeof entry === "string" || Array.isArray(entry)) {
	        itemToPlugin(context, entry, "main").apply(compiler);
	    } else if (typeof entry === "object") {
	        for (const name of Object.keys(entry)) {
		    itemToPlugin(context, entry[name], name).apply(compiler);
	        }
	    } else if (typeof entry === "function") {
	        new DynamicEntryPlugin(context, entry).apply(compiler);
	    }
	    return true;
	});
    }
};

Если это массив, он преобразуется в несколько записей для обработки, а если это объект, он преобразуется в одну запись для обработки.

Как показано в приведенном выше коде.

Экземпляр компилятора находится вnode_modules/webpack/lib/webpack.jsзавершено в. пройти черезEntryOptionPluginПлагин выполняет проверку параметров. пройти черезWebpackOptionsDefaulterОбъедините входящие параметры и параметры по умолчанию в новые параметры, создайте компилятор и соответствующие плагины и, наконец, передайтеWebpackOptionsApplyПреобразуйте все параметры параметров конфигурации во внутренние плагины Webpack.

Не волнуйтесь, это еще не конец.

приходите к намnode_modules/webpack/lib/webpack.jsсередина

if (options.watch === true || (Array.isArray(options) && options.some(o => o.watch))) {
  const watchOptions = Array.isArray(options)
	? options.map(o => o.watchOptions || {})
	: options.watchOptions || {};
	return compiler.watch(watchOptions, callback);
}
compiler.run(callback);

После компилятора экземпляра он будет судить, запущен ли он в соответствии с просмотром параметров.watch, если часы запущены, они будут называтьсяcompiler.watchдля мониторинга файлов сборки, в противном случае запуститеcompiler.runдля создания файла.

Скомпилировать и построить

compile

первый экземплярNormalModuleFactoryа такжеContextModuleFactory. затем войдите вrunметод.

// node_modules/webpack/lib/Compiler.js
run(callback) { 
    ...
    // beforeRun 如上文NodeEnvironmentPlugin插件清除缓存
    this.hooks.beforeRun.callAsync(this, err => {
        if (err) return finalCallback(err);
        // 执行run Hook开始编译
        this.hooks.run.callAsync(this, err => {
            if (err) return finalCallback(err);
            this.readRecords(err => {
                if (err) return finalCallback(err);
                // 执行compile
                this.compile(onCompiled);
            });
        });
    });
}

в исполненииthis.hooks.compileбудет выполняться доthis.hooks.beforeCompile, для выполнения плагинов, которые необходимо обработать перед компиляцией. Незамедлительно послеthis.hooks.compileбудет создан после выполненияCompilationобъект.

// node_modules/webpack/lib/compiler.js
compile(callback) {
    const params = this.newCompilationParams();
    this.hooks.beforeCompile.callAsync(params, err => {
	if (err) return callback(err);
	// 进入compile阶段
	this.hooks.compile.call(params);
	const compilation = this.newCompilation(params);
	// 进入make阶段
	this.hooks.make.callAsync(compilation, err => {
	    if (err) return callback(err);
	    compilation.finish(err => {
		if (err) return callback(err);
		// 进入seal阶段
		compilation.seal(err => {
		    if (err) return callback(err);
		    this.hooks.afterCompile.callAsync(compilation, err => {
			if (err) return callback(err);
			return callback(null, compilation);
		    })
 		})
	    })
	})
    })
}

make

новенькийCompilationПосле завершения создания файл будет прочитан из Entry, и файл будет скомпилирован в соответствии с типом файла и настроенным загрузчиком.После завершения компиляции будет найден файл, от которого зависит файл, и файл компилируется и анализируется рекурсивно.

Давайте посмотрим, где прослушивается хук make.

Как показано в комментариях в коде,addEntry是make构建阶段真正开始的标志.

// node_modules/webpack/lib/SingleEntryPlugin.js
compiler.hooks.make.tapAsync(
    "SingleEntryPlugin",
    (compilation, callback) => {
	const { entry, name, context } = this;
	cosnt dep = SingleEntryPlugin.createDependency(entry, name);
	// make构建阶段开始标志 
	compilation.addEntry(context, dep, name, callback);
    }
)

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

// node_modules/webpack/lib/Compilation.js
// addEntry -> addModuleChain
_addModuleChain(context, dependency, onModule, callback) {
...
this.buildModule(module, false, null, null, err => {
	...
})
...
}

Срабатывает, если сборка модуля завершенаfinishModules.

// node_modules/webpack/lib/Compilation.js
finish(callback) {
    const modules = this.modules;
    this.hooks.finishModules.callAsync(modules, err => {
        if (err) return callback(err);
	for (let index = 0; index < modules.length; index++) {
	    const module = modules[index];			
            this.reportDependencyErrorsAndWarnings(module, [module]);
        }
        callback();
    })
}

Module

Модуль включаетNormalModule(普通模块),ContextModule(./src/a ./src/b),ExternalModule(module.exports=jQuery),DelegatedModule(manifest)так же какMultiModule(entry:['a', 'b']).

В этой статье в качестве примера используется NormalModule (обычный модуль), посмотрите на конструкцию (Compilation) процесс.

  • Запуск погрузчиков с загрузчиком-бегуном
  • После преобразования загрузчика используйте acorn для синтаксического анализа и генерации AST.
  • Добавьте зависимости с помощью ParserPlugins

loader-runner

// node_modules/webpack/lib/NormalModule.js
const { getContext, runLoaders } = require("loader-runner");
doBuild(){
    ...
    runLoaders(
        ...
    )
    ...
}
...
try {
    const result = this.parser.parse()
}

doBuildОн загрузит ресурсы, а doBuild передаст путь к ресурсу и ресурсы подключаемого модуля для вызоваloader-runnerМетод плагина runLoaders используется для загрузки и выполнения загрузчиков.

acorn

// node_modules/webpack/lib/Parser.js
const acorn = require("acorn");

использоватьacornПроанализируйте преобразованный контент и выведите соответствующее абстрактное синтаксическое дерево (AST).

// node_modules/webpack/lib/Compilation.js
this.hooks.buildModule.call(module);
...
if (error) {
    this.hooks.failedModule.call(module, error);
    return callback(error);
}
this.hooks.succeedModule.call(module);
return callback();

Триггер успехаsucceedModule, срабатывает при сбоеfailedModule.

Наконец, продукты, созданные на вышеуказанных этапах, хранятся вCompilation.js的this.modules = [];начальство.

После этого пришло время запечатать.

Вот дополнительное введение в алгоритм, сгенерированный Chunk.

Алгоритм генерации чанка

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

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

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

4. Повторяйте описанный выше процесс, пока не будут получены все фрагменты.

seal

Все модули и зависимые от них модули конвертируются Loader, и Chunk генерируется в соответствии с зависимостями.

sealНа этапе также проделана большая работа по оптимизации, создание хэшей и генерация контента (createModuleAssets).

// node_modules/webpack/lib/Compilation.js
this.createHash();
this.modifyHash();
this.createModuleAssets();
// node_modules/webpack/lib/Compilation.js
createModuleAssets(){
    for (let i = 0; i < this.modules.length; i++) {
	const module = this.modules[i];
	if (module.buildInfo.assets) {
	    for (const assetName of Object.keys(module.buildInfo.assets)) {
		const fileName = this.getPath(assetName);
		this.assets[fileName] = module.buildInfo.assets[assetName];
		this.hooks.moduleAsset.call(module, fileName);
	    }
	}
    }
}

Стадия уплотнения претерпела множество оптимизаций, таких какtree shakingВыполняется на данном этапе. Окончательный код будет сохранен вCompilationизassetsхарактеристики.

emit

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

// node_modules/webpack/lib/compiler.js
this.hooks.emit.callAsync(compilation, err => {
    if (err) return callback(err);
    outputPath = compilation.getPath(this.outputPath);
    this.outputFileSystem.mkdirp(outputPath, emitFiles);
})

Реализовать простой Webpack

Чтобы лучше понять весь процесс Webpack, мы можем реализовать простой Webpack.

Простой исходный адрес WebpackАдрес склада, добро пожаловать, звезда~

Суммировать

Webpack объединяет параметры конфигурации, параметры командной строки и параметры по умолчанию на этапе запуска и инициализирует плагины. После завершения работы по инициализации вызовите компиляторrunЗапустите процесс компиляции и сборки Webpack.Основной процесс сборки включает в себя:compile,make,build,seal,emitи так далее.

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

Ссылаться на

❤️Любовное тройное комбо

1. Когда увидишь это, ставь лайк и поддержи, твой лайк - движущая сила моего творчества.

2. Подпишитесь на официальный аккаунт前端食堂,Ваша передняя столовая, не забывайте есть вовремя!

3. Фоновый ответБлагосостояние, вы можете получить много учебных материалов.