在上一篇博客中我介绍了如何用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