前言
在上一篇文章中,我们已经简述了Rollup的入口文件和命令行工具提供的能力,这一篇文章开始,我们会建一个测试项目给Rollup进行打包,在阅读源代码的同时,尝试打断点的形式嗅探Rollup核心的底层运行原理。
测试项目介绍
以下是我的Rollup配置文件:
js
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",
},
output: {
dir: "dist",
chunkFileNames: "chunks/[name]-[hash].js"
},
plugins: [
del({ targets: "dist/*" }),
resolve(),
replace({
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
preventAssignment: true
}),
],
});
配了三个插件:
- 第一个是为了每次构建的时候把前一次构建的内容删除;
- 第二个插件是为了让Rollup能够找到外部模块
- 第三个插件主要是为我们后面的文章探索Rollup的
TreeShaking
做准备。
然后是项目目录结构:
先把项目的每个JS的内容向大家展示一下。
index.js
js
import { A } from "./deps/a";
console.log("hello world");
A();
function Demo() {
import("./deps/q").then(({ Q }) => {
Q();
});
}
Demo();
a.js
js
import { B, demoB } from "./b";
import { D } from "./d";
export async function A() {
console.log("AAAAA");
D();
B();
import("./e").then(({ E }) => {
E();
});
if (process.env.NODE_ENV !== "production") {
demoB();
}
}
import("./f").then(({ F }) => {
F();
});
b.js
js
import { C } from "./c";
import { D } from "./d";
export function B() {
console.log("BBBB");
C();
D();
}
export function demoB() {
console.log("demo b");
}
c.js
js
export function C() {
console.log("CCCCCCCCC");
}
d.js
js
export function D() {
console.log("DDDDDD");
}
e.js
js
import { F } from "./f";
export function E() {
console.log("eee");
F();
}
f.js
js
import { G } from "./g";
export function F() {
console.log("FFFF");
G();
}
g.js
js
import { F } from "./f";
export function G() {
F();
console.log("GGGGGG");
}
q.js
js
export function Q() {
console.log("QQQQQQQQQQQ");
}
为了方便大家有一个直观的项目文件依赖关系的认识,我画了一个图,这个图即是Image
,又是数据结构里面所讲的Graph
图,便于一会儿阐述依赖分析。
文件内容读取
在上一篇文章中,我们提到了Graph
这个核心类,此刻我们就开始从它开始去研究Rollup文件读取的逻辑。 在上一篇文章中,我们已经聊过这个方法了,大家应该不会特别陌生。
build
方法开始生成资源的依赖关系图,我们会花一定的篇幅来研究它的处理逻辑。
此刻,它会调用ModuleLoader
类的addEntryModules
方法,这个ModuleLoader也是一个核心类,后面的篇幅我们会一直跟它打交道,这个类是在Graph类的构造函数初始化的。 紧接着调用loadEtryModule
方法: 然后确定文件路径,在这个位置就触发了Rollup的生命周期resolveId
。
我们利用resolveId
可以处理路径别名,可以实现虚拟模块的技术。
现在,我们再回过头来看Rollup的文档,resolveId
这个生命周期是不是就觉得倍感熟悉啦,哈哈。
在确定了文件的路径之后,就要开始尝试加载文件内容了。 这儿又来了一个新的核心类Module
,这个类主要就是用来处理资源的,后面会经常见到它的。 然后就可以触发生命周期load
,并且读取文件的内容了(读取不到则输出错误信息),不过load这个生命周期Rollup并没有向外暴露。 到这个位置,简单的文件加载逻辑就已经完成了。
文件读取到之后,插件有可能对其进行转换,所以得接着看: 简单起见,我们就不研究模块缓存了。
然后就调用了trsansform
方法,在这个方法内,得到插件对文件内容的转换结果。 transform
的逻辑很复杂,我们暂时不做研究,后续如果有时间的话,会专门写一篇文章来阐述Rollup的插件系统。
到这个位置,文件的读取内容就已经完成了。
不过,别高兴的太早,我们只分析了一个最简单的入口文件读取,入口文件还会依赖其它文件,这是一个递归的过程,后面的文章才会分析。
文件依赖分析
到这个位置,对于你的能力提高最大之一的章节将会出现,这个位置我们将会使用到AST
(Abstract Syntax Tree)的内容,如果你还不知道什么是AST的话,建议先了解一下AST的基础知识点。
文章中会将之前的项目文件源代码转化成AST进行举例子,我使用的转换工具是它:astexplorer.net
回到之前的setSource
: 插件返回的内容有可能是AST,也有可能不是AST,我们以非AST的内容为例,接下来将进行源代码转成AST的逻辑。 大家先对这块内容引起重视,这个astContext
将传入到AST转换工具函数中去,根据解析到的语句决定调用Module相应的方法。 接下来,是从源码上不好看,需要真正把程序运行起来才能知道的逻辑。 在源码里面,nodeContrustors是一大堆的Class,其对应的是各种类型的AST节点处理,然后这些节点处理器均实现了一个initialise
的方法。
我们以目前最关心的import
语句的为例,看一下它的实现。
我们用前文提到的AST解析工具解析index.js的内容看看:
接下来,我们来看一下我们测试项目运行的堆栈,确定一下我们刚才的阐述是否正确。
从上面的堆栈信息可以验证我们的阐述是正确的。
我们暂时先看这个addSource
方法,后面还有一些关键内容,猜测是跟后续的TreeShaking
逻辑相关的。 然后记录下依赖内容的线索,一会儿再处理。 此刻,我们需要回过去的位置有点儿远,我们将回到之前的fetchModule
方法。 我们暂时先看getResolveStaticDependencyPromises
这个方法。 现在读取的就是刚才我们在解析AST时得到的依赖文件线索,此刻又在调用前面我们聊文件加载时提到过的resolveId
,至于Rollup处理资源未找到的逻辑,我们暂时就不看了,大家有兴趣的话自行查看源码吧。
我们暂时还不考虑import
函数加载资源的方式。
此时Rollup触发moduleParsed
的生命周期钩子。
来看一下解析到的module Info:
我们暂时先留一个猜想,Module类就是我们的文件在Rollup内部的包裹类,即这个类上绑定着文件的资源和文件依赖等复杂信息等等,至于对不对,我们后面再验证。
结语
考虑到篇幅的关系,在本文中,我们暂时就介绍这么多内容,在下一篇文章中我们接着分析Rollup的构建流程,未完待续.....