往期回顾
- 从0到1搭建一个属于自己的工作流站点——入门篇(bpmn.js、Next.js)
- 对bpmn-js的基本概念以及BPMN基础元素进行解释说明,并演示了如何使用bpmn-js来构建和配置一个简单的工作流程。
- 从0到1搭建一个属于自己的工作流站点——筑基篇(bpmn.js、Next.js)
- 更具体地介绍了什么是流程图,一张规范的流程图里都有哪些元素,配置流程图的编辑器都具备哪些功能,以及如何使用流程编辑器去配置一张流程图。
前言
在本系列的前两篇文章里,我们基本上完成了对工作流的理论知识的学习。
这就像我们考驾照一样,先学习基本的理论,明白交通规则;然后了解小汽车的相关信息,比如掌握刹车、油门、方向盘、离合器等等的作用;最后,我们才能够去尝试实车上路。
如今我们也到了“实车上路”的环节了,在本篇文章中,我们将会真正地去动手写代码,旨在 让我们的项目里成功出现流程设计器这个组件 ,并使用这个组件展示一个简单的流程图。
(本篇文章的重点主要还是放在基于 bpmn-js
实现的组件身上,视角的层次停留在组件层次。对于基于 create-next-app
创建的Next.js项目不做展开讨论,会在下一篇文章里和大家聊一聊 create-next-app
。)
创建第一步,ReadMe来帮助
我想无论我们接触什么新的工具,我们在试图使用它们时,第一步往往都是先阅读此工具的README.md
文件。
使用 bpmn-js
自然也免不了俗,一起来看一下它的README.md
文件吧。
好像非常的抽象,对吧?
我觉得这也是为什么 bpmn-js
的入门难度比较高的原因。这一块有关【使用】的描述内容实在是太意识流了。
我们拿它和 antd 的 README.md
文件对比一下:
很明显,在阅读完 antd 的 README.md
文件后,我们明确的知道,只要在我们的文件里从 'antd'
中 import
一些组件(如:<Button>
、DatePicker
),我们便可以直接使用它们。😊
但是阅读完 bpmn-js
的 README.md
文件,我们并不知道这行
const viewer = new BpmnJS({
container: 'body'
});
中的new BpmnJS()
中的这个BpmnJS
是从哪来的?😵
我该从哪个依赖中导出这玩意儿??
于是我们带着这个疑问又去官网看了一眼:
好嘛,一模一样,只不过这次加了个<script>
标签,告诉你这是在 JavaScript 中使用的。
(👆说了和没说一样,我还能不知道这几行代码要在 JavaScript 中用吗😶?👆)
怎么办呢,我该如何在一个 React 项目里使用
bpmn-js
去帮助我创建一个组件呢?
从这一章节开始直到现在的全部内容,真的都是当初我在公司接手这个需求后,去做技术调研时真实遇到的情况,让人欲哭无泪🤦♂️。
bpmn-js:我知道你很急,但你先别急,你看看官网截图的下面那行小字呗?
walkthrough
,可以翻译为 操作指南
。点进去之后,不禁感叹“ 踏破铁鞋无觅处,得来全不费工夫 ”。
👆在 《操作指南》 的这一部分内容里👆,我们终于看到了类似 antd 的 README.md
文件中令我们熟悉的写法了。
那就让我们直接动手试试看:
创建实例
在写代码之前,还有一件事
我们可以从示例代码中看到:
new Modeler({ container: '#canvas' })
当我们 new
一个 Modeler
实例时,我们将一个类似 dom id
格式的字符串作为值传递给了初始化对象中的 container
属性。这一步操作意味着 我们需要在HTML中提供一个带有id为"canvas"的元素,供 Modeler
渲染器使用。
因此我们一定要确保我们的函数组件return的内容里,一定包含一个id名为"canvas"的元素,否则将会报错。
我们使用的是 React,因此我们期望 Modeler
在以id为"canvas"的元素为渲染平台执行某些初始化操作时(后续的文章会分析这个初始化操作,这里先一笔带过),这个元素(这个真实DOM)一定已经挂载到了页面上。
因此我们使用一个依赖为空的 useEffect
去执行这个副作用,在挂载完毕后,将 Modeler
的实例创建出来,我们可以写出如下代码:
import { useEffect, useState } from "react";
import Modeler from "bpmn-js/lib/Modeler";
const BpmDemo = () => {
const [bpmnModeler, setBpmnModeler] = useState<any | null>(null);
useEffect(() => {
setBpmnModeler(new Modeler({ container: "#canvas" }));
}, []);
return <div id="canvas" style={{ width: 400, height: 300 }}></div>;
};
export default BpmDemo;
我们期望能够创建出一张 400 * 300
的画布,运行后我们看下效果:
为啥创建了两张画布?即我们以id为"canvas"的元素为渲染平台,初始化了2次?
因为我们现在是开发环境,在开发环境下,React 有意重复挂载我们的组件。由此也衍生出一个经典的问题:
如何处理在开发环境中 Effect 执行两次?
有关 useEffect
的小插曲
这里我将介绍2种方式去处理这种情况,一种是不优雅的,直接粗暴的,另一种是官方文档建议的优雅实现。
使用 useRef
我们可以使用 useRef
帮助我们去判断 Modeler
实例是否被初始化过了,如果没有,则初始化,反之,则什么也不做。
import { useEffect, useState, useRef } from "react";
import Modeler from "bpmn-js/lib/Modeler";
const BpmDemo = () => {
const [bpmnModeler, setBpmnModeler] = useState<any | null>(null);
const renderFlag = useRef<boolean>(false);
useEffect(() => {
if (!renderFlag.current) {
renderFlag.current = true;
setBpmnModeler(new Modeler({ container: "#canvas" }));
}
}, []);
return <div id="canvas" style={{ width: 400, height: 300 }}></div>;
};
export default BpmDemo;
从上述的代码中我们可以看到,在 Effect
第一次执行的时候,renderFlag.current
是false
,因此我们进行 Modeler
的实例化,并将 renderFlag.current
置为 true
。
之后在 Effect
第二次执行时,由于不满足条件语句中的 !renderFlag.current
,因此便不会进入内部逻辑,从而避免了 Modeler
的重复实例化。
改版后运行效果如下,我们成功地使得页面上只出现了一张画布:
及时在卸载阶段做相应处理
第一种使用 useRef
的方式固然能够帮助我们解决问题,但这并不是解决问题的最佳实践。
我们访问 React 官方文档,看看它建议我们的处理方式:
因此我们应该在清理函数中,及时清空画布、并释放 new Modeler
创建的实例。我们应该使用这种方案去解决问题:
import { useEffect, useState } from "react";
import Modeler from "bpmn-js/lib/Modeler";
const BpmDemo = () => {
const [bpmnModeler, setBpmnModeler] = useState<any | null>(null);
useEffect(() => {
setBpmnModeler(new Modeler({ container: "#canvas" }));
return () => {
// 步骤1: 清理画布
const canvasContainer = document.querySelector("#canvas");
if (canvasContainer) {
canvasContainer.innerHTML = ""; // 清空画布内容
}
// 步骤2: 释放实例
setBpmnModeler(null);
};
}, []);
return <div id="canvas" style={{ width: 400, height: 300 }}></div>;
};
export default BpmDemo;
改版后,再次运行代码,我们可以看到如下结果:
我们同样成功地使得页面上只出现了一张画布,并且遵循了 React 官方文档的建议,采用了更优雅的方式。
不管怎么说,我们现在成功地创建出了一张可以绘制流程图的画布,虽然它现在是空白的,并且看起来和官网Demo也不太相似。
相信我,会变得相似的。
怎么样都好,我想先看到张流程图
一张空白的画布真的很难有什么说服力,至少出现一点前两篇文章提到过的BPMN图元吧?像是开始节点、结束节点、用户任务之类的。
我们回顾一下 《操作指南》 中的实例代码,并结合 README.md
文件一起看:
const bpmnXML = '...'; // my BPMN 2.0 xml
// import diagram
try {
await modeler.importXML(bpmnXML);
// ...
} catch (err) {
// err...
}
从上面的代码中我们可以看到, Modeler
提供了一个实例方法 importXML
,让我们可以导入一份符合 BPMN 2.0 规范的xml,从而使我们的画布上能够呈现出流程图。
那么从哪里可以获得一份符合 BPMN 2.0 规范的xml文件呢?
我们可以用官网的在线demo导出一份xml数据,然后再在我们自己的项目里使用它。
导出后的结果如下:diagram.bpmn
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_1q2zuyn" targetNamespace="http://bpmn.io/schema/bpmn" exporter="bpmn-js (https://demo.bpmn.io)" exporterVersion="17.7.1">
<bpmn:process id="Process_0pw7aj3" isExecutable="false">
<bpmn:startEvent id="StartEvent_1nls6bn">
<bpmn:outgoing>Flow_0y3ajkq</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:task id="Activity_1o6n4zx" name="海石">
<bpmn:incoming>Flow_0y3ajkq</bpmn:incoming>
<bpmn:outgoing>Flow_1gbaigo</bpmn:outgoing>
</bpmn:task>
<bpmn:sequenceFlow id="Flow_0y3ajkq" sourceRef="StartEvent_1nls6bn" targetRef="Activity_1o6n4zx" />
<bpmn:endEvent id="Event_0jxha22">
<bpmn:incoming>Flow_1gbaigo</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_1gbaigo" sourceRef="Activity_1o6n4zx" targetRef="Event_0jxha22" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_0pw7aj3">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1nls6bn">
<dc:Bounds x="152" y="102" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1o6n4zx_di" bpmnElement="Activity_1o6n4zx">
<dc:Bounds x="240" y="80" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0jxha22_di" bpmnElement="Event_0jxha22">
<dc:Bounds x="392" y="102" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_0y3ajkq_di" bpmnElement="Flow_0y3ajkq">
<di:waypoint x="188" y="120" />
<di:waypoint x="240" y="120" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1gbaigo_di" bpmnElement="Flow_1gbaigo">
<di:waypoint x="340" y="120" />
<di:waypoint x="392" y="120" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>
请注意,直接使用文件路径导入是不行的,我们查看
bpmn-js
的源码,可以找到importXML
方法的相关文件:
👇在这个方法的内部处理逻辑中,它会使用 fromXML
去解析我们传入的 xml
字符串👇。
⚠因此我们要记得对刚刚导出的文件做字符串化的处理。⚠
我们修改一下代码,并将画布大小调整为 800*600
,看看效果:
import { useEffect, useState } from "react";
import Modeler from "bpmn-js/lib/Modeler";
const bpmnXMLString = `<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_17n1c2u" targetNamespace="http://bpmn.io/schema/bpmn" exporter="bpmn-js (https://demo.bpmn.io)" exporterVersion="17.7.1">
<bpmn:process id="Process_1q0g8bt" isExecutable="false">
<bpmn:startEvent id="StartEvent_0hcpxtn">
<bpmn:outgoing>Flow_0c7xmc2</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:task id="Activity_1xe73bk" name="海石">
<bpmn:incoming>Flow_0c7xmc2</bpmn:incoming>
<bpmn:outgoing>Flow_0ox8xns</bpmn:outgoing>
</bpmn:task>
<bpmn:sequenceFlow id="Flow_0c7xmc2" sourceRef="StartEvent_0hcpxtn" targetRef="Activity_1xe73bk" />
<bpmn:endEvent id="Event_0vau36o">
<bpmn:incoming>Flow_0ox8xns</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_0ox8xns" sourceRef="Activity_1xe73bk" targetRef="Event_0vau36o" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1q0g8bt">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_0hcpxtn">
<dc:Bounds x="152" y="102" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1xe73bk_di" bpmnElement="Activity_1xe73bk">
<dc:Bounds x="240" y="80" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0vau36o_di" bpmnElement="Event_0vau36o">
<dc:Bounds x="392" y="102" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_0c7xmc2_di" bpmnElement="Flow_0c7xmc2">
<di:waypoint x="188" y="120" />
<di:waypoint x="240" y="120" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0ox8xns_di" bpmnElement="Flow_0ox8xns">
<di:waypoint x="340" y="120" />
<di:waypoint x="392" y="120" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>`;
const BpmDemo = () => {
const [bpmnModeler, setBpmnModeler] = useState<any | null>(null);
useEffect(() => {
setBpmnModeler(new Modeler({ container: "#canvas" }));
return () => {
// 步骤1: 清理画布
const canvasContainer = document.querySelector("#canvas");
if (canvasContainer) {
canvasContainer.innerHTML = ""; // 清空画布内容
} // 步骤2: 释放实例
setBpmnModeler(null);
};
}, []);
useEffect(() => {
if (bpmnModeler) {
(async function () {
await bpmnModeler.importXML(bpmnXMLString);
})();
}
}, [bpmnModeler]);
return <div id="canvas" style={{ width: 800, height: 600 }}></div>;
};
export default BpmDemo;
我们成功地将一张流程图展示在了我们的画布上,这也意味着我们成功迈出了从0到1的第一步!
结语
在本篇文章中,我们亲自感受了一番接取需求任务,调研技术栈的过程。
万事开头难,在经历了多次检索之后,我们找到了如何以npm安装依赖的方式去调用 bpmn-js
创建一个 Modeler
实例的使用方式。
顺便还回顾了 React 在开发环境时会重复挂载组件的这一问题,探讨了优雅实现。
最后,我们通过调用 Modeler
实例方法:importXML
,成功地在我们的流程设计器组件上呈现了一张流程图!
但是我们发现,目前的流程设计器对比官方的在线Demo,还缺少了很多东西,比如左侧工具栏、属性面板、导出/保存 按钮等等。
在下一篇文章中,我们会继续动手实践,将这些缺少的功能一个接着一个地实现。
期待与你在下一篇文章相遇!
示例完整代码
我将代码push到了我的 git仓库里,大家可以在这里找到。