Zurück zum Blog

Observables mit Angular und Feathers

Lena Fuhrimann•

Feathers ist ein modernes API-Framework für Node.js. Es stellt seine Backend-Services als REST-API oder als WebSocket-API bereit. Um die bereitgestellten WebSockets aus einer Angular-App zu konsumieren, ist es sinnvoll, Angular-Services zu bauen, welche die jeweiligen Feathers-Services so abstrahieren, dass unsere Angular-Komponenten sie einfach nutzen können. Dieses Tutorial geht davon aus, dass du für deine App die Angular CLI verwendest.

In meinem Beispiel nutze ich einen einfachen To-do-Service und eine Komponente, die diese To-dos auflistet.

Bibliotheken einbinden

Als Erstes müssen wir die beiden Bibliotheken socket.io-client und feathers-client zu unserem Projekt hinzufügen. Das geht bequem über npm:

$ npm install --save socket.io-client feathers-client

Die TypeScript-Typings von feathers-client sind bereits in der Bibliothek enthalten. Diejenigen für socket.io-client müssen wir aber manuell hinzufügen:

$ npm install --save @types/socket.io-client

Basis-API-Service erstellen

Danach erstellen wir eine abstrakte Klasse als Basis, welche die grundlegenden Eigenschaften eines Backend-Service enthält:

// src/app/api.service.ts

export abstract class ApiService {
  private _url: string = "https://my-todos-api.com";

  get url(): string {
    return this._url;
  }
}

Datenmodell erstellen

Als Nächstes erstellen wir eine Klasse, die das Datenmodell unserer To-dos abbildet:

// src/app/todos/todo.ts

export class Todo {
  title: string;
}

Den eigentlichen Service erstellen

Dann erstellen wir den eigentlichen Service, der sich mit dem Feathers-Backend verbindet und es als Service in Angular bereitstellt. Er erbt vom Basis-Service, den wir oben erstellt haben. Der Feathers-Service wird als RxJS-Observable bereitgestellt, das unsere Komponenten dann abonnieren können:

// src/app/todos/todo.service.ts

import { Injectable } from "@angular/core";
import { Observable } from "rxjs/Observable";
import { Observer } from "rxjs/Observer";
import * as io from "socket.io-client";
import feathers from "feathers-client";

import { ApiService } from "../api.service";
import { Todo } from "./todo";

@Injectable()
export class TodoService extends APIService {
  public todos$: Observable<Todo[]>;
  private todosObserver: Observer<Todo[]>;
  private feathersService: any;
  private dataStore: {
    todos: Todo[];
  };

  constructor() {
    super();

    const socket = io(this.url);
    const client = feathers().configure(feathers.socketio(socket));
    this.feathersService = client.service("todo");

    this.feathersService.on("created", (todo) => this.onCreated(todo));
    this.feathersService.on("updated", (todo) => this.onUpdated(todo));
    this.feathersService.on("removed", (todo) => this.onRemoved(todo));

    this.todos$ = new Observable((observer) => (this.todosObserver = observer));

    this.dataStore = { todos: [] };
  }

  public find() {
    this.feathersService.find((err, todos: Todo[]) => {
      if (err) return console.error(err);

      this.dataStore.todos = todos;
      this.todosObserver.next(this.dataStore.todos);
    });
  }

  private getIndex(id: string): number {
    let foundIndex = -1;

    for (let i = 0; i < this.dataStore.todos.length; i++) {
      if (this.dataStore.todos[i].id === id) {
        foundIndex = i;
      }
    }

    return foundIndex;
  }

  private onCreated(todo: Todo) {
    this.dataStore.todos.push(todo);

    this.todosObserver.next(this.dataStore.todos);
  }

  private onUpdated(todo: Todo) {
    const index = this.getIndex(todo.id);

    this.dataStore.todos[index] = todo;

    this.todosObserver.next(this.dataStore.todos);
  }

  private onRemoved(todo) {
    const index = this.getIndex(todo.id);

    this.dataStore.todos.splice(index, 1);

    this.todosObserver.next(this.dataStore.todos);
  }
}

Aus einer Komponente konsumieren

Jetzt ist unser Angular-Service bereit. Um das Observable, das er bereitstellt, in einer Angular-Komponente zu nutzen, folgst du dieser Struktur:

// todos/todos.component.ts

import {
  ChangeDetectorRef,
  ChangeDetectionStrategy,
  Component,
  OnDestroy,
  OnInit,
} from "@angular/core";
import { Subscription } from "rxjs/Subscription";

import { TodoService } from "./todo.service";
import { Todo } from "./todo";

@Component({
  selector: "app-todos",
  providers: [TodoService],
  changeDetection: ChangeDetectionStrategy.OnPush,
  templateUrl: "./todos.component.html",
})
export class TodosComponent implements OnDestroy, OnInit {
  private todos: Todo[] = [];
  private subscription: Subscription;

  constructor(
    private todoService: TodoService,
    private ref: ChangeDetectorRef,
  ) {}

  public ngOnInit(): void {
    this.subscription = this.todoService.todos$.subscribe(
      (todos: Todo[]) => {
        this.todos = todos;
        this.ref.markForCheck();
      },
      (err) => {
        console.error(err);
      },
    );
    this.todoService.find();
  }

  public ngOnDestroy() {
    this.subscription.unsubscribe();
  }
}

Unsere Komponente TodosComponent hat jetzt eine Property todos, welche die To-dos aus dem entsprechenden Feathers-Service enthält und in der App verwendet werden kann. Sie aktualisiert das UI live, sobald in der Feathers-API ein To-do entfernt oder hinzugefügt wird.