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
| Einstellung | Wert |
|---|---|
| URL | wss://broker.connectmq.com:8084/mqtt |
| Host / Port / Pfad | broker.connectmq.com / 8084 / /mqtt |
| Protokoll | MQTT 3.1.1 oder 5 |
| Zugangsdaten | ein 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
npm i ngx-mqttngx-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:
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:
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:
npm i mqttimport { 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
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
- Starte
ng serveund prüfe, dass die KomponenteBroker: connectedzeigt. Steht dorterror, öffne die Browser-Konsole:Connection refused: Not authorizedbedeutet Zugangsdaten oder ACLs; ein gescheiterter WebSocket-Handshake deutet auf URL, Port8084oder Pfad/mqtt. - Öffne den Online-MQTT-Client, verbinde dich mit demselben Broker und publiziere
{"t": 23.4}auftestmqtt/angular/sensors/room-1. Die Zeile erscheint innerhalb von 100 ms. - Publiziere auf
testmqtt/angular/sensors/room-2und prüfe, dass beide Zeilen angezeigt werden. - Abonniere im Online-Client
testmqtt/angular/#und klicke auf Ping, um das Publizieren zu prüfen. - 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:
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
| Symptom | Ursache | Lösung |
|---|---|---|
npm i ngx-mqtt scheitert mit Peer-Dependency-Konflikt | Bibliothek noch nicht für deine Angular-Version aktualisiert | MQTT.js-Service aus Variante 2 nutzen |
| SSR-Build hängt oder läuft in einen Timeout | Client verbindet sich auf dem Server | Mit isPlatformBrowser oder afterNextRender() absichern |
| UI ruckelt bei aktiven Topics | Change Detection bei jeder Nachricht | Mit auditTime oder throttleTime drosseln |
| Tabs trennen sich gegenseitig | Überall dieselbe clientId | Pro 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().



