ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Rolldown 插件上下文 this.load 深度解析:预加载模块、避免死锁与代理模块实战

Rolldown 插件上下文 this.load 深度解析:预加载模块、避免死锁与代理模块实战 Rolldown 插件上下文 this.load 深度解析预加载模块、避免死锁与代理模块实战【免费下载链接】rolldownFast Rust bundler for JavaScript/TypeScript with Rollup-compatible API.项目地址: https://gitcode.com/GitHub_Trending/ro/rolldownthis.load是 Rolldown 插件系统中与resolveId等钩子协同工作的核心上下文方法它允许插件在模块尚未被其他模块引用时提前读取并解析其最终内容。本文基于 plugin-context-load.md 与 plugin-context.ts 源码系统讲解其工作原理、与resolveId的配合方式、resolveDependencies参数、模块副作用处理以及循环依赖下的死锁陷阱并给出可运行的代理模块插件完整示例。什么是this.load在解析阶段提前洞察模块内容在 Rolldown 的插件生命周期中resolveId钩子负责把导入语句中的 source 字符串解析成模块 id而load/transform钩子负责真正读取并转换模块内容。通常情况下插件只有在load/transform阶段才能看到模块代码但如果一个插件需要在决定如何解析某个模块之前就知道它的最终内容就需要用到this.load。this.load允许插件按模块 id 主动加载并解析一个模块从而提前获取其ModuleInfo包含转换后的code、导出列表exports等信息。它的典型应用场景是在resolveId钩子里检查模块内容若满足特定条件例如包含某个特殊注释、特定导出模式则把解析结果指向一个代理模块proxy module从而实现按需替换、懒加载、代码注入等高级功能。从源码签名看Rolldown 中this.load的定义位于 plugin-context.tsload( options: { id: string; resolveDependencies?: boolean } PartialPartialNullModuleOptions, ): PromiseModuleInfo;它接受一个对象参数id为要加载的模块标识通常来自this.resolve的返回值resolveDependencies控制返回时机详见下文同时可附带与resolveId钩子相同的meta、moduleSideEffects等模块选项。对应的 Rust 侧实现位于 binding_plugin_context.rs它通过try_get_package_json_or_create推断模块定义格式后调用inner.load(...)最终走的是与正常模块加载完全一致的管线因此提前加载的模块在后续进入模块图时不会产生额外开销——这一点与 Rollup 的语义一致模块只会被解析一次。返回时机Promise 何时 resolvethis.load返回的 Promise 会在以下两个条件同时满足时 resolve模块已经完整经过load→transform→ 解析parse全流程但尚未开始解析它的导入imports。这意味着此时拿到的ModuleInfo中importedIds和dynamicallyImportedIds都是空的。这样设计有一个明确目的避免在resolveId钩子里 awaitthis.load时产生死锁。// 此时返回的 moduleInfo.importedIds 与 dynamicallyImportedIds 均为空 const moduleInfo await this.load(resolution);如果插件关心模块的importedIds/dynamicallyImportedIds文档给出了两种替代方案实现moduleParsed钩子在模块被真正解析完依赖后获取完整信息给this.load传入resolveDependencies: true让返回的 Promise 一直等到所有依赖 id 被解析完成。Rolldown 当前的 JS 侧实现对resolveDependencies的处理是始终以完整解析为目标load内部通过loadModulePromiseMap缓存加载 Promise若模块已解析moduleInfo.code ! null则直接返回现有信息见 plugin-context.ts。与this.resolve的配合把解析结果直接传给 loadthis.load的签名允许直接传入this.resolve的返回值只要该返回值既不是null也不是 externalconst resolution await this.resolve(source, importer, options); // resolution 非 null 且非 external 时整个 resolution 对象可直接传给 this.load if (resolution !resolution.external) { const moduleInfo await this.load(resolution); }这种「先 resolve 再 load」的组合是官方推荐的固定套路原因在于meta和moduleSideEffects两个选项的生效规则与resolveId钩子相同它们的值只有在模块尚未被加载时才会生效。因此必须在加载前先用this.resolve走一遍完整的解析链确认是否有其他插件在resolveId中为这些选项设置了特殊值然后把这些值原样传给this.load否则提前加载很可能会绕过其他插件精心设置的模块元信息。从实现上看Rolldown 的resolve会合并插件在resolveId阶段设置的模块选项并返回给调用方见 plugin-context.tsload内部则通过updateModuleOption把这些选项记录下来plugin-context.ts从而保证this.resolve与this.load之间选项传递的完整性。实战基于代码注释的代理模块插件文档给出了一个完整的实战示例——为包含特殊代码注释/* use proxy */的模块生成代理模块。下面是对该示例的完整保留与逐步解读export default function addProxyPlugin() { return { async resolveId(source, importer, options) { if (importer?.endsWith(?proxy)) { // 不要代理已经在代理里使用的 id return null; } // 确保把 resolveId 的选项透传给 this.resolve 以拿到模块 id const resolution await this.resolve(source, importer, options); // 只能预加载已存在且非 external 的 id if (resolution !resolution.external) { // 把整个 resolution 信息透传给 this.load const moduleInfo await this.load(resolution); if (moduleInfo.code.includes(/* use proxy */)) { return ${resolution.id}?proxy; } } // 既然模块已经完整解析过没有理由再解析一次 return resolution; }, load: { filter: { id: /\?proxy$/ }, handler(id) { const importee id.slice(0, -?proxy.length); // 注意命名空间重导出不会重导出 default 导出 let code console.log(proxy for ${importee}); export * from ${JSON.stringify(importee)};; // 在解析代理时importee 已经完成加载与解析 // 因此可以放心依赖 this.getModuleInfo 的 exports if (this.getModuleInfo(importee).exports.includes(default)) { code export { default } from ${JSON.stringify(importee)};; } return code; }, }, }; }这个插件的完整工作链路如下resolveId 阶段拦截对每个导入调用this.resolve拿到最终模块 id透传原有 options随后调用this.load提前加载该模块内容检测通过返回的moduleInfo.code判断模块源码中是否包含/* use proxy */注释返回代理 id若命中则把解析结果改写为${resolution.id}?proxy使后续load钩子按代理 id 处理代理生成load钩子通过filter: { id: /\?proxy$/ }匹配代理 id生成一段「打印日志 重导出原模块」的代码由于原模块在步骤 1 中已经加载完毕这里可以放心用this.getModuleInfo(importee).exports判断原模块是否有default导出并在有默认导出时手动补一条export { default }语句——因为命名空间重导出export *不会转发 default 导出。几个值得注意的细节代理模块的resolveId分支用importer?.endsWith(?proxy)判断当前导入是否来自代理代码内部避免对代理中使用的 id 再次代理形成无限循环load使用对象形式的钩子{ filter, handler }filter.id为正则/\?proxy$/这是 Rolldown 支持的钩子过滤器写法可参考 hook-filters.md即便没有命中代理条件this.resolve的返回值也会被原样返回此时模块已被提前加载过后续正式加载时不会重复解析这正是文档强调的「没有额外开销」的具体体现。已加载与未加载模块的行为差异文档明确了this.load在两种场景下的不同表现模块已被加载this.load只会等待解析完成然后直接返回该模块的ModuleInfo不会触发额外工作模块尚未被其他模块引用this.load不会自动触发加载该模块导入的其他模块不会级联加载依赖。静态依赖和动态依赖只有在模块被至少实际导入一次后才会被加载。这一点可以在 Rolldown 的测试用例中看到印证在 load/_config.ts 中插件先在load(main.js)阶段通过this.load({ id: foo.js })预加载foo.js随后在transform阶段再次加载同一个 id并断言fooHookCalls仍为 1——证明第二次this.load复用了已加载的模块load钩子没有被再次触发。与此同时该测试还验证了this.load的使用范围限制在buildStart钩子中调用会抛出错误The PluginContext.load only work at resolveId/load/transform/moduleParsed hooks。这是因为this.load依赖模块加载管线上下文只能在构建过程中的特定钩子内调用。警惕循环依赖中的死锁文档专门用一个 warning 强调了死锁风险虽然在resolveId钩子中 awaitthis.load是安全的但在load或transform钩子中 awaitthis.load必须非常谨慎。如果模块图中存在循环依赖而循环中的某个模块又在自己的load/transform里等待this.load加载循环里的另一个模块就很容易造成互相等待的死锁。因此任何插件都需要手动保证不要在与被加载模块处于循环关系中的任何模块的load/transform钩子里等待this.load。Rolldown 对这种误用并非毫无防护。在 plugin-context.ts 中load实现会记录currentLoadingModule如果options.id等于当前正在加载的模块会通过onLog(LOG_LEVEL_WARN, logCycleLoading(...))发出CYCLE_LOADING警告而非直接崩溃由插件自行决定如何处理。对应的测试用例 cycle-load-error/_config.ts 验证了这一行为插件在load钩子中直接this.load({ id })加载自己随后断言onLog收到级别为warn、code 为CYCLE_LOADING的日志。这既是对死锁风险的兜底提醒也是插件作者编写循环安全代码时的调试依据。最佳实践小结综合文档与源码实现使用this.load时应遵循以下原则只接受已解析成功的 id始终先this.resolve并把返回值非null、非 external传给this.load不要手动拼接路径优先在resolveId中使用resolveId中 awaitthis.load是安全的在load/transform中使用前先评估模块图是否存在循环透传模块选项this.resolve返回结果中携带的meta、moduleSideEffects应原样传给this.load避免绕过其他插件的配置需要依赖信息时用resolveDependencies: true或moduleParsed默认返回的ModuleInfo中importedIds/dynamicallyImportedIds为空若需要这些字段应显式开启resolveDependencies或改用moduleParsed钩子避免在循环关系内等待若模块 A 与 B 存在循环依赖不要在 A 的load/transform里await this.load(B)否则可能死锁。通过将this.load与this.resolve组合使用插件可以在解析阶段就获得模块的最终形态为代理模块、按需加载、内容驱动的代码替换等高级场景提供可靠的实现基础。【免费下载链接】rolldownFast Rust bundler for JavaScript/TypeScript with Rollup-compatible API.项目地址: https://gitcode.com/GitHub_Trending/ro/rolldown创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表