背景
不知道bpmn-js 是什么的,想看一下这里。 我对bpmn-js 不爽的地方是,很原生,没有相关的react、vue 库,对于现代化的前端项目用起来都很不自然,所以有了标题说讲的想法。
第三方库的定制能力都比较少
首先,官方是不打算出react、vue 适配库的,官方连API文档都不写的。然后我搜索了一下其他第三方库,只找到几个简单传参的,要么只能传xml,例如这个这个,要么多传一下element 事件的,例如这个,如果想定制 palette、element 和context-pad 的dom 的话,只能参照官方例子写additionalModules 对内部渲染方法进行重写才能实现。
定制方案的成本比较高
重写的成本有多高呢?可以看看这里 。
- 需要熟悉贯穿整个bpmn-js 项目的 didi (依赖注入功能)
- 从官方diagram-js 和 bpmn-js 的源码开始了解,知道要重写哪些关键方法
- 微调样式,可以参考bpmn-js 源码,bpmn 如何结合diagram-js 实现功能
- 定制paletee 参考 bpmn-js 项目的lib\features\palette\index.js,重写getPaletteEntries 方法
- 定制元素渲染参考 bpmn-js 项目的lib\draw\BpmnRenderer.js,重写drawShape 方法
- 定制context-pad参考 bpmn-js 项目的lib\features\context-pad\ContextPadProvider.js,重写getContextPadEntries 方法
- 如果需要重写dom,可以参考diagram-js 源码
- 定制paletee 参考 diagram-js 项目的lib\features\palette\Palette.js,重写Palette.prototype._update 方法
- 定制context-pad 参考 diagram-js 项目的lib\features\context-pad\ContextPad.js,重写ContextPad.prototype._updateAndOpen、ContextPad.prototype.trigger 等方法
定制成本高的原因
bpmn-js 依赖diagram-js,全部的dom 创建、操作都是内部完成的,要定制就需要用原生js 的方式重写渲染方法,但现代化前端项目都已经用jsx 语法来写ui 了,还用原生js 的方式写dom 明显是退步。
渲染还是交给react 更高效,目标设想如下
如果有以下这样react 使用方式,是不是会舒适很多
const Demo = () => {
return (
<div style={{height: 500}}>
<Bpmn palette={CustomPalette} />
</div>
)
}
function CustomPalette(props) {
return <Palette >
<Palette.Group name="tools" className="" >
<Palette.Action name="create.task" className="" onClick={_createTask} />
</Palette.Group>
</Palette>
}
export default Demo
抛砖引玉一下,如何实现 Bpmn 、 Palette 组件和_createTask 方法
export default function Bpmn(props) {
const { xml=diagramXML, palette, additionalModules=[] } = props;
const [, updateState] = React.useState();
const domRef = useRef(null)
const modelerRef = useRef(null)
// 初始化modeler
useEffect(() => {
const _additionalModules = additionalModules
if(palette){
_additionalModules.unshift(createCustomPalette(palette))
}
_additionalModules.push(customRenderer())
modelerRef.current = new Modeler({
container: domRef.current,
additionalModules: _additionalModules
});
}, [])
// 更新xml
useEffect(()=>{
if(modelerRef.current && xml){
modelerRef.current.importXML(xml);
updateState({})
}
},[modelerRef.current, xml])
const Palette = palette
return <div ref={domRef} style={{ width: '100%', height: '100%', backgroundColor: '#ccc' }}>
<Palette modeler={modelerRef.current} />
</div>
}
// 使用react jsx 完成palette 面板的dom 渲染工作
export default function Palette(props){
const [, updateState] = React.useState();
const entriesRef = useRef(null)
function _init(){
if(entriesRef.current !== null) return
const dom = document.querySelector('.djs-palette-entries') // diagram-js 内部会创建.djs-palette-entries 的dom 节点
if(dom){
entriesRef.current = dom
updateState({})
}else{
setTimeout(()=>{ // 等待.djs-palette-entries 的dom 节点创建完成
_init()
},100)
}
}
useEffect(()=>{
if(!props.modeler) return
_init()
},[props.modeler])
if(entriesRef.current === null){
return null
}
// 把palette 面板和react root 节点绑定一起,
return ReactDOM.createPortal(<div className='djs-palette-entries'>
{props.children}
</div>, entriesRef.current);
}
// 保留关键dom attribute,避免diagram-js 内部计算出错
Palette.Group = function Group(props){
const {name, children} = props
return <div data-group={name}>
{children}
</div>
}
Palette.Action = function Action(props){
const {name, children, className, onClick} = props
return <div data-group={name} onClick={onClick} className={className} style={{width: '20px', height: '20px', backgroundColor: 'red'}}>
{children}
</div>
}
// 通过在全局暴露常用palette 的内部方法,实现react 化后的组件能够恢复bpmn-js 的原始能力
export function createTask(event, modeler){
const elementFactory = modeler.get('elementFactory')
const create = modeler.get('create')
var shape = elementFactory.createShape(Object.assign({ type:'bpmn:Task' }, {}))
create.start(event, shape)
}
更好的设计
以上只是考虑定制palette dom 的情况,如果用户只想调整palette 样式的话,可以设计成允许传个json 进去,也就是这里 的方案,整合两个方案后,个人认为适用人群会更多。具体的ui 库代码,作者还在优化中,敬请期待。。。