纯前端JS工作流引擎与审批流源码解析:从JSON定义到二次开发

发布时间:2026/10/25 16:44:50

纯前端JS工作流引擎与审批流源码解析:从JSON定义到二次开发 简介面向Web前端与全栈开发者的一份JavaScript工作流与审批流实践资料聚焦如何在浏览器端组织任务分配、状态流转与多人协作审批适合已具备一定JS基础、希望理解审批流程实现思路的中级学习者参考。压缩包共49个文件约69KB以js脚本、html页面、xml流程定义与gif示意图为主辅以css样式、aspx与cs后端页面、db数据文件及xsl、config等配置覆盖流程设计、界面交互与数据持久化等环节。资源中可见流程定义文件、步骤与动作对话框页面、多语言资源及右键菜单、拖拽移动步骤等交互脚本便于读者对照理解状态机驱动的流程推进、角色权限控制与异常回退处理。目前已有1288人学习下载可作为梳理审批流前后端协作与流程引擎设计思路的轻量参考。1. 从一张审批单说起这个 js 工作流项目到底解决什么问题去年接手一个内部审批系统时我第一反应是找现成的流程引擎结果发现要么是 Java 生态的重型方案要么是拖拽式低代码平台前端想改一个节点样式都得翻半天文档。后来拿到这份 js 工作流、审批流源码才算把「流程定义」和「页面渲染」这两件事拆开了看。它本质上是一套纯前端可跑的工作流引擎加审批流界面用 JSON 描述节点和连线引擎负责推进状态、判断分支、记录流转历史页面负责把当前节点、待办人、审批意见渲染出来。适合两类人一是要在管理后台里嵌审批功能的前端开发者二是想搞懂工作流引擎内部怎么跑、准备自己改一版的中级选手。下面按「能干什么 → 怎么跑 → 核心怎么实现 → 怎么改」的顺序拆。2. 项目结构与技术选型一个纯前端工作流引擎的骨架2.1 目录分层与职责边界拿到源码先别急着 npm install花十分钟把目录扫一遍后面改代码能少走很多弯路。这类 js 工作流项目通常按「引擎核心 / 流程定义 / 视图层 / 工具函数」四块分我按实际拆过的结构给你还原一下workflow-demo/ ├── src/ │ ├── engine/ # 引擎核心与 UI 无关 │ │ ├── Workflow.js # 流程实例负责 start/next/back │ │ ├── Node.js # 节点模型类型、审批人、条件 │ │ ├── Condition.js # 分支条件求值 │ │ └── History.js # 流转历史记录 │ ├── definition/ # 流程定义JSON 模板 │ │ └── leave.json # 请假审批示例流程 │ ├── views/ # 页面层 │ │ ├── FlowDesigner.js # 简易流程设计器 │ │ └── ApprovalPanel.js # 审批操作面板 │ ├── utils/ │ │ └── expression.js # 条件表达式解析 │ └── index.js # 对外入口 ├── public/ │ └── index.html ├── package.json └── README.md关键点在于engine/目录不依赖任何 DOM 和框架纯 JS 类这意味着你可以把它抽出来放到 Vue、React 甚至小程序里跑。views/才是和页面绑定的部分。我第一次改的时候把审批逻辑写进了视图层后来换框架重构血泪经验就是引擎和视图一定要守住边界引擎只认数据不认页面。2.2 技术栈与依赖取舍这类项目一般走「原生 JS 轻量构建」路线不绑死框架。典型依赖如下依赖作用是否可替换vite / webpack本地开发与打包可换看团队习惯原生 DOM 或轻量模板视图渲染可换 Vue/React自研 expression.js条件表达式求值不建议换和引擎耦合localStorage / 内存流程实例持久化可换后端接口为什么不用现成流程引擎因为审批流的核心诉求其实很窄串行审批、条件分支、会签、驳回、历史留痕。重型引擎带了一堆你用不上的能力反而让前端改一个节点字段都要穿透好几层。这个项目选择自己实现代价是功能没那么全好处是每一行都能改。我一般会建议流程节点不超过 20 个、审批人规则不复杂的场景自研这套完全够用一旦涉及复杂会签和动态加签再考虑接后端引擎。2.3 流程定义的 JSON 长什么样整个项目的「配置入口」就是流程定义 JSON引擎读它、设计器改它、页面渲染它。先看一个请假审批的示例{ id: leave-flow, name: 请假审批, start: apply, nodes: [ { id: apply, type: start, name: 提交申请, next: leader }, { id: leader, type: approval, name: 直属领导审批, assignee: leaderId, next: checkDays }, { id: checkDays, type: condition, name: 天数判断, branches: [ { expr: days 3, next: hr }, { expr: days 3, next: manager } ] }, { id: hr, type: approval, name: HR 备案, assignee: hrId, next: end }, { id: manager, type: approval, name: 部门经理审批, assignee: managerId, next: end }, { id: end, type: end, name: 结束 } ] }字段含义type决定节点行为start是入口approval是人工审批节点condition是自动分支节点end是终点。assignee存的是变量名而不是具体人名运行时从表单数据里取值这样同一份流程定义能复用给不同的人。branches里的expr是条件表达式由expression.js求值。改流程基本就是改这份 JSON不用动引擎代码这是这套设计最舒服的地方。3. 跑起来从安装到走通一条审批流3.1 本地启动与最小验证先把项目跑起来别一上来就读源码。标准流程# 安装依赖 npm install # 启动开发服务器vite 项目默认 5173 端口 npm run dev # 生产构建验证打包是否正常 npm run build启动后浏览器打开本地地址应该能看到一个流程设计器加一个审批面板。如果页面空白先看控制台有没有Cannot read properties of undefined多半是流程定义 JSON 路径没对上。我一般会先手动走一遍提交申请 → 领导审批通过 → 天数判断 → 对应分支 → 结束确认引擎能推进再去看代码。这一步能帮你快速判断「是环境问题还是逻辑问题」。3.2 引擎启动与节点推进的核心代码跑通之后重点看engine/Workflow.js。这是整个项目的心脏核心方法就三个start、next、back。下面是我按这类项目常见实现还原的核心片段// engine/Workflow.js class Workflow { constructor(definition, formData {}) { this.definition definition; this.formData formData; // 表单数据条件求值和审批人取值都靠它 this.currentNodeId null; this.history []; // 流转历史驳回和追溯要用 } // 启动流程定位到 start 节点 start() { this.currentNodeId this.definition.start; this.record(start); return this.getCurrentNode(); } // 推进到下一个节点审批通过时调用 next(comment ) { const node this.getCurrentNode(); if (node.type end) return null; let nextId; if (node.type condition) { // 条件节点逐条求值命中第一条即走 const branch node.branches.find(b evaluate(b.expr, this.formData) ); nextId branch ? branch.next : null; } else { nextId node.next; } if (!nextId) throw new Error(节点 ${node.id} 没有可走的下一节点); this.record(approve, comment); this.currentNodeId nextId; return this.getCurrentNode(); } // 驳回回退到上一个审批节点 back(comment ) { const last [...this.history].reverse() .find(h h.action approve); if (!last) throw new Error(没有可回退的节点); this.record(reject, comment); this.currentNodeId last.nodeId; return this.getCurrentNode(); } getCurrentNode() { return this.definition.nodes.find(n n.id this.currentNodeId); } record(action, comment ) { this.history.push({ nodeId: this.currentNodeId, action, comment, time: Date.now() }); } }逐段说明构造函数接收流程定义和表单数据formData是条件求值和审批人解析的数据源必须提前传进来。start()只做定位和记录不推进。next()是核心先判断当前节点类型条件节点走branches求值普通节点直接取next找不到下一节点就抛错这是防止流程定义写错导致死循环的兜底。back()从历史里倒着找最近一次审批通过的节点回退注意这里用的是history而不是节点定义里的prev因为条件分支下「上一个节点」不唯一只能靠历史记录。record()统一写历史驳回、追溯、审计都靠它。参数上要留意comment是审批意见会进历史formData里的字段名必须和 JSON 里assignee、expr引用的变量名一致对不上就是undefined条件求值会静默走错分支这个坑后面单独说。3.3 条件表达式求值别直接上 evalexpression.js负责把days 3这种字符串算出布尔值。新手最容易图省事直接eval能跑但埋雷。常见做法是用new Function包一层或者写个极简解析器// utils/expression.js // 用 Function 构造求值函数比 eval 作用域更可控 export function evaluate(expr, context) { try { const keys Object.keys(context); const values keys.map(k context[k]); // 只把上下文变量注入避免访问全局对象 const fn new Function(...keys, return (${expr});); return Boolean(fn(...values)); } catch (e) { console.error(表达式求值失败: ${expr}, e); return false; // 求值失败默认不走该分支防止误放行 } }逻辑说明把formData的键作为函数参数名、值作为实参传进去表达式里就能直接用days、amount这些变量。用new Function而不是eval是因为前者不会继承当前作用域的变量注入面更小。求值失败返回false而不是抛出去是为了让流程在脏数据下也能继续但一定要打日志否则条件写错了你根本不知道。参数上context就是formDataexpr来自流程定义两者都不可信时比如流程定义允许用户上传就得换成白名单解析器这是安全底线。3.4 审批面板如何绑定当前节点视图层ApprovalPanel.js的职责很单一拿到引擎当前节点渲染审批人、按钮、意见框点通过就调workflow.next()点驳回就调workflow.back()。典型写法// views/ApprovalPanel.js import { Workflow } from ../engine/Workflow.js; import definition from ../definition/leave.json; const wf new Workflow(definition, { days: 5, leaderId: u1001 }); wf.start(); function render() { const node wf.getCurrentNode(); document.querySelector(#nodeName).textContent node.name; // 审批人从 formData 里按 assignee 变量名取 const assignee node.assignee ? wf.formData[node.assignee] : ; document.querySelector(#assignee).textContent assignee || 系统自动; } document.querySelector(#approve).onclick () { const comment document.querySelector(#comment).value; wf.next(comment); render(); }; document.querySelector(#reject).onclick () { const comment document.querySelector(#comment).value; wf.back(comment); render(); }; render();这段代码的关键是「引擎状态驱动视图」每次操作后重新render()而不是手动改 DOM 状态。assignee用变量名去formData里取值所以表单里必须有leaderId这个字段。如果你接后端把formData换成接口返回的数据即可引擎不用动。这也是前面强调引擎和视图分离的回报——换 UI 只改这一层。4. 核心模块拆解节点模型、历史记录与持久化4.1 节点类型与扩展点设计引擎里所有节点共用一个模型靠type区分行为。这种「一个模型 类型分发」的设计扩展新节点类型成本很低。比如要加一个「抄送节点」只需在next()里加一个分支// 在 Workflow.next() 中扩展节点类型 switch (node.type) { case condition: nextId resolveCondition(node); break; case cc: // 抄送节点记录后自动往下走 this.record(cc, 抄送给 ${node.assignee}); nextId node.next; break; default: nextId node.next; }为什么用switch而不是给每个节点写一个类因为审批流的节点行为差异很小大多是「记录 取 next」上类继承反而增加理解成本。真正需要独立逻辑的是条件节点和会签节点这两个单独抽函数就够了。我一般会建议节点类型超过 6 种、且每种都有独立校验逻辑时再考虑策略模式重构否则switch更直白。4.2 历史记录与驳回的可靠性驳回是审批流里最容易翻车的地方。前面back()依赖history倒查这里有个隐含前提历史必须完整且有序。如果中途有人手动改了currentNodeId却没记历史驳回就会跳错节点。所以record()必须在每次状态变更时调用且currentNodeId的赋值只能出现在start/next/back三个方法里。这是纪律问题不是技术问题但恰恰是二次开发时最容易破坏的约定。历史记录的结构建议固定成{ nodeId, action, comment, time, operator }operator是当前操作人审计要用。如果接后端历史表就按这个结构建前端只负责展示。别把历史塞进流程定义 JSON 里定义是模板历史是实例数据混在一起后面做多实例就崩了。4.3 持久化内存、localStorage 还是后端示例项目默认用内存存流程实例刷新就没了。要落地必须持久化三种选择方案适用场景注意点内存演示、单次会话刷新丢失不能用于生产localStorage纯前端 Demo、离线工具容量有限多标签页会冲突后端接口真实审批系统需处理并发和幂等我一般会先把Workflow实例序列化成{ definitionId, currentNodeId, formData, history }存起来恢复时用Object.assign还原。注意definition不要跟着实例存只存definitionId恢复时重新加载定义否则流程定义一改老实例就带着旧定义跑了这是版本管理的坑。5. 避坑与排查审批流落地时最容易踩的五个坑5.1 条件分支静默走错现象明明days 5却走了days 3的分支。原因formData里字段名是day而不是days表达式求值时变量是undefinedundefined 3为false……等等这里更隐蔽的是字符串比较5 3会走类型转换。解决求值前对formData做类型规整数字字段强制Number()并在evaluate里对undefined变量直接抛错而不是静默返回false。5.2 驳回后历史错乱现象连续驳回两次第二次跳到了错误的节点。原因back()每次都在history里找最近一次approve但驳回本身也记了历史倒查逻辑没排除驳回记录。解决倒查时过滤action approve且节点类型为approval的记录或者单独维护一个「审批栈」进栈出栈更清晰。5.3 流程定义改了老实例跑不动现象流程加了一个节点之前跑到一半的实例点「通过」报「没有下一节点」。原因实例存的是旧定义快照或旧currentNodeId新定义里节点next变了。解决实例只存definitionId 版本号加载时按版本取定义定义变更走新版本老实例继续用老版本跑完。5.4 会签节点被当成普通节点现象要求三个人都审批结果第一个人点通过就往下走了。原因next()对approval节点一律直接推进没判断会签。解决会签节点加mode: all字段维护一个approvedList集齐才推进否则只记录当前人通过。这个功能示例项目没内置二次开发高频需求。5.5 刷新页面流程丢失现象审批到一半刷新回到初始节点。原因只用了内存存储。解决每次next/back后把实例序列化存 localStorage 或调后端保存接口页面加载时先尝试恢复。注意恢复时要校验currentNodeId在新定义里是否还存在不存在就提示流程已变更。6. 二次开发把示例引擎改成能上生产的审批流示例跑通只是起点真正落地要补三块会签、动态审批人、流程版本。会签前面提了思路核心是给节点加mode和approvedListnext()里判断是否集齐。动态审批人则是把assignee从「变量名」升级成「规则」可以是{ type: role, value: manager }运行时查组织架构解析成人。这块建议单独抽一个resolveAssignee(node, formData, orgService)函数引擎只调不实现保持核心干净。流程版本管理我踩过最狠的一次直接改线上流程定义 JSON结果在途的 30 多单全部卡死。从那以后我每次改流程定义都强制走一遍「复制新版本 → 灰度验证 → 老实例锁旧版本」的流程。具体做法是定义文件按leave.v1.json、leave.v2.json命名实例存definitionId version加载时精确匹配。验证方法也简单写一个脚本用历史表单数据把每个分支都跑一遍断言最终节点符合预期跑通了再发布。// 流程定义回归脚本示例 import { Workflow } from ../src/engine/Workflow.js; import def from ../src/definition/leave.v2.json assert { type: json }; const cases [ { form: { days: 2 }, expect: hr }, { form: { days: 7 }, expect: manager } ]; cases.forEach(({ form, expect }) { const wf new Workflow(def, form); wf.start(); wf.next(); // 领导审批通过 const node wf.getCurrentNode(); console.assert(node.id expect, 期望 ${expect}实际 ${node.id}); });这个脚本每次改定义都跑一遍比手点靠谱得多。参数上form要覆盖所有条件分支的边界值expect写分支后的第一个节点 id。断言失败直接看是哪个分支的表达式写错了。二次开发还有个小技巧把Workflow类的事件回调留出来比如onNodeChange、onHistoryAdd视图层订阅这些事件而不是每次手动render()后面接 WebSocket 推送待办也方便。引擎里加一行this.emit(nodeChange, node)的事但能让整个项目从「示例」往「框架」靠一步。希望帮到你。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/10/25 16:39:50

客服对话变了,订单状态不能跟着被改写

“前一轮已经答应我退款”是对话内容,不是订单状态。10月19日微软讨论了Agent的内部访问风险。把聊天记忆直接当成业务状态,会让一段看似连贯的解释获得不该有的权限。 状态由事实驱动 客服可以从询问物流切换到售后咨询,这属于意图变化。能…

2026/10/25 16:39:50

给Agent放行一个域名,为什么还挡不住错误外联

应用代码看到一个HTTPS地址,就能认为它属于批准的网站吗?域名解析、重定向和代理链都可能改变最终目的地。10月23日OpenAI的隔离说明再次把出站路径放到了显眼的位置。 应用校验只是第一道门 URL中有scheme、hostname、port、userinfo和路径。用starts…

2026/10/25 16:39:50

cloud-pacs-api实战:DICOMweb与对象存储驱动云端在线阅片平台

简介:面向医学影像系统开发者,PostDICOM Cloud PACS API 资源包提供了一套基于云的 DICOM 存档与在线查看方案,支持 MRI、CT、超声、射线等设备图像的存储、访问与共享,有效替代传统硬件 PACS,降低归档成本。压缩包共 …

2026/10/25 17:49:55

单目图像如何看出3D效果?深度线索与多模态融合实战解析

1. 从一张平面图里“读出”深度:这件事到底难在哪先抛一个我经常在技术分享会上问的问题:给你一张普通的手机照片,你能说出画面里哪个杯子离你更近吗?大多数人会觉得这问题很蠢——当然能,一眼就看出来了。但如果我告诉…

2026/10/25 17:49:55

灵初智能20亿融资:具身智能与机器人协同技术新突破

1. 灵初智能20亿融资事件概述2023年智能科技领域最引人注目的融资事件之一,当属灵初智能(Lingchu AI)完成的20亿元人民币巨额融资。这家专注于认知智能与机器人协同技术的创新企业,在种子轮就获得了蓝驰创投与智元机器人两家顶级机…

2026/10/25 17:49:55

DQN实战避坑指南:经验回放与目标网络的Python源码级解析

简介:本资源是一份面向计算机、电子信息工程及数学等专业学生的深度强化学习实践材料,聚焦DQN(Deep Q-Network)算法原理与工程实现,适用于课程设计、期末大作业及毕业设计等中阶项目开发场景。压缩包共2个Python源文件…

2026/10/25 17:49:55

深度可分离卷积原理、计算优势与工程实践

1. 卷积神经网络基础与计算代价解析在计算机视觉领域,卷积神经网络(CNN)已经成为图像处理任务的标准架构。传统CNN中最核心的组件就是普通卷积层(Standard Convolution),它通过滑动窗口的方式在输入特征图上进行局部连接和权重共享,有效减少了…

2026/10/25 17:44:54

目标检测中的画质评估与自适应处理技术

1. 项目背景与核心价值在计算机视觉领域,目标检测技术已经发展得相当成熟,但大多数检测器都默认输入图像是高质量的。然而现实世界中,我们获取的图像常常受到各种质量退化因素的影响——运动模糊、传感器噪声、JPEG压缩伪影、低分辨率等等。这…

2026/10/25 0:02:22

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

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

2026/10/25 0:02:23

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

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

2026/10/25 0:02:23

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

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

2026/10/25 0:02:22

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

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

2026/10/25 0:02:23

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

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

2026/10/25 0:02:23

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

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

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

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

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