二次封装mqqt
import mqtt, { IClientOptions } from 'mqtt';
//这些都是抽离出来的方法---可以忽略
import { getClientId } from "@/utils/idGenerator"
import time from "@/utils/time"
import { getSSLFile } from "@/utils/getFiles"
import { cloneDeep,isObject } from 'lodash-es'
import { jsonStringify } from '@/utils/parseNested'
import { increment} from '@/hooks/web/useSequence';
import { mqttLogger } from '@/utils/mqtt-logger';
const setMQTT5Properties = option => {
if (option === undefined) {
return undefined
}
const properties = cloneDeep(option)
return Object.fromEntries(
Object.entries(properties).filter(([_, v]) => v !== null && v !== undefined)
)
}
const setWillMQTT5Properties = option => {
if (option === undefined) {
return undefined
}
const properties = cloneDeep(option)
return Object.fromEntries(
Object.entries(properties).filter(([_, v]) => v !== null && v !== undefined)
)
}
const getClientOptions = record => {
const mqttVersionDict = {
"3.1.1": 4,
"5.0": 5
}
const {
clientId,
username,
password,
keepalive,
clean,
connectTimeout,
ssl,
certType,
mqttVersion,
reconnect,
reconnectPeriod, // reconnectPeriod = 0 在客户端中禁用自动重新连接
will,
rejectUnauthorized,
ALPNProtocols,
clientIdWithTime
} = record
const protocolVersion = mqttVersionDict[mqttVersion]
const options:IClientOptions = {
clientId,
keepalive,
clean,
reconnectPeriod: reconnect ? reconnectPeriod : 0,
protocolVersion
}
options.connectTimeout = time.convertSecondsToMs(connectTimeout)
// 将时间戳附加到MQTT客户端id
if (clientIdWithTime) {
const clickIconTime = Date.parse(new Date().toString())
options.clientId = `${options.clientId}_${clickIconTime}`
}
// Auth
if (username !== "") {
options.username = username
}
if (password !== "") {
options.password = password
}
// MQTT Version
if (protocolVersion === 5 && record.properties) {
const properties = setMQTT5Properties(record.properties)
if (properties && Object.keys(properties).length > 0) {
options.properties = properties
}
}
// else if (protocolVersion === 3) {
// options.protocolId = 'MQIsdp'
// }
// SSL
if (ssl) {
options.rejectUnauthorized =
rejectUnauthorized === undefined ? true : rejectUnauthorized
if (ALPNProtocols) {
options.ALPNProtocols = ALPNProtocols.replace(/[\[\] ]/g, "").split(",")
}
if (certType === "self") {
const sslRes = getSSLFile({
ca: record.ca,
cert: record.cert,
key: record.key
})
if (sslRes) {
options.ca = sslRes.ca
options.cert = sslRes.cert
options.key = sslRes.key
}
}
}
// Will Message
if (will) {
const {
lastWillTopic: topic,
lastWillPayload: payload,
lastWillQos: qos,
lastWillRetain: retain
} = will
if (topic) {
options.will = { topic, payload, qos: qos, retain }
if (protocolVersion === 5) {
const { properties } = will
if (properties) {
const willProperties = setWillMQTT5Properties(properties)
if (willProperties && Object.keys(willProperties).length > 0) {
options.will.properties = willProperties
}
}
}
}
}
// 自动重新订阅,仅在重新连接时有效
options.resubscribe = true
return options
}
const getUrl = record => {
const { host, port, path='' } = record
const protocol = getMQTTProtocol(record)
let url = `${protocol}://${host}:${port}`
if (protocol === "ws" || protocol === "wss") {
url = `${url}${path.startsWith("/") ? "" : "/"}${path}`
}
return url
}
const shouldKeep = (item) => !mqttLogger.skipTopics.some(exclude =>{
// 如果 item 是对象的话 取除 key 进行判断
if (isObject(item)) {
return Object.keys(item).some(key => /^(?:[^/]*\/){7,}/.test(item[key]) && item[key].includes(exclude))
}
return item && item.includes(exclude)
})
// 添加日志消息监听处理
export function setupMqttMessageLogging(mqttService) {
mqttService.on('message', (topic: string, message: Buffer) => {
// 避免循环记录日志服务自己的消息
if (shouldKeep(topic)) {
mqttLogger.logMqttOperation('message', topic, {
messageSize: message.length,
messageContent: message.toString()
});
}
});
}
export const createClient = record => {
const defaultRecord = getDefaultRecord();
const options = getClientOptions({...defaultRecord,...record})
const url = getUrl(record)
const curConnectClient:any = mqtt.connect(url, options)
const subscribedTopics = new Set()
// 保存所有原始方法引用
const original_connect = curConnectClient.connect;
const original_subscribe = curConnectClient.subscribe;
const original_publish = curConnectClient.publish;
const original_disconnect = curConnectClient.disconnect;
const original_publishAsync = curConnectClient.publishAsync;
// 扩展connect方法
curConnectClient.connect = function(brokerUrl: string, options = {}) {
mqttLogger.logMqttOperation('connect', undefined, { brokerUrl, options });
return original_connect.call(this, brokerUrl, options);
};
// 扩展subscribe方法 - 合并两次重写的功能
curConnectClient.subscribe = function(topic, options, ...args) {
// 记录日志功能
const topicStr = Array.isArray(topic) ? topic.join(',') : topic;
mqttLogger.logMqttOperation('subscribe', topicStr, { options });
// 订阅管理功能
if (!Array.isArray(topic) && !subscribedTopics.has(topic)) {
subscribedTopics.add(topic);
} else if (Array.isArray(topic)) {
topic.forEach(t => subscribedTopics.add(t));
}
shouldKeep(topic) && window.log.warn(`订阅主题 ${topic}, 消息内容: ${JSON.stringify(options)}`,options);
// console.log(topic, options, ...args,'subscribe')
return original_subscribe.call(this, topic, options, ...args);
};
// 扩展publish方法
curConnectClient.publish = function(topic, message, options) {
// 避免循环记录日志服务自己的消息
if (shouldKeep(topic)) {
mqttLogger.logMqttOperation('publish', topic, {
messageSize: message.length,
messageContent: message.toString(),
options
});
}
return original_publish.call(this, topic, message, options);
};
// 扩展publishAsync方法
curConnectClient.publishAsync = function(topic, message, ...args) {
// 添加消息ID和时间戳
if (isObject(message)) {
const msgObj = message as any;
msgObj.msgId = msgObj.msgId || increment();
msgObj.sendTime = time.getNowDate('YYYY-MM-DDTHH:mm:ss');
message = jsonStringify(message);
}
// 添加日志功能 (类似publish方法)
if (shouldKeep(topic)) {
mqttLogger.logMqttOperation('publishAsync', topic, {
messageSize: message.length,
messageContent: message.toString(),
});
}
return original_publishAsync.call(this, topic, message, ...args);
};
// 扩展disconnect方法
curConnectClient.disconnect = function() {
mqttLogger.logMqttOperation('disconnect');
return original_disconnect.call(this);
};
// 添加检查订阅状态的方法
curConnectClient.isSubscribed = function(topic) {
return subscribedTopics.has(topic);
};
// 添加获取 topic 的方法
curConnectClient.getTopic = function(topicName) {
// 自动订阅
this.subscribe(topicName);
return topicName;
};
setupMqttMessageLogging(curConnectClient);
console.log('MQTT日志记录已初始化');
return {
curConnectClient,
connectUrl: url
};
}
// 防止旧数据丢失协议字段
export const getMQTTProtocol = data => {
const { protocol, ssl } = data
if (!protocol) {
return ssl ? "wss" : "ws"
}
return protocol
}
export const getDefaultRecord = () => {
return {
//**生成唯一标识**:为每个客户端实例创建一个唯一的 `clientId` 和时间戳
clientId: getClientId(),
createAt: time.getNowDate(),// 记录配置创建时间
updateAt: time.getNowDate(),// 记录配置最后更新时间
name: "",
// 启动“干净会话”,连接断开后,代理会清除该客户端的所有会话信息(订阅、未确认消息等)
clean: true,
//根据环境变量(`import.meta.env.VITE_IS_ONLINE_ENV`)自动选择 WebSocket 协议(`ws` 或 `wss`)和默认端口
protocol: import.meta.env.VITE_IS_ONLINE_ENV === "true" ? "wss" : "ws",
// MQTT代理地址,支持环境变量覆盖
host: import.meta.env.VITE_IS_DEFAULT_HOST ?? "broker.emqx.io",
// 心跳间隔(秒),客户端每5秒发送一次心跳包以维持连接
keepalive: 5,
connectTimeout: 5, // 连接超时改为5秒
reconnect: true,
// 重连间隔(毫秒),连接断开后每4秒尝试重连一次,默认的是1s
reconnectPeriod: 4000,
username: "",
password: "",
path: "/mqtt",// WebSocket的路径,与代理配置匹配
port: import.meta.env.VITE_IS_ONLINE_ENV === "true" ? 8084 : 8083,
ssl: false,// 是否启用SSL(对于wss,通常由协议本身处理)
certType: "",
rejectUnauthorized: true,
ALPNProtocols: "",
ca: "",// CA证书
cert: "",// 客户端证书
key: "",// 客户端私钥
mqttVersion: "5.0",// 指定使用的MQTT协议版本
subscriptions: [],// 用于在UI或内存中存储当前订阅的主题列表
messages: [],// 用于存储接收到的消息历史
pushProps: {},// 可能用于存储推送通知的配置
unreadMessageCount: 0,// 未读消息计数器
// 客户端的连接状态,可用于UI绑定
client: {
connected: false
},
//#### **MQTT 5.0 高级特性**
will: {
lastWillTopic: "",
lastWillPayload: "",
lastWillQos: 0,
lastWillRetain: false,
properties: {
payloadFormatIndicator: undefined,
willDelayInterval: undefined,
messageExpiryInterval: undefined,
contentType: "",
responseTopic: "",
correlationData: undefined,
userProperties: undefined
}
},
properties: {
sessionExpiryInterval: undefined,
receiveMaximum: undefined,
maximumPacketSize: undefined,
topicAliasMaximum: undefined,
requestResponseInformation: undefined,
requestProblemInformation: undefined,
userProperties: undefined,
authenticationMethod: undefined,
authenticationData: undefined
},
clientIdWithTime: false
}
}
封装可以直接调用mqttHandler
import { ref, shallowRef, onMounted } from 'vue'
import { createClient } from '@/utils/mqttUtils'
import { MqttMessageHandler, MqttMessageHandlerType } from '@/utils/mqtt-message-handler'
import { mqttNotificationManager } from '@/components/MqttNotification/manager'
// 单例模式,确保全应用只有一个MQTT实例
const clientInstances = ref<any>(null)
const clientInitData = ref({
connected: false
})
// 连接状态
const isConnected = ref(false)
// MQTT消息处理器单例
const mqttHandlerInstance = ref<MqttMessageHandlerType | null>(null)
// 创建连接的单例实现,确保全局只执行一次
let connectionPromise: Promise<any> | null = null;
// 统一的MQTT状态通知管理
const updateMqttNotification = (title: string, message: string, type: 'success' | 'warning' | 'info' | 'error', duration?: number) => {
// 如果是错误消息,提供重连回调
const onReconnect = type === 'error' ? () => {
console.log('用户点击重连按钮')
// 获取当前最新的 useMqttClient 实例并调用强制重连
const { forceReconnect } = useMqttClient()
forceReconnect()
} : undefined
mqttNotificationManager.update(title, message, type, onReconnect)
// 如果是成功消息,设置自动关闭
if (type === 'success' && duration) {
setTimeout(() => {
mqttNotificationManager.destroy()
}, duration)
}
}
// 默认连接配置
const defaultConnectionInfo = {
protocol: window.config.VITE_MQTT_PROTOCOL,
host: window.config.VITE_MQTT_URL,
port: window.config.VITE_MQTT_PORT,
path: window.config.VITE_MQTT_PATH,
username: window.config.VITE_MQTT_USERNAME,
password: window.config.VITE_MQTT_PASSWORD,
clean: false,
properties: {
sessionExpiryInterval: 0,
requestResponseInformation: true,
requestProblemInformation: true
}
}
/**
* MQTT客户端Hook
* @param connectionParams 可选的连接参数,覆盖默认设置
* @returns MQTT客户端实例和相关状态
*/
// let logId = '';
export function useMqttClient(connectionParams = {}) {
let reconnectCount = 0
let reconnectTimeout: NodeJS.Timeout | null = null
let isManualClose = false // 是否为手动关闭
// 合并连接参数
const connectionInfo = {
...defaultConnectionInfo,
...connectionParams
}
// 创建连接
const createConnection = () => {
console.count('createConnection called')
// 如果已经有实例了,直接返回
if (clientInstances.value && clientInitData.value.connected) {
return { client: clientInstances, mqttHandler: mqttHandlerInstance.value }
}
// 如果连接已经在创建中,等待该连接创建完成
if (connectionPromise) {
return connectionPromise;
}
// 创建新的连接
connectionPromise = new Promise((resolve) => {
// logId = addLog(`MQTT连接中...`)
console.log('连接参数', connectionInfo)
const { curConnectClient } = createClient(connectionInfo)
// 统一处理消息
const mqttHandler = MqttMessageHandler.getInstance(curConnectClient)
mqttHandlerInstance.value = mqttHandler
// 连接失败
curConnectClient.on('error', (err) => {
console.log(err, '连接失败')
clientInitData.value.connected = false
isConnected.value = false
// 重置连接Promise以允许重新连接
connectionPromise = null
})
//重新连接
curConnectClient.on('reconnect', () => {
console.log('reconnect attempt:', reconnectCount + 1)
reconnectCount++
connectionPromise = null
updateMqttNotification('网络连接', `正在重新连接...`, 'warning')
})
// 连接成功
curConnectClient.on('connect', (connack) => {
// updateLog(logId,{
// type: 'success',
// message: `MQTT连接成功`
// })
updateMqttNotification('网络连接', 'MQTT连接成功', 'success', 3000)
clientInitData.value.connected = true
isConnected.value = true
reconnectCount = 0 // 连接成功后重置重连计数
clientInstances.value = curConnectClient
// 清除连接超时定时器
if (reconnectTimeout) {
clearTimeout(reconnectTimeout)
reconnectTimeout = null
}
})
// 设置连接超时检查
reconnectTimeout = setTimeout(() => {
if (!curConnectClient.connected && !isConnected.value) {
console.log('连接超时,当前重连次数:', reconnectCount)
console.log('连接建立中,请等待...')
}
}, 10000) // 10秒超时
// 保存实例
clientInstances.value = curConnectClient
resolve({ client: clientInstances, mqttHandler })
});
return connectionPromise;
}
// 关闭连接
const closeConnection = () => {
isManualClose = true // 标记为手动关闭
if (clientInstances.value) {
clientInstances.value.end(true) // 强制关闭连接
clientInitData.value.connected = false
isConnected.value = false
clientInstances.value = null
// 关闭通知
mqttNotificationManager.destroy()
// 清除超时定时器
if (reconnectTimeout) {
clearTimeout(reconnectTimeout)
reconnectTimeout = null
}
// 重置连接Promise以允许重新连接
connectionPromise = null
reconnectCount = 0 // 重置重连计数
mqttHandlerInstance.value?.destroy() // 销毁消息处理器实例
mqttHandlerInstance.value = null // 清理消息处理器实例
}
setTimeout(() => {
isManualClose = false // 重置手动关闭标记
}, 1000)
}
// 组件挂载时自动初始化连接
onMounted(() => {
if (!mqttHandlerInstance.value && !connectionPromise) {
initConnection()
}
})
// 需要立即创建连接并获取初始mqttHandler
const initConnection = () => {
console.count('initConnection called')
if (!connectionPromise) {
createConnection()
}
return mqttHandlerInstance.value
}
// 初始化 mqttHandler 如果需要
if (!mqttHandlerInstance.value && !connectionPromise) {
initConnection()
}
// 强制重连方法
const forceReconnect = () => {
console.log('强制重连中...')
// 先关闭现有连接
if (clientInstances.value) {
clientInstances.value.end(true)
clientInstances.value = null
}
// 清理状态
clientInitData.value.connected = false
isConnected.value = false
connectionPromise = null
reconnectCount = 0
// 清除超时定时器
if (reconnectTimeout) {
clearTimeout(reconnectTimeout)
reconnectTimeout = null
}
// 短暂延迟后重新创建连接
setTimeout(() => {
createConnection()
}, 1000)
}
return {
clientInstances,
isConnected,
createConnection,
closeConnection,
forceReconnect,
clientInitData,
mqttHandler: mqttHandlerInstance.value
}
}
调用
import { useMqttClient } from '@/hooks/web/useMqttClient'
const { mqttHandler} = useMqttClient()
//调用
//getPsamAuthConfig.topic mqqt主题
mqttHandler?.onMessage(`${getPsamAuthConfig.topic}`, (message) => {
if(message.code !== 0){
console.log('PSAM卡授权失败', message)
logStore.addLog(`PSAM卡授权失败`, 'error')
}else{
console.log('PSAM卡授权成功', message)
logStore.addLog(`PSAM卡授权成功`, 'success')
}
})