把bpmn-js 包成react 组件的想法

580 阅读3分钟

背景

不知道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 库代码,作者还在优化中,敬请期待。。。