rollup 插件架构-驱动设计 PluginDriver

文章目录

  • Graph
  • PluginDriver
    • [生成 PluginDriver 实例和 PluginCache 缓存](#生成 PluginDriver 实例和 PluginCache 缓存)
    • [创建插件上下文 pluginContext](#创建插件上下文 pluginContext)
    • [初始化 pluginContext 缓存设置、方法](#初始化 pluginContext 缓存设置、方法)
    • 插件中使用缓存
    • [可替换的 replace pluginContext](#可替换的 replace pluginContext)
    • [PluginDriver 提供 asyn、first、parallel 等类型 hook](#PluginDriver 提供 asyn、first、parallel 等类型 hook)
      • [getSortedPlugins 运行时收集并存储插件对应 hook](#getSortedPlugins 运行时收集并存储插件对应 hook)
      • [sync hook](#sync hook)
      • [parallel hook](#parallel hook)
    • this.runHookSync
    • this.runHook
  • [和 webpack 插件系统区别](#和 webpack 插件系统区别)

Graph

  • 在依赖图谱 Graph 中创建 PluginDriver
  • Graph 负责整个 Rollup 打包过程中模块的解析、转化、生成,所以在Graph 中创建 PlunginDriver 能够获得整个打包生命周期的模块信息
javascript 复制代码
	const graph = new Graph(inputOptions, watcher);

PluginDriver

生成 PluginDriver 实例和 PluginCache 缓存

  • 整个 Rollup 构建生命周期中,通过 pluginDriver 这个实例去触发 hook
javascript 复制代码
export default class Graph {
	readonly acornParser: typeof acorn.Parser;
	readonly cachedModules = new Map<string, ModuleJSON>();
	// ...
	entryModules: Module[] = [];
	readonly fileOperationQueue: Queue;
	readonly moduleLoader: ModuleLoader;
	readonly modulesById = new Map<string, Module | ExternalModule>();
	needsTreeshakingPass = false;
	phase: BuildPhase = BuildPhase.LOAD_AND_PARSE;
	readonly pluginDriver: PluginDriver;
	// ...

	constructor (private readonly options: NormalizedInputOptions, watcher: RollupWatcher | null) {
		// 根据用户设置配置插件缓存
		this.pluginCache = options.cache?.plugins || Object.create(null);
		// ...
		this.pluginDriver = new PluginDriver(this, options, options.plugins, this.pluginCache); // 给插件封装能调用 graph 的方法
		// ...
	}

创建插件上下文 pluginContext

  • pluginContext 是交给用户在插件中可以调用的相关方法、属性
  • 可以看到多个插件的上下文共享同一个 pluginCache、graph
javascript 复制代码
export class PluginDriver {
 	// ... 相关方法
	private readonly pluginContexts: ReadonlyMap<Plugin, PluginContext>;
	private readonly plugins: readonly Plugin[];
	private readonly sortedPlugins = new Map<AsyncPluginHooks, Plugin[]>();
	private readonly unfulfilledActions = new Set<HookAction>();

	constructor(
		private readonly graph: Graph,
		private readonly options: NormalizedInputOptions,
		userPlugins: readonly Plugin[],
		private readonly pluginCache: Record<string, SerializablePluginCache> | undefined,
		basePluginDriver?: PluginDriver
	) {
		// ...
		
		this.plugins = [...(basePluginDriver ? basePluginDriver.plugins : []), ...userPlugins];
		const existingPluginNames = new Set<string>();
		
		// 为每个插件创建一个 pluginContext 
		this.pluginContexts = new Map(
			this.plugins.map(plugin => [
				plugin,
				getPluginContext(plugin, pluginCache, graph, options, this.fileEmitter, existingPluginNames)
			])
		);
		
		// ...
	}
}

初始化 pluginContext 缓存设置、方法

  • 将每个 plugin 按照传递的配置设置缓存 key,存放进 Graph 的 pluginCache 集合中保存
javascript 复制代码
export function getPluginContext(
	plugin: Plugin,
	pluginCache: Record<string, SerializablePluginCache> | void,
	graph: Graph,
	options: NormalizedInputOptions,
	fileEmitter: FileEmitter,
	existingPluginNames: Set<string>
): PluginContext {
	let cacheable = true;
	
	if (typeof plugin.cacheKey !== 'string') {
		if ( // 插件没写 name 不缓存
			plugin.name.startsWith(ANONYMOUS_PLUGIN_PREFIX) ||
			plugin.name.startsWith(ANONYMOUS_OUTPUT_PLUGIN_PREFIX) ||
			existingPluginNames.has(plugin.name)
		) {
			cacheable = false;
		} else {
			existingPluginNames.add(plugin.name);
		}
	}

	let cacheInstance: PluginCache;
	if (!pluginCache) {
		cacheInstance = NO_CACHE;
	} else if (cacheable) {
		// 根据插件传递的配置设置缓存 key
		const cacheKey = plugin.cacheKey || plugin.name;
		cacheInstance = createPluginCache( // 封装操作 cache 缓存的操作(get、has、set、delete)
			// 在创建时已经根据 key 分配了对象
			pluginCache[cacheKey] || (pluginCache[cacheKey] = Object.create(null))
		);
	} else {
		cacheInstance = getCacheForUncacheablePlugin(plugin.name);
	}
	
	// 返回给开发者可以调用的方法、属性
	return {
		addWatchFile(id) {
			if (graph.phase >= BuildPhase.GENERATE) {
				return this.error(errorInvalidRollupPhaseForAddWatchFile());
			}
			graph.watchFiles[id] = true;
		},
		cache: cacheInstance,
		// ...
	};
}

// 记录缓存操作
export function createPluginCache(cache: SerializablePluginCache): PluginCache {
	return {
		delete(id: string) {
			return delete cache[id];
		},
		get(id: string) {
			const item = cache[id];
			if (!item) return;
			item[0] = 0;
			return item[1];
		},
		has(id: string) {
			const item = cache[id];
			if (!item) return false;
			item[0] = 0;
			return true;
		},
		set(id: string, value: any) {
			cache[id] = [0, value];
		}
	};
}

插件中使用缓存

  • 插件 cache 的内容都会放在 Graph 的 pluginCache 中,在分配缓存时已经根据插件的 key 进行了设置,所以在插件中可以直接 this.cache 进行使用而不必担心和其它插件的缓存冲突
javascript 复制代码
{
  name: "test-plugin",
  buildStart() {
    if (!this.cache.has("cache")) {
      this.cache.set("cache", "cache something");
    } else {
      // 第二次执行rollup的时候会执行
      console.log(this.cache.get("cache"));
    }
  },
}

可替换的 replace pluginContext

  • pluginContext 会根据不同的 hook,动态增加属性、方法,比如 transform hook
  • 在通过 pluginDriver.hookReduceArg0 调用 transform hook时,第四个参数即是替换后的 pluginContext
javascript 复制代码
		code = await pluginDriver.hookReduceArg0(
			'transform',
			[currentSource, id],
			transformReducer,
			(pluginContext, plugin): TransformPluginContext => { 
				pluginName = plugin.name;
				return {
					...pluginContext, // 在原来 context 的基础上再添加额外的属性
					addWatchFile(id: string) {
						transformDependencies.push(id); // 收集插件中通过 this.addWatchFile 添加的文件 id
						pluginContext.addWatchFile(id);
					},
					cache: customTransformCache
						? pluginContext.cache
						: getTrackedPluginCache(pluginContext.cache, useCustomTransformCache),

				};
			}
		);

PluginDriver 提供 asyn、first、parallel 等类型 hook

  • rollup 根据不同场景提供了不同类型的 hook
    • async:该钩子也可以返回一个解析为相同类型的值的 Promise;否则,该钩子被标记为 sync。
    • first:如果有多个插件实现此钩子,则钩子按顺序运行,直到钩子返回一个不是 null 或 undefined 的值。
    • sequential:如果有多个插件实现此钩子,则所有这些钩子将按指定的插件顺序运行。如果钩子是 async,则此类后续钩子将等待当前钩子解决后再运行。
    • parallel:如果有多个插件实现此钩子,则所有这些钩子将按指定的插件顺序运行。如果钩子是 async,则此类后续钩子将并行运行,而不是等待当前钩子。
javascript 复制代码
export class PluginDriver {
	// ...
	
	hookFirstSync<H extends SyncPluginHooks & FirstPluginHooks>(
		hookName: H,
		parameters: Parameters<FunctionPluginHooks[H]>,
		replaceContext?: ReplaceContext
	): ReturnType<FunctionPluginHooks[H]> | null {
		// ...
	}

	async hookParallel<H extends AsyncPluginHooks & ParallelPluginHooks>(
		hookName: H,
		parameters: Parameters<FunctionPluginHooks[H]>,
		replaceContext?: ReplaceContext
	): Promise<void> {
		// ...
	}
	
	// ...
}

getSortedPlugins 运行时收集并存储插件对应 hook

  • Rollup 通过 getSortedPlugins 对插件的对应 hook 进行排序后收集在 this.sortedPlugins 集合中存储
  • 根据 pre、normal、post 顺序排序每个hook
javascript 复制代码
// 抽取插件中有对应 hookName 的插件
private getSortedPlugins( 
	hookName: keyof FunctionPluginHooks | AddonHooks,
	validateHandler?: (handler: unknown, hookName: string, plugin: Plugin) => void
): Plugin[] {
	return getOrCreate( // 根据 hookName 抽取所有符合的插件,运行时收集对应 hook 的插件放进 this.sortedPlugins 里
		this.sortedPlugins,
		hookName,
		() => getSortedValidatedPlugins(hookName, this.plugins, validateHandler) // 抽取插件中有对应 hookName 的插件
	);
}

export function getOrCreate<K, V>(map: Map<K, V>, key: K, init: () => V): V {
	const existing = map.get(key);
	if (existing !== undefined) {
		return existing;
	}
	const value = init();
	map.set(key, value);
	return value;
}

export function getSortedValidatedPlugins(
	hookName: keyof FunctionPluginHooks | AddonHooks,
	plugins: readonly Plugin[],
	validateHandler = validateFunctionPluginHandler
): Plugin[] {
	const pre: Plugin[] = [];
	const normal: Plugin[] = [];
	const post: Plugin[] = [];
	// 遍历所有插件,根据指定hook(options、transform、...)提取插件
	for (const plugin of plugins) {
		const hook = plugin[hookName];
		if (hook) {
			if (typeof hook === 'object') {
				validateHandler(hook.handler, hookName, plugin);
				if (hook.order === 'pre') {
					pre.push(plugin);
					continue;
				}
				if (hook.order === 'post') {
					post.push(plugin);
					continue;
				}
			} else {
				validateHandler(hook, hookName, plugin);
			}
			normal.push(plugin);
		}
	}
	// 根据 pre、normal、post 顺序排序每个hook
	return [...pre, ...normal, ...post];
}

sync hook

  • 同步执行 hook,返回第一个非 null 的结果
javascript 复制代码
	// chains synchronously, first non-null result stops and returns
	hookFirstSync<H extends SyncPluginHooks & FirstPluginHooks>(
		hookName: H,
		parameters: Parameters<FunctionPluginHooks[H]>,
		replaceContext?: ReplaceContext
	): ReturnType<FunctionPluginHooks[H]> | null {
		for (const plugin of this.getSortedPlugins(hookName)) {
			const result = this.runHookSync(hookName, parameters, plugin, replaceContext);
			if (result != null) return result;
		}
		return null;
	}

parallel hook

  • 用于并行执行的 hook,忽略返回值
  • 通过 Promise.all 并行执行
javascript 复制代码
	async hookParallel<H extends AsyncPluginHooks & ParallelPluginHooks>(
		hookName: H,
		parameters: Parameters<FunctionPluginHooks[H]>,
		replaceContext?: ReplaceContext
	): Promise<void> {
		const parallelPromises: Promise<unknown>[] = [];
		for (const plugin of this.getSortedPlugins(hookName)) { // getSortedPlugins 根据 hookName 提取对应的插件
			if ((plugin[hookName] as { sequential?: boolean }).sequential) { // 非顺序执行就先存起来然后并行执行
				await Promise.all(parallelPromises);
				parallelPromises.length = 0;
				await this.runHook(hookName, parameters, plugin, replaceContext);
			} else {
				parallelPromises.push(this.runHook(hookName, parameters, plugin, replaceContext)); // 将调用过程放进 Promise.then 任务中,等待 parallelPromises.push 同步任务收集完成后执行
			}
		}
		await Promise.all(parallelPromises); // 并行执行所有收集到的 runHook 
	}

this.runHookSync

  • 同步调用时,通过 this.pluginContexts.get(plugin) 获取到插件上下文供插件开发者使用
javascript 复制代码
	private runHookSync<H extends SyncPluginHooks>(
		hookName: H,
		parameters: Parameters<FunctionPluginHooks[H]>,
		plugin: Plugin,
		replaceContext?: ReplaceContext
	): ReturnType<FunctionPluginHooks[H]> {
		const hook = plugin[hookName]!;
		const handler = typeof hook === 'object' ? hook.handler : hook;

		let context = this.pluginContexts.get(plugin)!;
		if (replaceContext) {
			context = replaceContext(context, plugin);
		}

		try {
			// eslint-disable-next-line @typescript-eslint/ban-types
			return (handler as Function).apply(context, parameters);
		} catch (error_: any) {
			return error(errorPluginError(error_, plugin.name, { hook: hookName }));
		}
	}

this.runHook

  • 异步调用时,如果是结果也是 Promise,会暂存然后等待所有Promise都执行结束
javascript 复制代码
	private runHook<H extends AsyncPluginHooks | AddonHooks>(
		hookName: H,
		parameters: unknown[],
		plugin: Plugin,
		replaceContext?: ReplaceContext | null
	): Promise<unknown> {
		// We always filter for plugins that support the hook before running it
		const hook = plugin[hookName];
		const handler = typeof hook === 'object' ? hook.handler : hook;

		let context = this.pluginContexts.get(plugin)!; // 获取插件的上下文,上下文通过 PluginDriver 封装了一系列和 graph 等相关的方法
		if (replaceContext) { //transform 钩子需要往 plugin 上下文中添加额外的内容
			context = replaceContext(context, plugin);
		}

		let action: [string, string, Parameters<any>] | null = null;
		return Promise.resolve()
			.then(() => {
				if (typeof handler !== 'function') {
					return handler;
				}

				const hookResult = (handler as Function).apply(context, parameters); // 执行插件对应的 hookName 钩子

				if (!hookResult?.then) {
					return hookResult;
				}

				action = [plugin.name, hookName, parameters]; // 如果钩子返回 Promise 存储对应操作
				this.unfulfilledActions.add(action);
				
				return Promise.resolve(hookResult).then(result => { 
					// action was fulfilled
					this.unfulfilledActions.delete(action!); // 钩子返回 Promise 完成清除对应未 fullfilled 的操作
					return result;
				});
			})
			.catch(error_ => {
				if (action !== null) {
					this.unfulfilledActions.delete(action);
				}
				return error(errorPluginError(error_, plugin.name, { hook: hookName }));
			});
	}

和 webpack 插件系统区别

  • Rollup 通过抽象化一个 PluginDriver 的实例负责专门驱动插件的调用,并且 PluginDriver 和 Graph 绑定,能够共享打包过程的信息;webpack 通过 tapable 进行订阅发布,本身可以脱离 webpack 使用
  • 在运行模式上,Rollup 是运行时根据 hookName 收集对应的插件 ,然后对插件进行排序后存储,通过 runHook 或 runHookSync 进行调用;webpack 通过订阅发布,先注册插件,然后在生命周期的流程中调用。总的来说 Rollup 的运行时收集比起 webpack 具有一点点内存优势
相关推荐
小白菜学前端几秒前
Threejs 材质贴图、光照和投影详解
前端·3d·three.js
随遇而安622&50812 分钟前
分布式微服务项目,同一个controller方法间的转发导致cookie丢失,报错null pointer异常
分布式·微服务·架构·bug
浮华似水14 分钟前
Docker入门系列——Docker-Compose
前端
真的很上进21 分钟前
⚡️如何在 React 和 Next.js 项目里优雅的使用 Zustand
java·前端·javascript·react.js·前端框架·vue·es6
小牛itbull21 分钟前
ReactPress 安装指南:从 MySQL 安装到项目启动
前端·javascript·数据库·mysql·react.js·开源·reactpress
@大迁世界25 分钟前
释放 PWA 的力量:2024 年的现代Web应用|React + TypeScript 示例
前端·javascript·react.js·前端框架·ecmascript
宅博士小陈25 分钟前
NodeJS的安装 npm 配置和使用 Vue-cli安装 Vue项目介绍
前端·javascript·vue.js
布兰妮甜33 分钟前
Angular框架:构建现代Web应用的全面指南
前端·javascript·前端框架·angular.js
未命名冀1 小时前
微服务day07
微服务·架构·jenkins
雪碧聊技术1 小时前
01-Ajax入门与axios使用、URL知识
前端·javascript·ajax·url·axios库