Установка и подключение Angular как основы для работы с картами
предполагает заранее продуманную архитектуру приложения, поскольку
Google Maps JavaScript API не является «родной» частью Angular и требует
обёртки над глобальным объектом google.
Работа с картами начинается с создания Angular-приложения и определения слоя абстракции над внешним API. Важный принцип — изоляция логики загрузки Google Maps от компонентов интерфейса.
Типовая структура модулей:
core/ — сервисы загрузки API и общие утилитыshared/maps/ — компоненты карты, маркеры, оверлеиfeatures/ — бизнес-логика, использующая картыПодключение API-ключа обычно выносится в
environment.ts:
export const environment = {
production: false,
googleMapsApiKey: 'YOUR_API_KEY'
};
Прямое подключение через <script> в
index.html ограничивает гибкость. В Angular-подходе
используется динамическая загрузка скрипта.
Сервис загрузки:
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class GoogleMapsLoaderService {
private promise: Promise<void> | null = null;
load(apiKey: string): Promise<void> {
if (this.promise) return this.promise;
this.promise = new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places`;
script.async = true;
script.defer = true;
script.onl oad = () => resolve();
script.oner ror = () => reject('Google Maps failed to load');
document.head.appendChild(script);
});
return this.promise;
}
}
Ключевая идея — гарантировать загрузку API один раз, независимо от количества компонентов.
Компонент карты должен учитывать жизненный цикл Angular и момент доступности DOM.
import { Component, ElementRef, ViewChild, AfterViewInit } from '@angular/core';
import { environment } from '../environments/environment';
import { GoogleMapsLoaderService } from './google-maps-loader.service';
declare const google: any;
@Component({
selector: 'app-map',
template: `<div #mapContainer class="map-container"></div>`,
styles: [`.map-container { height: 400px; width: 100%; }`]
})
export class MapComponent implements AfterViewInit {
@ViewChild('mapContainer', { static: false }) mapElement!: ElementRef;
map!: google.maps.Map;
constructor(private loader: GoogleMapsLoaderService) {}
async ngAfterViewInit() {
await this.loader.load(environment.googleMapsApiKey);
this.map = new google.maps.Map(this.mapElement.nativeElement, {
center: { lat: 51.1694, lng: 71.4491 },
zoom: 10
});
}
}
Google Maps JavaScript API выполняет большое количество операций вне Angular Zone, что может приводить к проблемам с обновлением интерфейса.
Для контроля используется NgZone:
import { NgZone } from '@angular/core';
constructor(
private loader: GoogleMapsLoaderService,
private zone: NgZone
) {}
async ngAfterViewInit() {
await this.loader.load(environment.googleMapsApiKey);
this.zone.runOutsideAngular(() => {
this.map = new google.maps.Map(this.mapElement.nativeElement, {
center: { lat: 51.1694, lng: 71.4491 },
zoom: 10
});
});
}
Использование runOutsideAngular снижает нагрузку на
change detection.
Добавление маркеров — базовый сценарий, требующий аккуратного управления ссылками на объекты API.
addMarker(lat: number, lng: number) {
return new google.maps.Marker({
position: { lat, lng },
map: this.map
});
}
Для масштабируемых приложений важно хранить маркеры в коллекции:
markers: google.maps.Marker[] = [];
createMarker(position: google.maps.LatLngLiteral) {
const marker = new google.maps.Marker({
position,
map: this.map
});
this.markers.push(marker);
}
Удаление маркеров:
clearMarkers() {
this.markers.forEach(m => m.setMap(null));
this.markers = [];
}
В Angular архитектуре карты часто связываются с потоками данных.
import { BehaviorSubject } from 'rxjs';
private markers$ = new BehaviorSubject<google.maps.LatLngLiteral[]>([]);
setMarkers(data: google.maps.LatLngLiteral[]) {
this.markers$.next(data);
}
Подписка в компоненте:
this.markers$.subscribe(points => {
this.clearMarkers();
points.forEach(p => this.createMarker(p));
});
Google Maps JavaScript API предоставляет geocoding через
google.maps.Geocoder.
geocodeAddress(address: string): Promise<any> {
const geocoder = new google.maps.Geocoder();
return new Promise((resolve, reject) => {
geocoder.geocode({ address }, (results, status) => {
if (status === 'OK') {
resolve(results);
} else {
reject(status);
}
});
});
}
Применение в Angular сервисе позволяет отделить логику API от UI.
События требуют корректной интеграции с Angular зоной:
this.map.addListener('click', (event: google.maps.MapMouseEvent) => {
this.zone.run(() => {
console.log(event.latLng?.toJSON());
});
});
Типичные события:
clickdragzoom_changedbounds_changedДля сложных интерфейсов используется OverlayView.
class CustomOverlay extends google.maps.OverlayView {
div: HTMLDivElement | null = null;
onAdd() {
this.div = document.createElement('div');
this.div.innerHTML = 'Custom Overlay';
this.getPanes().overlayLayer.appendChild(this.div);
}
draw() {
const projection = this.getProjection();
const position = projection.fromLatLngToDivPixel(
new google.maps.LatLng(51.1694, 71.4491)
);
if (this.div && position) {
this.div.style.left = position.x + 'px';
this.div.style.top = position.y + 'px';
}
}
onRemove() {
if (this.div) {
this.div.remove();
}
}
}
Загрузка Google Maps JavaScript API должна происходить только при необходимости, особенно в крупных Angular-приложениях.
Подходы:
loadChildren маршрутов{
path: 'map',
loadChildren: () =>
import('./map/map.module').then(m => m.MapModule)
}
Карты создают множество подписок и слушателей событий, поэтому необходимо освобождение ресурсов:
ngOnDestroy() {
if (this.map) {
google.maps.event.clearInstanceListeners(this.map);
}
}
Также важно очищать:
При серверном рендеринге window и google
отсутствуют, поэтому загрузка API должна быть строго клиентской.
Проверка:
if (typeof window !== 'undefined') {
// загрузка карты
}
Или через isPlatformBrowser:
constructor(@Inject(PLATFORM_ID) private platformId: object) {}
if (isPlatformBrowser(this.platformId)) {
// безопасная инициализация
}
Для крупных систем рекомендуется выделять отдельный слой:
MapsApiService — работа с Google MapsMapsStateService — хранение состоянияMapComponent — только отображениеMapFacade — оркестрация логикиТакой подход снижает связность и упрощает тестирование.
Хотя Google Maps JavaScript API имеет собственные типы, часто требуется расширение:
declare global {
interface Window {
google: typeof google;
}
}
Дополнительно можно создавать доменные модели:
interface LocationPoint {
lat: number;
lng: number;
title?: string;
}
Карты часто используются вместе с ReactiveForms:
this.form = new FormGroup({
location: new FormControl('')
});
Обновление координат через карту:
this.map.addListener('click', (e: google.maps.MapMouseEvent) => {
this.form.patchValue({
location: e.latLng?.toJSON()
});
});
Либрари places расширяет возможности поиска:
const input = document.getElementById('searchBox') as HTMLInputElement;
const autocomplete = new google.maps.places.Autocomplete(input);
autocomplete.addListener('place_changed', () => {
const place = autocomplete.getPlace();
});
Интеграция с Angular требует синхронизации DOM и жизненного цикла компонентов.
Основные проблемы:
Решение — использование marker clustering:
new MarkerClusterer({ map: this.map, markers: this.markers });
Также важно ограничивать обновления через trackBy в
списках, связанных с картой.
Ключ Google Maps JavaScript API должен быть ограничен:
Нельзя хранить ключ в публичных репозиториях без ограничений доступа.