前言
🚀🚀现在开始第 13 小节,对 Renderer 元素渲染部分的解析和修改。
1. 源码解析
整个 Renderer 渲染部分,大致包括 diagram.js/ElementFactory, diagram.js/GraphicsFactory, diagram.js/BaseRenderer, diagram.js/Styles, bpmn.js/BpmnRenderer, bpmn.js/PathMap, bpmn.js/TextRenderer, bpmn.js/BpmnFactory, bpmn.js/ElementFactory 这些模块。
这里对这几个部分的功能大致描述一下:
diagram.js/ElementFactory: 最底层元素实例创建工厂,根据diagram.js/model内定义的四种实例类型(Root,Label,Shape,Connection)创建对应的元素实例
diagram.js/GraphicsFactory: 创建元素实例对应的 SVG 分组元素,除Root类型实例外,其他元素都创建一个g.djs-group的 SVG 元素分组,然后根据剩下的三种实例类型,在该分组下创建对应(以Shape为例)的g.djs-element djs-shape分组元素(第二个类名就是); 之后通过djs-${type}Renderer函数将元素实例对应的 SVG 插入到该分组下
diagram.js/BaseRenderer: 最底层的元素节点Renderer模块,不能直接使用。在实例化时注册[ 'render.shape', 'render.connection' ]事件监听函数以创建元素实例对应的 SVG 元素,注册[ 'render.getShapePath', 'render.getConnectionPath' ]用来获取元素实例对应的 SVG 元素路径。并要求继承者实现创建 SVG 元素和获取 SVG 路径的四个方法:drawShape,drawConnection,getShapePath,getConnectionPath;以及判断是否可以绘制 SVG 元素的方法canRender
diagram.js/Styles: 用来管理元素样式的模块,具有默认配置,但是不接受通过config传递自定义配置。默认提供三个方法:
style: 接受一个固定参数additionalAttrs和一个可选参数traits,计算得到一个 SVG 元素的属性对象
cls: 比style方法多接受一个固定参数className,得到一个包含class定义的 SVG 元素的属性对象
computeStyle: 接受一个custom自定义属性对象,跟默认配置合并后返回一个 SVG 元素的属性对象
bpmn.js/BpmnRenderer:bpmn.js核心模块之一,提供多个handler元素创建方法,根据bpmn.json中定义的所有元素类型来调用对应的handler方法创建 SVG 元素。因为bpmn.js中将Connection连线元素也作为一种Shape图形,所以只实现了drawShape,drawConnection,getShapePath和canRender方法
bpmn.js/PathMap: 包含了所有的复杂元素的路径path,并提供getRawPath和getScaledPath来获取某个图形对应的路径和缩放后的路径
bpmn.js/TextRenderer: 文字标签绘制模块,用来创建 SVG 文本标签以及计算文本标签大小等等
bpmn.js/BpmnFactory: 用来创建BPMN业务流程实例以及对应的BPMNDI实例
bpmn.js/ElementFactory: 继承diagram.js/ElementFactory
- 使用
baseCreate来重新定义diagram.js/ElementFactory.prototype.create指向;
- 重新定义
create方法来区分label元素和其他元素的实例化;
- 增加
createBpmnElement扩展本身的create方法,用来实现BPMN业务实例的业务属性businessObject以及元素大小(getDefaultSize),并且通过bpmn.js/BpmnFactory来创建业务元素实例对应的DI实例,并挂载到businessObject.di属性上。
- 增加
getDefaultSize来根据元素类型区分元素大小
- 增加
createParticipantShape来创建泳道图形
以 Palette 创建一个新元素来拆分整个实例和 SVG 元素创建的过程:
- 首先,调用
elementFactory.createShape来创建一个元素实例,执行createShape => create => createBpmnElement => baseCreate
- 调用
create.start开始拖拽创建过程,调用dragging.init。
- 在拖拽结束后,触发
create.end,并调用modeling.createElements创建对应的元素。
modeling.createElements内部区分Shape和Connection来调用modeling.createShape或者modeling.createConnection
modeling[createShape|createConnection]都会调用canvas[addShape|addConnection], 最终都调用canvas._addElement, 这里就会触发[shape|connection].add和[shape|connection].added事件,并调用graphicsFactory.create来创建元素的外层分组元素,并注册到elementRegistry中,最后调用graphicsFactory.update来触发真正的 SVG 元素绘制过程
- 在
graphicsFactory.update方法内部就是通过eventBus模块触发render.shape事件,来通过Renderer模块绘制 SVG
🚀🚀 元素最终的显示效果都是在
Renderer过程中实现的,所以直接更改这个过程中或者这个过程之前的某些方法来实现自定义渲染。
2. 难度1:更改元素大小
在第一小节中可以知道,修改元素显示效果必须在 Renderer 过程中或者 Renderer 前进行调整。
这里提供两个比较简单的方法:
2.1 继承 BaseRenderer 重写 drawShape 方法来控制元素大小。
这一步主要是修改 element 实例的 width 和 height 属性,当然这一步也可以进行扩展,接受一个 config 配置项来动态修改。
/* 1. 直接在 drawShape 中修改 */ class RewriteRendererProvider extends BaseRenderer { constructor(config, eventBus, styles, pathMap, canvas, textRenderer) { super(config, eventBus, styles, pathMap, canvas, textRenderer, 3000) } public drawShape(parentGfx: SVGElement, element: Shape): SVGRectElement { const type = element.type // 修改元素大小(可以根据类型来实现重新定义) element.width = 400 element.height = 400 const h = this._renderer(type) return <SVGRectElement>h(parentGfx, element) } } /* 2. 接受 config 配置项修改(可以通过修改 new Modeler 时的配置动态更改) */ class RewriteRendererProvider extends BaseRenderer { constructor(config, eventBus, styles, pathMap, canvas, textRenderer) { super(config, eventBus, styles, pathMap, canvas, textRenderer, 3000) this._config = config } public drawShape(parentGfx: SVGElement, element: Shape): SVGRectElement { const type = element.type // 修改元素大小(可以根据类型来实现重新定义) if (this._config.size) { const size = this._config.size[type] if (size) { element.width = size.width element.height = size.height } } const h = this._renderer(type) return <SVGRectElement>h(parentGfx, element) } } // 导出 RewriteRendererProvider.$inject = [ 'config.bpmnRenderer', 'eventBus', 'styles', 'pathMap', 'canvas', 'textRenderer', 'elementRegistry', 'interactionEvents' ] export default RewriteRendererProvider
2.2 继承 bpmn.js/ElementFactory 重写 getDefaultSize 方法
个人觉得这种方式修改比较符合开闭原则,也更加优雅。
这里笔者添加了一个 config 配置项来设置元素默认大小
type ElementConfig = Record<string, Dimensions> class CustomElementFactory extends ElementFactory { _config: ElementConfig | undefined constructor( config: Record<string, Dimensions>, bpmnFactory: BpmnFactory, moddle: BpmnModdle, translate: Translate ) { super(bpmnFactory, moddle, translate) this._config = config } getDefaultSize(element, di) { const bo = getBusinessObject(element) const types: string[] = Object.keys(this._config || {}) for (const type of types) { if (is(bo, type)) { return this._config![type] } } return super.getDefaultSize(element, di) } } CustomElementFactory.$inject = ['config.elementFactory', 'bpmnFactory', 'moddle', 'translate'] ElementFactory.$inject = ['bpmnFactory', 'moddle', 'translate'] export default CustomElementFactory
这种方式可以在实例化的时候直接配置
const modeler = new Modeler({ container: 'xxx', elementFactory: { 'bpmn:Task': { width: 120, height: 120 }, 'bpmn:SequenceFlow': { width: 100, height: 80 } } })
3. 难度2: 改变某几个节点渲染
这一步也有两种方式,虽然原理差不多,但是对以后的代码阅读会有影响。
3.1 继承 BpmnRenderer 重写 drawShape
这种方式与上面的修改元素大小有点类似,只是需要在该方法内部判断需要修改的元素类型来重新调用 SVGcreate 来创建 SVG 元素
这种方式可以参见 Bpmn.js 进阶指南(万字长文) 的 7.1 小节
3.2 继承 BpmnRenderer 重写 handlers
在 drawShape 方法中,可以看到最终是调用 this._renderer(type) 来实现,而 this._renderer(type) 返回的就是 this.handlers[type]() 的结果。
所以我们可以替换 this.handlers 中的某个类型的 handler 方法来实现自定义渲染。或者扩展自定义类型的渲染方法。
class RewriteRendererProvider extends BaseRenderer { constructor() { super(); function sqlRender (parentGfx, element, attr) { // 渲染外层边框 const attrs = { fill: getFillColor(element, defaultFillColor), fillOpacity: defaultTaskOpacity, stroke: getStrokeColor(element, defaultTaskColor) } handlers['bpmn:Activity'](parentGfx, element, attrs) // 自定义节点 const customIcon = svgCreate('image') svgAttr(customIcon, { ...(attr || {}), width: element.width, height: element.height, href: './icons/mysql.png' }) svgAppend(parentGfx, customIcon) return customIcon } this.handlers['miyue:SqlTask'] = sqlRender } }