正所谓百家争鸣、见仁见智、众说纷纭、各有千秋!在工作流bpmn2.0可视化建模工具实现的细分领域,网上扑面而来的是 bpmn.js 这个渲染工具包和web建模器,而笔者却认为使用flowable官方开源 editor-app 才是王道。
Flowable 开源版本中的 web 版流程设计器editor-app,展示风格和功能基本跟 activiti-modeler 一样,集成简单,开发工作量小,界面美观大方,功能强大,用户体验友好。
通过以下两张Gif动图来个PK,您的直观感受如何呢?
bpmn.js运行效果图(gif动图取自互联网)

Flowable editor-app运行效果:

boot-admin 是一款采用前后端分离模式、基于SpringCloud微服务架构的SaaS后台管理框架。系统内置基础管理、权限管理、运行管理、定义管理、代码生成器和办公管理6个功能模块,集成分布式事务Seata、工作流引擎Flowable、业务规则引擎Drools、后台作业调度框架Quartz等,技术栈包括Mybatis-plus、Redis、Nacos、Seata、Flowable、Drools、Quartz、SpringCloud、Springboot Admin Gateway、Liquibase、jwt、Openfeign、I18n等。
gitee源码地址
github源码地址
下面介绍 boot-admin 对flowable官方bpmn2.0可视化建模工具 editor-app 的集成改造步骤:
获取前端源码
- 下载官方数据包flowable-6.4.1.zip
- 从压缩包中解压出flowable-6.4.1\wars下面的flowable-modeler.war
- 从flowable-modeler.war中解压出 WEB-INF\classes\static\editor-app 文件夹
- 将数据包中 editor-app 文件夹复制到 boot-admin项目 前端工程的 public 文件夹下面
- 在 boot-admin项目 前端工程 public 文件夹下面创建 modeler.html 作为编辑器入口
modeler.html内容:整合改造前端源码
- 修改 ACTIVITI.CONFIG ,设置网关 URL
- var ACTIVITI = ACTIVITI || {};
- ACTIVITI.CONFIG = {
- 'contextRoot' : 'http://网关IP:网关端口号/api/workflow/auth/activiti',
- };
复制代码
- 修改 configuration\url-config.js,设置各具体访问点URL
- var KISBPM = KISBPM || {};
- KISBPM.URL = {
- //通过modelId,获取已保存模型的json数据
- getModel: function(modelId) {
- return ACTIVITI.CONFIG.contextRoot + '/model/json?modelId=' + modelId;
- },
- //获取汉化资源json数据
- getStencilSet: function() {
- return ACTIVITI.CONFIG.contextRoot + '/editor/stencilset?version=' + Date.now();
- },
- //保存模型数据
- putModel: function(modelId) {
- return ACTIVITI.CONFIG.contextRoot + '/model/save?modelId=' + modelId;
- },
- //从cookie中读取令牌
- getToken: function() {
- var cookies = document.cookie;
- var list = cookies.split("; "); // 解析出名/值对列表
- for (var i = 0; i < list.length; i++) {
- var arr = list[i].split("="); // 解析出名和值
- if (arr[0] == "Admin-Token") {
- var cookieVal = decodeURIComponent(arr[1]); // 对cookie值解码
- break;
- }
- }
- return 'Bearer' + cookieVal;
- }
- };
复制代码
- 修改 /public/editor-app/stencil-controller.js 中获取汉化包的方法,由源码中自由访问修改为携带令牌访问后台资源
- $http({method: 'GET',
- headers: {
- 'X-Token': KISBPM.URL.getToken()
- },
- url: KISBPM.URL.getStencilSet()})
- .success(function (data, status, headers, config) {
- var quickMenuDefinition = ['UserTask', 'EndNoneEvent', 'ExclusiveGateway',
- 'CatchTimerEvent', 'ThrowNoneEvent', 'TextAnnotation',
- 'SequenceFlow', 'Association'];
- var ignoreForPaletteDefinition = ['SequenceFlow', 'MessageFlow', 'Association', 'DataAssociation', 'DataStore', 'SendTask'];
- var quickMenuItems = [];
- var morphRoles = [];
- for (var i = 0; i < data.rules.morphingRules.length; i++)
- {
- var role = data.rules.morphingRules[i].role;
- var roleItem = {'role': role, 'morphOptions': []};
- morphRoles.push(roleItem);
- }
- // Check all received items
- for (var stencilIndex = 0; stencilIndex < data.stencils.length; stencilIndex++)
- {
- // Check if the root group is the 'diagram' group. If so, this item should not be shown.
- var currentGroupName = data.stencils[stencilIndex].groups[0];
- if (currentGroupName === 'Diagram' || currentGroupName === 'Form') {
- continue; // go to next item
- }
- var removed = false;
- if (data.stencils[stencilIndex].removed) {
- removed = true;
- }
- var currentGroup = undefined;
- if (!removed) {
- // Check if this group already exists. If not, we create a new one
- if (currentGroupName !== null && currentGroupName !== undefined && currentGroupName.length > 0) {
- currentGroup = findGroup(currentGroupName, stencilItemGroups); // Find group in root groups array
- if (currentGroup === null) {
- <template>
-
- <el-dialog :visible.sync="dialogVisible" :close-on-click-modal="false" width="80%" height="100%" title="模型编辑器" @close="closeDialog">
-
- <iframe ref="Modeler" id="map" scrolling="auto" v-bind:src="contents"
- frameborder="0" ></iframe>
-
- </el-dialog>
-
- </template>currentGroup = addGroup(currentGroupName, stencilItemGroups);
- }
- // Add all child groups (if any)
- for (var groupIndex = 1; groupIndex < data.stencils[stencilIndex].groups.length; groupIndex++) {
- <template>
-
- <el-dialog :visible.sync="dialogVisible" :close-on-click-modal="false" width="80%" height="100%" title="模型编辑器" @close="closeDialog">
-
- <iframe ref="Modeler" id="map" scrolling="auto" v-bind:src="contents"
- frameborder="0" ></iframe>
-
- </el-dialog>
-
- </template>var childGroupName = data.stencils[stencilIndex].groups[groupIndex];
- <template>
-
- <el-dialog :visible.sync="dialogVisible" :close-on-click-modal="false" width="80%" height="100%" title="模型编辑器" @close="closeDialog">
-
- <iframe ref="Modeler" id="map" scrolling="auto" v-bind:src="contents"
- frameborder="0" ></iframe>
-
- </el-dialog>
-
- </template>var childGroup = findGroup(childGroupName, currentGroup.groups);
- <template>
-
- <el-dialog :visible.sync="dialogVisible" :close-on-click-modal="false" width="80%" height="100%" title="模型编辑器" @close="closeDialog">
-
- <iframe ref="Modeler" id="map" scrolling="auto" v-bind:src="contents"
- frameborder="0" ></iframe>
-
- </el-dialog>
-
- </template>if (childGroup === null) {
- <template>
-
- <el-dialog :visible.sync="dialogVisible" :close-on-click-modal="false" width="80%" height="100%" title="模型编辑器" @close="closeDialog">
-
- <iframe ref="Modeler" id="map" scrolling="auto" v-bind:src="contents"
- frameborder="0" ></iframe>
-
- </el-dialog>
-
- </template> childGroup = addGroup(childGroupName, currentGroup.groups);
- <template>
-
- <el-dialog :visible.sync="dialogVisible" :close-on-click-modal="false" width="80%" height="100%" title="模型编辑器" @close="closeDialog">
-
- <iframe ref="Modeler" id="map" scrolling="auto" v-bind:src="contents"
- frameborder="0" ></iframe>
-
- </el-dialog>
-
- </template>}
- <template>
-
- <el-dialog :visible.sync="dialogVisible" :close-on-click-modal="false" width="80%" height="100%" title="模型编辑器" @close="closeDialog">
-
- <iframe ref="Modeler" id="map" scrolling="auto" v-bind:src="contents"
- frameborder="0" ></iframe>
-
- </el-dialog>
-
- </template>// The current group variable holds the parent of the next group (if any),
- <template>
-
- <el-dialog :visible.sync="dialogVisible" :close-on-click-modal="false" width="80%" height="100%" title="模型编辑器" @close="closeDialog">
-
- <iframe ref="Modeler" id="map" scrolling="auto" v-bind:src="contents"
- frameborder="0" ></iframe>
-
- </el-dialog>
-
- </template>// and is basically the last element in the array of groups defined in the stencil item
- <template>
-
- <el-dialog :visible.sync="dialogVisible" :close-on-click-modal="false" width="80%" height="100%" title="模型编辑器" @close="closeDialog">
-
- <iframe ref="Modeler" id="map" scrolling="auto" v-bind:src="contents"
- frameborder="0" ></iframe>
-
- </el-dialog>
-
- </template>currentGroup = childGroup;
- }
- }
- }
- // Construct the stencil item
- var stencilItem = {'id': data.stencils[stencilIndex].id,
- 'name': data.stencils[stencilIndex].title,
- 'description': data.stencils[stencilIndex].description,
- 'icon': data.stencils[stencilIndex].icon,
- 'type': data.stencils[stencilIndex].type,
- 'roles': data.stencils[stencilIndex].roles,
- 'removed': removed,
- 'customIcon': false,
- 'canConnect': false,
- 'canConnectTo': false,
- 'canConnectAssociation': false};
- if (data.stencils[stencilIndex].customIconId && data.stencils[stencilIndex].customIconId > 0) {
- stencilItem.customIcon = true;
- stencilItem.icon = data.stencils[stencilIndex].customIconId;
- }
- if (!removed) {
- if (quickMenuDefinition.indexOf(stencilItem.id) >= 0) {
- quickMenuItems[quickMenuDefinition.indexOf(stencilItem.id)] = stencilItem;
- }
- }
- if (stencilItem.id === 'TextAnnotation' || stencilItem.id === 'BoundaryCompensationEvent') {
- stencilItem.canConnectAssociation = true;
- }
- for (var i = 0; i < data.stencils[stencilIndex].roles.length; i++) {
- var stencilRole = data.stencils[stencilIndex].roles[i];
- if (stencilRole === 'sequence_start') {
- stencilItem.canConnect = true;
- } else if (stencilRole === 'sequence_end') {
- stencilItem.canConnectTo = true;
- }
- for (var j = 0; j < morphRoles.length; j++) {
- if (stencilRole === morphRoles[j].role) {
- if (!removed) {
- morphRoles[j].morphOptions.push(stencilItem);
- }
- stencilItem.morphRole = morphRoles[j].role;
- break;
- }
- }
- }
- if (currentGroup) {
- // Add the stencil item to the correct group
- currentGroup.items.push(stencilItem);
- if (ignoreForPaletteDefinition.indexOf(stencilItem.id) < 0) {
- currentGroup.paletteItems.push(stencilItem);
- }
- } else {
- // It's a root stencil element
- if (!removed) {
- stencilItemGroups.push(stencilItem);
- }
- }
- }
- for (var i = 0; i < stencilItemGroups.length; i++)
- {
- if (stencilItemGroups[i].paletteItems && stencilItemGroups[i].paletteItems.length == 0)
- {
- stencilItemGroups[i].visible = false;
- }
- }
- $scope.stencilItemGroups = stencilItemGroups;
- var containmentRules = [];
- for (var i = 0; i < data.rules.containmentRules.length; i++)
- {
- var rule = data.rules.containmentRules[i];
- containmentRules.push(rule);
- }
- $scope.containmentRules = containmentRules;
- // remove quick menu items which are not available anymore due to custom pallette
- var availableQuickMenuItems = [];
- for (var i = 0; i < quickMenuItems.length; i++)
- {
- if (quickMenuItems[i]) {
- availableQuickMenuItems[availableQuickMenuItems.length] = quickMenuItems[i];
- }
- }
- $scope.quickMenuItems = availableQuickMenuItems;
- $scope.morphRoles = morphRoles;
- }).
- error(function (data, status, headers, config) {
- console.log('Something went wrong when fetching stencil items:' + JSON.stringify(data));
- });
复制代码
- 修改 /public/editor-app/app.js 中获取模型数据的方法,由源码中自由访问修改为携带令牌访问后台资源
- function fetchModel(modelId) {
- var modelUrl = KISBPM.URL.getModel(modelId);
- $http({method: 'GET',
- headers: {'X-Token': KISBPM.URL.getToken()},
- url: modelUrl}).
- success(function (data, status, headers, config) {
- $rootScope.editor = new ORYX.Editor(data);
- $rootScope.modelData = angular.fromJson(data);
- $rootScope.editorFactory.resolve();
- }).
- error(function (data, status, headers, config) {
- console.log('Error loading model with id ' + modelId + ' ' + data);
- });
- }
复制代码
- 修改 /public/editor-app/configuration/toolbar-default-actions.js 中保存模型的方法,由源码中自由访问修改为携带令牌访问后台资源
- $http({ method: 'PUT',
- data: params,
- ignoreErrors: true,
- headers: {'Accept': 'application/json',
- 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
- 'X-Token': KISBPM.URL.getToken()},
- transformRequest: function (obj) {
- var str = [];
- for (var p in obj) {
- str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
- }
- return str.join("&");
- },
- url: KISBPM.URL.putModel(modelMetaData.modelId)})
- .success(function (data, status, headers, config) {
- $scope.editor.handleEvents({
- type: ORYX.CONFIG.EVENT_SAVED
- });
- $scope.modelData.name = $scope.saveDialog.name;
- $scope.modelData.lastUpdated = data.lastUpdated;
- $scope.status.loading = false;
- $scope.$hide();
- // Fire event to all who is listening
- var saveEvent = {
- type: KISBPM.eventBus.EVENT_TYPE_MODEL_SAVED,
- model: params,
- modelId: modelMetaData.modelId,
- eventType: 'update-model'
- };
- KISBPM.eventBus.dispatch(KISBPM.eventBus.EVENT_TYPE_MODEL_SAVED, saveEvent);
- // Reset state
- $scope.error = undefined;
- $scope.status.loading = false;
- // Execute any callback
- if (successCallback) {
- successCallback();
- }
- })
- .error(function (data, status, headers, config) {
- $scope.error = {};
- console.log('Something went wrong when updating the process model:' + JSON.stringify(data));
- $scope.status.loading = false;
- });
复制代码
- 创建 Modeler.vue 组件,以 iframe 形式将 editor-app 嵌入 vue-element-ui的弹窗 el-dialog 中
- <template>
-
- <el-dialog :visible.sync="dialogVisible" :close-on-click-modal="false" width="80%" height="100%" title="模型编辑器" @close="closeDialog">
-
- <iframe ref="Modeler" id="map" scrolling="auto" v-bind:src="contents"
- frameborder="0" ></iframe>
-
- </el-dialog>
-
- </template>
复制代码 workflow-model.js- import request from '@/utils/request'
- //分页获取模型数据
- export function fetchModelPage(data) {
- return request({
- url: '/api/workflow/auth/activiti/model/page',
- method: 'post',
- data
- })
- }
- //保存模型
- export function saveNewModel(data) {
- return request({
- url: '/api/workflow/auth/activiti/model/add',
- method: 'post',
- data
- })
- }
- //删除模型数据
- export function delModel(data) {
- return request({
- url: '/api/workflow/auth/activiti/model/del',
- method: 'post',
- data
- })
- }
- //部署模型
- export function deployModel(data) {
- return request({
- url: '/api/workflow/auth/activiti/model/deploy',
- method: 'post',
- data
- })
- }
- //获取模型XML
- export function fetchXml(data) {
- return request({
- url: '/api/workflow/auth/activiti/model/xml',
- method: 'post',
- data
- })
- }
复制代码 后端功能实现
对应前端需求,后端主要实现使用flowable引擎,获取汉化资源、读取模型数据、保存模型数据三个功能。
具体内容参见下一篇博文
项目源码仓库github
项目源码仓库gitee
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |