发布时间:2026/9/7 7:09:00
纯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/9/7 7:09:00

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

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

2026/9/7 12:14:36

电商商品三级类目体系设计:从表结构到落地实践

简介:面向移动端开发学习者的电商商品三级类目示例项目,是一套完整的Xcode工程实现,演示了在电商应用中将商品按大类、中类、小类组织,并通过列表逐级展示和联动筛选。资源以zip压缩包形式提供,共23个文件,…

2026/9/7 12:14:36

TRANSCAD交通分配实操详解:从Wardrop原理到V/C比判读

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/7 12:14:36

EhLib 10.0.031 For Delphi 11 安装指南与核心功能实战解析

简介:面向使用 Delphi 11 开发环境的 EhLib 组件库完整安装包,专注于扩展图形图像处理、数据库连接、报表设计、图表分析和界面美化等能力,适合需要快速构建数据管理应用的中高级开发者。压缩包共计八百九十个文件,大小约二十四点…

2026/9/7 12:14:36

原地哈希算法详解:O(1)空间复杂度解决数组统计问题

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/7 12:09:35

加扰与解扰原理及工程实践:从LFSR到同步恢复的完整指南

简介:加扰与解扰是数字通信中改善信号质量与数据安全的关键环节。这一资源面向通信工程与FPGA开发学习者或相关工程师,聚焦基于VHDL的加解扰算法设计、Modelsim功能仿真及硬件板卡验证。压缩包共132个文件,约1.37MB,以vhd源码文件…

2026/9/7 0:47:43

超人会飞不算本事:系统稳定依赖清晰规则与边界设计

开头先不绕弯子。“#斯坦李吐槽dc 所以超人是无缘无故会飞的嘛哈哈哈哈哈哈哈锤哥真是技术人才啊!#雷神 #复联”这类调侃式短标题,第一波冲击力在于它把两个宇宙的角色塞进同一个吐槽箱里,但细想一下就能发现,它真正碰到的根本不是…

2026/9/7 0:14:19

超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论

把“蜘蛛侠 vs 超人”放在 CSDN 上聊,可能很多人第一反应是走错片场了。但如果把这两个角色看成“两个持续运营了 80 多年的文化产品”,你会发现,这场比较本质上是两个不同 IP 策略的长期结果对比:超人赢在定义了整个超级英雄题材…

2026/9/7 0:14:17

基于CNN的调制信号识别:MATLAB实现时频图分类实战

简介:本资源是一套面向通信工程与信号处理方向学习者、研究者的深度学习实践方案,聚焦调制信号自动检测与识别这一典型无线通信任务,解决传统方法依赖人工特征、低信噪比下性能下降等痛点。压缩包共12个文件(10.73MB)&…

2026/9/7 0:03:36

基于YOLOv8和PyQt5的麦穗稻穗检测识别系统设计与实现

这次我们来看一个把目标检测算法和桌面端工具结合得很典型的项目:基于 YOLOv8 PyQt5 的麦穗稻穗检测识别系统。这个项目本身不是新概念,但它的价值在于落地形态很完整。YOLOv8 负责核心的麦穗稻穗目标检测,PyQt5 负责提供可视化的桌面交互界…

2026/9/7 0:03:36

UL 1642锂电池安全标准全解析:测试项目、认证流程与避坑指南

简介:UL 1642是锂电池安全领域的重要规范,本中文版资源适合锂电池制造商、检测机构工程师及产品认证相关人员阅读,用于理解电池在设计与制造层面的安全要求、测试方法与合规要点。资源共1个PDF文件,压缩包大小834KB,便…

2026/9/7 0:03:36

BS EN 13814-1-2019游乐设施安全标准:设计与制造核心要点解析

简介:BS EN 13814-1:2019是英国采纳欧洲标准EN 13814-1:2019的正式版本,由BSI标准出版,重点规定游乐设施和游乐设备在设计与制造环节的安全准则,与BS EN 13814-2:2019、BS EN 13814-3:2019共同取代旧版BS EN 13814:2004。该标准面…

2026/9/6 11:40:10

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/6 19:33:50

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/6 10:19:40

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…