Angular: torne o código legível para o back-end. Bônus: troca de APIs e cache de consultas

Muitas vezes, em um projeto, o ritmo de desenvolvimento do front-end está à frente do ritmo de desenvolvimento do back-end. Nesta situação, duas coisas surgem:

  1. a capacidade de iniciar a frente sem um back-end ou sem pontos de extremidade separados;
  2. Descreva para o back-end quais pontos de extremidade são necessários, o formato da solicitação, resposta etc.

Quero compartilhar minha maneira de organizar o código responsável pelas solicitações de API, que resolve perfeitamente esses dois problemas e também permite que você armazene em cache as solicitações.

Crie o arquivo de configuração api.config.json na raiz do projeto:

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

Aqui, definimos o URL base da API, se o parâmetro useFakeApiByDefault = true, nosso aplicativo usará apenas stubs em vez de todas as solicitações. Se falso, os stubs serão usados ​​apenas para solicitações da matriz fakeEndPoints.

Para importar JSON para o código, adicionamos duas linhas à seção CompilerOptions do arquivo tsconfig.json:

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

Crie a classe 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 instâncias de solicitação e resposta. Em resposta, a carga útil já foi retirada.

Não postarei as classes ApiService e ErrorService, para não inflar a postagem, não há nada de especial nela. O ApiService envia solicitações HTTP, o ErrorService permite que você assine erros. Para exibir erros, você precisa se inscrever no ErrorService no componente em que realmente queremos exibir erros (assino o layout principal no qual faço um modal ou uma dica de ferramenta).

A magia começa quando herdamos dessa classe. A classe

derivada terá a seguinte aparência: /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 mais, do conteúdo do arquivo fica imediatamente claro qual URL, qual formato de solicitação (GetUserRequest), qual formato de resposta.

Se esses arquivos estiverem em uma pasta / api separada, cada arquivo corresponder a seu próprio ponto de extremidade, acho que você pode mostrar essa pasta para o back-end e, teoricamente, se você tiver preguiça de escrever documentação na API, poderá fazê-lo sem ela. Você ainda pode espalhar arquivos dentro de pastas / api em pastas de acordo com os controladores.

Se você adicionar 'GetUsers' à matriz de configuração "fakeEndPoints", não haverá solicitação e a resposta será substituída pelos dados de stub do sampleResponse.

Para que solicitações falsas sejam depuradas (na guia Rede, é claro, não veremos nada), forneci a saída do console para gerar duas linhas na classe do console:

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

Se você substituir a propriedade da classe cache = true, a solicitação será armazenada em cache (na primeira vez que uma solicitação para a API for feita, o resultado da primeira solicitação será sempre retornado). A verdade vale a pena refinar: você precisa fazer o cache funcionar apenas se os parâmetros de solicitação (o conteúdo de uma instância da classe UserRequest).

Redefinimos o método payloadMap se precisarmos de transformações nos dados recebidos do servidor. Se o método não for redefinido, os dados da carga útil estarão na promessa retornada.

Agora, obtemos os dados da API no 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();
  }
}


Em tal implementação, é possível mostrar ao cliente o resultado da conclusão de sua Lista de desejos, mesmo que o back-end ainda não tenha sido concluído para essa Lista de desejos. Coloque os pontos de extremidade necessários no esboço - e você já pode "tocar" nos recursos e obter um feedback.

Nos comentários, espero críticas construtivas sobre quais problemas podem surgir ao criar funcionalidades, que outras armadilhas podem surgir.

No futuro, quero reescrever esta solução de promessas para assíncrono / aguardar. Eu acho que o código será ainda mais elegante.

Em estoque, existem várias outras soluções arquitetônicas para a Angular. Em breve, pretendo compartilhar.

All Articles