优化 mqtt 退出路由 不订阅消息
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react';
|
||||
import {
|
||||
Layout, Card, Row, Col, Statistic, Tag, Button, Table,
|
||||
Typography, Space, Progress, Badge, Timeline, message,
|
||||
@@ -291,7 +291,7 @@ export default function DeviceOverviewPage() {
|
||||
}, [selectedDevice, uavState, persistentHostData, persistentDroneData?.wind_speed]);
|
||||
|
||||
// MQTT消息处理
|
||||
const handleMqttMessage = (topic, message) => {
|
||||
const handleMqttMessage = useCallback((topic, message) => {
|
||||
|
||||
|
||||
try {
|
||||
@@ -398,7 +398,7 @@ export default function DeviceOverviewPage() {
|
||||
} catch (err) {
|
||||
console.error('MQTT解析失败', err);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// MQTT订阅
|
||||
const mqttTopics = useMemo(() => {
|
||||
@@ -417,6 +417,7 @@ export default function DeviceOverviewPage() {
|
||||
url: envConfig.mqtt_url,
|
||||
topic: mqttTopics,
|
||||
onMessage: handleMqttMessage,
|
||||
enabled: mqttTopics.length > 0,
|
||||
});
|
||||
|
||||
// 机器人视频
|
||||
|
||||
@@ -247,7 +247,7 @@ export default function DeviceStatusPage() {
|
||||
};
|
||||
|
||||
// 当MQTT数据更新时,更新持久化数据(只在有新数据时更新)
|
||||
const handleMqttMessage = (topic, message) => {
|
||||
const handleMqttMessage = useCallback((topic, message) => {
|
||||
|
||||
try {
|
||||
const parsedMessage = JSON.parse(message);
|
||||
@@ -355,7 +355,7 @@ export default function DeviceStatusPage() {
|
||||
} catch (err) {
|
||||
console.error('DeviceStatusPage MQTT 消息解析失败:', err);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 根据选中设备动态构建MQTT主题
|
||||
const mqttTopics = useMemo(() => {
|
||||
@@ -377,6 +377,7 @@ export default function DeviceStatusPage() {
|
||||
url: envConfig.mqtt_url,
|
||||
topic: mqttTopics,
|
||||
onMessage: handleMqttMessage,
|
||||
enabled: mqttTopics.length > 0,
|
||||
});
|
||||
|
||||
// 合并无人机和机场数据
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import {
|
||||
Row, Col, Card, Button, Space, Table, Tag, Typography, Progress, Badge,
|
||||
Tabs, Input, Switch, Divider, Tooltip, Timeline, Select, message, DatePicker,
|
||||
@@ -93,7 +93,7 @@ export default function WayLinePage({ onRefresh }) {
|
||||
|
||||
|
||||
// 当MQTT数据更新时,更新持久化数据(只在有新数据时更新)
|
||||
const handleMqttMessage = (topic, message) => {
|
||||
const handleMqttMessage = useCallback((topic, message) => {
|
||||
|
||||
try {
|
||||
const parsedMessage = JSON.parse(message);
|
||||
@@ -201,7 +201,7 @@ export default function WayLinePage({ onRefresh }) {
|
||||
} catch (err) {
|
||||
console.error('WayLinePage MQTT 消息解析失败:', err);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 根据选中设备动态构建MQTT主题
|
||||
const mqttTopics = React.useMemo(() => {
|
||||
@@ -223,6 +223,7 @@ export default function WayLinePage({ onRefresh }) {
|
||||
url: envConfig.mqtt_url,
|
||||
topic: mqttTopics,
|
||||
onMessage: handleMqttMessage,
|
||||
enabled: mqttTopics.length > 0,
|
||||
});
|
||||
|
||||
// 优先使用持久化数据,保持显示稳定
|
||||
|
||||
@@ -1,53 +1,188 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useEffect, useRef, useCallback, useState } from 'react';
|
||||
import mqtt, { MqttClient } from 'mqtt';
|
||||
|
||||
interface Options {
|
||||
url: string;
|
||||
topic: string | string[];
|
||||
onMessage: (topic: string, message: string) => void;
|
||||
/* ============================================================
|
||||
* MQTT 连接池 —— 同一 URL 共享一个连接,避免重复建连
|
||||
* ============================================================ */
|
||||
const connectionPool = new Map<string, { client: MqttClient; refCount: number }>();
|
||||
|
||||
function getOrCreateClient(url: string, options?: mqtt.IClientOptions): MqttClient {
|
||||
const entry = connectionPool.get(url);
|
||||
if (entry) {
|
||||
entry.refCount++;
|
||||
return entry.client;
|
||||
}
|
||||
const client = mqtt.connect(url, {
|
||||
reconnectPeriod: 3000,
|
||||
clean: true,
|
||||
...options,
|
||||
});
|
||||
connectionPool.set(url, { client, refCount: 1 });
|
||||
|
||||
// 连接断开后自动清理池条目
|
||||
client.on('close', () => {
|
||||
const cur = connectionPool.get(url);
|
||||
if (cur && cur.client === client) {
|
||||
connectionPool.delete(url);
|
||||
}
|
||||
});
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
export function useMqtt({ url, topic, onMessage }: Options) {
|
||||
const clientRef = useRef<MqttClient | null>(null);
|
||||
function releaseClient(url: string): void {
|
||||
const entry = connectionPool.get(url);
|
||||
if (!entry) return;
|
||||
entry.refCount--;
|
||||
if (entry.refCount <= 0) {
|
||||
entry.client.end(true);
|
||||
connectionPool.delete(url);
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* useMqtt Hook
|
||||
* ============================================================ */
|
||||
interface UseMqttOptions {
|
||||
/** broker 地址,如 wss://xxx/mqtt */
|
||||
url: string;
|
||||
/** 要订阅的主题(字符串或数组),传空则不订阅 */
|
||||
topic: string | string[];
|
||||
/** 收到消息的回调 */
|
||||
onMessage: (topic: string, message: string) => void;
|
||||
/** 是否启用订阅,默认 true;设为 false 时不会建立连接 */
|
||||
enabled?: boolean;
|
||||
/** MQTT 连接额外配置(可选) */
|
||||
connectOptions?: mqtt.IClientOptions;
|
||||
}
|
||||
|
||||
interface UseMqttReturn {
|
||||
/** 当前连接状态 */
|
||||
connected: boolean;
|
||||
/** 手动发布消息 */
|
||||
publish: (topic: string, message: string) => void;
|
||||
}
|
||||
|
||||
export function useMqtt({
|
||||
url,
|
||||
topic,
|
||||
onMessage,
|
||||
enabled = true,
|
||||
connectOptions,
|
||||
}: UseMqttOptions): UseMqttReturn {
|
||||
const clientRef = useRef<MqttClient | null>(null);
|
||||
const onMessageRef = useRef(onMessage);
|
||||
const [connected, setConnected] = useState(false);
|
||||
|
||||
// 始终保持 onMessage 回调为最新引用,避免闭包过期
|
||||
useEffect(() => {
|
||||
const client = mqtt.connect(url, {
|
||||
reconnectPeriod: 3000,
|
||||
onMessageRef.current = onMessage;
|
||||
}, [onMessage]);
|
||||
|
||||
// 将 topic 统一转为数组,方便做 diff
|
||||
const topicList = (Array.isArray(topic) ? topic : [topic]).filter(Boolean);
|
||||
const topicKey = topicList.join(',');
|
||||
|
||||
/* ---------- 核心:建连 / 订阅 / 清理 ---------- */
|
||||
useEffect(() => {
|
||||
if (!enabled || !url) return;
|
||||
|
||||
const client = getOrCreateClient(url, {
|
||||
username: 'maibu',
|
||||
password: 'jsmbzn520',
|
||||
clean: true,
|
||||
...connectOptions,
|
||||
});
|
||||
|
||||
clientRef.current = client;
|
||||
|
||||
client.on('connect', () => {
|
||||
console.log('MQTT connected');
|
||||
client.subscribe(topic);
|
||||
});
|
||||
|
||||
client.on('message', (t, payload) => {
|
||||
onMessage(t, payload.toString());
|
||||
});
|
||||
|
||||
client.on('error', err => {
|
||||
console.error('MQTT error', err);
|
||||
});
|
||||
|
||||
return () => {
|
||||
client.end(true);
|
||||
/* --- 连接成功 → 订阅主题 --- */
|
||||
const handleConnect = () => {
|
||||
setConnected(true);
|
||||
if (topicList.length > 0) {
|
||||
client.subscribe(topicList, (err) => {
|
||||
if (err) console.error('[useMqtt] subscribe failed:', err);
|
||||
});
|
||||
}
|
||||
};
|
||||
}, [url, JSON.stringify(topic)]);
|
||||
|
||||
return clientRef;
|
||||
/* --- 收到消息 → 回调 --- */
|
||||
const handleMessage = (t: string, payload: Buffer) => {
|
||||
console.log(payload.toString(), "订阅无人机数据")
|
||||
onMessageRef.current(t, payload.toString());
|
||||
};
|
||||
|
||||
/* --- 连接状态变化 --- */
|
||||
const handleReconnect = () => setConnected(false);
|
||||
const handleOffline = () => setConnected(false);
|
||||
const handleError = (err: Error) => {
|
||||
console.error('[useMqtt] error:', err);
|
||||
};
|
||||
|
||||
client.on('connect', handleConnect);
|
||||
client.on('message', handleMessage);
|
||||
client.on('reconnect', handleReconnect);
|
||||
client.on('offline', handleOffline);
|
||||
client.on('error', handleError);
|
||||
|
||||
// 如果连接已经建立,直接订阅
|
||||
if (client.connected && topicList.length > 0) {
|
||||
client.subscribe(topicList);
|
||||
setConnected(true);
|
||||
}
|
||||
|
||||
/* --- 清理:退出路由时取消订阅 & 释放连接 --- */
|
||||
return () => {
|
||||
// 取消当前 hook 订阅的主题
|
||||
if (topicList.length > 0) {
|
||||
client.unsubscribe(topicList);
|
||||
}
|
||||
// 移除本 hook 注册的事件监听
|
||||
client.removeListener('connect', handleConnect);
|
||||
client.removeListener('message', handleMessage);
|
||||
client.removeListener('reconnect', handleReconnect);
|
||||
client.removeListener('offline', handleOffline);
|
||||
client.removeListener('error', handleError);
|
||||
|
||||
setConnected(false);
|
||||
|
||||
// 释放连接池引用(refCount=0 时自动断开)
|
||||
releaseClient(url);
|
||||
clientRef.current = null;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [enabled, url, topicKey]);
|
||||
|
||||
/* ---------- 主题热更新:topic 变化时 diff 订阅 ---------- */
|
||||
const prevTopicKeyRef = useRef(topicKey);
|
||||
useEffect(() => {
|
||||
const client = clientRef.current;
|
||||
if (!client || !client.connected) return;
|
||||
|
||||
const prevKey = prevTopicKeyRef.current;
|
||||
prevTopicKeyRef.current = topicKey;
|
||||
|
||||
if (prevKey === topicKey) return;
|
||||
|
||||
// 取消旧主题
|
||||
const prevTopics = prevKey ? prevKey.split(',').filter(Boolean) : [];
|
||||
if (prevTopics.length > 0) {
|
||||
client.unsubscribe(prevTopics);
|
||||
}
|
||||
// 订阅新主题
|
||||
if (topicList.length > 0) {
|
||||
client.subscribe(topicList);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [topicKey]);
|
||||
|
||||
/* ---------- 手动发布 ---------- */
|
||||
const publish = useCallback((t: string, message: string) => {
|
||||
const client = clientRef.current;
|
||||
if (client && client.connected) {
|
||||
client.publish(t, message);
|
||||
} else {
|
||||
console.warn('[useMqtt] publish skipped: not connected');
|
||||
}
|
||||
}, []);
|
||||
|
||||
return { connected, publish };
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 使用 */
|
||||
// useMqtt({
|
||||
// url: 'wss://broker.emqx.io:8084/mqtt',
|
||||
// topic: 'robot/status',
|
||||
// onMessage: (_, message) => {
|
||||
// setMsg(message);
|
||||
// },
|
||||
// });
|
||||
|
||||
@@ -489,8 +489,8 @@ const zh = {
|
||||
controlLogTitle: '控制日志标题',
|
||||
controlRequestContent: '控制请求内容',
|
||||
countUnit: '数量单位',
|
||||
coverClosed: '覆盖已关闭',
|
||||
coverOpen: '覆盖打开',
|
||||
coverClosed: '已关闭',
|
||||
coverOpen: '打开',
|
||||
coverageEquipment: '覆盖设备',
|
||||
coverageValue: '{{types}}/{{units}}',
|
||||
createSuccess: '创建成功',
|
||||
@@ -586,7 +586,7 @@ const zh = {
|
||||
headingAngle: '航向角度',
|
||||
horizontalSpeed: '水平速度',
|
||||
hotspotSuspect: '热斑疑似',
|
||||
inDock: '内停靠',
|
||||
inDock: '舱内',
|
||||
inProgressDesc: '进行中',
|
||||
indoor: '室内',
|
||||
inputCustomAlias: '输入自定义别名',
|
||||
@@ -673,7 +673,7 @@ const zh = {
|
||||
optimize: '优化',
|
||||
optionalEmpty: '可选空',
|
||||
otherWebControlling: '其他网页控制中',
|
||||
outDock: '外停靠',
|
||||
outDock: '舱外',
|
||||
outdoor: '室外',
|
||||
panorama: '全景',
|
||||
paramArmChannel: '参数布防渠道',
|
||||
@@ -841,18 +841,18 @@ const zh = {
|
||||
stationStatus: '电站状态',
|
||||
stationTopology: '电站拓扑',
|
||||
stationVideo: '电站视频',
|
||||
statusExecFailed: '状态执行失败',
|
||||
statusExecSuccess: '状态执行成功',
|
||||
statusExecFailed: '执行失败',
|
||||
statusExecSuccess: '执行成功',
|
||||
statusPendingApproval: '状态待处理审批',
|
||||
statusPreparing: '状态准备中',
|
||||
statusRestored: '状态已恢复',
|
||||
statusStartFailed: '状态开始失败',
|
||||
statusSuggestExec: '状态建议执行',
|
||||
statusTerminated: '状态已终止',
|
||||
statusTimeout: '状态超时',
|
||||
statusUpdateSuccess: '状态更新成功',
|
||||
statusWaitExec: '状态等待执行',
|
||||
statusWaitingStart: '状态等待中开始',
|
||||
statusPreparing: '准备中',
|
||||
statusRestored: '已恢复',
|
||||
statusStartFailed: '开始失败',
|
||||
statusSuggestExec: '建议执行',
|
||||
statusTerminated: '已终止',
|
||||
statusTimeout: '超时',
|
||||
statusUpdateSuccess: '更新成功',
|
||||
statusWaitExec: '等待执行',
|
||||
statusWaitingStart: '等待中开始',
|
||||
stopVideoBtn: '停止视频按钮',
|
||||
suggestAction: '建议操作',
|
||||
suggestTime: '建议时间',
|
||||
|
||||
Reference in New Issue
Block a user