IoT-Tutorials

Vue MQTT Client: Echtzeitdaten über WebSocket mit Vue 3

Schreiben Sie ein wiederverwendbares Vue-3-Composable rund um MQTT.js, verbinden Sie sich per wss mit einem Broker und bauen Sie ein Live-Sensor-Widget – inklusive der Reaktivitäts- und Lebenszyklus-Fallen, über die die meisten Vue-MQTT-Apps stolpern.

Vue-3-Single-File-Component mit einem Live-Sensor-Widget, gespeist von einer Vue-MQTT-Verbindung über sicheren WebSocket
Probieren Sie es auf Ihrem eigenen MQTT-Broker ausHolen Sie sich im Free-Tarif einen privaten, verwalteten MQTT-Broker und starten Sie mit fertigen Codebeispielen.
Kostenlos testen Codebeispiele Keine Kreditkarte erforderlich

Mit einem Vue MQTT Client zeigt Ihre Vue-3-App Gerätedaten in dem Moment an, in dem sie publiziert werden – ohne eine API abzufragen. In diesem Tutorial schreiben Sie ein wiederverwendbares useMqtt-Composable rund um MQTT.js, verbinden es über sicheren WebSocket mit einem Broker und bauen ein Live-Sensor-Widget mit Befehls-Button. Dabei vermeiden Sie die Reaktivitäts- und Lebenszyklusfehler, die viele Vue-MQTT-Apps langsam machen oder Verbindungen verwaisen lassen.

Wie sich Vue mit MQTT verbindet

Vue läuft im Browser, und Browser können keine TCP-Sockets öffnen – MQTT reist deshalb innerhalb eines WebSockets. MQTT.js erledigt das transparent, wenn Sie ihm eine wss://-URL übergeben. ConnectMQ-Broker stellen sicheren WebSocket auf Port 8084 mit dem Pfad /mqtt bereit:

wss://broker.connectmq.com:8084/mqtt

Der kostenlose öffentliche MQTT-Broker unter broker.connectmq.com akzeptiert anonyme Verbindungen mit jeder nicht leeren, eindeutigen Client ID und ist damit ideal für Entwicklung und Prototypen. Alles darauf ist für alle lesbar, verwenden Sie also ein zufälliges Topic-Präfix und nur Testdaten, niemals Produktivdaten. Hintergründe zum Transport finden Sie unter MQTT over WebSocket.

Projekt-Setup

npm create vue@latest vue-mqtt-demo
cd vue-mqtt-demo
npm install
npm install mqtt
npm run dev

Übernehmen Sie bei den Scaffolding-Abfragen die Standardwerte; TypeScript, Pinia und der Router sind für dieses Tutorial optional. MQTT.js v5 enthält einen Browser-Build, den Vite automatisch auflöst.

Das useMqtt-Composable schreiben

Erstellen Sie src/composables/useMqtt.js:

import { ref, reactive, readonly, onMounted, onBeforeUnmount } from "vue";
import mqtt from "mqtt";

export function useMqtt(url, options = {}, topics = []) {
  const status = ref("idle");
  const error = ref(null);
  // letzte Nachricht pro Topic: { [topic]: { value, receivedAt } }
  const messages = reactive({});
  // einfache Variable, NICHT reaktiv: Vue darf den MQTT-Client nie proxyen
  let client = null;

  function parse(payload) {
    const text = payload.toString();
    try {
      return JSON.parse(text);
    } catch {
      return text;
    }
  }

  function connect() {
    status.value = "connecting";
    client = mqtt.connect(url, {
      protocolVersion: 5,
      reconnectPeriod: 3000,
      connectTimeout: 10000,
      ...options,
    });

    client.on("connect", () => {
      status.value = "connected";
      error.value = null;
      if (topics.length) client.subscribe(topics, { qos: 0 });
    });
    client.on("reconnect", () => (status.value = "reconnecting"));
    client.on("offline", () => (status.value = "offline"));
    client.on("error", (err) => (error.value = err.message));
    client.on("message", (topic, payload) => {
      messages[topic] = { value: parse(payload), receivedAt: Date.now() };
    });
  }

  function publish(topic, message, publishOptions = { qos: 0 }) {
    if (!client || !client.connected) return false;
    const body = typeof message === "string" ? message : JSON.stringify(message);
    client.publish(topic, body, publishOptions);
    return true;
  }

  function disconnect() {
    if (client) {
      client.end(true);
      client = null;
    }
    status.value = "disconnected";
  }

  onMounted(connect);
  onBeforeUnmount(disconnect);

  return {
    status: readonly(status),
    error: readonly(error),
    messages: readonly(messages),
    publish,
    disconnect,
  };
}

Die Designentscheidungen erklärt

Entscheidung Warum
Client in einer einfachen Variable Wird der MQTT-Client in ref oder reactive verpackt, proxyt Vue seine Interna – das kostet Performance und kann das Event-Handling stören. Müssen Sie ihn im State halten, nutzen Sie shallowRef oder markRaw.
Letzter Wert pro Topic Ein wachsendes Array aller Nachrichten verursacht in dauerhaft laufenden Dashboards Speicherlecks.
Abonnieren im connect-Handler Subscriptions werden nach Reconnects automatisch wiederhergestellt.
Cleanup in onBeforeUnmount Beim Wegnavigieren wird der Socket geschlossen, statt eine Geisterverbindung zu hinterlassen, die auf Ihr Limit angerechnet wird.
readonly-Rückgabewerte Komponenten können den gemeinsamen Verbindungszustand nicht versehentlich verändern.

Vue bündelt DOM-Updates pro Tick, daher ist es bei wenigen Nachrichten pro Sekunde unproblematisch, bei jeder eingehenden Nachricht in messages zu schreiben. Publiziert ein Gerät deutlich schneller, puffern Sie die Nachrichten in einem einfachen Objekt und kopieren sie per Timer in messages, wie im Artikel zum React-MQTT-Hook beschrieben.

Ein Live-Sensor-Widget

Erstellen Sie src/components/SensorWidget.vue:

<script setup>
import { computed } from "vue";
import { useMqtt } from "../composables/useMqtt";

const props = defineProps({
  prefix: { type: String, required: true },
  device: { type: String, required: true },
});

const clientId = `vue-${Math.random().toString(16).slice(2, 10)}`;
const telemetryTopic = `${props.prefix}/${props.device}/telemetry`;

const { status, messages, publish } = useMqtt(
  "wss://broker.connectmq.com:8084/mqtt",
  { clientId },
  [telemetryTopic]
);

const reading = computed(() => messages[telemetryTopic]?.value ?? {});
const updatedAt = computed(() => {
  const ts = messages[telemetryTopic]?.receivedAt;
  return ts ? new Date(ts).toLocaleTimeString() : "never";
});

function toggleFan() {
  publish(`${props.prefix}/${props.device}/cmd`, { action: "toggle-fan" }, { qos: 1 });
}
</script>

<template>
  <div class="widget">
    <header>
      <h3>{{ device }}</h3>
      <span :class="['status', status]">{{ status }}</span>
    </header>
    <p class="big">{{ reading.temperature ?? "--" }} &deg;C</p>
    <p>Humidity: {{ reading.humidity ?? "--" }} %</p>
    <small>Updated: {{ updatedAt }}</small>
    <button :disabled="status !== 'connected'" @click="toggleFan">Toggle fan</button>
  </div>
</template>

<style scoped>
.widget { border: 1px solid #ccc; border-radius: 8px; padding: 1rem; max-width: 280px; }
.big { font-size: 2rem; margin: 0.5rem 0; }
.status.connected { color: green; }
.status.reconnecting, .status.offline { color: orange; }
</style>

Verwenden Sie es in src/App.vue:

<script setup>
import SensorWidget from "./components/SensorWidget.vue";
const prefix = "connectmq/demo/vue-c42e";
</script>

<template>
  <main>
    <h1>Vue MQTT Demo</h1>
    <SensorWidget :prefix="prefix" device="greenhouse1" />
  </main>
</template>

Die Client ID wird einmal in <script setup> erzeugt, das pro Komponenteninstanz genau einmal läuft – sie bleibt also über Re-Renders hinweg stabil.

Testdaten senden

Publizieren Sie aus einem Terminal Messwerte an den kostenlosen öffentlichen MQTT-Broker:

for i in $(seq 1 30); do
  mosquitto_pub -h broker.connectmq.com -p 1883 -i vue-pub-c42e \
    -t connectmq/demo/vue-c42e/greenhouse1/telemetry \
    -m "{\"temperature\": $((18 + RANDOM % 8)), \"humidity\": $((55 + RANDOM % 15))}"
  sleep 1
done

Das Widget aktualisiert sich jede Sekunde. Drücken Sie den Button und beobachten Sie die eintreffenden Befehle mit mosquitto_sub -h broker.connectmq.com -p 1883 -i vue-sub-c42e -v -t "connectmq/demo/vue-c42e/+/cmd".

Eine Verbindung für die ganze App

Das obige Composable öffnet eine Verbindung pro Komponente, die es verwendet. Für ein einzelnes Widget ist das in Ordnung, ein Dashboard mit zwanzig Widgets würde jedoch zwanzig Verbindungen öffnen. Zwei bessere Muster:

  1. Singleton auf Modulebene. Verschieben Sie Client und messages aus der Funktion heraus, sodass alle Aufrufe sie teilen, und zählen Sie die Nutzer, um zu wissen, wann getrennt werden soll. Einfach und ohne Abhängigkeiten.
  2. Pinia-Store. Erstellen Sie den Client in einer Store-Action, halten Sie status und messages als Store-State und lassen Sie Komponenten aus dem Store lesen. Das harmoniert gut mit den Vue Devtools und mit Schutzmechanismen für serverseitiges Rendering.

Hier eine kompakte Version des Singleton-Ansatzes, gespeichert als src/composables/useSharedMqtt.js:

import { ref, reactive, readonly, onBeforeUnmount } from "vue";
import mqtt from "mqtt";

const status = ref("idle");
const messages = reactive({});
const topicUsers = new Map();
let client = null;
let users = 0;

function ensureClient(url, options) {
  if (client) return;
  client = mqtt.connect(url, { protocolVersion: 5, reconnectPeriod: 3000, ...options });
  client.on("connect", () => {
    status.value = "connected";
    const filters = [...topicUsers.keys()];
    if (filters.length) client.subscribe(filters);
  });
  client.on("reconnect", () => (status.value = "reconnecting"));
  client.on("offline", () => (status.value = "offline"));
  client.on("message", (topic, payload) => {
    let value = payload.toString();
    try { value = JSON.parse(value); } catch { /* Text beibehalten */ }
    messages[topic] = { value, receivedAt: Date.now() };
  });
}

export function useSharedMqtt(url, options, topicFilter) {
  ensureClient(url, options);
  users++;
  const count = topicUsers.get(topicFilter) ?? 0;
  topicUsers.set(topicFilter, count + 1);
  if (count === 0 && client.connected) client.subscribe(topicFilter);

  onBeforeUnmount(() => {
    const left = topicUsers.get(topicFilter) - 1;
    if (left === 0) {
      topicUsers.delete(topicFilter);
      client?.unsubscribe(topicFilter);
    } else {
      topicUsers.set(topicFilter, left);
    }
    if (--users === 0) {
      client.end(true);
      client = null;
      status.value = "disconnected";
    }
  });

  return { status: readonly(status), messages: readonly(messages) };
}

Jedes Widget ruft useSharedMqtt mit seinem eigenen Topic-Filter auf. Der erste Aufrufer öffnet die Verbindung, jedes Topic wird genau einmal abonniert, egal wie viele Widgets es anzeigen, und das letzte Widget, das unmountet, schließt den Socket. Da messages ein einziges reaktives Objekt mit Topics als Schlüssel ist, werden die Computed Properties eines Widgets nur neu berechnet, wenn sich genau der Schlüssel ändert, den sie lesen – so bleiben auch große Dashboards reaktionsschnell.

So oder so hat jeder Browser-Tab am Ende genau eine MQTT-Verbindung. Das hält Sie deutlich innerhalb der Verbindungslimits und macht die Client-Übersicht des Brokers leichter verständlich.

Performance-Tipps für Vue MQTT

Einige Gewohnheiten halten ein Vue-MQTT-Dashboard flüssig, während es von einem Widget auf Dutzende wächst:

  • Speichern Sie den letzten Wert, leiten Sie den Rest ab. Halten Sie die Rohnachrichten im Composable und berechnen Sie Anzeigewerte wie gerundete Zahlen, Einheiten und Farben mit computed. Computed Properties werden gecacht und nur neu berechnet, wenn sich die Nachricht ändert, von der sie abhängen.
  • Begrenzen Sie die Diagrammhistorie. Führt ein Widget eine rollierende Reihe für eine Sparkline, schieben Sie neue Punkte in ein Array fester Länge und verwerfen Sie die ältesten. Unbegrenzte Arrays sind die häufigste Ursache für Dashboards, die nach ein paar Stunden langsam werden.
  • Nutzen Sie Retained Messages für die erste Anzeige. Publizieren Geräte ihren Zustand mit Retain-Flag, zeigt ein nach der Navigation gemountetes Widget sofort den aktuellen Wert statt eines leeren Platzhalters.
  • Vermeiden Sie Deep Watcher auf messages. Ein watch(messages, ..., { deep: true }) feuert bei jedem Update jedes Topics. Beobachten Sie stattdessen den einen Schlüssel, der Sie interessiert.
  • Zeigen Sie veraltete Werte an. Ein Wert, der sich seit Minuten nicht geändert hat, kann bedeuten, dass das Gerät offline ist. Die Anzeige der Aktualisierungszeit, wie im Widget, verhindert, dass Nutzer eingefrorenen Zahlen vertrauen.

Häufige Fallstricke bei Vue MQTT

  1. Der Client ist in ref() verpackt. Symptome sind träge Performance oder merkwürdige Fehler aus dem Inneren von MQTT.js. Halten Sie ihn in einer einfachen Variable oder in shallowRef.
  2. Connect-Disconnect-Schleife. Zwei Komponenten oder Tabs teilen sich eine feste Client ID. Erzeugen Sie pro Verbindung eindeutige IDs.
  3. Mixed-Content-Fehler. Die Seite wird über HTTPS ausgeliefert, die URL verwendet aber ws://. Wechseln Sie auf wss:// auf Port 8084.
  4. Nach einem kurzen Netzwerkaussetzer aktualisiert sich nichts. Die Subscriptions wurden bei einer Clean Session außerhalb des connect-Handlers angelegt.
  5. SSR-Abstürze. In Nuxt oder einem anderen SSR-Setup verbinden Sie sich nur auf dem Client, was onMounted bereits garantiert.

Produktions-Setup

Für eine echte Anwendung verbinden Sie sich mit Zugangsdaten zu einem privaten Broker. Erstellen Sie auf ConnectMQ im Portal einen Broker und übergeben Sie den generierten Benutzernamen und das Passwort über Vite-Umgebungsvariablen:

const { status, messages } = useMqtt(
  "wss://YOUR_BROKER_HOST:8084/mqtt",
  {
    clientId,
    username: import.meta.env.VITE_MQTT_USERNAME,
    password: import.meta.env.VITE_MQTT_PASSWORD,
  },
  ["factory1/+/telemetry"]
);

Im Werk abonniert das Dashboard typischerweise einen Zweig des Unified Namespace (UNS), dessen Topics der ISA-95-Hierarchie folgen, etwa acme/werk-muenchen/montage/linie-1/+/telemetry statt factory1/+/telemetry.

Bedenken Sie, dass alles, was in Frontend-Code gebündelt wird, von Nutzern gelesen werden kann. Verwenden Sie eigene Zugangsdaten für das Dashboard und senden Sie sensible Befehle über eine Backend-API statt direkt aus dem Browser. Ein verwalteter MQTT-Broker mit gültigem Let's-Encrypt-Zertifikat auf dem WebSocket-Port sorgt dafür, dass sich Browser ohne Warnungen oder zusätzliche Konfiguration verbinden.

Verwandte Tutorials

Nutzt Ihr Team auch andere Frameworks, vergleichen Sie dieses Composable mit dem Angular-MQTT-Service auf Basis von RxJS. Um Topics und Payloads zur Compile-Zeit zu prüfen, lesen Sie typsicheres MQTT in TypeScript. Der Leitfaden zu MQTT-Client-Bibliotheken behandelt Optionen außerhalb von JavaScript für Ihre Backend-Dienste, und die Dokumentation zu Vue-Composables erklärt das hier verwendete Muster im Detail.

Starten Sie mit ConnectMQ

Geben Sie Ihrer Vue-App in wenigen Minuten einen privaten Broker mit sicherem WebSocket. Registrieren Sie sich unter app.connectmq.com, wählen Sie den kostenlosen Serverless-Plan ohne Kreditkarte, erstellen Sie einen Broker und tragen Sie die Zugangsdaten in useMqtt ein. Der Live-Topic-Explorer von ConnectMQ im Portal zeigt genau, welche Nachrichten Ihre Widgets empfangen sollten.

Häufig gestellte Fragen

Welche MQTT-Bibliothek sollte ich mit Vue verwenden?

MQTT.js ist die Standardwahl. Es funktioniert im Browser über WebSocket, unterstützt MQTT 3.1.1 und 5.0 und lässt sich sauber in ein Composable wie das in diesem Artikel verpacken.

Funktioniert das mit Vue 2?

Das Composable nutzt die Composition API, die in Vue 2.7 integriert ist. Für ältere Vue-2-Versionen legen Sie dieselbe Logik in die Hooks mounted und beforeDestroy einer Komponente.

Warum zeigt mein Widget ständig „reconnecting"?

Meist verwendet eine andere Verbindung dieselbe Client ID, eine Firewall blockiert Port 8084 oder der URL fehlt der Pfad /mqtt. Prüfen Sie die Browserkonsole auf die Fehlermeldung von MQTT.js.

Kann ich aus einer Vue-App publizieren?

Ja, mit der publish-Funktion, die das Composable zurückgibt. Aktionen, die echte Anlagen beeinflussen, leiten Sie besser über ein authentifiziertes Backend, damit Browser-Nutzer keine beliebigen Nachrichten publizieren können.

Verwandte Artikel

Die besten MQTT-CLI-Tools zum Testen, Skripten und Debuggen

Ein praxisnaher Vergleich von mosquitto_pub/sub, MQTTX CLI, HiveMQ MQTT CLI, der MQTT.js-CLI, curl und mqttui – mit demselben Test gegen broker.connectmq.com, damit Sie das richtige Tool zum Skripten und Debuggen wählen.

8 Min. Lesezeit

Alle Artikel →

Ihr privater MQTT-Broker ist nur wenige Klicks entfernt

Konto anlegen, Broker-Zugangsdaten erhalten und das erste Gerät verbinden — ohne Server, die installiert, gepatcht oder skaliert werden müssen.