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

