Интеграция с Angular

Установка и подключение 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'
};

Динамическая загрузка Google Maps API

Прямое подключение через <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
    });
  }
}

Проблема зоны Angular и Google Maps

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 = [];
}

Реактивный подход с RxJS

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

Типичные события:

  • click
  • drag
  • zoom_changed
  • bounds_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();
    }
  }
}

Lazy loading и оптимизация

Загрузка Google Maps JavaScript API должна происходить только при необходимости, особенно в крупных Angular-приложениях.

Подходы:

  • динамический импорт сервиса карты
  • использование loadChildren маршрутов
  • загрузка API только при входе на страницу карты
{
  path: 'map',
  loadChildren: () =>
    import('./map/map.module').then(m => m.MapModule)
}

Управление памятью

Карты создают множество подписок и слушателей событий, поэтому необходимо освобождение ресурсов:

ngOnDestroy() {
  if (this.map) {
    google.maps.event.clearInstanceListeners(this.map);
  }
}

Также важно очищать:

  • маркеры
  • таймеры
  • подписки RxJS

SSR и Angular Universal

При серверном рендеринге window и google отсутствуют, поэтому загрузка API должна быть строго клиентской.

Проверка:

if (typeof window !== 'undefined') {
  // загрузка карты
}

Или через isPlatformBrowser:

constructor(@Inject(PLATFORM_ID) private platformId: object) {}

if (isPlatformBrowser(this.platformId)) {
  // безопасная инициализация
}

Архитектура масштабируемых решений

Для крупных систем рекомендуется выделять отдельный слой:

  • MapsApiService — работа с Google Maps
  • MapsStateService — хранение состояния
  • MapComponent — только отображение
  • MapFacade — оркестрация логики

Такой подход снижает связность и упрощает тестирование.

Типизация и расширение API

Хотя Google Maps JavaScript API имеет собственные типы, часто требуется расширение:

declare global {
  interface Window {
    google: typeof google;
  }
}

Дополнительно можно создавать доменные модели:

interface LocationPoint {
  lat: number;
  lng: number;
  title?: string;
}

Интеграция с формами Angular

Карты часто используются вместе с ReactiveForms:

this.form = new FormGroup({
  location: new FormControl('')
});

Обновление координат через карту:

this.map.addListener('click', (e: google.maps.MapMouseEvent) => {
  this.form.patchValue({
    location: e.latLng?.toJSON()
  });
});

Поддержка Places API

Либрари 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 и жизненного цикла компонентов.

Производительность и рендеринг

Основные проблемы:

  • избыточные перерисовки маркеров
  • частые вызовы change detection
  • отсутствие кластеризации

Решение — использование marker clustering:

new MarkerClusterer({ map: this.map, markers: this.markers });

Также важно ограничивать обновления через trackBy в списках, связанных с картой.

Безопасность ключа API

Ключ Google Maps JavaScript API должен быть ограничен:

  • HTTP referrers
  • домены приложения
  • ограничения по API (Maps JavaScript API, Places API)

Нельзя хранить ключ в публичных репозиториях без ограничений доступа.