Uygulamalı Eğitimler

Angular'da ngx-mqtt ve MQTT.js ile MQTT

Angular'da MQTT: ngx-mqtt'yi kurun ya da RxJS observable'ları, signal'ler, otomatik unsubscribe ve güvenli WebSocket ile kendi MQTT.js servisinizi yazın.

Güncellendi · 9 dk okuma

Angular'da ngx-mqtt ve MQTT.js ile MQTT

Angular'da MQTT kullanmanın iki iyi yolu vardır. ngx-mqtt, MQTT.js'i bir Angular modülüne sarar ve RxJS observable'ları döndüren MqttService.observe() metodunu sunar; mesajları bir bileşene getirmenin en hızlı yolu budur. Ya da MQTT.js etrafında küçük, inject edilebilir bir servisi kendiniz yazarsınız: yaklaşık altmış satır, client sürümü, MQTT 5 seçenekleri ve bağlantı durumu üzerinde tam kontrol sağlar. İkisi de güvenli WebSocket (wss://) üzerinden çalışır, çünkü tarayıcılar ham TCP bağlantısı açamaz.

Bu eğitim iki yolu da gösteriyor, sonucu signal'li standalone bir bileşene bağlıyor ve gerçek bir broker'a karşı test ediyor.

Bağlantı ayarları

AyarDeğer
URLwss://broker.connectmq.com:8084/mqtt
Host / port / pathbroker.connectmq.com / 8084 / /mqtt
ProtokolMQTT 3.1.1 veya 5
Kimlik bilgileriyalnızca arayüzün topic'lerine erişimi olan bir tarayıcı hesabı

HTTPS üzerinden sunulan bir Angular uygulaması wss:// kullanmak zorundadır; ws:// mixed content olarak engellenir. WebSocket üzerinden MQTT transport'u ayrıntılı olarak açıklar.

Seçenek 1: ngx-mqtt

Kurulum
npm i ngx-mqtt

ngx-mqtt, Angular'dan bağımsız olarak geliştirilir ve kendi MQTT.js bağımlılığını sabitler. Kurmadan önce peer dependency'lerinin Angular sürümünüzle uyumlu olduğunu kontrol edin; kurulum hatalarının en yaygın kaynağı budur.

Standalone bir uygulamanın application config'ine bir kez kaydedin:

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

Ardından mesaja ihtiyaç duyduğunuz her yerde MqttService'i inject edin:

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(), aynı filtrenin tüm gözlemcileri arasında broker'daki tek bir aboneliği paylaşır ve sonuncusu ayrıldığında aboneliği iptal eder. publish(), broker mesajı onayladığında tamamlanan bir observable döndürür; unsafePublish() ise gönderip unutur.

Seçenek 2: kendi MQTT.js servisiniz

Güncel MQTT.js sürümünü, MQTT 5 property'lerini ya da sadece daha az bağımlılık istiyorsanız, elle yazılmış bir servis oldukça basittir:

Kurulum
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; // SSR sırasında MQTT yok
    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);
    }
  }
}

Tasarım kararları:

  • Uygulama başına bir client. providedIn: 'root' servisi singleton yapar; böylece tüm bileşenler tek bir WebSocket'i paylaşır.
  • Soğuk, referans sayımlı observable'lar. Bir bileşen abone olana kadar broker'da hiçbir şeye abone olunmaz; son bileşen ayrıldığında topic aboneliği iptal edilir. MQTT.js, yeniden bağlandıktan sonra aktif topic'lere kendiliğinden tekrar abone olur.
  • SSR için güvenli. Sunucuda servis idle kalır; böylece Angular SSR veya prerendering asla soket açmaz.
  • Wildcard'lar client tarafında eşleştirilir. Birden fazla filtre eşleşse bile broker her mesajı bir kez gönderir; matches() onu doğru gözlemcilere yönlendirir. Filtrelerinizi topic matcher ile deneyin.

Signal'li bir bileşen

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() bileşenin injection context'inde abone olur ve bileşen yok edildiğinde aboneliği otomatik olarak bitirir: ne ngOnDestroy ne takeUntil gerekir. scan cihaz başına son değeri tutar; auditTime(100) ise mesajlar ne kadar hızlı gelirse gelsin arayüz güncellemelerini saniyede ona sınırlar ve yoğun topic'lerde change detection'ı ucuz tutar. (Angular 19'dan önceki sürümlerde decorator'a standalone: true ekleyin.)

Template'lerde observable tercih ediyorsanız observe(), async pipe ile veya bir serviste takeUntilDestroyed() ile de aynı şekilde çalışır.

Test

Elle uçtan uca kontrol

  1. ng serve çalıştırın ve bileşenin Broker: connected gösterdiğini doğrulayın. error gösteriyorsa tarayıcı konsolunu açın: Connection refused: Not authorized kimlik bilgilerine veya ACL'lere; başarısız bir WebSocket handshake'i ise URL'ye, 8084 portuna veya /mqtt path'ine işaret eder.
  2. Online MQTT client'ı açın, aynı broker'a bağlanın ve testmqtt/angular/sensors/room-1 topic'ine {"t": 23.4} publish edin. Satır 100 ms içinde görünür.
  3. testmqtt/angular/sensors/room-2 topic'ine publish edin ve iki satırın da göründüğünü kontrol edin.
  4. Online client'ta testmqtt/angular/# topic'ine abone olun ve publish işlemini doğrulamak için Ping'e tıklayın.
  5. Bileşenden başka bir route'a geçin ve debugger'da veya broker logunda topic aboneliğinin iptal edildiğini doğrulayın.

Angular uygulamanıza test ölçümleri publish edin

Unit test'ler

Bileşenlerin bir broker'a ihtiyacı olmamalıdır. Servisi bir Subject ile desteklenen bir stub ile değiştirin:

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)); // auditTime'ı bekle
  fixture.detectChanges();
  expect(fixture.nativeElement.textContent).toContain('room-1');
});

Servisin kendisini, matches() ve yeniden bağlanma davranışı dahil, küçük bir entegrasyon test paketinde gerçek bir broker'a karşı test edin. Jest ile test rehberi Angular test runner'larına da uygulanabilecek kalıpları gösterir; TypeScript ile MQTT ise payload'ları uçtan uca tiplemeyi anlatır.

Sorun giderme

BelirtiNedenÇözüm
npm i ngx-mqtt peer dependency çakışmasıyla başarısız oluyorKütüphane henüz Angular sürümünüz için güncellenmemişSeçenek 2'deki MQTT.js servisini kullanın
SSR build'i takılıyor veya zaman aşımına uğruyorClient sunucuda bağlanıyorisPlatformBrowser veya afterNextRender() ile koruyun
Yoğun topic'lerde arayüz yavaşlıyorHer mesajda change detectionauditTime veya throttleTime ile sınırlayın
Sekmeler birbirinin bağlantısını koparıyorHer yerde aynı clientIdHer sekme için rastgele bir client ID üretin

Diğer hata mesajları için MQTT.js hataları yazısına bakın.

Angular ve MQTT doğal olarak uyum sağlar, çünkü ikisi de akışlarla düşünür: bağlantının sahibi tek bir singleton servis, bileşenler observable veya signal tüketir ve temizlik bileşen ortadan kalktığında yapılır. Angular sürümünüzü destekliyorsa ngx-mqtt ile başlayın, daha fazla kontrole ihtiyaç duyduğunuzda MQTT.js servisine geçin ve yayına almadan önce her topic'i ikinci bir client ile doğrulayın.

Sıkça sorulan sorular

Angular'da ngx-mqtt mi yoksa doğrudan MQTT.js mi kullanmalıyım?

ngx-mqtt en hızlı başlangıçtır: tek bir forRoot() çağrısı ve RxJS observable döndüren bir observe() metodu. MQTT.js etrafında küçük bir servis ise en güncel MQTT.js'i, MQTT 5 seçeneklerini ve tam kontrolü verir; Angular sürümlerine ayak uyduran bir wrapper'a bağımlı kalmazsınız.

Angular bileşenlerinde MQTT observable'larından unsubscribe olmam gerekir mi?

Evet, ya da bunu sizin yerinize yapan bir yöntem kullanın. toSignal(), async pipe ve takeUntilDestroyed() bileşen yok edildiğinde aboneliği bitirir. Referans sayımlı bir observe() metoduyla, kimse dinlemediğinde topic aboneliği broker'da da iptal edilir.

Uygulamam MQTT kullanınca Angular SSR neden takılıyor veya hata veriyor?

MQTT client'ı sunucuda bir WebSocket açmaya çalışır ve render'ın bitmesini engeller. Yalnızca tarayıcıda bağlanın: bağlanmadan önce isPlatformBrowser(PLATFORM_ID) kontrol edin ya da bağlantıyı afterNextRender() içinde başlatın.