从0到1做Web组态编辑器,哪些能力必须自己做哪些千万别自己做

0 阅读11分钟

做 Web 组态编辑器这件事,特别容易进入一种危险状态:

一开始你以为自己在做“一个带拖拽的画布”,做着做着发现自己其实在做的是——渲染引擎、交互系统、协议适配层、配置面板、历史记录、资源管理、权限系统、脚本系统、导出系统,以及一个情绪极不稳定的前端应用。

我这两年看过不少可视化/SCADA/大屏编辑器项目,最后能活下来的团队,通常都不是“技术最炫”的那批,而是边界感最强的那批:知道哪些能力必须咬牙自己做,哪些能力应该站在成熟轮子上二次封装,哪些东西则要坚决忍住,不要第一阶段就手痒。

这篇就聊这个。

image.png

先说结论:别把“组态编辑器”理解成一个大组件

它不是一个组件,而是一组能力系统:

  1. 编辑态:拖拽、吸附、框选、缩放、图层、分组、快捷键、撤销重做。
  2. 运行态:实时数据绑定、状态映射、动画、告警、权限、性能稳定性。
  3. 配置态:属性面板、事件系统、变量系统、脚本、数据源配置。
  4. 交付态:发布、嵌入、导出、版本管理、资源依赖、运行容器。

如果你的团队还在产品 0→1 阶段,我的建议很简单:

  • 自己做画布内的核心体验
  • 复用画布外的通用基础设施
  • 别在“看起来也挺重要”的边角系统里提前透支团队

下面分开说。


一、这些能力,必须自己做

1)场景模型:节点、连线、分组、锚点、层级

很多团队上来先挑画图库,结果最后发现最难的不是“画出来”,而是你的业务对象怎么表达

一个真正可用的组态编辑器,底层必须有稳定的场景模型,至少要想清楚:

  • 节点和连线分别是什么数据结构
  • 分组是逻辑分组还是几何分组
  • 锚点、吸附点、连接点怎么表示
  • 旋转、缩放、父子层级如何继承
  • 运行态属性和编辑态属性是否分离

一个最小可用的数据结构大概会长这样:

interface GraphNode {
  id: string;
  type: string;
  x: number;
  y: number;
  width: number;
  height: number;
  rotate?: number;
  style?: Record<string, any>;
  dataBind?: {
    pointId: string;
    formatter?: string;
    qualityField?: string;
  };
  events?: EditorEvent[];
  children?: string[];
}
​
interface GraphLine {
  id: string;
  from: { nodeId: string; anchorId: string };
  to: { nodeId: string; anchorId: string };
  style?: Record<string, any>;
}

这类模型最好自己定义。原因很现实:

  • 你迟早会遇到行业组件和通用图元混用
  • 你迟早会做运行态数据映射
  • 你迟早会加批量操作、规则引擎、导出

如果一开始把数据模型完全交给第三方库,后期改起来会很痛。

2)编辑器交互:这是用户每天骂不骂你的核心

组态编辑器的价值,不在 demo 截图,而在“编辑 3 小时以后人会不会崩”。

真正决定体验的,是这些细节:

  • 拖拽时是否稳定
  • 缩放后命中是否准确
  • 框选是否符合预期
  • 多选后移动是否顺手
  • 对齐线是否聪明但不烦人
  • 吸附和自动布局会不会互相打架
  • 键盘快捷键是否符合通用习惯

这部分我非常建议自己做,至少核心逻辑必须掌握在手里。

因为第三方库一般只解决“能编辑”,而你需要的是“能高频编辑”。这两者中间差了一个产品代际。

3)撤销/重做:别再全量快照一把梭了

撤销重做是组态编辑器第一批会被做错的能力。

很多项目初版都这么写:

undoStack.push(JSON.stringify(pageState));

小项目能跑,大项目会出三个问题:

  1. 内存涨得快:节点一多,快照巨大。
  2. 性能抖动明显:频繁序列化/反序列化。
  3. 语义不够:你只知道“状态变了”,不知道“发生了什么操作”。

更靠谱的方案通常是:

  • 命令模式记录操作语义
  • patch 或最小变更集记录数据差异
  • 对连续拖拽、连续输入做合并提交

像这样:

interface Command {
  label: string;
  execute(): void;
  undo(): void;
  merge?(next: Command): boolean;
}
​
class MoveNodesCommand implements Command {
  constructor(
    private ids: string[],
    private before: Array<{ x: number; y: number }>,
    private after: Array<{ x: number; y: number }>
  ) {}
​
  label = 'move-nodes';
​
  execute() {
    applyPositions(this.ids, this.after);
  }
​
  undo() {
    applyPositions(this.ids, this.before);
  }
​
  merge(next: Command) {
    return next instanceof MoveNodesCommand;
  }
}

我这次查资料时,国内几篇关于 Canvas 编辑器 history 的文章,基本也都在强调两条路线:快照法适合入门,命令/差异法才适合生产。这点很一致。

另外,现代浏览器里的 structuredClone() 确实可以比 JSON.parse(JSON.stringify()) 更稳一点,适合某些中间态复制;但它不是 history 的银弹。深拷贝工具能优化实现细节,替代不了正确的历史模型。

4)渲染调度:不是“用了 Canvas”就自动高性能

很多人把 Canvas 当性能护身符。其实不是。

组态编辑器卡不卡,关键看你有没有做这些:

  • 分层渲染:背景层、主图层、交互层、辅助层分开
  • 脏区刷新:只重绘变化区域
  • 命中检测优化:空间索引或粗细两阶段检测
  • 预渲染:静态元素缓存
  • 高频交互降频:drag 过程减少无意义计算

如果节点数大、动画多,还可以考虑把部分渲染放进 OffscreenCanvas + Worker。查 MDN 和相关资料时能看到,OffscreenCanvas 现在在现代浏览器上的可用性已经比前几年成熟得多,适合把重渲染、位图生成、部分预处理搬离主线程。

但这里要注意一句:

OffscreenCanvas 适合“重绘密集型任务”,不适合把整个编辑器逻辑一股脑扔进 Worker。

因为你的交互命中、DOM 面板、快捷键、属性编辑,核心还是主线程协调。正确姿势通常是:

  • 主线程负责交互编排
  • Worker 负责重计算/离屏绘制
  • 通过消息传递同步最小状态

5)数据绑定和状态表达:这是组态,不是纯绘图

流程图编辑器和组态编辑器最本质的差别,是后者一定要连接运行数据。

也就是说你必须自己定义:

  • 点位绑定方式
  • 实时值怎么写入节点状态
  • 告警如何表现
  • 质量码如何影响 UI
  • 动画和状态切换的优先级谁更高

比如同一个泵图元,可能同时存在:

  • 运行中:绿色旋转
  • 停止:灰色静止
  • 故障:红色闪烁
  • 通讯异常:半透明 + 斜纹遮罩

这套规则必须是你的领域语言,不可能完全靠通用图库给你。

6)行业组件库:差异化就藏在这里

通用矩形、圆形、折线谁都能画。

真正有壁垒的是:

  • 电力接线图元件
  • 水务/化工管道元件
  • 仪表盘与阀门
  • 风机/泵/皮带/液位等动态组件
  • 告警、联锁、状态切换的语义化封装

用户买的不是一个“会画矩形”的编辑器,买的是一个“我半天能拼出业务现场”的系统。

所以行业组件一定要自己沉淀。


二、这些能力,优先复用或二次封装

1)属性面板和表单引擎

真的没必要从零手搓一个复杂属性面板框架。

如果你的属性编辑本质上是“配置驱动表单”,那就直接站在成熟 UI 组件库或 Schema 表单能力上做二次封装。

比如你可以把每类图元的属性声明成 schema:

const pumpSchema = [
  { key: 'name', label: '名称', component: 'Input' },
  { key: 'style.fill', label: '填充色', component: 'ColorPicker' },
  { key: 'bind.pointId', label: '绑定点位', component: 'PointSelector' },
  { key: 'alarm.enable', label: '开启告警闪烁', component: 'Switch' }
];

然后:

  • 通用字段走通用表单渲染
  • 少量行业字段单独插槽扩展

这样团队不会被一堆表单细节拖死。

2)代码编辑器、脚本编辑器

表达式、脚本、事件处理器这些需求,最后大概率都要接 Monaco 或 CodeMirror。

别自己做。

“自研代码编辑器”听起来很酷,实际上属于给自己安排加班。

3)图表库

组态平台里总有人会提一句:“要不图表也自己画吧?”

不要。

ECharts、Highcharts 这类生态成熟、能力边界清楚的库,拿来用就行。你的价值不在于重写折线图,而在于:

  • 图表组件如何接入数据源
  • 如何在编辑器中配置
  • 如何统一主题和交互
  • 如何和组态页面其他组件联动

4)资源上传、权限、监控

这类能力虽然重要,但对组态编辑器来说不是核心差异点。

更合理的方式是:

  • 直接接现有对象存储/上传服务
  • 权限接统一账号体系
  • 前端监控接现成平台
  • 埋点用统一采集方案

这类基础设施做得再漂亮,用户也不会因为“你们上传 SDK 写得真优雅”而续费。

5)协同底座(如果真的要做协同)

如果你已经进入多人协同编辑阶段,优先考虑成熟 CRDT/协同协议方案,不要第一版就自造同步协议。

协同编辑最难的从来不是“别人改了我也看见了”,而是:

  • 命令历史怎么与协同操作兼容
  • 选区/光标状态怎么表达
  • 多人同时拖同一元素如何处理
  • 网络抖动下如何保持可恢复

这套东西太容易低估。


三、这些东西,第一阶段千万别自己做

1)完整富文本系统

如果你的组态编辑器里只是偶尔要写说明文字、注释、标题,够用就行。

不要因为一个“文本框支持加粗斜体”需求,把自己拉进富文本深坑。

2)自研图表引擎

除非你的产品本质就是图表引擎,否则不要碰。

3)完整低代码平台全家桶

很多团队做组态编辑器时,会逐渐产生一种幻觉:

既然我们已经有画布、有组件、有属性面板,是不是顺手把页面搭建器、表单设计器、BI、流程编排也做了?

这就是经典项目膨胀现场。

请记住:编辑器成功的关键,不是能力多,而是主链路顺。

4)一次做完所有行业组件

早期最靠谱的策略永远是:

  • 先打一个行业样板
  • 把 20% 高频组件做到 80 分
  • 用真实项目逼出抽象层
  • 再做组件扩展体系

而不是会议室里凭空列 300 个组件。


四、一个我更推荐的 0→1 落地顺序

如果让我从头带一个小团队做,我会按这个顺序切:

第 1 阶段:先把“最小可编辑闭环”做出来

目标:能画、能选、能拖、能保存、能撤销。

必做:

  • 画布与场景模型
  • 基础图元
  • 选中/拖拽/缩放
  • 属性面板最小集
  • 保存/加载
  • undo/redo

第 2 阶段:补齐“能交付”的运行能力

目标:不只是编辑器 demo,而是能跑现场页面。

必做:

  • 数据源接入(WebSocket/MQTT/SSE)
  • 实时绑定
  • 动画和状态规则
  • 权限和发布
  • 大页面性能优化

这里可以把一些低优先级计算放进 requestIdleCallback() 做分片处理,比如:

  • 非关键 schema 预处理
  • 缩略图生成
  • 资源索引整理
  • 批量检查和分析

不过 MDN 也明确提醒过,requestIdleCallback() 更适合低优先级后台任务,不要把关键 UI 更新塞进去,更建议加 timeout 兜底。

第 3 阶段:再考虑扩展体系

目标:让产品开始可规模化。

必做:

  • 插件机制
  • 行业组件扩展
  • 事件脚本体系
  • 导出和嵌入
  • 模板市场/物料体系

第 4 阶段:最后再碰协同和平台化

因为这时候你至少知道:

  • 用户到底在哪里卡
  • 哪些模型已经稳定
  • 哪些交互不会再大改

这时做平台化,才不是空中楼阁。


五、技术选型上,我现在的一个偏见

如果目标是工业场景、SCADA、大屏、拓扑、实时监控这类高频重绘编辑器,我会更偏向:

  • Canvas 做主渲染层
  • DOM 做面板和外层交互
  • 命令模式 + patch 做历史系统
  • Schema 驱动属性配置
  • Worker / OffscreenCanvas 做重任务隔离

原因很简单:

  • SVG 在节点量大、频繁更新时压力更早出现
  • 全 DOM 方案做复杂画布交互会比较痛
  • 纯快照 history 很难扛到生产环境
  • 先把“可编辑”和“可运行”分层,后面更容易扩展

当然,具体项目怎么选,还得看节点规模、动画密度、团队经验和目标行业。但如果你问我 2026 年做这类产品最容易少走弯路的路线,我大概还是这套答案。


最后总结成一句大白话

自己做用户每天都在用、且决定产品上限的部分;复用那些成熟但不构成差异化的部分;克制住把项目做成“宇宙级平台”的冲动。

组态编辑器这类产品,最怕的不是难,而是贪。

你可以先做小,但一定要把骨架做对。

不然第一版写得越快,第二版重写得越彻底。