По мере роста картографического приложения количество слоёв, источников данных, обработчиков событий и пользовательских интерфейсов быстро увеличивается. Если вся логика размещается в одном файле или внутри одного экземпляра карты, код становится трудно поддерживать, расширять и тестировать.
Компонентный подход представляет собой организацию приложения в виде набора независимых модулей, каждый из которых отвечает за конкретную задачу:
В результате карта превращается в платформу, на которой взаимодействуют отдельные специализированные компоненты.
При разработке небольшого прототипа допустимо размещать весь код в одном месте:
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12',
center: [37.6176, 55.7558],
zoom: 10
});
map.on('load', () => {
map.addSource(...);
map.addLayer(...);
map.addLayer(...);
map.addLayer(...);
map.on('click', ...);
map.on('mousemove', ...);
});
По мере развития проекта появляются:
В такой ситуации компонентный подход позволяет:
Повысить читаемость кода
Каждая функция находится в отдельном модуле.
Упростить сопровождение
Изменение логики одного компонента не затрагивает остальные части приложения.
Улучшить повторное использование
Компоненты можно переносить между проектами.
Упростить тестирование
Отдельный компонент легче проверять изолированно.
Один из распространённых вариантов организации проекта:
src/
│
├── map/
│ ├── MapManager.js
│ ├── LayerManager.js
│ ├── SourceManager.js
│
├── components/
│ ├── MarkersComponent.js
│ ├── RoutesComponent.js
│ ├── BuildingsComponent.js
│ └── PopupComponent.js
│
├── services/
│ ├── ApiService.js
│ └── GeoService.js
│
└── main.js
Каждый файл отвечает только за одну область ответственности.
Практически все компоненты взаимодействуют с экземпляром Mapbox GL JS.
Часто создаётся специальный класс управления картой.
import mapboxgl from 'mapbox-gl';
export class MapManager {
constructor(container) {
this.map = new mapboxgl.Map({
container,
style: 'mapbox://styles/mapbox/light-v11',
center: [37.6176, 55.7558],
zoom: 10
});
}
getMap() {
return this.map;
}
}
Использование:
const manager = new MapManager('map');
const map = manager.getMap();
Теперь остальные компоненты получают ссылку на карту через единый интерфейс.
Каждый компонент может представлять собой отдельный класс.
Пример компонента маркеров.
export class MarkersComponent {
constructor(map) {
this.map = map;
this.markers = [];
}
addMarker(coordinates) {
const marker = new mapboxgl.Marker()
.setLngLat(coordinates)
.addTo(this.map);
this.markers.push(marker);
}
clear() {
this.markers.forEach(marker => marker.remove());
this.markers = [];
}
}
Подключение:
const markers = new MarkersComponent(map);
markers.addMarker([37.61, 55.75]);
markers.addMarker([37.63, 55.76]);
Компонент полностью скрывает внутреннюю реализацию работы с маркерами.
Слои являются одной из самых важных частей Mapbox GL JS.
Для управления ими удобно создавать отдельные компоненты.
export class BuildingsComponent {
constructor(map) {
this.map = map;
}
init() {
this.map.addLayer({
id: 'buildings',
source: 'composite',
'source-layer': 'building',
type: 'fill-extrusion',
paint: {
'fill-extrusion-color': '#aaaaaa',
'fill-extrusion-height': [
'get',
'height'
]
}
});
}
}
Использование:
map.on('load', () => {
const buildings = new BuildingsComponent(map);
buildings.init();
});
Весь код, связанный с отображением зданий, сосредоточен внутри одного объекта.
Источник данных может использоваться несколькими слоями одновременно.
Выделение отдельного компонента упрощает управление.
export class GeoJsonSource {
constructor(map) {
this.map = map;
}
create(data) {
this.map.addSource('cities', {
type: 'geojson',
data
});
}
upd ate(data) {
this.map.getSource('cities')
.setData(data);
}
}
Использование:
const source = new GeoJsonSource(map);
source.create(geojsonData);
Изменение данных:
source.update(newGeojsonData);
События карты быстро становятся источником хаоса в приложении.
Вместо размещения всех обработчиков в одном месте их лучше переносить в соответствующие компоненты.
Плохой вариант:
map.on('click', ...);
map.on('mousemove', ...);
map.on('mouseenter', ...);
map.on('mouseleave', ...);
map.on('zoom', ...);
map.on('move', ...);
Компонентный вариант:
export class PopupComponent {
constructor(map) {
this.map = map;
}
initialize() {
this.map.on(
'click',
'cities-layer',
this.handleClick.bind(this)
);
}
handleClick(event) {
const feature = event.features[0];
new mapboxgl.Popup()
.setLngLat(feature.geometry.coordinates)
.setHTML(feature.properties.name)
.addTo(this.map);
}
}
Теперь логика открытия всплывающих окон изолирована от остальных частей приложения.
Крупные приложения требуют явного управления созданием и уничтожением компонентов.
Типичная структура:
class LayerComponent {
mount() {
}
unmount() {
}
}
Пример:
export class TrafficComponent {
constructor(map) {
this.map = map;
}
mount() {
this.map.addLayer({
id: 'traffic',
type: 'line',
source: 'traffic-source'
});
}
unmount() {
if (this.map.getLayer('traffic')) {
this.map.removeLayer('traffic');
}
}
}
Включение:
traffic.mount();
Отключение:
traffic.unmount();
Такой подход особенно полезен для переключаемых режимов отображения.
В крупных системах удобно использовать единый контейнер управления.
export class ComponentRegistry {
constructor() {
this.components = new Map();
}
register(name, component) {
this.components.se t(name, component);
}
get(name) {
return this.components.get(name);
}
}
Использование:
const registry = new ComponentRegistry();
registry.register(
'markers',
new MarkersComponent(map)
);
registry.register(
'routes',
new RoutesComponent(map)
);
Получение компонента:
registry
.get('markers')
.addMarker([37.6, 55.7]);
Компоненты могут объединяться друг с другом.
Например:
RouteComponent
│
├── RouteLayer
├── RouteMarkers
└── RoutePopup
Класс маршрута:
export class RouteComponent {
constructor(map) {
this.layer =
new RouteLayer(map);
this.markers =
new RouteMarkers(map);
this.popup =
new RoutePopup(map);
}
initialize() {
this.layer.initialize();
this.markers.initialize();
this.popup.initialize();
}
}
Каждый вложенный модуль решает свою задачу.
Многие компоненты работают с удалёнными API.
Например, слой объектов недвижимости.
export class EstateComponent {
constructor(map, api) {
this.map = map;
this.api = api;
}
async load() {
const data =
await this.api.getEstates();
this.map.addSource(
'estates',
{
type: 'geojson',
data
}
);
}
}
Использование:
await estateComponent.load();
Компонент полностью скрывает процесс загрузки данных.
Одним из принципов компонентной архитектуры является отделение данных от отображения.
Нежелательный вариант:
async function loadObjects() {
const response =
await fetch('/api/objects');
const data =
await response.json();
map.addSource(...);
map.addLayer(...);
}
Более правильный вариант:
export class ObjectsService {
async getObjects() {
const response =
await fetch('/api/objects');
return response.json();
}
}
export class ObjectsLayer {
constructor(map, service) {
this.map = map;
this.service = service;
}
async render() {
const data =
await this.service.getObjects();
this.map.addSource(
'objects',
{
type: 'geojson',
data
}
);
}
}
Визуальный слой больше не знает, откуда поступают данные.
Mapbox GL JS часто используется совместно с React.
Компонент карты:
function MapView() {
const mapRef = useRef();
useEffect(() => {
const map = new mapboxgl.Map({
container: mapRef.current,
style: 'mapbox://styles/mapbox/streets-v12'
});
return () => map.remove();
}, []);
return (
<div
ref={mapRef}
className="map"
/>
);
}
Компонент слоя:
function CitiesLayer({ map, data }) {
useEffect(() => {
if (!map) {
return;
}
map.addSource('cities', {
type: 'geojson',
data
});
map.addLayer({
id: 'cities',
type: 'circle',
source: 'cities'
});
}, [map, data]);
return null;
}
Каждый React-компонент становится отдельным логическим блоком карты.
Во Vue аналогичная идея реализуется через однофайловые компоненты.
<script setup>
import { onMounted } from 'vue';
onMounted(() => {
const map = new mapboxgl.Map({
container: 'map'
});
});
</script>
<template>
<div id="map"></div>
</template>
Логика карты может разделяться между множеством специализированных компонентов.
Для уменьшения связанности часто применяется внедрение зависимостей.
Без внедрения:
class LayerComponent {
constructor(map) {
this.map = map;
this.api =
new ApiService();
}
}
С внедрением:
class LayerComponent {
constructor(map, api) {
this.map = map;
this.api = api;
}
}
Создание:
const component =
new LayerComponent(
map,
apiService
);
Преимущества:
Наиболее часто в Mapbox GL JS выделяются следующие универсальные компоненты:
| Компонент | Назначение |
|---|---|
| MarkerComponent | Работа с маркерами |
| PopupComponent | Управление всплывающими окнами |
| RouteComponent | Построение маршрутов |
| LayerComponent | Управление слоями |
| SourceComponent | Работа с источниками данных |
| DrawComponent | Редактирование геометрии |
| SearchComponent | Геопоиск |
| ThemeComponent | Переключение стилей |
| ClusterComponent | Кластеризация точек |
| ControlsComponent | Навигационные элементы |
Подобная организация позволяет постепенно наращивать функциональность приложения без превращения кодовой базы в набор тесно связанных обработчиков и конфигураций. Компоненты становятся самостоятельными строительными блоками картографической системы, обеспечивая масштабируемость, переиспользуемость и предсказуемость поведения приложения на всех этапах его развития.