rollup技术揭秘系列三 rollup函数(可能是全网最系统性的rollup源码分析文章)

841 阅读5分钟

调用 rollup 发生了什么

在源码的 src/rollup/rollup.ts 文件中找到 rollup 的定义:

export default function rollup(rawInputOptions: RollupOptions): Promise<RollupBuild> {
  return rollupInternal(rawInputOptions, null);
}

rollup 方法就是将用户传入的 rawInputOptions 原封不动的传给了 rollupInternal 方法,并且第二个参数的值为 null(watcher 参数我们在源码分析阶段可以忽略它,把关注点放在主线逻辑上这样可以减少干扰项。感兴趣的同学可以自行查看源码进行分析)。

  • rawInputOptions 就是用户传入的参数对象,它遵守 InputOptions 接口的约定。InputOptions 接口定义在 src/rollup/types.d.ts 文件中:
export interface InputOptions {
  acorn?: Record<string, unknown>; //对象类型。acorn是一个js语法解析器,非必传。代表用户可以使用自定义的语法解析器,如果不传则默认使用acorn解析器
  acornInjectPlugins?: (() => unknown)[] | (() => unknown); //InjectPlugins
  cache?: false | RollupCache; //缓存
  context?: string; //context
  experimentalCacheExpiry?: number; //graph.pluginCache ,默认 最大值为10。这个是实验性质的功能
  external?: ExternalOption; //自定义判断是否为外部模块的函数,非必传
  /** @deprecated Use the "inlineDynamicImports" output option instead. */
  inlineDynamicImports?: boolean;
  input?: InputOption; //打包入口模块,可以是 string | string[] | { [entryAlias: string]: string } 这几种类型
  makeAbsoluteExternalsRelative?: boolean | 'ifRelativeSource';
  /** @deprecated Use the "manualChunks" output option instead. */
  manualChunks?: ManualChunksOption; //代码块
  maxParallelFileOps?: number; //最大队列执行数,默认为20
  /** @deprecated Use the "maxParallelFileOps" option instead. */
  maxParallelFileReads?: number;
  moduleContext?: ((id: string) => string | NullValue) | { [id: string]: string }; //模块id默认是undefined
  onwarn?: WarningHandlerWithDefault; //告警函数
  perf?: boolean; //显示性能计时
  plugins?: InputPluginOption; //自定义插件
  preserveEntrySignatures?: PreserveEntrySignaturesOption; //如果值为false则不保留exports代码,默认值为"exports-only"
  /** @deprecated Use the "preserveModules" output option instead. */
  preserveModules?: boolean; //保留模块结构,默认值undefined
  preserveSymlinks?: boolean; //默认值false
  shimMissingExports?: boolean; //默认值false
  strictDeprecations?: boolean; //默认值false
  treeshake?: boolean | TreeshakingPreset | TreeshakingOptions; //treeshake 配置
  watch?: WatcherOptions | false;
}

我们接着来看 rollupInternal 方法,rollupInternal 内部主要干了 4 件事:

  1. 首先会调用 getInputOptions 方法将用户传入的 inputOptions 与 rollup 内部默认的 InputOptions 进行合并操作拿到最终的 inputOptions 配置。
  2. 接着实例化 Graph, 在 new Graph 的时候主要是初始化 this.pluginDriver、this.acornParser、this.moduleLoader、this.fileOperationQueue、生成全局作用域等等属性和方法。
  3. 然后调用 graph.build() 方法执行打包逻辑。
  4. 最后返回了 RollupBuild 对象。

这个 RollupBuild 对象包含了 cache、close、closed、generate、getTimings、watchFiles、write 等属性或方法。cache 保存了原始的模块相关的信息,例如打包前的原始代码。 watchFiles 保存了被 rollup 处理过的模块 id 信息。RollupBuild 对象中的 write 方法可以将最终的代码写入到指定的文件里。

export async function rollupInternal(
	rawInputOptions: RollupOptions,
	watcher: RollupWatcher | null
): Promise<RollupBuild> {
	//合并用户传入的inputOptions
	const { options: inputOptions, unsetOptions: unsetInputOptions } = await getInputOptions(
		rawInputOptions,
		watcher !== null
	);
	//实例化Graph,这个 Graph 保存了所有的 module 。
	const graph = new Graph(inputOptions, watcher);
	//...
	await catchUnfinishedHookActions(graph.pluginDriver, async () => {
		try {
      //...
			await graph.build();
		} catch (error_: any) {
			//...
	});
	//...
	const result: RollupBuild = {
		cache: useCache ? graph.getCache() : undefined,
		async close() {
			if (result.closed) return;
			result.closed = true;
			await graph.pluginDriver.hookParallel('closeBundle', []);
		},
		closed: false,
		async generate(rawOutputOptions: OutputOptions) {
			if (result.closed) return error(errorAlreadyClosed());
			return handleGenerateWrite(false, inputOptions, unsetInputOptions, rawOutputOptions, graph);
		},
		watchFiles: Object.keys(graph.watchFiles),
		async write(rawOutputOptions: OutputOptions) {
			if (result.closed) return error(errorAlreadyClosed());
			return handleGenerateWrite(true, inputOptions, unsetInputOptions, rawOutputOptions, graph);
		}
	};
	if (inputOptions.perf) result.getTimings = getTimings;
	return result;
}

总结

rollup 执行打包的逻辑非常清楚,开发者把不同功能的逻辑进行拆分到一些单独的函数或者 class 中,从而让主线逻辑非常清晰。这种方式其实就是“解耦”的思想。在使用 rollup 的配置上,纵观一些框架、库的设计几乎都是类似的,在其内部预先定义了默认的配置,然后在初始化的时候传入自定义配置。最后在它的内部会帮我进行合并配置来达到不同的需求的目的。我们在学习源码的过程中,不仅需要知道为什么,而且还要学习作者一些优秀的编程思想,这才是我们学习源码的目的所在。

到目前为止我们学习了 rollup 的打包的主要过程,那么接下来我们顺着主线逻辑继续深入代码一探究竟吧!

rollup 揭秘相关文章