Анализ процесса сборки webpack

Webpack

предисловие

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

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

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~

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