玩转 bpmn-js打造高颜值流程图的自定义渲染器在企业级工作流应用中标准 BPMN 节点的默认样式往往无法满足业务对个性化展示的需求。你可能需要为不同类型的节点配上专属图标、品牌色甚至加入状态标识。bpmn-js 提供了强大的自定义渲染能力通过实现一个BaseRenderer我们可以完全接管节点的绘制过程。本文将带你一步步实现一个支持丰富节点类型的自定义渲染器让你的流程图“颜值”与“功能”齐飞。为什么需要自定义渲染器bpmn-js 默认渲染的节点是矢量图形SVG样式调整较为受限。而在真实项目中我们经常遇到需要区分“申请人节点”、“抄送任务”、“定时任务”等业务含义不同的任务希望为每个节点配上直观的 SVG 图标要求节点尺寸统一、支持 hover/选中状态需要根据委托表达式动态识别服务任务的具体类型自定义渲染器正是为了解决这些问题而生。我们可以利用foreignObject嵌入 HTML/CSS实现任意复杂的布局和样式同时保持 bpmn-js 原有的交互如拖拽、选中、缩放。自定义渲染器核心原理bpmn-js 的渲染机制基于Renderer和ElementFactory。自定义渲染器只需继承BaseRenderer并实现几个关键方法canRender(element)决定当前渲染器是否负责绘制该元素。通常我们只拦截感兴趣的元素其余的交给默认渲染器。drawShape(parentNode, element)绘制形状节点返回插入后的 SVG 元素。getShapePath(shape)定义节点的点击/选中区域形状路径必须与实际可见区域一致否则交互会不准确。drawLabel(parentNode, element)控制标签的渲染。如果我们在自定义内容中已经包含了名称可以返回null禁用默认标签。此外我们还需要注入一些依赖服务如modeling用于调整节点尺寸、bpmnRenderer用于回退默认渲染等。代码实战一步步实现 CustomRenderer下面我们按照代码逻辑逐步解析自定义渲染器的实现。1. 定义节点配置与尺寸首先我们需要为每种节点类型定义对应的 SVG 图标、背景色、CSS 类。为了便于维护我们将这些信息统一放在nodeConfigMap中同时为特殊节点开始、结束、网关单独设置尺寸。// 节点尺寸统一管理constNODE_SIZE{SPECIAL:{width:100,height:40},// 开始、结束、网关NORMAL:{width:220,height:66}// 任务、调用活动等};// 节点样式配置interfaceNodeConfig{svg:string;color?:string;cssClass:string;}consttaskClassw-[42px] h-[42px] p-3 flex justify-center items-center rounded-xl;constnodeConfigMap:Recordstring,NodeConfig{// 特殊节点[BpmnNodeKey.START]:{svg:startSvg,cssClass:start-node},[BpmnNodeKey.END]:{svg:endSvg,cssClass:end-node},// ... 网关类似// 任务节点applyTask:{svg:applyNodeSvg,color:#8459c2,cssClass:apply-task${taskClass}},userTask:{svg:userTaskSvg,color:#2594d9,cssClass:user-task${taskClass}},// ... 其他任务};// 委托表达式到配置键的映射用于服务任务constdelegateExpressionKeyMap:Recordstring,string{${ccListener}:copyToTask,${dynamicInterfaceListener}:dataInterface,// ...};2. 继承 BaseRenderer 并注入依赖exportdefaultclassCustomRendererextendsBaseRenderer{static$inject[eventBus,bpmnRenderer,config.paletteEntries,modeling];privatebpmnRenderer:any;privatepaletteEntries:any;constructor(eventBus:any,bpmnRenderer:any,paletteEntries:any,modeling:any){super(eventBus,HIGH_PRIORITY);this.bpmnRendererbpmnRenderer;this.paletteEntriespaletteEntries;this.bpmnRenderer.defaultSizeNODE_SIZE.NORMAL;// 设置默认尺寸this.modelingmodeling;}// ...}HIGH_PRIORITY确保我们的渲染器优先于默认渲染器执行。3. 决定哪些元素由我们绘制在canRender中我们只拦截符合条件的元素并且排除标签本身labelTarget。canRender(element:{labelTarget:any}){return(isAny(element,[bpmn:Task,bpmn:Event,bpmn:Gateway,bpmn:CallActivity,bpmn:SequenceFlow,bpmn:ServiceTask,])!element.labelTarget);}这里我们使用了is和isAny工具函数来判断元素类型。4. 核心绘制方法 drawShapedrawShape是渲染的核心它负责创建foreignObject并填充 HTML 内容。asyncdrawShape(parentNode:SVGElement,element:any){constnodeNameelement.businessObject?.name||;constconfigthis.getNodeConfig(element);if(!config){returnthis.bpmnRenderer.drawShape(parentNode,element);// 回退}constisSpecialthis.isSpecialNode(element.type);constsizeisSpecial?NODE_SIZE.SPECIAL:NODE_SIZE.NORMAL;// 调整节点尺寸如果与配置不符if(element.width!size.width||element.height!size.height){this.modeling.resizeShape(element,{x:element.x,y:element.y,width:size.width,height:size.height});}constforeignObjectsvgCreate(foreignObject,{width:size.width,height:size.height,class:isSpecial?flow-start-or-end-node:flow-node-container});foreignObject.innerHTMLthis.buildNodeHtml(config,nodeName,isSpecial);svgAppend(parentNode,foreignObject);returnparentNode;}首先根据节点类型获取配置如果没有匹配则使用默认渲染。判断是否为特殊节点开始/结束/网关以确定尺寸。使用modeling.resizeShape强制调整节点尺寸保证所有节点统一大小。创建foreignObject设置宽高和 CSS 类并填充 HTML。最后将foreignObject追加到父节点。5. 节点类型映射逻辑getNodeConfig方法负责将 bpmn 元素映射到我们的配置privategetNodeConfig(element:any):NodeConfig|null{consttypeelement.type;// 特殊节点直接获取if(this.isSpecialNode(type)){returnnodeConfigMap[type];}// 用户任务通过 id 判断是否为申请人节点if(typeBpmnNodeKey.USER){constkeyelement.id.includes(applyNode)?applyTask:userTask;returnnodeConfigMap[key];}// 调用活动/脚本任务if(typeBpmnNodeKey.CALLACTIVITY||typeBpmnNodeKey.SCRIPT){constkeytypeBpmnNodeKey.CALLACTIVITY?callActivity:scriptTask;returnnodeConfigMap[key];}// 服务任务根据委托表达式映射if(typeBpmnNodeKey.SERVICE_TASK){constexprelement.businessObject.get(flowable:delegateExpression);constkeydelegateExpressionKeyMap[expr];returnkey?nodeConfigMap[key]:null;}// 接收任务作为定时任务if(typeBpmnNodeKey.RECEIVE_TASK){returnnodeConfigMap.timerTask;}returnnull;}这样即使业务中有多种服务任务我们也能通过delegateExpression精确识别并渲染对应的图标。6. 构建 HTML 内容buildNodeHtml方法生成包含图标和名称的 HTML 结构支持动态样式privatebuildNodeHtml(config:NodeConfig,nodeName:string,isSpecial:string):string{consticonStyleconfig.color?background:${config.color};:;constnodeClassflow-node-content w-full h-full flex items-center relative;constnodeStyleconfig.color?border: 1px solid${config.color}; background: linear-gradient(to bottom,${config.color}33, white):;conststateClassabsolute right-1 top-1 hidden w-4 h-4;returndiv class${nodeClass}${isSpecial?rounded-[32px]:rounded-[12px]} style${nodeStyle} div classnode-state${stateClass}/div div classnode-icon${config.cssClass}mr-1 style${iconStyle}${config.svg}/div${nodeName?span classtext title${nodeName}${nodeName}/span:}/div;}这里我们使用了 Tailwind CSS 的类名如flex、items-center你也可以根据项目需要自行调整。config.svg是直接内嵌的 SVG 字符串通过?raw导入因此可以完美显示图标。7. 定义点击区域getShapePath为了让选中、拖拽等交互与节点实际视觉区域匹配必须覆盖getShapePath返回一个路径对象。我们根据节点类型返回矩形路径圆角或直角并手动设置shape.width和shape.height确保路径计算时使用正确的尺寸。getShapePath(shape:any){if(is(shape,bpmn:StartEvent)||is(shape,bpmn:EndEvent)||is(shape,bpmn:Gateway)){shape.widthNODE_SIZE.SPECIAL.width;shape.heightNODE_SIZE.SPECIAL.height;returngetRoundRectPath(shape,TASK_BORDER_RADIUS);}if(isAny(shape,[bpmn:UserTask,bpmn:CallActivity,bpmn:ServiceTask])){shape.widthNODE_SIZE.NORMAL.width;shape.heightNODE_SIZE.NORMAL.height;returngetRoundRectPath(shape,TASK_BORDER_RADIUS);}returnthis.bpmnRenderer.getShapePath(shape);}8. 禁用默认标签由于我们已经在自定义内容中包含了节点名称nodeName为了避免重复我们让drawLabel返回null。drawLabel(parentNode:SVGElement,element:any):SVGElement|null{returnnull;}集成到 bpmn-js 中完成自定义渲染器后我们需要将其注册到 bpmn-js 的依赖注入系统中。通常我们会在创建 Modeler 时通过additionalModules添加importCustomRendererfrom./CustomRenderer;constmodelernewBpmnModeler({container:#canvas,additionalModules:[{__init__:[customRenderer],customRenderer:[type,CustomRenderer]}]});如果项目中已经使用了bpmn-js的扩展记得确保依赖modeling等服务正确注入。效果与扩展性实现上述渲染器后你的流程图将焕然一新每个节点都有独特的图标和主题色节点尺寸统一布局更整齐通过foreignObject可以轻松添加状态标识例如待办、完成支持复杂 HTML/CSS可轻松适配暗色主题或响应式设计你可以在此基础上继续扩展比如添加 hover 效果通过 CSS 类动态更新节点状态修改foreignObject内容支持更多自定义属性如显示耗时、负责人头像总结通过实现自定义渲染器我们彻底释放了 bpmn-js 的视觉表现力让流程图不仅“能用”更“好看”。整个过程涉及对 bpmn-js 渲染机制的深入理解但实现起来并不复杂。核心思路是识别节点类型 → 映射配置 → 创建foreignObject嵌入 HTML → 保证交互区域匹配。希望这篇文章能帮助你为自己的工作流编辑器添上个性的一笔。如果你有任何问题或新的想法欢迎在评论区交流本文代码基于 bpmn-js 8.x 版本不同版本 API 可能略有差异请以实际为准。