3、流程设计器:流程设计器架构、拖拽式设计、节点属性配置
好,咱们今天聊聊流程设计器。这东西说白了,就是工作流引擎的「脸面」。用户能不能用好你的引擎,一半看设计器好不好用。我见过太多项目,引擎内核牛得不行,结果设计器一塌糊涂,最后项目还是黄了。
我个人习惯把设计器分成三层来看:展示层、交互层、数据层。这样分层,后期维护起来心里有数。
3.1 流程设计器架构
先说说整体架构。你想想看,一个流程设计器要干哪些事?
- 画流程图(节点+连线)
- 拖拽操作
- 配置节点属性
- 保存/加载流程定义
- 校验流程合法性
我建议采用MVVM模式来做。视图层只管渲染,ViewModel层处理交互逻辑,Model层管理数据。这样做的好处是,哪天你想换UI库,比如从jQuery换成Vue,只需要动视图层,业务逻辑不用重写。
我在项目中遇到过一种情况:团队直接用原生Canvas画流程图,结果后来要支持移动端适配,改得想哭。所以我现在更推荐基于SVG的方案,或者直接用成熟的图形库,比如mxGraph、JointJS。别自己造轮子,除非你团队有图形学大牛。
架构上,核心模块就这几个:
| 模块 | 职责 | 技术选型参考 |
|---|---|---|
| 画布引擎 | 管理节点和连线的渲染、缩放、平移 | SVG / Canvas / WebGL |
| 拖拽管理器 | 处理拖拽事件、碰撞检测、吸附对齐 | HTML5 Drag & Drop / 自定义事件 |
| 属性面板 | 展示和编辑节点/连线的配置 | 表单引擎 / JSON Schema |
| 序列化器 | 将图形数据转为JSON/XML,或反向解析 | 自定义序列化 / BPMN 2.0 |
| 校验器 | 检查流程是否合法(如:是否有孤立节点) | 图算法(DFS/BFS) |
3.2 拖拽式设计
拖拽是设计器的灵魂。用户从左侧工具栏拖一个「审批节点」到画布上,这个动作背后发生了什么?
嗯,这里要注意几个关键点:
- 拖拽源:左侧的节点模板,要设置draggable="true",并绑定dragstart事件,把节点类型信息放到dataTransfer里。
- 拖拽目标:画布区域监听dragover(阻止默认行为)和drop事件。drop时读取dataTransfer里的类型,创建节点实例。
- 位置计算:drop时获取鼠标相对于画布的坐标,这个坐标就是节点的初始位置。
我曾经踩过一个坑:直接用了鼠标事件的clientX/clientY,结果画布有滚动条或者缩放时,节点位置全偏了。后来改成用getBoundingClientRect结合画布的transform矩阵来计算,才搞定。
代码示例(简化版):
// 画布drop事件
canvas.addEventListener('drop', function(e) {
e.preventDefault();
const nodeType = e.dataTransfer.getData('nodeType');
if (!nodeType) return;
// 获取画布相对坐标
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.y;
// 创建节点
const newNode = {
id: generateId(),
type: nodeType,
x: x,
y: y,
label: '新节点',
properties: {}
};
// 添加到数据模型
flowData.nodes.push(newNode);
renderFlow();
});
除了基本的拖拽,还有几个体验细节:
- 吸附对齐:拖拽节点时,当它靠近其他节点或网格线时,自动「吸」过去。这个用取模运算就能实现。
- 连线拖拽:从节点的连接点拖出一条线,到目标节点松开。这里要判断目标点是否有效,无效就取消连线。
- 多选拖拽:按住Ctrl键可以多选节点,然后整体拖拽。这个在调整布局时非常有用。
3.3 节点属性配置
节点画出来了,但每个节点都有自己的「脾气」。审批节点要配置审批人、超时时间;条件节点要配置表达式;抄送节点要配置抄送人列表。这些属性怎么配?
我建议用属性面板 + 表单引擎的方案。点击某个节点,右侧弹出属性面板,面板里的表单根据节点类型动态生成。
具体做法:
- 为每种节点类型定义一份属性Schema,描述这个节点有哪些属性、每个属性的类型、校验规则等。
- 点击节点时,根据节点类型加载对应的Schema,渲染成表单。
- 用户修改属性后,实时更新到数据模型中的节点properties字段。
举个例子,审批节点的Schema可能是这样的:
{
"type": "approval",
"label": "审批节点",
"fields": [
{
"key": "assignee",
"label": "审批人",
"type": "userPicker", // 用户选择器
"required": true
},
{
"key": "timeout",
"label": "超时时间(小时)",
"type": "number",
"default": 24,
"min": 1,
"max": 720
},
{
"key": "allowReject",
"label": "允许驳回",
"type": "boolean",
"default": true
}
]
}
这样做的好处是:
- 扩展性强:新增节点类型只需要加一份Schema,不用改代码。
- 前后端统一:后端也可以用同一份Schema来做校验。
- 支持复杂属性:比如「审批人」可以是一个用户选择器,也可以是一个表达式(如:部门经理)。
我记得有个项目,客户要求「审批节点」的审批人可以是动态的,比如「发起人的直属上级」。这个在属性配置里就要支持表达式编辑。我当时的做法是加一个「表达式模式」开关,打开后输入框变成代码编辑器,支持Groovy或JavaScript表达式。
最后说一句,属性面板的交互也很重要。我习惯把属性分组:
- 基础属性:节点名称、描述
- 流程属性:审批人、超时时间、跳转条件
- 高级属性:监听器、脚本、自定义扩展
分组后用户找配置项更方便,不会一上来就被一堆字段吓到。
好,关于流程设计器的架构、拖拽设计和属性配置,今天就聊到这儿。下一节咱们讲讲流程引擎的核心——流程实例的创建与执行,到时候会涉及状态机、令牌推进这些硬核内容。