BPMN使用小結

內部管理系統須要多種流程設計,方便在 Web PC 手動設計業務流程,保證單個業務能夠設計多個流程,而且能夠進行流程跟蹤的綜合要求。css

後端有一套本身的流程引擎,解析相應的 xml,而後部署業務流程。
起初後端是想直接用 activiti-designeer 作流程設計,該方法基本知足業務需求,但後期發現這樣作太麻煩,因而推薦前端使用 BPMN插件,放在管理系統中使用,生成 xmlsvg 字符串傳給接口,保存該業務流程。官方實例html

基本用法

  • 安裝依賴包
yarn add bpmn-js
yarn add bpmn-js-properties-panel // 屬性面板
yarn add camunda-bpmn-moddle // BPMN讀寫,與流程引擎相關聯
  • 初始化
let xml; // BPMN 2.0 xml 
let viewer = new BpmnJS({
    container: '#canvas',
    height: 400,
});

viewer.importXML(xml, function(err) {

    if (err) {
        console.log('error rendering: ', err)
    } else {
        console.log('rendered:')
    }
});

React 中用法

使用了 ant-design-pro 最初搭建好的後臺項目(非 ts 版本) 搭建的項目:
BPMN React 例子,更多關注 思否前端


流程設計的界面按照圖片上的佈局能夠分紅四部分:左(工具面板)、中(畫布)、右(表單面板)、懸浮(附加操做)react

  • 1.工具面板 --> 添加流程網關、工具跟相應的節點,起到流程設計過程當中的輔助做用
  • 2.畫布 --> 繪製流程圖,點擊網關/節點,可操做對應網關/節點,調整節點間的關係git

    在項目中引入 bpmn-js/lib/Modeler 獲取 BPMN 建模器,而後建立一個建摸器github

    import BpmnModeler from 'bpmn-js/lib/Modeler';
    this.bpmnModeler = new BpmnModeler({
        container: '#canvas',
    });

    能夠在當前 Modeler 的基礎上添加一些額外的功能。好比,工具面板,調色板等等canvas

    import Modeler from 'bpmn-js/lib/Modeler';
    import {assign, isArray} from 'min-dash';
    import inherits from 'inherits';
    import CustomTranslate from './customTranslate';
    import CustomPalette from './customPalette';
    import ColorPickerModule from './customColor';
    
    export default function CustomModeler(options) {
        Modeler.call(this, options);
    
        this.customElements = [];
    }
    
    inherits(CustomModeler, Modeler);
    
    CustomModeler.prototype._modules = [].concat(CustomModeler.prototype._modules, [
        CustomTranslate,
        CustomPalette,
        ColorPickerModule,
    ]);
  • 3.表單面板 --> 可對流程網關/節點添加字段標記及條件segmentfault

    import propertiesPanelModule from 'bpmn-js-properties-panel'; // bpmn中自帶的控件
    import propertiesProviderModule from './PanelToolbar'; // 自定義表單
    // PanelToolbar/index.js
    import inherits from 'inherits';
    import PropertiesActivator from 'bpmn-js-properties-panel/lib/PropertiesActivator';
    import baseInfo from './parts/BaseInfoProps';
    
    // 建立基礎信息看板
    function createBaseInfoTab(element, bpmnFactory, elementRegistry, translate) {
        const generalGroup = {
            id: 'baseInfo',
            label: '',
            entries: [],
        };
    
        baseInfo(generalGroup, element, bpmnFactory, translate);
        return [generalGroup];
    }
    
    function MagicPropertiesProvider(eventBus, bpmnFactory, elementRegistry, translate) {
        PropertiesActivator.call(this, eventBus);
        this.getTabs = function(element) {
            const baseInfoTab = {
                id: 'baseInfo',
                label: '基本信息',
                groups: createBaseInfoTab(element, bpmnFactory, elementRegistry, translate),
            };
    
            return [baseInfoTab];
        };
    }
    
    inherits(MagicPropertiesProvider, PropertiesActivator);
    
    MagicPropertiesProvider.$inject = ['eventBus', 'bpmnFactory', 'elementRegistry', 'translate'];
    
    export default {
        __init__: ['propertiesProvider'],
        propertiesProvider: ['type', MagicPropertiesProvider],
    };
  • 4.附加操做 --> 能夠提供 打開bpmn文件、回退、縮放、下載、預覽、保存等功能(按照本身須要,調用 BPMN 對象中提供的方法)後端

    import React, {Component} from 'react';
    import {notification} from 'antd';
    
    class EditTools extends Component {
        state = {
            scale: 1, // 流程圖比例
        }
        // 打開文件
        handleOpen = () => {
            this.file.click();
        };
    
        // 下載xml/svg
        download = (type, data, name) => {
            let dataTrack = '';
            const a = document.createElement('a');
    
            switch (type) {
                case 'xml':
                    dataTrack = 'bpmn';
                    break;
                case 'svg':
                    dataTrack = 'svg';
                    break;
                default:
                    break;
            }
    
            name = name || `diagram.${dataTrack}`;
    
            a.setAttribute(
                'href',
                `data:application/bpmn20-xml;charset=UTF-8,${encodeURIComponent(data)}`
            );
            a.setAttribute('target', '_blank');
            a.setAttribute('dataTrack', `diagram:download-${dataTrack}`);
            a.setAttribute('download', name);
    
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
        };
    
        // 導入 xml 文件
        handleOpenFile = e => {
            const that = this;
            const file = e.target.files[0];
            const reader = new FileReader();
            let data = '';
            reader.readAsText(file);
            reader.onload = function(event) {
                data = event.target.result;
                that.renderDiagram(data, 'open');
            };
        };
    
        // 保存
        handleSave = () => {
            this.bpmnModeler.saveXML({format: true}, (err, xml) => {
                console.log(xml);
            });
            this.bpmnModeler.saveSVG({format: true}, (err, data) => {
                console.log(data);
            });
        };
    
        // 前進
        handleRedo = () => {
            this.bpmnModeler.get('commandStack').redo();
        };
    
        // 後退
        handleUndo = () => {
            this.bpmnModeler.get('commandStack').undo();
        };
    
        // 下載 SVG 格式
        handleDownloadSvg = () => {
            this.bpmnModeler.saveSVG({format: true}, (err, data) => {
                this.download('svg', data);
            });
        };
    
        // 下載 XML 格式
        handleDownloadXml = () => {
            this.bpmnModeler.saveXML({format: true}, (err, data) => {
                this.download('xml', data);
            });
        };
    
        // 流程圖放大縮小
        handleZoom = radio => {
            const newScale = !radio
                ? 1.0 // 不輸入radio則還原
                : this.state.scale + radio <= 0.2 // 最小縮小倍數
                ? 0.2
                : this.state.scale + radio;
    
            this.bpmnModeler.get('canvas').zoom(newScale);
            this.setState({
                scale: newScale,
            });
        };
    
        // 渲染 xml 格式
        renderDiagram = xml => {
            this.bpmnModeler.importXML(xml, err => {
                if (err) {
                    notification.error({
                        message: '提示',
                        description: '導入失敗',
                    });
                }
            });
        };
    
        render () {
            return (<ul className={styles.controlList}>
                <li className={`${styles.control} ${styles.line}`}>
                    <input
                        ref={file => {
                            this.file = file;
                        }}
                        className={styles.openFile}
                        type="file"
                        onChange={this.onOpenFIle}
                    />
                    <button type="button" title="打開BPMN文件" onClick={this.handleOpen}>
                        <i className={styles.open} />
                    </button>
                </li>
    
                <li className={styles.control}>
                    <button type="button" title="撤銷" onClick={this.handleUndo}>
                        <i className={styles.undo} />
                    </button>
                </li>
                <li className={`${styles.control} ${styles.line}`}>
                    <button type="button" title="恢復" onClick={this.handleRedo}>
                        <i className={styles.redo} />
                    </button>
                </li>
    
                <li className={styles.control}>
                    <button type="button" title="重置大小" onClick={this.handleZoom}>
                        <i className={styles.zoom} />
                    </button>
                </li>
                <li className={styles.control}>
                    <button type="button" title="放大" onClick={() => this.handleZoom(0.1)}>
                        <i className={styles.zoomIn} />
                    </button>
                </li>
                <li className={`${styles.control} ${styles.line}`}>
                    <button type="button" title="縮小" onClick={() => this.handleZoom(-0.1)}>
                        <i className={styles.zoomOut} />
                    </button>
                </li>
                <li className={styles.control}>
                    <button type="button" title="下載BPMN文件" onClick={this.handleDownloadXml}>
                        <i className={styles.download} />
                    </button>
                </li>
                <li className={styles.control}>
                    <button type="button" title="下載流程圖片" onClick={this.handleDownloadSvg}>
                        <i className={styles.image} />
                    </button>
                </li>
            </ul>)
        }
    }
    
    export default EditTools;
    • 導入文件 this.bpmnModeler.importXML(xml, err => {});
    • 前進 this.bpmnModeler.get('commandStack').redo();
    • 後退 this.bpmnModeler.get('commandStack').undo();
    • 放大/縮小/重置 this.bpmnModeler.get('canvas').zoom(newScale);
    • 下載 svg this.bpmnModeler.saveSVG({format: true}, (err, data) => {});
    • 下載 xml this.bpmnModeler.saveXML({format: true}, (err, data) => {});
    • 點擊 xml,獲取節點 id
源碼中
```js
/**
 * Register an event listener
*
* Remove a previously added listener via {@link #off(event, callback)}.
*
* @param {String} event
* @param {Number} [priority]
* @param {Function} callback
* @param {Object} [that]
*/
Viewer.prototype.on = function(event, priority, callback, target) {
    return this.get('eventBus').on(event, priority, callback, target);
};
```

元素添加相應事件。好比,點擊、懸浮等等
```js
import React, {Component, Fragment} from 'react';
import BpmnViewer from 'bpmn-js';
import {diagramXML} from './xml';
import './Bpmn.css';

class Bpmn extends Component {
    componentDidMount() {
        const {callback} = this.props;
        let viewer = new BpmnViewer({
            container: '#canvas',
            // height: 400
        });

        viewer.importXML(diagramXML, function(err) {
            if (err) {
                console.error('failed to load diagram');
                console.error(err);
                return console.log('failed to load diagram', err);
            }
            let eventBus = viewer.get('eventBus');
            let events = [
                'element.click',
                // 'element.dblclick',
                // 'element.hover',
                // 'element.out',
                // 'element.mousedown',
                // 'element.mouseup'
            ];
            events.forEach(function(event) {
                eventBus.on(event, function(e) {
                    console.log(event, 'on', e.element.id);
                    callback(e.element.id); // 流程圖點擊回調
                });
            });
            // 刪除 bpmn logo
            const bjsIoLogo = document.querySelector('.bjs-powered-by');
            while (bjsIoLogo.firstChild) {
                bjsIoLogo.removeChild(bjsIoLogo.firstChild);
            }
        });
    }

    render() {
        const {data} = this.props;
        return (<Fragment>
            <div id="canvas" style={{height: '100%'}} />
            <div>{data.id}</div>
        </Fragment>);
    }
}

export default Bpmn;
```

遇到的問題

  • 自定義節點信息表單

官網提供了一些 BPMN 實例,能夠自定義單個表單(inout、select、checkbox...)antd

import entryFactory from 'bpmn-js-properties-panel/lib/factory/EntryFactory';
import script from 'bpmn-js-properties-panel/lib/provider/camunda/parts/implementation/Script';
import {query} from 'min-dom';

// 編號
const BaseInfoProps = (group, element, bpmnFactory, translate) => {
    group.entries.push(
        entryFactory.textField({
            id: 'id',
            label: translate('編號'),
            modelProperty: 'id',
        })
    );

    group.entries.push(
        entryFactory.textField({
            id: 'name',
            label: translate('名稱'),
            modelProperty: 'name',
            validate: function(element, values) {
                let validationResult = {};

                if (!values.name) {
                    validationResult.name = '請輸入節點名稱';
                }

                if (values.name && values.name.length > 30) {
                    validationResult.name = '名稱最多30個字';
                }

                return validationResult;
            },
        })
    );

    group.entries.push({
        id: 'condition',
        label: translate('Condition'),
        html: `
            <div class="bpp-row">
                <label for="cam-condition">${translate('Expression')}</label>
                <div class="bpp-field-wrapper">
                    <input id="cam-condition" type="text" name="condition" placeholder="請輸入" />
                    <button class="clear" data-action="clear" data-show="canClear">
                        <span>X</span>
                    </button>
                </div>
            </div>
        `,
        get: function(element) {
            let values = {};
            // ...
            return values;
        },
        set: function(element, values) {
            let commands = [];
            // ...
            return commands;
        },
        validate: function(element, values) {
            let validationResult = {};

            if (!values.condition) {
                validationResult.condition = '請輸入表達式${表達式}';
            }

            return validationResult;
        },
        clear: function(element, inputNode) {
            query('input[name=condition]', inputNode).value = '';
            return true;
        },
        canClear: function(element, inputNode) {
            let input = query('input[name=condition]', inputNode);
            return input.value !== '';
        },
        script: script,
        cssClasses: ['bpp-textfield'],
    });
}

export default BaseInfoProps;
  • No provider for "e"!

在本地聯調部署都沒有問題,打包到正式環境的時候,進入初始化截斷,開始報如下錯誤:

Error: No provider for "e"! (Resolving: colorPicker -> e)

起初覺得 colorPicker 中的代碼不夠完善,反正這個也不用,就刪了吧,上線要緊,結果錯誤老是驚人的類似,又出現如下錯誤:

Error: No provider for "e"! (Resolving: propertiesPanel -> propertiesProvider -> e)
No provider for "e"! (Resolving: colorPicker -> e)

因而找到了這個網站 BPMN問題網站,裏面有一些解釋,意思就是:定義的函數須要使用 $inject 來註釋服務 annotate your service.

export default function ColorPicker(eventBus, contextPad, commandStack) {
    // ...
}
ColorPicker.$inject = [
    'eventBus',
    'contextPad',
    'commandStack',
];
  • 生成的 xml 位置不能進行調節,可是 svg 能夠

關於 viewBox preserveAspectRatio

  • viewBox="x, y, width, height"

更形象的解釋就是:SVG 就像是咱們的顯示器屏幕,viewBox 就是截屏工具選中的那個框框,最終的呈現就是把框框中的截屏內容再次在顯示器中全屏顯示!

  • preserveAspectRatio="xMinYMin meet"

preserveAspectRatio 屬性的值爲空格分隔的兩個值組合而成。例如,上面的 xMidYMidmeet.

網址

相關文章
相關標籤/搜索