Как стать автором
Обновить

Angular: делаем код читаемым для бэкендера. Бонус: подмена API заглушками и кэширование запросов

Время на прочтение4 мин
Количество просмотров3.7K
Очень часто на проекте темпы разработки фронтенда опережают темпы разработки бэкенда. При такой ситуации возникает необходимость двух вещей:

  1. возможность запускать фронт без бэкэнда, либо без отдельных эндпоинтов;
  2. описывать бэкендеру, какие нужны эндпоинты, формат запроса, ответа, итд.

Хочу поделиться своим способом организации кода, отвечающего за запросы к API, который отлично решает эти две задачи, а также позволяет кэшировать запросы.

Создаем в корне проекта файл конфигурации api.config.json:

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

Здесь прописываем базовый урл для API, если параметр useFakeApiByDefault = true, то наше приложение будет использовать только заглушки вместо всех запросов. Если false — то заглушки будут использоваться только для запросов из массива fakeEndPoints.

Чтобы можно было импортировать JSON в код, добавляем в секцию CompilerOptions файла tsconfig.json две строчки:

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

Создаем базовый класс 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> — типы экземпляров запроса и ответа. В ответ выдергивается уже payload.

Классы ApiService и ErrorService выкладывать не буду, дабы не раздувать пост, там ничего особенного. ApiService шлет http-запросы, ErrorService позволяет подписываться на ошибки. Для вывода ошибок нужно подписаться на ErrorService в том компоненте, где собственно хотим отображать ошибки (я подписываю главный layout, в котором делаю модал либо всплывающую подсказку).

Магия начинается, когда мы от этого класса наследуемся. Класс-наследник будет выглядеть так:

/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;
}

Ничего лишнего, из содержимого файла сразу понятно, какой урл, какой формат запроса (GetUserRequest), какой формат ответа.

Если в отдельной папке /api будут лежать такие файлы, каждый файл соответствует своему эндпоинту, я думаю можно показать эту папку бэкендеру, и теоретически, если очень лень писать документацию по api, можно обойтись без нее. Можно внутри папки /api файлы еще раскидать по папкам соответственно контроллерам.

Если в массив «fakeEndPoints» конфига добавить 'GetUsers', то запроса не будет, а ответ подмениться данными-заглушкой из sampleResponse.

Для того, чтобы фейковые запросы можно было отлаживать (во вкладке Network, естественно, мы ничего не увидим), я предусмотрел в базовом классе вывод в консоль двух строк:

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

Если переопределить свойство класса cache=true, то запрос будет кэшироваться (первый раз делается запрос к API, затем всегда возвращается результат первого запроса). Правда здесь стоит доработать: нужно сделать, чтобы кэширование работало только если параметры запроса (содержимое экземпляра класса UserRequest).

Метод payloadMap переопределяем в том случае, если нам нужны какие-либо преобразования данных, полученных с сервера. Если метод не переопределять, то в возвращаемом Promise будут данные из payload.

Теперь получаем данные с API в компоненте:

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


В такой реализации можно показывать заказчику результат выполнения его «хотелок», даже если под эти хотелки еще не допилен бэкенд. Поставил нужные эндпоинты на заглушку — и уже можно «пощупать» фичи и получить фидбэк.

В комментариях жду конструктивную критику, какие проблемы могут возникнуть при наращивании функционала, какие другие подводные камни могут вылезти.

В дальнейшем хочу переписать это решение с промисов на async/await. Думаю код получиться еще более элегантным.

В запасе есть еще несколько архитектурных решений по Angular, в ближайшей перспективе планирую поделиться.
Теги:
Хабы:
Всего голосов 3: ↑1 и ↓2+1
Комментарии8

Публикации

Истории

Работа

Ближайшие события

27 августа – 7 октября
Премия digital-кейсов «Проксима»
МоскваОнлайн
28 – 29 сентября
Конференция E-CODE
МоскваОнлайн
28 сентября – 5 октября
О! Хакатон
Онлайн
30 сентября – 1 октября
Конференция фронтенд-разработчиков FrontendConf 2024
МоскваОнлайн
3 – 18 октября
Kokoc Hackathon 2024
Онлайн
7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн