以下内容基于webpack@^5.12.3版本
简略流程图
文末有详细流程图
webpack函数
这快的代码比较简单就不贴了,讲下大致流程
- 归一化options,将部分配置转换成webpack需要的格式
- 创建context上下文,取的是process.cwd()
- 创建compiler实例
- 初始化流插件
- 初始化用户配置的插件,注册插件钩子
- 进一步优化options,给一些配置赋上默认值
- 初始化webpack内部插件,例如js解析器、缓存插件、添加入口的插件等。
缓存机制
以下介绍下webpack4.x和webpack5.x缓存实现的区别
webpack 5.x
这里大致介绍下webpack的两种缓存机制memory: 内存缓存和filesystem: 持久缓存。上个步骤讲到webpack函数执行的时候会初始化缓存插件,这时候会根据配置的cache.type是memory还是filesystem执行不同的操作
memory存储在内存中,用于热更新,对重新编译不起作用。实现插件为MemoryCachePlugin,存储方式为Map对象filesystem会生成本地文件, 编译过程中只会创建延时写入队列,在编译完之后才会循坏该队列,写入文件。缓存文件默认保存在node_modules/.cache中,一个chunk生成一个缓存文件。实现插件为IdleFileCachePlugin。并且如果配置filesystem做永久化储存,webpack还是会同时使用memory存储,用于watch模式,MemoryCachePlugin插件执行顺序在IdleFileCachePlugin插件之前。
webpack4.x
在webpack中,只有内存缓存,在compilation实例中,有一个实例属性cache,为对象类型, 所有的内容皆缓存在这里。
具体实现如下:
- 在webpack函数执行的时候初始化
CachePlugin插件, 这里会初始化compilation.cache, 在watch模式下,可以直接取到上次编译缓存的内容
//webpack.js
compiler.options = new WebpackOptionsApply().process(options, compiler);
//WebpackOptionsApply.js
if (options.cache) {
const CachePlugin = require("./CachePlugin");
new CachePlugin(
typeof options.cache === "object" ? options.cache : null
).apply(compiler);
}
//CachePlugin.js
compiler.hooks.thisCompilation.tap("CachePlugin", compilation => {
compilation.cache = cache;
compilation.hooks.childCompiler.tap(
"CachePlugin",
(childCompiler, compilerName, compilerIndex) => {
let childCache;
if (!cache.children) {
cache.children = {};
}
if (!cache.children[compilerName]) {
cache.children[compilerName] = [];
}
if (cache.children[compilerName][compilerIndex]) {
childCache = cache.children[compilerName][compilerIndex];
} else {
cache.children[compilerName].push((childCache = {}));
}
registerCacheToCompiler(childCompiler, childCache);
}
);
});
- 编译过程中,通过
compilation.cache获取和存储内容
//compilation.js
class Compilation extends Tapable {
constructor(compiler) {
...
this.cache = null;
}
addModule() {
...
if (this.cache && this.cache[cacheName]) {
const cacheModule = this.cache[cacheName];
...
}
}
}
当然,webpack5.0之前也可以通过hard-source-webpack-plugin实现持久化缓存的。具体原理,其实通过上面内存缓存过程的的说明,也很清晰了,只要注册适当的钩子,去做读取compilation.cache即可。
构建过程
在webpack函数执行完之后,就会执行compiler.run了,然后触发一堆钩子函数(具体钩子函数可看最下方的详细流程)开始执行compiler.compile,这里贴一下代码。
构建入口
compile(callback) {
//初始化构建需要的模块插件
const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => {
if (err) return callback(err);
this.hooks.compile.call(params);
//创建构建实例,构建过程的内容都会保存在compilation中
const compilation = this.newCompilation(params);
const logger = compilation.getLogger("webpack.Compiler");
logger.time("make hook");
//开始构建模块
this.hooks.make.callAsync(compilation, err => {
logger.timeEnd("make hook");
if (err) return callback(err);
logger.time("finish make hook");
//模块构建完成
this.hooks.finishMake.callAsync(compilation, err => {
logger.timeEnd("finish make hook");
if (err) return callback(err);
process.nextTick(() => {
logger.time("finish compilation");
//做一些模块的错误和警告的处理
compilation.finish(err => {
logger.timeEnd("finish compilation");
if (err) return callback(err);
logger.time("seal compilation");
//封装模块开始
compilation.seal(err => {
logger.timeEnd("seal compilation");
if (err) return callback(err);
logger.time("afterCompile hook");
//编译完成 this.hooks.afterCompile.callAsync(compilation, err => {
logger.timeEnd("afterCompile hook");
if (err) return callback(err);
//执行onCompiled回调
return callback(null, compilation);
});
});
});
});
});
});
});
}
整个方法看着超级简单,50行不到的代码就把编译过程做完了。那么,webpack是怎么开始构建的呢?
在webpack函数执行的时候,有初始化内部插件的步骤,其中会初始化一个叫EntryPlugin的插件
class EntryPlugin {
constructor(context, entry, options) {
this.context = context;
this.entry = entry;
this.options = options || "";
}
apply(compiler) {
compiler.hooks.compilation.tap(
"EntryPlugin",
(compilation, { normalModuleFactory }) => {
compilation.dependencyFactories.set(
EntryDependency,
normalModuleFactory
);
}
);
compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
const { entry, options, context } = this;
const dep = EntryPlugin.createDependency(entry, options);
compilation.addEntry(context, dep, options, err => {
callback(err);
});
});
}
}
这里监听了2个钩子:hooks.compilation 和 hooks.make。
在compilation钩子中,会为compilation实例注入normalModuleFactory参数,这个是在this.newCompilationParams()的时候创建的,包含了创建模块的方法。
在make钩子中,会创建编译入口,然后执行compilation.addEntry,这个方法才是真正构建的开始。
构建开始
以下内容主要在compilation.js文件内完成
- 执行
_addEntryItem将入口文件存入this.entries,后续构建chunk遍历的是该map对象 - 执行
addModuleTree, 获取在EntryPlugin存入的dependencyFactories中的moduleFactory - 执行
handleModuleCreation,开始创建模块实例 - 执行
moduleFactory.create创建模块,这里主要做了三件事- 执行
factory.hooks.factorize.call钩子,然后会调用ExternalModuleFactoryPlugin中注册的钩子,用于配置外部文件的模块加载方式, 例如fs, http, events等node原生方法 - 使用
enhanced-resolve解析模块和loader真实绝对路径 new NormalModule()创建module实例
- 执行
- 执行
addModule,存储module buildModule,构建模块, 这里会调用normalModule中的build开启构建。主要过程为:- 创建loader上下文
runLoaders,通过enhanced-resolve解析得到的模块和loader的路径获取函数,执行loader- 调用
JavascriptParser.js将loader执行完的源码解析成ast(使用了acorn工具),这步会生成当前模块的以来集合 - 生成模块的hash
- 缓存解析完的module至
_modulesCache,此时已经有_source(解析后的源码)
- 执行
processModuleDependencies,获得模块依赖,重复第3步
以上,所有模块已经构建完成,生成了模块的集合。
产物封装
执行compilation.seal进行产物的封装。
- 循环遍历
entrys(在构建第一步添加的this.entries),生成chunks - 执行
buildChunkGraph,这里会将import()、require.ensure等方法生成的动态模块添加到chunks中 - 后续就是一堆优化模块和chunks等的钩子
- 执行
hooks.optimizeChunkModules的钩子,这里开始进行代码生成和封装- 同样的是触发各种钩子函数
- 执行
createModuleHashes更新模块hash - 执行
codeGeneration生成模块代码,这里会遍历modules,创建构建任务,循环使用JabascriptGenerator构建代码,这时会使用不同的依赖处理,将import等模块引入方式替换为__webpack_require__等,并将生成结果存入缓存 - 执行
processRuntimeRequirements,根据生成的内容所使用到的webpack_require的函数,增加添加对应的代码,例如__webpack_require__、__webpack_require__.n、__webpack_require__.r等 - 执行
createHash创建chunk的hash - 执行
clearAssets清除chunk的files和auxiliary,这里缓存的是生成的chunk的文件名,应该是为了热更新模式把,防止残留上次构建的遗弃内容 createModuleAssets这步如果module.buildInfo.assets也会将该内容存入compilation.assets,暂时不知道怎么触发这个场景的createChunkAssets生成render函数,执行render函数,将chunk内容缓存在compilation.assets对象中,会把生成的chunk文件名缓存至chunk.files或chunk.auxiliary中
到这里所有产物内容已经生成了,但是还没有正式生成产物文件
产物生成
这里会回到compiler的进程中,执行onCompiled回调:
- 触发
shouldEmit钩子函数,这里是最后能优化产物的钩子了 - 写入本地文件,用的是webpack函数执行时初始化的文件流工具
- 执行
done钩子函数,这里会执行compiler.run的回调当中,再执行compiler.close,然后执行永久化存储(前提是使用的filesystem缓存模式)
compiler和compilation
我们在开发插件时常常会用到compiler和compilation,那么compiler和compilation到底有啥区别呢?
结合上述流程介绍和末尾的详细流程图其实能很清晰的看出来:
compiler: 覆盖编译的整个生命周期,包括初始化、启动、暂停、开始解析、开始封装等等,可以看作是编译过程的推手。理所当然,在整个编译过程只有有一个compiler实例。
compilation: 每个编译过程都会生成一个compilation实例。这里的每个编译过程可以看作是watch模式下的文件修改引发的重新编译。原因也很简单,上面指出了,每次watch都会执行compiler.run方法,而初始化compilation是在这之后的。而compilation主要负责的是构建,包括模块的解析、代码生成和封装。
module、chunk和bundle
module: 这个很简单,import一个模块就是一个module。
chunk: 一个入口文件会生成一个chunk,代码分割也会生成chunk。
bundle: 最终的产物,一个产物就是一个bundle。
两者的关系是,一个chunk可能对应一个或多个bundle。
找了一张图来对比下:
entry: {
index: "../index.js",
utils: '../utils.js',
}
其中utils.js和index.js是两个入口文件,所以是两个chunk。分离css,所以生成了3个bundle: index.bundle.css、index.bundle.js和utils.bundle.js。前两个属于chunk 0 ,最后一个属于chunk 1。
结语
总算结束了,很多细节其实还是没搞懂意思的,例如missingDependenices是什么?ensureChunkConditionsPlugin、RemoveEmptyChunksPlugin等插件干嘛用的,为什么生成了module的hash只会还要执行createModuleHashes?基本下列详细流程图中没有注释的除非太简单了,不然就是没完全搞明白作用的 ==!
以上。有任何错误和补充,欢迎留言。
详细流程图
黄色标注代表重要节点绿色标注代表compiler的钩子函数蓝色标注代表compilation的钩子函数,因为太多了,后续一些产物生成的钩子可能不是很全。