ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

Umbraco Backoffice 扩展条件(Condition)开发指南:从零创建自定义扩展条件

Umbraco Backoffice 扩展条件(Condition)开发指南:从零创建自定义扩展条件
  • CMS
  • 后端

【免费下载链接】Umbraco-CMS

Umbraco is a free and open source .NET content management system helping you deliver delightful digital experiences.

项目地址:https://gitcode.com/gh_mirrors/um/Umbraco-CMS
点击查看免费下载

导读

Umbraco 14+ 的新版 Backoffice(基于 Web Components 与扩展注册机制构建)允许通过"条件(Condition)"扩展精确控制任意扩展在何时激活——例如仅在内容(Content)分区内显示某个操作按钮、仅在当前用户拥有媒体分区权限时加载某个菜单项、或仅在实体处于回收站时展示"还原"动作。本文以官方开发指南 general-create-condition/SKILL.md 为骨架,结合 Umbraco-CMS 仓库中Umbraco.Web.UI.Client的真实源码与测试,完整演示如何创建一个自定义条件扩展:从 alias 常量、配置类型、条件控制器到 manifest 注册的四个步骤,并深入解析permitted响应式求值、多条件 AND 语义等底层原理,让你既能照抄落地,也能理解其运行机制。

一、理解 Condition 扩展:它解决什么问题

在 Umbraco Backoffice 的扩展体系中,condition是一种特殊的扩展类型。它本身不提供 UI,而是作为"守门员"存在:其他扩展(workspaceAction、menuItem、tree、propertyEditorUi 等)可以通过conditions数组声明自己何时应该激活。只有当数组中所有条件都判定为permitted时,扩展才会被加载并展示给用户。

从源码可见,条件配置的载体是 condition.types.ts 中定义的ManifestWithDynamicConditions,任何支持条件的扩展 manifest 都继承它:

export interface ManifestWithDynamicConditions<...> extends ManifestBase { conditions?: Array<ConditionConfigs>; overwrites?: string | Array<string>; }

典型的应用场景包括:

  • 按分区/工作区控制:仅当当前处于内容分区(Umb.Section.Content)时显示某个操作;
  • 按权限控制:仅当当前用户拥有某分区访问权限、或当前用户是管理员时显示某功能;
  • 按实体状态控制:仅当实体位于回收站(is-trashed)、或尚未发布时启用动作;
  • 按运行环境控制:如is-production-mode条件判断当前是否生产模式。

开发一个自定义条件,你需要回答三个问题:

  1. 它检查什么(What it checks)——何种上下文或状态决定条件结果(当前分区、用户角色、实体状态等);
  2. 配置属性(Config properties)——使用方在 manifest 中提供哪些参数(如match、oneOf);
  3. 上下文依赖(Context dependency)——由哪个 context token 提供用于求值的数据。

二、文件结构与命名规范

按照官方指南,条件扩展应放在所属功能目录的conditions/子目录下,每个条件独占一个文件夹:

my-feature/ └── conditions/ └── my-condition/ ├── constants.ts # 条件 alias 常量 ├── types.ts # 配置类型 + 全局声明 ├── my-condition.condition.ts # 条件控制器 └── manifests.ts # 条件 manifest

这一结构与仓库中的真实实现完全一致,例如核心的 section-alias 条件 就包含constants.ts、types.ts、section-alias.condition.ts、manifests.ts四个文件。

三、Step 1:定义 Alias 常量

条件在 manifest 中通过alias被引用,alias 必须全局唯一。官方约定采用Umb.Condition.前缀:

// constants.ts export const UMB_MY_CONDITION_ALIAS = 'Umb.Condition.MyCondition';

仓库中的真实命名可作参考:section-alias 的常量 为'Umb.Condition.SectionAlias',is-admin 的常量 为'Umb.Condition.CurrentUserIsAdmin'。

四、Step 2:定义 Config 类型

配置类型是使用方(消费此条件的扩展 manifest)可以传入的参数描述。它必须继承UmbConditionConfigBase<typeof 别名常量>,并通过declare global注册到UmbExtensionConditionConfigMap,从而让消费方 manifest 获得类型提示与校验。

// types.ts import type { UMB_MY_CONDITION_ALIAS } from './constants.js'; import type { UmbConditionConfigBase } from '@umbraco-cms/backoffice/extension-api'; export interface MyConditionConfig extends UmbConditionConfigBase<typeof UMB_MY_CONDITION_ALIAS> { /** * The value to match against. * @example "Umb.Section.Content" */ match?: string; /** * One or more values to match against (any match passes). * @example ["Umb.Section.Content", "Umb.Section.Media"] */ oneOf?: Array<string>; } declare global { interface UmbExtensionConditionConfigMap { UmbMyConditionConfig: MyConditionConfig; } }

关键规则

  • 必须继承UmbConditionConfigBase<typeof ALIAS_CONSTANT>——这会为alias属性提供正确的字面量类型,防止配置中 alias 与常量不一致;
  • declare global中的UmbExtensionConditionConfigMap声明让配置在扩展 manifest 中使用时具备类型安全;
  • Map 中的键(如UmbMyConditionConfig)必须唯一;
  • 为每个属性添加带@example的 JSDoc 注释——这些注释会进入 JSON schema 生成流程,为配置提供文档与自动完成。

UmbConditionConfigBase的定义位于 condition.types.ts,本质上只要求一个alias字段:

export interface UmbConditionConfigBase<AliasType extends string = string> { alias: AliasType; }

常见配置模式

模式适用场景
match: string单一值精确匹配
oneOf: Array<string>命中多个值中的任意一个即通过
allOf: Array<string>所有值都必须匹配
noneOf: Array<string>所有值都不匹配
无配置属性条件检查布尔状态(如"已回收"、"是管理员")

五、Step 3:实现条件控制器

条件控制器是真正执行判定逻辑的类。它继承UmbConditionBase<ConfigType>、实现UmbExtensionCondition接口,并在构造函数中通过consumeContext()获取 context、通过observe()响应式地更新this.permitted。

// my-condition.condition.ts import type { MyConditionConfig } from './types.js'; import { UMB_MY_CONTEXT } from '../../my-context.token.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { UmbConditionControllerArguments, UmbExtensionCondition } from '@umbraco-cms/backoffice/extension-api'; import { UmbConditionBase } from '@umbraco-cms/backoffice/extension-registry'; export class UmbMyCondition extends UmbConditionBase<MyConditionConfig> implements UmbExtensionCondition { constructor(host: UmbControllerHost, args: UmbConditionControllerArguments<MyConditionConfig>) { super(host, args); this.consumeContext(UMB_MY_CONTEXT, (context) => { this.observe( context.someObservable, (value) => { this.permitted = this.#check(value); }, '_observeConditionValue', ); }); } #check(value: string | undefined): boolean { if (!value) return false; if (this.config.match) { return value === this.config.match; } if (this.config.oneOf) { return this.config.oneOf.includes(value); } return false; } } export { UmbMyCondition as api };

关键规则

  • 继承UmbConditionBase<ConfigType>——基类提供permitted属性与config访问器;
  • 实现UmbExtensionCondition接口(该接口定义于 extension-condition.interface.ts,要求暴露只读的permitted与config);
  • 构造函数签名固定为(host: UmbControllerHost, args: UmbConditionControllerArguments<ConfigType>);UmbConditionControllerArguments在 condition-controller-arguments.type.ts 中定义为{ host, config, onChange }三元组;
  • 设置this.permitted = true/false——基类会在值变化时通知扩展系统;
  • 使用consumeContext()+observe()实现响应式判定,使条件能随状态变化实时更新;
  • 以命名导出api导出类,供 manifest 导入;
  • 为 observer 使用显式别名(如'_observeConditionValue'),使重新求值时替换掉之前的订阅,避免重复订阅与内存泄漏。

基类 UmbConditionBase 的原理

condition-base.controller.ts 是理解一切的关键。基类持有私有字段#permitted(初始为false),并通过 getter/setter 暴露:

public set permitted(value) { if (value === this.#permitted) return; // 值未变化则忽略 this.#permitted = value; this.#onChange?.(value); // 通知扩展系统重新评估 }

可以看到两点重要行为:重复赋值相同值不会触发回调(避免无意义的重新评估);destroy()时先清除#onChange再调用super.destroy(),并清空config引用,防止销毁后仍触发回调或持有引用。这些行为都有对应的单元测试验证(见 condition-base.controller.test.ts),测试覆盖了"初始为 false""值跳变才触发 onChange""销毁后不再触发""销毁后 config 被清空"等场景。

无配置的简单布尔条件

当条件只需检查布尔状态(如"当前用户是否为管理员")时,可以省去自定义配置类型,直接使用UmbConditionConfigBase:

export class UmbIsSomethingCondition extends UmbConditionBase<UmbConditionConfigBase> implements UmbExtensionCondition { constructor(host: UmbControllerHost, args: UmbConditionControllerArguments<UmbConditionConfigBase>) { super(host, args); this.consumeContext(UMB_SOME_CONTEXT, (context) => { this.observe(context.isSomething, (value) => { this.permitted = value === true; }); }); } }

仓库中 is-admin.condition.ts 就是这种模式的真实范例——它甚至不需要定义额外配置类型(is-admin.condition-config.ts 只是空的类型占位),通过isCurrentUserAnAdmin(host).then(...)异步判断后设置permitted。

六、Step 4:注册 Condition Manifest

条件必须快速加载——官方指南强调:条件一律直接导入类(import { UmbMyCondition }),不要使用懒加载(() => import()),因为扩展系统在初始化任何带条件的扩展时都需要先实例化其条件控制器,懒加载会拖慢整个初始化链路。

// manifests.ts import { UMB_MY_CONDITION_ALIAS } from './constants.js'; import { UmbMyCondition } from './my-condition.condition.js'; export const manifests: Array<UmbExtensionManifest> = [ { type: 'condition', name: 'My Condition', alias: UMB_MY_CONDITION_ALIAS, api: UmbMyCondition, }, ];

type: 'condition'对应的接口是 manifest-condition.interface.ts 中的ManifestCondition,它通过ManifestApi<UmbExtensionCondition>约束api必须是一个构造后能产生UmbExtensionCondition的类。

向上冒泡到包级 manifests

新条件的manifests.ts需要被所属功能的manifests.ts引用,逐层汇聚,最终到达包级manifests.ts才会被打包注册。仓库中的冒泡链条很直观:

  • 条件级:section-alias/manifests.ts 导出{ type: 'condition', alias: 'Umb.Condition.SectionAlias', api: UmbSectionAliasCondition };
  • 功能级:conditions/manifests.ts 通过[...sectionAliasManifests, ...sectionUserPermissionManifests]合并;
  • 包级:section/manifests.ts 再将其展开进manifests数组。

七、消费者如何使用条件

任何支持conditions的扩展 manifest,都可以通过数组引用一个或多个条件:

{ type: 'workspaceAction', alias: 'My.WorkspaceAction', name: 'My Action', conditions: [ { alias: 'Umb.Condition.MyCondition', match: 'some-value', }, ], }

数组中的所有条件都必须permitted,扩展才会激活——即多个条件是 AND 关系。这一语义在扩展初始化器 base-extension-initializer.controller.ts 中有直接体现:

#checkConditionsAreGood() { if (this.#manifest === undefined) return false; // 条件配置的数量必须与已实例化的条件控制器数量一致 const hasAllConditions = (this.#manifest.conditions ?? []).length === this.#conditionControllers.length; if (hasAllConditions === false) return false; // 每个条件配置都必须有对应的控制器 const allConditionsHaveControllers = (this.#manifest.conditions ?? []).every((condition) => this.#conditionControllers.some((controller) => controller.config === condition), ); if (allConditionsHaveControllers === false) return false; // 只要有一个条件未通过,整体即不通过(AND 语义) return this.#conditionControllers.some((condition) => condition.permitted === false) === false; }

初始化器还会根据 manifest 中conditions数组的 alias 列表,通过extensionRegistry.byTypeAndAliases('condition', conditionAliases)动态实例化对应的条件控制器,并把onChange: this.#onConditionsChangedCallback传入构造函数——这正是UmbConditionBase.permitted被 setter 赋值后能实时触发整体重新评估的机制所在。整个评估是响应式的:manifest 变化、条件控制器新增/移除、permitted跳变都会触发重算,而值未变化时则跳过以优化性能。

八、仓库中的真实范例与内置条件

仓库中已有大量条件实现可直接对照学习,这里列举几类有代表性的:

条件文件要点
SectionAliassection-alias.condition.ts消费UMB_SECTION_CONTEXT,观察context.alias,支持match与oneOf两种匹配模式,与本文示例结构几乎一致
SectionUserPermissionsection-user-permission.condition.ts消费UMB_CURRENT_USER_CONTEXT,检查currentUser.allowedSections是否包含config.match,演示"权限类"条件
CurrentUserIsAdminis-admin.condition.ts无配置布尔条件,异步判定管理员
Delay(内置调试用)delay.condition.ts定时器到点后permitted = true,注意其配置校验(offset必须为正数,否则抛错)
Switch(内置调试用)switch.condition.ts按frequency周期在 permitted true/false 间切换

其中 delay.condition.ts 和 switch.condition.ts 还展示了另一种注册形态——直接在文件内导出manifest(export const manifest: ManifestCondition = {...}),而非manifests数组,两种方式等价。

九、验证与收尾

完成四个步骤后,按清单自查,并运行编译验证:

  • Alias 常量从constants.ts导出;
  • 配置类型继承UmbConditionConfigBase<typeof ALIAS>并使用 alias 常量;
  • 配置类型已声明到全局UmbExtensionConditionConfigMap;
  • 条件类继承UmbConditionBase<ConfigType>并实现UmbExtensionCondition;
  • 构造函数调用super(host, args)并建立 context 观察;
  • this.permitted通过observe()响应式设置——而非只在构造函数里设置一次;
  • 条件 manifest 以type: 'condition'注册;
  • manifests 已冒泡到包级manifests.ts;
  • 编译通过:npm run compile。

如需补充单元测试,可参考 condition-base.controller.test.ts:它使用@open-wc/testing的fixture创建宿主元素,直接实例化UmbConditionBase子类并断言config暴露、初始permitted=false、值跳变触发onChange、同值不触发、destroy()后不再触发等行为。

总结

自定义 Condition 扩展是 Umbraco Backoffice 扩展体系中最实用的能力之一。四个步骤——alias 常量、配置类型(含全局 map 声明)、继承UmbConditionBase的条件控制器(响应式设置permitted)、以type: 'condition'注册 manifest——构成了完整的开发闭环。理解permittedsetter 触发onChange、初始化器对所有条件做 AND 求值这两层机制,你就能像官方内置的 SectionAlias、SectionUserPermission 等条件一样,写出反应迅速、随应用状态实时变化的扩展开关逻辑。

  • CMS
  • 后端

【免费下载链接】Umbraco-CMS

Umbraco is a free and open source .NET content management system helping you deliver delightful digital experiences.

项目地址:https://gitcode.com/gh_mirrors/um/Umbraco-CMS
点击查看免费下载

相关推荐

上一篇:HyperFrames v0.7.72:从 2 GiB 目录打包到内容寻址 Plan v2
下一篇:Gopeed 下载器全解析:多协议下载内核、MCP 自然语言接入与三端构建实战

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

RELATED READING

延伸阅读

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