предисловие
О предыдущей статьете... (1)Многие друзья отметили, что это было слишком просто. Я был тронут до слез здесь. (Мой уровень ограничен, но я все равно приветствую вас.)
Question 2:
webpack
Каков процесс компиляции?
Интервьюер, вероятно, спросит вас об этом:
- Как много знает вебпак?
- Вы понимаете принцип компиляции webpack?
- Вы написали плагин для веб-пакета?
- Перечислить узлы ловушек в процессе компиляции веб-пакета
По сути, все эти вопросы можно рассматривать как один и тот же вопрос, то есть интервьюер спрашивает вас: выwebpack
Как много вы знаете о процессе компиляции?
Подводя итог ответам, которые я услышал, попробуйте точно воспроизвести то, что кандидат сказал во время интервью.
答案1:
Webpack предназначен для загрузки ресурсов через загрузчик, изменения с помощью плагинов и, наконец, упаковки и создания пакетов.js
答案2:
Работающий процесс Webpack является последовательным процессом, от начала до конца последовательно выполняются следующие процессы:
1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;
2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译;
3. 确定入口: 根据配置中的 entry 找出所有的入口文件;
4. 编译模块: 从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模
块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
5. 完成模块编译: 在经过第 4 步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内
容以及它们之间的依赖关系;
6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个
Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系
统。
Ух ты! Звучит как набор, будьте готовы! Откуда взялась эта книга? В это время осмелитесь ответить так, должно быть, это место смерти. Потому что интервьюер обязательно сгенерирует серию вопросов, которые заставят вас плакать. Например:
- Что такое выходной список, который вы только что сказали?
- 其实是在问chunks是挂在哪个对象上。
- Как обрабатываются входные модули?
- 其实在问如何生成入口模块的
- Как вы окончательно записали содержимое файла в файловую систему?
- 其实是在问compiler是如何具备文件读写能力的
- Когда начинается этап сборки?
- 其实是在问webpack开始构建之前做了哪些构建准备
- Как вы вызываете Loader для перевода модулей?
- 其实是在问在哪个阶段处理loader的
- ...
Итак, вы чувствуете, что вот-вот заплачете? Могу я сказать вам мой ответ?
Интервьюирование на самом деле является запутанным искусством.webpack
Есть так много вещей, которые невозможно объяснить в трех-двух предложениях. Поэтому над тем, как потратить как можно меньше времени на максимально понятное объяснение содержания, нужно тщательно подумать.
答案3:
Сначала проясните ключевые моменты, вид с исходным кодом, и чем нагляднее, тем лучше.Потратьте 30 секунд, чтобы закончить следующие 1-4 пункта, в конце концов, это для интервьюера.
- параметры инициализации,
webpack.config.js
изmodule.export
, в сочетании с параметрами по умолчанию,merge
окончательные параметры. - Начать компиляцию, создать экземпляр, инициализировав параметры
Compiler
объект, загружает все настроенные плагины и выполняет метод запуска объекта. - Подтвердите входной файл.
- Скомпилируйте модуль: начиная с входного файла, вызовите все настроенные загрузчики для загрузки модуля, а затем узнайте модули, от которых зависит модуль. Повторяя этот процесс до тех пор, пока не будут обработаны все входные файлы, получается строка зависимости.
-
webpack.js
Основная операцияrequire
охватыватьnode_modules/webpack-cli/bin/cli.js
-
cli.js
- 01 Текущий файл обычно имеет две операции, параметры обработки и параметры передачи в другую логику (дело распространения)
- 02
options
(параметры инициализации) - 03
complier
(Создать экземплярCompiler
объект) - 04
complier.run
( Этоrun
Что внутри, увидим позже)
- Создайте экземпляр объекта компилятора, и компилятор выполнит весь рабочий процесс веб-пакета.
-
complier
наследоватьTabable
,такcomplier
Имеет возможность манипулировать крючками. такие как прослушивание, запуск событий иwebpack
является потоком событий. - примеры слов
complier
При создании объекта на него будет смонтировано множество свойств. вNodeEnvironmentPlugin让complier
Возможность чтения и записи файлов. - Буду
plugins
Все плагины смонтированы наcpmplier
тело - внутренний по умолчанию
plugins
а такжеcomplier
установить связи, одна из которыхEntryOptionsPlugin
входной модуль ручкиid
- существует
webpack/lib/SingleEntryPlugin.js
внутри,compiler
слушалmake
крюк- существует
singleEntryPlugin.js
В методе применения модуля есть два прослушивателя ловушек. dep = SingleEntryPlugin.createDependency(entry,name)
- в
compilation
Крюк, чтобы позволитьcompilation
использоватьnormalModuleFactory
Возможность фабрики создать нормальный модуль. потому чтоcompilation
Это использование модулей, созданных вами, для загрузки модулей, которые необходимо упаковать. - в
make
зацепитьCompiler.run
Он будет вызван при выполнении пакета, и здесь он означает, что все приготовления для модуля, который будет упакован, завершены. - потом
Compilation
передачаaddEntry
это отмечаетmake
Начинается этап сборки.
- существует
-
run
выполнение метода- Я просто сказал, что выполнение метода Compiler.run вызовет хук make, а в методе run есть куча хуков, которые срабатывают последовательно, например
beforeRun
,run
,compile
- Выполнение метода компиляции
- Сначала подготовьте некоторые параметры, такие как только что упомянутый
normalModuleFactory
, для последующего создания модуля. - вызывать
beforeCompile
- Передайте подготовленный параметр в метод (
newCompilation
) для создания компиляции. существуетnewCompilation
Внутренне сначала позвонитеcreateCompilation
, затем срабатываетthis.compilation
крючок иcompilation
мониторинг крючков
- Сначала подготовьте некоторые параметры, такие как только что упомянутый
- созданный
compilation
Объект срабатывает послеmake
крюк. при срабатыванииmake
Когда крючок слушает, он будетcompilation
переданный объект.compilation.addEntry
Это означает, что начинается фаза make build. -
make
Хук сработал, полученcompilation
объект, то изcompilation
Три значения могут быть деконструированы.entry
: относительный путь к текущему упакованному модулю,name
,context
: путь текущего проекта -
processDependencies
Обработка зависимостей между модулями. пройти через функциюasync.forEach
для рекурсивного создания каждого загруженного модуля. -
compilation
передачаaddEntry
метод, который внутренне вызывает метод _addModuleChain для обработки зависимостей. -
compilation
через который можно пройтиnormalModuleFactory
Фабрика для создания обычного объекта модуля.webpack
Внутренне включено по умолчанию100
Параллельные операции упаковки То, что вы видите в исходном коде,normalModuleFactory.create
такой метод. - затем в
beforeResolve
метод вызоветfactory
Крюк слушателя. После завершения вышеуказанных операцийfactory
Получите функцию и вызовите ее. В функции есть еще одинresolver
крючок срабатывает,resolver
По сути, обработкаloader
.при срабатыванииresolver
крючок, значит всеLoader
Обработанный. - сработает следующий
afterResolve
Этот хук вызываетnew NormalModule
- Наконец, позвоните
buildModule
метод начинает компилироваться -> вызовbuild
-> позвонитьdoBuild。bulid
в процессе будетjs
код вast
синтаксическое дерево, если текущееjs
Если модуль ссылается на другие модули, его необходимо повторять рекурсивно.bulid
. В настоящее время все входные модули хранятся вcompilation
объектentries
в массиве. - Тогда мне также нужен текущий модуль
ast
Синтаксическое дерево модифицируется несколько раз, а затем преобразуется обратно вjs
код. Например, будетrequire
Конвертировано в__webpack_require__
- наконец
compile
последний вызванный методcompilation.seal
метод обработкиchunk
. Сгенерируйте содержимое кода и, наконец, выведите файл по указанному пути упаковки.
- Я просто сказал, что выполнение метода Compiler.run вызовет хук make, а в методе run есть куча хуков, которые срабатывают последовательно, например
-
-
Фу. Ты плакал?Не будьте настолько подробны во время интервью, потому что у интервьюера нет такого уровня детализации. Итак, если вы хотите поговорить, разберитесь, что вы считаете важным.
Далее переходим непосредственно к коду, процесс компиляции webpack будем реализовывать сами
- run.js
let webpack = require('./youWebpack')
let options = require('./webpack.config')
let compiler = webpack(options); // webpack 初始化 webpack.config.js 的 module.exports
// 执行run方法
compiler.run((err, stats) => {
console.log(err)
console.log(stats)
})
- Поскольку метод запуска требует
youWebpack
, то вы должны написать это.
/*
* youWebpack.js
*/
const Compiler = require('./Compiler');
const NodeEnvironmentPlugin = require('./NodeEnvironmentPlugin')
const WebpackOptionsApply = require('./WebpackOptionsApply')
const webpack = function (options) {
// 01 实例化 compiler 对象
let compiler = new Compiler(options.context)
compiler.options = options
// 02 初始化 NodeEnvironmentPlugin(让compiler具体文件读写能力)
new NodeEnvironmentPlugin().apply(compiler)
// 03 挂载所有 plugins 插件至 compiler 对象身上
if (options.plugins && Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
plugin.apply(compiler)
}
}
// 04 挂载所有 webpack 内置的插件(入口)
new WebpackOptionsApply().process(options, compiler);
// 05 返回 compiler 对象即可
return compiler
}
module.exports = webpack
- ты в порядке,
youWebpack
Требуются Compiler, NodeEnvironmentPlugin, WebpackOptionsApply. Ни за что, продолжайте писать.
/*
* WebpackOptionsApply.js
*/
const EntryOptionPlugin = require("./EntryOptionPlugin")
class WebpackOptionsApply {
process(options, compiler) {
new EntryOptionPlugin().apply(compiler)
compiler.hooks.entryOption.call(options.context, options.entry)
}
}
module.exports = WebpackOptionsApply
/*
* NodeEnvironmentPlugin.js
*/
const fs = require('fs'); // webpack为提升文件读写性能, 源码里是对 node 的 fs 模块进行了二次封装的。我们这勉强够用,就不封装了。 /捂脸
class NodeEnvironmentPlugin {
constructor(options) {
this.options = options || {}
}
apply(complier) {
complier.inputFileSystem = fs
complier.outputFileSystem = fs
}
}
module.exports = NodeEnvironmentPlugin
Compiler, один из основных веб-пакетовCompilerприходящий.
/*
* Compiler
*/
const {
Tapable,
SyncHook,
SyncBailHook,
AsyncSeriesHook,
AsyncParallelHook
} = require('tapable')
const path = require('path')
const mkdirp = require('mkdirp')
const Stats = require('./Stats')
const NormalModuleFactory = require('./NormalModuleFactory')
const Compilation = require('./Compilation')
const { emit } = require('process')
class Compiler extends Tapable {
constructor(context) {
super()
this.context = context
this.hooks = {
done: new AsyncSeriesHook(["stats"]),
entryOption: new SyncBailHook(["context", "entry"]),
beforeRun: new AsyncSeriesHook(["compiler"]),
run: new AsyncSeriesHook(["compiler"]),
thisCompilation: new SyncHook(["compilation", "params"]),
compilation: new SyncHook(["compilation", "params"]),
beforeCompile: new AsyncSeriesHook(["params"]),
compile: new SyncHook(["params"]),
make: new AsyncParallelHook(["compilation"]),
afterCompile: new AsyncSeriesHook(["compilation"]),
emit: new AsyncSeriesHook(['compilation'])
}
}
emitAssets(compilation, callback) {
// 当前需要做的核心: 01 创建dist 02 在目录创建完成之后执行文件的写操作
// 01 定义一个工具方法用于执行文件的生成操作
const emitFlies = (err) => {
const assets = compilation.assets
let outputPath = this.options.output.path
for (let file in assets) {
let source = assets[file]
let targetPath = path.posix.join(outputPath, file)
this.outputFileSystem.writeFileSync(targetPath, source, 'utf8')
}
callback(err)
}
// 创建目录之后启动文件写入
this.hooks.emit.callAsync(compilation, (err) => {
mkdirp.sync(this.options.output.path)
emitFlies()
})
}
run(callback) {
console.log('run 方法执行了~~~~')
const finalCallback = function (err, stats) {
callback(err, stats)
}
const onCompiled = (err, compilation) => {
// 最终在这里将处理好的 chunk 写入到指定的文件然后输出至 dist
this.emitAssets(compilation, (err) => {
let stats = new Stats(compilation)
finalCallback(err, stats)
})
}
this.hooks.beforeRun.callAsync(this, (err) => {
this.hooks.run.callAsync(this, (err) => {
this.compile(onCompiled)
})
})
}
compile(callback) {
const params = this.newCompilationParams()
this.hooks.beforeRun.callAsync(params, (err) => {
this.hooks.compile.call(params)
const compilation = this.newCompilation(params)
this.hooks.make.callAsync(compilation, (err) => {
// console.log('make钩子监听触发了~~~~~')
// callback(err, compilation)
// 在这里我们开始处理 chunk
compilation.seal((err) => {
this.hooks.afterCompile.callAsync(compilation, (err) => {
callback(err, compilation)
})
})
})
})
}
newCompilationParams() {
const params = {
normalModuleFactory: new NormalModuleFactory()
}
return params
}
newCompilation(params) {
const compilation = this.createCompilation()
this.hooks.thisCompilation.call(compilation, params)
this.hooks.compilation.call(compilation, params)
return compilation
}
createCompilation() {
return new Compilation(this)
}
}
module.exports = Compiler
- Глядишь, ты должен сам это осознать
NormalModuleFactory
,Compilation
,Stats
/*
* Stats 其实看代码就能明白,Stats只是将compilation身上挂载的 入口模块、模块内容、chunks、文件目录等拿了出来。 这里可以回头看看run 方法
*/
class Stats {
constructor(compilation) {
this.entries = compilation.entries
this.modules = compilation.modules
this.chunks = compilation.chunks
this.files = compilation.files
}
toJson() {
return this
}
}
module.exports = Stats
/*
* NormalModuleFactory
*/
const NormalModule = require("./NormalModule");
class NormalModuleFactory {
create(data) {
return new NormalModule(data)
}
// 源码里头还实现了其它方法,所以这里不要嫌弃为什么又要单独require一个 NormalModule
}
module.exports = NormalModuleFactory
Один из основных веб-пакетовCompilation
, а что он делает? Пожалуйста, вернитесь к статистике, чтобы понять.
const ejs = require('ejs')
const Chunk = require('./Chunk')
const path = require('path')
const async = require('neo-async')
const Parser = require('./Parser')
const NormalModuleFactory = require('./NormalModuleFactory')
const { Tapable, SyncHook } = require('tapable')
// 实例化一个 normalModuleFactory parser
const normalModuleFactory = new NormalModuleFactory()
const parser = new Parser()
class Compilation extends Tapable {
constructor(compiler) {
super()
this.compiler = compiler
this.context = compiler.context
this.options = compiler.options
// 让 compilation 具备文件的读写能力
this.inputFileSystem = compiler.inputFileSystem
this.outputFileSystem = compiler.outputFileSystem
this.entries = [] // 存入所有入口模块的数组
this.modules = [] // 存放所有模块的数据
this.chunks = [] // 存放当前次打包过程中所产出的 chunk
this.assets = []
this.files = []
this.hooks = {
succeedModule: new SyncHook(['module']),
seal: new SyncHook(),
beforeChunks: new SyncHook(),
afterChunks: new SyncHook()
}
}
/**
* 完成模块编译操作
* @param {*} context 当前项目的根
* @param {*} entry 当前的入口的相对路径
* @param {*} name chunkName main
* @param {*} callback 回调
*/
addEntry(context, entry, name, callback) {
this._addModuleChain(context, entry, name, (err, module) => {
callback(err, module)
})
}
_addModuleChain(context, entry, name, callback) {
this.createModule({
parser,
name: name,
context: context,
rawRequest: entry,
resource: path.posix.join(context, entry),
moduleId: './' + path.posix.relative(context, path.posix.join(context, entry))
}, (entryModule) => {
this.entries.push(entryModule)
}, callback)
}
/**
* 定义一个创建模块的方法,达到复用的目的
* @param {*} data 创建模块时所需要的一些属性值
* @param {*} doAddEntry 可选参数,在加载入口模块的时候,将入口模块的id 写入 this.entries
* @param {*} callback
*/
createModule(data, doAddEntry, callback) {
let module = normalModuleFactory.create(data)
const afterBuild = (err, module) => {
// 在 afterBuild 当中我们就需要判断一下,当前次module 加载完成之后是否需要处理依赖加载
if (module.dependencies.length > 0) {
// 当前逻辑就表示module 有需要依赖加载的模块,因此我们可以再单独定义一个方法来实现
this.processDependencies(module, (err) => {
callback(err, module)
})
} else {
callback(err, module)
}
}
this.buildModule(module, afterBuild)
// 当我们完成了本次的 build 操作之后将 module 进行保存
doAddEntry && doAddEntry(module)
this.modules.push(module)
}
/**
* 完成具体的 build 行为
* @param {*} module 当前需要被编译的模块
* @param {*} callback
*/
buildModule(module, callback) {
module.build(this, (err) => {
// 如果代码走到这里就意味着当前 Module 的编译完成了
this.hooks.succeedModule.call(module)
callback(err, module)
})
}
processDependencies(module, callback) {
// 1 当前的函数核心功能就是实现一个被依赖模块的递归加载
// 2 加载模块的思想都是创建一个模块,然后想办法将被加载模块的内容拿进来?
// 3 当前我们不知道 module 需要依赖几个模块, 此时我们需要想办法让所有的被依赖的模块都加载完成之后再执行 callback?【 neo-async 】
let dependencies = module.dependencies
async.forEach(dependencies, (dependency, done) => {
this.createModule({
parser,
name: dependency.name,
context: dependency.context,
rawRequest: dependency.rawRequest,
moduleId: dependency.moduleId,
resource: dependency.resource
}, null, done)
}, callback)
}
seal(callback) {
this.hooks.seal.call()
this.hooks.beforeChunks.call()
// 1 当前所有的入口模块都被存放在了 compilation 对象的 entries 数组里
// 2 所谓封装 chunk 指的就是依据某个入口,然后找到它的所有依赖,将它们的源代码放在一起,之后再做合并
for (const entryModule of this.) {
// 核心: 创建模块加载已有模块的内容,同时记录模块信息
const chunk = new Chunk(entryModule)
// 保存 chunk 信息
this.chunks.push(chunk)
// 给 chunk 属性赋值
chunk.modules = this.modules.filter(module => module.name === chunk.name)
}
// chunk 流程梳理之后就进入到 chunk 代码处理环节(模板文件 + 模块中的源代码==》chunk.js)
this.hooks.afterChunks.call(this.chunks)
// 生成代码内容
this.createChunkAssets()
callback()
}
createChunkAssets() {
for (let i = 0; i < this.chunks.length; i++) {
const chunk = this.chunks[i]
const fileName = chunk.name + '.js'
chunk.files.push(fileName)
// 1 获取模板文件的路径
let tempPath = path.posix.join(__dirname, 'temp/main.ejs')
// 2 读取模块文件中的内容
let tempCode = this.inputFileSystem.readFileSync(tempPath, 'utf8')
// 3 获取渲染函数
let tempRender = ejs.compile(tempCode)
// 4 按ejs的语法渲染数据
let source = tempRender({
entryModuleId: chunk.entryModule.moduleId,
modules: chunk.modules
})
// 输出文件
this.emitAssets(fileName, source)
}
}
emitAssets(fileName, source) {
this.assets[fileName] = source
this.files.push(fileName)
}
}
module.exports = Compilation
Некоторые части не написаны. Как Парсер, Чанк.
На самом деле, это в основном потому, что я ленив, я слишком устал, чтобы писать, мне нужно идти на поиски пищи. Ладно, шучу. К этому моменту должен быть полностью понят весь процесс компиляции основного веб-пакета. .
Резюме
Каков процесс компиляции веб-пакета? Код должен быть очень четким.
step1: 实例化compiler
- создать экземпляр объекта компилятора
- Инициализировать NodeEnvironmentPlugin (позволить компилятору иметь определенные возможности чтения и записи файлов)
- Смонтировать все плагины на объекте компилятора
- Смонтировать все встроенные плагины webpack (запись)
step2: compiler.run
- this.hooks.beforeRun.callAsync -> this.hooks.run.callAsync -> this.compile
-
this.compile получает onCompiled
-
Содержимое onCompiled: Наконец, обработанный фрагмент записывается в указанный файл и выводится в dist (путь вывода файла, не обязательно dist)
-
step3: compile方法做的事情
- newCompilationParams, инициализируйте необходимые параметры объекта Compilation перед его созданием
- вызовите this.hooks.beforeRun.callAsync
- this.newCompilation(params) создает экземпляр объекта Compilation
- Триггеры this.hooks.make.callAsync выполняют мониторинг ловушек
- компиляция.seal начать обработку чанков
- this.hooks.afterCompile.callAsync(compilation,...)
- Процесс вступил в компиляцию. . .
step4: 完成模块编译操作
- addEntry
- _addModuleChain
- createModule: определить метод для создания модуля для повторного использования
- module = normalModuleFactory.create(data): создать обычный модуль для загрузки js-модулей.
- afterBuild
- this.processDependencies : Найдите зависимости между модулями и модулями.
- this.buildModule(module, afterBuild)
- module.build : Это означает, что компиляция текущего модуля завершена.
- createModule: определить метод для создания модуля для повторного использования
- _addModuleChain
- печать: генерировать содержимое кода и выходные файлы
Over.