环境监测 CMMS 的表单 DSL 实践:从逐一开发到声明式生成,工单交付效率提升 10 倍

5 阅读8分钟

目录

  1. 背景与业务语境
  2. 技术方案全景
  3. 难点攻坚
  4. 业务价值与 ROI
  5. 个人思考与复用性

"低代码的本质不是少写代码,而是让正确的人做正确的事。"


1. 背景与业务语境

AirOps 是一套面向环境监测行业的 CMMS(计算机化维护管理系统),核心解决两个问题:计划性维护执行合规性质控审核。系统覆盖全国多个省市的空气质量监测站点,管理 PM2.5、PM10、SO₂、O₃ 等多类监测设备的运维全生命周期。

业务规模方面,系统承载 60+ 种工单类型(周检、月检、半年校准、故障排除、易耗品更换等),每种工单对应一张独立的表单记录表,涉及不同的检查项目、正常范围、判定逻辑和审核流程。

技术挑战的紧迫性:按传统开发模式,每种工单类型需要一名前端工程师开发独立的 Vue 组件——包含表单布局、校验逻辑、数据绑定、提交流程。60+ 种工单意味着 60+ 个定制组件,且业务方持续新增工单类型。按每个组件平均 2-3 人天计算,仅表单开发就需要 120-180 人天,占据前端团队近半年的产能。更致命的是,一线运维人员提出的表单调整需求(增删字段、修改检查项)必须等待前端排期,响应周期长达 1-2 周

不解决这个问题,前端团队将沦为"表单工厂",无法投入 GIS 可视化、数据分析等高价值功能的开发。


2. 技术方案全景

架构演进路径

Phase 1: 硬编码时代          Phase 2: DSL 引擎          Phase 3: 编辑器工程化
┌─────────────────┐    ┌──────────────────────┐    ┌─────────────────────────┐
  60+ Vue 组件          DSL 模板文本                CodeMirror 6 编辑器     
  每种工单一个                                   语法高亮 + Lint 检查     
  前端独占开发           解析引擎  动态渲染          非开发人员也能高效编写    
  2-3天/工单            运维人员可自行配置           错误实时反馈             
└─────────────────┘    └──────────────────────┘    └─────────────────────────┘

DSL 引擎核心架构

DSL 模板文本
    │
    ▼
┌─────────────────────────────────────────────┐
│            parseTableConfig2()               │
│  ┌─────────┐  ┌──────────┐  ┌────────────┐  │
│  │行分割     │→│ 控制流处理 │→│ 表达式归一化│  │
│  │splitLines│  │for/default│  │parseStrToAll│  │
│  └─────────┘  └──────────┘  └────────────┘  │
│                      │                       │
│               ┌──────▼──────┐                │
│               │  getTdConfig │                │
│               │  单元格解析   │                │
│               └──────┬──────┘                │
│                      │                       │
│            ┌─────────▼─────────┐             │
│            │  组件工厂 (15+类型) │             │
│            │ input/select/date  │             │
│            │ calc/formula/image │             │
│            └─────────┬─────────┘             │
│                      │                       │
│               ┌──────▼──────┐                │
│               │  JSX 渲染    │                │
│               │  Vue VNode   │                │
│               └─────────────┘                │
└─────────────────────────────────────────────┘

选型决策:为什么自研 DSL 而非使用现成低代码平台?

维度通用低代码平台自研 DSL
学习成本高(拖拽 + 配置面板 + 概念体系)低(类似填表,纯文本)
表单表达力通用但冗余精准匹配业务(合并单元格、条件判断、公式计算)
部署方式需额外平台/服务零依赖,模板即文本,存数据库即可
定制深度受限于平台能力完全可控(自定义组件工厂)

反直觉决策:我们没有选择市面上任何低代码平台(如 Formily、Amis),原因在于——环境监测工单表单的核心特征是强表格布局 + 合规性公式计算,而非通用的表单交互。一张典型的 SO₂ 分析仪周检记录表包含 20+ 行 × 6 列的严格表格布局,内嵌零点漂移计算公式、合格/不合格自动判定、多图片凭证上传。通用低代码平台的 JSON Schema 描述这种布局极其冗长,而我们的 DSL 只需 25 行文本即可完整表达。


3. 难点攻坚

难点一:DSL 语法设计——如何让非程序员 3 分钟上手?

Situation:运维团队有 60+ 种工单表单需要配置,但团队中没有前端工程师,只有熟悉业务的运维工程师。需要设计一种语法,让他们能直接"描述"表单结构。

Task:设计一套 DSL,满足:① 5 分钟可学会基本语法;② 覆盖 90% 的表单场景;③ 支持高级特性(循环、公式、条件判定)。

Action:核心设计原则是 "所见即所得的文本表格"——每行就是表格的一行,| 就是单元格分隔符,{字段名} 就是数据绑定。

{NoTable}SO₂分析仪运行状况检查记录表(每周)
{Table$24$2}省(区市):{下拉$Province$ProvinceName} 城市:{下拉$City$CityName}
{Table$5:7:6:6$1}仪器型号|{InstrumentModel}|校准日期|{日期$CalibrationDateTime$YYYY-MM-DD}
{Table$24$1}零点漂移(PPB):{计算$ZeroDrift$expr$ZeroDisplayValue-ZeroStandardConc$2}
{Table$24$1}  {判断$ZeroResult$ZeroDrift<=5 &&ZeroDrift>=-5$合格&&不合格}

关键语法糖设计:16 个中文简写关键字{下拉$}{日期$}{单选$}{计算$}{判断$} 等),在解析阶段通过 parseStrToAll() 归一化为统一的 {key$name#component$type} 完整格式。这让业务人员只需记忆几个中文词,而底层保持了完整的表达能力。

Result

  • 运维工程师 3 分钟学会基础语法,30 分钟独立完成第一张工单表单
  • 覆盖了 100% 现有工单类型(包括含循环、公式、条件判定的复杂表单)
  • 新工单类型从需求到上线:2-3 天 → 0.5-1 小时

难点二:CodeMirror 6 自定义语言支持——从"能用"到"好用"

Situation:DSL 投入使用后,一个实际问题浮现——模板在纯文本 <textarea> 中编写,没有语法高亮和错误提示。运维同事反馈:① 花括号容易漏写/多写;② {for$}{forEnd} 配对全靠肉眼数;③ Table$ 参数格式写错只有提交后才发现。每次调试平均浪费 15-30 分钟

Task:为自定义 DSL 实现完整的编辑器体验——语法高亮 + 实时错误检测,且不引入过重的依赖。

Action:选择 CodeMirror 6 的 StreamLanguage(流式解析器)而非完整的 Lezer Grammar。

为什么不用 Lezer Grammar? 这是第二个反直觉决策。Lezer 是 CodeMirror 6 官方推荐的解析器生成工具,但我们的 DSL 是严格按行解析的(每行独立,无跨行嵌套语法),用 Lezer 编写 .grammar 文件然后编译反而增加了构建链复杂度。StreamLanguage 逐行逐 token 扫描,天然匹配我们的语法结构,实现代码量仅 180 行

Tokenizer 状态机设计(核心 3 个状态):

interface DslState {
  inBrace: boolean;       // 是否在 {} 表达式内
  inDefaultData: boolean; // 是否在 defaultData JSON 块内
  afterHash: boolean;     // 是否在 # 属性分隔符之后
}

{} 外部只需识别 |(单元格分隔符)和 {(表达式开始);{} 内部按优先级匹配:行级控制关键字 → 组件关键字 → meta 属性 → 属性名 → 数字 → 标识符。关键字列表按长度降序排列,确保 {灵活下拉$} 优先于 {下拉$} 匹配。

Lint 规则引擎(10 条规则):

规则 1:  花括号匹配(不允许嵌套)
规则 2-4: for/joinTextFor/defaultData 块配对检查(栈追踪)
规则 5:  defaultData  JSON.parse 格式校验
规则 6:  Table$ 参数格式验证(正则:/^\d+(\s*:\s*\d+)*$/ 
规则 7:  中文关键字拼写检查(Levenshtein 距离  1  warning)
规则 8:  组件参数完整性({下拉$} 至少需要 2  $ 参数)
规则 9:  meta 属性目标有效性(仅限 table/tr/td)
规则 10: 必填标记 $* 位置检查

踩坑复盘:首版 Lint 引擎上线后,{defaultData$} 块内的 JSON 内容中的 { } 被规则 1 误判为"不允许嵌套花括号"。根因是规则执行顺序错误——花括号匹配检查在 inDefaultData 状态判断之前执行。修复方案:将 defaultData 块检测提升为循环内第一个执行步骤,在进入任何语法规则前先 continue 跳过 JSON 内容行。这个 Bug 提醒我们:状态机的优先级比规则本身更重要

Result

  • 语法错误从"提交后才发现"变为实时红色波浪线提示
  • 模板调试时间:15-30 分钟 → 2-3 分钟
  • 引入依赖体积可控:CodeMirror 6 按需引入仅 ~150KB gzipped

4. 业务价值与 ROI

指标优化前(硬编码)优化后(DSL + 编辑器)业务解读
单工单交付周期2-3 人天0.5-1 小时交付速度提升 16-48 倍
前端人力占用120-180 人天/60种工单≈0(运维自行配置)前端团队释放,投入 GIS 可视化等高价值模块
需求响应周期1-2 周(等前端排期)当天一线运维可自行调整表单字段
模板调试耗时15-30 分钟/次2-3 分钟/次编辑器实时反馈,减少 80% 调试时间
配置参与者仅前端工程师运维工程师可独立完成技术民主化,解除人力瓶颈

关键数据:通过 DSL 引擎 + 编辑器工程化的组合方案,在投入约 1.5 人月的情况下,替代了原本需要 6+ 人月的逐一组件开发工作量,ROI 达 1:4。更重要的是,这是一次性投入持续收益——每新增一种工单类型,节省的边际成本趋近于零。


5. 个人思考与复用性

方法论沉淀

这个实践验证了一个普适的工程策略:当你发现团队在重复解决"同构但不同参"的问题时,就是引入 DSL 的最佳时机。DSL 的价值不在于技术复杂度,而在于它重新定义了"谁来做这件事"——将配置权从开发者转移到领域专家。

该方案可抽象为通用的"表格型表单 DSL 引擎",适用于任何以表格为核心布局的表单场景(质检记录、实验报告、巡检清单等)。

技术贡献

  • 编写了完整的 DSL 语法文档,包含所有关键字说明和示例
  • 面向运维团队进行了 2 次 DSL 编写培训,实现了技术能力的团队复制
  • CodeMirror 自定义语言实现可作为团队内部其他 DSL 编辑器需求的参考模板

"最好的架构不是最复杂的架构,而是让最多人能参与其中的架构。"


技术关键词DSL 低代码 CodeMirror 6 StreamLanguage 自定义Lint Vue 3 动态表单 CMMS 环境监测 工程效能

image.png