共计 11105 个字符,预计需要花费 28 分钟才能阅读完成。
文章首发于我的博客 https://github.com/mcuking/bl…
在开始剖析源码之前,笔者先把之前收集到的 webpack 构建流程图贴在上面。前面的剖析过程读者能够对照着这张图来进行了解。
构建筹备阶段
回顾后面的文章,在 webpack-cli 从新调用 webpack 包时,首先执行的就是 node_module/webpack/lib/webpack.js
中的函数。如下:
const webpack = (options, callback) => {
...
let compiler;
if (Array.isArray(options)) {
compiler = new MultiCompiler(Array.from(options).map(options => webpack(options))
);
} else if (typeof options === "object") {
// 查看传入的 options 并设置默认项
options = new WebpackOptionsDefaulter().process(options);
// 初始化一个 compiler 对象实例
compiler = new Compiler(options.context);
// 将 options 挂在到这个实例对象上
compiler.options = options;
// 清理构建的缓存
new NodeEnvironmentPlugin({infrastructureLogging: options.infrastructureLogging}).apply(compiler);
// 遍历 options.plugins 数组,将用户配置的 plugins 全副初始化
// 并将插件外部业务逻辑绑定到 Compiler 实例对象上,期待实例对象触发对应钩子后执行
// 请参考上篇剖析文章
if (options.plugins && Array.isArray(options.plugins)) {for (const plugin of options.plugins) {if (typeof plugin === "function") {plugin.call(compiler, compiler);
} else {plugin.apply(compiler);
}
}
}
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();
// 依据 options 中配置的参数,实例化外部插件并绑定到 compiler 实例对象上
// 例如 externals 有配置,则须要配置 ExternalsPlugins
compiler.options = new WebpackOptionsApply().process(options, compiler);
} else {throw new Error("Invalid argument: options");
}
...
if (callback) {
...
compiler.run(callback);
}
return compiler;
};
其中 WebpackOptionsDefaulter 这个类的作用就是检测 options 并设置默认配置项,其要害代码如下:
class WebpackOptionsDefaulter extends OptionsDefaulter {constructor() {super();
this.set("entry", "./src");
this.set("devtool", "make", options =>
options.mode === "development" ? "eval" : false
);
this.set("cache", "make", options => options.mode === "development");
this.set("context", process.cwd());
this.set("target", "web");
...
}
}
set 办法和 process 办法都是继承自父类 OptionsDefaulter,这里就不赘述了。
接着是初始化了一个 compiler 对象,并将解决好的 options 挂载到实例上。而后开始初始化 options.plugins 上的插件,将插件绑定到 compiler 实例对象上,如果 plugin 是函数,则间接调用;如果是对象,则调用对象上的 apply 办法。这也就为什么 webpack 的插件配置个别都是对象实例数组的起因,如下:
{plugins: [new HtmlWebpackPlugin()];
}
对于 webpack 插件机制的内容请参考上篇文章 Webpack 源码剖析(2)— Tapable 与 Webpack 的关联。
最初调用了一个名为 WebpackOptionsApply 的类,咱们看下其实现的局部代码:
class WebpackOptionsApply extends OptionsApply {constructor() {super();
}
process(options, compiler) {
...
new EntryOptionPlugin().apply(compiler);
compiler.hooks.entryOption.call(options.context, options.entry);
...
if (options.externals) {ExternalsPlugin = require("webpack/lib/ExternalsPlugin");
new ExternalsPlugin(
options.output.libraryTarget,
options.externals
).apply(compiler);
}
...
}
}
从中不难发现,WebpackOptionsApply 次要作用就是依据 options 中的设置,来挂载对应的插件到 compiler 实例对象上,例如如果设置了 externals,则挂载 ExternalsPlugin 插件。须要留神的是,有些插件是默认必须要挂载的,而不禁 options 中的设置决定,例如 EntryOptionPlugin 插件。
这里咱们正好能够到 EntryOptionPlugin 看下咱们在 options 里常常设置的参数 entry 到底反对几种类型,次要代码如下:
const itemToPlugin = (context, item, name) => {if (Array.isArray(item)) {return new MultiEntryPlugin(context, item, name);
}
return new SingleEntryPlugin(context, item, name);
};
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;
});
}
};
有下面代码咱们能够晓得 entry 能够是字符串、数组、对象和函数,其中当是数组时,则挂载 MultiEntryPlugin 插件,也就是说 webpack 会将多个文件打包成一个文件。而当是对象时,则遍历每个键值对,而后执行 itemToPlugin 办法,也就是说 webpack 会将对象中的每一项入口对应的文件别离打包成不同的文件,这个就对应到了咱们常说的多页面打包场景。
到这里是不是发现当看懂了源码,就会对之前死记硬背的 webpack 配置有了更深刻的了解了呢?其实这就是浏览源码的一个十分棒的益处。
接下来则是调用了 compiler 对象的 run 办法,那么咱们就回到 Compiler 文件中,进一步剖析 Compiler 中到底做了哪些事件。
模块构建(make)阶段
上面就是 Compiler 类的要害代码:
class Compiler extends Tapable {constructor(context) {super();
this.hooks = {
// 总共有 26 个钩子,上面列举的是比拟常见的
run: new AsyncSeriesHook(["compiler"]),
emit: new AsyncSeriesHook(["compilation"]),
compilation: new SyncHook(["compilation", "params"]),
compile: new SyncHook(["params"]),
make: new AsyncParallelHook(["compilation"]),
...
},
...
}
watch(watchOptions, handler) { }
run(callback) {const onCompiled = (err, compilation) => {...};
this.hooks.beforeRun.callAsync(this, err => {if (err) return finalCallback(err);
this.hooks.run.callAsync(this, err => {if (err) return finalCallback(err);
this.readRecords(err => {if (err) return finalCallback(err);
this.compile(onCompiled);
});
});
});
}
...
emitAssets(compilation, callback) { }
...
createCompilation() {return new Compilation(this);
}
newCompilation(params) {const compilation = this.createCompilation();
compilation.fileTimestamps = this.fileTimestamps;
compilation.contextTimestamps = this.contextTimestamps;
compilation.name = this.name;
compilation.records = this.records;
compilation.compilationDependencies = params.compilationDependencies;
this.hooks.thisCompilation.call(compilation, params);
this.hooks.compilation.call(compilation, params);
return compilation;
}
createNormalModuleFactory() {
const normalModuleFactory = new NormalModuleFactory(
this.options.context,
this.resolverFactory,
this.options.module || {});
this.hooks.normalModuleFactory.call(normalModuleFactory);
return normalModuleFactory;
}
newCompilationParams() {
const params = {normalModuleFactory: this.createNormalModuleFactory(),
contextModuleFactory: this.createContextModuleFactory(),
compilationDependencies: new Set()};
return params;
}
...
compile(callback) {const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => {if (err) return callback(err);
this.hooks.compile.call(params);
const compilation = this.newCompilation(params);
this.hooks.make.callAsync(compilation, err => {if (err) return callback(err);
compilation.finish(err => {if (err) return callback(err);
compilation.seal(err => {if (err) return callback(err);
this.hooks.afterCompile.callAsync(compilation, err => {if (err) return callback(err);
return callback(null, compilation);
});
});
});
});
});
}
}
在剖析这块源码前,咱们先明确下 webpack 两个外围概念:Compiler 和 Compilation。
- Compiler 类(
./lib/Compiler.js
):webpack 的次要引擎,在 compiler 对象记录了残缺的 webpack 环境信息,在 webpack 从启动到完结,compiler 只会生成一次。你能够在 compiler 对象上读取到 webpack config 信息,outputPath 等; - Compilation 类(
./lib/Compilation.js
):代表了一次繁多的版本构建和生成资源。compilation 编译作业能够屡次执行,比方 webpack 工作在 watch 模式下,每次监测到源文件发生变化时,都会从新实例化一个 compilation 对象。一个 compilation 对象体现了以后的模块资源、编译生成资源、变动的文件、以及被跟踪依赖的状态信息。
Compiler 和 Compilation 区别?
Compiler 代表的是不变的 webpack 环境;Compilation 代表的是一次编译作业,每一次的编译都可能不同。
compiler.run()
独自截取 run 办法如下:
run(callback) {const onCompiled = (err, compilation) => {
...
if (this.hooks.shouldEmit.call(compilation) === false) {const stats = new Stats(compilation);
stats.startTime = startTime;
stats.endTime = Date.now();
this.hooks.done.callAsync(stats, err => {if (err) return finalCallback(err);
return finalCallback(null, stats);
});
return;
}
...
};
this.hooks.beforeRun.callAsync(this, err => {if (err) return finalCallback(err);
this.hooks.run.callAsync(this, err => {if (err) return finalCallback(err);
this.readRecords(err => {if (err) return finalCallback(err);
this.compile(onCompiled);
});
});
});
}
在 run 函数里,首先触发了一些钩子:beforeRun -> run -> done
,并在触发 run 钩子的时候,执行了 this.compile 办法。那么咱们就去看下这个 compile 办法具体做了些什么。
compiler.compile()
首先截取 compile 办法要害代码:
compile(callback) {const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => {if (err) return callback(err);
this.hooks.compile.call(params);
const compilation = this.newCompilation(params);
this.hooks.make.callAsync(compilation, err => {if (err) return callback(err);
compilation.finish(err => {if (err) return callback(err);
compilation.seal(err => {if (err) return callback(err);
this.hooks.afterCompile.callAsync(compilation, err => {if (err) return callback(err);
return callback(null, compilation);
});
});
});
});
});
}
代码中初始化了一个 compilation 实例对象,另外和 run 办法一些,compile 也触发一系列钩子:beforeCompile -> compile -> make -> afterCompile
。
其中依据最下面的流程图,在 make 钩子阶段,webpack 开始了真正的对模块的编译。那么咱们看下到底什么逻辑订阅了 make 钩子。通过全局搜寻 hooks.make.tapAsync
,咱们能够看到 SingleEntryPlugin、MultiEntryPlugin、DllEntryPlugin、DynamicEntryPlugin 等插件中都订阅了 make 钩子。
那么咱们先进入 SingleEntryPlugin 文件(./lib/SingleEntryPlugin.js
)中查看,要害代码如下:
class SingleEntryPlugin {constructor(context, entry, name) {
this.context = context;
this.entry = entry;
this.name = name;
}
apply(compiler) {
...
compiler.hooks.make.tapAsync(
"SingleEntryPlugin",
(compilation, callback) => {const { entry, name, context} = this;
const dep = SingleEntryPlugin.createDependency(entry, name);
compilation.addEntry(context, dep, name, callback);
}
);
}
...
}
其中次要调用了 compilation.addEntry 办法,持续查看 compilation.addEntry(./lib/Compilation.js
)。
addEntry(context, entry, name, callback) {this.hooks.addEntry.call(entry, name);
...
this._addModuleChain(
context,
entry,
module => {this.entries.push(module);
},
(err, module) => {
...
if (module) {slot.module = module;} else {const idx = this._preparedEntrypoints.indexOf(slot);
if (idx >= 0) {this._preparedEntrypoints.splice(idx, 1);
}
}
this.hooks.succeedEntry.call(entry, name, module);
return callback(null, module);
}
);
}
通过下面代码咱们能够看到 addEntry 又调用了 _addModuleChain,前面调用我就不在这里展现代码了,间接把调用栈列出来,感兴趣的同学能够自行查看源码。调用栈如下:
this.addEntry -> this._addModuleChain -> this.addModule -> this.buidModule -> module.build
addEntry 的作用是将模块的入口信息传递给模块链中,即 addModuleChain,随后持续调用 compiliation.factorizeModule,这些调用最初会将 entry 的入口信息”翻译“成一个模块(严格上说,模块个别是 NormalModule 实例化后的对象)。
当模块开始构建时,会触发 buidModule 钩子。上面是 buidModule 办法的要害代码,其中 module.build 执行胜利后,会触发 succeedModule 钩子,如果失败则触发 failedModule 钩子。
buildModule(module, optional, origin, dependencies, thisCallback) {
...
this.hooks.buildModule.call(module);
module.build(
this.options,
this,
this.resolverFactory.get("normal", module.resolveOptions),
this.inputFileSystem,
error => {
...
const originalMap = module.dependencies.reduce((map, v, i) => {map.set(v, i);
return map;
}, new Map());
module.dependencies.sort((a, b) => {const cmp = compareLocations(a.loc, b.loc);
if (cmp) return cmp;
return originalMap.get(a) - originalMap.get(b);
});
if (error) {this.hooks.failedModule.call(module, error);
return callback(error);
}
this.hooks.succeedModule.call(module);
return callback();}
);
}
那么 module 又是从哪里来的?从 Compilation.js 代码中咱们能够晓得这个是 Module 类的实例,其中又具体分为 NormalModule、ExternalModule、MutiModule、DelegatedModule 等。
咱们先进入到常见的 NormalModule 中查看源码(文件地址 ./lib/NormalModule.js
)。nomalModule.build 又调用了本身的 nomalModule.doBuild 办法
doBuild(options, compilation, resolver, fs, callback) {
...
runLoaders(
{
resource: this.resource,
loaders: this.loaders,
context: loaderContext,
readResource: fs.readFile.bind(fs)
},
(err, result) => {...}
)
}
nomalModule.doBuild 办法又调用了 runLoaders 办法来调用对应的 loader 对模块进行编译,最终会通过 loader 的组合将所有模块(css,less,jpg 等)编译成规范的 js 模块。
写过 webpack loader 童鞋应该对 runLoader 比拟相熟,这个能够独立运行 webpack loader,而无需装置整个 webpack,对于调试 webpack loader 很不便。
模块构建实现之后,在 normalModule.doBuild 办法的最初一个参数即回调函数中,会应用 acorn 的 parse 办法将构建后的规范 js 模块内容转换成 AST 语法树,通过其中的 require 语句来找到这个模块所依赖的其余模块,而后将该模块也增加到依赖列表中,最初遍历依赖列表顺次去构建。总结来说就是一直的剖析模块的依赖和一直的构建模块,直到所有波及到的模块都构建实现。
// 将 js 模块转成 AST 语法书,并剖析该模块所以来的模块
const result = this.parser.parse(source);
...
当所有模块都构建实现,会寄存在 Compilation 对象的 modules 数组属性中。构建胜利后会触发 succeedModule 钩子,否则会触发 failedModule 钩子。到此模块构建(make)阶段就完结了。
优化阶段
模块构建实现后,就会调用 Compilation 对象上的 seal 办法,该办法次要是触发 seal 钩子,开始对模块构建后果进行很多的优化操作,其中就蕴含了基于 module 生成 chunk 的逻辑。上面是 chunk 生成的算法:
- webpack 先将 entry 中对应的 module 都生成一个新的 chunk;
- 遍历 module 的依赖列表,将依赖的 module 也退出到 chunk 中;
- 如果一个依赖的 module 是动静引入的模块(例如 require.ensure 或者 es6 中的 dynamic-import 的引入形式),那么就会依据这个 module 创立新的 chunk,并持续遍历依赖;
- 反复下面的过程,直到失去所有的 chunks。
生成 chunk 之后,接下来还会调用 Compilation.createHash 办法为文件生成 hash,例如 js 个别设置 chunkHash,css 个别设置 contentHash 等。
文件 hash 创立实现之后,则会调用 createModuleAssets 办法将上个阶段构建传进去的规范 js 模块,放在 Compilation 的 assets 对象属性下来,key 是文件名,value 是构建后的模块内容。到此优化阶段就完结了。
文件生成阶段
优化阶段实现之后,就会进入到文件生成阶段。次要是在 Compiler 中触发 emit 钩子,调用 compilation.getPath 获取到文件输入的目录,而后将生成的文件写到磁盘对应的目录中。
到此为止,Webpack 的构建过程就实现了。经验了整个源码解读过程,置信读者对 Webpack 的了解会更加深刻了。
相干文章
- Webpack 源码剖析(1)— Webpack 启动过程剖析
- Webpack 源码剖析(2)— Tapable 与 Webpack 的关联
- Webpack 源码剖析(3)— Webpack 构建流程剖析