Я из тех, кто обычно очень нетерпелив, когда менеджер раскрывает функции нового продукта. После спринта я задаю этот ужасающий, трепещущий вопрос:
И ответ часто отрицательный. Иногда коллеги по бэкенду заняты другими делами и не могут предоставить даже заглушку. Но ничего страшного! Если у вас есть жизнеспособный интерфейс объекта переноса данных, с которым согласны коллеги, вы можете начать создавать новые функции.
Минимизируем изменения для интеграции: используем Injectables
и будем работать на двух клиентах API одновременно:
- Первый — заглушка, назовём её
MockPriceApiService
. - Второй — реальный сервис, он будет поставляться приложением, когда API будет готов. Его мы назовём
PriceApiService
. - Классы реализуют интерфейс, устанавливающий контракт между API.
Ещё раз. Я создам минимальное приложение для криптовалюты. Оно будет работать как с поддельными ценами, так и с реальным сервисом Coincap. Читать лень? Посмотрите готовый проект Stackblitz!
Общий интерфейс
Во-первых, мы хотим определить общий интерфейс для классов API. Создаём ApiService
:
import { Observable } from 'rxjs';
export interface PriceApi {
getPrice(currency: string): Observable<string>;
unsubscribe(): void;
}
Как видите, сервисам нужно только два общих метода:
- Первый — подписка на поток цен.
- Второй — отписка от него.
Заглушка
Начнём с сервиса-заглушки. Вот, что нужно реализовать:
- Получение потока случайных чисел (цен). Он будет основан на базовых ценах, чтобы приблизить числа к реальным.
- Отписка от потока.
Приложение поддерживает 3 валюты: биткоин, лайткоин и эфириум.
const BASE_PRICES = {
bitcoin: 11000,
litecoin: 130,
ethereum: 300
};
const randomize = (price: number) => {
return (Math.random() * 2) + price;
};
@Injectable()
export class MockPriceApiService implements PriceApi {
static latency = 250;
private unsubsciptions$ = new Subject();
public getPrice(currency: string): Observable<string> {
return timer(0, MockPriceApiService.latency).pipe(
delay(1000),
map(() => BASE_PRICES[currency]),
map((price: number) => randomize(price)),
map((price: number) => price.toFixed(5)),
takeUntil(this.unsubsciptions$),
)
}
public unsubscribe() {
this.unsubsciptions$.next();
}
}
О методе getPrice
:
- Метод принимает параметр
currency
— имя валюты. - Таймер срабатывает через 250 мс.
- Замораживаем оповещённую
Observable
на 1 секунду, имитируя задержку сети. - Берём
BASE_PRICE
выбранной валюты и каждый раз возвращаем случайное число. - Повторяем до оповещения от
Subject
.
Реальный сервис
Как мы уже говорили, вашей команде может потребоваться некоторое время, прежде чем будет предоставлен реальный API для внедрения в ваш интерфейс. Если команда уже согласилась с интерфейсом, ничто не мешает вам реализовать API заранее и создать реальный сервис вместе с имитацией.
Чтобы завершить пример, создадим реальный сервис и возьмём цены у Coincap. Будем использовать WebSocket
для потоковой передачи цен нашим клиентам.
Два открытых общих метода будут:
- Создавать соединение
WebSocket
и поток цен черезObservable
. - Закрывать соединение при вызове
unsubscribe
. Поток перестаёт отправлять цены.
const WEB_SOCKET_ENDPOINT = 'wss://ws.coincap.io/prices/';
@Injectable()
export class PriceApiService implements PriceApi {
private webSocket: WebSocket;
public getPrice(currency: string): Observable<string> {
return this.connectToPriceStream(currency);
}
public unsubscribe() {
this.webSocket.close();
}
private connectToPriceStream(asset: string): Observable<string> {
this.createConnection(asset);
return new Observable(observer => {
const webSocket = this.webSocket;
webSocket.onmessage = (msg: MessageEvent) => {
const data = JSON.parse(msg.data);
observer.next(data[asset]);
};
return {
unsubscribe(): void {
webSocket.close();
}
};
});
}
private createConnection(asset: string) {
if (this.webSocket) {
this.webSocket.close();
}
this.webSocket = new WebSocket(
WEB_SOCKET_ENDPOINT + `?assets=${asset}`
);
}
}
Данные для сервиса
Теперь давайте покажем, как использовать данные, передаваемые любыми из двух сервисов. Ниже компонент, внедряющий PriceApiService
:
- Мы создаем свойство
price
, которому назначаем поток. - Когда пользователь меняет валюту, мы вызываем метод отписки, если валюта не выбрана.
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent {
public price$: Observable<string | undefined>;
public currency$ = new BehaviorSubject<string | undefined>(undefined);
constructor(private api: PriceApiService) { }
ngOnInit() {
this.price$ = this.currency$.pipe(
mergeMap((currency: string | undefined) => {
return currency ? this.api.getPrice(currency) : of(undefined);
}),
shareReplay(1)
);
}
onCryptoSelected(currency: string) {
if (this.currency$.value) {
this.api.unsubscribe();
}
this.currency$.next(currency);
}
}
Вот, что в шаблоне:
- Отображаем цену.
- Если цена не определена, но валюта выбрана, значит, подписка действует. Показываем загружаемое сообщение.
- Валюта не выбрана? Тогда сообщение пустое.
<div class='mt-5'>
<crypto-selector (selected)="onCryptoSelected($event)"></crypto-selector>
<div class='price'>
{{ price$ | async }}
</div>
<ng-container *ngIf="(price$ | async) === undefined && (currency$ | async)">
<div class='alert alert-info mt-2'>
Awaiting for Price...
</div>
</ng-container>
<ng-container *ngIf="!(currency$ | async)">
<div class='alert alert-warning mt-2'>
No Crypto Subscribed Yet
</div>
</ng-container>
</div>
Переключаем сервисы
Здесь — магия. Благодаря внедрению зависимости мы можем предоставить нужный нам сервис, передав ему имя класса. Если useMocks
истинна, используемMockPriceApiService
, иначе PriceApiService
.
// в реальном приложении может быть environment.useMocks.
const useMocks = true;
@NgModule({
imports: [BrowserModule, CommonModule],
declarations: [AppComponent, CryptoSelectorComponent],
bootstrap: [AppComponent],
providers: [
{
provide: PriceApiService,
useClass: useMocks ? MockPriceApiService : PriceApiService
}
]
})
export class AppModule { }
Конечно, пока конечная точка не готова, используйте заглушку. useMocks
можно сделать переменной среды в системе непрерывной интеграции. Переключение станет очень удобным!
Внедрение зависимости
Кроме useClass
, вы можете использовать:
useValue
для строк, чисел и так далее.useFactory
для передачи функций.InjectionToken
для отличных от классов значений.
Полное описание смотрите в документации Angular.
Заключение
Внедрение зависимостей Angular — мощный инструмент, используемый для разных задач:
- Прототипирование.
- Переключатели возможностей приложений.
- Заглушки сервисов для тестов.
- Конфигурирование.
Освоение этого инструмента улучшит архитектуру приложений и позволит использовать шаблоны проектирования там, где это казалось невозможным.
Читайте также:
Перевод статьи Giancarlo Buomprisco: API-less Prototyping with Angular