master
lld 2025-12-30 20:19:28 +08:00
parent 01bece15cc
commit e5c243b74e
4 changed files with 310 additions and 200 deletions

64
App.vue
View File

@ -1,10 +1,51 @@
<script>
import config from './config'
import { getToken } from '@/utils/auth'
import {
initMQTT,
disconnectMQTT,
getMQTTStatus,
manualReconnect
} from '@/utils/mqtt';
export default {
onLaunch: function() {
globalData: {
mqttClient: null, // MQTT
mqttConfig: { // MQTT
host: '1.94.254.176',
port: 9001, // wssEMQ X8084
isSSL: false, // true=wssfalse=ws
username: 'admin',
password: 'Admin#12345678',
keepalive: 60,
clean: true
},
globalSubscribeTopic: [ //
{topic: 'dtu/#', qos: 0},
// {topic: 'system/status', qos: 1}
],
mqttStatus: 'disconnected' // connected/disconnected/reconnecting
},
onLaunch() {
this.initApp()
console.log('应用启动初始化MQTT连接');
this.initGlobalMQTT();
},
onShow() {
console.log('应用切前台检查MQTT连接');
const { isConnected } = getMQTTStatus();
if (!isConnected && this.globalData.mqttStatus !== 'disconnected') {
//
manualReconnect(this.globalData.mqttConfig);
this.globalData.mqttStatus = 'reconnecting';
}
},
onHide() {
console.log('应用切后台断开MQTT连接');
//
disconnectMQTT();
this.globalData.mqttStatus = 'disconnected';
this.globalData.mqttClient = null;
},
methods: {
//
@ -23,11 +64,28 @@
if (!getToken()) {
this.$tab.reLaunch('/pages/login')
}
},
// MQTT
async initGlobalMQTT() {
try {
// +
const client = await initMQTT(
this.globalData.mqttConfig,
this.globalData.globalSubscribeTopic
);
//
this.globalData.mqttClient = client;
this.globalData.mqttStatus = 'connected';
console.log('全局MQTT初始化成功');
} catch (err) {
console.error('全局MQTT初始化失败', err);
this.globalData.mqttStatus = 'disconnected';
}
}
}
}
</script>
<style lang="scss">
@import '@/static/scss/index.scss'
@import '@/static/scss/index.scss';
</style>

View File

@ -1,6 +1,6 @@
// 应用全局配置
module.exports = {
baseUrl: "production" ? "/api" : "http://localhost:8088",
baseUrl: process.env.NODE_ENV === "production" ? "/api" : "http://localhost:8088",
// 应用信息
appInfo: {
// 应用名称

View File

@ -58,7 +58,9 @@
</uni-section>
<uni-section title="设备控制" titleFontSize="16px" type="line" v-if="value!== 1">
<template v-slot:right >
{{ control }}
</template>
<!-- 卷膜/卷被卡片容器2列栅格布局 -->
<view class="card-grid">
<!-- 卷被开卡片 -->
@ -169,12 +171,12 @@ export default {
mqttConfig: {
host: '1.94.254.176',
port: 9001,
clientId: 'uniapp_mqtt_' + Math.random().toString(16).substr(2, 8),
username: 'admin',
password: 'Admin#12345678',
subscribeTopic:'/up',
},
value: 1,
control: '正在加载中...',
range: [{
"value": '864865085016294',
"text": "十方北棚"
@ -273,7 +275,7 @@ export default {
}
findDtuDataByInfo(queryParams).then(response => {
this.liveData = {
temp1: response.data.temp1 || '已离线...',
temp1: response.data.temp1 || '已离线..',
temp2: response.data.temp2 || '已离线..',
temp3: response.data.temp3 || '已离线..',
temp4: response.data.temp4 || '已离线..',
@ -317,6 +319,7 @@ export default {
jm3k: 0,
jm3g: 0
};
this.control = '正在加载中...';
this.message = {};
this.temp = '';
this.liveData = {
@ -393,7 +396,7 @@ export default {
},
connectMqtt() {
const options = {
clientId: this.mqttConfig.clientId,
clientId: 'uniapp_mqtt_' + Math.random().toString(16).substr(2, 8),
username: this.mqttConfig.username,
password: this.mqttConfig.password,
clean: true,
@ -409,6 +412,7 @@ export default {
this.connected = true
this.client.subscribe('dtu/+/up', {qos: 0})
this.addMessage('已连接到MQTT服务器')
console.info(this.client)
})
this.client.on("message", this.ackMessage);
@ -425,6 +429,7 @@ export default {
this.client.on('close', () => {
this.addMessage('连接已关闭')
this.connected = false
console.info(this.client)
})
},
@ -527,6 +532,7 @@ export default {
// 01
this.show[key] = value === 0 ? '暂停' : '运行';
});
this.control = '最后更新时间:' + this.getCurrentTime();
// console.info("imei: "+this.publishTopic+"copy: ",this.status)
}
const allKeysNumeric2 = Object.keys(msgData).every(key => /^\d+$/.test(key));
@ -546,7 +552,7 @@ export default {
humi4: div10(msgData["104"]) || "已离线...",
}
//
this.temp = "最更新时间:" + this.getCurrentTime();
this.temp = "最更新时间:" + this.getCurrentTime();
this.fontStyle = 'font-size:16px;'
}

View File

@ -1,214 +1,260 @@
/**
* MQTT工具类 - 全局唯一连接自动重连支持自定义配置和订阅列表
* 适配uniapp小程序/APP/H5小程序仅支持ws/wss
*/
// 引入mqtt库需先安装npm install mqtt --save或下载mqtt.min.js到本地引入
import mqtt from 'mqtt'
// 全局MQTT实例单例避免多页面重复创建
let mqttInstance = null;
// 全局变量
let client = null; // MQTT客户端实例
let subscribeList = []; // 订阅主题列表(重连后自动恢复订阅)
let reconnectTimer = null; // 重连定时器
let maxReconnectTimes = 10; // 最大重连次数
let currentReconnectTimes = 0; // 当前重连次数
let isManualDisconnect = false; // 是否主动断开(用于区分主动/被动断开,避免主动断开后重连)
/**
* 初始化MQTT客户端单例模式
* @param {Object} mqttConfig 配置项
* @param {Array|String} subTopic 初始订阅主题
* @returns {Object} MQTT实例
* 初始化MQTT连接
* @param {Object} config - MQTT连接配置
* @param {Array} subs - 初始订阅列表格式[{ topic: 'topic1', qos: 0 }, { topic: 'topic2', qos: 1 }]
* @returns {Promise} 连接成功/失败的Promise
*/
function createMqttClient(mqttConfig, subTopic) {
// 已存在实例则直接返回
if (mqttInstance && mqttInstance.connected) {
// 若传入新主题,补充订阅
if (subTopic) {
mqttInstance.subscribe(subTopic);
}
return mqttInstance;
}
// 标准化配置(默认值 + 防错)
const config = {
host: mqttConfig.host || '1.94.254.176',
port: mqttConfig.port || 9001,
clientId: mqttConfig.clientId || `uniapp_mqtt_${Date.now()}${Math.random().toString(16).substr(2, 4)}`, // 时间戳+随机数,减少重复
username: mqttConfig.username || 'admin',
password: mqttConfig.password || 'Admin#12345678',
clean: false, // 关键改为false保持会话缓存消息
reconnectPeriod: mqttConfig.reconnectPeriod || 3000,
connectTimeout: mqttConfig.connectTimeout || 5000,
qos: mqttConfig.qos || 1 // 默认QoS1确保消息不丢
};
// 连接选项
const options = {
clientId: config.clientId,
username: config.username,
password: config.password,
clean: config.clean,
connectTimeout: config.connectTimeout,
reconnectPeriod: config.reconnectPeriod,
keepalive: 60 // 新增心跳,避免连接被断开
};
// 拼接WS地址兼容配置错误
const url = `ws://${config.host}:${config.port}/mqtt`;
// 创建客户端
const client = mqtt.connect(url, options);
// 实例状态管理
const instance = {
client: client,
connected: false,
subscribedTopics: new Set(), // 记录已订阅主题
config: config,
messageCallback: null, // 消息接收回调
statusCallback: null, // 状态变更回调
// 订阅主题(支持单个/多个,带重试)
subscribe: function (topics, qos = config.qos) {
if (!this.connected) {
console.warn('MQTT未连接延迟订阅:', topics);
// 连接成功后自动订阅
client.once('connect', () => this.subscribe(topics, qos));
return;
}
const topicList = Array.isArray(topics) ? topics : [topics];
client.subscribe(topicList, { qos }, (err) => {
if (err) {
console.error('订阅失败:', err, '主题:', topicList);
// 订阅失败重试(仅一次)
setTimeout(() => this.subscribe(topics, qos), 1000);
} else {
topicList.forEach(t => this.subscribedTopics.add(t));
console.log(`订阅成功${topicList.length > 1 ? '(批量)' : '(单个)'}:`, topicList);
this.statusCallback && this.statusCallback('subscribe_success', topicList);
}
});
},
// 取消订阅
unsubscribe: function (topics) {
if (!this.connected) return;
const topicList = Array.isArray(topics) ? topics : [topics];
client.unsubscribe(topicList, (err) => {
if (err) {
console.error('取消订阅失败:', err);
} else {
topicList.forEach(t => this.subscribedTopics.delete(t));
console.log('取消订阅成功:', topicList);
this.statusCallback && this.statusCallback('unsubscribe_success', topicList);
}
});
},
// 发布消息(带参数,失败重试)
publish: function (topic, message, qos = config.qos, retain = false) {
export function initMQTT(config, subs = []) {
return new Promise((resolve, reject) => {
if (!this.connected) {
reject(new Error('MQTT未连接无法发布消息'));
// 自动重连后发布
this.reconnectAndPublish(topic, message, qos, retain);
// 1. 校验配置(必填项)
if (!config.host || !config.port) {
reject(new Error('MQTT配置错误host和port为必填项'));
return;
}
// 标准化消息格式对象转JSON
const payload = typeof message === 'object' ? JSON.stringify(message) : String(message);
// 2. 保存订阅列表(用于重连恢复)
subscribeList = subs;
client.publish(topic, payload, { qos, retain }, (err) => {
if (err) {
console.error('发布失败:', err, '主题:', topic);
reject(err);
// 发布失败重试
setTimeout(() => this.publish(topic, message, qos, retain), 1000);
} else {
console.log('发布成功:', topic, '内容:', payload);
resolve({ topic, payload });
// 3. 避免重复连接
if (client && client.connected) {
resolve(client);
return;
}
});
});
},
// 重连后补发消息
reconnectAndPublish: function (topic, message, qos, retain) {
client.once('connect', () => {
this.publish(topic, message, qos, retain);
});
},
// 4. 标记为非主动断开(允许重连)
isManualDisconnect = false;
// 断开连接
disconnect: function () {
if (this.connected && this.client) {
this.client.end(false, () => { // false等待剩余消息发送完成
this.connected = false;
this.subscribedTopics.clear();
console.log('MQTT连接已断开保留会话');
this.statusCallback && this.statusCallback('disconnect');
});
}
}
// 5. 构建连接地址小程序仅支持ws/wss优先用wss更安全
const protocol = config.protocol || (config.isSSL ? 'wss' : 'ws');
const connectUrl = `${protocol}://${config.host}:${config.port}/mqtt`;
// 6. 构建MQTT连接选项
const mqttOptions = {
clientId: config.clientId || `uni_mqtt_${Math.random().toString(16).substr(2, 8)}`,
username: config.username || '',
password: config.password || '',
keepalive: config.keepalive || 60, // 心跳间隔(秒)
clean: config.clean !== undefined ? config.clean : true, // 是否清除会话
reconnectPeriod: 0, // 关闭内置重连(自定义重连逻辑)
connectTimeout: config.connectTimeout || 10000, // 连接超时(毫秒)
...config.extraOptions // 额外扩展配置
};
// 绑定客户端事件
try {
// 7. 创建客户端并连接
client = mqtt.connect(connectUrl, mqttOptions);
// 8. 监听连接成功
client.on('connect', () => {
instance.connected = true;
console.log('MQTT连接成功ClientId:', config.clientId);
instance.statusCallback && instance.statusCallback('connect_success');
// 初始订阅主题
if (subTopic) {
instance.subscribe(subTopic);
}
})
.on('reconnect', (error) => {
instance.connected = false;
console.log('MQTT正在重连...', error);
instance.statusCallback && instance.statusCallback('reconnect', error);
})
.on('error', (error) => {
instance.connected = false;
console.error('MQTT连接错误:', error);
instance.statusCallback && instance.statusCallback('error', error);
})
.on('close', () => {
instance.connected = false;
console.log('MQTT连接关闭');
instance.statusCallback && instance.statusCallback('close');
})
.on('offline', () => {
instance.connected = false;
console.log('MQTT客户端下线');
instance.statusCallback && instance.statusCallback('offline');
})
.on('message', (topic, payload) => {
const msg = payload.toString();
console.log('收到MQTT消息:', topic, msg);
// 消息回调,交给业务层处理
instance.messageCallback && instance.messageCallback(topic, msg);
console.log('MQTT连接成功', connectUrl);
currentReconnectTimes = 0; // 重置重连次数
// 9. 订阅初始主题
subscribeTopics(subscribeList);
resolve(client);
});
// 赋值单例
mqttInstance = instance;
return instance;
// 10. 监听连接错误
client.on('error', (err) => {
console.error('MQTT连接错误', err);
client.end();
reject(err);
});
// 11. 监听连接断开(被动断开则触发重连)
client.on('close', () => {
console.log('MQTT连接已断开');
client = null;
// 非主动断开 + 未达最大重连次数 → 触发重连
if (!isManualDisconnect && currentReconnectTimes < maxReconnectTimes) {
reconnectMQTT(config);
}
});
// 12. 监听消息全局消息转发页面层通过uni.$on监听
client.on('message', (topic, message) => {
const msg = {
topic,
payload: message.toString(), // 转字符串原始是Buffer
timestamp: Date.now()
};
// 全局广播消息,页面层按需监听
uni.$emit('mqtt_message', msg);
});
} catch (err) {
console.error('MQTT初始化失败', err);
reject(err);
}
});
}
/**
* 对外暴露的核心方法
* 订阅主题支持单个/多个
* @param {Array} topics - 订阅列表格式[{ topic: 'topic1', qos: 0 }, { topic: 'topic2', qos: 1 }]
*/
export const mqttTool = {
// 初始化连接
connect: function (mqttConfig, subTopic) {
return createMqttClient(mqttConfig, subTopic);
},
// 获取全局实例
getInstance: function () {
return mqttInstance;
},
// 页面切换时的订阅管理(核心解决多页面订阅问题)
switchPageTopic: function (newTopics, oldTopics) {
const instance = mqttInstance;
if (!instance) return;
// 先订阅新主题,再取消旧主题(避免漏消息)
if (newTopics) {
instance.subscribe(newTopics);
export function subscribeTopics(topics = []) {
if (!client || !client.connected) {
console.warn('MQTT未连接无法订阅主题');
return;
}
if (oldTopics) {
instance.unsubscribe(oldTopics);
// 过滤空主题
const validTopics = topics.filter(item => item && item.topic);
if (validTopics.length === 0) return;
// 转换为mqtt库要求的格式{ topic1: { qos: 0 }, topic2: { qos: 1 } }
const subscribeMap = {};
validTopics.forEach(item => {
subscribeMap[item.topic] = { qos: item.qos || 0 };
});
client.subscribe(subscribeMap, (err) => {
if (err) {
console.error('MQTT订阅失败', err);
} else {
console.log('MQTT订阅成功', validTopics.map(item => item.topic));
// 更新订阅列表(用于重连恢复)
subscribeList = [...new Set([...subscribeList, ...validTopics])]; // 去重
}
});
}
/**
* 取消订阅主题
* @param {Array} topics - 取消订阅的主题列表格式['topic1', 'topic2']
*/
export function unsubscribeTopics(topics = []) {
if (!client || !client.connected) {
console.warn('MQTT未连接无法取消订阅');
return;
}
client.unsubscribe(topics, (err) => {
if (err) {
console.error('MQTT取消订阅失败', err);
} else {
console.log('MQTT取消订阅成功', topics);
// 更新订阅列表
subscribeList = subscribeList.filter(item => !topics.includes(item.topic));
}
});
}
/**
* 发布消息
* @param {String} topic - 发布的主题
* @param {String/Buffer} payload - 发布的消息内容
* @param {Object} options - 发布选项{ qos: 0, retain: false }
* @returns {Promise} 发布成功/失败的Promise
*/
export function publishMQTT(topic, payload, options = { qos: 0, retain: false }) {
return new Promise((resolve, reject) => {
if (!client || !client.connected) {
reject(new Error('MQTT未连接无法发布消息'));
return;
}
if (!topic) {
reject(new Error('发布失败:主题不能为空'));
return;
}
client.publish(topic, payload, options, (err) => {
if (err) {
console.error(`MQTT发布${topic}失败:`, err);
reject(err);
} else {
console.log(`MQTT发布${topic}成功:`, payload);
resolve(true);
}
});
});
}
/**
* 断开MQTT连接主动断开不会触发重连
*/
export function disconnectMQTT() {
// 标记为主动断开
isManualDisconnect = true;
// 清除重连定时器
if (reconnectTimer) {
clearTimeout(reconnectTimer);
reconnectTimer = null;
}
// 断开连接
if (client && client.connected) {
client.end(false, () => { // false不发送遗嘱消息
console.log('MQTT主动断开连接');
client = null;
subscribeList = [];
});
}
}
/**
* 重连MQTT内部调用也可外部手动触发
* @param {Object} config - 连接配置同initMQTT的config
*/
export function reconnectMQTT(config) {
// 清除已有定时器,避免重复触发
if (reconnectTimer) {
clearTimeout(reconnectTimer);
}
// 重连间隔1秒/次(可自定义)
reconnectTimer = setTimeout(async () => {
currentReconnectTimes++;
console.log(`MQTT重连中${currentReconnectTimes}/${maxReconnectTimes}`);
try {
await initMQTT(config, subscribeList);
} catch (err) {
// 重连失败,继续尝试(直到达到最大次数)
if (currentReconnectTimes < maxReconnectTimes) {
reconnectMQTT(config);
} else {
console.error('MQTT重连次数已达上限停止重连');
uni.$emit('mqtt_reconnect_fail'); // 全局通知重连失败
}
}
};
}, 1000);
}
/**
* 获取MQTT客户端状态
* @returns {Object} 状态信息
*/
export function getMQTTStatus() {
return {
isConnected: !!client && client.connected, // 是否连接
client: client, // 客户端实例
subscribeList: [...subscribeList], // 已订阅列表(浅拷贝,避免外部修改)
currentReconnectTimes, // 当前重连次数
maxReconnectTimes // 最大重连次数
};
}
/**
* 手动触发重连外部调用比如页面主动刷新连接
* @param {Object} config - 连接配置
*/
export function manualReconnect(config) {
currentReconnectTimes = 0; // 重置重连次数
reconnectMQTT(config);
}