ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

OHIF Viewer 扩展生命周期钩子完全指南:preRegistration、onModeEnter 与 onModeExit 的调用机制与实战

OHIF Viewer 扩展生命周期钩子完全指南:preRegistration、onModeEnter 与 onModeExit 的调用机制与实战 OHIF Viewer 扩展生命周期钩子完全指南preRegistration、onModeEnter 与 onModeExit 的调用机制与实战【免费下载链接】ViewersOHIF zero-footprint DICOM viewer and oncology specific Lesion Tracker, plus shared extension packages项目地址: https://gitcode.com/GitHub_Trending/vi/ViewersOHIF Viewer本项目采用扩展Extensions架构构建可插拔的医学影像查看器而生命周期钩子则是扩展与平台核心交互的三个关键时机点。本文将围绕preRegistration、onModeEnter、onModeExit三个钩子结合 ExtensionManager 源码、单元测试 以及仓库内多个真实扩展如cornerstone-dicom-sr、tmtv、cornerstone的实现深入讲解每个钩子的调用时机、接收参数、适用场景与编写规范帮助你写出初始化正确、切换模式无残留、退出清理到位的扩展代码。Overview三个钩子各司其职在 OHIF-v3 的扩展体系中扩展是一个包含id、可选version以及若干模块module与生命周期钩子的普通 JavaScript/TypeScript 对象扩展骨架结构可参考 扩展介绍文档。当前扩展支持三个生命周期钩子preRegistration在ExtensionManager注册扩展的任何模块之前被调用用于初始化扩展的全局状态、引导第三方库、接线服务与命令onModeEnter在进入一个新模式或模式的数据/数据源被切换时被调用用于初始化该模式需要的数据onModeExit在导航离开一个模式时被调用用于清理数据与任务例如反注册服务、移除无需持久化的标注等。从类型定义看这三个钩子都是扩展对象上的可选方法在 ExtensionManager.ts 的Extension接口中声明为export interface Extension { id: string; preRegistration?: (p: ExtensionParams) Promisevoid | void; // ... 各 getXxxModule 方法 onModeEnter?: (p: AppTypes) void; onModeExit?: (p: AppTypes) void; }其中preRegistration既可以返回Promiseasync 函数也可以同步返回onModeEnter与onModeExit为同步回调。preRegistration模块注册前的初始化窗口调用时机如果扩展定义了preRegistration钩子它会在ExtensionManager注册该扩展的任何模块之前被调用。这个钩子是async函数可以在其中完成初始化第三方库注册事件监听器添加或调用服务services添加或调用命令commands。从源码实现看ExtensionManager.registerExtensionExtensionManager.ts的执行顺序是先校验扩展存在性与id唯一性重复注册会被跳过并输出警告然后率先执行preRegistration随后才进入各模块的注册流程// preRegistrationHook if (extension.preRegistration) { await extension.preRegistration({ servicesManager: this._servicesManager, serviceProvidersManager: this._serviceProvidersManager, commandsManager: this._commandsManager, hotkeysManager: this._hotkeysManager, extensionManager: this, appConfig: this._appConfig, configuration, }); } if (extension.onModeEnter) { this._extensionLifeCycleHooks.onModeEnter[extensionId] extension.onModeEnter; } if (extension.onModeExit) { this._extensionLifeCycleHooks.onModeExit[extensionId] extension.onModeExit; }也就是说先跑生命周期钩子再注册模块且onModeEnter/onModeExit也会在此时被收集进_extensionLifeCycleHooks注册表按扩展 id 索引供后续模式切换时统一调度。接收参数preRegistration接收一个对象包含ExtensionManager关联的ServicesManager、CommandsManager以及扩展在注册时提供的configuration。结合源码完整参数还包括serviceProvidersManager、hotkeysManager、extensionManager与appConfig。这一行为被 ExtensionManager.test.js 中的单元测试明确锁定it(calls preRegistration() passing dependencies and extension configuration to extension, () { const extensionConfiguration { config: Some configuration }; const extension { id: 1, preRegistration: jest.fn() }; extensionManager.registerExtension(extension, extensionConfiguration); // 断言参数对象包含 servicesManager / commandsManager / // extensionManager / appConfig / configuration expect(extension.preRegistration.mock.calls[0][0]).toEqual({ servicesManager, commandsManager, extensionManager, appConfig, configuration: extensionConfiguration, }); });注意configuration的传递方式registerExtensions支持以[扩展, 配置]元组形式传入配置同样在 ExtensionManager.test.js 中有测试覆盖registerExtension再把它原样交给preRegistration。这意味着你可以在pluginConfig.json或应用初始化时为扩展注入运行时配置并在preRegistration中消费它。典型用法一注册一个全新的服务原文档给出的经典场景是在preRegistration中注册一个新服务让整个应用可用。服务本身的定义如下创建工厂接收servicesManager产出具有name与create的注册描述符// new service inside new extension import MyNewService from ./MyNewService; export default function MyNewServiceWithServices(servicesManager) { return { name: MyNewService, create: ({ configuration {} }) { return new MyNewService(servicesManager); }, }; }然后在扩展入口的preRegistration中通过servicesManager.registerService(...)完成注册import MyNewService from ./MyNewService export default { id, /** * param {object} params * param {object} params.configuration * param {ServicesManager} params.servicesManager * param {CommandsManager} params.commandsManager * returns void */ async preRegistration({ servicesManager, commandsManager, configuration }) { console.log(Wiring up important stuff.); window.importantStuff () { console.log(configuration); }; console.log(Important stuff has been wired.); window.importantStuff(); // Registering new services servicesManager.registerService(MyNewService(servicesManager)); }, };服务注册后即可通过servicesManager.services.MyNewService不同服务以不同键注册在应用各处被访问。若需要更完整地了解 OHIF-v3 的服务机制可参考 服务管理文档。典型用法二仓库内真实扩展的 preRegistrationpreRegistration并非理论概念仓库内多个官方维护扩展都实际使用了它tmtv 扩展入口 解构了servicesManager, commandsManager, extensionManager, configuration {}在进入 TMTV 模式前完成工具组与服务的接线measurement-tracking 扩展入口 在preRegistration中通过servicesManager初始化测量跟踪相关的上下文与服务dicom-microscopy 扩展入口 使用async preRegistration({ servicesManager })异步完成显微镜扩展所需服务的初始化。onModeEnter模式进入时的数据初始化调用时机如果扩展定义了onModeEnter钩子它会在进入一个新模式或模式的数据/数据源被切换时被调用。典型用途是初始化数据——例如文档中提到的 DICOM 结构化报告扩展cornerstone-dicom-sr正是利用onModeEnter在每次新模式进入后重建重新水合显示集displaySets。与服务的调用顺序服务先扩展后ExtensionManager.onModeEnterExtensionManager.ts的实现揭示了一个重要的时序约定先调用所有服务的onModeEnter再调用各扩展的onModeEnterpublic onModeEnter(): void { const services this.getUniqueServicesList(_servicesManager); // The onModeEnter of the service must occur BEFORE the extension // onModeEnter in order to reset the state to a standard state // before the extension restores and cached data. for (const service of services) { service?.onModeEnter?.(); } registeredExtensionIds.forEach(extensionId { const onModeEnter _extensionLifeCycleHooks.onModeEnter[extensionId]; if (typeof onModeEnter function) { onModeEnter({ servicesManager: _servicesManager, commandsManager: _commandsManager, hotkeysManager: _hotkeysManager, extensionManager: this, }); } }); }源码注释明确说明了这一顺序的原因服务必须先重置到标准状态扩展随后再恢复/重建其缓存数据。此外在应用层面Mode.tsx 中的setupRouteInit会在extensionManager.onModeEnter({ servicesManager, extensionManager, commandsManager, appConfig })之后继续调用模式自身的onModeEnterMode.tsx完成工具条注册等模式级初始化——因此整体顺序是服务 → 扩展 → 模式。典型用法cornerstone-dicom-sr 重水合 SR 显示集原文档给出了cornerstone-dicom-sr的onModeEnter示例其核心逻辑是取出DisplaySetService的显示集缓存过滤出由本扩展 SOP Class Handler 管理的 SR 显示集将isHydrated重置为false从而在新模式路由下允许 SR 被再次水合。仓库内的真实实现位于 extensions/cornerstone-dicom-sr/src/onModeEnter.tsx它在文档示例基础上做了增强——同时匹配 2D 与 3D 两个 SOP Class Handler idimport { SOPClassHandlerId, SOPClassHandlerId3D } from ./id; export default function onModeEnter({ servicesManager }) { const { displaySetService } servicesManager.services; const displaySetCache displaySetService.getDisplaySetCache(); const srDisplaySets [...displaySetCache.values()].filter( ds ds.SOPClassHandlerId SOPClassHandlerId || ds.SOPClassHandlerId SOPClassHandlerId3D ); srDisplaySets.forEach(ds { // New mode route, allow SRs to be hydrated again ds.isHydrated false; }); }该钩子在 cornerstone-dicom-sr 扩展入口 中被挂载到扩展对象上导出。作为对比原文档中的教学示例省略了 3D handler 与values()展开如下逻辑本质一致export default { id: ohif/extension-cornerstone-dicom-sr, onModeEnter({ servicesManager }) { const { DisplaySetService } servicesManager.services; const displaySetCache DisplaySetService.getDisplaySetCache(); const srDisplaySets displaySetCache.filter( ds ds.SOPClassHandlerId SOPClassHandlerId ); srDisplaySets.forEach(ds { // New mode route, allow SRs to be hydrated again ds.isHydrated false; }); }, };onModeExit离开模式时的清理回收调用时机与目的如果扩展定义了onModeExit钩子它会在导航离开一个模式时被调用。这个钩子用于清理数据任务例如反注册服务、移除不需要持久化的标注等。由于进入下一个模式前并不确定未来会使用哪个模式退出后的状态应当与干净启动时的状态一致。与服务的调用顺序扩展先服务后与onModeEnter恰好相反ExtensionManager.onModeExitExtensionManager.ts先调用所有扩展的onModeExit再调用服务的onModeExitpublic onModeExit(): void { registeredExtensionIds.forEach(extensionId { const onModeExit _extensionLifeCycleHooks.onModeExit[extensionId]; if (typeof onModeExit function) { onModeExit({ servicesManager: _servicesManager, commandsManager: _commandsManager, }); } }); // The service onModeExit calls must occur after the extension ones // so that extension ones can store/restore data. for (const service of services) { try { service?.onModeExit?.(); } catch (e) { console.warn(onModeExit caught, e); } } }源码注释解释了这一顺序扩展的onModeExit需要先执行以便存取数据随后才轮到服务清理同时服务的退出调用被try/catch包裹避免单个服务清理失败阻断整体流程。另外应用层 Mode.tsx 在路由卸载时的清理顺序为模式自身onModeExit→ 销毁热键 → 退订路由订阅 → 最后extensionManager.onModeExit()从而保证任何残留事件不会在清理过程中引发异常。典型用法原文档给出的onModeExit示例极为简洁展示了如何通过解构出的servicesManager/commandsManager清理缓存export default { id: myExampleExtension, onModeExit({ servicesManager, commandsManager }) { myCacheService.purge(); }, };仓库内的真实案例同样印证了这一模式。例如 cornerstone 扩展入口 定义了onModeExit来收尾渲染相关状态default 扩展入口 也实现了onModeExit。更典型的场景出现在服务层——SegmentationService、ToolGroupService、ColorbarService 等均实现了onModeExit()在模式退出时重置工具组、分段与颜色条状态并有对应的 SegmentationService 单元测试 验证其行为。三个钩子的对比与最佳实践钩子调用时机主要用途参数要点调用顺序相对服务preRegistration扩展任何模块注册之前应用初始化阶段初始化第三方库、注册事件监听、添加/调用服务与命令servicesManager、commandsManager、configuration另含serviceProvidersManager、hotkeysManager、extensionManager、appConfig独立于服务模块注册前onModeEnter进入新模式 / 模式数据或数据源切换初始化模式数据如重建显示集、恢复缓存servicesManager、commandsManager等服务 → 扩展onModeExit导航离开模式清理数据与任务恢复干净启动状态servicesManager、commandsManager扩展 → 服务实战建议preRegistration只做一次性初始化。它随扩展注册执行全应用生命周期只跑一次重复注册会被ExtensionManager去重拦截适合引导全局资源而非模式相关逻辑。模式相关状态放到onModeEnter/onModeExit。进入时重建、离开时清理并确保退出后状态与干净启动一致因为下一个模式是未知的。注意与模式自身钩子的区分。模式mode也有自己的onModeEnter/onModeExit执行顺序为扩展后、模式后进入时以及模式先、扩展后退出时。需要完整时序可进一步阅读 模式生命周期文档。快速生成扩展骨架。官方 CLI 的 extension 模板 已内置preRegistration等钩子的占位实现推荐用ohif/cli创建新扩展而不是手写全部样板。小结扩展生命周期钩子是 OHIF Viewer 扩展体系与平台核心协作的契约点preRegistration在模块注册前完成全局初始化onModeEnter在每次模式进入时重建数据onModeExit在离开模式时恢复干净状态。理解它们的调用时机、参数对象与相对服务的执行顺序进入时服务先扩展后退出时扩展先服务后是编写健壮、可组合扩展的前提。本文涉及的源码与测试均可直接在仓库中查阅ExtensionManager 实现、ExtensionManager 单元测试、cornerstone-dicom-sr 的 onModeEnter 实现 以及 扩展体系总览文档。【免费下载链接】ViewersOHIF zero-footprint DICOM viewer and oncology specific Lesion Tracker, plus shared extension packages项目地址: https://gitcode.com/GitHub_Trending/vi/Viewers创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进