Я из тех, кто обычно очень нетерпелив, когда менеджер раскрывает функции нового продукта. После спринта я задаю этот ужасающий, трепещущий вопрос:
… API уже готов?..
И ответ часто отрицательный. Иногда коллеги по бэкенду заняты другими делами и не могут предоставить даже заглушку. Но ничего страшного! Если у вас есть жизнеспособный интерфейс объекта переноса данных, с которым согласны коллеги, вы можете начать создавать новые функции.
Минимизируем изменения для интеграции: используем 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 — мощный инструмент, используемый для разных задач:
- Прототипирование.
- Переключатели возможностей приложений.
- Заглушки сервисов для тестов.
- Конфигурирование.
Освоение этого инструмента улучшит архитектуру приложений и позволит использовать шаблоны проектирования там, где это казалось невозможным.
Специально для сайта ITWORLD.UZ. Новость взята с сайта NOP::Nuances of programming