基于Vue3 + Node.js全栈开发全功能Web HMI的调研与解决方案
一、方案概述
1.1 方案定位
本方案旨在基于Vue3 + Node.js全栈技术,从零构建一套全功能的工业Web HMI系统。不同于购买商业组态软件或直接套用开源框架,本方案强调自主掌控核心代码、完全定制化功能、与现有业务系统深度融合的能力,适用于设备制造商构建自有品牌的差异化HMI产品、系统集成商开发定制化工业监控平台、以及有特定行业需求的终端用户自建监控体系。
1.2 Web HMI的行业趋势
Web HMI正加速成为工业可视化的主流方向。西门子WinCC Unified全面采用HTML5、SVG和JavaScript等原生Web技术构建可视化系统,支持通过任何现代Web浏览器访问。力控科技推出FWebSCADA云组态监控平台,采用分层式架构(设备接入层→数据处理层→应用服务层→网络传输层→用户交互层),依托Web技术实现免客户端安装、多平台操作。N3uron的Web Vision模块是纯Web的HMI/SCADA界面,完全基于JavaScript、HTML5和CSS技术,支持智能手机、平板、触摸屏、笔记本电脑和控制室视频墙等全设备类型。
传统HMI的根本缺陷在于封闭的架构——依赖专用硬件终端、需要安装特定运行环境和驱动、缺乏原生远程访问能力,导致设备数据成为“孤岛”。基于Web的HMI则具备更高的灵活性和可扩展性,正在从根本上改变工业可视化的部署和运维模式。

二、技术栈选型详解
2.1 前端技术栈
核心框架:Vue 3.4+(Composition API + TypeScript)
Vue3的Composition API提供更好的逻辑复用和代码组织能力,TypeScript确保大型项目的类型安全和可维护性。参考成熟的工业可视化项目,Vue3已被广泛验证适用于工业监控场景。
构建工具:Vite 5.x
Vite提供极速冷启动和热更新体验,适合大型HMI工程的开发效率要求。
状态管理:Pinia 2.x
Vue官方推荐的状态管理库,轻量且类型安全,适合管理HMI中的设备状态、报警队列、用户会话等全局数据。
UI组件库:Element Plus / Ant Design Vue
提供成熟的表单、表格、弹窗、菜单等通用UI组件,加速非核心界面的开发。二者均为Vue3生态中TypeScript优先、活跃维护的官方推荐组件库。
数据可视化核心引擎:ECharts 5.x
处理HMI核心的实时趋势曲线、柱状图、饼图、仪表盘等数据可视化需求。ECharts支持大规模数据的高性能渲染和动态数据更新。
Web组态编辑器:自研 + SVG/Canvas
基于Vue3的SVG可视化Web组态编辑器可实现工业控制系统的拓扑图、流程图设计,通过拖拽方式添加SVG元素,并利用Vue3的响应式特性实现数据动态绑定。开源方案如maotu-webtopo提供了将SVG文件转换为可自由缩放、交互的图形化组件库的参考实现,支持低代码拖拽开发。
其他关键依赖:
| 依赖 | 版本 | 用途 |
|---|---|---|
| Vue Router | 4.x | 画面导航与路由 |
| Axios | 1.x | HTTP通信 |
| Day.js | 1.x | 时间处理 |
| GSAP | 3.x | 高级动画效果 |
| Three.js | 0.170+ | 3D设备模型渲染(可选) |
2.2 后端技术栈
运行环境:Node.js 20 LTS + TypeScript
Node.js的事件驱动和非阻塞I/O模型天然适合处理大量并发的工业设备连接。
Web框架:NestJS / Express + Socket.IO
NestJS的模块化架构适合构建大型后端服务,提供依赖注入、中间件、守卫等企业级特性。若团队倾向轻量方案,Express配合中间件模式同样可行。
实时通信:Socket.IO 4.x / 原生WebSocket
Web组态本质上通过HTML5+WebGL技术栈实现浏览器端工业控制界面开发,关键技术突破在于实现了低延迟数据通信(WebSocket协议)与高精度图形渲染(Canvas/WebGL)的融合。Socket.IO在WebSocket基础上提供自动重连、房间管理、事件命名空间等增强能力。
协议驱动层:
| 协议 | 驱动方案 | 说明 |
|---|---|---|
| Modbus TCP | modbus-serial |
最通用的工业协议 |
| Modbus RTU | modbus-serial + serialport |
串口通信支持 |
| OPC UA | node-opcua |
高级数据互通标准 |
| MQTT | mqtt.js |
物联网/云平台连接 |
| Siemens S7 | nodes7 / plc.js |
西门子PLC直连 |
FUXA等成熟开源项目已验证了上述驱动在Node.js生态中的可行性——其设备连接层支持OPC UA、Modbus、MQTT、西门子S7等多种工业协议,并通过Node.js后端实现与PLC、传感器等设备的实时数据交互。
数据存储:
| 场景 | 方案 | 说明 |
|---|---|---|
| 时序数据 | InfluxDB / TimescaleDB | 历史趋势和报警归档 |
| 配置数据 | PostgreSQL / MySQL | 用户、权限、配方、工程文件 |
| 缓存/会话 | Redis | 实时数据缓存、会话管理 |
| 本地轻量 | SQLite | 单机部署方案 |
2.3 打包部署方案
Web部署(标准模式):
前端Vue3项目通过Vite打包为静态资源,部署至Nginx或CDN;后端Node.js服务通过PM2进程守护运行,支持Docker容器化部署。FUXA即采用此模式,支持Windows/Linux/Docker跨平台部署。
桌面端部署(Tauri模式):
对于需要在嵌入式或工控机上独立运行的场景,可采用Tauri方案。Tauri使用系统原生WebView渲染前端,Rust编写后端,打包体积小(<10MB)、内存占用低。已有专为工业HMI应用打造的Vue3 + Tauri库,提供开箱即用的Modbus通信指令、数据操作API和工业级UI组件。参考架构:前端Vue3通过invoke调用Tauri后端的Rust命令,Rust后端封装Modbus/OPC UA等协议的通信逻辑。
三、系统架构设计
3.1 分层架构总览
┌─────────────────────────────────────────────────────────┐
│ 展示交互层(Presentation) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 监控画面 │ │ 报警管理 │ │ 趋势分析 │ │ 报表中心 │ │
│ │ (组态渲染)│ │ (实时推送)│ │ (ECharts)│ │ (导出) │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Web组态编辑器(拖拽设计、SVG组件库) │ │
│ └────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────┤
│ 业务逻辑层(Application) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 用户权限 │ │ 报警引擎 │ │ 配方管理 │ │ 脚本引擎 │ │
│ │ (RBAC) │ │ (规则判断)│ │ (版本控制)│ │ (JS沙箱) │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
├─────────────────────────────────────────────────────────┤
│ 数据服务层(Data Service) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 实时数据 │ │ 历史归档 │ │ 数据缓存 │ │ API网关 │ │
│ │ (推送) │ │ (压缩存储)│ │ (Redis) │ │ (REST) │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
├─────────────────────────────────────────────────────────┤
│ 设备接入层(Device Connectivity) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Modbus │ │ OPC UA │ │ MQTT │ │ S7 │ │
│ │ TCP/RTU │ │ Client │ │ Broker │ │ Client │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 协议适配器(Protocol Adapter) │ │
│ │ 统一数据模型抽象,新协议热插拔 │ │
│ └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
3.2 数据流设计
实时数据上行(设备→HMI):
PLC/传感器 → 协议驱动 → 数据解析 → Redis缓存 + InfluxDB归档
↓
WebSocket推送 → Vue3响应式更新 → 画面刷新
控制指令下行(HMI→设备):
用户操作 → 权限校验 → 指令队列 → 协议驱动 → 设备执行
↓
操作日志 → 审计追踪
3.3 目录结构规划
web-hmi-platform/
├── packages/
│ ├── frontend/ # Vue3前端
│ │ ├── src/
│ │ │ ├── views/ # 画面(监控、报警、趋势、报表)
│ │ │ ├── components/ # 公共组件
│ │ │ │ ├── hmi/ # HMI核心组件(仪表、按钮、指示灯)
│ │ │ │ ├── charts/ # 图表组件(趋势图、仪表盘)
│ │ │ │ └── editor/ # 组态编辑器组件
│ │ │ ├── composables/ # 组合式函数(useWebSocket、useTag等)
│ │ │ ├── stores/ # Pinia状态管理
│ │ │ ├── router/ # 路由配置
│ │ │ ├── assets/ # 静态资源与SVG图元库
│ │ │ └── types/ # TypeScript类型定义
│ │ └── vite.config.ts
│ │
│ ├── backend/ # Node.js后端
│ │ ├── src/
│ │ │ ├── modules/
│ │ │ │ ├── device/ # 设备管理(协议驱动、连接池)
│ │ │ │ ├── tag/ # 标签管理(变量定义、映射)
│ │ │ │ ├── alarm/ # 报警引擎
│ │ │ │ ├── recipe/ # 配方管理
│ │ │ │ ├── history/ # 历史数据服务
│ │ │ │ ├── auth/ # 认证授权
│ │ │ │ └── report/ # 报表服务
│ │ │ ├── common/ # 公共工具、中间件、守卫
│ │ │ └── config/ # 配置文件
│ │ └── package.json
│ │
│ └── shared/ # 前后端共享类型定义
│ └── types/
│
├── docker/ # Docker部署配置
├── docs/ # 技术文档
└── package.json # Monorepo根配置

四、完整功能模块需求
4.1 功能模块全景图
本全功能HMI系统涵盖以下十二大功能模块:
| 模块 | 核心功能 | 优先级 |
|---|---|---|
| 组态编辑器 | 可视化画面设计、拖拽布局、SVG图元库、属性绑定 | P0 |
| 实时监控 | 设备状态、过程变量、动态画面、动画效果 | P0 |
| 报警管理 | 报警规则、分级分类、实时推送、确认处理、归档查询 | P0 |
| 趋势分析 | 实时趋势、历史趋势、多曲线对比、缩放与导出 | P0 |
| 设备通信 | 多协议驱动、连接管理、断线重连、数据映射 | P0 |
| 用户权限 | 角色管理、操作权限、电子签名、审计追踪 | P1 |
| 配方管理 | 配方编辑、版本管理、下发与上传、对比 | P1 |
| 报表中心 | 模板设计、定时生成、多格式导出、自动推送 | P1 |
| 数据归档 | 历史数据存储、压缩、查询、备份恢复 | P1 |
| 系统管理 | 工程管理、备份恢复、系统日志、许可管理 | P1 |
| 移动适配 | 响应式布局、移动端APP壳、消息推送 | P2 |
| 扩展集成 | REST API、OPC UA Server、MQTT桥接、第三方对接 | P2 |
4.2 各模块详细需求
4.2.1 组态编辑器(核心差异化模块)
组态编辑器是Web HMI区别于简单大屏的关键——使用者无需编码即可创建监控画面。
功能需求:
- 图元库管理: 提供100+工业标准图元(阀门、泵、管道、电机、储罐、传感器、仪表盘),支持SVG格式自定义导入
- 拖拽式布局: 画布上自由拖拽、缩放、旋转、对齐、分布组件,支持标尺、网格吸附、图层管理、多选(鼠标框选)、CTRL+C/V复制粘贴等精确操控能力
- 属性面板: 选中组件后在右侧面板调整颜色、大小、动画、数据绑定等属性
- 数据绑定: 将图元的颜色、位置、文本、可见性等属性与后端标签(Tag)动态绑定,颜色随设备状态变化
- 交互配置: 为组件配置点击事件——画面跳转、弹出窗口、远程开门/开关阀门等控制指令下发(操作前需进行权限校验与合法性验证)
- 画面管理: 支持多层级画面结构(总览→区域→设备→参数)、画面模板复用
- 保存与导出: 支持将设计保存为JSON文件或导出为SVG格式
参考开源实现phynos/WebTopo,其组件系统无缝集成了传统DOM组件(文本、图片)、自定义Canvas组件(矩形、箭头)和外部可视化库(如ECharts),所有组件在同一可编辑画布内协同工作。
4.2.2 实时监控
- 支持毫秒级至100ms级数据刷新,通过WebSocket实现服务端主动推送
- 画面自适应不同分辨率(1920×1080大屏至1024×768工控机),采用动态屏幕适配方案
- 导航逻辑支持总览→区域→设备→参数的逐级钻取
- 画面间切换动画过渡,操作反馈视觉提示
4.2.3 报警管理
- 支持多种报警类型:限值报警(高/高高/低/低低)、偏差报警、变化率报警、开关量报警
- 报警级别至少4级(紧急/严重/警告/提示)
- 报警发生时自动弹窗、语音播报、画面闪烁、邮件/短信推送
- 报警确认支持单条确认和批量确认,确认操作关联用户信息和时间戳
- 报警归档支持按时间、级别、设备等维度多条件组合查询
4.2.4 设备通信
- 支持同时连接多个不同类型设备,每个设备独立处理循环,互不阻塞
- 连接池管理:每个设备独立连接,支持自动重连、故障恢复、心跳检测
- 统一标签(Tag)数据模型:屏蔽底层协议差异,上层只读写标签
- 标签属性包括:名称、地址(协议相关)、数据类型、读写权限、采集周期、缩放系数、死区等
- 事件驱动架构实现实时状态更新和数据同步
4.2.5 用户权限与安全
- RBAC角色权限模型:至少支持操作员、工艺员、管理员三级角色
- 关键操作(配方修改、参数变更、报警确认)需电子签名
- 操作日志满足审计追踪要求
- 通信安全:HTTPS/WSS加密、TLS证书认证
- API接口需进行身份认证与权限校验
五、全栈开发具体实施步骤
5.1 阶段划分总览
| 阶段 | 周期(参考) | 里程碑交付 |
|---|---|---|
| 第一阶段:基础框架 | 第1-3周 | 项目脚手架、前后端通信、数据库模型、基础UI布局 |
| 第二阶段:设备通信 | 第4-6周 | Modbus/OPC UA驱动集成、标签管理、实时数据推送 |
| 第三阶段:组态编辑器 | 第7-10周 | 图元库、拖拽画布、属性绑定、画面管理 |
| 第四阶段:核心功能 | 第11-14周 | 报警引擎、趋势曲线、配方管理、用户权限 |
| 第五阶段:完善增强 | 第15-17周 | 报表中心、数据归档、移动适配、性能优化 |
| 第六阶段:测试部署 | 第18-20周 | 集成测试、压力测试、文档编写、生产部署 |
5.2 第一阶段:基础框架搭建
步骤1:项目初始化
# 创建Monorepo根目录
mkdir web-hmi-platform && cd web-hmi-platform
pnpm init
# 创建前端项目
pnpm create vite packages/frontend --template vue-ts
cd packages/frontend
pnpm add vue-router@4 pinia axios dayjs element-plus echarts
pnpm add -D @types/node sass unplugin-auto-import unplugin-vue-components
# 创建后端项目
mkdir -p packages/backend/src
cd packages/backend
pnpm init
pnpm add express socket.io cors helmet jsonwebtoken bcryptjs
pnpm add better-sqlite3 sequelize pg ioredis
pnpm add modbus-serial node-opcua mqtt
pnpm add -D typescript @types/node @types/express ts-node nodemon
npx tsc --init
步骤2:前端基础架构
在packages/frontend/src/下建立核心结构——配置Vue Router画面导航、Pinia全局状态管理、Axios HTTP封装、以及基于Element Plus的主布局框架(侧边栏画面导航 + 顶部状态栏 + 主内容区)。同时定义共享TypeScript类型文件(Tag、Alarm、Device等核心实体类型)。
步骤3:后端基础架构
搭建Express/NestJS服务框架,实现WebSocket服务(基于Socket.IO)、JWT认证中间件、统一响应格式封装和全局错误处理。配置数据库连接(开发环境使用SQLite,生产环境切换PostgreSQL)。
5.3 第二阶段:设备通信层
这是HMI系统的核心基础。设备通信层负责与PLC、传感器等工业设备建立连接、收发数据。
步骤1:协议适配器架构设计
定义IProtocolAdapter接口,统一不同协议的接入方式:
// packages/backend/src/modules/device/protocols/adapter.interface.ts
export interface IProtocolAdapter {
readonly protocol: string
connect(config: DeviceConfig): Promise<void>
disconnect(): void
read(tags: TagDefinition[]): Promise<Record<string, any>>
write(tagName: string, value: any): Promise<void>
isConnected(): boolean
onData(callback: (data: Record<string, any>) => void): void
onError(callback: (error: Error) => void): void
}
步骤2:实现Modbus TCP驱动
// packages/backend/src/modules/device/protocols/modbus-tcp.adapter.ts
import ModbusRTU from 'modbus-serial'
import { IProtocolAdapter } from './adapter.interface'
import { EventEmitter } from 'events'
export class ModbusTCPAdapter extends EventEmitter implements IProtocolAdapter {
readonly protocol = 'modbus-tcp'
private client: ModbusRTU
private pollingTimer: NodeJS.Timeout | null = null
private connected = false
constructor() {
super()
this.client = new ModbusRTU()
}
async connect(config: DeviceConfig): Promise<void> {
await this.client.connectTCP(config.host, { port: config.port || 502 })
this.client.setID(config.unitId || 1)
this.client.setTimeout(3000)
this.connected = true
}
async read(tags: TagDefinition[]): Promise<Record<string, any>> {
const results: Record<string, any> = {}
for (const tag of tags) {
const addr = parseInt(tag.address)
if (tag.registerType === 'holding') {
const { data } = await this.client.readHoldingRegisters(addr, tag.length || 1)
results[tag.name] = this.convertValue(data, tag.dataType)
} else if (tag.registerType === 'coil') {
const { data } = await this.client.readCoils(addr, 1)
results[tag.name] = data[0] === 1
}
}
return results
}
async write(tagName: string, value: any): Promise<void> {
const tag = this.getTagDefinition(tagName)
const addr = parseInt(tag.address)
if (tag.registerType === 'holding') {
await this.client.writeRegister(addr, value)
} else if (tag.registerType === 'coil') {
await this.client.writeCoil(addr, value ? 1 : 0)
}
}
async disconnect(): Promise<void> {
if (this.pollingTimer) clearInterval(this.pollingTimer)
this.client.close(() => {})
this.connected = false
}
isConnected(): boolean { return this.connected }
startPolling(tags: TagDefinition[], interval: number = 500): void {
this.pollingTimer = setInterval(async () => {
try {
const data = await this.read(tags)
this.emit('data', data)
} catch (err) {
this.emit('error', err)
}
}, interval)
}
}
步骤3:标签管理与实时数据推送
建立标签配置数据库模型,启动时加载所有设备配置并建立连接。设备读取数据后,数据进入Redis缓存层(热数据),同时通过WebSocket推送到前端。前端通过useWebSocket组合式函数接收数据,更新Pinia Store中的实时数据,画面中的绑定组件自动响应式刷新。
5.4 第三阶段:组态编辑器
步骤1:图元组件库
创建基于SVG的工业图元组件库,每种图元是一个Vue组件,接收统一的props(value值决定颜色/状态、animated控制动画、label标签文本):
// 图元组件接口
interface SymbolProps {
uid: string
value: any
config: SymbolConfig // 颜色、动画、缩放等配置
interactive: boolean // 编辑模式/运行模式
events?: SymbolEvents // 点击等交互事件
}
步骤2:画布编辑器
画布使用CSS Transform实现缩放和平移。使用vue-draggable-resizable或自行实现组件的拖拽、缩放、旋转手柄,支持标尺辅助线和网格吸附。图元面板从组件库中加载所有可用图元,拖入画布后生成组件实例。属性面板根据选中组件类型动态渲染编辑表单(颜色选择器、尺寸调整、数据绑定配置)。
步骤3:数据绑定与画面保存
在属性面板中配置数据绑定:选择标签→选择属性→选择变换规则(如值>80时红色,否则绿色)。编辑完成的画面序列化为JSON格式存储到后端,JSON格式包含画布尺寸、背景、组件列表及其属性。
5.5 第四阶段:核心功能开发
报警引擎:
后端定义报警规则模型(Tag引用、条件类型、阈值、级别、消息模板)。数据采集后,每条数据经过规则引擎评估,命中规则时生成报警记录(报警时间、级别、Tag名称、当前值、消息、确认状态),报警记录写入数据库并通过WebSocket实时推送到前端。
趋势曲线:
后端提供历史数据查询API——按标签和时间范围查询时序数据,支持聚合降采样(原始数据→平均值/最大值/最小值,适用于长期数据查询)。前端使用ECharts渲染,支持实时趋势滚动刷新和历史趋势时间范围选择、多Y轴、缩放漫游。
配方管理:
数据库设计配方模型(配方ID、名称、版本、创建时间、参数列表JSON)。前端提供配方编辑器(表格形式编辑参数→命名→保存),配方下发时逐条将参数值写入对应PLC地址。
用户权限:
实现JWT认证(登录颁发Token、请求验证Token、过期刷新),RBAC权限模型(定义角色→为角色分配画面和操作权限→创建用户时关联角色→前端根据权限控制菜单和按钮显隐),关键操作写入审计日志。
5.6 第五阶段:完善增强
- 报表中心: 使用模板引擎(如EJS/Pug)定义报表模板→查询数据→渲染HTML→通过Puppeteer生成PDF→定时任务自动发送邮件
- 移动适配: 使用CSS媒体查询和弹性布局适配移动端尺寸;通过Capacitor打包为Android/iOS APP
- 性能优化: 大量标签采用虚拟滚动渲染画面;使用Web Worker处理数据转换;为设备数据添加缓存层
5.7 第六阶段:测试与部署
测试策略:
| 测试类型 | 工具 | 覆盖范围 |
|---|---|---|
| 单元测试 | Vitest + Jest | 工具函数、协议解析、报警引擎逻辑 |
| 组件测试 | Vue Test Utils | 图元组件、表单组件 |
| E2E测试 | Playwright | 画面导航、报警流程、配方下发 |
| 性能测试 | k6 / wrk | WebSocket并发连接、数据推送吞吐 |
部署方案:
- 单机部署: PM2进程守护 + Nginx反向代理
- Docker部署: 编写docker-compose.yml,含前端Nginx容器、后端Node.js容器、Redis容器、PostgreSQL容器、InfluxDB容器
- 边缘部署: 前端打包为Tauri桌面应用,后端使用SQLite,整体打包为单个可执行文件
六、全栈开发示例:温度监控场景从零到运行
6.1 场景描述
某注塑机需要监控三个温度点(料筒温度、喷嘴温度、模具温度),每个温度点设置高报警阈值。操作人员需要在HMI上实时查看温度数值和趋势曲线,超温时自动弹窗报警并记录。
6.2 后端实现
定义标签和报警规则:
// packages/backend/src/modules/tag/tag.manager.ts
import { EventEmitter } from 'events'
interface TagConfig {
name: string; description: string; deviceId: string
address: string; dataType: string; registerType: string
scanRate: number; alarmHigh?: number; alarmHighHigh?: number
}
interface AlarmRecord {
id: string; tagName: string; alarmType: string
alarmLevel: string; currentValue: number; threshold: number
message: string; timestamp: Date; acknowledged: boolean
}
export class TagManager extends EventEmitter {
private tags: Map<string, TagConfig> = new Map()
private values: Map<string, any> = new Map()
private alarms: AlarmRecord[] = []
// ========== 标签配置管理 ==========
registerTag(config: TagConfig): void {
this.tags.set(config.name, config)
console.log(`[TagManager] 标签已注册: ${config.name} (${config.address})`)
}
getTagConfig(name: string): TagConfig | undefined {
return this.tags.get(name)
}
getAllTags(): TagConfig[] {
return Array.from(this.tags.values())
}
// ========== 实时数据更新与报警评估 ==========
updateValue(tagName: string, value: any): AlarmRecord | null {
const tag = this.tags.get(tagName)
if (!tag) return null
this.values.set(tagName, value)
// 评估报警规则
let alarmRecord: AlarmRecord | null = null
if (tag.alarmHighHigh && value >= tag.alarmHighHigh) {
alarmRecord = this.createAlarm(tag, 'highHigh', value, tag.alarmHighHigh)
} else if (tag.alarmHigh && value >= tag.alarmHigh) {
alarmRecord = this.createAlarm(tag, 'high', value, tag.alarmHigh)
}
if (alarmRecord) {
this.alarms.push(alarmRecord)
this.emit('alarm', alarmRecord)
}
return alarmRecord
}
getValue(tagName: string): any {
return this.values.get(tagName) ?? null
}
getAllValues(): Record<string, any> {
const result: Record<string, any> = {}
this.values.forEach((v, k) => { result[k] = v })
return result
}
// ========== 报警处理 ==========
private createAlarm(tag: TagConfig, type: string, value: number, threshold: number): AlarmRecord {
return {
id: `ALM-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
tagName: tag.name,
alarmType: type,
alarmLevel: type === 'highHigh' ? '紧急' : '警告',
currentValue: value,
threshold,
message: `${tag.description}: ${value} (阈值${threshold})`,
timestamp: new Date(),
acknowledged: false,
}
}
acknowledgeAlarm(alarmId: string, userId: string): boolean {
const alarm = this.alarms.find(a => a.id === alarmId)
if (!alarm) return false
alarm.acknowledged = true
this.emit('alarm-acknowledged', { alarmId, userId })
return true
}
getAlarms(filter?: { acknowledged?: boolean; level?: string; startDate?: Date; endDate?: Date }): AlarmRecord[] {
let result = [...this.alarms]
if (filter?.acknowledged !== undefined) {
result = result.filter(a => a.acknowledged === filter.acknowledged)
}
if (filter?.level) {
result = result.filter(a => a.alarmLevel === filter.level)
}
if (filter?.startDate) {
result = result.filter(a => a.timestamp >= filter.startDate!)
}
if (filter?.endDate) {
result = result.filter(a => a.timestamp <= filter.endDate!)
}
return result.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())
}
}
WebSocket服务端推送:
// packages/backend/src/modules/websocket/ws.gateway.ts
import { Server as SocketIOServer } from 'socket.io'
import { createServer } from 'http'
import { TagManager } from '../tag/tag.manager'
import { ModbusTCPAdapter } from '../device/protocols/modbus-tcp.adapter'
export function createHMIWebSocketServer(httpServer: any) {
const io = new SocketIOServer(httpServer, {
cors: { origin: '*', methods: ['GET', 'POST'] },
pingInterval: 10000,
pingTimeout: 5000,
})
const tagManager = new TagManager()
// 注册温度标签
tagManager.registerTag({ name: 'barrel_temp', description: '料筒温度', deviceId: 'injection_molding', address: '300', dataType: 'float32', registerType: 'holding', scanRate: 500, alarmHigh: 280, alarmHighHigh: 320 })
tagManager.registerTag({ name: 'nozzle_temp', description: '喷嘴温度', deviceId: 'injection_molding', address: '302', dataType: 'float32', registerType: 'holding', scanRate: 500, alarmHigh: 260, alarmHighHigh: 300 })
tagManager.registerTag({ name: 'mold_temp', description: '模具温度', deviceId: 'injection_molding', address: '304', dataType: 'float32', registerType: 'holding', scanRate: 500, alarmHigh: 120, alarmHighHigh: 160 })
// 监听报警
tagManager.on('alarm', (alarm) => {
console.log(`[告警] ${alarm.message}`)
io.emit('alarm:new', alarm)
})
io.on('connection', (socket) => {
console.log(`[WebSocket] 客户端已连接: ${socket.id}`)
// 订阅实时数据(客户端按需订阅具体标签)
socket.on('subscribe:tags', (tagNames: string[]) => {
const room = `tags:${tagNames.sort().join(',')}`
socket.join(room)
})
// 报警确认
socket.on('alarm:acknowledge', (data: { alarmId: string; userId: string }) => {
const success = tagManager.acknowledgeAlarm(data.alarmId, data.userId)
if (success) {
io.emit('alarm:updated', { alarmId: data.alarmId, acknowledged: true })
}
})
socket.on('disconnect', () => {
console.log(`[WebSocket] 客户端已断开: ${socket.id}`)
})
})
// Modbus模拟数据轮询(实际对接真实PLC)
setInterval(async () => {
const simulatedData = {
barrel_temp: 250 + Math.random() * 60,
nozzle_temp: 240 + Math.random() * 50,
mold_temp: 90 + Math.random() * 40,
}
for (const [name, value] of Object.entries(simulatedData)) {
tagManager.updateValue(name, Math.round(value * 10) / 10)
}
io.emit('tags:update', tagManager.getAllValues())
}, 500)
console.log('[WebSocket] HMI WebSocket服务已启动')
return { io, tagManager }
}
6.3 前端实现
WebSocket通信层封装:
// packages/frontend/src/composables/useWebSocket.ts
import { io, Socket } from 'socket.io-client'
import { ref, onMounted, onUnmounted } from 'vue'
import type { AlarmRecord } from '@shared/types'
export function useHMIWebSocket(serverUrl: string = 'http://localhost:3000') {
const socket = ref<Socket | null>(null)
const connected = ref(false)
const tagValues = ref<Record<string, number>>({})
const alarms = ref<AlarmRecord[]>([])
const unacknowledgedCount = ref(0)
let reconnectTimer: ReturnType<typeof setTimeout> | null = null
function connect() {
socket.value = io(serverUrl, {
transports: ['websocket'],
reconnection: true,
reconnectionDelay: 1000,
reconnectionAttempts: 10,
})
socket.value.on('connect', () => {
connected.value = true
console.log('[HMI] WebSocket已连接')
})
socket.value.on('tags:update', (data: Record<string, number>) => {
tagValues.value = { ...data }
})
socket.value.on('alarm:new', (alarm: AlarmRecord) => {
alarms.value.unshift(alarm)
unacknowledgedCount.value = alarms.value.filter(a => !a.acknowledged).length
})
socket.value.on('alarm:updated', (data: { alarmId: string; acknowledged: boolean }) => {
const alarm = alarms.value.find(a => a.id === data.alarmId)
if (alarm) alarm.acknowledged = data.acknowledged
unacknowledgedCount.value = alarms.value.filter(a => !a.acknowledged).length
})
socket.value.on('disconnect', () => { connected.value = false })
}
function subscribeTags(tagNames: string[]) {
socket.value?.emit('subscribe:tags', tagNames)
}
function acknowledgeAlarm(alarmId: string, userId: string) {
socket.value?.emit('alarm:acknowledge', { alarmId, userId })
}
onMounted(() => { connect() })
onUnmounted(() => {
if (reconnectTimer) clearTimeout(reconnectTimer)
socket.value?.disconnect()
})
return { connected, tagValues, alarms, unacknowledgedCount, subscribeTags, acknowledgeAlarm }
}
Pinia Store:
// packages/frontend/src/stores/hmi.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { useHMIWebSocket } from '@/composables/useWebSocket'
export const useHMIStore = defineStore('hmi', () => {
const { connected, tagValues, alarms, unacknowledgedCount, subscribeTags, acknowledgeAlarm } = useHMIWebSocket()
// 订阅需要的标签
subscribeTags(['barrel_temp', 'nozzle_temp', 'mold_temp'])
// 各温度点的计算属性
const barrelTemp = computed(() => tagValues.value.barrel_temp ?? '--')
const nozzleTemp = computed(() => tagValues.value.nozzle_temp ?? '--')
const moldTemp = computed(() => tagValues.value.mold_temp ?? '--')
// 温度状态判断
function getTempStatus(temp: number, high: number, highHigh: number): 'normal' | 'warning' | 'critical' {
if (temp >= highHigh) return 'critical'
if (temp >= high) return 'warning'
return 'normal'
}
const barrelStatus = computed(() => getTempStatus(Number(barrelTemp.value), 280, 320))
const nozzleStatus = computed(() => getTempStatus(Number(nozzleTemp.value), 260, 300))
const moldStatus = computed(() => getTempStatus(Number(moldTemp.value), 120, 160))
return {
connected, barrelTemp, nozzleTemp, moldTemp,
barrelStatus, nozzleStatus, moldStatus,
alarms, unacknowledgedCount, acknowledgeAlarm,
}
})
温度监控画面核心组件示例:
<!-- packages/frontend/src/views/TemperatureMonitor.vue -->
<template>
<div class="temperature-monitor">
<!-- 顶部状态栏 -->
<div class="status-bar">
<span class="connection-status" :class="{ connected: store.connected }">
{{ store.connected ? '● 已连接' : '○ 未连接' }}
</span>
<el-badge :value="store.unacknowledgedCount" :hidden="store.unacknowledgedCount === 0">
<el-button type="danger" size="small" @click="showAlarmPanel = true">
🚨 报警
</el-button>
</el-badge>
</div>
<!-- 温度仪表盘 -->
<div class="temp-panel">
<TempGauge
title="料筒温度"
:value="store.barrelTemp"
unit="°C"
:min="0" :max="400"
:warning="280" :critical="320"
:status="store.barrelStatus"
/>
<TempGauge
title="喷嘴温度"
:value="store.nozzleTemp"
unit="°C"
:min="0" :max="400"
:warning="260" :critical="300"
:status="store.nozzleStatus"
/>
<TempGauge
title="模具温度"
:value="store.moldTemp"
unit="°C"
:min="0" :max="200"
:warning="120" :critical="160"
:status="store.moldStatus"
/>
</div>
<!-- 报警弹窗 -->
<el-dialog v-model="showAlarmPanel" title="实时报警" width="700px">
<el-table :data="store.alarms" max-height="400" stripe>
<el-table-column prop="timestamp" label="时间" width="170" />
<el-table-column prop="tagName" label="标签" width="100" />
<el-table-column prop="message" label="报警信息" />
<el-table-column prop="alarmLevel" label="级别" width="70">
<template #default="{ row }">
<el-tag :type="row.alarmLevel === '紧急' ? 'danger' : 'warning'" size="small">
{{ row.alarmLevel }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="状态" width="80">
<template #default="{ row }">
<span :style="{ color: row.acknowledged ? '#67c23a' : '#f56c6c' }">
{{ row.acknowledged ? '已确认' : '未确认' }}
</span>
</template>
</el-table-column>
<el-table-column label="操作" width="80">
<template #default="{ row }">
<el-button v-if="!row.acknowledged" type="primary" size="small" @click="store.acknowledgeAlarm(row.id, 'operator')">确认</el-button>
</template>
</el-table-column>
</el-table>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useHMIStore } from '@/stores/hmi'
import TempGauge from '@/components/hmi/TempGauge.vue'
const store = useHMIStore()
const showAlarmPanel = ref(false)
</script>
<style scoped>
.temperature-monitor {
height: 100vh;
background: #0a1628;
color: #e0e0e0;
}
.status-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 24px;
background: rgba(255, 255, 255, 0.05);
}
.connection-status { font-size: 14px; color: #f56c6c; }
.connection-status.connected { color: #67c23a; }
.temp-panel {
display: flex;
justify-content: center;
gap: 40px;
padding: 60px 20px;
flex-wrap: wrap;
}
</style>
七、与竞品分析对比及脱颖而出策略
7.1 竞品矩阵分析
当前工业Web HMI市场存在以下几类竞争方案:
7.1.1 商业HMI/SCADA平台
| 竞品 | 定位 | 技术栈 | 核心优势 | 核心劣势 | 典型价格 |
|---|---|---|---|---|---|
| 西门子WinCC Unified | 高端全集成 | HTML5/SVG/JS (Web) | 全集成自动化、TIA Portal一体化、工业级可靠性 | 封闭生态、授权费用高、需要西门子PLC配合 | 数万-数十万 |
| 力控FWebSCADA | 国产SCADA云平台 | Web组态+SmartDB | 采集/存储/服务一体化、支持OPC UA/Modbus、国产信创 | 商业授权、定制化受限 | 按点收费 |
| N3uron Web Vision | 纯Web HMI/SCADA | JS/HTML5/CSS | 标签/连接/设备数无限制、跨全设备类型、纯Web无插件 | 按模块收费、中小企业门槛高 | 按模块订阅 |
| HELIO | Web HMI开发平台 | React+Rust+WASM | 强大TypeScript SDK、响应式UI、OPC UA/MQTT原生支持 | 闭源商业产品、React生态绑定 | 商业授权 |
与商业平台对比,自研方案的核心优势:
- 零授权费用: 商业HMI通常按点数或模块收费,1000个标签点的授权费可达数万至数十万元
- 完全定制化: 商业平台的UI风格、交互逻辑、功能边界受限于厂商设计,自研方案可根据行业需求深度定制
- 集成灵活性: 自研方案可无缝对接企业现有IT系统,不受厂商SDK和API边界的限制
7.1.2 开源HMI/SCADA框架
| 竞品 | 定位 | 技术栈 | 核心优势 | 核心劣势 |
|---|---|---|---|---|
| FUXA | 开源Web SCADA/HMI | Vue.js + Node.js | 低代码可视化配置、多协议支持(Modbus/OPC UA/MQTT)、跨平台部署、社区活跃 | 核心开发者主导、定制度受限于框架架构、界面风格偏传统 |
| ISM Web组态 | 开源IoT组态系统 | Vue.js + Go | 1000+工业组件、多协议支持(Modbus/OPC UA/S7)、终身免费 | Go后端增加技术栈复杂度、社区规模较小 |
| WebTopo | 开源Web组态编辑器 | Vue.js + Quasar | SVG自由缩放、拖拽设计、组件系统融合DOM/Canvas/ECharts | 侧重编辑器本身、缺乏完整HMI后端 |
FUXA基于Web的HMI/SCADA开源方案,其交互逻辑和扩展方式受限于框架架构。FUXA的功能边界由核心团队决定,二次开发需要深入理解其内部代码结构和设计模式。
7.1.3 无代码WebHMI平台
无代码平台支持跨平台访问,通过拖拽式操作几分钟内即可完成界面设计,无需购买昂贵的硬件设备。但其通用化设计难以适配特定行业的深度需求(如制药行业的电子签名和审计追踪、电力行业的IEC 61850协议),复杂交互逻辑(如多设备联动控制、条件分支操作)在无代码平台中实现困难,高度依赖特定云平台存在供应商锁定风险。
7.1.4 传统嵌入式HMI
传统HMI的根本缺陷在于封闭的架构——依赖专用硬件终端、需要安装特定运行环境和驱动、缺乏原生远程访问能力、功能升级或Bug修复必须派遣工程师到客户现场手动操作、设备数据成为“孤岛”。
7.2 Vue3+Node.js自研方案的差异化优势总览
| 维度 | Vue3+Node.js自研 | 商业HMI平台 | 开源框架 | 无代码平台 | 传统嵌入式HMI |
|---|---|---|---|---|---|
| 授权费用 | ✅ 零授权费 | ❌ 数万-数十万 | ✅ 免费 | ⚠️ 按量收费 | ❌ 硬件绑定 |
| 代码掌控 | ✅ 100%自主 | ❌ 黑盒 | ⚠️ 需深入理解 | ❌ 闭源 | ❌ 闭源 |
| 定制深度 | ✅ 无限制 | ⚠️ 受限于SDK | ⚠️ 受限于架构 | ❌ 通用化设计 | ❌ 厂商锁定 |
| 跨平台 | ✅ 全端覆盖 | ✅ Web | ✅ Web | ✅ Web | ❌ 单设备 |
| 远程运维 | ✅ 原生支持 | ✅ | ✅ | ✅ | ❌ 需额外方案 |
| IT/OT融合 | ✅ API自由设计 | ⚠️ 有限接口 | ⚠️ 有限 | ⚠️ 受限于平台 | ❌ 封闭 |
| AI集成 | ✅ 自由接入 | ⚠️ 受限 | ❌ 不支持 | ❌ 不支持 | ❌ 不支持 |
| 技术门槛 | ⚠️ 需全栈团队 | ✅ 低(组态) | ⚠️ 中等 | ✅ 极低 | ✅ 低 |
| 维护成本 | ✅ 自主可控 | ⚠️ 依赖厂商 | ⚠️ 依赖社区 | ⚠️ 依赖平台 | ✅ 低 |
7.3 如何脱颖而出:六大差异化策略
策略一:AI驱动的智能运维集成
在所有竞品方案中,AI原生集成是自研方案最独特的差异化优势。可直接在后端集成TensorFlow.js或ONNX Runtime,实现:
- 预测性报警: 分析历史趋势数据,在温度达到阈值之前就预测异常趋势并提前预警
- 智能操作建议: 根据当前设备状态、历史数据和最佳实践模型,自动给出参数调整建议
- 自然语言查询: 操作员可直接输入“上周三料筒温度最高多少度”进行查询
策略二:深度行业化模板与快速交付
针对特定垂直行业(注塑、包装、暖通、水处理等),预置完整的行业画面模板和报警规则库,实现半天内完成从部署到上线的交付速度。
策略三:原生级数字孪生3D可视化
利用Three.js或Babylon.js实现设备3D模型的Web端实时渲染,模型颜色、动画、数据标注与真实设备状态实时同步。3D视角直观展示设备内部结构,快速定位故障点;在虚拟环境中模拟操作指令效果后再下发,降低操作风险。
策略四:极致开放的多协议互联
自研方案的协议适配器支持热插拔式扩展,每个新协议驱动只需实现标准接口即可接入系统。随着工业互联网对统一数据互通标准的要求提升,OPC UA等开放协议的支持愈发重要。
策略五:数据驱动的增值服务
- 自动核算设备综合效率(OEE)并以图表直观呈现
- 可配置报表模板并定时邮件推送
- 设备健康度评分与主动维保建议
策略六:开发者生态与社区运营
开放组态编辑器的组件SDK,允许第三方开发者贡献图元组件和协议驱动。建立组件市场和模板市场,形成开发者社区生态。提供完善的REST API和Webhook机制,便于系统集成商二次开发。
八、总结与建议
8.1 方案适用场景
Vue3 + Node.js全栈自研Web HMI方案最适用于:
- 设备制造商: 需要为自有设备配套差异化HMI产品,建立品牌辨识度
- 系统集成商: 项目需求多变,需要灵活定制,且看重长期成本优势
- 中大型制造企业: 有自建IT团队,需要HMI与MES/ERP深度融合
- 有AI/数据分析需求的企业: 需要将预测性维护、OEE分析等能力原生集成到HMI中
8.2 资源投入估算
| 资源 | 最小团队 | 推荐团队 |
|---|---|---|
| 前端工程师(Vue3) | 1人 | 2人 |
| 后端工程师(Node.js) | 1人 | 2人 |
| 工业自动化工程师 | 1人(兼职) | 1人 |
| UI/UX设计师 | 0.5人(兼职) | 1人 |
| QA测试 | 0.5人(兼职) | 1人 |
8.3 风险与应对
| 风险 | 影响 | 应对策略 |
|---|---|---|
| 工业协议兼容性不足 | 高 | 优先实现Modbus和OPC UA;非标协议通过脚本引擎扩展 |
| 实时性能瓶颈 | 中 | WebSocket + 数据分片;大规模场景引入Redis Stream |
| 团队技术门槛 | 中 | 先用FUXA快速验证,再逐步替换核心模块 |
| 长期维护成本 | 中 | Monorepo统一管理、自动化CI/CD、完善文档 |
8.4 最终建议
如果团队具备Vue3和Node.js的全栈能力,建议采用“核心自研 + 生态借力”的渐进策略——关键差异化模块(组态编辑器、报警引擎、AI集成、行业模板)完全自主研发以构建核心壁垒;通用基础能力(协议驱动、数据存储、认证授权)充分利用成熟开源库加速开发;在项目初期可参考FUXA或ISM等开源方案的架构设计思路,但保持代码层面的独立性和可控性。
Web HMI的核心价值在于将传统封闭的工业可视化系统转变为开放、灵活、可定制的平台。Vue3 + Node.js全栈技术方案为这一转变提供了坚实的技术基础——在保证工业级可靠性的前提下,赋予开发者前所未有的自由度与创新能力。
更多推荐

所有评论(0)