OHIF v3.9 迁移指南:browserImport 外部库动态加载与 ViewReference 视图导航机制
【免费下载链接】ViewersOHIF zero-footprint DICOM viewer and oncology specific Lesion Tracker, plus shared extension packages项目地址: https://gitcode.com/GitHub_Trending/vi/Viewers
在 OHIF 3.8 升级到 3.9 的过程中,除了核心渲染管线和扩展 API 的变更外,还有一批"其他变更"同样影响着每一个自建部署:外部库(如dicom-microscopy-viewer)不再作为 webpack 构建期依赖打进包内,而是通过全局browserImport函数在浏览器运行时动态加载,并借助pluginConfig.json声明模块来源;同时,跨视图导航从传统的"相机参数跳转"全面转向基于getViewReference/isReferenceViewable/setViewReference的 ViewReference 机制,MPR、Stack、视频与显微镜视口之间的测量跳转行为也因此发生变化。本文基于 9-other.md 展开,逐项说明这两个主题的迁移步骤、配置细节与底层源码原理,帮助你在升级到 3.9 后正确配置外部依赖、理解并规避导航行为变化带来的影响。
外部库的动态加载:从构建期依赖到运行时导入
为什么需要 browserImport
OHIF 3.9 的插件体系(扩展 extension、模式 mode)在构建时会被静态解析。但对于体积较大、更新频繁或由第三方发布的库(典型如整张切片显微图像的渲染库dicom-microscopy-viewer),将其作为构建期依赖会显著拉长构建时间、增大产物体积,也会让"零足迹"的轻量部署变得困难。
迁移文档给出的方案是:这些库改为在运行时通过动态import()按需加载。为了让动态导入不参与 webpack 的依赖静态分析(否则 webpack 仍会尝试打包并可能失败),需要在应用的根 HTML 文件中提供一个全局函数browserImportFunction:
<script> function browserImportFunction(moduleId) { return import(moduleId); } </script>该函数定义在window作用域,接收一个模块标识(moduleId)并返回import()的 Promise。在 OHIF 平台应用中,这个函数已经内置在默认的 HTML 模板中,见 platform/app/public/html-templates/index.html#L209-L215:
<script> function browserImportFunction(moduleId) { return import(moduleId); } window.PUBLIC_URL = '<%= PUBLIC_URL %>'; </script>迁移到 3.9 后,你的自定义部署如果沿用该模板则无需额外改动;如果是自建的 HTML 入口,则必须自行注入等价的browserImportFunction。
迁移步骤:移除依赖、声明引用
将某个库改为运行时动态加载,需要同时完成两件事:
- 移除对该外部库的构建期
dependencies依赖——即不要把它放进package.json的 dependencies 中被 webpack 静态打包(或者在构建配置中将其标记为 external); - 在
pluginConfig.json的public段中声明该外部模块,说明它的加载路径、导出方式和文件来源。
pluginConfig.json位于 platform/app/pluginConfig.json,其中public数组的每一项定义一个"目录模块"(其内容会被原样复制到构建输出目录)或一个"动态导入模块"(通过packageName识别,运行时由browserImportFunction加载)。
实战示例:dicom-microscopy-viewer
迁移文档给出的dicom-microscopy-viewer配置(该示例正是默认pluginConfig.json中的内容):
"public": [ { "directory": "./platform/public" }, { "packageName": "dicom-microscopy-viewer", "importPath": "/dicom-microscopy-viewer/dicomMicroscopyViewer.min.js", "globalName": "dicomMicroscopyViewer", "directory": "./node_modules/dicom-microscopy-viewer/dist/dynamic-import" } ]逐字段说明:
directory:./platform/public是应用自身的静态资源目录,会被原样复制到构建输出目录;packageName:dicom-microscopy-viewer,用于在插件体系内唯一标识这个外部模块;importPath:传入上文browserImportFunction的模块标识,即实际加载的 JS 文件地址;globalName:加载完成后挂载在window上的全局变量名,即通过window.dicomMicroscopyViewer访问该库;directory:外部库文件的来源目录,构建时将其内容复制到输出目录,供importPath引用。
需要说明的是,当前仓库中 platform/app/pluginConfig.json#L104-L115 的实际写法在此基础上做了一点演进:importPath使用相对路径dicom-microscopy-viewer/dicomMicroscopyViewer.min.js,并增加了"to": "/dicom-microscopy-viewer/"字段来显式指定复制目标目录:
"public": [ { "directory": "./platform/public" }, { "packageName": "dicom-microscopy-viewer", "importPath": "dicom-microscopy-viewer/dicomMicroscopyViewer.min.js", "globalName": "dicomMicroscopyViewer", "directory": "./node_modules/dicom-microscopy-viewer/dist/dynamic-import", "to": "/dicom-microscopy-viewer/" } ]底层原理:writePluginImportsFile 与生成的 pluginImports.js
pluginConfig.json并不直接被运行时消费,而是由构建脚本 platform/app/.webpack/writePluginImportsFile.js 在构建时读取,动态生成pluginImports.js(platform/app/src/pluginImports.js为构建产物,仓库中不静态存在)。生成逻辑位于getRuntimeLoadModesExtensions,其关键分支(writePluginImportsFile.js#L66-L108)如下:
- 对配置了
importPath的模块,生成通过window.browserImportFunction(...)加载的代码,且会自动拼接PUBLIC_URL前缀(除非importPath以http或/开头,见isAbsolutePath判断); - 加载完成后按
globalName取window["dicomMicroscopyViewer"],未声明globalName时则回退到imported["default"](或importName指定的具名导出); - 未配置
importPath的扩展/模式仍走静态import("packageName"); - 同时,
public段中的目录(如./platform/public与./node_modules/dicom-microscopy-viewer/dist/dynamic-import)会被复制进构建输出目录,保证运行时importPath可解析。
应用启动时,platform/app/src/appInit.js#L27 引入生成的pluginImports.js,并将其中的loadModule作为peerImport注入appConfig(appInit.js#L49-L50):
// Default the peer import function appConfig.peerImport ||= peerImport;peerImport随后被ExtensionManager持有(见 platform/core/src/extensions/ExtensionManager.ts#L34、ExtensionManager.ts#L119),供各扩展在运行时加载外部模块。
引用外部导入:peerImport 与 CS3D 的衔接
迁移文档指出:appConfig要么自定义、要么默认提供一个peerImport函数,用于加载pluginConfig.json中声明的模块;cornerstone 扩展的init.tsx是展示如何将其传给 CS3D 以加载全切片成像(WSI,Whole Slide Imaging)库的示例。
对应源码在 extensions/cornerstone/src/init.tsx#L77-L80:
await cs3DInit({ peerImport: appConfig.peerImport, debug: { statsOverlay }, });即 cornerstone 核心初始化时把peerImport作为外部模块加载通道交给 CS3D。显微镜扩展则直接通过该通道在运行时获取dicom-microscopy-viewer,见 extensions/dicom-microscopy/src/services/MicroscopyService.ts#L42 与 MicroscopyService.ts#L73-L75:
public importDicomMicroscopyViewer(): Promise<any> { return this.peerImport('dicom-microscopy-viewer'); }迁移到 3.9 时,如果你的自定义扩展需要加载类似的运行时外部库,遵循同样的三步:在根 HTML 定义browserImportFunction、在pluginConfig.json的public段声明模块、在代码中通过appConfig.peerImport(或扩展内注入的peerImport)发起加载。
使用 ViewReference 进行导航
三个核心方法
3.9 起,测量跳转与导航位置的保存/恢复统一围绕三个 viewport 方法展开:
viewport.getViewReference()——获取当前视图位置的引用(参考信息);viewport.isReferenceViewable(reference, options)——检查某个引用能否应用到当前视口;viewport.setViewReference(reference)——将视口导航到引用所描述的视图位置。
在 cornerstone 扩展中,这三个方法由CornerstoneViewportService的视口实现暴露,getViewReference在 extensions/cornerstone/src/services/ViewportService/Viewport.ts#L209 处定义。注意:这一变更改变了 MPR 与 Stack 视口之间的导航行为,并且使得 CS3D 中视频(video)与显微镜(microscopy)视口也能参与统一导航。因此,导航是否如预期工作,取决于帧参考系(Frame of Reference)相关数值如何配置——不同的 FOR 配置会直接影响跨视口导航的结果。
getViewReference 与 forFrameOfReference 标志
getViewReference的行为由forFrameOfReference("为帧参考系")标志决定:
- 当该标志为
true时:返回的引用可以被"任何包含同一帧参考系、且覆盖给定 FOR、且能显示所需方向"的视口显示。换言之,它描述的是"该 FOR 中的这个位置",与具体哪一帧图像绑定较松; - 当该标志为
false(默认)时:返回的引用会被"包含指定 imageId 的 Stack 视口,或包含该 imageId/指定 volume 的 Volume 视口"显示。即它更贴近具体的图像/体积实例。
这一区别解释了为什么在 Stack 与 Volume 之间切换时导航表现可能不同:取自某个具体 Stack 的引用(未带 FOR 标志)只能回到包含该 imageId 的视口;而带 FOR 标志的引用则可以跨到同 FOR 下任意能呈现该方向的视口。
isReferenceViewable 与导航/方向标志
isReferenceViewable在"引用能被视口原样直接显示"时才返回true。但它可以接收各种标志,用来判断"如果对视口做某种修改(例如改变位置或方向),引用是否能够被显示"。这允许对"接近程度"进行分级检查,从而选出最合适的视口。
源码中预定义了两组标志,见 CornerstoneViewportService.ts#L111-L112:
export const WITH_NAVIGATION = { withNavigation: true, withOrientation: false }; export const WITH_ORIENTATION = { withNavigation: true, withOrientation: true };WITH_NAVIGATION:仅允许通过改变位置(滚动/平移)来显示引用,不允许改变方向;WITH_ORIENTATION:允许通过改变方向来显示引用(同时保留导航能力)。
findNavigationCompatibleViewportId(CornerstoneViewportService.ts#L680-L713)正是按"接近程度"分级搜索的典型实现,其查找顺序为:
- 当前激活视口能否仅靠导航显示该引用(
isReferenceViewable(metadata, WITH_NAVIGATION)); - 其他任意视口能否仅靠导航显示该引用;
- 通过
getViewportAlignmentData计算各视口与引用方向的对齐分数(基于viewPlaneNormal与planeRestriction的点积),按最接近方向排序后,检查是否可通过方向变化显示(WITH_ORIENTATION); - 以上都不满足则返回
null,表示需要更换视口的显示集/类型才能展示。
这里需要特别留意文档强调的行为变化:一个视口中的测量可能被显示在完全不同的另一个视口上。例如,Stack 视口上用 Probe 工具画的测量,在 MPR 视图上也能被展示出来。这是isReferenceViewable在WITH_NAVIGATION/WITH_ORIENTATION语义下"就近选视口"的预期结果,但也意味着迁移后测量跳转的目标视口可能与 3.8 时代不同。
源码级佐证:jumpToMeasurementViewport 的完整链路
测量跳转命令jumpToMeasurementViewport展示了三个方法的完整协作(extensions/cornerstone/src/commandsModule.ts#L236-L262):
- 先选中该标注(
setAnnotationSelected); - 调用
findNavigationCompatibleViewportId找出"最适合展示该测量引用"的视口——它可能不是当前激活视口; - 命中后调用
viewport.setViewReference(metadata)导航到测量所在切片并render(); - 若测量在当前视口中不可见,再由
ops.centerOnMeasurement进行平面内重定位(legacy 后端走getCamera/setCamera重定位,native 后端因setViewReference已导航到位而跳过)。
这条调用链同时也印证了"存储/记住导航位置"的机制:位置状态通过getViewReference()保存(例如 SegmentationService 在交互前后记录prevViewReference并在需要时用setViewReference恢复,见 extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts#L1772-L1779)。
此外,cornerstone 扩展还提供了一个面向挂载协议/自定义逻辑的辅助函数isReferenceViewable(extensions/cornerstone/src/utils/isReferenceViewable.ts#L6-L39),其默认行为等价于同时开启withNavigation: true与asVolume: true;当传入显式viewportOptions时,Stack 视口只要求包含被引用的imageId,Volume 视口则通过getClosestOrientationFromIOP计算最接近的解剖方向并与之比对(isReferenceViewable.ts#L48-L95),该函数已在 extensions/cornerstone/src/utils/index.ts 中导出,并绑定servicesManager后注册为isReferenceViewable命令(见 extensions/cornerstone/src/index.tsx#L245)。
迁移清单与注意事项
针对 3.8 → 3.9 的这两项变更,升级时建议按以下清单核对:
- HTML 入口:确认根 HTML 中定义了全局
browserImportFunction(或沿用 platform/app/public/html-templates/index.html 默认模板); - 依赖声明:需要运行时加载的外部库从
dependencies中移除,避免被 webpack 静态打包; - pluginConfig.json:在
public段补齐packageName/importPath/globalName/directory(必要时加to指定复制目标),参照 platform/app/pluginConfig.json#L104-L115; - 加载通道:代码中通过
appConfig.peerImport发起加载(ExtensionManager会持有该函数,cornerstone 扩展将其传给 CS3D,见 extensions/cornerstone/src/init.tsx#L77-L80); - 导航语义:理解
getViewReference的forFrameOfReference标志差异,以及isReferenceViewable的withNavigation/withOrientation分级——同一测量可能在完全不同的视口(如 Stack 的 Probe 出现在 MPR)上展示; - 帧参考系配置:若发现跨 MPR/Stack 导航行为与预期不符,优先检查帧参考系(FOR)数值的配置方式,它直接决定了 ViewReference 能否在视口间传递。
【免费下载链接】ViewersOHIF zero-footprint DICOM viewer and oncology specific Lesion Tracker, plus shared extension packages项目地址: https://gitcode.com/GitHub_Trending/vi/Viewers
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考