Vue3?+?MQTT實現(xiàn)前端與硬件設備直接通訊(附完整代碼解析)
前言
在物聯(lián)網(IoT)開發(fā)場景中,前端頁面與硬件設備的實時通訊是核心需求之一。MQTT(Message Queuing Telemetry Transport)作為輕量級、低帶寬消耗的通訊協(xié)議,非常適合硬件設備與前端的雙向數(shù)據(jù)交互。本文將講解如何使用 Vue3(Composition API)+ MQTT.js 實現(xiàn)硬件設備的搜索、匹配、通訊配置管理等功能,附帶完整代碼解析。
一、項目背景與技術棧
1. 需求場景
我們需要開發(fā)一個 “硬件配置頁面”,核心功能包括:
- 展示 MQTT 通訊核心參數(shù)(通訊關鍵字、發(fā)送 / 接收主題、連接狀態(tài));
- 觸發(fā)硬件搜索,實時顯示搜索進度;
- 展示搜索到的硬件列表,支持逐個匹配硬件;
- 記錄已匹配的硬件地址,管理 MQTT 連接生命周期。
2. 技術棧選型
| 技術 / 工具 | 用途說明 |
|---|---|
| Vue3 (script setup) | 前端框架核心,使用 Composition API 組織邏輯,腳本 setup 語法簡化代碼結構 |
| MQTT.js | 實現(xiàn) MQTT 客戶端功能,負責與 MQTT 服務器建立連接、訂閱 / 發(fā)布消息 |
| SCSS | 樣式預處理器,支持嵌套、變量、動畫,提升樣式可維護性 |
二、核心概念鋪墊:MQTT 基礎
在開始代碼實現(xiàn)前,先快速回顧 MQTT 的核心概念,幫助理解后續(xù)邏輯:
- 發(fā)布 / 訂閱(Pub/Sub)模式:前端(客戶端)與硬件(客戶端)通過 MQTT 服務器中轉消息,雙方無需直接連接;
- 主題(Topic):消息的 “地址”,客戶端通過訂閱指定主題接收消息,通過發(fā)布指定主題發(fā)送消息(例如
/topic1/1111111); - 客戶端(Client):前端和硬件都是 MQTT 客戶端,需通過唯一
clientId標識; - 連接狀態(tài):客戶端與 MQTT 服務器的連接狀態(tài)(已連接 / 未連接),影響消息收發(fā)能力。
三、功能模塊拆解與實現(xiàn)
下面按 “頁面結構 → 核心邏輯 → 樣式優(yōu)化” 的順序,逐步解析完整實現(xiàn)過程。
模塊 1:頁面結構設計(Template)
頁面采用 “卡片式布局”,分為「通訊配置卡片」和「硬件配置卡片」,結構清晰。
<template>
<div class="room-config-container">
<!-- 1. 通訊配置卡片:展示MQTT核心參數(shù) -->
<div class="card communication-card">
<div class="card-header">
<h2 class="card-title">通訊配置</h2>
<span class="card-helper">MQTT通訊相關參數(shù)</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>
<!-- 發(fā)送主題 -->
<div class="config-item">
<span class="config-label">發(fā)送主題:</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連接狀態(tài)(帶視覺指示器) -->
<div class="config-item">
<span class="config-label">MQTT連接狀態(tài):</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">
<!-- 硬件操作區(qū):匹配按鈕 + 搜索進度 -->
<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>
<!-- 空狀態(tài) -->
<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:核心邏輯實現(xiàn)(Script Setup)
這部分是整個功能的核心,包含 MQTT 連接管理、硬件搜索、硬件匹配、資源清理 四大關鍵邏輯,使用 Vue3 Composition API 組織代碼,邏輯更聚合。
2.1 初始化響應式變量與常量
首先定義 MQTT 基礎配置、核心業(yè)務變量、流程控制變量,統(tǒng)一管理狀態(tài)。
<script setup>
import { ref, watch, onUnmounted } from 'vue'
import mqtt from 'mqtt'; // 引入MQTT.js
// 1. MQTT基礎配置(常量,可根據(jù)實際環(huán)境修改)
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連接狀態(tài)
const mqttConfig = ref({
...MQTT_BASE_CONFIG,
options: {
clientId: `device-client-${Math.random().toString(16).slice(2, 8)}`, // 隨機生成客戶端ID
clean: true, // 清理會話(斷開后不保留訂閱)
connectTimeout: 4000 // 連接超時時間(4秒)
}
});
// 3. 業(yè)務核心變量
const key = ref('1111111'); // 通訊關鍵字(用于生成唯一主題)
const sendTopic = ref(''); // MQTT發(fā)送主題
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. 定時器統(tǒng)一管理(避免分散聲明導致內存泄漏)
const timers = ref({
statusCheck: null, // 搜索超時定時器
timeout: null, // 單個硬件匹配超時定時器
searchInterval: null// 搜索進度更新定時器
});
</script>2.2 MQTT 主題動態(tài)更新
通訊關鍵字(key)是硬件與前端通訊的 “唯一標識”,需監(jiān)聽其變化,動態(tài)生成發(fā)送 / 接收主題(避免不同設備消息沖突)。
// 更新MQTT主題:根據(jù)通訊關鍵字生成唯一主題
const updateTopics = (newKey) => {
if (!newKey) return; // 關鍵字為空時不更新
sendTopic.value = `/topic1/${newKey}`; // 前端→硬件的主題
receiveTopic.value = `/topic2/${newKey}`; // 硬件→前端的主題
}
// 監(jiān)聽關鍵字變化,立即更新主題(immediate: true 初始加載時執(zhí)行)
watch(key, updateTopics, { immediate: true })2.3 MQTT 連接生命周期管理
實現(xiàn) MQTT 客戶端的連接、重連、關閉、消息監(jiān)聽邏輯,確保通訊穩(wěn)定性。
// 連接MQTT服務器
const connectMqtt = () => {
// 1. 斷開現(xiàn)有連接(避免重復連接)
if (client.value) client.value.end();
// 2. 拼接MQTT連接地址(WebSocket協(xié)議,格式: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;
// 訂閱接收主題(接收硬件發(fā)送的消息)
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;
});
};
// 監(jiān)聽主題變化,重新連接MQTT(確保訂閱最新主題)
watch(
[sendTopic, receiveTopic],
([newSend, newReceive], [oldSend, oldReceive]) => {
if (newSend && newReceive && newSend !== oldSend && newReceive !== oldReceive) {
connectMqtt();
}
},
{ immediate: true } // 初始加載時連接MQTT
);2.4 硬件搜索與匹配邏輯
這是最復雜的部分,需實現(xiàn) “觸發(fā)搜索 → 顯示進度 → 逐個匹配 → 確認結果” 的完整流程,核心是 定時器控制 和 異步遞歸處理。
// 發(fā)送MQTT消息(通用函數(shù),封裝發(fā)布邏輯)
const sendMessage = (topic, message) => {
if (!isConnected.value || !client.value) return; // 未連接時不發(fā)送
client.value.publish(topic, message, (err) => {
err ? console.error('消息發(fā)送失敗:', err) : console.log('消息發(fā)送成功:', message);
});
};
// 處理單個硬件匹配(返回Promise,成功true/失敗false)
const processSingledev = (dev) => {
return new Promise((resolve) => {
// 1. 清理上一輪殘留資源(定時器、消息監(jiān)聽)
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. 向前端發(fā)送“匹配指令”(pressdown為自定義指令,需與硬件協(xié)商)
client.value.publish(`/topic1/${dev}`, JSON.stringify({ operation: 'pressdown' }), (err) => {
if (err) {
console.error(`向硬件 ${dev} 發(fā)送指令失敗:`, err);
resolve(false);
return;
}
console.log(`已向硬件 ${dev} 發(fā)送匹配指令`);
});
// 4. 監(jiān)聽硬件的匹配響應
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' })); // 發(fā)送確認
console.log(`硬件 ${dev} 匹配成功`);
clearTimeout(timers.value.timeout); // 清除超時定時器
resolve(true);
}
};
client.value.on('message', messageHandler);
messageHandlers.value.push(messageHandler); // 記錄處理器,用于后續(xù)清理
// 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}`);
// 處理當前硬件,成功則終止,失敗則繼續(xù)下一個
const isSuccess = await processSingledev(currentdev);
if (isSuccess) {
isProcessing.value = false;
return;
}
currentIndex.value++; // 索引+1,處理下一個硬件
processdevQueue(); // 遞歸調用
};
// 監(jiān)聽搜索狀態(tài)與硬件列表,自動觸發(fā)匹配(搜索到硬件后立即開始匹配)
watch([isListening, devlist], () => {
if (devlist.value.length > 0 && !devmac.value && !isProcessing.value) {
console.log('開始逐個匹配硬件...');
currentIndex.value = 0; // 重置索引
processdevQueue();
}
});
// 「匹配硬件」按鈕點擊事件(觸發(fā)搜索流程)
const configdev = () => {
// 1. 清理所有殘留資源(避免上一輪影響)
Object.values(timers.value).forEach(timer => timer && clearTimeout(timer));
messageHandlers.value.forEach(handler => client.value?.off('message', handler));
// 2. 重置狀態(tài)
devmac.value = '';
devlist.value = [];
currentIndex.value = 0;
isProcessing.value = false;
searchProgress.value = 0;
// 3. 發(fā)送“搜索硬件”指令(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(`搜索結束,共發(fā)現(xiàn) ${devlist.value.length} 個硬件`);
}, 10000);
};2.5 資源清理(避免內存泄漏)
Vue3 組件卸載時,需清理 定時器、MQTT 消息監(jiān)聽、MQTT 連接,防止內存泄漏。
// 組件卸載鉤子:清理所有資源
onUnmounted(() => {
// 清理所有定時器
Object.values(timers.value).forEach(timer => timer && clearTimeout(timer));
// 移除所有MQTT消息監(jiān)聽
messageHandlers.value.forEach(handler => client.value?.off('message', handler));
// 斷開MQTT連接
client.value?.end();
});四、常見問題與解決方案
在實際開發(fā)中,可能會遇到以下問題,這里提供對應的解決方案:
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發(fā)送的指令(status: 'configdev')與硬件端的指令解析邏輯一致; - 原因 2:主題錯誤:檢查硬件發(fā)送的消息主題是否與前端
receiveTopic一致(需與硬件端協(xié)商統(tǒng)一); - 原因 3:硬件未聯(lián)網:確保硬件已連接到與 MQTT 服務器同一網絡。
3. 硬件匹配超時
- 解決方案 1:延長超時時間:將
processSingledev中的10000(10 秒)改為更長時間(如20000); - 解決方案 2:增加重試機制:在匹配失敗后,增加 1-2 次重試邏輯,避免網絡波動導致的誤判;
- 解決方案 3:檢查指令格式:確認發(fā)送的匹配指令(
operation: 'pressdown')和硬件響應格式(confirm: 'confirm')是否正確。
五、總結與優(yōu)化方向
1. 功能總結
本實例實現(xiàn)了 Vue3 與硬件設備的 MQTT 通訊全流程,核心亮點包括:
MQTT 生命周期管理:連接、重連、關閉、訂閱 / 發(fā)布消息的完整邏輯; 硬件搜索與匹配:定時器控制搜索進度,異步遞歸處理硬件匹配,確保流程嚴謹;
2. 后續(xù)優(yōu)化方向
- 錯誤提示可視化:當前錯誤僅在控制臺打印,可增加彈窗或 Toast 提示用戶(如 MQTT 連接失敗、硬件匹配超時);
- 硬件匹配重試:增加手動重試按鈕,允許用戶重新匹配未成功的硬件;
- MQTT 連接狀態(tài)持久化:使用
localStorage保存 MQTT 配置,頁面刷新后自動恢復連接; - 多硬件管理:支持匹配多個硬件,展示多個已匹配硬件地址,實現(xiàn)多設備通訊
- 資源清理:組件卸載時清理定時器、消息監(jiān)聽、MQTT 連接,避免內存泄漏;
- 用戶體驗優(yōu)化:狀態(tài)指示器、進度條、空狀態(tài)提示,提升頁面交互友好性。
完整代碼:
<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通訊相關參數(shù)</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">發(fā)送主題:</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連接狀態(tài):</span>
<span class="config-value">
<span
:class="isConnected ? 'status-indicator connected' : 'status-indicator disconnected'"
:title="isConnected ? '已連接' : '未連接'"></span>
{{ isConnected ? '已連接' : '未連接' }}
</span>
</div>
</div>
</div>
</div>
<!-- 硬件配置區(qū)域 -->
<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
}
});
// 業(yè)務核心變量(刪除未使用的hasNewDevice)
const key = ref('1111111'); // 通訊關鍵字
const sendTopic = ref(''); // 發(fā)送主題
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消息處理器(用于清理)
// 定時器統(tǒng)一管理(避免分散聲明)
const timers = ref({
statusCheck: null, // 搜索超時定時器
timeout: null, // 單個硬件超時定時器
searchInterval: null// 進度更新定時器
});
// 更新MQTT主題(核心邏輯保留)
const updateTopics = (newKey) => {
if (!newKey) return;
sendTopic.value = `/topic1/${newKey}`;
receiveTopic.value = `/topic2/${newKey}`;
}
// 監(jiān)聽關鍵字變化,同步更新主題
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. 發(fā)送匹配指令
client.value.publish(`/topic1/${dev}`, JSON.stringify({ operation: 'pressdown' }), (err) => {
if (err) {
console.error(`向硬件 ${dev} 發(fā)送指令失敗:`, err);
resolve(false);
return;
}
console.log(`已向硬件 ${dev} 發(fā)送匹配指令`);
});
// 4. 監(jiān)聽硬件響應
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();
};
// 監(jiān)聽搜索狀態(tài)與硬件列表,自動觸發(fā)匹配
watch([isListening, devlist], () => {
if (devlist.value.length > 0 && !devmac.value && !isProcessing.value) {
console.log('開始逐個匹配硬件...');
currentIndex.value = 0;
processdevQueue();
}
});
// 連接MQTT(簡化冗余邏輯)
const connectMqtt = () => {
// 斷開現(xiàn)有連接
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;
});
};
// 監(jiān)聽主題變化,重連MQTT
watch(
[sendTopic, receiveTopic],
([newSend, newReceive], [oldSend, oldReceive]) => {
if (newSend && newReceive && newSend !== oldSend && newReceive !== oldReceive) {
connectMqtt();
}
},
{ immediate: true }
);
// 發(fā)送MQTT消息(核心功能保留)
const sendMessage = (topic, message) => {
if (!isConnected.value || !client.value) return;
client.value.publish(topic, message, (err) => {
err ? console.error('消息發(fā)送失敗:', err) : console.log('消息發(fā)送成功:', message);
});
};
// 匹配硬件按鈕點擊事件(簡化定時器管理)
const configdev = () => {
// 1. 清理所有殘留資源
Object.values(timers.value).forEach(timer => timer && clearTimeout(timer));
messageHandlers.value.forEach(handler => client.value?.off('message', handler));
// 2. 重置狀態(tài)
devmac.value = '';
devlist.value = [];
currentIndex.value = 0;
isProcessing.value = false;
searchProgress.value = 0;
// 3. 發(fā)送搜索指令
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(`搜索結束,共發(fā)現(xiàn) ${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);
}
// 卡片頂部色條(區(qū)分類型)
&.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;
}
}
// 卡片內容區(qū)
.card-body {
padding: 24px;
}
}
// 通訊配置 - 網格布局
.config-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
}
// 配置項樣式(優(yōu)化背景與間距)
.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;
}
}
// 狀態(tài)指示器(優(yōu)化大小與間距)
.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);
}
}
// 硬件配置 - 操作區(qū)
.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;
}
}
}
// 搜索進度條(優(yōu)化色彩)
.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;
}
}
}
// 已匹配硬件(優(yōu)化背景色與圖標)
.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;
}
}
}
// 硬件列表區(qū)域
.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;
}
}
// 硬件列表樣式(優(yōu)化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;
}
}
// 空狀態(tài)(優(yōu)化間距與透明度)
.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;
}
}
// 加載動畫(統(tǒng)一大小與色彩)
.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;
}
// 處理中動畫(區(qū)分顏色)
.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實現(xiàn)前端與硬件設備直接通訊的文章就介紹到這了,更多相關Vue3 MQTT前端與硬件設備通訊內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Vue?echarts實例項目地區(qū)銷量趨勢堆疊折線圖實現(xiàn)詳解
Echarts,它是一個與框架無關的 JS 圖表庫,但是它基于Js,這樣很多框架都能使用它,例如Vue,估計IONIC也能用,因為我的習慣,每次新嘗試做一個功能的時候,總要新創(chuàng)建個小項目,做做Demo2022-09-09
vue中使用keep-alive動態(tài)刪除已緩存組件方式
這篇文章主要介紹了vue中使用keep-alive動態(tài)刪除已緩存組件方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-08-08
Vue 數(shù)值改變頁面沒有刷新的問題解決(數(shù)據(jù)改變視圖不更新的問題)
這篇文章主要介紹了Vue 數(shù)值改變頁面沒有刷新的問題解決(數(shù)據(jù)改變視圖不更新的問題),本文結合實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-09-09
typescript+vite項目配置別名的方法實現(xiàn)
我們?yōu)榱耸÷匀唛L的路徑,經常喜歡配置路徑別名,本文主要介紹了typescript+vite項目配置別名的方法實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2022-07-07

