Vue 3'te MQTT kullanmanın temiz yolu bir composable'dır: güvenli WebSocket üzerinden paylaşılan tek bir MQTT.js client'ı, reaktif bağlantı durumunu ve bir publish metodunu sunan bir useMqtt() fonksiyonu ve bileşen yaşadığı sürece abone olup yok edildiğinde aboneliği otomatik iptal eden bir useTopic() fonksiyonu. Mesajlar shallowRef state'ine gider; böylece Vue her payload için derin proxy oluşturmaz.
Bu eğitim o composable'ı kuruyor, bir <script setup> bileşeninde kullanıyor ve gerçek bir broker'a karşı nasıl test edeceğinizi gösteriyor.
Kurulum ve bağlantı ayarları
npm i mqttMQTT.js 5, TypeScript tiplerini ve Vite ile ek ayar gerektirmeden çalışan bir tarayıcı build'ini içerir. Tarayıcılar yalnızca WebSocket üzerinden MQTT konuşur; bu yüzden URL her zaman ws:// veya wss:// olur:
| Ayar | Değer |
|---|---|
| URL | wss://broker.connectmq.com:8084/mqtt |
| Protokol | MQTT 3.1.1 veya 5 |
| Kimlik bilgileri | arayüzün gösterdiği topic'lerde okuma yetkisi olan bir tarayıcı hesabı |
HTTPS üzerinden sunulan her sayfada wss:// kullanın; ws:// mixed content olarak engellenir. WebSocket üzerinden MQTT transport'u açıklar; handshake başarısız olduğunda WebSocket bağlantısını test etmek yazısı yardımcı olur.
Composable
Client modül kapsamında yaşar; dolayısıyla useMqtt() çağıran tüm bileşenler tek bir bağlantıyı paylaşır. Basit bir sayaç, son kullanıcı ayrıldığında bağlantıyı kapatır:
import { onScopeDispose, readonly, ref, shallowRef, toValue, watch, type MaybeRefOrGetter } from 'vue';
import mqtt, { type MqttClient } from 'mqtt';
type Status = 'idle' | 'connecting' | 'connected' | 'reconnecting' | 'offline' | 'error';
type Handler = (topic: string, payload: Uint8Array) => void;
const URL = import.meta.env.VITE_MQTT_URL ?? 'wss://broker.connectmq.com:8084/mqtt';
let client: MqttClient | null = null;
let users = 0;
const status = ref<Status>('idle');
const lastError = ref<string | null>(null);
const handlers = new Map<string, Set<Handler>>();
export function matches(filter: string, topic: string): boolean {
const f = filter.split('/');
const t = topic.split('/');
for (let i = 0; i < f.length; i++) {
if (f[i] === '#') return true;
if (i >= t.length || (f[i] !== '+' && f[i] !== t[i])) return false;
}
return f.length === t.length;
}
function getClient(): MqttClient {
if (client) return client;
status.value = 'connecting';
client = mqtt.connect(URL, {
username: import.meta.env.VITE_MQTT_USER,
password: import.meta.env.VITE_MQTT_PASS,
clientId: `vue-${crypto.randomUUID().slice(0, 8)}`,
reconnectPeriod: 2000,
connectTimeout: 10_000,
});
client.on('connect', () => { status.value = 'connected'; lastError.value = null; });
client.on('reconnect', () => { status.value = 'reconnecting'; });
client.on('offline', () => { status.value = 'offline'; });
client.on('error', (err) => { status.value = 'error'; lastError.value = err.message; });
client.on('message', (topic, payload) => {
handlers.forEach((set, filter) => {
if (matches(filter, topic)) set.forEach((h) => h(topic, payload));
});
});
return client;
}
export function useMqtt() {
const c = getClient();
users++;
onScopeDispose(() => {
if (--users === 0) {
c.end(true);
client = null;
status.value = 'idle';
}
});
function subscribe(filter: string, handler: Handler): () => void {
let set = handlers.get(filter);
if (!set) {
set = new Set();
handlers.set(filter, set);
c.subscribe(filter, { qos: 1 });
}
set.add(handler);
return () => {
set!.delete(handler);
if (set!.size === 0) {
handlers.delete(filter);
c.unsubscribe(filter);
}
};
}
function publish(topic: string, message: string, retain = false) {
return c.publishAsync(topic, message, { qos: 1, retain });
}
return { status: readonly(status), error: readonly(lastError), subscribe, publish };
}
export interface TopicMessage<T> { topic: string; data: T; at: number }
export function useTopic<T = unknown>(filter: MaybeRefOrGetter<string>, max = 50) {
const { subscribe } = useMqtt();
const messages = shallowRef<TopicMessage<T>[]>([]);
const latest = shallowRef<Record<string, T>>({});
watch(
() => toValue(filter),
(f, _old, onCleanup) => {
messages.value = [];
latest.value = {};
const off = subscribe(f, (topic, payload) => {
const text = new TextDecoder().decode(payload);
let data: T;
try { data = JSON.parse(text) as T; } catch { data = text as T; }
messages.value = [{ topic, data, at: Date.now() }, ...messages.value].slice(0, max);
latest.value = { ...latest.value, [topic]: data };
});
onCleanup(off);
},
{ immediate: true },
);
return { messages, latest };
}Neden böyle yazıldı:
- Client asla reaktif değildir. Düz bir
letiçinde durur. BirMqttClient'ıref()veyareactive()ile sarmak, büyük bir event emitter'ı hiçbir fayda sağlamadan derin proxy'ye çevirir. Pinia store'una koyacaksanızmarkRaw()ile sarın. - Mesajlar için
shallowRef. Vue her payload'un her alanını değil, yalnızca referansı izler. Her güncelleme yeni bir dizi veya nesne atar; yeniden render'ı tetikleyen de tam olarak budur. - Temizlik effect scope'a bağlıdır.
onScopeDisposeve watcher'ınonCleanup'ı bileşen unmount olduğunda (veonCleanupiçin topic filtresi değiştiğinde) çalışır; böylece ne handler'lar ne de broker abonelikleri sızıntı yapar. - Abonelikler filtre başına referans sayımlıdır. Aynı topic'i gösteren iki bileşen tek bir MQTT aboneliğini paylaşır. MQTT.js yeniden bağlandıktan sonra otomatik olarak tekrar abone olur; bağlantı kurulmadan yapılan çağrılar kuyruğa alınır ve bağlanınca gönderilir.
useTopic bir ref veya getter kabul ettiğinden bir bileşen topic'i reaktif olarak değiştirebilir: watcher eski filtrenin aboneliğini iptal eder ve yenisine abone olur. Wildcard kullandığınızda filtrelerinizi topic matcher ile kontrol edin.
Bir bileşende kullanım
<script setup lang="ts">
import { computed, ref } from 'vue';
import { useMqtt, useTopic } from '@/composables/mqtt';
interface Reading { t: number; h?: number }
const room = ref('lab');
const { status, error, publish } = useMqtt();
const { latest, messages } = useTopic<Reading>(() => `testmqtt/vue/${room.value}/+`);
const rows = computed(() =>
Object.entries(latest.value).map(([topic, r]) => ({ device: topic.split('/').pop(), t: r.t })),
);
function ping() {
publish(`testmqtt/vue/${room.value}/cmd/ping`, String(Date.now()));
}
</script>
<template>
<section>
<p>Broker: {{ status }} <span v-if="error">({{ error }})</span></p>
<select v-model="room">
<option value="lab">lab</option>
<option value="office">office</option>
</select>
<table>
<tr v-for="row in rows" :key="row.device">
<td>{{ row.device }}</td>
<td>{{ row.t.toFixed(1) }} °C</td>
</tr>
</table>
<button :disabled="status !== 'connected'" @click="ping">Ping</button>
<p>{{ messages.length }} messages received</p>
</section>
</template><select> değiştirildiğinde abonelik, bağlantıya dokunmadan testmqtt/vue/lab/+'dan testmqtt/vue/office/+'a geçer.
Çok yoğun topic'lerde her mesaj yine bir render'a yol açar. Aynı panele saniyede onlarca mesaj geliyorsa bunları düz bir dizide biriktirin ve requestAnimationFrame başına bir kez shallowRef'e aktarın ya da VueUse'un useThrottleFn gibi bir yardımcısıyla hızı sınırlayın.
Bir Vue uygulamasında kimlik bilgileri
VITE_* değişkenleri bundle'a derlenir ve DevTools'u açan herkes tarafından görülebilir. Tarayıcıdaki kimlik bilgilerini herkese açık sayın:
- Dashboard topic'lerine abone olabilen ve gerekirse tek bir komut topic'ine publish edebilen tarayıcıya özel bir kullanıcı oluşturun.
- Kullanıcı bazlı erişim için backend'inizin girişten sonra kısa ömürlü kimlik bilgileri üretmesini sağlayın ve bunları
mqtt.connect'e verin (seçeneklerigetClient()'tan bir parametreye taşıyın). - Backend servislerinizin kullandığı hesabı asla tarayıcıya göndermeyin.
Tarayıcı client'larında kimlik doğrulama seçenekleri ayrıntılı olarak karşılaştırır.
Composable'ı test etmek
- Geliştirme sunucusunu başlatın ve panelin
Broker: connectedgösterdiğini kontrol edin.errorgörüyorsanız mesaja bakın:Connection refused: Not authorizedkimlik bilgilerine; bir WebSocket hatası ise URL'ye,8084portuna veya/mqttpath'ine işaret eder. - Online MQTT client'ı açın, aynı broker'a bağlanın ve
testmqtt/vue/lab/sensor-1topic'ine{"t": 22.1}publish edin. Satır anında görünür. - Select'i
officeolarak değiştirin ve tekrartestmqtt/vue/lab/sensor-1topic'ine publish edin: hiçbir şey değişmemelidir.testmqtt/vue/office/sensor-9topic'ine publish edin; yeni satır görünür. Bu, eski aboneliğin temizlendiğini kanıtlar. - Online client'ta
testmqtt/vue/#topic'ine abone olun ve publish işlemini doğrulamak için Ping'e tıklayın. - Paneli içermeyen bir route'a geçin ve hiçbir bileşen kullanmadığında bağlantının kapandığını broker logunda veya client listesinde doğrulayın.
Vue uygulamanıza test mesajları gönderin
Otomatik testler için matches() ve ayrıştırma mantığını saf fonksiyonlar olarak tutun ve Vitest ile test edin; bağlantının kendisi için gerçek bir broker'a karşı küçük bir entegrasyon testi, mock'lanmış bir client'tan çok daha fazlasını yakalar. Uygulama döngü halinde yeniden bağlanıyorsa JavaScript'te WebSocket üzerinden MQTT yeniden bağlanma yazısını ve MQTT.js hata referansını okuyun.
Sorun giderme
| Belirti | Neden | Çözüm |
|---|---|---|
| Mesajlar gelmesine rağmen arayüz güncellenmiyor | shallowRef dizisini yerinde değiştirmek (push) | Yeni bir dizi atayın veya triggerRef() çağırın |
| Her sekme diğerlerinin bağlantısını koparıyor | Sabit clientId | Her sekme için rastgele bir client ID üretin |
| Sayfadan çıktıktan sonra mesajlar hâlâ işleniyor | Handler kaldırılmamış | Yukarıdaki gibi onScopeDispose / onCleanup kullanın |
WebSocket connection to 'wss://...' failed | Yanlış port veya path, ya da engellenmiş ağ | Aynı URL'yi online client'ta deneyin |
Bir composable, MQTT'yi Vue'nun state'i beklediği yerde tutar: tek bağlantı, reaktif durum ve bileşen yaşam döngüsünü izleyen abonelikler. Gerçek mesajlar publish eden ikinci bir client ile test edin; aynı kod, üretim broker'ınıza yönlendirdiğinizde de değişmeden çalışacaktır.
Sıkça sorulan sorular
MQTT client'ını bir Vue ref'inde veya reactive nesnesinde mi saklamalıyım?
Hayır. Client, iç durumu olan büyük bir event emitter'dır; onu derin bir reaktif proxy'ye sarmak ek yük getirir ve kimlik karşılaştırmalarını bozabilir. Düz bir modül değişkeninde tutun; reaktif state içinde olması gerekiyorsa shallowRef veya markRaw kullanın.
Nuxt'ta MQTT'yi nasıl kullanırım?
Client'ı yalnızca tarayıcıda çalıştırın: composable'ı bir .client.ts plugin'inin arkasına koyun, onMounted içinde çağırın veya bileşeni <ClientOnly> ile sarın. Tarayıcı 1883 veya 8883 portlarına TCP bağlantısı açamadığı için wss:// URL'si kullanın.
Vue bileşenim yok edildikten sonra neden hâlâ MQTT mesajı alıyor?
Mesaj listener'ı veya abonelik hiç kaldırılmamıştır. Temizliği onScopeDispose ya da bir watcher'ın onCleanup'ı ile kaydedin; böylece bileşenin effect scope'u durduğunda handler kaldırılır ve topic aboneliği iptal edilir.



