news 2026/9/19 20:49:00

OHIF v3 ServicesManager 服务管理器实战指南:注册、访问、自定义与生命周期

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
OHIF v3 ServicesManager 服务管理器实战指南:注册、访问、自定义与生命周期

OHIF v3 ServicesManager 服务管理器实战指南:注册、访问、自定义与生命周期

【免费下载链接】ViewersOHIF zero-footprint DICOM viewer and oncology specific Lesion Tracker, plus shared extension packages项目地址: https://gitcode.com/GitHub_Trending/vi/Viewers

本文是 OHIF v3 平台 "Managers" 系列的核心篇,聚焦ServicesManager这一服务注册与访问的单一入口。结合 platform/docs/versioned_docs/version-3.11/platform/managers/service.md 与 platform/core/src/services/ServicesManager.ts 的真实实现,你将掌握:内置服务如何被注册、如何在组件与扩展中访问服务、如何编写自定义服务并通过扩展preRegistration挂载,以及模式(Mode)进出时的服务状态生命周期契约。

ServicesManager 概述:服务注册的单一入口

ServicesManager是整个 OHIF v3 应用的服务注册中心。所有功能模块(测量、显示集、挂片协议、工具栏、弹窗等)都以"服务"的形式存在,而ServicesManager是它们唯一的注册点与访问点。

其核心约定十分简单:

  • 每个服务必须实现一个create方法ServicesManager在注册时调用该方法来实例化服务;
  • 实例化完成后,服务会被挂到ServicesManagerservices属性上;
  • 应用内任何位置都可以通过servicesManager.services访问已注册的服务实例。

从源码结构看,ServicesManager持有三个关键成员(见 platform/core/src/services/ServicesManager.ts):

  • services:注册完成后保存所有服务实例的映射表(name -> instance);
  • registeredServiceNames:已注册服务名的数组,用于去重与跟踪;
  • _commandsManager:构造函数注入的命令管理器,在调用create时透传给服务,使服务可以发布/执行命令。
export default class ServicesManager { public services: AppTypes.Services = {}; public registeredServiceNames: string[] = []; private _commandsManager: CommandsManager; private _extensionManager: ExtensionManager; constructor(commandsManager: CommandsManager) { this._commandsManager = commandsManager; this._extensionManager = null; this.services = {}; this.registeredServiceNames = []; } // ... }

ServicesManager还通过setExtensionManager(extensionManager)在应用初始化阶段拿到扩展管理器引用(见 platform/app/src/appInit.js),并在创建服务时一并注入,使服务能够反向使用扩展体系。

Skeleton:registerService 与 registerServices

ServicesManager对外暴露两个公开注册方法:

  • registerService(service, configuration = {}):注册单个服务,可附带配置;
  • registerServices(services):批量注册一组服务。

文档中给出了简化骨架,而当前仓库中的真实实现(platform/core/src/services/ServicesManager.ts)在此之上增加了完整校验逻辑:

public registerService(service, configuration = {}) { if (!service) { log.warn('Attempting to register a null/undefined service. Exiting early.'); return; } if (!service.name) { log.warn(`Service name not set. Exiting early.`); return; } if (this.registeredServiceNames.includes(service.name)) { log.warn(`Service name ${service.name} has already been registered. Exiting before duplicating services.`); return; } if (service.create) { this.services[service.name] = service.create({ configuration, extensionManager: this._extensionManager, commandsManager: this._commandsManager, servicesManager: this, }); if (service.altName) { // TODO - remove this registration this.services[service.altName] = this.services[service.name]; } } else { log.warn(`Service create factory function not defined. Exiting early.`); return; } this.registeredServiceNames.push(service.name); } public registerServices(services) { services.forEach(service => { const hasConfiguration = Array.isArray(service); if (hasConfiguration) { const [ohifService, configuration] = service; this.registerService(ohifService, configuration); } else { this.registerService(service); } }); }

值得注意的四个实现细节:

  1. 四重校验:空服务、缺name、重复注册、缺create工厂函数都会打印log.warn并提前退出,避免产生损坏的半注册状态;
  2. create收到的注入参数:除了configuration,还包含extensionManagercommandsManagerservicesManager自身,服务可以在工厂函数中按需使用这些依赖;
  3. altName别名机制:部分服务(如MeasurementService)会提供altName,注册时同一实例会同时挂载到主名与别名下,保持向后兼容(见下文);
  4. 元组批量注册registerServices支持[service, configuration]元组形式,为单个服务附带专属配置——这是appInit中为MultiMonitorServiceCustomizationServiceStudyPrefetcherService注入应用配置的手段。

这些校验行为全部由单元测试覆盖(见 platform/core/src/services/ServicesManager.test.js),包括空服务警告、缺 name 警告、缺 create 警告、重复注册警告、配置透传等场景,可作为自定义服务开发时的行为参照。

默认注册服务清单

在应用初始化文件 platform/app/src/appInit.js 中,OHIF v3 默认注册了一批核心服务(真实实现比文档示例更完整,部分服务以元组形式携带应用配置):

servicesManager.registerServices([ [MultiMonitorService.REGISTRATION, appConfig.multimonitor], UINotificationService.REGISTRATION, UIModalService.REGISTRATION, UIDialogService.REGISTRATION, UIViewportDialogService.REGISTRATION, MeasurementService.REGISTRATION, DisplaySetService.REGISTRATION, [CustomizationService.REGISTRATION, appConfig.customizationService], ToolbarService.REGISTRATION, ViewportGridService.REGISTRATION, HangingProtocolService.REGISTRATION, CineService.REGISTRATION, UserAuthenticationService.REGISTRATION, PanelService.REGISTRATION, WorkflowStepsService.REGISTRATION, [StudyPrefetcherService.REGISTRATION, appConfig.studyPrefetcher], ]);

这些服务覆盖了 v3 应用运行所需的全部横切能力:

服务职责
MultiMonitorService多显示器布局支持,注册时传入appConfig.multimonitor
UINotificationService全局通知(toast)提示
UIModalService模态弹窗管理
UIDialogService/UIViewportDialogService通用对话框与视口内对话框
MeasurementService测量数据存储、事件发布与报告导出
DisplaySetService显示集(DisplaySet)的注册与检索
CustomizationService定制化配置(自定义 UI/工具行为),传入appConfig.customizationService
ToolbarService工具栏按钮、节与组的管理
ViewportGridService视口网格布局状态管理
HangingProtocolService挂片协议解析与执行
CineService电影循环(CINE)播放
UserAuthenticationService用户认证
PanelService侧边面板管理
WorkflowStepsService工作流步骤管理
StudyPrefetcherService影像预取,注册时传入appConfig.studyPrefetcher

注意:当前仓库 v3 的默认注册清单比文档所列的 11 个服务更多(新增了UserAuthenticationServicePanelServiceWorkflowStepsServiceStudyPrefetcherServiceMultiMonitorService),这是版本演进的结果。若你的应用基于本仓库版本,应以appInit.js的实际清单为准。

服务架构:name+create的对象约定

打开platform/core/src/services目录下的任一服务文件夹,会发现每个服务都以"导出对象"的形式对外暴露,该对象只要求两个字段:namecreate

ToolbarService为例(v3 中实际位于 platform/core/src/services/ToolBarService/index.ts),其真正的注册定义在 platform/core/src/services/ToolBarService/ToolbarService.ts:

public static REGISTRATION = { // ... name: 'toolbarService', create: ({ configuration = {}, commandsManager }) => { return new ToolbarService(commandsManager); }, };

即:ToolbarService类被封装为一个{ name, create }的注册对象,create负责用命令管理器构造服务实例。服务实现类与注册入口位于同一目录。

注意:create方法是任何自定义服务能否被成功注册的关键。没有create的服务会被ServicesManager直接拒绝(log.warn后跳过)。

命名约定:lower camel case 与 upper camel case

从源码中可以总结出官方推荐的命名规范(文档中的 TypeScript 演进建议):

  • 服务实例名(name)使用小驼峰(lower camel case):例如measurementServicetoolbarServicebackEndService
  • 服务类/类型使用大驼峰(upper camel case):例如MeasurementServiceToolbarServiceBackEndService

以 platform/core/src/services/MeasurementService/MeasurementService.ts 为实例,这正是当前服务的标准形态:

public static REGISTRATION = { name: 'measurementService', altName: 'MeasurementService', // 兼容旧版访问方式的别名 create: _options => { return new MeasurementService(); }, };

这种static REGISTRATION静态属性模式在CineServiceDisplaySetServiceHangingProtocolServiceCustomizationServicePanelService等服务中已全面铺开(可在platform/core/src/services下搜索public static REGISTRATION验证),是 v3 推荐的服务导出范式。同时建议将服务类型作为模块的 Types 导出(例如export { BackEndService }),便于消费者获得完整的类型提示。

访问服务:servicesManager.services

应用内任何持有servicesManager的地方,都可以通过services属性解构出目标服务实例。文档中给出的PanelMeasurementTableTracking(longitudinal 模式的右侧测量面板)示例展示了这一访问模式:

function PanelMeasurementTableTracking({ servicesManager }) { const { MeasurementService } = servicesManager.services; // ... async function exportReport() { const measurements = MeasurementService.getMeasurements(); // ... downloadCSVReport(measurements, MeasurementService); } // ... }

由于注册时MeasurementService同时注册了measurementService主名与MeasurementService别名(见上文altName机制),两种写法都可以解构出同一实例,这也是旧版代码能平滑迁移到 v3 的原因之一。访问到实例后,即可调用其公开 API(如getMeasurements())驱动业务逻辑。

注册自定义服务:扩展的 preRegistration 钩子

如果你需要在扩展(Extension)中新增业务服务,文档给出了标准做法:在扩展的preRegistration钩子中调用servicesManager.registerService

扩展入口示例:

// extensions/customExtension/src/index.js import WrappedBackEndService from './services/backEndService'; export default { id: 'myExtension', preRegistration({ servicesManager }) { servicesManager.registerService(WrappedBackEndService(servicesManager)); }, };

服务封装模块(注意name采用小驼峰backEndService,类采用大驼峰BackEndService):

// extensions/customExtension/src/services/backEndService/index.js import BackEndService from './BackEndService'; export default function WrappedBackEndService(servicesManager) { return { name: 'backEndService', create: ({ configuration = {} }) => { return new BackEndService(servicesManager); }, }; }

服务实现类(构造函数中可以持有servicesManager,从而在服务内部访问其他服务):

export default class BackEndService { constructor(servicesManager) { this.servicesManager = servicesManager; } putAnnotations() { return post(/*...*/); } }

类型导出(便于消费者获得类型提示):

// types/index.ts import BackEndService from "../services/BackEndService/BackEndService"; export { BackEndService };

ServicesManager.registerService的真实实现可以推断,create在自定义服务场景下同样会收到{ configuration, extensionManager, commandsManager, servicesManager }四个依赖,因此即使你的工厂函数签名只声明了{ configuration = {} },也仍然可以按需取用其余注入项。

服务模式生命周期:onModeEnter 与 onModeExit

OHIF v3 对服务提出了一个重要的模式状态契约

服务在"模式进入"时的状态,应当与"首次进入"或"退出后再次进入"时保持一致。

这一契约用于防止"首次显示"与"后续显示"之间因状态残留而产生差异缺陷。具体来说:

  • 状态的保存与恢复由模式(Mode)负责:模式在onModeExit时保存持久数据,在onModeEnter时恢复数据。例如模式可以在退出时保留测量数据、进入时恢复它们。这并不违反契约,因为"是否应用已缓存的状态"是模式自己的决策;
  • 满足契约后,onModeExit在模式自身的onModeExit已经存储完持久数据之后执行,服务可以据此清理自己的状态。

两个可选的服务钩子:

  • onModeEnter:服务可以实现该方法,用于初始化服务、为进入某个模式做好准备。它会在模式自身的onModeEnter之前被调用;
  • onModeExit:服务可以实现该方法,用于在模式onModeExit完成持久数据存储后清理自身状态,确保下次进入模式时服务处于干净、确定的状态。

组合起来,完整的生命周期时序是:

进入模式: 服务.onModeEnter → 模式.onModeEnter → ...(使用模式) 退出模式: 模式.onModeExit(保存持久数据)→ 服务.onModeExit(清理临时状态)

这种"模式负责持久化、服务负责重置"的分工,保证了同一个研究(Study)无论被展示一次还是多次,服务侧看到的状态都是一致的,从架构上消除了状态漂移类缺陷。

小结

ServicesManager是 OHIF v3 面向服务的架构基石:

  • 注册:任意服务以{ name, create }对象形态,通过registerService/registerServices注册,create工厂获得configurationcommandsManagerextensionManagerservicesManager四类注入;
  • 访问:全应用通过servicesManager.services按名获取实例,altName保证兼容性;
  • 扩展:自定义服务在扩展preRegistration中注册,遵循"小驼峰实例名 + 大驼峰类型名 + 类型导出"的命名规范;
  • 生命周期:服务可通过onModeEnter/onModeExit与模式协同,维护进入模式时的状态一致性。

进一步阅读:完整实现见 platform/core/src/services/ServicesManager.ts,行为验证见 platform/core/src/services/ServicesManager.test.js,默认服务注册清单见 platform/app/src/appInit.js,各服务的标准REGISTRATION定义可在 platform/core/src/services 目录下逐一查看。

【免费下载链接】ViewersOHIF zero-footprint DICOM viewer and oncology specific Lesion Tracker, plus shared extension packages项目地址: https://gitcode.com/GitHub_Trending/vi/Viewers

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/19 20:48:51

子代理结果没回传?TaoToken 这样改 LangChain model 配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 20:48:33

把 Cursor 的模型通道改到 TaoToken,再对照 FastMCP 的 stdio 通信

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 20:45:46

2026上海紧固件展:产业链创新与数字化转型

1. 展会定位与行业背景2026上海紧固件专业展作为紧固件产业链的年度盛会,其核心价值在于构建覆盖原材料、生产设备、成品件到应用解决方案的全产业链展示平台。当前全球紧固件市场规模已突破1000亿美元,中国作为全球最大的紧固件生产国和消费国&#xff…

作者头像 李华