Vue3 + MQTT实现前端与硬件设备直接通讯(附完整代码解析)
作者:小钱在学前端
前言
在物联网(IoT)开发场景中,前端页面与硬件设备的实时通讯是核心需求之一。MQTT(Message Queuing Telemetry Transport)作为轻量级、低带宽消耗的通讯协议,非常适合硬件设备与前端的双向数据交互。本文将讲解如何使用 Vue3(Composition API)+ MQTT.js 实现硬件设备的搜索、匹配、通讯配置管理等功能,附带完整代码解析。
一、项目背景与技术栈
1. 需求场景
我们需要开发一个 “硬件配置页面”,核心功能包括:
- 展示 MQTT 通讯核心参数(通讯关键字、发送 / 接收主题、连接状态);
- 触发硬件搜索,实时显示搜索进度;
- 展示搜索到的硬件列表,支持逐个匹配硬件;
- 记录已匹配的硬件地址,管理 MQTT 连接生命周期。
2. 技术栈选型
| 技术 / 工具 | 用途说明 |
|---|---|
| Vue3 (script setup) | 前端框架核心,使用 Composition API 组织逻辑,脚本 setup 语法简化代码结构 |
| MQTT.js | 实现 MQTT 客户端功能,负责与 MQTT 服务器建立连接、订阅 / 发布消息 |
| SCSS | 样式预处理器,支持嵌套、变量、动画,提升样式可维护性 |
二、核心概念铺垫:MQTT 基础
在开始代码实现前,先快速回顾 MQTT 的核心概念,帮助理解后续逻辑:
- 发布 / 订阅(Pub/Sub)模式:前端(客户端)与硬件(客户端)通过 MQTT 服务器中转消息,双方无需直接连接;
- 主题(Topic):消息的 “地址”,客户端通过订阅指定主题接收消息,通过发布指定主题发送消息(例如
/topic1/1111111); - 客户端(Client):前端和硬件都是 MQTT 客户端,需通过唯一
clientId标识; - 连接状态:客户端与 MQTT 服务器的连接状态(已连接 / 未连接),影响消息收发能力。
三、功能模块拆解与实现
下面按 “页面结构 → 核心逻辑 → 样式优化” 的顺序,逐步解析完整实现过程。
模块 1:页面结构设计(Template)
页面采用 “卡片式布局”,分为「通讯配置卡片」和「硬件配置卡片」,结构清晰。
<template>
<div class="room-config-container">
<!-- 1. 通讯配置卡片:展示MQTT核心参数 -->
<div class="card communication-card">
<div class="card-header">
<h2 class="card-title">通讯配置</h2>
<span class="card-helper">MQTT通讯相关参数</span>
</div>
<div class="card-body">
<div class="config-grid">
<!-- 通讯关键字 -->
<div class="config-item">
<span class="config-label">通讯关键字:</span>
<span class="config-value">{{ key || '未设置' }}</span>
</div>
<!-- 发送主题 -->
<div class="config-item">
<span class="config-label">发送主题:</span>
<span class="config-value">{{ sendTopic || '未设置' }}</span>
</div>
<!-- 接收主题 -->
<div class="config-item">
<span class="config-label">接收主题:</span>
<span class="config-value">{{ receiveTopic || '未设置' }}</span>
</div>
<!-- MQTT连接状态(带视觉指示器) -->
<div class="config-item">
<span class="config-label">MQTT连接状态:</span>
<span class="config-value">
<span
:class="isConnected ? 'status-indicator connected' : 'status-indicator disconnected'"
:title="isConnected ? '已连接' : '未连接'"></span>
{{ isConnected ? '已连接' : '未连接' }}
</span>
</div>
</div>
</div>
</div>
<!-- 2. 硬件配置卡片:搜索、匹配硬件 -->
<div class="card dev-config-card">
<div class="card-header">
<h2 class="card-title">硬件配置</h2>
<span class="card-helper">管理与设备通讯的硬件</span>
</div>
<div class="card-body">
<!-- 硬件操作区:匹配按钮 + 搜索进度 -->
<div class="dev-actions">
<button @click="configdev" class="btn primary" :disabled="isProcessing || isListening">
<template v-if="isProcessing">
<span class="loading-spinner"></span>匹配中...
</template>
<template v-else-if="isListening">
<span class="loading-spinner"></span>搜索中...
</template>
<template v-else>匹配硬件</template>
</button>
<!-- 搜索进度条(仅搜索期显示) -->
<div v-if="isListening" class="search-progress">
<span>正在搜索硬件设备...</span>
<div class="progress-bar">
<div class="progress" :style="{ width: searchProgress + '%' }"></div>
</div>
</div>
</div>
<!-- 已匹配硬件信息 -->
<div v-if="devmac" class="matched-dev">
<h3>已匹配硬件</h3>
<div class="dev-address">
<span class="address-label">硬件地址:</span>
<span class="address-value">{{ devmac }}</span>
</div>
</div>
<!-- 硬件列表(搜索结果) -->
<div class="dev-list-section">
<h3>硬件设备列表 <span class="count-badge">{{ devlist.length }}</span></h3>
<!-- 空状态 -->
<div v-if="devlist.length === 0" class="empty-state">
<div class="empty-icon">🔍</div>
<p>暂无硬件设备,请点击"匹配硬件"按钮搜索</p>
</div>
<!-- 硬件列表 -->
<ul class="dev-list">
<li
v-for="(item, index) in devlist"
:key="item"
:class="{
'dev-item': true,
'processing': isProcessing && currentIndex === index, // 正在处理的硬件
'matched': devmac === item // 已匹配的硬件
}"
>
<span class="dev-name">{{ item }}</span>
<span v-if="isProcessing && currentIndex === index" class="processing-indicator">
<span class="spinner"></span>处理中...
</span>
<span v-if="devmac === item" class="matched-indicator">✓ 已匹配</span>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>模块 2:核心逻辑实现(Script Setup)
这部分是整个功能的核心,包含 MQTT 连接管理、硬件搜索、硬件匹配、资源清理 四大关键逻辑,使用 Vue3 Composition API 组织代码,逻辑更聚合。
2.1 初始化响应式变量与常量
首先定义 MQTT 基础配置、核心业务变量、流程控制变量,统一管理状态。
<script setup>
import { ref, watch, onUnmounted } from 'vue'
import mqtt from 'mqtt'; // 引入MQTT.js
// 1. MQTT基础配置(常量,可根据实际环境修改)
const MQTT_BASE_CONFIG = {
server: '127.0.0.1', // MQTT服务器地址(本地测试)
port: 3333 // MQTT服务器端口
}
// 2. MQTT核心变量
const client = ref(null); // MQTT客户端实例
const isConnected = ref(false); // MQTT连接状态
const mqttConfig = ref({
...MQTT_BASE_CONFIG,
options: {
clientId: `device-client-${Math.random().toString(16).slice(2, 8)}`, // 随机生成客户端ID
clean: true, // 清理会话(断开后不保留订阅)
connectTimeout: 4000 // 连接超时时间(4秒)
}
});
// 3. 业务核心变量
const key = ref('1111111'); // 通讯关键字(用于生成唯一主题)
const sendTopic = ref(''); // MQTT发送主题
const receiveTopic = ref('');// MQTT接收主题
const devlist = ref([]); // 搜索到的硬件列表
const devmac = ref(''); // 已匹配的硬件地址(MAC地址或唯一标识)
const searchProgress = ref(0); // 硬件搜索进度(0-100%)
// 4. 流程控制变量
const isListening = ref(false); // 是否处于硬件搜索期
const currentIndex = ref(0); // 当前处理的硬件索引(用于逐个匹配)
const isProcessing = ref(false); // 是否正在匹配硬件
const messageHandlers = ref([]); // MQTT消息处理器(用于卸载时清理)
// 5. 定时器统一管理(避免分散声明导致内存泄漏)
const timers = ref({
statusCheck: null, // 搜索超时定时器
timeout: null, // 单个硬件匹配超时定时器
searchInterval: null// 搜索进度更新定时器
});
</script>2.2 MQTT 主题动态更新
通讯关键字(key)是硬件与前端通讯的 “唯一标识”,需监听其变化,动态生成发送 / 接收主题(避免不同设备消息冲突)。
// 更新MQTT主题:根据通讯关键字生成唯一主题
const updateTopics = (newKey) => {
if (!newKey) return; // 关键字为空时不更新
sendTopic.value = `/topic1/${newKey}`; // 前端→硬件的主题
receiveTopic.value = `/topic2/${newKey}`; // 硬件→前端的主题
}
// 监听关键字变化,立即更新主题(immediate: true 初始加载时执行)
watch(key, updateTopics, { immediate: true })2.3 MQTT 连接生命周期管理
实现 MQTT 客户端的连接、重连、关闭、消息监听逻辑,确保通讯稳定性。
// 连接MQTT服务器
const connectMqtt = () => {
// 1. 断开现有连接(避免重复连接)
if (client.value) client.value.end();
// 2. 拼接MQTT连接地址(WebSocket协议,格式:ws://server:port/mqtt)
const url = `ws://${mqttConfig.value.server}:${mqttConfig.value.port}/mqtt`;
client.value = mqtt.connect(url, mqttConfig.value.options);
// 3. 连接成功回调
client.value.on('connect', () => {
console.log('MQTT连接成功');
isConnected.value = true;
// 订阅接收主题(接收硬件发送的消息)
client.value.subscribe(receiveTopic.value, (err) => {
err ? console.error('订阅失败:', err) : console.log(`订阅成功: ${receiveTopic.value}`);
});
});
// 4. 接收硬件消息(仅在搜索期处理硬件列表)
client.value.on('message', (topic, message) => {
if (isListening.value && topic === receiveTopic.value) {
const msg = JSON.parse(message.toString());
// 若消息包含硬件地址且未在列表中,加入列表
if (msg.config && !devlist.value.includes(msg.config)) {
devlist.value.push(msg.config);
console.log(`新增硬件: ${msg.config}`);
}
}
});
// 5. 错误与断开处理
client.value.on('error', (err) => {
console.error('MQTT连接错误:', err);
isConnected.value = false;
});
client.value.on('reconnect', () => console.log('MQTT正在重连...'));
client.value.on('close', () => {
console.log('MQTT连接关闭');
isConnected.value = false;
});
};
// 监听主题变化,重新连接MQTT(确保订阅最新主题)
watch(
[sendTopic, receiveTopic],
([newSend, newReceive], [oldSend, oldReceive]) => {
if (newSend && newReceive && newSend !== oldSend && newReceive !== oldReceive) {
connectMqtt();
}
},
{ immediate: true } // 初始加载时连接MQTT
);2.4 硬件搜索与匹配逻辑
这是最复杂的部分,需实现 “触发搜索 → 显示进度 → 逐个匹配 → 确认结果” 的完整流程,核心是 定时器控制 和 异步递归处理。
// 发送MQTT消息(通用函数,封装发布逻辑)
const sendMessage = (topic, message) => {
if (!isConnected.value || !client.value) return; // 未连接时不发送
client.value.publish(topic, message, (err) => {
err ? console.error('消息发送失败:', err) : console.log('消息发送成功:', message);
});
};
// 处理单个硬件匹配(返回Promise,成功true/失败false)
const processSingledev = (dev) => {
return new Promise((resolve) => {
// 1. 清理上一轮残留资源(定时器、消息监听)
if (timers.value.timeout) clearTimeout(timers.value.timeout);
messageHandlers.value.forEach(handler => client.value?.off('message', handler));
messageHandlers.value = [];
// 2. 订阅当前硬件的专属主题(用于接收匹配响应)
client.value.subscribe(`/topic2/${dev}`, (err) => {
if (err) {
console.error(`订阅硬件 ${dev} 失败:`, err);
resolve(false);
return;
}
console.log(`已订阅硬件 ${dev} 主题: /topic2/${dev}`);
// 3. 向前端发送“匹配指令”(pressdown为自定义指令,需与硬件协商)
client.value.publish(`/topic1/${dev}`, JSON.stringify({ operation: 'pressdown' }), (err) => {
if (err) {
console.error(`向硬件 ${dev} 发送指令失败:`, err);
resolve(false);
return;
}
console.log(`已向硬件 ${dev} 发送匹配指令`);
});
// 4. 监听硬件的匹配响应
const messageHandler = (topic, message) => {
if (topic !== `/topic2/${dev}`) return; // 只处理当前硬件的消息
const msg = JSON.parse(message.toString());
console.log('收到硬件响应:', msg);
// 硬件返回“confirm: confirm”表示匹配成功
if (msg.confirm === 'confirm') {
devmac.value = dev; // 记录已匹配硬件地址
client.value.publish(`/topic1/${dev}`, JSON.stringify({ confirm: 'ok' })); // 发送确认
console.log(`硬件 ${dev} 匹配成功`);
clearTimeout(timers.value.timeout); // 清除超时定时器
resolve(true);
}
};
client.value.on('message', messageHandler);
messageHandlers.value.push(messageHandler); // 记录处理器,用于后续清理
// 5. 10秒超时控制(硬件未响应则视为匹配失败)
timers.value.timeout = setTimeout(() => {
console.log(`硬件 ${dev} 超时未响应`);
resolve(false);
}, 10000);
});
});
};
// 递归处理硬件队列(逐个匹配,直到找到目标硬件或遍历完成)
const processdevQueue = async () => {
// 终止条件:已匹配到硬件 或 所有硬件处理完毕
if (devmac.value || currentIndex.value >= devlist.value.length) {
isProcessing.value = false;
console.log(devmac.value ? '匹配成功' : '所有硬件处理完毕,未找到匹配项');
return;
}
isProcessing.value = true;
const currentdev = devlist.value[currentIndex.value];
console.log(`处理第 ${currentIndex.value + 1} 个硬件: ${currentdev}`);
// 处理当前硬件,成功则终止,失败则继续下一个
const isSuccess = await processSingledev(currentdev);
if (isSuccess) {
isProcessing.value = false;
return;
}
currentIndex.value++; // 索引+1,处理下一个硬件
processdevQueue(); // 递归调用
};
// 监听搜索状态与硬件列表,自动触发匹配(搜索到硬件后立即开始匹配)
watch([isListening, devlist], () => {
if (devlist.value.length > 0 && !devmac.value && !isProcessing.value) {
console.log('开始逐个匹配硬件...');
currentIndex.value = 0; // 重置索引
processdevQueue();
}
});
// 「匹配硬件」按钮点击事件(触发搜索流程)
const configdev = () => {
// 1. 清理所有残留资源(避免上一轮影响)
Object.values(timers.value).forEach(timer => timer && clearTimeout(timer));
messageHandlers.value.forEach(handler => client.value?.off('message', handler));
// 2. 重置状态
devmac.value = '';
devlist.value = [];
currentIndex.value = 0;
isProcessing.value = false;
searchProgress.value = 0;
// 3. 发送“搜索硬件”指令(status: configdev 为自定义指令)
sendMessage(sendTopic.value, JSON.stringify({ status: 'configdev' }));
// 4. 启动10秒搜索期
isListening.value = true;
console.log('开始10秒硬件搜索...');
// 5. 实时更新搜索进度(每100ms更新一次)
let elapsed = 0;
timers.value.searchInterval = setInterval(() => {
elapsed += 100;
searchProgress.value = Math.min(100, (elapsed / 10000) * 100); // 进度不超过100%
}, 100);
// 6. 搜索超时处理(10秒后结束搜索)
timers.value.statusCheck = setTimeout(() => {
isListening.value = false;
clearInterval(timers.value.searchInterval);
console.log(`搜索结束,共发现 ${devlist.value.length} 个硬件`);
}, 10000);
};2.5 资源清理(避免内存泄漏)
Vue3 组件卸载时,需清理 定时器、MQTT 消息监听、MQTT 连接,防止内存泄漏。
// 组件卸载钩子:清理所有资源
onUnmounted(() => {
// 清理所有定时器
Object.values(timers.value).forEach(timer => timer && clearTimeout(timer));
// 移除所有MQTT消息监听
messageHandlers.value.forEach(handler => client.value?.off('message', handler));
// 断开MQTT连接
client.value?.end();
});四、常见问题与解决方案
在实际开发中,可能会遇到以下问题,这里提供对应的解决方案:
1. MQTT 连接失败
- 原因 1:服务器地址 / 端口错误:确认 MQTT 服务器是否启动,地址(
127.0.0.1)和端口(3333)是否与实际一致; - 原因 2:跨域问题:若前端与 MQTT 服务器不在同一域名,需在 MQTT 服务器配置跨域允许(例如 EMQ X 服务器在 Dashboard 中设置 CORS);
- 原因 3:客户端 ID 重复:代码中通过
Math.random()生成随机clientId,避免重复,若需固定 ID,需确保唯一性。
2. 硬件搜索不到
- 原因 1:指令不匹配:确认
sendMessage发送的指令(status: 'configdev')与硬件端的指令解析逻辑一致; - 原因 2:主题错误:检查硬件发送的消息主题是否与前端
receiveTopic一致(需与硬件端协商统一); - 原因 3:硬件未联网:确保硬件已连接到与 MQTT 服务器同一网络。
3. 硬件匹配超时
- 解决方案 1:延长超时时间:将
processSingledev中的10000(10 秒)改为更长时间(如20000); - 解决方案 2:增加重试机制:在匹配失败后,增加 1-2 次重试逻辑,避免网络波动导致的误判;
- 解决方案 3:检查指令格式:确认发送的匹配指令(
operation: 'pressdown')和硬件响应格式(confirm: 'confirm')是否正确。
五、总结与优化方向
1. 功能总结
本实例实现了 Vue3 与硬件设备的 MQTT 通讯全流程,核心亮点包括:
MQTT 生命周期管理:连接、重连、关闭、订阅 / 发布消息的完整逻辑; 硬件搜索与匹配:定时器控制搜索进度,异步递归处理硬件匹配,确保流程严谨;
2. 后续优化方向
- 错误提示可视化:当前错误仅在控制台打印,可增加弹窗或 Toast 提示用户(如 MQTT 连接失败、硬件匹配超时);
- 硬件匹配重试:增加手动重试按钮,允许用户重新匹配未成功的硬件;
- MQTT 连接状态持久化:使用
localStorage保存 MQTT 配置,页面刷新后自动恢复连接; - 多硬件管理:支持匹配多个硬件,展示多个已匹配硬件地址,实现多设备通讯
- 资源清理:组件卸载时清理定时器、消息监听、MQTT 连接,避免内存泄漏;
- 用户体验优化:状态指示器、进度条、空状态提示,提升页面交互友好性。
完整代码:
<template>
<div class="room-config-container">
<!-- 通讯配置卡片 -->
<div class="card communication-card">
<div class="card-header">
<h2 class="card-title">通讯配置</h2>
<span class="card-helper">MQTT通讯相关参数</span>
</div>
<div class="card-body">
<div class="config-grid">
<div class="config-item">
<span class="config-label">通讯关键字:</span>
<span class="config-value">{{ key || '未设置' }}</span>
</div>
<div class="config-item">
<span class="config-label">发送主题:</span>
<span class="config-value">{{ sendTopic || '未设置' }}</span>
</div>
<div class="config-item">
<span class="config-label">接收主题:</span>
<span class="config-value">{{ receiveTopic || '未设置' }}</span>
</div>
<div class="config-item">
<span class="config-label">MQTT连接状态:</span>
<span class="config-value">
<span
:class="isConnected ? 'status-indicator connected' : 'status-indicator disconnected'"
:title="isConnected ? '已连接' : '未连接'"></span>
{{ isConnected ? '已连接' : '未连接' }}
</span>
</div>
</div>
</div>
</div>
<!-- 硬件配置区域 -->
<div class="card dev-config-card">
<div class="card-header">
<h2 class="card-title">硬件配置</h2>
<span class="card-helper">管理与设备通讯的硬件</span>
</div>
<div class="card-body">
<div class="dev-actions">
<button @click="configdev" class="btn primary" :disabled="isProcessing || isListening">
<template v-if="isProcessing">
<span class="loading-spinner"></span>
匹配中...
</template>
<template v-else-if="isListening">
<span class="loading-spinner"></span>
搜索中...
</template>
<template v-else>
匹配硬件
</template>
</button>
<div v-if="isListening" class="search-progress">
<span>正在搜索硬件设备...</span>
<div class="progress-bar">
<div class="progress" :style="{ width: searchProgress + '%' }"></div>
</div>
</div>
</div>
<!-- 已匹配硬件信息 -->
<div v-if="devmac" class="matched-dev">
<h3>已匹配硬件</h3>
<div class="dev-address">
<span class="address-label">硬件地址:</span>
<span class="address-value">{{ devmac }}</span>
</div>
</div>
<!-- 硬件列表 -->
<div class="dev-list-section">
<h3>硬件设备列表 <span class="count-badge">{{ devlist.length }}</span></h3>
<div v-if="devlist.length === 0" class="empty-state">
<div class="empty-icon">🔍</div>
<p>暂无硬件设备,请点击"匹配硬件"按钮搜索</p>
</div>
<ul class="dev-list">
<li v-for="(item, index) in devlist" :key="item" :class="{
'dev-item': true,
'processing': isProcessing && currentIndex === index,
'matched': devmac === item
}">
<span class="dev-name">{{ item }}</span>
<span v-if="isProcessing && currentIndex === index" class="processing-indicator">
<span class="spinner"></span>
处理中...
</span>
<span v-if="devmac === item" class="matched-indicator">✓ 已匹配</span>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, watch, onUnmounted } from 'vue'
import mqtt from 'mqtt';
// MQTT基础配置(常量)
const MQTT_BASE_CONFIG = {
server: '127.0.0.1', // MQTT服务器地址
port: 3333 // MQTT服务器端口
}
// MQTT核心变量(仅保留必要项)
const client = ref(null);
const isConnected = ref(false);
const mqttConfig = ref({
...MQTT_BASE_CONFIG,
options: {
clientId: `device-client-${Math.random().toString(16).slice(2, 8)}`,
clean: true,
connectTimeout: 4000
}
});
// 业务核心变量(删除未使用的hasNewDevice)
const key = ref('1111111'); // 通讯关键字
const sendTopic = ref(''); // 发送主题
const receiveTopic = ref('');// 接收主题
const devlist = ref([]); // 硬件设备列表
const devmac = ref(''); // 已匹配硬件地址
const searchProgress = ref(0); // 搜索进度
// 流程控制变量(仅保留必要项)
const isListening = ref(false); // 是否处于硬件搜索期
const currentIndex = ref(0); // 当前处理的硬件索引
const isProcessing = ref(false); // 是否正在匹配硬件
const messageHandlers = ref([]); // MQTT消息处理器(用于清理)
// 定时器统一管理(避免分散声明)
const timers = ref({
statusCheck: null, // 搜索超时定时器
timeout: null, // 单个硬件超时定时器
searchInterval: null// 进度更新定时器
});
// 更新MQTT主题(核心逻辑保留)
const updateTopics = (newKey) => {
if (!newKey) return;
sendTopic.value = `/topic1/${newKey}`;
receiveTopic.value = `/topic2/${newKey}`;
}
// 监听关键字变化,同步更新主题
watch(key, updateTopics, { immediate: true })
// 处理单个硬件匹配(核心逻辑保留,简化定时器清理)
const processSingledev = (dev) => {
return new Promise((resolve) => {
// 1. 清理上一轮残留资源
if (timers.value.timeout) clearTimeout(timers.value.timeout);
messageHandlers.value.forEach(handler => client.value?.off('message', handler));
messageHandlers.value = [];
// 2. 订阅当前硬件主题
client.value.subscribe(`/topic2/${dev}`, (err) => {
if (err) {
console.error(`订阅硬件 ${dev} 失败:`, err);
resolve(false);
return;
}
console.log(`已订阅硬件 ${dev} 主题: /topic2/${dev}`);
// 3. 发送匹配指令
client.value.publish(`/topic1/${dev}`, JSON.stringify({ operation: 'pressdown' }), (err) => {
if (err) {
console.error(`向硬件 ${dev} 发送指令失败:`, err);
resolve(false);
return;
}
console.log(`已向硬件 ${dev} 发送匹配指令`);
});
// 4. 监听硬件响应
const messageHandler = (topic, message) => {
if (topic !== `/topic2/${dev}`) return;
const msg = JSON.parse(message.toString());
console.log('收到硬件响应:', msg);
if (msg.confirm === 'confirm') {
devmac.value = dev;
client.value.publish(`/topic1/${dev}`, JSON.stringify({ confirm: 'ok' }));
console.log(`硬件 ${dev} 匹配成功`);
clearTimeout(timers.value.timeout);
resolve(true);
}
};
client.value.on('message', messageHandler);
messageHandlers.value.push(messageHandler);
// 5. 10秒超时控制
timers.value.timeout = setTimeout(() => {
console.log(`硬件 ${dev} 超时未响应`);
resolve(false);
}, 10000);
});
});
};
// 递归处理硬件队列(核心逻辑保留)
const processdevQueue = async () => {
if (devmac.value || currentIndex.value >= devlist.value.length) {
isProcessing.value = false;
console.log(devmac.value ? '匹配成功' : '所有硬件处理完毕,未找到匹配项');
return;
}
isProcessing.value = true;
const currentdev = devlist.value[currentIndex.value];
console.log(`处理第 ${currentIndex.value + 1} 个硬件: ${currentdev}`);
const isSuccess = await processSingledev(currentdev);
if (isSuccess) {
isProcessing.value = false;
return;
}
currentIndex.value++;
processdevQueue();
};
// 监听搜索状态与硬件列表,自动触发匹配
watch([isListening, devlist], () => {
if (devlist.value.length > 0 && !devmac.value && !isProcessing.value) {
console.log('开始逐个匹配硬件...');
currentIndex.value = 0;
processdevQueue();
}
});
// 连接MQTT(简化冗余逻辑)
const connectMqtt = () => {
// 断开现有连接
if (client.value) client.value.end();
const url = `ws://${mqttConfig.value.server}:${mqttConfig.value.port}/mqtt`;
client.value = mqtt.connect(url, mqttConfig.value.options);
// 连接成功
client.value.on('connect', () => {
console.log('MQTT连接成功');
isConnected.value = true;
client.value.subscribe(receiveTopic.value, (err) => {
err ? console.error('订阅失败:', err) : console.log(`订阅成功: ${receiveTopic.value}`);
});
});
// 接收硬件列表(仅在搜索期处理)
client.value.on('message', (topic, message) => {
if (isListening.value && topic === receiveTopic.value) {
const msg = JSON.parse(message.toString());
if (msg.config && !devlist.value.includes(msg.config)) {
devlist.value.push(msg.config);
console.log(`新增硬件: ${msg.config}`);
}
}
});
// 错误与断开处理
client.value.on('error', (err) => {
console.error('MQTT连接错误:', err);
isConnected.value = false;
});
client.value.on('reconnect', () => console.log('MQTT正在重连...'));
client.value.on('close', () => {
console.log('MQTT连接关闭');
isConnected.value = false;
});
};
// 监听主题变化,重连MQTT
watch(
[sendTopic, receiveTopic],
([newSend, newReceive], [oldSend, oldReceive]) => {
if (newSend && newReceive && newSend !== oldSend && newReceive !== oldReceive) {
connectMqtt();
}
},
{ immediate: true }
);
// 发送MQTT消息(核心功能保留)
const sendMessage = (topic, message) => {
if (!isConnected.value || !client.value) return;
client.value.publish(topic, message, (err) => {
err ? console.error('消息发送失败:', err) : console.log('消息发送成功:', message);
});
};
// 匹配硬件按钮点击事件(简化定时器管理)
const configdev = () => {
// 1. 清理所有残留资源
Object.values(timers.value).forEach(timer => timer && clearTimeout(timer));
messageHandlers.value.forEach(handler => client.value?.off('message', handler));
// 2. 重置状态
devmac.value = '';
devlist.value = [];
currentIndex.value = 0;
isProcessing.value = false;
searchProgress.value = 0;
// 3. 发送搜索指令
sendMessage(sendTopic.value, JSON.stringify({ status: 'configdev' }));
// 4. 启动10秒搜索期
isListening.value = true;
console.log('开始10秒硬件搜索...');
// 5. 更新搜索进度
let elapsed = 0;
timers.value.searchInterval = setInterval(() => {
elapsed += 100;
searchProgress.value = Math.min(100, (elapsed / 10000) * 100);
}, 100);
// 6. 搜索超时处理
timers.value.statusCheck = setTimeout(() => {
isListening.value = false;
clearInterval(timers.value.searchInterval);
console.log(`搜索结束,共发现 ${devlist.value.length} 个硬件`);
}, 10000);
};
// 组件卸载:清理所有资源(避免内存泄漏)
onUnmounted(() => {
Object.values(timers.value).forEach(timer => timer && clearTimeout(timer));
messageHandlers.value.forEach(handler => client.value?.off('message', handler));
client.value?.end();
});
</script>
<style lang="scss" scoped>
// 容器基础样式
.room-config-container {
max-width: 1200px;
margin: 0 auto;
padding: 24px;
box-sizing: border-box;
}
// 卡片通用样式(升级视觉效果)
.card {
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
margin-bottom: 24px;
overflow: hidden;
transition: box-shadow 0.3s ease, transform 0.2s ease;
// 卡片悬浮效果
&:hover {
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08);
transform: translateY(-2px);
}
// 卡片顶部色条(区分类型)
&.communication-card {
border-top: 4px solid #722ED1;
}
&.dev-config-card {
border-top: 4px solid #0FC6C2;
}
// 卡片头部
.card-header {
padding: 16px 24px;
border-bottom: 1px solid #f5f5f5;
display: flex;
justify-content: space-between;
align-items: center;
.card-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #1D2129;
}
.card-helper {
font-size: 14px;
color: #86909C;
}
}
// 卡片内容区
.card-body {
padding: 24px;
}
}
// 通讯配置 - 网格布局
.config-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
}
// 配置项样式(优化背景与间距)
.config-item {
padding: 14px 18px;
background-color: #F7F8FA;
border-radius: 8px;
transition: background-color 0.2s ease;
&:hover {
background-color: #F0F2F5;
}
.config-label {
display: block;
margin-bottom: 6px;
font-size: 14px;
color: #4E5969;
font-weight: 500;
}
.config-value {
font-family: 'Consolas', 'Monaco', monospace;
font-size: 14px;
color: #1D2129;
word-break: break-all;
}
}
// 状态指示器(优化大小与间距)
.status-indicator {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 8px;
vertical-align: middle;
transition: background-color 0.3s ease;
&.connected {
background-color: #00B42A;
box-shadow: 0 0 0 2px rgba(0, 180, 42, 0.2);
}
&.disconnected {
background-color: #F53F3F;
box-shadow: 0 0 0 2px rgba(245, 63, 63, 0.2);
}
}
// 硬件配置 - 操作区
.dev-actions {
margin-bottom: 24px;
display: flex;
flex-direction: column;
gap: 16px;
}
// 按钮样式(升级交互效果)
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 10px 24px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
border: none;
outline: none;
&.primary {
background-color: #165DFF;
color: #fff;
&:hover {
background-color: #0E42D2;
transform: translateY(-1px);
}
&:active {
transform: translateY(0);
}
&:disabled {
background-color: #84ADFF;
cursor: not-allowed;
transform: none;
}
}
}
// 搜索进度条(优化色彩)
.search-progress {
width: 100%;
max-width: 500px;
span {
display: block;
margin-bottom: 8px;
font-size: 14px;
color: #4E5969;
}
.progress-bar {
height: 8px;
background-color: #F2F3F5;
border-radius: 4px;
overflow: hidden;
.progress {
height: 100%;
background-color: #165DFF;
transition: width 0.1s linear;
}
}
}
// 已匹配硬件(优化背景色与图标)
.matched-dev {
padding: 18px;
background-color: #E8F3E8;
border-radius: 8px;
margin-bottom: 24px;
h3 {
margin-top: 0;
margin-bottom: 12px;
font-size: 16px;
color: #00B42A;
display: flex;
align-items: center;
&::before {
content: "✓";
margin-right: 8px;
font-size: 18px;
}
}
.dev-address {
display: flex;
flex-wrap: wrap;
.address-label {
font-weight: 500;
margin-right: 8px;
color: #4E5969;
}
.address-value {
font-family: 'Consolas', 'Monaco', monospace;
color: #1D2129;
word-break: break-all;
}
}
}
// 硬件列表区域
.dev-list-section {
h3 {
margin-top: 0;
margin-bottom: 16px;
font-size: 16px;
color: #1D2129;
display: flex;
align-items: center;
}
.count-badge {
margin-left: 8px;
padding: 2px 8px;
background-color: #F2F3F5;
color: #86909C;
border-radius: 12px;
font-size: 12px;
font-weight: normal;
}
}
// 硬件列表样式(优化hover与选中效果)
.dev-list {
list-style: none;
padding: 0;
margin: 0;
border: 1px solid #F2F3F5;
border-radius: 8px;
overflow: hidden;
}
.dev-item {
padding: 16px;
border-bottom: 1px solid #F2F3F5;
display: flex;
justify-content: space-between;
align-items: center;
transition: background-color 0.2s ease;
&:last-child {
border-bottom: none;
}
&:hover {
background-color: #F7F8FA;
}
&.processing {
background-color: #FFF8E6;
border-left: 3px solid #FF7D00;
}
&.matched {
background-color: #E8F3E8;
border-left: 3px solid #00B42A;
}
.dev-name {
font-family: 'Consolas', 'Monaco', monospace;
color: #1D2129;
word-break: break-all;
}
.processing-indicator {
font-size: 12px;
color: #FF7D00;
display: flex;
align-items: center;
}
.matched-indicator {
font-size: 12px;
color: #00B42A;
font-weight: 500;
}
}
// 空状态(优化间距与透明度)
.empty-state {
padding: 48px 24px;
text-align: center;
color: #86909C;
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.4;
}
p {
margin: 0;
font-size: 14px;
}
}
// 加载动画(统一大小与色彩)
.loading-spinner,
.spinner {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: white;
animation: spin 1s ease-in-out infinite;
margin-right: 8px;
}
// 处理中动画(区分颜色)
.spinner {
border-top-color: #FF7D00;
border-color: rgba(255, 125, 0, 0.3);
}
// 旋转动画
@keyframes spin {
to {
transform: rotate(360deg);
}
}
// 响应式适配(简化逻辑)
@media (max-width: 768px) {
.room-config-container {
padding: 16px;
}
.config-grid {
grid-template-columns: 1fr;
}
}
</style>总结
到此这篇关于Vue3 + MQTT实现前端与硬件设备直接通讯的文章就介绍到这了,更多相关Vue3 MQTT前端与硬件设备通讯内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
