Vue3 储能 EMS 前端实战(中):MQTT 实时数据与组态联动
Vue3 储能 EMS 前端实战(中):MQTT 实时数据与组态联动
系列说明:本文是《Vue3 储能 EMS 前端实战》第 2 篇 / 共 3 篇
- 第 1 篇:配置化表单与表格体系
- 本篇:MQTT + HPreview + Maotu 组态
- 第 3 篇:Element Plus 深度定制与工程总结
适用读者:Vue3 开发者、工业物联网前端、需要实现 SCADA 监控大屏的工程师
技术栈:Vue 3.5 · Pinia · MQTT · Maotu 组态 · WebSocket
项目类型:储能云平台 EMS UI
一、EMS 的实时数据挑战
普通 Admin 系统是「用户点查询 → 发 HTTP 请求 → 渲染结果」。EMS 监控大屏则需要:
- 秒级推送设备 SOC、电压、告警状态
- 拓扑图上的每个图元与真实设备属性一一绑定
- 切换站点时,旧订阅必须清理,避免脏数据和内存泄漏
本项目用 MQTT over WebSocket 承载实时通道,用 Maotu SVG 组态 承载可视化层,两者通过 HPreview 组件打通。
二、MQTT 主题规范
项目 MQTT 主题遵循统一规范(见 topic.md):
/ess/{stationId}/{deviceType}/{deviceId}/web/data
/ess/{stationId}/{deviceType}/{deviceId}/web/status
deviceType 示例:5=BMS、6=簇、PCS 等,由后端字典维护。
父主题通配符形式:
/ess/{stationId}/+/+/web/data
/ess/{stationId}/+/+/web/status
三、连接与权限流程
前端不能随意订阅任意 MQTT 主题,Broker 通常有 ACL。本项目采用 先连接 → 再申请权限 → 再订阅 的三段式流程:
// stores/mqtt.ts 核心流程
const connect = async () => {
// 1. 从后端获取 Broker 地址和临时 Token
config.value = await getMqttConfig()
userInfo.value = await getMqttToken()
// 2. 根据页面协议 ws/wss 选择端口
const brokerUrl = `${isWss ? 'wss' : 'ws'}://${host}:${port}/mqtt`
// 3. 连接
await mqttClient.connect(brokerUrl, { ...userInfo.value })
}
// 订阅前先申请权限(后端 ACL 校验)
const applySubscribePermission = async (topic: string) => {
const res = await applyMqttTopicPermission([topic])
return res.allowedTopics?.includes(topic)
}
为什么需要权限申请?
后端根据用户角色返回 allowedTopics 列表,前端只对授权主题执行 subscribe,避免越权订阅。
四、通配符 + 的客户端匹配
后端可能授权父主题 /ess/1/+/+/web/data,实际消息 topic 是 /ess/1/5/101/web/data。
mqtt.js 按精确 topic 路由 handler,不会自动把 + 订阅的消息分发给子 topic 的回调。项目在 mqttClient.ts 做了客户端正则匹配:
this.client.on('message', (topic, message) => {
let handlers = this.messageHandlers.get(topic) || []
// 遍历所有注册的 key,把 + 转成 [\d]+ 做正则匹配
this.messageHandlers.keys().forEach(key => {
if (key.includes('+')) {
let regStr = key.replace(/\+/g, '[\\d]+')
if (new RegExp(regStr).test(topic) && handlers.length === 0) {
handlers.push(...this.messageHandlers.get(key) || [])
}
}
})
handlers.forEach(handler => handler(topic, JSON.parse(message.toString())))
})
踩坑:如果只 subscribe 精确 topic 而不做通配符路由,父主题授权后仍然收不到消息。
源码位置:src/stores/mqtt.ts、src/utils/mqttClient.ts
五、组态预览 HPreview:MQTT + SVG 联动
这是项目最有特色的功能 —— 把 Maotu 组态图和 MQTT 实时数据打通。
5.1 整体流程
5.2 核心代码
// 1. 连接并申请父主题权限
const parentTopic = `/ess/${stationId}/+/+/web/data`
const parentStatusTopic = `/ess/${stationId}/+/+/web/status`
mqttStore.connect().then(async () => {
hasDataPermit.value = await mqttStore.applySubscribePermission(parentTopic)
hasStatusPermit.value = await mqttStore.applySubscribePermission(parentStatusTopic)
})
// 2. MQTT 消息 → 更新 SVG 节点
const handleSvgUpdate = (topic: string, data: any) => {
const nodeInfos = subscribeObj.value[topic]
nodeInfos.forEach((nodeInfo) => {
mtPreviewRef.value?.setItemAttrByID(
nodeInfo.svgNodeId,
'props.text.val',
data[nodeInfo.deviceProp] + ''
)
})
}
// 3. 权限就绪后再订阅具体 topic
watchEffect(() => {
if (mqttStore.isConnected && hasDataPermit.value && hasStatusPermit.value) {
subscribeArr.value.forEach(topic => {
mqttStore.doSubscribe(topic, handleSvgUpdate)
})
}
})
5.3 生命周期管理
切换站点时必须:
unsubscribe旧 topic- 清空
subscribeObj - 重新加载组态 JSON
nextTick后调用setImportJson(Maotu 是条件渲染,v-else挂载前调 API 会空白)- 重新拉取绑定点列表并订阅新 topic
onBeforeUnmount(() => {
subscribeArr.value.forEach(topic => mqttStore.unsubscribe(topic))
})
源码位置:src/components/HPreview/index.vue
六、Maotu 组态编辑器 —— 可视化 SCADA
6.1 技术选型
选用开源库 Maotu(0.6.5),提供:
MtEdit:SVG 画布编辑器MtPreview:只读预览,支持缩放、拖拽、事件回调
6.2 自定义图元自动注册
工业场景需要电池、开关、仪表等专用图元,放在 customComponents/ 目录,Vite glob 自动注册:
const customComponents = import.meta.glob('./customComponents/*.vue', { eager: true })
for (const key in customComponents) {
const name = key.split('/').pop()!.split('.')[0]!
if (!app.component(name)) {
app.component(name, customComponents[key].default)
}
}
新增图元只需 Drop 一个 .vue 文件,无需改注册代码。
6.3 设备绑定点位
编辑器中每个 SVG 元素可绑定真实设备属性(复用 HForm 配置化表单):
const formItems = shallowRef<HFormItem[]>([
{ type: 'select', label: '设备类型', prop: 'deviceType', options: protocol_device_type },
{ type: 'select', label: '设备', prop: 'deviceId', options: deviceList },
{ type: 'select', label: '属性字段', prop: 'fieldId', options: deviceProps },
])
保存到后端 svgnode 表,预览时按绑定关系订阅 MQTT 并更新节点。
6.4 交互跳转
组态图支持点击图元跳转:
const onEventCallBack = (type, id) => {
const { jump_type, jump_url } = info
if (jump_type === 1) {
router.push(jump_url) // 路由跳转
} else if (jump_type === 2) {
// 动态加载组件弹层展示
route.components.default().then(res => {
currentComponent.value = markRaw(res.default)
show.value = true
})
}
}
源码位置:src/components/MtEditor/index.vue、src/components/MtEditor/customComponents/
七、本篇小结
| 模块 | 解决的问题 | 关键技术 |
|---|---|---|
| mqttStore | 连接管理、权限申请 | ws/wss 自适应、Pinia 状态 |
| mqttClient | 通配符 topic 路由 | 正则匹配 + |
| HPreview | 组态 + 实时数据 | watchEffect 订阅、setItemAttrByID |
| MtEditor | 可视化编辑、设备绑定 | import.meta.glob 自动注册 |
MQTT 解决了数据从哪来,Maotu 解决了数据怎么展示。下一篇讲 UI 层:HTabs 指示线、Element Plus 主题定制,以及全项目工程经验总结。
更多推荐


所有评论(0)