Svelte integration

Архитектурная модель взаимодействия карты и реактивного UI

Интеграция Mapbox GL JS в Svelte опирается на различие между императивной графической библиотекой и реактивной UI-моделью. Mapbox GL JS управляет WebGL-контекстом напрямую, создаёт слой рендеринга карты, обрабатывает события и состояние камеры. Svelte, напротив, компилирует реактивные зависимости в оптимизированный JavaScript без виртуального DOM.

Основная задача интеграции заключается в синхронизации жизненного цикла карты с жизненным циклом компонента и в аккуратном связывании реактивных переменных с императивным API карты.

Инициализация карты в жизненном цикле компонента

В Svelte ключевым моментом является стадия монтирования компонента. Инициализация экземпляра карты выполняется после того, как DOM-узел уже существует.

<script>
  import { onMount, onDestroy } from "svelte";
  import mapboxgl from "mapbox-gl";

  let mapContainer;
  let map;
</script>

<div bind:this={mapContainer} class="map"></div>

Инициализация экземпляра Mapbox GL JS происходит в onMount:

onMount(() => {
  map = new mapboxgl.Map({
    container: mapContainer,
    style: "mapbox://styles/mapbox/streets-v12",
    center: [0, 0],
    zoom: 2
  });
});

Такой подход гарантирует наличие DOM-элемента контейнера и корректное создание WebGL контекста.

Управление жизненным циклом и освобождение ресурсов

Mapbox GL JS создаёт WebGL контекст и подписки на события, поэтому требуется явное освобождение ресурсов при уничтожении компонента.

onDestroy(() => {
  if (map) {
    map.remove();
    map = null;
  }
});

Удаление экземпляра карты предотвращает утечки памяти и освобождает GPU-ресурсы.

Реактивная синхронизация состояния карты

Svelte предоставляет реактивные блоки $: для синхронизации состояния. Это используется для обновления карты при изменении переменных.

Центр карты

<script>
  let lng = 0;
  let lat = 0;
  let zoom = 2;

  $: if (map) {
    map.setCenter([lng, lat]);
  }
</script>

Изменение lng или lat автоматически приводит к обновлению положения карты.

Масштабирование

$: if (map) {
  map.setZoom(zoom);
}

Такая реактивная связь упрощает интеграцию состояния интерфейса с камерой карты без необходимости вручную отслеживать события изменения.

Обработка событий карты

Mapbox GL JS предоставляет богатую систему событий: перемещение карты, клики, загрузка стиля, взаимодействие с слоями.

onMount(() => {
  map = new mapboxgl.Map({
    container: mapContainer,
    style: "mapbox://styles/mapbox/light-v11",
    center: [30, 50],
    zoom: 3
  });

  map.on("move", () => {
    const center = map.getCenter();
    lng = center.lng;
    lat = center.lat;
  });
});

Таким образом достигается двусторонняя синхронизация: изменения состояния обновляют карту, а действия пользователя обновляют состояние.

Работа со стилями и динамическая смена слоя

Mapbox GL JS поддерживает динамическое переключение стилей карты.

let style = "mapbox://styles/mapbox/streets-v12";

$: if (map && style) {
  map.setStyle(style);
}

Изменение переменной style приводит к полной перезагрузке визуального представления карты.

При смене стиля важно учитывать момент завершения загрузки:

map.on("style.load", () => {
  // повторное добавление слоёв и источников
});

Добавление маркеров и синхронизация с реактивными данными

Маркер представляет собой отдельный DOM-объект, управляемый Mapbox GL JS.

let marker;

onMount(() => {
  map = new mapboxgl.Map({
    container: mapContainer,
    style: "mapbox://styles/mapbox/outdoors-v12",
    center: [10, 10],
    zoom: 4
  });

  marker = new mapboxgl.Marker()
    .setLngLat([10, 10])
    .addTo(map);
});

Реактивное обновление позиции маркера:

$: if (marker) {
  marker.setLngLat([lng, lat]);
}

Работа с источниками данных и слоями

Mapbox GL JS оперирует источниками (sources) и слоями (layers). Добавление GeoJSON источника:

onMount(() => {
  map.on("load", () => {
    map.addSource("points", {
      type: "geojson",
      data: {
        type: "FeatureCollection",
        features: [
          {
            type: "Feature",
            geometry: {
              type: "Point",
              coordinates: [20, 20]
            }
          }
        ]
      }
    });

    map.addLayer({
      id: "points-layer",
      type: "circle",
      source: "points",
      paint: {
        "circle-radius": 6,
        "circle-color": "#ff0000"
      }
    });
  });
});

Обновление данных источника при изменении реактивной переменной:

$: if (map && map.getSource("points")) {
  map.getSource("points").setData(geojsonData);
}

Интеграция через stores Svelte

Для масштабных приложений используется разделение состояния через store-модель.

// mapStore.js
import { writable } from "svelte/store";

export const coordinates = writable({ lng: 0, lat: 0 });

Подписка внутри компонента карты:

import { coordinates } from "./mapStore";

coordinates.subscribe(value => {
  if (map) {
    map.setCenter([value.lng, value.lat]);
  }
});

Обратная синхронизация:

map.on("move", () => {
  const center = map.getCenter();
  coordinates.set({
    lng: center.lng,
    lat: center.lat
  });
});

Обработка SSR-ограничений

Svelte может использовать серверный рендеринг, однако Mapbox GL JS зависит от WebGL и DOM, что исключает выполнение на сервере.

Поэтому инициализация выполняется исключительно в клиентской среде:

import { browser } from "$app/environment";

onMount(() => {
  if (!browser) return;

  map = new mapboxgl.Map({
    container: mapContainer,
    style: "mapbox://styles/mapbox/dark-v11"
  });
});

Динамическое добавление интерактивных слоёв

Интерактивные слои позволяют реагировать на события наведения и клика.

map.on("click", "points-layer", (e) => {
  const feature = e.features[0];
  console.log(feature.geometry.coordinates);
});

Подсветка при наведении:

map.on("mouseenter", "points-layer", () => {
  map.getCanvas().style.cursor = "pointer";
});

map.on("mouseleave", "points-layer", () => {
  map.getCanvas().style.cursor = "";
});

Композиция компонентов карты

В архитектуре Svelte карта часто выделяется в отдельный компонент, а слои, маркеры и источники управляются через props и stores. Такой подход разделяет:

  • создание WebGL контекста
  • управление состоянием камеры
  • управление данными GeoJSON
  • UI-логика приложения

Работа с асинхронной загрузкой ресурсов

Mapbox GL JS загружает стили и тайлы асинхронно. Для синхронизации состояния используется событие load:

map.on("load", () => {
  mapLoaded = true;
});

Флаг загрузки позволяет безопасно выполнять операции добавления слоёв и источников.

Управление производительностью

При частых реактивных обновлениях важно учитывать стоимость операций:

  • setCenter и setZoom вызывают перерасчёт камеры
  • setData пересобирает source
  • setStyle полностью пересоздаёт стиль

Для оптимизации применяется группировка реактивных изменений и контроль частоты обновлений через промежуточные состояния.

Обработка resize и адаптивность

Размер контейнера карты должен синхронизироваться с layout:

import { onMount } from "svelte";

onMount(() => {
  const resizeObserver = new ResizeObserver(() => {
    map?.resize();
  });

  resizeObserver.observe(mapContainer);
});

Это обеспечивает корректное перерасчёт WebGL viewport при изменении размеров интерфейса.