Replace bpmn-js and Let Frontend Developers Become More Familiar with Workflow Business
此篇为我推广内部开源项目发在国外社区的文章
Preface
Seeing this title, some of you may wonder: Isn't bpmn-js the most common frontend solution for workflow systems? Why do we need to replace bpmn-js? Here, from a frontend perspective, let's first clarify the relationships among workflows, workflow management systems, workflow engines, BPMN specifications, and bpmn-js.
Workflow
The concept of a workflow is described in a somewhat abstract manner on Baidu Baike and Wikipedia. In general terms, we can understand a workflow as a series of steps used to perform a task, where these steps are organized in a logical sequence to achieve the desired outcome. For example, the activities we perform in the morning can be viewed as a workflow, which can be broken down into steps like going to the restroom, washing hands, having breakfast, and brushing teeth. Different individuals may organize these steps differently; some might follow the sequence "restroom -> washing hands -> having breakfast -> brushing teeth," while others may follow "brushing teeth -> restroom -> having breakfast -> washing hands." The variation in the organization of these steps leads to different outcomes.
By transferring the processes of task decomposition, organization, execution, and management to the realm of computers, we create what is known as a workflow in the digital context.
Workflow Management System:
This is the type of system we usually develop, which revolves around processes and uses a graphical representation of workflows. Since it involves graphics, drawing is required, and most systems have moved the drawing process to web-based platforms. This is why the frontend involvement becomes necessary.
Workflow Engine:
The workflow engine is generally used on the backend to execute the organized steps. It determines the sequence of events, like whether we should have breakfast or wash hands after going to the restroom. There are several workflow engines available, with the most common ones being Activiti, Camunda BPM, and Flowable.
BPMN Specification:
BPMN stands for "Business Process Model and Notation," which translates to "业务流程建模符号" in Chinese. It is an industry standard defined by an organization. As mentioned earlier, describing how to perform a task can be done graphically. However, different people may draw different diagrams, and different workflow engines may produce different data from the diagrams. To ensure better understanding of diagrams by both humans and machines, an organization introduced a standard that provides specific definitions for the meaning of nodes and connections on the diagram, as well as the data format to be submitted to the workflow engine.
Currently, most open-source workflow engines support the BPMN specification. Below is a snippet of information from Wikipedia:
bpmn-js
This is easy to understand; it is an open-source web-based modeler that implements the BPMN 2.0 specification. It is essential to note that although bpmn-js has "bpmn" in its name, it is not developed by the official organization responsible for the BPMN specification. Therefore, the support of the BPMN specification by a workflow engine does not necessarily mean that frontend developers must use bpmn-js. Instead, we can develop a custom process designer based on the BPMN specification that aligns with our specific business needs.
Why am I choosing to replace bpmn-js?
From the above description, we can see that bpmn-js naturally complies with the BPMN specification, and the vast majority of workflow engines also support the BPMN specification. As frontend developers, we can easily integrate bpmn-js into our project and use it out of the box. However, why would we consider replacing bpmn-js? The main reason is that bpmn-js has brought us the following pain points.
Business Logic Black Box
In workflow-related projects, as the maintenance period grows, frontend developers often take on resource-oriented roles and may not be concerned with the entire workflow's business logic. Since bpmn-js implements all the business logic internally, frontend developers cannot directly perceive the business logic at the code level. In such cases, any modifications made at the frontend level can become chaotic due to the lack of constraints.
For instance, if the backend needs to add a field to the data generated at a certain node, some may choose to directly replace it using aggressive regex during data export, while others might hardcode it into the bpmn-js source code or forcefully retrieve all nodes and update the corresponding properties. As more and more ad-hoc code is scattered throughout the project, changing frontend developers can eventually make the project difficult to maintain.
Currently, there are various tutorials and "hacks" available online for customizing bpmn-js. However, these are rooted in the fact that bpmn-js does not provide complete openness for customization. Although there are many examples of predefined scenarios, they might not cover all real-world project scenarios.
Furthermore, the bpmn-js documentation is relatively lacking and mostly consists of knowledge shared by users based on their practical experiences.
Readability of the Source Code
Due to the limited openness of bpmn-js' customization mechanism, when certain products require more in-depth extensions, its default customization often falls short. For example, I came across a requirement to add an expand and collapse feature to the process diagram, which is virtually impossible to achieve using bpmn-js. Some projects might introduce the bpmn-js source code into their project for extensive custom modifications. However, they may eventually find that due to issues with the source code's readability, frontend developers struggle to understand bpmn-js's underlying logic. In order to meet the requirements, they have to resort to making ad-hoc changes, and the final result is often frontend developers leaving the project.
In the past, our department worked on an approval project for the education sector. However, no matter which frontend developer took over, they would leave shortly after. The end result was “a rock-solid workflow backend, but a constantly changing frontend”, which was quite frustrating.
Career Development
For frontend developers, investing a significant amount of effort to study the bpmn-js source code is not very meaningful. As iron-clad bpmn-js frontend developers, it hinders our career choices and growth. In terms of bpmn-js itself, its form and specifications are fixed, which limits our frontend expertise in the UI layer. If faced with any requirements, answering "no, bpmn-js doesn't have that functionality" challenges our own professionalism and hampers our ability to advance in this field.
How to Replace bpmn-js
Technology Selection
Since we are going to replace bpmn-js, we need to find a flowchart editing tool that offers better support for customization and higher maintainability. After searching online, we chose LogicFlow for the following reasons:
-
Completely open-source: Developers maintain it frequently, and issues are handled promptly
-
Comprehensive customization mechanism: LogicFlow allows you to customize any SVG effects on the flowchart. This mechanism ensures that we can fully meet the requirements of any product, with the remaining challenge being the workload.
-
Controllable workload: LogicFlow already provides an official plugin for bpmn customization, and the code for this plugin is relatively simple. We can directly copy it to our project and continue with further customization.
-
Clearer source code and frontend-friendliness: Built on the MobX and Preact technology stack, LogicFlow's source code is easy for frontend developers to understand, especially those familiar with React, as it uses an inheritance-based customization mechanism.
Defining Basic Elements in a Flowchart
While BPMN 2.0 categorizes the semantics of process execution into three elements - Events, Gateways, and Activities, in practical projects, we don't necessarily need to adhere strictly to the BPMN specification for defining the content of flowcharts. Instead, we should define the flowchart's content based on our project's specific business requirements. Let's take the example of an educational approval process that I previously worked on: In the flowchart, there is an "Approval" node, which corresponds to an "Activities" element in the BPMN specification, specifically a "UserTask."
In our implementation, we simply define an "ApprovalNode" node, and when exporting data, we convert it to a "UserTask" as required by the backend engine. The advantage of this approach is that both the product's term "Approval" and the backend's term "UserTask" are reflected in our code, ensuring that we are no longer disconnected from the business logic.
Here is the main code for LogicFlow's fully customized node implementation:
import { h, RectNode, RectNodeModel } from '@logicflow/core';
class ApprovalModel extends RectNodeModel {
initNodeData(data) {
super.initNodeData(data);
this.text = {
value: data.text || "",
x: data.x,
y: data.y + 40,
};
this.width = 100;
this.height = 80;
}
}
class ApprovalView extends RectNode {
/**
* Methods for Fully Customizing Node Appearance
*/
getShape() {
const { model, graphModel } = this.props;
const { x, y, width, height, radius } = model;
const style = model.getNodeStyle();
return h("g", {}, [
h("rect", {
...style,
x: x - width / 2,
y: y - height / 2,
rx: radius,
ry: radius,
width,
height
})
]);
}
}
export default {
type: 'approval',
view: ApprovalView,
model: ApprovalModel,
}
For information on customizing LogicFlow, you can refer to these resources:
Get to know the basic nodes of LogicFlow
For fully customizing nodes in bpmn-js, the main code is as follows:
import inherits from 'inherits'
import BaseRenderer from 'diagram-js/lib/draw/BaseRenderer'
import { append as svgAppend, create as svgCreate } from 'tiny-svg'
import { customElements, customConfig } from '../../utils/util'
export default function CustomRenderer(eventBus, styles) {
BaseRenderer.call(this, eventBus, 2000)
var computeStyle = styles.computeStyle
this.drawCustomElements = function(parentNode, element) {
const shape = this.bpmnRenderer.drawShape(parentNode, element)
return shape
}
}
inherits(CustomRenderer, BaseRenderer)
CustomRenderer.$inject = ['eventBus', 'styles']
CustomRenderer.prototype.canRender = function(element) {
return !element.labelTarget;
}
/**
* Methods for Fully Customizing Node Appearance
*/
CustomRenderer.prototype.drawShape = function(p, element) {
const type = element.type// Get the type
// Define the customization for the required types
if (customElements.includes(type)) {
const { url, attr } = customConfig[type]
const customIcon = svgCreate('image', {
...attr,
href: url
})
element['width'] = attr.width
element['height'] = attr.height
svgAppend(parentNode, customIcon)
return customIcon
}
const shape = this.bpmnRenderer.drawShape(parentNode, element)
return this.drawCustomElements(p, element)
}
CustomRenderer.prototype.getShapePath = function(shape) {
console.log(shape)
}
It appears that, similar to LogicFlow, we can fully customize the appearance of nodes using SVG within the "drawShape" function. However, there are still many differences in practice: First, in the "drawShape" function, all elements share common attributes, so we need to identify the element type before applying customization. Unlike LogicFlow, there is no straightforward way to define a separate class for each element, and the model-view architecture of LogicFlow is more aligned with frontend development practices. Additionally, bpmn-js does not support fine-grained definitions for text position, selection status, outline, and other details. In our actual development, we may need to modify the source code or use global CSS to forcefully override these aspects.
For defining bpmn-js elements, you can refer to the following resources: bpmn-js-examples
Customizing Business Properties
As mentioned earlier, if we need to add an attribute to a certain type of node, let's see how LogicFlow handles it. Firstly, LogicFlow reserves a "properties" attribute in the data, allowing us to set any custom properties inside it. LogicFlow separates business data and graph data at the data level, which is more intuitive.
Let's assume we have an attribute called "rollback" to indicate whether a node supports rollback. If a node supports rollback, its color is green; otherwise, it is blue. For the backend service, it doesn't need to care about the color; it only requires the presence of the "rollback" field in the node information.
{
id: "userTask",
type: "rect",
x: 100,
y: 100,
text: { x: 100, y: 100, value: '节点文本' },
properties: { // Any business properties can be placed here.
rollback: true
}
}
When customizing a node in LogicFlow, you can write it like this:
class ApprovalModel extends RectNodeModel {
getNodeStyle() {
const style = super.getNodeStyle()
const { properties } = this
if (properties.rollback) {
style.fill = 'green'
} else {
style.fill = 'blue'
}
return style
}
}
Converting to XML Recognizable by the Workflow Engine
From the code above, we can see that our code focuses more on defining business and UI elements, without delving too much into the BPMN specification. So, how can we convert the data generated by this business code into a format recognizable by the workflow engine?
The most convenient method is to let the backend handle the conversion. For example, if the backend is using Java, they can write a filter to perform the conversion. However, in some cases, the backend might not be willing to take on this task, and in such situations, the frontend can handle the conversion. The process of writing the conversion logic also helps frontend developers gain a better understanding of the BPMN specification.
LogicFlow provides a plugin called "bpmnAdapter" to achieve mutual conversion between LogicFlow data and BPMN data (both in JSON and XML formats). In practical projects, I recommend copying the source code of this LogicFlow plugin into our local codebase to avoid making this part of the logic a black box. Our product might have more specific logic, and as the business evolves, the data format produced might not fully comply with the BPMN specification.
Summary
In workflow projects, starting with bpmn-js for quick results is not a problem. Moreover, many projects may be aimed at providing backend management tools for R&D colleagues, with no specific UI requirements. In some cases, the backend team might handle everything on their own without involving the frontend.
However, as the project evolves, especially when product roles come into play and there are plans to commercialize the application, sticking with bpmn-js as the process designer can create tremendous pressure for frontend development. In such cases, it is advisable for frontend developers to be more proactive and opt for a more maintainable process designer for development.
LogicFlow is indeed a good choice in this regard, with comprehensive documentation and understandable source code. Using LogicFlow will liberate you from the pain of working with bpmn-js. When your code becomes more maintainable and closely aligned with the business requirements, it also becomes easier for other team members to quickly take over the project, avoiding the predicament of being stuck when someone leaves.
Appendix
GitHub repository link for LogicFlow: https://github.com/didi/LogicFlow
discord: https://discord.gg/3Px753mg