Angular: haga que el código sea legible para el back-end. Bonificación: intercambio de API y almacenamiento en caché de consultas

Muy a menudo en un proyecto, el ritmo de desarrollo frontend está por delante del ritmo de desarrollo de backend. En esta situación, surgen dos cosas:

  1. la capacidad de lanzar el frente sin un backend o sin puntos finales separados;
  2. Describa al back-end qué puntos finales se necesitan, el formato de la solicitud, la respuesta, etc.

Quiero compartir mi forma de organizar el código responsable de las solicitudes de API, que resuelve perfectamente estos dos problemas y también le permite almacenar en caché las solicitudes.

Cree el archivo de configuración api.config.json en la raíz del proyecto:

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

Aquí establecemos la URL base para la API, si el parámetro useFakeApiByDefault = true, entonces nuestra aplicación usará solo stubs en lugar de todas las solicitudes. Si es falso, los apéndices se usarán solo para solicitudes de la matriz fakeEndPoints.

Para importar JSON en el código, agregamos dos líneas a la sección CompilerOptions del archivo tsconfig.json:

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

Cree la clase 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>: tipos de instancias de solicitud y respuesta. En respuesta, la carga útil ya se retiró.

No publicaré las clases ApiService y ErrorService, para no inflar la publicación, no hay nada especial allí. ApiService envía solicitudes http, ErrorService le permite suscribirse a errores. Para mostrar errores, debe suscribirse a ErrorService en el componente en el que realmente queremos mostrar errores (firmo el diseño principal en el que hago un modal o una información sobre herramientas).

La magia comienza cuando heredamos de esta clase. La clase

derivada se verá así: /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;
}

Nada más, del contenido del archivo queda inmediatamente claro qué URL, qué formato de solicitud (GetUserRequest), qué formato de respuesta.

Si dichos archivos están en una carpeta / api separada, cada archivo corresponde a su propio punto final, creo que puede mostrar esta carpeta en el back-end y, en teoría, si es demasiado vago para escribir documentación en la api, puede prescindir de ella. Todavía puede dispersar archivos dentro de carpetas / api en carpetas de acuerdo con los controladores.

Si agrega 'GetUsers' a la matriz de configuración “fakeEndPoints”, no habrá ninguna solicitud y la respuesta se reemplazará con datos de código auxiliar de sampleResponse.

Para que las solicitudes falsas se depuren (en la pestaña Red, por supuesto, no veremos nada), proporcioné la salida de la consola para enviar dos líneas a la clase de consola:

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

Si anula la propiedad de clase cache = true, la solicitud se almacenará en caché (la primera vez que se realiza una solicitud a la API, siempre se devuelve el resultado de la primera solicitud). La verdad aquí es finalizar: debe hacer que el almacenamiento en caché funcione solo si los parámetros de solicitud (el contenido de una instancia de la clase UserRequest).

Redefinimos el método payloadMap si necesitamos alguna transformación de los datos recibidos del servidor. Si el método no se redefine, los datos de la carga útil se incluirán en la Promesa devuelta.

Ahora obtenemos los datos de la API en el componente:

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


En dicha implementación, es posible mostrarle al cliente el resultado de completar su lista de deseos, incluso si el back-end aún no se ha completado para estas listas de deseos. Coloque los puntos finales necesarios en el código auxiliar y ya puede "tocar" las funciones y obtener un comentario.

En los comentarios espero con interés la crítica constructiva de los problemas que pueden surgir al construir la funcionalidad, que pueden surgir otras dificultades.

En el futuro, quiero reescribir esta solución de las promesas a async / wait. Creo que el código será aún más elegante.

En stock hay varias soluciones arquitectónicas más para Angular, en un futuro próximo planeo compartir.

All Articles