把 BPMN.js 嵌入 Vue 页面后,企业通常会继续增加顶部工具栏、企业节点、节点图标和右键菜单。真正的难点不是"把按钮画出来",而是让所有入口共享同一套动作、权限、建模规则和撤销机制。
结论可以概括为四句话:顶部业务工具栏由 Vue 实现;Palette、Context Pad 和 Popup Menu 通过 Provider 扩展;节点持久图标使用 Custom Renderer,运行状态使用 Overlay;所有模型写操作进入 Modeling 或 CommandStack,并由 Rules 与后端发布校验共同约束。

图 1 不同入口负责不同交互场景,但底层动作和权限策略应统一。
一、先分清四种操作入口
"工具栏"在流程设计器中常被混用,至少应拆成四类:
| 入口 | 位置与用途 | 推荐扩展点 |
|---|---|---|
| 顶部工具栏 | 保存、撤销、缩放、校验、发布 | Vue 组件调用公开服务 |
| Palette | 创建任务、网关和企业节点 | PaletteProvider |
| Context Pad | 选中节点后的追加、连接、删除 | ContextPadProvider |
| 右键菜单 | 配置、复制、转换、删除 | element.contextmenu + PopupMenuProvider |
四类入口可以显示不同动作,但不要分别实现业务逻辑。推荐建立统一 actionCatalog,再通过 actionPolicy 判断当前用户、元素、设计模式和只读状态是否允许执行。
二、BPMN.js 扩展应该怎样分层
BPMN.js 建立在 diagram-js 和 bpmn-moddle 之上。前者提供画布、事件总线、Palette、Context Pad、Popup Menu、Modeling 和 CommandStack;后者负责 BPMN 元模型与 XML 读写。企业扩展应围绕公开服务和模块注入组织,而不是直接操作 SVG DOM。
javascript
export default {
__init__: [
"enterprisePalette",
"enterpriseContextPad",
"enterpriseRenderer",
"enterprisePopupMenu",
"enterpriseRules"
],
enterprisePalette: ["type", EnterprisePalette],
enterpriseContextPad: ["type", EnterpriseContextPad],
enterpriseRenderer: ["type", EnterpriseRenderer],
enterprisePopupMenu: ["type", EnterprisePopupMenu],
enterpriseRules: ["type", EnterpriseRules]
};
建议按五层组织代码:
- Vue 应用层:页面、业务工具栏、弹窗、路由和发布权限;
- Provider 层:Palette、Context Pad、Popup Menu 条目;
- 建模服务层:Create、AutoPlace、Modeling、CommandStack;
- 规则与协议层:Rules、moddle 扩展、后端发布校验;
- 渲染层:Custom Renderer、图标注册表和 Overlay。
三、顶部工具栏为什么留在 Vue
保存草稿、发布版本、打开评审和切换环境属于宿主应用能力,不是 BPMN 图形建模行为。让 Vue 工具栏通过 modeler.get() 调用公开服务,能避免查询 bpmn-js 内部 DOM 或依赖私有字段。
typescript
const commandStack = modeler.get("commandStack");
const canvas = modeler.get("canvas");
function undo() {
if (commandStack.canUndo()) commandStack.undo();
}
function zoomIn() {
canvas.zoom(canvas.zoom() + 0.1);
}
发布按钮也不应在浏览器中直接携带流程引擎凭证。正确链路是:浏览器提交草稿修订号,平台后端重新读取模型、执行权限与语义校验、冻结不可变版本,再由服务端适配器部署到目标引擎并记录审计。
四、Palette 和 Context Pad 怎样扩展
Palette 负责创建元素,Context Pad 负责对当前元素执行高频动作。两者应使用 Create、AutoPlace、Modeling 等服务产生建模命令,不能手写 XML 或直接插入 SVG。
javascript
getPaletteEntries() {
return {
"create-approval-task": {
group: "activity",
className: "icon-approval-task",
title: "创建审批节点",
action: {
dragstart: e => this.create(e),
click: e => this.create(e)
}
}
};
}
企业节点优先使用标准 BPMN 类型加业务属性。例如"审批节点"仍可使用 bpmn:UserTask,通过 moddle 扩展保存审批人策略、表单、按钮和数据权限。只有标准类型无法表达结构语义时,才考虑新增自定义类型。
Provider 只负责返回条目,不要在 getPaletteEntries() 或 getContextPadEntries() 中请求后端。权限、字典和节点模板应提前缓存,否则每次选中节点都会触发异步请求,造成菜单闪烁和性能问题。
五、节点图标应该放在哪一层
Palette 图标、模型节点图标和运行时状态图标不是同一件事:
- Palette 图标只是创建入口,可以使用 bpmn-font 或 CSS 图标;
- 模型节点图标属于持久语义,适合由 Custom Renderer 绘制;
- "运行中、已完成、异常"等状态不属于模型,适合使用 Overlay。

图 2 业务对象保存稳定图标键,Renderer 从白名单注册表解析 SVG,运行状态不写回模型。
不要把任意 SVG 或外部 URL 直接保存到 BPMN XML。更安全的方式是保存稳定键值,例如 yc:iconKey="approval",再从前端白名单图标注册表中解析。
javascript
class EnterpriseRenderer extends BaseRenderer {
canRender(element) {
return !element.labelTarget &&
is(element, "bpmn:UserTask") &&
!!element.businessObject.iconKey;
}
drawShape(parent, element) {
const shape = this.bpmnRenderer.drawShape(parent, element);
appendIcon(shape, iconRegistry[element.businessObject.iconKey]);
return shape;
}
}
如果改变节点几何尺寸,还要同步处理连接点和 getShapePath();如果只是增加角标或中心小图标,尽量复用默认 BpmnRenderer 画出的基础形状,减少与升级版本的耦合。
六、右键菜单应该怎样实现
element.contextmenu 是事件入口,Popup Menu Provider 是菜单条目提供者,两者不是同一个概念。事件处理器负责阻止浏览器默认菜单、换算可视区域坐标并打开指定 Provider;Provider 根据元素类型和权限返回可用动作。

图 3 右键事件只负责打开菜单,动作过滤交给策略层,模型变更最终进入建模内核。
javascript
eventBus.on("element.contextmenu", 1500, event => {
event.originalEvent.preventDefault();
if (!canOpen(event.element)) return;
popupMenu.open(event.element, "enterprise-actions", {
x: event.originalEvent.clientX,
y: event.originalEvent.clientY
});
});
右键菜单至少应过滤 Label、根元素、只读模式和无权限动作。复制、删除、转换类型、修改属性等写操作必须调用 Modeling 或自定义 CommandHandler,不能直接修改 businessObject,否则撤销、重做、脏状态和相关图形更新都会失效。
右键不是唯一入口。键盘用户和触屏用户无法稳定使用右键,因此关键动作还应出现在顶部工具栏、Context Pad 或可聚焦的更多菜单中。
七、所有入口如何共享动作和权限
统一动作目录可以把显示条件与执行逻辑分开:
typescript
const actions = {
delete: {
visible: ctx => !ctx.readonly && ctx.policy.canDelete(ctx.element),
execute: ctx => ctx.modeling.removeElements([ctx.element])
},
configure: {
visible: ctx => ctx.policy.canConfigure(ctx.element),
execute: ctx => ctx.bridge.openConfig(ctx.element.id)
}
};
Palette、Context Pad、右键菜单和顶部工具栏只把自身上下文传入动作目录。对于真正修改模型的动作,再由 Rules 判断连接、移动、删除或替换是否合法。前端规则改善建模体验,但导入 XML 和发布请求仍要经过后端校验,因为前端可以被绕过。
只读模式也不能只靠隐藏按钮。纯查看优先使用 Viewer;需要选择、缩放和查看属性时,可使用受限 Modeler,但要同时禁用键盘编辑、直接编辑、拖拽、Context Pad、Palette 和程序化命令入口。
八、从当前项目看,精简改造从哪里开始
当前项目的 ych-bpm-designer 已经组装了 Modeler、Viewer、Properties Panel、Camunda Provider、moddle 扩展以及 simpleDesign / simpleRender,并在 UserTask 属性中加入候选人、多实例、自动选人和审批操作等能力。
这套基础适合采用渐进方式治理:
- 先建立统一动作目录和权限策略;
- 把顶部工具栏改为只调用公开服务;
- 将企业节点创建收敛到 Palette Provider;
- 用稳定
iconKey和注册表管理节点图标; - 用 Popup Menu Provider 承载右键动作;
- 把所有模型写操作收敛到 Modeling 或 CommandStack;
- 增加 XML、SVG、条目集合和撤销重做回归测试;
- 最后再升级较旧的 bpmn-js 依赖并减少对内部源码的修改。
云程低代码开发平台可以保留当前审批属性与简洁渲染能力,同时把交互扩展逐步迁移到公开模块边界,降低后续升级和多人协作维护成本。 
九、上线检查清单
| 检查对象 | 必查项 |
|---|---|
| 顶部工具栏 | 撤销状态来自 CommandStack;发布只调用可信后端 |
| Palette / Context Pad | 条目按模式和权限过滤;创建动作进入 Create/AutoPlace |
| Renderer | canRender() 排除 Label;图标来自白名单;SVG 导出已回归 |
| 右键菜单 | 坐标正确;过滤根元素和只读模式;有键盘与触屏替代入口 |
| 模型变更 | 全部进入 Modeling/CommandStack;Rules 与后端共同校验 |
| 升级与测试 | 锁定依赖;不修改 node_modules;覆盖 XML、SVG 和撤销重做 |
十、如果只记住六句话
- 顶部业务工具栏留在 Vue;
- Palette、Context Pad 和 Popup Menu 使用 Provider;
- 企业节点优先使用标准 BPMN 类型加业务属性;
- 持久图标用 Renderer,运行状态用 Overlay;
- 所有写操作进入 Modeling 或 CommandStack;
- 前端 Rules 负责体验,后端校验负责可信发布。
这六条能让工具栏、节点图标和右键菜单继续扩展,同时保持模型语义、撤销能力、权限边界和版本升级路径一致。