Tutorials

MQTT in Angular mit ngx-mqtt und MQTT.js

MQTT in Angular: ngx-mqtt einrichten oder einen eigenen injizierbaren MQTT.js-Service mit RxJS-Observables, Signals, Auto-Unsubscribe und sicheren WebSockets bauen.

Aktualisiert · 9 Min. Lesezeit

MQTT in Angular mit ngx-mqtt und MQTT.js

Es gibt zwei gute Wege zu MQTT in Angular. ngx-mqtt verpackt MQTT.js in ein Angular-Modul und bietet MqttService.observe(), das RxJS-Observables liefert – der schnellste Weg, Nachrichten in eine Komponente zu bekommen. Oder du schreibst selbst einen kleinen injizierbaren Service um MQTT.js: rund sechzig Zeilen, die dir volle Kontrolle über die Client-Version, MQTT-5-Optionen und den Verbindungsstatus geben. Beide laufen über sichere WebSockets (wss://), weil Browser keine rohen TCP-Verbindungen öffnen können.

Dieses Tutorial zeigt beide Varianten, bindet das Ergebnis in eine Standalone-Komponente mit Signals ein und testet es gegen einen echten Broker.

Verbindungseinstellungen

EinstellungWert
URLwss://broker.connectmq.com:8084/mqtt
Host / Port / Pfadbroker.connectmq.com / 8084 / /mqtt
ProtokollMQTT 3.1.1 oder 5
Zugangsdatenein Browser-Account mit Zugriff nur auf die Topics der UI

Eine Angular-App, die über HTTPS ausgeliefert wird, muss wss:// nutzen; ws:// wird als Mixed Content blockiert. MQTT über WebSockets erklärt den Transport im Detail.

Variante 1: ngx-mqtt

Installation
npm i ngx-mqtt

ngx-mqtt wird unabhängig von Angular gepflegt und pinnt seine eigene MQTT.js-Abhängigkeit. Prüfe vor der Installation, ob die Peer Dependencies zu deiner Angular-Version passen; das ist die häufigste Ursache für Installationsfehler.

Registriere es einmal in der Application Config einer Standalone-App:

src/app/app.config.ts
import { ApplicationConfig, importProvidersFrom } from '@angular/core';
import { IMqttServiceOptions, MqttModule } from 'ngx-mqtt';

const MQTT_OPTIONS: IMqttServiceOptions = {
  hostname: 'broker.connectmq.com',
  port: 8084,
  protocol: 'wss',
  path: '/mqtt',
  username: 'username',
  password: 'password',
};

export const appConfig: ApplicationConfig = {
  providers: [importProvidersFrom(MqttModule.forRoot(MQTT_OPTIONS))],
};

Dann injizierst du MqttService überall dort, wo du Nachrichten brauchst:

src/app/status.component.ts
import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { IMqttMessage, MqttService } from 'ngx-mqtt';
import { map } from 'rxjs';

@Component({
  selector: 'app-status',
  template: `<p>Last status: {{ status() }}</p>
             <button (click)="ping()">Ping</button>`,
})
export class StatusComponent {
  private readonly mqtt = inject(MqttService);

  readonly status = toSignal(
    this.mqtt.observe('testmqtt/angular/status').pipe(map((m: IMqttMessage) => m.payload.toString())),
    { initialValue: '—' },
  );

  ping() {
    this.mqtt.unsafePublish('testmqtt/angular/cmd/ping', String(Date.now()), { qos: 1 });
  }
}

observe() teilt eine Broker-Subscription zwischen allen Beobachtern desselben Filters und bestellt sie ab, sobald der letzte verschwindet. publish() liefert ein Observable, das abschließt, wenn der Broker die Nachricht bestätigt; unsafePublish() ist Fire-and-Forget.

Variante 2: ein eigener MQTT.js-Service

Willst du das aktuelle MQTT.js-Release, MQTT-5-Properties oder einfach weniger Abhängigkeiten, ist ein selbst geschriebener Service unkompliziert:

Installation
npm i mqtt
src/app/mqtt-client.service.ts
import { Injectable, OnDestroy, PLATFORM_ID, inject } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import mqtt, { type MqttClient } from 'mqtt';
import { BehaviorSubject, Observable, Subject, filter, map } from 'rxjs';
import { environment } from '../environments/environment';

export type MqttState = 'idle' | 'connecting' | 'connected' | 'reconnecting' | 'offline' | 'error';
export interface MqttMessage<T> { topic: string; data: T }

export function matches(topicFilter: string, topic: string): boolean {
  const f = topicFilter.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;
}

@Injectable({ providedIn: 'root' })
export class MqttClientService implements OnDestroy {
  readonly state$ = new BehaviorSubject<MqttState>('idle');
  private readonly incoming$ = new Subject<{ topic: string; payload: Uint8Array }>();
  private readonly refCounts = new Map<string, number>();
  private client: MqttClient | null = null;

  constructor() {
    if (!isPlatformBrowser(inject(PLATFORM_ID))) return; // kein MQTT beim SSR
    this.state$.next('connecting');
    this.client = mqtt.connect(environment.mqtt.url, {
      username: environment.mqtt.username,
      password: environment.mqtt.password,
      clientId: `ng-${crypto.randomUUID().slice(0, 8)}`,
      reconnectPeriod: 2000,
      connectTimeout: 10_000,
    });
    this.client.on('connect', () => this.state$.next('connected'));
    this.client.on('reconnect', () => this.state$.next('reconnecting'));
    this.client.on('offline', () => this.state$.next('offline'));
    this.client.on('error', () => this.state$.next('error'));
    this.client.on('message', (topic, payload) => this.incoming$.next({ topic, payload }));
  }

  observe<T = unknown>(topicFilter: string): Observable<MqttMessage<T>> {
    return new Observable<MqttMessage<T>>((subscriber) => {
      this.retain(topicFilter);
      const sub = this.incoming$
        .pipe(
          filter((m) => matches(topicFilter, m.topic)),
          map((m) => {
            const text = new TextDecoder().decode(m.payload);
            let data: T;
            try { data = JSON.parse(text) as T; } catch { data = text as T; }
            return { topic: m.topic, data };
          }),
        )
        .subscribe(subscriber);
      return () => {
        sub.unsubscribe();
        this.release(topicFilter);
      };
    });
  }

  async publish(topic: string, message: string, retain = false): Promise<void> {
    await this.client?.publishAsync(topic, message, { qos: 1, retain });
  }

  ngOnDestroy(): void {
    this.client?.end(true);
  }

  private retain(topicFilter: string) {
    const n = this.refCounts.get(topicFilter) ?? 0;
    if (n === 0) this.client?.subscribe(topicFilter, { qos: 1 });
    this.refCounts.set(topicFilter, n + 1);
  }

  private release(topicFilter: string) {
    const n = (this.refCounts.get(topicFilter) ?? 1) - 1;
    if (n === 0) {
      this.refCounts.delete(topicFilter);
      this.client?.unsubscribe(topicFilter);
    } else {
      this.refCounts.set(topicFilter, n);
    }
  }
}

Die Designentscheidungen:

  • Ein Client pro App. providedIn: 'root' macht den Service zum Singleton, also teilen sich alle Komponenten einen WebSocket.
  • Kalte, referenzgezählte Observables. Auf dem Broker wird nichts abonniert, bis eine Komponente subscribt, und das Topic wird abbestellt, wenn die letzte Komponente geht. MQTT.js abonniert aktive Topics nach einem Reconnect selbstständig neu.
  • SSR-sicher. Auf dem Server bleibt der Service idle, sodass Angular SSR oder Prerendering nie einen Socket öffnen.
  • Wildcards werden clientseitig aufgelöst. Der Broker sendet jede Nachricht nur einmal, auch wenn mehrere Filter passen; matches() leitet sie an die richtigen Beobachter weiter. Teste deine Filter im Topic Matcher.

Eine Komponente mit Signals

src/app/sensors.component.ts
import { Component, inject } from '@angular/core';
import { DecimalPipe } from '@angular/common';
import { toSignal } from '@angular/core/rxjs-interop';
import { auditTime, map, scan } from 'rxjs';
import { MqttClientService } from './mqtt-client.service';

interface Reading { t: number; h?: number }

@Component({
  selector: 'app-sensors',
  imports: [DecimalPipe],
  template: `
    <p>Broker: {{ state() }}</p>
    <table>
      @for (row of rows(); track row.device) {
        <tr><td>{{ row.device }}</td><td>{{ row.t | number: '1.1-1' }} °C</td></tr>
      }
    </table>
    <button [disabled]="state() !== 'connected'" (click)="ping()">Ping</button>
  `,
})
export class SensorsComponent {
  private readonly mqtt = inject(MqttClientService);

  readonly state = toSignal(this.mqtt.state$, { requireSync: true });

  readonly rows = toSignal(
    this.mqtt.observe<Reading>('testmqtt/angular/sensors/+').pipe(
      scan((acc, m) => ({ ...acc, [m.topic.split('/').pop()!]: m.data.t }), {} as Record<string, number>),
      auditTime(100),
      map((latest) => Object.entries(latest).map(([device, t]) => ({ device, t }))),
    ),
    { initialValue: [] },
  );

  ping() {
    this.mqtt.publish('testmqtt/angular/cmd/ping', String(Date.now()));
  }
}

toSignal() subscribt im Injection Context der Komponente und beendet die Subscription automatisch, wenn die Komponente zerstört wird – kein ngOnDestroy, kein takeUntil. scan hält den letzten Wert pro Gerät, und auditTime(100) begrenzt UI-Updates auf zehn pro Sekunde, egal wie schnell Nachrichten eintreffen. So bleibt die Change Detection auch bei aktiven Topics günstig. (In Angular-Versionen vor 19 ergänzt du standalone: true im Decorator.)

Bevorzugst du Observables im Template, funktioniert observe() genauso mit der async-Pipe oder mit takeUntilDestroyed() in einem Service.

Testen

Manueller End-to-End-Check

  1. Starte ng serve und prüfe, dass die Komponente Broker: connected zeigt. Steht dort error, öffne die Browser-Konsole: Connection refused: Not authorized bedeutet Zugangsdaten oder ACLs; ein gescheiterter WebSocket-Handshake deutet auf URL, Port 8084 oder Pfad /mqtt.
  2. Öffne den Online-MQTT-Client, verbinde dich mit demselben Broker und publiziere {"t": 23.4} auf testmqtt/angular/sensors/room-1. Die Zeile erscheint innerhalb von 100 ms.
  3. Publiziere auf testmqtt/angular/sensors/room-2 und prüfe, dass beide Zeilen angezeigt werden.
  4. Abonniere im Online-Client testmqtt/angular/# und klicke auf Ping, um das Publizieren zu prüfen.
  5. Navigiere von der Komponente weg und bestätige im Debugger oder Broker-Log, dass das Topic abbestellt wird.

Testwerte an deine Angular-App publizieren

Unit-Tests

Komponenten sollten keinen Broker brauchen. Ersetze den Service durch einen Stub auf Basis eines Subject:

src/app/sensors.component.spec.ts
import { TestBed } from '@angular/core/testing';
import { BehaviorSubject, Subject } from 'rxjs';
import { MqttClientService, MqttMessage } from './mqtt-client.service';
import { SensorsComponent } from './sensors.component';

it('renders the latest reading per device', async () => {
  const messages = new Subject<MqttMessage<{ t: number }>>();
  await TestBed.configureTestingModule({
    imports: [SensorsComponent],
    providers: [{
      provide: MqttClientService,
      useValue: { state$: new BehaviorSubject('connected'), observe: () => messages, publish: async () => {} },
    }],
  }).compileComponents();

  const fixture = TestBed.createComponent(SensorsComponent);
  fixture.detectChanges();
  messages.next({ topic: 'testmqtt/angular/sensors/room-1', data: { t: 21 } });
  await new Promise((r) => setTimeout(r, 150)); // auf auditTime warten
  fixture.detectChanges();
  expect(fixture.nativeElement.textContent).toContain('room-1');
});

Den Service selbst, inklusive matches() und Reconnect-Verhalten, testest du in einer kleinen Integrations-Suite gegen einen echten Broker. Der Leitfaden zu Jest-Tests zeigt Muster, die sich auf Angular-Test-Runner übertragen lassen, und MQTT mit TypeScript behandelt durchgehend typisierte Payloads.

Fehlerbehebung

SymptomUrsacheLösung
npm i ngx-mqtt scheitert mit Peer-Dependency-KonfliktBibliothek noch nicht für deine Angular-Version aktualisiertMQTT.js-Service aus Variante 2 nutzen
SSR-Build hängt oder läuft in einen TimeoutClient verbindet sich auf dem ServerMit isPlatformBrowser oder afterNextRender() absichern
UI ruckelt bei aktiven TopicsChange Detection bei jeder NachrichtMit auditTime oder throttleTime drosseln
Tabs trennen sich gegenseitigÜberall dieselbe clientIdPro Tab eine zufällige Client-ID erzeugen

Weitere Fehlermeldungen findest du unter MQTT.js-Fehler.

Angular und MQTT passen natürlich zusammen, weil beide in Streams denken: Ein Singleton-Service besitzt die Verbindung, Komponenten konsumieren Observables oder Signals, und aufgeräumt wird, wenn die Komponente verschwindet. Starte mit ngx-mqtt, wenn es deine Angular-Version unterstützt, wechsle zum MQTT.js-Service, wenn du mehr Kontrolle brauchst, und prüfe jedes Topic mit einem zweiten Client, bevor du auslieferst.

Häufig gestellte Fragen

Sollte ich in Angular ngx-mqtt oder MQTT.js direkt verwenden?

ngx-mqtt ist der schnellste Einstieg: ein forRoot()-Aufruf und eine observe()-Methode, die RxJS-Observables liefert. Ein kleiner Service um MQTT.js bringt dir das aktuelle MQTT.js, MQTT-5-Optionen und volle Kontrolle – ohne darauf angewiesen zu sein, dass ein Wrapper mit Angular-Releases Schritt hält.

Muss ich MQTT-Observables in Angular-Komponenten abbestellen?

Ja – oder etwas nutzen, das es für dich erledigt. toSignal(), die async-Pipe und takeUntilDestroyed() beenden die Subscription, wenn die Komponente zerstört wird. Mit einer referenzgezählten observe()-Methode wird das Topic dann auch auf dem Broker abbestellt.

Warum hängt oder scheitert Angular SSR, wenn meine App MQTT nutzt?

Der MQTT-Client versucht, auf dem Server einen WebSocket zu öffnen, und verhindert, dass das Rendering endet. Verbinde dich nur im Browser: Prüfe isPlatformBrowser(PLATFORM_ID) vor dem Verbinden oder starte die Verbindung in afterNextRender().