搭建一个BPMN建模的Web服务

在上一篇博客中我介绍了如何用Camunda来做工作流的编排,其中用到了Camunda提供的Modeler软件来进行工作流的建模。这个软件是基于bpmn.io的开源软件基础上开发的,是一个桌面版的软件。如果我们需要以Web服务的方式来提供,那么我们也可以基于bpmn.io来做自己的一些定制开发,具体可以查看bpmn.io · GitHub

下面我将搭建一个Web版本的工作流建模服务,并且实现汉化。

项目设置

新建一个文件夹,运行以下命令

mkdir modeler-demo
cd modeler-demo
npm init -y
npm install webpack webpack-cli --save-dev
npm install copy-webpack-plugin --save-dev
npm install style-loader css-loader less-loader raw-loader --save-dev
npm install jquery --save-dev
npm install --save bpmn-js
npm install --save bpmn-js-properties-panel @bpmn-io/properties-panel
npm install --save camunda-bpmn-moddle

新建一个webpack.config.js文件,内容如下:

var CopyPlugin = require('copy-webpack-plugin');

const path = require('path');

module.exports = {
  entry: './src/app.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
  module: {
    rules: [
      {
        test: /\.less$/i,
        use: ['style-loader', 'css-loader', 'less-loader'],
      },
      {
        test: /\.(png|svg|jpg|jpeg|gif)$/i,
        type: 'asset/resource',
      },
      {
        test: /\.bpmn$/i,
        use: ['raw-loader'],
      },
    ],
  },
  plugins: [
    new CopyPlugin({
      patterns: [
        { from: 'node_modules/bpmn-js/dist/assets', to: 'vendor/bpmn-js/assets' },
        { from: 'node_modules/bpmn-js-properties-panel/dist/assets', to: 'vendor/bpmn-js-properties-panel/assets' },
      ]
    })
  ]
};

编写程序,加载BPMN的模块

在目录中新建一个src目录和一个dist目录.

在src目录里面新建一个app.js文件,加载bpmn的相关模块和设置。

import $ from 'jquery';

import './app.less';
import BpmnModeler from 'bpmn-js/lib/Modeler';
import diagramXML from './newDiagram.bpmn';

import {
  BpmnPropertiesPanelModule,
  BpmnPropertiesProviderModule,
  CamundaPlatformPropertiesProviderModule
} from 'bpmn-js-properties-panel';

import CamundaBpmnModdle from 'camunda-bpmn-moddle/resources/camunda.json';
import customTranslate from './customTranslate/customTranslate';

var container = $('#js-drop-zone');
var customTranslateModule = {
  translate: [ 'value', customTranslate ]
};

var modeler = new BpmnModeler({
  container: '#js-canvas',
  propertiesPanel: {
    parent: '#js-properties-panel'
  },
  additionalModules: [
    BpmnPropertiesPanelModule,
    BpmnPropertiesProviderModule,
    CamundaPlatformPropertiesProviderModule,
    customTranslateModule
  ],
  moddleExtensions: {
    camunda: CamundaBpmnModdle
  }
});

function createNewDiagram() {
  openDiagram(diagramXML);
}

async function openDiagram(xml) {

  try {

    await modeler.importXML(xml);

    container
      .removeClass('with-error')
      .addClass('with-diagram');
  } catch (err) {

    container
      .removeClass('with-diagram')
      .addClass('with-error');

    container.find('.error pre').text(err.message);

    console.error(err);
  }
}

function registerFileDrop(container, callback) {

  function handleFileSelect(e) {
    e.stopPropagation();
    e.preventDefault();

    var files = e.dataTransfer.files;

    var file = files[0];

    var reader = new FileReader();

    reader.onload = function(e) {

      var xml = e.target.result;

      callback(xml);
    };

    reader.readAsText(file);
  }

  function handleDragOver(e) {
    e.stopPropagation();
    e.preventDefault();

    e.dataTransfer.dropEffect = 'copy'; // Explicitly show this is a copy.
  }

  container.get(0).addEventListener('dragover', handleDragOver, false);
  container.get(0).addEventListener('drop', handleFileSelect, false);
}


// file drag / drop ///

// check file api availability
if (!window.FileList || !window.FileReader) {
  window.alert(
    'Looks like you use an older browser that does not support drag and drop. ' +
    'Try using Chrome, Firefox or the Internet Explorer > 10.');
} else {
  registerFileDrop(container, openDiagram);
}

// bootstrap diagram functions

$(function() {

  $('#js-create-diagram').click(function(e) {
    e.stopPropagation();
    e.preventDefault();

    createNewDiagram();
  });

  var downloadLink = $('#js-download-diagram');
  var downloadSvgLink = $('#js-download-svg');

  $('.buttons a').click(function(e) {
    if (!$(this).is('.active')) {
      e.preventDefault();
      e.stopPropagation();
    }
  });

  function setEncoded(link, name, data) {
    var encodedData = encodeURIComponent(data);

    if (data) {
      link.addClass('active').attr({
        'href': 'data:application/bpmn20-xml;charset=UTF-8,' + encodedData,
        'download': name
      });
    } else {
      link.removeClass('active');
    }
  }

  var exportArtifacts = debounce(async function() {

    try {

      const { svg } = await modeler.saveSVG();

      setEncoded(downloadSvgLink, 'diagram.svg', svg);
    } catch (err) {

      console.error('Error happened saving svg: ', err);
      setEncoded(downloadSvgLink, 'diagram.svg', null);
    }

    try {

      const { xml } = await modeler.saveXML({ format: true });
      setEncoded(downloadLink, 'diagram.bpmn', xml);
    } catch (err) {

      console.error('Error happened saving XML: ', err);
      setEncoded(downloadLink, 'diagram.bpmn', null);
    }
  }, 500);

  modeler.on('commandStack.changed', exportArtifacts);
});



// helpers //

function debounce(fn, timeout) {

  var timer;

  return function() {
    if (timer) {
      clearTimeout(timer);
    }

    timer = setTimeout(fn, timeout);
  };
}

新建一个app.less文件,包含格式方面的定义:

* {
  box-sizing: border-box;
}

body,
html {
  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;

  font-size: 12px;

  height: 100%;
  max-height: 100%;
  padding: 0;
  margin: 0;
}

#js-properties-panel {
  width: 400px;
}

a:link {
  text-decoration: none;
}

.content {
  position: relative;
  width: 100%;
  height: 100%;
  display: flex;

  > .message {
    width: 100%;
    height: 100%;
    text-align: center;
    display: table;

    font-size: 16px;
    color: #111;

    .note {
      vertical-align: middle;
      text-align: center;
      display: table-cell;
    }

    &.error {
      .details {
        max-width: 500px;
        font-size: 12px;
        margin: 20px auto;
        text-align: left;
        color: #BD2828;
      }

      pre {
        border: solid 1px #BD2828;
        background: #fefafa;
        padding: 10px;
        color: #BD2828;
      }
    }
  }
  &:not(.with-error) .error,
  &.with-error .intro,
  &.with-diagram .intro {
    display: none;
  }

  .canvas {
    width: 100%;
  }

  .canvas,
  .properties-panel-parent {
    display: none;
  }

  &.with-diagram {
    .canvas,
    .properties-panel-parent  {
      display: block;
    }
  }
}


.buttons {
  position: fixed;
  bottom: 20px;
  left: 20px;

  padding: 0;
  margin: 0;
  list-style: none;

  > li {
    display: inline-block;
    margin-right: 10px;

    > a {
      background: #DDD;
      border: solid 1px #666;
      display: inline-block;
      padding: 5px;
    }
  }

  a {
    opacity: 0.3;
  }

  a.active {
    opacity: 1.0;
  }
}

.properties-panel-parent {
  border-left: 1px solid #ccc;
  overflow: auto;
  &:empty {
    display: none;
  }
  > .djs-properties-panel {
    padding-bottom: 70px;
    min-height:100%;
  }
}

在以上程序中,加载了一个定制化的翻译组件。我们需要在src目录下新建一个customTranslate的目录,在目录下新建一个customTranslate.js,如以下内容:

import translations from './translations';


export default function customTranslate(template, replacements) {
  replacements = replacements || {};

  // Translate
  template = translations[template] || template;

  // Replace
  return template.replace(/{([^}]+)}/g, function(_, key) {
    return replacements[key] || '{' + key + '}';
  });
}

另外新建一个translation.js文件,里面保存对应的翻译信息:

/**
 * This is a sample file that should be replaced with the actual translation.
 *
 * Checkout https://github.com/bpmn-io/bpmn-js-i18n for a list of available
 * translations and labels to translate.
 */
export default {
  'Exclusive Gateway': 'Exklusives Gateway',
  'Parallel Gateway': 'Paralleles Gateway',
  'Inclusive Gateway': 'Inklusives Gateway',
  'Complex Gateway': 'Komplexes Gateway',
  'Service Task': '服务任务',
  'Event based Gateway': 'Ereignis-basiertes Gateway',
  'Message Start Event': '消息启动事件',
  'Timer Start Event': '定时启动事件',
  'Conditional Start Event': '条件启动事件',
  'Signal Start Event': '信号启动事件',
  'Error Start Event': '错误启动事件',
  'Escalation Start Event': '升级启动事件',
  'Compensation Start Event': '补偿启动事件',
  'Message Start Event (non-interrupting)': '消息启动事件 (非中断)',
  'Timer Start Event (non-interrupting)': '定时启动事件 (非中断)',
  'Conditional Start Event (non-interrupting)': '条件启动事件 (非中断)',
  'Signal Start Event (non-interrupting)': '信号启动事件 (非中断)',
  'Escalation Start Event (non-interrupting)': '升级启动事件 (非中断)',
  //---------属性面板--------
  'General':'通用',
  'Details':'详情',
  'Process':'进程',
  'Documentation':'文档',
  'Version Tag':'版本标签',
  'Category Value':'类别值',
  'Process Id':'进程 Id',
  'Process Name':'进程 Name',
  'Link Name':'链接名称',
  'Element documentation':'元素文档',
  'Process Documentation':'进程文档',
  'Executable':'可执行',
  'Task Priority':'任务优先级',
  'Startable':'可开始',
  'Version tag':'版本标签'
};

在dist目录新建一个index.html文件,里面需要加入相应的容器,用于包含bpmn的相应组件。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Getting Started</title>
    <link rel="stylesheet" href="https://unpkg.com/bpmn-js@9.1.0/dist/assets/diagram-js.css">
    <link rel="stylesheet" href="https://unpkg.com/bpmn-js@9.1.0/dist/assets/bpmn-js.css">
    <link rel="stylesheet" href="https://unpkg.com/bpmn-js@9.1.0/dist/assets/bpmn-font/css/bpmn-embedded.css">
    <link rel="stylesheet" href="vendor/bpmn-js-properties-panel/assets/properties-panel.css" />
    <style>
      html, body, #canvas {
        height: 100%;
        padding: 0;
        margin: 0;
      }
    </style>
  </head>
  <body>
    <div class="content" id="js-drop-zone">

      <div class="message intro">
        <div class="note">
          Drop BPMN diagram from your desktop or <a id="js-create-diagram" href>create a new diagram</a> to get started.
        </div>
      </div>
  
      <div class="message error">
        <div class="note">
          <p>Ooops, we could not display the BPMN 2.0 diagram.</p>
  
          <div class="details">
            <span>cause of the problem</span>
            <pre></pre>
          </div>
        </div>
      </div>
  
      <div class="canvas" id="js-canvas"></div>
      <div class="properties-panel-parent" id="js-properties-panel"></div>
    </div>
  
    <ul class="buttons">
      <li>
        download
      </li>
      <li>
        <a id="js-download-diagram" href title="download BPMN diagram">
          BPMN diagram
        </a>
      </li>
      <li>
        <a id="js-download-svg" href title="download as SVG image">
          SVG image
        </a>
      </li>
    </ul>

    <script src="bundle.js"></script>
  </body>
</html>

运行npm run build来进行编译,然后在web服务器中部署,访问dist目录下的index.html文件,即可在浏览器中进行工作流的建模。

有一个小的问题,在属性面板的显示里面,Header和Group都没有汉化。搜索了一下GitHub bpmn的issue,里面有人给出了一个解决方案。

在node_modules目录下,找到bpmn-js-properties-panel->dist->index.esm.js文件,修改里面的class BpmnPropertiesProvider的定义如下:

class BpmnPropertiesProvider {
  constructor(propertiesPanel, translate) {
    BpmnPropertiesProvider.prototype.translate = translate
    propertiesPanel.registerProvider(this);
  }

  getGroups(element) {
    const translate = this.translate;
    return groups => {
      groups = groups.concat(getGroups(element));
      forEach(groups, function(group){
        group.label = translate(group.label);
      })
      return groups;
    };
  }
}
BpmnPropertiesProvider.$inject = ['propertiesPanel', 'translate'];

修改const PanelHeaderProvider里面的getTypeLabel如下

getTypeLabel: element => {
    const elementTemplates = getTemplatesService();

    if (elementTemplates) {
      const template = getTemplate(element, elementTemplates);

      if (template && template.name) {
        return template.name;
      }
    }

    const concreteType = getConcreteType(element);
    const translate = useService('translate');
    return translate(concreteType.replace(/(\B[A-Z])/g, ' $1').replace(/(\bNon Interrupting)/g, '($1)'));
  }

重新npm run build即可

效果展示

运行效果如下:

bpmn_demo

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

gzroy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值