Работа с картографическим рендерингом в Angular требует аккуратной интеграции императивной библиотеки в реактивную архитектуру фреймворка. Mapbox GL JS представляет собой низкоуровневый WebGL-движок, который напрямую управляет DOM-элементом карты, тогда как Angular строится вокруг компонентов, DI-контейнера и зоны отслеживания изменений. Основная задача интеграции — изолировать Mapbox от механизма change detection и обеспечить предсказуемый жизненный цикл.
Подключение Mapbox GL JS выполняется через npm:
npm install mapbox-gl
Дополнительно требуется типизация:
npm install --save-dev @types/mapbox-gl
Подключение CSS библиотеки критично, так как без него карта не будет отображаться корректно:
// angular.json
"styles": [
"node_modules/mapbox-gl/dist/mapbox-gl.css",
"src/styles.css"
]
Ключ доступа хранится в environment-файле:
// environment.ts
export const environment = {
mapboxToken: 'YOUR_MAPBOX_TOKEN'
};
Прямое создание карты внутри компонента приводит к дублированию логики и сложностям тестирования. Более устойчивый подход — инкапсуляция работы с Mapbox в сервис.
import { Injectable } from '@angular/core';
import mapboxgl from 'mapbox-gl';
import { environment } from '../environments/environment';
@Injectable({
providedIn: 'root'
})
export class MapService {
constructor() {
(mapboxgl as any).accessToken = environment.mapboxToken;
}
createMap(container: string | HTMLElement, options?: mapboxgl.MapboxOptions) {
return new mapboxgl.Map({
container,
style: 'mapbox://styles/mapbox/streets-v12',
center: [0, 0],
zoom: 2,
...options
});
}
}
Сервис выступает единой точкой конфигурации токена, стиля и базовых настроек.
Mapbox требует наличия DOM-узла. Поэтому инициализация выполняется в
ngAfterViewInit, когда шаблон уже отрендерен.
import { Component, ElementRef, ViewChild, AfterViewInit, OnDestroy } from '@angular/core';
import { MapService } from './map.service';
import mapboxgl from 'mapbox-gl';
@Component({
selector: 'app-map',
template: `<div #mapContainer class="map-container"></div>`,
styles: [`
.map-container {
width: 100%;
height: 500px;
}
`]
})
export class MapComponent implements AfterViewInit, OnDestroy {
@ViewChild('mapContainer', { static: false }) mapContainer!: ElementRef;
private map!: mapboxgl.Map;
constructor(private mapService: MapService) {}
ngAfterViewInit(): void {
this.map = this.mapService.createMap(this.mapContainer.nativeElement, {
center: [37.6173, 55.7558],
zoom: 10
});
}
ngOnDestroy(): void {
if (this.map) {
this.map.remove();
}
}
}
Ключевой момент — обязательный вызов map.remove(), иначе
WebGL-контекст остаётся в памяти.
Mapbox генерирует большое количество событий (move, render, zoom), которые не должны триггерить Angular CD без необходимости.
Оптимизация достигается через
NgZone.runOutsideAngular:
import { NgZone } from '@angular/core';
constructor(private mapService: MapService, private zone: NgZone) {}
ngAfterViewInit(): void {
this.zone.runOutsideAngular(() => {
this.map = this.mapService.createMap(this.mapContainer.nativeElement);
this.map.on('move', () => {
const center = this.map.getCenter();
console.log(center);
});
});
}
Использование runOutsideAngular снижает нагрузку и
предотвращает лишние циклы проверки изменений.
Mapbox GL JS использует императивную модель добавления объектов на карту.
const marker = new mapboxgl.Marker()
.setLngLat([37.6173, 55.7558])
.addTo(this.map);
marker.remove();
Mapbox использует концепцию source и
layer.
this.map.on('load', () => {
this.map.addSource('points', {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [37.6173, 55.7558]
},
properties: {}
}
]
}
});
});
this.map.addLayer({
id: 'points-layer',
type: 'circle',
source: 'points',
paint: {
'circle-radius': 6,
'circle-color': '#ff0000'
}
});
Angular-архитектура часто требует реактивного обновления карты.
Например, обновление данных через Observable.
this.dataService.points$.subscribe(points => {
const source = this.map.getSource('points') as mapboxgl.GeoJSONSource;
source.setData({
type: 'FeatureCollection',
features: points.map(p => ({
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [p.lng, p.lat]
},
properties: {}
}))
});
});
Важно избегать пересоздания источника — только обновление через
setData.
Mapbox требует явного уведомления при изменении контейнера:
window.addEventListener('resize', () => {
this.map.resize();
});
Более корректный вариант — использование
ResizeObserver:
const observer = new ResizeObserver(() => {
this.map.resize();
});
observer.observe(this.mapContainer.nativeElement);
При смене маршрута контейнер может уничтожаться, что требует очистки состояния карты.
this.router.events.subscribe(event => {
if (this.map && event instanceof NavigationEnd) {
this.map.remove();
}
});
Чаще применяется локальная очистка в ngOnDestroy, но при
сложных layout-сценариях требуется контроль маршрутов.
Для масштабируемых приложений удобно вынести карту в директиву.
@Directive({
selector: '[appMap]'
})
export class MapDirective implements AfterViewInit, OnDestroy {
private map!: mapboxgl.Map;
constructor(private el: ElementRef, private mapService: MapService) {}
ngAfterViewInit(): void {
this.map = this.mapService.createMap(this.el.nativeElement);
}
ngOnDestroy(): void {
this.map?.remove();
}
}
Использование:
<div appMap class="map-container"></div>
Mapbox GL JS имеет значительный вес, поэтому часто применяется динамический импорт:
async ngAfterViewInit() {
const mapboxgl = await import('mapbox-gl');
this.map = new mapboxgl.Map({
container: this.mapContainer.nativeElement,
style: 'mapbox://styles/mapbox/streets-v12'
});
}
Это позволяет исключить библиотеку из начального бандла.
Mapbox поддерживает пользовательские контролы через интерфейс
IControl.
class CustomControl {
onAdd(map: mapboxgl.Map) {
this.map = map;
this.container = document.createElement('div');
this.container.className = 'custom-control';
this.container.textContent = 'Control';
return this.container;
}
onRemove() {
this.container.parentNode?.removeChild(this.container);
}
}
Добавление:
this.map.addControl(new CustomControl(), 'top-right');
Mapbox GL JS активно использует перегрузки типов, но некоторые методы требуют уточнения:
const source = this.map.getSource('points') as mapboxgl.GeoJSONSource;
Также важно учитывать, что события типизируются через
MapLayerEventType:
this.map.on('click', 'points-layer', (e) => {
console.log(e.features);
});
Основные узкие места:
setDataРекомендуемые практики:
runOutsideAngularOnDestroyПри работе с несколькими экземплярами карт важно изолировать состояния:
const maps: mapboxgl.Map[] = [];
maps.push(
this.mapService.createMap(container1)
);
maps.push(
this.mapService.createMap(container2)
);
Каждая карта должна иметь независимые источники и слои, иначе возможны конфликты идентификаторов.
Mapbox генерирует runtime-ошибки при:
Обработка:
this.map.on('error', (e) => {
console.error('Mapbox error:', e.error);
});
Типовая архитектура Angular + Mapbox GL JS строится на трёх уровнях:
Такая структура обеспечивает предсказуемость поведения карты в рамках Angular-приложения и минимизирует конфликты между императивным и реактивным подходами.