- 作者:老汪软件技巧
- 发表时间:2024-12-06 00:09
- 浏览量:
前言
在上一篇文章中,我们已经研究了Rollup是如何加载文件以及如何进行文件的依赖分析的,并没有继续分析它如何完成文件加载的,在这篇文章中,我们接着向下分析。
文件的递归加载
接上文我们分析到的fetchModule方法,然后回忆一下之前加载入口文件的函数调用堆栈:
addEntryModules->loadEntryModule->fetchModule
现在我们把目光聚焦到fetchModuleDependencies这个方法上来。
fetchModuleDependencies是在fetchModule这个函数里面调用的。那么好的,我们此刻再整理一下fetchModuleDependencies的调用堆栈:
fetchModule->fetchModuleDependencies->fetchStaticDependencies->fetchResolvedDependency->fetchModule
这个递归体就已经被我们找到了,这个递归并不是一个函数单独的自身调用,而是几个函数形成的调用链,这也符合我们八股文所背诵的Rollup的处理流程:
Rollup从入口文件开始处理,然后以深度优先搜索的方式依次搜索相关的依赖文件,直到搜索完成所有的依赖文件。
文件循环依赖怎么办?
我们来构建一个循环依赖的例子,看一下最终解析到的Module信息是怎么样的。
在之前,我们我们展示的fetchModule方法的源码,有两个if分支是被折叠起来了的,现在我们着重看其中的一个if分支,在这儿就可以处理循环依赖的问题:
直接从已经加载过的缓存里面读取:
可以明显的看到,这个跟我们处理深拷贝时嗅探循环引用的手段是一样的,所以,数据结构的知识点是万变不离其宗的。
虽然Rollup可以处理循环依赖,但是并不代表你就可以高枕无忧的在项目中编写有循环依赖的代码,在某些场景下是灾难性的,后续我们会聊这个事儿,我之前在实际项目中就遇到过,当时整个人看到报错麻了,哈哈哈,最后还是借助Chatgpt才解决的。
打印一下我临时构建的一个循环依赖的信息:
使用import函数导入的依赖文件处理
在第二篇的时候,我们说暂时还不考虑import函数的加载,此时此刻我们得需要考虑了,否则,什么关键内容都被跳过了,那还学什么呀,哈哈哈。
还是回到之前聊到的fetchModule方法。
在addModuleSource这个函数成功执行完成时候,AST就已经解析完成了的,Rollup负责对应语句处理的AST应该是已经把相应的动态导入依赖记录下来了。
至于是不是我们猜想的这样,一会儿我们还是用堆栈信息来跟踪一下AST的解析就可以一探究竟。
然后触发resolveDynamicImport生命周期。
以下堆栈信息就是AST解析过程中处理imort函数的逻辑:
同一份资源既有静态导入,又有了动态导入,怎么办?
我们先不看源码,直接瞎想,就是瞎想,我确定我不是打错字哦。
既然你已经是使用静态导入了,那么,当前代码执行的时候,所属依赖内容此刻是不是已经加载到磁盘了?那么,既然都加载进来了,分包还有意义吗?那显然是没有意义了,所以如果一个资源既有动态导入又有静态导入,默认情况下Rollup应该是不会对它进行自动分包的,在后续的文章我们来揭晓这个答案。
如何处理外部模块
我们以一个外部配置模块来举例子,就使用lodash进行举例。
首先得修改一下之前的那个rollup.config.mjs:
import { defineConfig } from "rollup";
import resolve from "@rollup/plugin-node-resolve";
import replace from "@rollup/plugin-replace";
import del from "rollup-plugin-delete";
export default defineConfig({
input: {
main: "src/index.js",
},

// 配置需要外部导入的内容
external: ["lodash"],
output: {
dir: "dist",
format: 'esm',
chunkFileNames: "chunks/[name]-[hash].js",
// 配置全局变量
globals: {
lodash: "_",
},
},
plugins: [
del({ targets: "dist/*" }),
resolve(),
replace({
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
preventAssignment: true,
}),
],
});
我们现在需要看一下,resolveId.external是在什么时候处理的。
这个external判断条件的函数是在Rollup调用的开头,Rollup在做标准化的时候赋值的。
最终的处理逻辑即用到了我们在配置选项里面external的内容:
目前来看,ExternalModule和Module在依赖内容解析的时候还是差不多的,最终的差异肯定是将来体现在代码打包输出的时候,那我们暂时就讨论这么多。
构建生命周期总结
以下是Rollup官方文档上的流程图:
接下来,我为大家总结一下这些生命周期的触发流程:
首先,Rollup触发options和buildStart生命周期,然后解析入口文件,这会导致触发resolveId生命周期。
当确定了入口文件之后,Rollup尝试加载入口文件,因此会触发load生命周期,当加载完成入口文件之后,Rollup开始解析入口文件的内容,因此触发transform生命周期,当解析完成入口文件的内容的时候,modulePasred生命周期就被触发了,因为当前入口文件的内容可能存在import函数的形式导入资源,因此有可能触发resolveDynamicImport生命周期,当然,也有可能是普通的import语句,因此,也有可能再次触发resolveId这个生命周期。
接着再进行的生命周期就跟之前主入口文件的流程是一样的了,前文我们已经分析过了,这是一个递归的解析过程,直到解析完成所有的资源,最后,当所有的文件都处理完成的时候,触发buildEnd生命周期。
有的同学可能会觉得,明明看了我的3篇文章了,一直分析的都是文件内容的读取和解析,怎么构建生命周期都已经分析完了却感觉不到呢?这是因为之前我们一笔带过了transform生命周期,原生的transform是做AST的解析,而Rollup能做很多事儿,是因为插件利用transform生命周期完成了很多复杂的业务逻辑,所以,这两篇文章所阐述的内容就是整个构建过程。
结语
到这个位置为止,我们基本上就已经把Rollup的构建流程搞清楚了,并且向大家分析了循环依赖和外部模块的处理逻辑,接下来就要开始分析Rollup的文件生成处理逻辑。
到这个位置,我们可以证明前文所提到的Module类是Rollup内部处理文件内容的代理对象这个结论可以是正确的。
这个Module类的实例上关联了很多关键的内容,比如文件依赖的资源,文件对外导出的内容,还有文件的AST以及源码等等关键信息,将来在代码合并、生成的时候有重要作用。
下一篇文章开始,我们将开始分析Rollup的产物生成处理逻辑,未完待续......