最近一直在做工作流的相关开发工作,自己也搭建了一个工作流的管理平台来方便开发调试。这里记录一下相关的工作。
工作流的引擎我采用的是业界流行的Camunda,这个引擎基于BPMN/DMN的标准来实现工作流,可以很方便的进行工作流的编排,实现低代码运行,以及业务逻辑编排等功能。
具体如何运行Camunda引擎在我之前的博客有介绍,这里不再重复,只描述一下如何搭建一个网站来实现工作流的管理。其实Camunda也提供了WEB的应用来管理工作流,但是如果自己有一些额外的需求,要进行一些自定义的功能,那么最好是自己开发一个WEB应用,通过调用Camunda引擎提供的API接口来实现对工作流的管理。
我选择用webpack+bootstrap的框架来快速搭建一个简单漂亮的Web应用。首先是用webpack来进行初始化的工作,新建一个名为workflow-manager的文件夹,在里面运行以下命令:
npm init -y
npm install webpack webpack-cli --save-dev
npm install copy-webpack-plugin --save-dev
npm install jquery bootstrap@4.6.2 feather-icons --save
npm install style-loader css-loader less-loader raw-loader --save-dev
在这个文件夹里面新建一个src目录,我们的html, js等文件都将放置在这个目录下.
在src目录里面我们新建一个workflow.html文件,内容如下:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="Mark Otto, Jacob Thornton, and Bootstrap contributors">
<meta name="generator" content="Hugo 0.101.0">
<title>Workflow Management</title>
<link rel="canonical" href="https://getbootstrap.com/docs/4.6/examples/dashboard/">
<!-- Bootstrap core CSS -->
<link href="assets/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
</style>
<!-- Custom styles for this template -->
<link href="assets/workflow.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0 shadow">
<a class="navbar-brand col-md-3 col-lg-2 mr-0 px-3" href="#">Company name</a>
<button class="navbar-toggler position-absolute d-md-none collapsed" type="button" data-toggle="collapse" data-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<ul class="navbar-nav px-3">
<li class="nav-item text-nowrap">
<a class="nav-link" href="#">Sign out</a>
</li>
</ul>
</nav>
<div class="container-fluid">
<div class="row">
<nav id="sidebarMenu" class="col-md-3 col-lg-2 d-md-block bg-light sidebar collapse">
<div class="sidebar-sticky pt-3">
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link active" href="#">
<span data-feather="home"></span>
Edit Workflow <span class="sr-only">(current)</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">
<span data-feather="file"></span>
View Workflow
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">
<span data-feather="shopping-cart"></span>
Edit Rule
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">
<span data-feather="users"></span>
View Rule
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">
<span data-feather="bar-chart-2"></span>
Reports
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">
<span data-feather="layers"></span>
Integrations
</a>
</li>
</ul>
</div>
</nav>
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 px-md-4">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">New/Edit Workflow</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group mr-2">
<button type="button" class="btn btn-sm btn-outline-secondary">Download</button>
<button type="button" class="btn btn-sm btn-outline-secondary">Deploy</button>
</div>
</div>
</div>
</main>
</div>
</div>
<script src="assets/jquery/dist/jquery.slim.min.js"></script>
<script src="assets/bootstrap/dist/bootstrap.bundle.min.js"></script>
<script src="assets/feather-icons/dist/feather.min.js"></script>
<script src="workflow.bundle.js"></script>
</body>
</html>
在这个html里面,可以看到引用了放置在本地的bootstrap的js, CSS,以及jquery,feather库的js文件。我们需要修改一下webpack.config.js文件,把node_modules里面安装的对应文件拷贝出来。这里需要用到前面安装的copy-webpack-plugin来进行拷贝。在文件夹的根目录新建一个webpack.config.js文件,内容如下:
var CopyPlugin = require('copy-webpack-plugin');
const path = require('path');
module.exports = {
entry: {
workflow: './src/workflow.js',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
{
test: /\.less$/i,
use: ['style-loader', 'css-loader', 'less-loader'],
},
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
}
],
},
plugins: [
new CopyPlugin({
patterns: [
{ from: 'node_modules/jquery/dist/jquery.slim.min.js', to: 'assets/jquery/dist' },
{ from: 'node_modules/bootstrap/dist/css/bootstrap.min.css', to: 'assets/bootstrap/dist/css' },
{ from: 'node_modules/bootstrap/dist/js/bootstrap.bundle.min.js', to: 'assets/bootstrap/dist' },
{ from: 'node_modules/feather-icons/dist/feather.min.js', to: 'assets/feather-icons/dist' },
{ from: 'src/workflow.html', to: 'workflow.html' },
{ from: 'src/workflow.css', to: 'assets/' },
]
}),
]
};
简要介绍一下,这里面的内容的entry, output部分描述了把src目录下的js文件编译为dist目录下的对应js文件(文件名增加了.bundle的字符)。module里面定义了解析不同后缀的文件所需要加载的loader。plugin里面定义了用copy-webpack-plugin来把文件拷贝到dist目录下。
最后我们修改一下package.json文件,增加命令来进行npm run build,编译拷贝文件到dist目录,内容如下:
{
"name": "dashboard",
"version": "1.0.0",
"description": "",
"private": true,
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"copy-webpack-plugin": "^11.0.0",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0"
},
"dependencies": {
"bootstrap": "^4.6.2",
"feather-icons": "^4.29.0",
"jquery": "^3.6.1"
}
}
在根目录下运行npm run build命令,等待执行完成后,在dist目录我们就可以找到编译后的内容了。在web服务器中访问dist目录的workflow.html文件,即可成功显示页面。
下一步我们需要修改这个workflow.html,使得可以进行Camunda workflow的编辑和部署。首先需要用npm安装相关的包:
npm install --save bpmn-js
npm install --save bpmn-js-properties-panel @bpmn-io/properties-panel
npm install --save camunda-bpmn-moddle
npm install --save axios
npm install --save keycloak-js
修改webpack.config.js文件,在module->rules里面增加一条规则,处理bpmn文件的加载。在copyplugin里面增加两行语句,把bpmn-js库里面的样式拷贝到本地。修改后的文件内容如下:
var CopyPlugin = require('copy-webpack-plugin');
const path = require('path');
module.exports = {
entry: {
workflow: './src/workflow.js',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
{
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/jquery/dist/jquery.slim.min.js', to: 'assets/jquery/dist' },
{ from: 'node_modules/bootstrap/dist/css/bootstrap.min.css', to: 'assets/bootstrap/dist/css' },
{ from: 'node_modules/bootstrap/dist/js/bootstrap.bundle.min.js', to: 'assets/bootstrap/dist' },
{ from: 'node_modules/feather-icons/dist/feather.min.js', to: 'assets/feather-icons/dist' },
{ from: 'src/workflow.html', to: 'workflow.html' },
{ from: 'src/workflow.css', to: 'assets/' },
{ from: 'node_modules/bpmn-js/dist/assets', to: 'vendor/bpmn-js/dist/assets' },
{ from: 'node_modules/bpmn-js-properties-panel/dist/assets', to: 'vendor/bpmn-js-properties-panel/dist/assets' },
]
}),
]
};
修改workflow.html文件,在<head>里面引入四个style文件
<link rel="stylesheet" href="vendor/bpmn-js/dist/assets/diagram-js.css">
<link rel="stylesheet" href="vendor/bpmn-js/dist/assets/bpmn-js.css">
<link rel="stylesheet" href="vendor/bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css">
<link rel="stylesheet" href="vendor/bpmn-js-properties-panel/assets/properties-panel.css" />
在<body>的<main>里面增加以下容器,用于放置bpmn编辑器:
<div class="content" id="js-drop-zone">
<div class="message intro">
<div class="note">
把本地的BPMN文件拖到浏览器或者 <a id="js-create-diagram" href>新建一个新的工作流</a>
</div>
</div>
<div class="message error">
<div class="note">
<p>出问题了,无法展示BPMN 2.0图表</p>
<div class="details">
<span>问题原因</span>
<pre></pre>
</div>
</div>
</div>
<div class="canvas" id="js-canvas"></div>
<div class="properties-panel-parent" id="js-properties-panel"></div>
</div>
这里面引入了一些新的样式定义,我们可以新建一个workflow.less文件,把这些样式定义在这个文件里面:
* {
box-sizing: border-box;
}
body,
html {
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%;
height: 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%;
}
}
新建workflow.js文件,如以下内容,这里要注意的是我引入了keycloak,因此会要求用户进行认证。具体如何配置camunda与keycloak集成,可以参考我之前的文章:Camunda工作流平台与Keycloak的集成_gzroy的博客-CSDN博客
import $ from 'jquery';
import './workflow.less';
import BpmnModeler from 'bpmn-js/lib/Modeler';
import diagramXML from './diagram.bpmn';
import Keycloak from 'keycloak-js';
import axios from 'axios';
import config from './config.json';
import {
BpmnPropertiesPanelModule,
BpmnPropertiesProviderModule,
CamundaPlatformPropertiesProviderModule
} from 'bpmn-js-properties-panel';
import CamundaBpmnModdle from 'camunda-bpmn-moddle/resources/camunda.json';
import customTranslate from './customTranslate/customTranslate';
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
}
});
var container = $('#js-drop-zone');
var token;
function initKeycloak() {
const keycloak = new Keycloak();
keycloak.init({onLoad: 'login-required'}).then(function(authenticated) {
if (authenticated) {
console.log(keycloak.token);
token = keycloak.token;
}
}).catch(function() {
alert('登录失败');
});
}
$('body').on('load', initKeycloak());
// Deployment button
$('#js-deployment').on("click", async function(event){
const { xml } = await modeler.saveXML({ format: true });
const parser = new DOMParser();
const xmldoc = parser.parseFromString(xml, "application/xml");
const processes = xmldoc.getElementsByTagName('bpmn2:process');
const process_name = processes[0].getAttribute('name');
const file = new File([xml], "diagram.bpmn", {type: "text/plain"});
const { svg } = await modeler.saveSVG();
const img_file = new File([svg], "diagram.svg", {type: "image/svg+xml"});
const data = new FormData();
data.append("deployment-name", process_name);
data.append("deployment-source", "process application");
data.append("data", file);
data.append("diagram", img_file);
axios.create({withCredentials: true}).post(
config.baseurl+'/engine-rest/deployment/create',
data,
{headers: {'Content-Type':'multipart/form-data', 'Authorization': 'Bearer '+token}}
).then(
res=>{
if (res.status==200) {
alert("部署成功,点击链接查看:"+res.data.links[0].href);
}
}
);
});
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);
console.log("registered");
}
// bootstrap diagram functions
$(function() {
$('#js-create-diagram').on('click', function(e) {
e.stopPropagation();
e.preventDefault();
createNewDiagram();
});
var downloadLink = $('#js-download-diagram');
var downloadSvgLink = $('#js-download-svg');
$('.buttons a').on('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);
};
}
最后运行npm run build编译之后,在Web服务器运行workflow.html,效果如下:
workflow_demo