Winkel: Machen Sie den Code für das Back-End lesbar. Bonus: APIs austauschen und Caching von Abfragen

Sehr oft liegt bei einem Projekt das Frontend-Entwicklungstempo vor dem Backend-Entwicklungstempo. In dieser Situation ergeben sich zwei Dinge:

  1. die Fähigkeit, die Front ohne Backend oder ohne separate Endpunkte zu starten;
  2. Beschreiben Sie dem Back-End, welche Endpunkte benötigt werden, das Format der Anforderung, Antwort usw.

Ich möchte meine Art der Organisation des Codes für API-Anforderungen teilen, wodurch diese beiden Probleme perfekt gelöst werden und Sie auch Anforderungen zwischenspeichern können.

Erstellen Sie die Konfigurationsdatei api.config.json im Projektstamm:

{
  "apiUrl": "https://api.example.com",
  "useFakeApiByDefault": false,
  "fakeEndPoints": ["Sample"]
}

Hier legen wir die Basis-URL für die API fest. Wenn der Parameter useFakeApiByDefault = true ist, verwendet unsere Anwendung nur Stubs anstelle aller Anforderungen. Wenn false, werden Stubs nur für Anforderungen aus dem Array fakeEndPoints verwendet.

Um JSON in den Code zu importieren, fügen wir dem Abschnitt CompilerOptions der Datei tsconfig.json zwei Zeilen hinzu:

    "resolveJsonModule": true,
    "esModuleInterop": true,

Erstellen Sie die Basisklasse BaseEndpoint.

/src/app/api/_base.endpoint.ts:

import {ApiMethod, ApiService} from '../services/api/api.service';
import * as ApiConfig from '../../../api.config.json';
import {ErrorService} from '../services/error/error.service';
import {NgModule} from '@angular/core';

@NgModule({providers: [ApiService, ErrorService]})

export abstract class BaseEndpoint<Request, ResponseModel> {
  protected url: string;
  protected name: string;
  protected method: ApiMethod = ApiMethod.Post;
  protected sampleResponse: ResponseModel;
  protected cache: boolean = false;
  protected responseCache: ResponseModel = null;

  constructor(private api: ApiService, private error: ErrorService) {
  }

  public execute(request: Request): Promise<ResponseModel> {
    if (this.cache && this.responseCache !== null) {
      return new Promise<ResponseModel>((resolve, reject) => {
        resolve(this.responseCache);
      });
    } else {
      if (ApiConfig.useFakeApiByDefault || ApiConfig.fakeEndPoints.includes(this.name)) {
        console.log('Fake Api Request:: ', this.name, request);
        console.log('Fake Api Response:: ', this.sampleResponse);
        return new Promise<ResponseModel>((resolve) => resolve(this.sampleResponse));
      } else {
        return new Promise<ResponseModel>((resolve, reject) => {
          this.api.execute(this.url, this.method, request).subscribe(
            (response: Response<ResponseModel>) => {
              if (response.status === 200) {
                if (this.cache) { this.responseCache = response.payload; }
                resolve(this.payloadMap(response.payload));
              } else {
                this.error.emit(response.error);
                reject(response.error);
              }
            }, response => {
              this.error.emit('    ')
              reject('    '));
           }
        });
      }
    }
  }

  protected payloadMap(payload: ResponseModel): ResponseModel { return payload; }
}

abstract class Response<T> {
  public status: number;
  public error: string;
  public payload: T;
}

<Request, ResponseModel> - Arten von Anforderungs- und Antwortinstanzen. Als Reaktion darauf ist die Nutzlast bereits herausgezogen.

Ich werde keine ApiService- und ErrorService-Klassen veröffentlichen, um den Beitrag nicht aufzublasen, gibt es dort nichts Besonderes. ApiService sendet http-Anfragen. Mit ErrorService können Sie Fehler abonnieren. Um Fehler anzuzeigen, müssen Sie ErrorService in der Komponente abonnieren, in der tatsächlich Fehler angezeigt werden sollen (ich signiere das Hauptlayout, in dem ich einen Modal- oder Tooltip erstelle).

Magie beginnt, wenn wir von dieser Klasse erben. Die

abgeleitete Klasse sieht folgendermaßen aus: /src/app/api/get-users.endpoint.ts

import {BaseEndpoint} from './_base.endpoint';
import {Injectable} from '@angular/core';
import {UserModel} from '../models/user.model';

@Injectable()
export class GetUsersEndpoint extends BaseEndpoint<GetUsersRequest, UserModel[]> {
  protected name = 'GetUsers';
  protected url= '/getUsers';
  protected sampleResponse = [{
      id: 0,
      name: 'Ivan',
      age: 18
  },
  {
      id: 1,
      name: 'Igor',
      age: 25
   }

  protected payloadMap(payload) {
    return payload.map(user => new UserModel(user));
  }
}

export class GetUsersRequest {
   page: number;
   limit: number;
}

Nichts weiter, aus dem Inhalt der Datei ist sofort ersichtlich, welche URL, welches Anforderungsformat (GetUserRequest), welches Antwortformat.

Wenn sich solche Dateien in einem separaten Ordner / einer separaten API befinden, entspricht jede Datei einem eigenen Endpunkt. Ich denke, Sie können diesen Ordner dem Back-End anzeigen. Wenn Sie zu faul sind, um eine Dokumentation über die API zu schreiben, können Sie theoretisch darauf verzichten. Sie können weiterhin Dateien in Ordnern / APIs entsprechend den Controllern in Ordner verteilen.

Wenn Sie dem Konfigurationsarray "fakeEndPoints" 'GetUsers' hinzufügen, erfolgt keine Anforderung, und die Antwort wird durch Stub-Daten von sampleResponse ersetzt.

Damit gefälschte Anforderungen debuggt werden können (auf der Registerkarte "Netzwerk" sehen wir natürlich nichts), habe ich vorgesehen, dass die Ausgabe der Konsole zwei Zeilen an die Konsolenklasse ausgibt:

console.log('Fake Api Request:: ', this.name, request);
console.log('Fake Api Response:: ', this.sampleResponse);

Wenn Sie die Klasseneigenschaft cache = true überschreiben, wird die Anforderung zwischengespeichert (wenn zum ersten Mal eine Anforderung an die API gestellt wird, wird immer das Ergebnis der ersten Anforderung zurückgegeben). Die Wahrheit hier ist zu finalisieren: Sie müssen das Caching nur dann zum Laufen bringen, wenn die Anforderungsparameter (der Inhalt einer Instanz der UserRequest-Klasse).

Wir definieren die payloadMap-Methode neu, wenn wir Transformationen der vom Server empfangenen Daten benötigen. Wenn die Methode nicht neu definiert wird, befinden sich die Daten aus der Nutzlast im zurückgegebenen Versprechen.

Jetzt erhalten wir die Daten von der API in der Komponente:

import {Component, OnInit, ViewChild} from '@angular/core';
import {UserModel} from '../../models/user.model';
import {GetUsersEndpoint, GetUsersRequest} from '../../api/get_users.endpoint';

@Component({
  selector: 'app-users',
  templateUrl: './users.component.html',
  styleUrls: ['./users.component.css'],
  providers: [GetUsersEndpoint]
})
export class UsersComponent implements OnInit {

  public users: UserModel[];
  public request: GetUsersRequest = {page: 1, limit: 20};

  constructor(private getUsersEndpoint: GetUsersEndpoint) {    
  }

  private load() {
    this.getUsersEndpoint.execute(this.request).then(data => {
      this.users = data;
    });
  }

  ngOnInit(): void {
    this.load();
  }
}


In einer solchen Implementierung ist es möglich, dem Kunden das Ergebnis der Vervollständigung seiner Wunschliste anzuzeigen, auch wenn das Backend für diese Wunschliste noch nicht abgeschlossen ist. Setzen Sie die erforderlichen Endpunkte auf den Stub - und Sie können die Funktionen bereits "berühren" und ein Feedback erhalten.

In den Kommentaren freue ich mich auf konstruktive Kritik daran, welche Probleme beim Erstellen von Funktionen auftreten können und welche anderen Fallstricke auftreten können.

In Zukunft möchte ich diese Lösung von Versprechen auf Async / Warten umschreiben. Ich denke, der Code wird noch eleganter.

Auf Lager gibt es mehrere weitere architektonische Lösungen für Angular, die ich in naher Zukunft teilen möchte.

All Articles