目录
"低代码的本质不是少写代码,而是让正确的人做正确的事。"
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 环境监测 工程效能