Angulaire: rendre le code lisible pour le back-end. Bonus: échange d'API et mise en cache des requêtes

Très souvent sur un projet, le rythme de développement frontend est en avance sur le rythme de développement backend. Dans cette situation, deux choses se produisent:

  1. la possibilité de lancer le front sans backend ou sans points de terminaison séparés;
  2. Décrivez au back-end quels points de terminaison sont nécessaires, le format de la demande, la réponse, etc.

Je souhaite partager ma façon d'organiser le code responsable des requêtes API, qui résout parfaitement ces deux problèmes, et vous permet également de mettre en cache les requêtes.

Créez le fichier de configuration api.config.json à la racine du projet:

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

Ici, nous définissons l'URL de base de l'API, si le paramètre useFakeApiByDefault = true, notre application n'utilisera que des talons au lieu de toutes les demandes. Si faux, les stubs seront utilisés uniquement pour les requêtes du tableau fakeEndPoints.

Afin d'importer JSON dans le code, nous ajoutons deux lignes à la section CompilerOptions du fichier tsconfig.json:

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

Créez la classe de base 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> - types d'instances de demande et de réponse. En réponse, la charge utile est déjà retirée.

Je ne publierai pas les classes ApiService et ErrorService, afin de ne pas gonfler le message, il n'y a rien de spécial là-bas. ApiService envoie des requêtes http, ErrorService vous permet de vous abonner aux erreurs. Pour afficher les erreurs, vous devez vous abonner à ErrorService dans le composant où nous voulons réellement afficher les erreurs (je signe la disposition principale dans laquelle je crée un modal ou une info-bulle).

La magie commence lorsque nous héritons de cette classe. La classe

dérivée ressemblera à ceci: /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;
}

Rien de plus, à partir du contenu du fichier, il est immédiatement clair quelle URL, quel format de demande (GetUserRequest), quel format de réponse.

Si ces fichiers se trouvent dans un dossier / api séparé, chaque fichier correspond à son propre point de terminaison, je pense que vous pouvez montrer ce dossier au back-end, et théoriquement, si vous êtes trop paresseux pour écrire de la documentation sur l'api, vous pouvez vous en passer. Vous pouvez toujours disperser les fichiers à l'intérieur des dossiers / api dans des dossiers en fonction des contrôleurs.

Si vous ajoutez «GetUsers» au tableau de configuration «fakeEndPoints», il n'y aura aucune demande et la réponse sera remplacée par des données de talon de sampleResponse.

Pour que les fausses demandes soient déboguées (dans l'onglet Réseau, bien sûr, nous ne verrons rien), j'ai fourni la sortie de la console pour sortir deux lignes vers la classe de console:

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

Si vous remplacez la propriété de classe cache = true, la demande sera mise en cache (la première fois qu'une demande est adressée à l'API, le résultat de la première demande est toujours renvoyé). La vérité ici est de finaliser: vous devez faire fonctionner la mise en cache uniquement si les paramètres de requête (le contenu d'une instance de la classe UserRequest).

Nous redéfinissons la méthode payloadMap si nous avons besoin de transformations des données reçues du serveur. Si la méthode n'est pas redéfinie, les données de la charge utile seront dans la promesse retournée.

Maintenant, nous obtenons les données de l'API dans le composant:

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();
  }
}


Dans une telle implémentation, il est possible de montrer au client le résultat de la réalisation de sa liste de souhaits, même si le backend n'est pas encore terminé pour ces liste de souhaits. Mettez les points d'extrémité nécessaires sur le talon - et vous pouvez déjà "toucher" les fonctionnalités et obtenir un retour.

Dans les commentaires, j'attends avec impatience une critique constructive de quels problèmes peuvent survenir lors de la construction de fonctionnalités, quels autres écueils peuvent sortir.

À l'avenir, je veux réécrire cette solution de promesses à async / wait. Je pense que le code sera encore plus élégant.

En stock, il existe plusieurs autres solutions architecturales pour Angular, dans un avenir proche, je prévois de partager.

All Articles