ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

OHIF 自定义定制服务类型化指南:用 AppTypes.Customizations 注册表让 getCustomization 读取类型化、setCustomizations 写入受检

OHIF 自定义定制服务类型化指南:用 AppTypes.Customizations 注册表让 getCustomization 读取类型化、setCustomizations 写入受检 OHIF 自定义定制服务类型化指南用 AppTypes.Customizations 注册表让 getCustomization 读取类型化、setCustomizations 写入受检【免费下载链接】ViewersOHIF zero-footprint DICOM viewer and oncology specific Lesion Tracker, plus shared extension packages项目地址: https://gitcode.com/GitHub_Trending/vi/Viewers本文围绕 OHIFDICOM 查看器与肿瘤 Lesion Tracker 平台CustomizationService 的类型化改造机制展开如何把任意字符串 id 的getCustomization(id)升级为读取有精确返回类型、写入被编译期校验的强类型 API。读完本篇你将掌握在任意扩展包中通过声明合并declaration merging注册定制键的完整流程理解AuthorableT如何区分写侧可写的标记与读侧干净的值并了解已知局限any 键降级、拼写错误静默通过等及其规避方法。背景为什么 getCustomization 默认是弱类型OHIF 中各模块通过customizationService.getCustomization(id)读取定制值。默认行为是该方法接受任意字符串返回宽松的Customization联合类型定义于 types.ts涵盖组件、字符串、数字、布尔及若干定制对象形态。这带来三个问题调用方被迫在调用点写类型断言as string、as unknown as ColorbarCustomization、as any等setCustomizations的载荷包括$set/$push/$merge命令 spec完全不受编译期检查无法从代码中发现究竟存在哪些定制 id——文档表格是手工维护的会与已注册的键漂移。类型化机制的目标就是把这三点补齐同时不改变任何运行时行为未声明的 id 保持原有工作方式整个机制是纯增量的编译期约束。机制核心声明合并的全局注册表注册表是一个通过声明合并扩展的全局接口与AppTypes.Services使用的模式完全一致。种子接口在 AppTypes.tsplatform/core中// platform/core/src/types/AppTypes.ts declare global { namespace AppTypes { export interface Customizations { sortingCriteria: (a: DisplaySet, b: DisplaySet) number; instanceSortingCriteria: { defaultSortFunctionName?: string; sortFunctions?: Recordstring, (a: unknown, b: unknown) number; }; } } }服务 API 通过函数重载利用这个注册表见 CustomizationService.tspublic getCustomizationK extends keyof AppTypes.Customizations( customizationId: K ): AppTypes.Customizations[K]; public getCustomization(customizationId: string): Customization | undefined;声明一个 id 后获得三样东西id 本身有自动补全——keyof AppTypes.Customizations是有限集合精确返回类型——getCustomization/getValue直接返回声明类型调用点的断言可以删除受检的写入——setCustomizations会用声明类型校验值包括$set/$push/$merge命令 spec。未声明的 id 走第二个重载签名返回Customization | undefined与改造前行为一致。如何声明一个键在你的包中添加types/AppTypes.ts或扩展现有文件合并你拥有的键// platform/ui-next/src/types/AppTypes.ts declare global { namespace AppTypes { interface Customizations { /** * Sort options offered by the study browsers sort dropdown. Read by * StudyBrowserSort, which indexes [0] for its initial selection, so a * default with at least one entry is expected. */ studyBrowser.sortFunctions: Array{ label: string; sortFunction: (a: AppTypes.DisplaySet, b: AppTypes.DisplaySet) number; }; } } } export {};这就是全部机制。效果立竿见影StudyBrowserSort.tsx 现在可以直接读取而无需断言和?.守卫const sortFunctions customizationService.getCustomization(studyBrowser.sortFunctions); const [selectedSort, setSelectedSort] useState(sortFunctions[0]); // ... {sortFunctions.map(sort DropdownMenuItem key{sort.label}{sort.label}/DropdownMenuItem)}对于任何实际规模的值类型建议从产生默认值的文件导出类型并在注册表中引用而不是内联形状。studyBrowser.sortFunctions的默认值由extension-default注册见 studyBrowserCustomization.ts。在哪里声明消费方包由消费该键的包声明其类型紧挨着消费方。这通常也是注册默认值的包但并非总是如此——这个差别很重要。上例的studyBrowser.sortFunctions声明在platform/ui-next因为读取发生在该包的StudyBrowserSort组件里而默认值由extension-default注册。在消费方声明类型正是让提供方的默认值能够针对消费方所依赖的契约做校验的方式。被platform/core读取的键则声明在 core 中以保持依赖方向不依赖扩展包——AppTypes.ts 中的sortingCriteria与instanceSortingCriteria就是这样的例子。声明sortingCriteria之后两个包中的同一处断言都被删除了createStudyBrowserTabs.ts 与 defaultRouteInit.ts- const sortCriteria customizationService.getCustomization(sortingCriteria) as (a, b) number; const sortCriteria customizationService.getCustomization(sortingCriteria);第三方扩展以同样方式在自己的仓库外声明自己的 id。无需任何集中注册。声明解析后的值读侧与写侧的类型分裂声明的类型描述的是getCustomization交还的东西——即inheritsFrom合并、$transform执行、$reference展开之后的值。它不是你写入的形状。这个区分正是组合类键能够被类型化的原因。toolbarButtons解析后是一个按钮列表所以要声明的也是按钮列表interface Customizations { toolbarButtons: Button[]; }……尽管它几乎总是用$reference标记来写入的// modes/basic/src/index.tsx toolbarButtons: [{ $reference: cornerstone.toolbarButtons }],两种写法都被接受。从源码看$reference/$transform是读时标记而非更新命令服务在读取时替换/调用它们CustomizationService.ts 的_resolveReferences递归展开引用数组项位置的引用若目标是数组会被展平进父列表transform处理inheritsFrom与$transform见 第 679-691 行而写入路径的hasDollarKey有意把这两个键排除在 immutability-helper 命令之外。因此标记可以出现在解析器会遍历的任何位置——整个值、数组项、或普通对象的属性值——同时读取端保持干净customizationService.setCustomizations({ // 整个值 toolbarButtons: { $reference: cornerstone.toolbarButtons }, // 追加到已有列表 toolbarSections: { $push: [{ $reference: cornerstone.toolbarSections }] }, // 与字面量条目混用 toolGroupAdditions: { default: [{ $reference: x }], mpr: [] }, // 读取时基于兄弟属性计算必须是 function 而非箭头函数—— // $transform 通过 this 读取兄弟属性 measurementsContextMenu: { inheritsFrom: ohif.contextMenu, $transform: function (customizationService) { return { ...this, menus: this.menus.map(menu ({ ...menu })) }; }, }, });这个写侧宽容由 types.ts 中的AuthorableT精确编码它递归穿透数组和普通对象在_resolveReferences原样返回的东西函数、构造函数、React 元素、Date、RegExp处停止。关键的结构决策是AuthorableT只应用在写侧CustomizationEntries中// platform/core/src/services/CustomizationService/types.ts export type CustomizationEntries { [K in KnownCustomizationIds]?: | AuthorableAppTypes.Customizations[K] | SpecAuthorableAppTypes.Customizations[K], CustomizationUpdateCommands; } { [customizationId: string]: unknown; };注册表声明的是一个键解析成什么所以getCustomization的返回类型保持干净。若在注册表里声明标记联合类型toolbarButtons: (Button | ReferenceMarker)[]就会迫使每个读取点去处理getCustomization永不返回的标记——既错误又不可用。自定义更新命令也是一个注册表$filter由服务自身注册其参数类型FilterSpec声明在 types.ts。如果你的扩展通过registerCustomUpdateCommandCustomizationService.ts在运行时注册更多命令就在平行的AppTypes.CustomizationUpdateCommands注册表中声明使用它们的 spec 即可无断言通过类型检查declare global { namespace AppTypes { interface CustomizationUpdateCommands { /** Reorders toolbar entries by weight. */ $reweight: { id: string; weight: number }; } } }customizationService.registerCustomUpdateCommand(reweight, (query, original) ...); customizationService.setCustomizations({ toolbarButtons: { $reweight: { id: Zoom, weight: 3 } }, });有一个不明显的约束值得提前知晓命令注册表在 CustomizationUpdateCommands 中被包成CustomCommandsPartialAppTypes.CustomizationUpdateCommands。这里的Partial不是装饰性的——immutability-helper的Spec通过C extends CustomCommandsinfer O ? O : never暴露自定义命令而O会推断到整个注册表接口没有Partial每个 spec 就必须同时提供所有已注册命令注册表中一旦有第二个命令普通的{ $filter: ... }就会被拒报误导性错误。可空性类型即有默认值的承诺不带| undefined的声明是对已注册默认值的承诺。消费方此后可以直接使用值——现有消费方就是这样写的StudyBrowserSort直接索引sortFunctions[0]没有任何守卫。仅对确实不附带默认值的 id 追加| undefinedinterface Customizations { // 有默认值由 extension-default 注册 studyBrowser.sortFunctions: SortFunction[]; // 无默认值每个消费方都必须处理缺失 studyBrowser.onDoubleClick: DoubleClickHandler | undefined; }注意仓库自身的 tsconfig.json 未开启strictNullChecks所以| undefined在仓库内会被擦除它是给严格编译的下游消费者的契约。从源码结构看这也是一处有意保留的残余风险承诺由声明的包作出却由注册默认值的包兑现——如果某个部署的pluginConfig.json省略了该提供方运行时会拿到类型所不容忍的undefined。这本来就是今天的运行时失败类型没有使其变坏但该不变式目前存在于约定而非编译器中。已知局限Gotchas以下是回退设计的已接受取舍不是待修的 bug。列出来以免使用者重新踩一遍1. 类型为any的键会关闭所有检查。如果你传入的 id 是any——例如从一个无类型的 props 袋里解构出来——调用会选中泛型重载并返回any这比未声明 id 得到的Customization | undefined更弱。把键注解为string// items: any —— 完全无检查 export default function MoreDropdownMenu(bindProps) { const { menuItemsKey } bindProps; const items customizationService.getCustomization(menuItemsKey); // items: Customization | undefined —— 正确的回退 export default function MoreDropdownMenu(bindProps) { const { menuItemsKey }: { menuItemsKey: string } bindProps; const items customizationService.getCustomization(menuItemsKey);从源码结构看这是方法对键做泛型化的固有代价any键会匹配K extends keyof AppTypes.Customizations的泛型重载并推断出any单签名条件返回形式也无法规避修复只能在调用点进行。2. 已声明 id 的拼写错误不会被捕获。因为未声明 id 必须继续工作setCustomizations接受任何字符串键CustomizationEntries末尾的[customizationId: string]: unknown索引签名同时禁用了多余属性检查所以panelSegmentation.disabledEditing会被静默当作未注册的动态键而不是被报告为某个已声明键的拼写错误。在未声明 id 必须继续工作的前提下这不可避免。3.$transform的返回类型不受检查——它被校验为函数但不针对声明的值类型校验。Spec本身就接受裸的(value: T) T形式而$transform的this是无类型的这是一个动态逃生舱口。受检的路径是直接值与$set/$push/ … 的 spec。4.getValue的回退值不受检查。对已声明 id 传入类型不匹配的fallbackValue不会报错调用落回宽松签名并返回回退值的类型见 CustomizationService.ts 的getValue重载。已声明 id 请优先使用getCustomization。仓库中的落地状态与推广计划该机制的仓库内推进由 CUSTOMIZATION_TYPING_PLAN.md 记录其分阶段计划可作为第三方包跟进类型化的参照Phase 1已完成基础设施加跨包证明。core 声明sortingCriteria/instanceSortingCriteriaui-next 声明studyBrowser.sortFunctions——证明声明能双向跨越包边界sortingCriteria在 core 声明、在extension-default注册默认、在 core和platform/app消费Phase 2为extension-default与extension-cornerstone填充注册表两者注册了约 90 个已知 id 中的 75 个。每个键的流程是从产生方文件导出值类型 → 加入扩展的types/AppTypes.ts增补 → 删除消费点冗余断言每删一处都是一次免费的正确性校验→ 把喂给getCustomization的动态键注解为stringPhase 3其余扩展cornerstone-dicom-seg、cornerstone-dicom-sr、measurement-tracking等modes 在onModeEnter中的setCustomizations调用会在键声明后自动获得检查Phase 4可选提交编译期测试// ts-expect-error断言、从注册表生成文档定制表格以避免漂移、断言每个无| undefined的键确实在init()后拥有默认值、从AppTypes.Customizations生成 JSON Schema 以给?customizationJSONC 文件提供编辑器校验。该 PR 的验证配方同样可复用npx jest platform/core/src/services/CustomizationService7 个套件 79 个测试通过以及全仓库npx tsc --noEmit与改动前基线的错误列表差分3341 → 3337零新增。由于仓库有数千条既有 tsc 错误且没有 tsc CI 门禁差分排序后的错误列表是唯一可靠的全局检查。小结类型化定制值的完整配方是消费方包声明键 → 声明解析后的值类型不声明标记→ 读侧获得精确类型、写侧由AuthorableT放行$reference/$transform/ 命令 spec → 动态键注解为string以免 any 降级。运行时行为零改动纯编译期收益自动补全、断言删除、写入受检。进一步的语义细节可参见 Advanced CustomizationinheritsFrom与$transform语义和 Customization Service$set/$push/$filter命令语法——本文这些类型正是校验它们的对象。【免费下载链接】ViewersOHIF zero-footprint DICOM viewer and oncology specific Lesion Tracker, plus shared extension packages项目地址: https://gitcode.com/GitHub_Trending/vi/Viewers创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表