Tutoriels

MQTT avec Angular, ngx-mqtt et MQTT.js

MQTT dans Angular : configurez ngx-mqtt ou écrivez votre service injectable MQTT.js avec observables RxJS, signals, désabonnement automatique et WebSockets sûrs.

Mis à jour · 9 min de lecture

MQTT avec Angular, ngx-mqtt et MQTT.js

Il existe deux bonnes façons d'utiliser MQTT dans Angular. ngx-mqtt enveloppe MQTT.js dans un module Angular et fournit MqttService.observe(), qui renvoie des observables RxJS : le moyen le plus rapide d'amener des messages dans un composant. Ou bien vous écrivez vous-même un petit service injectable autour de MQTT.js : une soixantaine de lignes qui vous donnent un contrôle total sur la version du client, les options MQTT 5 et l'état de connexion. Les deux passent par des WebSockets sécurisés (wss://), car les navigateurs ne peuvent pas ouvrir de connexions TCP brutes.

Ce tutoriel présente les deux, branche le résultat dans un composant standalone avec des signals et le teste avec un vrai broker.

Paramètres de connexion

ParamètreValeur
URLwss://broker.connectmq.com:8084/mqtt
Hôte / port / cheminbroker.connectmq.com / 8084 / /mqtt
ProtocoleMQTT 3.1.1 ou 5
Identifiantsun compte navigateur limité aux topics de l'UI

Une app Angular servie en HTTPS doit utiliser wss:// ; ws:// est bloqué comme contenu mixte. MQTT sur WebSockets détaille le transport.

Option 1 : ngx-mqtt

Installation
npm i ngx-mqtt

ngx-mqtt est maintenu indépendamment d'Angular et fige sa propre dépendance MQTT.js. Avant de l'installer, vérifiez que ses peer dependencies correspondent à votre version d'Angular ; c'est la cause la plus fréquente d'erreurs d'installation.

Enregistrez-le une fois dans la configuration d'une app standalone :

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))],
};

Injectez ensuite MqttService là où vous avez besoin des messages :

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() partage un seul abonnement broker entre tous les observateurs du même filtre et le résilie quand le dernier s'en va. publish() renvoie un observable qui se termine quand le broker accuse réception du message ; unsafePublish() envoie sans attendre.

Option 2 : votre propre service MQTT.js

Si vous voulez la version actuelle de MQTT.js, les propriétés MQTT 5 ou simplement moins de dépendances, un service écrit à la main est simple :

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; // pas de MQTT pendant le 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);
    }
  }
}

Les choix de conception :

  • Un client par app. providedIn: 'root' fait du service un singleton : tous les composants partagent un seul WebSocket.
  • Des observables froids à comptage de références. Rien n'est abonné sur le broker tant qu'aucun composant ne s'abonne, et le topic est désabonné quand le dernier composant se désabonne. MQTT.js se réabonne seul aux topics actifs après une reconnexion.
  • Compatible SSR. Côté serveur, le service reste idle : Angular SSR ou le prerendering n'ouvrent jamais de socket.
  • Les wildcards sont résolus côté client. Le broker envoie chaque message une seule fois même si plusieurs filtres correspondent ; matches() le route vers les bons observateurs. Testez vos filtres dans le topic matcher.

Un composant avec des 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() s'abonne dans le contexte d'injection du composant et se désabonne automatiquement à sa destruction : ni ngOnDestroy, ni takeUntil. scan conserve la dernière valeur par appareil, et auditTime(100) limite les mises à jour de l'UI à dix par seconde quelle que soit la cadence des messages, ce qui garde la détection de changements peu coûteuse sur les topics chargés. (Sur les versions d'Angular antérieures à la 19, ajoutez standalone: true au décorateur.)

Si vous préférez les observables dans les templates, observe() fonctionne tout aussi bien avec le pipe async ou avec takeUntilDestroyed() dans un service.

Tests

Vérification manuelle de bout en bout

  1. Lancez ng serve et vérifiez que le composant affiche Broker: connected. S'il affiche error, ouvrez la console du navigateur : Connection refused: Not authorized signale les identifiants ou les ACL ; un handshake WebSocket en échec désigne l'URL, le port 8084 ou le chemin /mqtt.
  2. Ouvrez le client MQTT en ligne, connectez-vous au même broker et publiez {"t": 23.4} sur testmqtt/angular/sensors/room-1. La ligne apparaît en moins de 100 ms.
  3. Publiez sur testmqtt/angular/sensors/room-2 et vérifiez que les deux lignes s'affichent.
  4. Abonnez-vous à testmqtt/angular/# dans le client en ligne et cliquez sur Ping pour vérifier la publication.
  5. Quittez le composant et confirmez, dans un debugger ou le log du broker, que le topic est désabonné.

Publier des mesures de test vers votre app Angular

Tests unitaires

Les composants ne devraient pas avoir besoin d'un broker. Remplacez le service par un stub basé sur un 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)); // attendre auditTime
  fixture.detectChanges();
  expect(fixture.nativeElement.textContent).toContain('room-1');
});

Testez le service lui-même, y compris matches() et le comportement de reconnexion, contre un vrai broker dans une petite suite d'intégration. Le guide des tests avec Jest présente des modèles transposables aux test runners Angular, et MQTT avec TypeScript couvre le typage des payloads de bout en bout.

Dépannage

SymptômeCauseCorrection
npm i ngx-mqtt échoue sur un conflit de peer dependenciesBibliothèque pas encore mise à jour pour votre version d'AngularUtiliser le service MQTT.js de l'option 2
Le build SSR bloque ou expireLe client se connecte côté serveurProtéger avec isPlatformBrowser ou afterNextRender()
L'UI rame sur les topics chargésDétection de changements à chaque messageLimiter avec auditTime ou throttleTime
Les onglets se déconnectent mutuellementMême clientId partoutGénérer un client ID aléatoire par onglet

Pour d'autres messages d'erreur, voir les erreurs MQTT.js.

Angular et MQTT vont naturellement ensemble, car les deux raisonnent en flux : un service singleton possède la connexion, les composants consomment des observables ou des signals, et le nettoyage se fait quand le composant disparaît. Commencez avec ngx-mqtt s'il prend en charge votre version d'Angular, passez au service MQTT.js quand il vous faut plus de contrôle, et vérifiez chaque topic avec un second client avant de livrer.

Questions fréquentes

Faut-il utiliser ngx-mqtt ou MQTT.js directement dans Angular ?

ngx-mqtt est le démarrage le plus rapide : un appel à forRoot() et une méthode observe() qui renvoie des observables RxJS. Un petit service autour de MQTT.js vous donne la dernière version de MQTT.js, les options MQTT 5 et un contrôle total, sans dépendre d'un wrapper qui suive le rythme d'Angular.

Dois-je me désabonner des observables MQTT dans les composants Angular ?

Oui, ou utiliser un mécanisme qui le fait pour vous. toSignal(), le pipe async et takeUntilDestroyed() se désabonnent à la destruction du composant. Avec une méthode observe() à comptage de références, le topic est aussi désabonné sur le broker quand plus personne n'écoute.

Pourquoi Angular SSR bloque-t-il ou échoue-t-il quand mon app utilise MQTT ?

Le client MQTT tente d'ouvrir un WebSocket côté serveur et empêche le rendu de se terminer. Ne vous connectez que dans le navigateur : vérifiez isPlatformBrowser(PLATFORM_ID) avant de vous connecter ou démarrez la connexion dans afterNextRender().