Webpack的执行流程思想很是简单,从入口文件开始,递归地查找文件的依赖,最终将全部依赖输出到一个文件中。在这过程当中又穿插了文件解析、输出优化等复杂的操做,咱们就从最简单的入口开始,逐渐剥茧抽丝,拨开webpack的神秘面纱。webpack
配置是衡量一个系统灵活性的主要标识,咱们在使用某个系统前都会先去查看提供的配置项,能够快速告诉咱们这个系统能够用什么样的方式运行。在深刻理解源码前,咱们有必要先熟悉它提供的配置,一是能够提早预习并大概想象它的实现思路,二是能够在研究源码时更好地追踪代码的起源。web
因为Webpack提供了强大的灵活性,它的配置也很是复杂,许多人刚开始就挂在了它的配置上,好在入口的配置还不算复杂,主要有如下两部分:数组
context
的中文意思是上下文
,上下文在不一样的环境下有不一样含义,在webpack中的配置项中有一项context
,它的意思是解析入口文件时的基准目录,会从该目录下查找入口文件,默认为执行编译命令时的绝对路径。app
一般状况下咱们不须要设置,可是若是咱们在其余目录执行webpack就会找不到路径,因此咱们最好给他设置一个默认值:异步
context: path.resolve(__dirname, "src")
复制代码
entry
就是咱们配置入口的地方了,支持多种配置类型,一般咱们只须要用到string
类型参数。若是使用object
配置,则会输出多份以它们的key为名称的文件。另外还能使用动态配置,入口能够支持异步获取:函数
// string
entry: "./src/entry"
// array
entry: ["./src/entry1", "./src/entry2"]
// object
entry: {
a: "./src/entry-a",
b: ["./src/entry-b1", "./src/entry-b2"]
}
// 动态
entry: () => './demo'
// 动态
entry: () => new Promise((resolve) => resolve(['./demo', './demo2']))
复制代码
因为Wepack的插件机制,致使了实现方式略显复杂,入口流程基本围绕如下几点进行:优化
EntryOptionPlugin
,SingleEntryPlugin
、MultiEntryPlugin
、DynamicEntryPlugin
NormalModuleFactory
解析单入口依赖,MultiModuleFactory
解析多入口依赖SingleEntryDependency
、MultiEntryDependency
NormalModule
及MultiModule
Webpack启动后会先预处理配置文件,预处理后便会交由WebpackOptionsApply
来根据配置注册各类插件,其中就会涉及到入口配置的处理插件EntryOptionPlugin
。EntryOptionPlugin
顾名思义就是处理入口配置的插件
,在这里将处理entry
的几种不一样配置方法:ui
// EntryOptionPlugin.js
function apply(context, entry) {
if (typeof entry === "string") {
// 默认输出文件名为 main
new SingleEntryPlugin(context, entry, 'main')
} else if(Array.isArray(entry)) {
new MultiEntryPlugin(context, entry, 'main')
} else if (typeof entry === "object") {
for (const name of Object.keys(entry)) {
if (typeof entry[name] === "string") {
// 默认输出文件名为 key
new SingleEntryPlugin(context, entry[name], name)
} else if(Array.isArray(entry[name])) {
new MultiEntryPlugin(context, entry[name], name)
}
}
} else if (typeof entry === "function") {
new DynamicEntryPlugin(context, entry)
}
})
复制代码
能够看到代码仍是很简洁,得益于Webpack的插件机制,这里很是方便地将不一样类型的配置分配给不一样的插件来处理。固然虽然代码会稍显啰嗦,可是带来的优势很是明显,职责分工明确,并且灵活性和扩展性都很是好。this
SingleEntryPlugin
用于处理string
类型的配置,如entry: "./src/entry"
及entry: { a: "./src/entry" }
。在这里一共作了两件事,一是注册单入口依赖处理器,二是建立单入口依赖并执行解析。spa
依赖是Webpack里一个重要概念,用于描述模块间的关系,每一个依赖都有对应的处理器对其解析。这里咱们只要知道单入口依赖SingleEntryDependency
是由NormalModuleFactory
进行解析,且他们之间的关系是在这里进行描述:
// SingleEntryPlugin.js
// 在compiler.hooks.compilation阶段注册,这个钩子会在建立Compilation后调用
compiler.hooks.compilation.tap(
(compilation, { normalModuleFactory }) => {
// dependencyFactories维护了依赖与解析依赖方法的关系
compilation.dependencyFactories.set(
SingleEntryDependency,
normalModuleFactory
);
}
);
复制代码
接着注册了建立依赖的方法,这里将是真正将入口文件转换称为依赖对象,接着将其添加到compilation
中正式开始解析,从这里开始就是入口和编译器的枢纽:
// SingleEntryPlugin.js
// compiler.hooks.make 钩子在开始执行编译时调用
compiler.hooks.make.tapAsync(
(compilation, callback) => {
const { entry, name, context } = this;
const dep = new SingleEntryDependency(entry);
dep.loc = { name };
compilation.addEntry(context, dep, name, callback);
}
);
复制代码
多入口的流程和单入口差很少,不一样的是这里要注册多入口和单入口两种处理器,能够从建立依赖对象中看到,一个多入口依赖里包含了多个单入口依赖:
// MultiEntryPlugin.js
compiler.hooks.compilation.tap(
(compilation, { normalModuleFactory }) => {
const multiModuleFactory = new MultiModuleFactory();
compilation.dependencyFactories.set(
MultiEntryDependency,
multiModuleFactory
);
compilation.dependencyFactories.set(
SingleEntryDependency,
normalModuleFactory
);
}
);
compiler.hooks.make.tapAsync(
(compilation, callback) => {
const { context, entries, name } = this;
const dep = new MultiEntryDependency(
entries.map((e => new SingleEntryDependency(e)),
name
));
compilation.addEntry(context, dep, name, callback);
}
);
复制代码
动态入口配置也很简单,就是在执行完配置函数后,根据执行结果转换成单入口依赖或多入口依赖:
// DynamicEntryPlugin.js
compiler.hooks.make.tapAsync(
(compilation, callback) => {
const addEntry = (entry, name) => {
const dep = Array.isArray(entry) ?
MultiEntryPlugin.createDependency(entry, name) :
SingleEntryPlugin.createDependency(entry, name);
return new Promise((resolve, reject) => {
compilation.addEntry(this.context, dep, name, err => {
if (err) return reject(err);
resolve();
});
});
};
Promise.resolve(this.entry()).then(entry => {
addEntry(entry, "main").then(() => callback(), callback);
});
}
);
复制代码
通过上面处理后,不一样的入口配置就转换成为了依赖
,接下来就开始编译器Compilation
的工做。Compilation
的做用就是递归解析依赖,从而获取全部须要打包的文件,因此它的工做原理基本上就是添加依赖 -> 解析依赖模块 -> 获得该模块的其余依赖 -> 添加依赖
这么一个循环。Compilation
中添加入口依赖的函数就是addEntry
。
函数首先将入口依赖添加到_preparedEntrypoints
中,这个数组在输出文件时使用,往数组添加几个入口依赖,就输出几个文件,输出的代码咱们在后面文章分析:
SingleEntryDependency
,输出一个文件MultiEntryDependency
,输出一个文件// Compilation.js
function addEntry(context, entry, name, callback) {
const slot = {
name: name,
request: entry.request,
module: null
};
this._preparedEntrypoints.push(slot);
this._addModuleChain(context, entry,
(module) => { this.entries.push(module); },
(err, module) => {
return callback(null, module);
}
);
}
复制代码
接着调用真正解析依赖的方法_addModuleChain
,这段代码比较复杂,咱们能够先忽略其中细节,看其中最重要的解析方法。
首先经过dependencyFactories
拿到依赖对应的解析器,前面配置的依赖处理在这里派上用场了,因此若是依赖是SingleEntryDependency
,这里的moduleFactory
拿到的就是前面注册的NormalModuleFactory
。NormalModuleFactory
的执行原理咱们后面再讲,如今只要知道它的做用是将依赖就转换为模块,最后就是构建模块而后递归处理依赖。
// Compilation.js
function _addModuleChain(context, dependency, onModule, callback) {
const Dep = /** @type {DepConstructor} */ (dependency.constructor);
const moduleFactory = this.dependencyFactories.get(Dep);
moduleFactory.create(
{
contextInfo: {
issuer: "",
compiler: this.compiler.name
},
context: context,
dependencies: [dependency]
},
(err, module) => {
// ...
// 构建模块
this.buildModule(module, false, null, null, err => {
// 处理入口模块的依赖
if (addModuleResult.dependencies) {
this.processModuleDependencies(module, err => {
if (err) return callback(err);
callback(null, module);
});
} else {
return callback(null, module);
}
});
}
);
}
复制代码
因为Webpack的插件模式使代码跳跃性比较大,下面咱们使用同步的伪代码来看数组形式的参数整个的入口运做流程,将上面的内容串起来:
function compile() {
// webpack.js 解析参数
const context = path.resolve(__dirname)
const entry = ['./src/foo', './src/bar']
const name = 'main'
// Compiler.js 建立编译器
const compilation = new Compilation();
const normalModuleFactory = new NormalModuleFactory();
const multiModuleFactory = new MultiModuleFactory();
// MultiEntryPlugin.js 注册解析器/建立依赖
compilation.dependencyFactories.set(MultiEntryDependency, multiModuleFactory)
compilation.dependencyFactories.set(SingleEntryDependency, normalModuleFactory)
const dep = new MultiEntryDependency(
entries.map((e => new SingleEntryDependency(e)),
name
));
// Compilation.js 解析依赖
const multiModuleFactory = compilation.dependencyFactories.get(dep)
// 解析出模块
const multiModule = multiModuleFactory.create(context, dependency)
// 构建模块
multiModule.buildModule()
// 处理入口模块的依赖
multiModule.dependencies.forEach((singleEntryDependency) => {
const normalModuleFactory = compilation.dependencyFactories.get(singleEntryDependency)
// 解析出模块
const normalModule = normalModuleFactory.create(context, dependency)
normalModule.buildModule()
// 继续循环解析依赖...
normalModule..dependencies.forEach(...)
})
// 构建完成打包输出...
}
复制代码