Архитектура Angular предполагает строгую компонентную модель, основанную на жизненном цикле компонентов, реактивности и управлении изменениями через zone.js. Leaflet, напротив, работает как императивная библиотека для работы с картами, напрямую манипулирующая DOM-элементами. Это создаёт характерную точку соприкосновения двух подходов: декларативного (Angular) и императивного (Leaflet).
Ключевой принцип интеграции заключается в изоляции Leaflet-карты внутри Angular-компонента с контролируемым доступом через сервисы и lifecycle hooks.
Подключение Leaflet в Angular-проект требует установки самой библиотеки и типов:
npm install leaflet
npm install --save-dev @types/leaflet
Дополнительно в Angular проекте необходимо подключить стили Leaflet, так как без них карта не будет корректно отображаться:
"styles": [
"node_modules/leaflet/dist/leaflet.css",
"src/styles.css"
]
Либо через styles.scss:
@import "leaflet/dist/leaflet.css";
Структура Angular-компонента для Leaflet обычно включает:
ngAfterViewInitimport { Component, AfterViewInit, ElementRef, ViewChild } from '@angular/core';
import * as L from 'leaflet';
@Component({
selector: 'app-map',
template: `<div #map class="map-container"></div>`,
styles: [`
.map-container {
height: 500px;
width: 100%;
}
`]
})
export class MapComponent implements AfterViewInit {
@ViewChild('map', { static: false }) mapElement!: ElementRef;
private map!: L.Map;
ngAfterViewInit(): void {
this.initMap();
}
private initMap(): void {
this.map = L.map(this.mapElement.nativeElement).setView([51.505, -0.09], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(this.map);
}
}
Критический момент интеграции — выбор правильного lifecycle hook.
Используется для инициализации карты, так как DOM уже доступен.
Обязателен для предотвращения утечек памяти:
ngOnDestroy(): void {
if (this.map) {
this.map.remove();
}
}
Leaflet создаёт слушатели событий и DOM-слои, которые не очищаются автоматически Angular.
Leaflet оперирует слоями (layers), что идеально ложится на компонентную модель Angular.
const marker = L.marker([51.5, -0.09]).addTo(this.map);
marker.bindPopup('Точка интереса');
Часто используется хранение маркеров в массиве:
private markers: L.Marker[] = [];
addMarker(lat: number, lng: number): void {
const marker = L.marker([lat, lng]).addTo(this.map);
this.markers.push(marker);
}
clearMarkers(): void {
this.markers.forEach(m => m.remove());
this.markers = [];
}
В Angular корректная практика — вынос логики карты в сервис, а компонент оставлять представлением.
import { Injectable } from '@angular/core';
import * as L from 'leaflet';
@Injectable({ providedIn: 'root' })
export class MapService {
private map!: L.Map;
initMap(container: HTMLElement): L.Map {
this.map = L.map(container).setView([51.505, -0.09], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: 'OpenStreetMap'
}).addTo(this.map);
return this.map;
}
getMap(): L.Map {
return this.map;
}
}
constructor(private mapService: MapService) {}
ngAfterViewInit(): void {
this.mapService.initMap(this.mapElement.nativeElement);
}
Angular использует механизм change detection, который может конфликтовать с частыми обновлениями карты.
Проблема проявляется при:
constructor(private ngZone: NgZone) {}
this.ngZone.runOutsideAngular(() => {
this.map.on('move', () => {
console.log('map moved');
});
});
Это предотвращает лишние циклы обнаружения изменений.
Leaflet позволяет задавать кастомные маркеры, что важно для Angular UI.
const icon = L.icon({
iconUrl: 'assets/marker.png',
iconSize: [25, 41],
iconAnchor: [12, 41]
});
L.marker([51.5, -0.09], { icon }).addTo(this.map);
При использовании Angular CLI важно учитывать путь
assets, который копируется в build.
Leaflet поддерживает GeoJSON как основной формат описания геометрии.
const geoJsonFeature = {
type: 'Feature',
geometry: {
type: 'Polygon',
coordinates: [[
[-0.1, 51.5],
[-0.12, 51.5],
[-0.12, 51.52],
[-0.1, 51.52],
[-0.1, 51.5]
]]
}
};
L.geoJSON(geoJsonFeature).addTo(this.map);
GeoJSON часто используется совместно с Angular HttpClient:
this.http.get('/api/geo-data').subscribe(data => {
L.geoJSON(data).addTo(this.map);
});
Leaflet предоставляет богатую систему событий:
Пример обработки клика:
this.map.on('click', (e: L.LeafletMouseEvent) => {
L.marker([e.latlng.lat, e.latlng.lng]).addTo(this.map);
});
При интеграции с Angular важно учитывать контекст зоны выполнения.
При большом количестве объектов на карте возникают проблемы:
Используется библиотека leaflet.markercluster:
npm install leaflet.markercluster
Слои добавляются только при необходимости:
if (zoomLevel > 10) {
layer.addTo(this.map);
}
ChangeDetectionStrategy.OnPush
Leaflet зависит от window и document,
поэтому при серверном рендеринге возникают ошибки.
Решение — проверка платформы:
import { isPlatformBrowser } from '@angular/common';
import { Inject, PLATFORM_ID } from '@angular/core';
constructor(@Inject(PLATFORM_ID) private platformId: Object) {}
ngAfterViewInit(): void {
if (isPlatformBrowser(this.platformId)) {
this.initMap();
}
}
Вместо компонента можно использовать директиву для переиспользования карты:
@Directive({
selector: '[appLeafletMap]'
})
export class LeafletMapDirective implements AfterViewInit {
constructor(private el: ElementRef) {}
ngAfterViewInit(): void {
const map = L.map(this.el.nativeElement).setView([51.5, -0.09], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
}
}
Такой подход позволяет внедрять карту в любой контейнер.
Часто требуется синхронизировать состояние карты с Angular формами или store (NgRx).
Пример:
this.map.on('zoomend', () => {
this.zoomLevel = this.map.getZoom();
});
И наоборот:
setZoom(level: number): void {
this.map.setZoom(level);
}
Существуют обёртки, упрощающие интеграцию:
Они добавляют декларативный стиль:
<div
leaflet
[leafletOptions]="options"
[leafletLayers]="layers">
</div>
Однако они ограничивают гибкость чистого Leaflet API.
Компонент только отображает контейнер, сервис управляет картой.
Каждая карта — отдельный feature module.
Слои управляются через RxJS streams.
Leaflet хорошо сочетается с Observables:
this.location$.subscribe(loc => {
this.map.setView([loc.lat, loc.lng]);
});
Для оптимизации:
this.location$
.pipe(debounceTime(300))
.subscribe(loc => this.updateMap(loc));
Основные источники утечек:
Правильное удаление:
ngOnDestroy(): void {
this.map.off();
this.map.remove();
}
Leaflet позволяет подключать любые tile servers:
L.tileLayer('https://tiles.example.com/{z}/{x}/{y}.png', {
maxZoom: 18
}).addTo(this.map);
Также поддерживаются:
Leaflet используется не только для маркеров:
Пример линии маршрута:
const route = L.polyline([
[51.5, -0.1],
[51.51, -0.11],
[51.52, -0.12]
]).addTo(this.map);
Пример синхронизации координат:
this.form.valueChanges.subscribe(val => {
this.map.setView([val.lat, val.lng]);
});
И обратная связь:
this.map.on('moveend', () => {
const center = this.map.getCenter();
this.form.patchValue({
lat: center.lat,
lng: center.lng
});
});