纯JS自研审批流:从节点流转到会签驳回的完整实践

发布时间:2026/10/27 15:34:20

纯JS自研审批流:从节点流转到会签驳回的完整实践 简介工作流与审批流是Web系统中常见的业务协作模块面向需要在前端实现流程设计、任务流转与审批操作的中级开发者提供了一套可运行的JavaScript实现。压缩包共49个文件仅69KB含8个js脚本、8个html页面、6个xml流程定义、2个aspx后端示例以及css样式、gif演示、png图标等辅助文件其中xml用于描述流程模型js处理前端逻辑html则展示审批页面覆盖从流程建模、任务分配到界面交互的完整链路。已有1282人学习浏览。借助其中JS框架与示例页面可掌握基于BPMN与状态机设计流程模型的方法理解任务触发、条件分支、回滚与异常处理等关键机制同时flow.aspx和数据库db文件展示了前后端协作与状态持久化方式配合gif动图能直观看到审批操作演示。整套示例结构清晰适合二次开发能快速搭建轻量级审批流系统。 最近手头接了一个前后端分离的项目里面有个模块是请假审批流后端只把流程引擎的接口给到前端具体怎么做节点流转、怎么渲染流程图、怎么处理驳回退回全交给了前端 JS 这边。被后端同事一句工作流你们前端搞吧摆了一道之后我花了两天时间把整个流程梳理清楚顺手把一套轻量级的 JS 审批流方案沉淀了下来。这篇文章就用实际案例讲清楚不依赖重型 BPM 引擎纯靠前端 JS 也能把一个审批流做得稳定好用。先说说这套东西能解决什么问题像请假、报销、合同审批、采购申请这类业务通常需要多层级的审批链路欧美金发展规划、节点驳回、会签或签、流程进度追踪这些需求如果从头开发一套完整的工作流引擎成本极高但如果直接接 Flowable、Camunda 这类 Java 系引擎前后端语言栈不统一前端还得自建一整套流程定义协议。这里我从实际工程的角度用纯 JS 实现了一套适配常见业务场景的审批流方案覆盖了流程设计、节点流转、状态控制、历史轨迹几个核心模块适合中小团队的前端同学直接参考。很多前端第一反应是审批流不就应该上 Flowable 吗说实话在我参与过的项目里真正需要 Flowable 这类重型引擎的场景其实不多大部分业务停留在串行审批、条件分支、驳回和撤回这几种模式上纯 JS 完全能扛住。我先把选型思路、数据模型设计、代码实现、踩坑经验四个部分展开来讲。1. 工作流选型先分清你需要的到底是不是一套 BPM1.1 主流工作流引擎优劣势对比在讨论 JS 方案之前先厘清市面常见工作流的定位。Flowable、Camunda 这类源于 Java 生态的引擎归属于 BPMN业务流程建模与符号标准的落地实现功能完整但接入成本高部署重通常要配套数据库表和 REST 服务。n8n、Dify、Coze 这类偏向自动化的工作流工具适合做 AI 编排、接口串联、数据加工跟企业内部的审批流并不是同一个应用场景。我当时做的内部审批小系统如果把 Flowable 引进来光是学习 BPMN 各种事件、网关、子流程的概念就得花掉一整周而且流程定义文件.bpmn是 XML 格式前端要解析成可视化图形还得另写一套渲染器性价比太低。方案适用场景前端接入成本审批链路能力推荐指数Flowable/Camunda复杂 BPM、强流程引擎需求高需同时维护 BPMN 和 REST API强但复杂度也高中n8n/Dify/Coze自动化编排、AI 工作流中需定义 Trigger 和 Node弱不擅长审批状态管理低纯 JS 自研审批流中小型业务、内部系统、前后端分离低直接操作 JSON 数据结构够用完全可控高1.2 纯 JS 自研的边界在哪里我推荐的这套纯 JS 方案不是要替代工作流引擎而是在特定场景下给出更经济的解法。它的边界是支持串行审批、条件判断、会签、或签、驳回、撤回、转办、抄送这些覆盖了九成以上企业内部审批需求。但要注意当你需要复杂的并行分支、多实例子流程、定时事件、消息补偿、版本热部署时自研的成本会指数级上升这时候就该老实考虑接引擎了。我用了一个很简单的判断标准流程定义是否会在运营过程中频繁调整。如果一个月改一次流程结构纯 JS 方案依然能顶住如果一周改三次还是上正规引擎吧。1.3 怎么定自定义流程结构自研审批流的命门是数据模型我的做法是流程 节点Node 连线Edge。这个理念本身来源于 BPMN但实现上我做了大幅精简。const workflow { workflowId: leave_workflow, name: 请假审批, nodes: [ { id: start, type: start, name: 发起 }, { id: leader_approve, type: approval, name: 主管审批, assignee: ${owner.leader} }, { id: hr_approve, type: approval, name: HR审批, assignee: hr }, { id: end, type: end, name: 结束 } ], edges: [ { id: e1, source: start, target: leader_approve, condition: null }, { id: e2, source: leader_approve, target: hr_approve, condition: null }, { id: e3, source: hr_approve, target: end, condition: null } ] };这套结构的核心是把流程画布抽象成 JSON前端能直接遍历渲染不用解析 XML也无需额外维护一份独立的流程模板文件。每条连线的condition字段就是条件分支的开关比如请假天数大于等于3天走 CTO 审批小于3天直接 HR 审批最终都落在 condition 上做动态判断。2. 审批流的数据模型与状态机设计2.1 实例数据与任务数据拆分流程定义是模板真正每次发起请假生成的是流程实例Process Instance。这两个概念如果不从设计上区分开后面很容易写出野代码。我是这样组织的流程实例ProcessInstance记录某一次申请包含流程定义 ID、当前所在节点、发起人、业务表单数据、状态。审批任务Task记录当前待办的审批项包含归属节点、审批人、审批状态、审批意见。const processInstance { processInstanceId: pi_20250110_001, workflowId: leave_workflow, currentNodes: [leader_approve], status: running, // running / approved / rejected / canceled businessData: { applicant: 张三, days: 2, reason: 回老家 }, createTime: 2025-01-10 09:00:00 }; const task { taskId: task_001, processInstanceId: pi_20250110_001, nodeId: leader_approve, approver: 李四, status: pending, // pending / approved / rejected / transferred comment: , handledTime: null };正因为把流程实例和任务拆开后面做审批流水记录时只需要把每次任务的变更轨迹推入一个historyList数组前端不用额外请求接口就能实时渲染时间线。2.2 状态机审批动作如何驱动节点跳转审批流的本质是状态机。发起人发起申请后系统进入审批中状态每个节点审批结束后要么推进到下一节点要么驳回最后一节点通过后流程进入已完成状态。我用一个纯函数来驱动状态变化避免到处改动状态function transition(instance, action, payload) { const currentNode instance.nodes.find(n n.id instance.currentNodeId); switch (action) { case AGREE: return moveToNext(instance, currentNode, payload); case REJECT: return backToPrevious(instance, currentNode, payload); case TERMINATE: return { ...instance, status: terminated }; default: return instance; } }moveToNext内部会先检查是否有条件分支有则逐条判断 condition 对应的表达式找出命中路径没有则找当前节点默认连线的 target 节点。这样设计的好处是所有状态变更都收敛到一个函数里方便在每次跳转前后埋点、记录日志、清理不用的临时数据。2.3 节点类型审批节点、条件节点、抄送节点、开始结束节点我在实际开发中只用了四类节点几乎能覆盖所有需求开始节点没有审批人创建流程实例后自动跳过。审批节点核心节点包含审批人、审批策略。条件节点不产生审批动作只做路由判断根据业务字段把流程引向不同分支。抄送节点把审批结果通知给指定人不阻断流程处理完成后直接进入下一节点。审批策略我单独拿出来说因为它最容易踩坑。审批节点上的approveMode字段用来区分或签和会签或签是组内任意一人审批即可通过会签则需要组内所有人审批通过。我曾经把会签逻辑写成一个人通过就直接流转结果被业务方追着打。正确的做法是会签节点需要维护一个approvedCount和totalCount当approvedCount totalCount时才触发节点推进。3. 前端渲染流程图与审批操作区的实现3.1 用 JSON 数据直接渲染审批链路图拿到了流程定义和实例数据前端要做的第一件事是把审批链路可视化。这里没有用第三方图编辑器而是直接用 flex 布局加 SVG 线条把节点纵向排列节点之间用带箭头的 SVG line 连接。这样写出来的流程图虽然不像 BPMN 那样能拖拽编辑但作为审批进度展示已经足够清晰。div classflow-container div classflow-node>const historyList [ { nodeName: 主管审批, operator: 李四, action: agree, comment: 同意, time: 2025-01-10 10:00:00 }, { nodeName: HR审批, operator: , action: , comment: , time: } ];渲染时间线的时候注意不要用第三方时间线组件库自己写一个v-for循环加几条 CSS 样式就够了体积小、样式可控不会被 UI 库版本升级牵连。4. 核心代码实操JS 审批流的关键实现4.1 寻找下一节点条件分支与默认路径节点流转是整个审批流最核心的逻辑。我先定义了一个findNextNode函数根据当前节点和业务数据返回下一个应进入的节点 IDfunction findNextNode(workflow, currentNode, businessData) { const outgoingEdges workflow.edges.filter(e e.source currentNode.id); if (outgoingEdges.length 0) return null; // 优先级高的条件连线先匹配 const sortedEdges outgoingEdges.sort((a, b) a.priority - b.priority); for (const edge of sortedEdges) { if (!edge.condition) return edge.target; if (evalCondition(edge.condition, businessData)) { return edge.target; } } return null; }对于条件表达式我没有引入复杂规则引擎而是用一个简单的表达式解析函数支持、、、、||和括号对大多数业务字段判断来说完全足够。需要注意条件表达式里不要写复杂函数调用纯 JSON 化配置才是持久化友好的。4.2 会签与或签审批模式的分流处理会签和或签的实际处理逻辑必须封装成一个独立模块。function approveTask(instance, task, currentUser, comment) { if (task.approver ! currentUser.id) { return { success: false, message: 您不是当前审批人 }; } if (task.approveMode or) { return passNode(instance, task, comment); } if (task.approveMode and) { task.approvedCount (task.approvedCount || 0) 1; if (task.approvedCount task.totalCount) { return passNode(instance, task, comment); } } return { success: true, needMoreApproval: true }; }这里有几个边角场景会签过程中如果某个人点了驳回整个流程直接驳回不需要等其他人或签时如果某个人先驳回、另一个人后同意最终结果以先触达终态为准。这些规则建议写进接口文档避免前端后端理解不一致。4.3 驳回与撤回两个最容易出错的动作驳回有两种驳回到上一节点REJECT_TO_PREV和驳回到发起人REJECT_TO_START。我在实际项目里统一用REJECT_TO_NODE由前端指定目标节点 ID后端不做默认猜测这样最灵活也最不容易出 bug。function rejectToNode(instance, targetNodeId, comment) { if (!isValidTargetNode(instance, targetNodeId)) { return { success: false, message: 目标节点不合法 }; } instance.currentNodes [targetNodeId]; instance.status rejected; return { success: true, message: 已驳回至指定节点 }; }撤回则是发起人专用的动作前提是流程还未被任何审批人处理过也就是historyList.length 0。撤回后流程实例被标记为canceled所有相关任务一并作废。4.4 节点执行状态与前端交互状态的双向同步审批流的最终用户是业务人员前端交互状态的可视化相当关键。我的做法是维护一个viewState对象把流程实例数据、任务数据映射成前端视图状态const viewState { currentNodeId: instance.currentNodes[0], canApprove: checkPermission(instance, currentUser), canReject: checkPermission(instance, currentUser), canWithdraw: instance.status running historyList.length 0 currentUser.id instance.initiatorId, progressPercent: calcProgress(instance) };这样页面上的按钮灰置逻辑完全由viewState控制逻辑统一且易于测试。每当审批动作触发后调用一次refreshViewState()重新计算 viewState页面即刻更新。5. 常见问题与排查技巧实录5.1 流程卡在某个审批节点不走这个现象我在联调时遇到过无数次。排查思路固定为三步检查findNextNode返回是否为空为空说明连线配置有误或者条件分支没有任何一条命中默认路径。检查目标节点状态是否为待处理审批节点可能需要初始化对应的 task。检查接口返回值如果后端没有正确更新实例的currentNodes前端渲染自然停留在旧节点。一个隐藏问题是条件分支的priority没有配置导致排序不稳定。我后来在代码里专门对priority做了默认值 999 避免两条条件分支同时命中。5.2 审批人表达式取不到值在节点配置里写${owner.leader}结果实际运行时发现取不到 leader 的值。大多数原因是业务表单数据里没有owner对象或者owner.leader字段为空。建议在findNextNode执行之前由后端统一注入一份contextData把发起人、上级领导、部门等数据都塞进去前端直接用而不是自己从业务表单里猜。5.3 大数据量下时间线渲染卡顿一年以上的审批历史可能有几百条记录全部渲染成 DOM 会导致首页加载变慢。我的建议是后端接口做分页或者前端做虚拟滚动只渲染可视区域内的历史节点。如果项目是用 Vue/React 构建的这条经验尤其重要大量不稳定的 DOM 节点聚焦和展开操作会消耗大量资源纯粹用 v-for 渲染几百个节点性能会迅速恶化。5.4 状态机不同步页面刷新后审批按钮依然可用当流程实例在后端已经被其他用户处理完前端页面上依然显示可用按钮此时如果不做处理审批提交直接报错用户体验极差。我采用前端缓存 后端校验双重方案在后端处理审批动作时校验当前任务状态必须是 pending同时前端的viewState.canApprove做展示用提交失败后弹出该任务已被他人处理的提示并刷新流程数据。6. 这套方案还能往哪些方向扩展目前这套 JS 审批流已经能覆盖多数业务场景但如果团队业务复杂度继续上升可以往下几个方向扩展引入工作流可视化编辑器在节点和连线 JSON 结构的基础上套一个基于 mxGraph 或 LogicFlow 的拖拽画布实现业务流程的可视化配置让运营人员也能自己调整审批链路。支持定时节点与超时提醒在节点上加一个timeout字段配合定时任务扫描超时自动推送提醒或自动通过。流程版本管理流程定义如果发生了变更不要直接修改原记录而是生成新版本让正在运行的实例继续按旧版本流转新发起的流程用新版本避免流程漂移。我在实际项目中已经尝试了第一点改造后运营同事非常满意不再每次调整流程都提工单找开发。后续有机会再把超时提醒的完整实现写一篇。这套方案的源码结构和核心代码并不复杂真正花时间的其实是在想清楚边界这一步什么场景用自研 JS 审批流什么场景必须上重型引擎哪些逻辑前端管、哪些逻辑后端管。把这些想明白实施方案自然水到渠成。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/10/23 16:43:16

连通域分析实战:从Zip代码包到OpenCV图像处理应用

简介:一份基于FPGA的二值图像连通域标记算法实现工程,面向从事实时图像处理、硬件加速的FPGA开发者。该算法仅需缓存若干行图像数据,在固定行延时内输出结果,无需外部SRAM或DDR,适合高帧率视频流场景。压缩包共592个文…

2026/10/27 15:33:14

自演进模型路由:Agent应用降本50%的工程实践

做Agent应用做到第4个月的时候,我对着账单愣了很久。同一个任务,在不同模型上来回切换,有时候效果不错,有时候钱哗哗烧掉。团队里都在聊怎么优化Prompt、怎么改工具调用逻辑,但谁都没注意到一个更底层的问题&#xff1…

2026/10/27 15:33:14

YOLOv26目标检测架构解析与工程实践

1. 项目概述在计算机视觉领域,目标检测算法YOLO系列一直保持着快速迭代的节奏。最近提出的YOLOv26架构通过两项关键技术革新显著提升了检测性能:并行多核卷积结构和异构感受野协同机制。这种设计在保持YOLO系列实时性的同时,大幅提升了复杂场…

2026/10/27 15:33:14

德卡T10读卡源码实战:串口通信、读卡流程与避坑指南

简介:德卡T10身份证读卡器是专门读取身份证、社保卡、就诊卡等智能卡信息的常用设备,这份压缩包提供完整的C#调用DLL动态链接库实现读卡的源码,适合需要快速集成读卡功能的桌面应用开发者,也适合想深入了解智能卡通讯协议&#xf…

2026/10/27 15:33:14

AWQ技术:大语言模型高效量化实践指南

1. AWQ技术概述:重新定义大语言模型量化在本地部署大语言模型(LLM)成为趋势的当下,模型体积和推理速度始终是开发者面临的两大挑战。传统量化方法如GPTQ虽然能压缩模型大小,但往往会牺牲过多精度。MIT等机构提出的激活…

2026/10/27 15:33:14

电商评论情感分析系统:从深度学习模型到工程落地

先说一个我观察到的现象:每年毕设季,选“基于深度学习的电商评论情感分析系统”这个题目的同学特别多,但真正能把这套东西讲明白、演示顺畅的不到三成。多数人的路径是找一个开源仓库,换成自己的数据,跑出一个九成多的…

2026/10/27 15:28:08

C++ Excel处理库选型与实战:Spire.XLS集成、报表生成与性能优化

1. 先把选型想明白:C 里为什么值得引入一个 Excel 处理库前阵子帮一个做设备数据采集的团队看代码,一个 1000 多行的文件里全是拼 xlsx 的逻辑:压缩包怎么组织、sharedStrings.xml 怎么去重、列号 27 怎么换成 AA、日期为什么显示成 45000 这…

2026/10/27 14:24:03

全国计算机二级C语言真题PDF高效刷题指南

简介:这份PDF是全国计算机二级C语言考试的试题与答案解析,专为备考二级C语言的考生准备。内容涵盖C语言特点与设计理念、程序基本组成单位、main函数执行流程、函数嵌套调用规则、标识符命名规范、整型实型字符型数据类型、八进制十六进制数合法形式、运…

2026/10/27 9:34:54

requests+BeautifulSoup:爬虫抓取h1标题的入门实战指南

简介:一份以实战为导向的Python爬虫入门资源,面向具备基本编程基础、希望快速掌握网络数据抓取的初学者,可解决从零编写简单爬虫的常见困惑。实例以requests库发送HTTP请求、BeautifulSoup解析HTML文档,完整演示从网页中提取 标题…

2026/10/27 0:59:21

递推最小二乘参数辨识MATLAB编程:二阶系统与渐消记忆实现

简介:这是一篇面向控制系统、信号处理与机器学习等领域学习者的最小二乘参数辨识程序说明文档。资源共包含一个docx格式文档,压缩包大小七十八KB,内容围绕递推最小二乘算法和渐消记忆最小二乘算法展开:先构造一个极点位于单位圆内…

2026/10/27 0:02:28

WorkBuddy_新建任务与工作模式19_“模式选错”的典型代价

“模式选错”的典型代价 摘要 在软件工程中,设计模式、架构模式、并发模式乃至团队协作模式的选择,往往在项目早期就决定了后续的开发效率和维护成本。本文通过多个真实场景,剖析“模式选错”带来的典型代价——包括返工成本、技术债务累积、…

2026/10/27 0:02:28

WorkBuddy_新建任务与工作模式20_新手推荐路径

新手推荐路径 摘要 在 AI 辅助开发工具快速普及的今天,新手面对最多的困惑往往不是"工具够不够强",而是"我该从哪一步开始用"。本文提出一条经过实践验证的三阶段学习路径:先从 Ask(问答式交互)起…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑