Очень часто на проекте темпы разработки фронтенда опережают темпы разработки бэкенда. При такой ситуации возникает необходимость двух вещей:
Хочу поделиться своим способом организации кода, отвечающего за запросы к API, который отлично решает эти две задачи, а также позволяет кэшировать запросы.
Создаем в корне проекта файл конфигурации api.config.json:
Здесь прописываем базовый урл для API, если параметр useFakeApiByDefault = true, то наше приложение будет использовать только заглушки вместо всех запросов. Если false — то заглушки будут использоваться только для запросов из массива fakeEndPoints.
Чтобы можно было импортировать JSON в код, добавляем в секцию CompilerOptions файла tsconfig.json две строчки:
Создаем базовый класс BaseEndpoint.
/src/app/api/_base.endpoint.ts:
<Request, ResponseModel> — типы экземпляров запроса и ответа. В ответ выдергивается уже payload.
Классы ApiService и ErrorService выкладывать не буду, дабы не раздувать пост, там ничего особенного. ApiService шлет http-запросы, ErrorService позволяет подписываться на ошибки. Для вывода ошибок нужно подписаться на ErrorService в том компоненте, где собственно хотим отображать ошибки (я подписываю главный layout, в котором делаю модал либо всплывающую подсказку).
Магия начинается, когда мы от этого класса наследуемся. Класс-наследник будет выглядеть так:
/src/app/api/get-users.endpoint.ts
Ничего лишнего, из содержимого файла сразу понятно, какой урл, какой формат запроса (GetUserRequest), какой формат ответа.
Если в отдельной папке /api будут лежать такие файлы, каждый файл соответствует своему эндпоинту, я думаю можно показать эту папку бэкендеру, и теоретически, если очень лень писать документацию по api, можно обойтись без нее. Можно внутри папки /api файлы еще раскидать по папкам соответственно контроллерам.
Если в массив «fakeEndPoints» конфига добавить 'GetUsers', то запроса не будет, а ответ подмениться данными-заглушкой из sampleResponse.
Для того, чтобы фейковые запросы можно было отлаживать (во вкладке Network, естественно, мы ничего не увидим), я предусмотрел в базовом классе вывод в консоль двух строк:
Если переопределить свойство класса cache=true, то запрос будет кэшироваться (первый раз делается запрос к API, затем всегда возвращается результат первого запроса). Правда здесь стоит доработать: нужно сделать, чтобы кэширование работало только если параметры запроса (содержимое экземпляра класса UserRequest).
Метод payloadMap переопределяем в том случае, если нам нужны какие-либо преобразования данных, полученных с сервера. Если метод не переопределять, то в возвращаемом Promise будут данные из payload.
Теперь получаем данные с API в компоненте:
В такой реализации можно показывать заказчику результат выполнения его «хотелок», даже если под эти хотелки еще не допилен бэкенд. Поставил нужные эндпоинты на заглушку — и уже можно «пощупать» фичи и получить фидбэк.
В комментариях жду конструктивную критику, какие проблемы могут возникнуть при наращивании функционала, какие другие подводные камни могут вылезти.
В дальнейшем хочу переписать это решение с промисов на async/await. Думаю код получиться еще более элегантным.
В запасе есть еще несколько архитектурных решений по Angular, в ближайшей перспективе планирую поделиться.
- возможность запускать фронт без бэкэнда, либо без отдельных эндпоинтов;
- описывать бэкендеру, какие нужны эндпоинты, формат запроса, ответа, итд.
Хочу поделиться своим способом организации кода, отвечающего за запросы к 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, в ближайшей перспективе планирую поделиться.