ARTICLE DETAIL

资讯详情

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

OHIF v3 Managers 系统详解:扩展注册、服务管理、命令执行与快捷键绑定

OHIF v3 Managers 系统详解:扩展注册、服务管理、命令执行与快捷键绑定 OHIF v3 Managers 系统详解扩展注册、服务管理、命令执行与快捷键绑定【免费下载链接】ViewersOHIF zero-footprint DICOM viewer and oncology specific Lesion Tracker, plus shared extension packages项目地址: https://gitcode.com/GitHub_Trending/vi/Viewers在 OHIF v3 的零足迹zero-footprintDICOM 查看器架构中Managers管理器是一组负责关键应用基础设施的类它们承担扩展注册、服务管理、命令执行与快捷键绑定等职责将平台、扩展Extension与模式Mode之间的功能协调在一起。本文将围绕 Managers 官方文档 的主线结合ohif/coreplatform/core与platform/app的实际源码深入剖析ExtensionManager、ServicesManager、CommandsManager与HotkeysManager的职责边界、核心 API、生命周期约定与实战用法。读完本文你将掌握 OHIF v3 应用启动时四个管理器是如何被创建并相互协作的能够在自己的扩展中注册服务、命令、快捷键与模块并理解模式进入/退出时的状态管理契约。Managers 系统总览OHIF 使用Managers完成多种目的注册新服务、实现依赖注入dependency injection以及聚合和暴露extension的功能。OHIF-v3 提供了以下四个管理器它们构成了应用基础设施的中枢ManagerDescription职责描述Extension Manager聚合整个应用中各模块与功能特性并向应用暴露Services Manager所有内部与外部服务的统一注册点Commands Manager在特定 context 中注册命令并在应用中执行命令Hotkeys Manager将键盘按键映射到命令四个管理器在应用启动时被集中创建其真实实例化代码位于 platform/app/src/appInit.jsasync function appInit(appConfigOrFunc, defaultExtensions, defaultModes) { const commandsManagerConfig { getAppState: () {}, }; const commandsManager new CommandsManager(commandsManagerConfig); const servicesManager new ServicesManager(commandsManager); const serviceProvidersManager new ServiceProvidersManager(); const hotkeysManager new HotkeysManager(commandsManager, servicesManager); // ... appConfig 归一化默认填充 peerImport / measurementTrackingMode / routerBasename const extensionManager new ExtensionManager({ commandsManager, servicesManager, serviceProvidersManager, hotkeysManager, appConfig, }); servicesManager.setExtensionManager(extensionManager); servicesManager.registerServices([...]); // ... }从实例化顺序可以清晰看出管理器之间的依赖关系CommandsManager最先创建不依赖其他管理器ServicesManager依赖CommandsManagerHotkeysManager依赖前两者而ExtensionManager聚合了其余所有管理器以及应用的appConfig。创建完ExtensionManager后还会调用servicesManager.setExtensionManager(extensionManager)建立反向引用使服务在创建时也能访问到扩展管理器见 ServicesManager.ts。Extension Manager扩展功能的聚合中枢ExtensionManager是ohif/core提供的一个类应用只实例化一个单例并在构造时向其传入ServicesManager、CommandsManager以及HotkeysManager、serviceProvidersManager和可选的appConfig。发布的事件ExtensionManager继承自PubSubService会发布以下事件见 ExtensionManager.ts事件描述ACTIVE_DATA_SOURCE_CHANGEDevent::activedatasourcechanged当活动数据源被更换时触发——可能是切换为完全不同的数据源也可能是通过updateDataSourceConfiguration修改了现有活动数据源的定义公共 APIExtensionManager的公共 API 包括setActiveDataSource(dataSource)设置应用的当前活动数据源若目标数据源名与当前相同则直接返回否则更新_activeDataSourceName并广播ACTIVE_DATA_SOURCE_CHANGED事件getDataSources(dataSourceName?)返回已注册的数据源不传参时默认返回活动数据源getDataSourceInstance(dataSourceName)返回数据源的实例dataSourceMap[name][0]getActiveDataSource()/getActiveDataSourceOrNull()返回当前活动数据源getModuleEntry(stringEntry)根据形如${extensionId}.${moduleType}.${moduleName}的字符串键返回对应的模块条目getModulesByType(moduleType)返回所有已注册扩展中某类模块的数组addDataSource(dataSourceDef, options)动态添加数据源可通过{ activate: true }将其设为活动数据源updateDataSourceConfiguration(dataSourceName, dataSourceConfiguration)更新指定名称数据源的配置通过_createDataSourceInstance重建实例若为活动数据源则广播事件getDataSourceDefinition(dataSourceName?)获取某数据源的定义不传参时返回活动数据源定义registerExtensions(extensions, dataSources)/registerExtension(extension, configuration, dataSources)注册扩展及其模块onModeEnter()/onModeExit()驱动服务与扩展的模式生命周期钩子。通过 getModuleEntry 访问模块应用通过getModuleEntry按 ID 查找模式Mode配置中引用的面板等模块。例如extensionManager.getModuleEntry(ohif/extension-measurement-tracking.panelModule.seriesList)该调用会访问ohif/extension-measurement-tracking扩展中panelModule的seriesList面板。真实代码中processExtensionModule会将每个模块元素以${extensionId}.${moduleType}.${element.name}为键写入modulesMap见 ExtensionManager.ts。实际用法如下const getPanelData id { const entry extensionManager.getModuleEntry(id); const content entry.component; return { iconName: entry.iconName, iconLabel: entry.iconLabel, label: entry.label, name: entry.name, content, }; };扩展注册流程与模块类型registerExtension是扩展进入系统的入口其完整流程ExtensionManager.ts为校验extension.id并防止重复注册同一扩展 ID若扩展提供了preRegistration钩子先执行它这是扩展注册自定义服务的标准时机收集扩展的onModeEnter/onModeExit生命周期钩子遍历MODULE_TYPES中定义的所有模块类型通过get 首字母大写模块名 的方式如getCommandsModule、getPanelModule获取扩展模块并按类型分发commandsModule→_initCommandsModule自动注册命令与上下文dataSourceModule→_initDataSourcesModule将数据源定义与扩展模块绑定hangingProtocolModule→_initHangingProtocolsModule协议非空时自动注册到HangingProtocolServicepanelModule、toolbarModule等 → 写入modulesMaptoolbar 还支持evaluate函数注册到ToolbarService将扩展 ID 记录到registeredExtensionIds。从源码结构可以推断扩展可以按需提供getCommandsModule、getViewportModule、getUtilityModule、getCustomizationModule、getSopClassHandlerModule、getToolbarModule、getPanelModule、getHangingProtocolModule、getDataSourceModule等模块工厂函数ExtensionManager会自动识别并注册。模式生命周期编排ExtensionManager.onModeEnter()与onModeExit()负责协调服务与扩展的生命周期ExtensionManager.ts进入模式时先调用所有服务去重后的唯一实例列表的onModeEnter再调用各扩展的onModeEnter。这样能先把服务重置到标准状态再由扩展恢复其缓存的数据退出模式时先调用各扩展的onModeExit允许其保存/恢复数据再调用服务的onModeExit确保扩展有最后的机会落盘持久化数据。Services Manager统一的服务注册中心ServicesManager是服务的唯一注册点。每个服务都必须实现一个create方法ServicesManager在注册时调用它来实例化服务应用内可通过ServicesManager的services属性访问已注册的服务。核心骨架ServicesManager的真实实现位于 platform/core/src/services/ServicesManager.ts下面是其简化骨架两个公共方法export default class ServicesManager { constructor(commandsManager) { this._commandsManager commandsManager; this.services {}; this.registeredServiceNames []; } registerService(service, configuration {}) { /** validation checks **/ this.services[service.name] service.create({ configuration, commandsManager: this._commandsManager, }); /* Track service registration */ this.registeredServiceNames.push(service.name); } registerServices(services) { /** ... **/ } }真实实现还包含若干健壮性细节service或service.name为空时打印警告并提前返回同名服务重复注册时直接跳过防止重复实例支持altName别名注册如StudyPrefetcherService与studyPrefetcherServicecreate工厂函数不存在时同样提前返回。registerServices支持传[service, configuration]二元组数组以便在批量注册时为单个服务附带配置。默认注册的服务OHIF-v3在appInit中默认注册的服务如下platform/app/src/appInit.jsservicesManager.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, ]);其中MultiMonitorService与CustomizationService以[服务, 配置]二元组的形式注册将appConfig.multimonitor与appConfig.customizationService注入服务。这些服务的实现均位于 platform/core/src/services 目录下除上述服务外还有DicomMetadataStore、StudyPrefetcherService、UserAuthenticationService、ServiceProvidersManager、_sharedPubSub 接口等基础设施。服务架构name create打开每个服务实现的文件夹可以发现服务必须以包含name与create方法的对象形式导出。例如ToolBarService的导出import ToolBarService from ./ToolBarService; export default { name: ToolBarService, create: ({ configuration {}, commandsManager }) { return new ToolBarService(commandsManager); }, };而ToolBarService的实现类位于同一目录下。需要特别说明注意create方法对任何自定义服务都是关键想加入服务列表就必须实现它。注意对于 TypeScript 定义服务类型应作为模块的Types导出的一部分。这是后续推荐的做法现有服务将逐步迁移。此外服务名称name应使用小驼峰lower camel case类型名使用大驼峰upper camel case。以上例而言服务实例应命名为toolBarService类命名为ToolBarService。访问服务整个应用中都可以通过服务管理器的services属性访问所需服务。例如在longitudinal模式的右侧面板PanelMeasurementTableTracking中导出测量数据的简化代码如下function PanelMeasurementTableTracking({ servicesManager }) { const { MeasurementService } servicesManager.services; /** ... **/ async function exportReport() { const measurements MeasurementService.getMeasurements(); /** ... **/ downloadCSVReport(measurements, MeasurementService); } /** ... **/ return /** ... **/ /; }注册自定义服务如果你需要在扩展中编写自己的服务preRegistration钩子是注册自定义服务的标准位置import WrappedBackEndService from ./services/backEndService; export default { // ID of the extension id: myExtension, preRegistration({ servicesManager }) { servicesManager.registerService(WrappedBackEndService(servicesManager)); }, };服务逻辑的写法注意命名约定类名大驼峰BackEndService服务名小驼峰backEndService// Canonical name of upper camel case BackEndService for the class import BackEndService from ./BackEndService; export default function WrappedBackEndService(servicesManager) { return { // Note the canonical name of lower camel case backEndService name: backEndService, create: ({ configuration {} }) { return new BackEndService(servicesManager); }, }; }服务实现类export default class BackEndService { constructor(servicesManager) { this.servicesManager servicesManager; } putAnnotations() { return post(/*...*/); } }类型注册便于消费方获得类型提示import BackEndService from ../services/BackEndService/BackEndService; export { BackEndService };服务的模式生命周期契约服务可以实现针对模式的初始化和清理逻辑。为了防止首次展示与再次进入同一模式之间的状态差异缺陷服务契约要求无论模式是全新进入还是退出后再次进入服务在进入模式时应处于相同的状态。要实现状态的保存/恢复模式必须在退出模式时保存数据并在其onModeEnter中恢复数据。例如模式可以在onModeExit中保留测量数据并在onModeEnter中恢复它。这不违反契约因为是否应用已缓存状态是模式自己的决定。onModeEnter服务可实现onModeEnter来初始化自身以准备进入模式。它会在模式自身的onModeEnter之前被调用onModeExit进入模式时服务契约要求服务在全新加载与重复进入时状态一致。onModeExit允许服务在模式的onModeExit保存完持久化数据之后进行自我清理。Commands Manager按上下文分发命令CommandsManager是ohif/core中定义的类。它跟踪限定在某个上下文context内的命名命令或函数。当尝试运行某个名称的命令时会按照指定的顺序在活动上下文中查找它找到后执行该命令并传入命令定义中指定的应用级或调用级数据。上下文的优先级规则需要重点理解的是查找顺序查找顺序与模式依赖中模块的注册顺序相反——最后注册的模块优先级最高最先被搜索。这与命令本身何时注册无关不过不同模块在同一上下文中注册同名命令时也是后注册者覆盖先注册者last registration wins。核心骨架CommandsManager的实现位于 platform/core/src/classes/CommandsManager.ts简化骨架如下export class CommandsManager { contexts {}; contextOrder []; constructor(_ignoredConfig) {} getContext(contextName) { const context this.contexts[contextName]; return context; } createContext(contextName) { /** ... **/ this.contexts[contextName] {}; this.contextOrder.push at beginning(contextName) } registerCommand(contextName, commandName, definition) { /**...**/ const context this.getContext(contextName); /**...**/ context[commandName] definition; } getCommand(commandName, contextName) { const useContext contextName || first context having commandName in contextOrder return useContext[commandName]; } runCommand(commandName, options {}, contextName) { const definition this.getCommand(commandName, contextName); /**...**/ const { commandFn } definition; const commandParams Object.assign( {}, definition.options, // Command configuration options // Time of call info ); /**...**/ return commandFn(commandParams); } /**...**/ }源码层面可以补充的细节CommandsManager.tscreateContext通过this.contextOrder.splice(0, 0, contextName)将新上下文插入到列表开头——这正是最后注册者优先级最高的实现基础若上下文已存在会调用clearContext清空其命令registerCommand支持直接传入函数自动包装为{ commandFn: definition, options: {} }并会对contextName/commandName做__proto__、constructor、prototype等键名校验防止原型链污染命令执行时runCommand将定义中的options命令配置与调用时传入的options调用时信息通过Object.assign合并再调用commandFn(commandParams)并返回结果。命令/上下文的自动注册ExtensionManager负责注册命令并创建上下文因此你通常无需手动注册所有命令。只需在扩展中创建commandsModule它就会被自动注册到所提供的context中。简化版注册过程如下export default class ExtensionManager { constructor({ commandsManager }) { this._commandsManager commandsManager } /** ... **/ registerExtension (extension, configuration {}, dataSources []) { let extensionId extension.id /** ... **/ moduleTypeNames.forEach((moduleType) { const extensionModule this._getExtensionModule( moduleType, extension, extensionId, configuration ) if (moduleType commandsModule) { this._initCommandsModule(extensionModule) } /** registering other modules **/ }) } _initCommandsModule (extensionModule) { let { definitions, defaultContext } extensionModule defaultContext defaultContext || VIEWER if (!this._commandsManager.getContext(defaultContext)) { this._commandsManager.createContext(defaultContext) } Object.keys(definitions).forEach((commandName) { const commandDefinition definitions[commandName] const commandHasContextThatDoesNotExist commandDefinition.context !this._commandsManager.getContext(commandDefinition.context) if (commandHasContextThatDoesNotExist) { this._commandsManager.createContext(commandDefinition.context) } this._commandsManager.registerCommand( commandDefinition.context || defaultContext, commandName, commandDefinition ) }) } }从实际源码看_initCommandsModuleExtensionManager.ts还有两处增强definitions为空时打印警告并跳过命令定义若是函数会自动包装为{ commandFn }对象。此外ExtensionManager还提供registerCommandsModule(commandsModule, defaultContext VIEWER)方法供模式在应用初始化时注册命令模块例如在工作列表阶段就需要可用的命令其内部同样走_initCommandsModule路径。手动注册命令/上下文如果你确实遇到无法通过扩展注册命令的场景可以使用CommandsManager的 API 手动注册// Command Registration commandsManager.registerCommand(context, name, commandDefinition); // Context Creation commandsManager.createContext(string);CommandsManager 公共 API在消费应用或扩展中运行命令使用runCommand(commandName, options {}, contextName)// Run a command, it will run all the speak commands in all contexts commandsManager.runCommand(speak, { command: hello }); // Run command, from Default context commandsManager.runCommand(speak, { command: hello }, DEFAULT); // Returns all commands for a given context commandsManager.getContext(string);注意不指定contextName时runCommand会依据contextOrder创建顺序的逆序在第一个含有该命令的上下文中执行若指定了contextName则只在该上下文中查找。CommandsManager的行为测试位于 platform/core/src/classes/CommandsManager.test.js可作为理解上下文查找与覆盖规则的参考。Hotkeys Manager键盘快捷键绑定HotkeysManager负责快捷键的添加、设置与启用/禁用等全部逻辑。实例化HotkeysManager与其他管理器一样在appInit中实例化其内部基于 Mousetrap.js 封装见 HotkeysManager.tsconst commandsManager new CommandsManager(commandsManagerConfig); const servicesManager new ServicesManager(commandsManager); const hotkeysManager new HotkeysManager(commandsManager, servicesManager); const extensionManager new ExtensionManager({ commandsManager, servicesManager, hotkeysManager, appConfig, });HotkeysManager APIsetHotkeys(hotkeyDefinitions)HotkeysManager中最关键的方法负责将按键与命令绑定。绑定失败时会通过UINotificationService弹出错误提示setDefaultHotKeys(hotkeys)设置defaultHotkeys属性。注意此方法不会立即绑定传入的快捷键只有当restoreDefaultBindings被调用时提供的默认快捷键才会被绑定restoreDefaultBindings()将当前hotkeyDefaults重新绑定内部即this.setHotkeys(this.hotkeyDefaults)destroy()重置HotkeysManager移除已设置的快捷键并清空defaultHotkeys其他辅助方法record(event)暴露 Mousetrap 的录制能力用于快捷键偏好面板录制新键位、cancel()、disable()/enable()通过mouseTrapAPI.pause()/unpause()全局暂停/恢复快捷键监听。HotkeysManager还会发布HOTKEY_PRESSEDevent::hotkeysManager:hotkeyPressed事件并支持通过 PubSub 接口订阅便于界面或日志系统响应按键事件。快捷键定义结构一个快捷键定义HotkeyDefinition应包含以下属性commandName已注册命令的名称commandOptions传给命令的额外参数keys定义要绑定到命令的按键数组遵循 Mousetrap.js 的绑定语法label在快捷键偏好面板中显示的标签isEditable用户是否可以在快捷键面板中编辑该键位。默认快捷键绑定默认键位绑定定义在 platform/core/src/defaults/hotkeyBindings.ts// platform/core/src/defaults/hotkeyBindings.ts export default [ /**..**/ { commandName: setToolActive, commandOptions: { toolName: Zoom }, label: Zoom, keys: [z], isEditable: true, }, { commandName: flipViewportVertical, label: Flip Vertically, keys: [v], isEditable: true, }, /**..**/ ];从实际文件看默认绑定还覆盖了scaleUpViewport、scaleDownViewport-、fitViewportToWindow、rotateViewportCWr、rotateViewportCCWl、flipViewportHorizontalh、invertViewporti、incrementActiveViewportright、decrementActiveViewportleft等其中toggleCinec未标记isEditable。HotkeysManager在构造时还会调用migrateOldHotkeyDefinitions检查旧格式的快捷键定义并自动迁移。其行为测试位于 platform/core/src/classes/HotkeysManager.test.js。全局 vs 模式特定快捷键你可以设置全局快捷键并使用自定义服务CustomizationService的$set方法覆盖它们从而在特定模式下提供不同的键位绑定。这也是 OHIF 通过 customization 机制实现模式级快捷键覆盖的推荐路径。结语四个 Manager 构成了 OHIF v3 运行时的心脏CommandsManager提供了按上下文隔离的命令分发与优先级机制ServicesManager以name create的契约统一管理内置与自定义服务并约束了模式进入/退出时的状态一致性HotkeysManager基于 Mousetrap 把按键绑定到命令并提供默认键位与用户自定义能力而ExtensionManager则把三者与数据源、面板、工具栏、挂片协议等模块聚合起来并通过getModuleEntry向整个应用暴露统一的功能入口。理解这四者及其协作关系是深入阅读 OHIF v3 源码、编写自定义扩展与模式的第一步建议进一步阅读 Extension Manager、Service Manager、Commands Manager 与 Hotkeys Manager 的分篇文档并结合 ExtensionManager.ts、ServicesManager.ts、CommandsManager.ts、HotkeysManager.ts 等源码深入验证。【免费下载链接】ViewersOHIF zero-footprint DICOM viewer and oncology specific Lesion Tracker, plus shared extension packages项目地址: https://gitcode.com/GitHub_Trending/vi/Viewers创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表