Svelte интеграция

Базовая интеграция MapLibre GL JS в Svelte начинается с установки зависимостей и подготовки контейнера для WebGL-карты.

npm install maplibre-gl

Дополнительно требуется CSS-бандл библиотеки, обеспечивающий корректное отображение элементов карты:

import 'maplibre-gl/dist/maplibre-gl.css';

В типичном Svelte-проекте (Vite-based) структура компонентов позволяет изолировать карту в отдельный модуль, что упрощает управление жизненным циклом WebGL-контекста и предотвращает утечки ресурсов.


Базовый компонент карты

Создание карты в Svelte строится вокруг onMount, поскольку доступ к DOM и WebGL-контексту возможен только на клиенте.

<script>
  import { onMount, onDestroy } from 'svelte';
  import maplibregl from 'maplibre-gl';
  import 'maplibre-gl/dist/maplibre-gl.css';

  let mapContainer;
  let map;

  onMount(() => {
    map = new maplibregl.Map({
      container: mapContainer,
      style: 'https://demotiles.maplibre.org/style.json',
      center: [37.6173, 55.7558],
      zoom: 10
    });

    map.addControl(new maplibregl.NavigationControl());

    return () => {
      map.remove();
    };
  });
</script>

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

<style>
  .map {
    width: 100%;
    height: 100vh;
  }
</style>

Ключевым элементом является bind:this, обеспечивающий передачу DOM-узла в MapLibre как контейнера рендеринга.


Жизненный цикл и управление ресурсами

MapLibre GL JS создаёт WebGL-контекст, который требует явного освобождения. В Svelte это связывается с фазой уничтожения компонента.

Основные аспекты управления:

  • инициализация происходит внутри onMount
  • уничтожение выполняется через map.remove()
  • предотвращается накопление WebGL-контекстов при повторных рендерах

Типичная ошибка — повторное создание карты без очистки предыдущего экземпляра, что приводит к утечкам памяти и блокировке GPU-ресурсов.


Реактивное управление состоянием карты

Svelte-реактивность позволяет связывать параметры карты с переменными состояния. MapLibre не является реактивным по своей природе, поэтому синхронизация выполняется вручную через set-методы API.

Центр карты

<script>
  export let center = [0, 0];
  let map;

  $: if (map && center) {
    map.setCenter(center);
  }
</script>

Изменение center приводит к вызову setCenter, синхронизирующему визуальное состояние карты.


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

<script>
  export let zoom = 5;
  export let bearing = 0;
  export let pitch = 0;

  $: if (map) {
    map.setZoom(zoom);
    map.setBearing(bearing);
    map.setPitch(pitch);
  }
</script>

При частых изменениях параметров целесообразно применять debounce-механизмы, чтобы снизить нагрузку на WebGL-поток.


Двусторонняя синхронизация состояния

MapLibre генерирует события, отражающие изменения камеры. Эти события могут синхронизироваться с состоянием Svelte.

map.on('move', () => {
  const center = map.getCenter();
  const zoom = map.getZoom();

  // обновление внешнего состояния
});

Для предотвращения циклических обновлений используется флаг блокировки:

let internalUpdate = false;

map.on('move', () => {
  if (internalUpdate) return;
});

Добавление источников и слоёв

MapLibre GL JS опирается на модель источников данных и слоёв визуализации. В Svelte их добавление обычно выполняется после события load.

map.on('load', () => {
  map.addSource('points', {
    type: 'geojson',
    data: {
      type: 'FeatureCollection',
      features: []
    }
  });

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

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

map.getSource('points').setData(newGeojson);

Интеграция с реактивными данными Svelte stores

Svelte stores обеспечивают централизованное состояние для геоданных и параметров карты.

import { writable } from 'svelte/store';

export const geoData = writable({
  type: 'FeatureCollection',
  features: []
});

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

<script>
  import { geoData } from './stores.js';

  let map;

  geoData.subscribe((data) => {
    if (map && map.getSource('points')) {
      map.getSource('points').setData(data);
    }
  });
</script>

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


Асинхронная загрузка стилей

MapLibre позволяет динамически менять стиль карты. В Svelte это часто связано с реактивными переменными.

<script>
  export let styleUrl;

  $: if (map && styleUrl) {
    map.setStyle(styleUrl);
  }
</script>

После смены стиля требуется повторное добавление источников и слоёв, так как они не сохраняются при полной перезагрузке style JSON.

map.on('styledata', () => {
  // восстановление слоёв
});

Работа с маркерами и DOM-элементами

MapLibre поддерживает HTML-маркеры через Marker, что удобно в Svelte благодаря возможности рендеринга компонентов в DOM.

const markerNode = document.createElement('div');
markerNode.className = 'marker';

new maplibregl.Marker(markerNode)
  .setLngLat([37.6173, 55.7558])
  .addTo(map);

Svelte-компоненты могут быть смонтированы внутрь маркера через new Component({ target }), создавая полностью кастомные интерактивные элементы.


Геопространственные события

События MapLibre интегрируются с Svelte через стандартную модель подписок:

  • click
  • mousemove
  • zoom
  • rotate
  • drag
map.on('click', (e) => {
  const coordinates = e.lngLat;
});

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


Оптимизация производительности

При работе с MapLibre GL JS внутри Svelte критичны следующие аспекты:

Минимизация реактивных обновлений

Частые изменения состояния без контроля приводят к перегрузке WebGL:

  • объединение обновлений через батчинг
  • использование derived stores
  • ограничение частоты setData

Кэширование геоданных

GeoJSON-объекты больших размеров целесообразно мемоизировать. Повторная передача идентичных объектов в setData может вызывать ненужный пересчёт рендеринга.


Управление анимациями

MapLibre использует GPU-ускоренные анимации. При интеграции с Svelte важно избегать конкурирующих таймеров (requestAnimationFrame вне MapLibre), чтобы не создавать конфликт циклов отрисовки.


SSR и совместимость с SvelteKit

MapLibre GL JS зависит от WebGL и window, поэтому в SSR-окружениях требуется изоляция клиентского кода.

<script>
  import { browser } from '$app/environment';

  if (browser) {
    import('maplibre-gl').then((module) => {
      // инициализация
    });
  }
</script>

Компонент карты размещается только на клиентской стороне, предотвращая ошибки гидратации.


Обработка resize и адаптация контейнера

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

new ResizeObserver(() => {
  map.resize();
}).observe(mapContainer);

Это обеспечивает корректную перерисовку при изменении размеров flex/grid контейнеров.


Интеграция пользовательских контролов

MapLibre позволяет добавлять кастомные контролы через интерфейс IControl.

class CustomControl {
  onAdd(map) {
    this._map = map;
    this._container = document.createElement('div');
    this._container.className = 'custom-control';
    return this._container;
  }

  onRemove() {
    this._container.parentNode.removeChild(this._container);
    this._map = undefined;
  }
}

map.addControl(new CustomControl(), 'top-right');

В Svelte такие контролы часто синхронизируются с store-состоянием интерфейса.


Управление несколькими картами

При наличии нескольких экземпляров MapLibre в одном приложении требуется строгая изоляция состояния:

  • отдельный контейнер на каждую карту
  • независимые store-слои
  • уникальные идентификаторы источников и слоёв

Пересечение source id между картами приводит к конфликтам глобального registry MapLibre.


Работа с протоколом векторных тайлов

MapLibre поддерживает vector tiles через vector source:

map.addSource('tiles', {
  type: 'vector',
  url: 'pmtiles://endpoint'
});

В Svelte это часто комбинируется с динамической сменой источников в зависимости от состояния приложения, например выбора слоя данных или темы отображения.


Обработка ошибок и деградация

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

maplibregl.supported();

При отсутствии поддержки рендеринг может быть заменён статическим изображением или упрощённым DOM-слоем, управляемым Svelte.


Архитектура интеграции

Типовая структура приложения с MapLibre и Svelte:

  • store слой: геоданные, UI-состояние
  • map service слой: инкапсуляция MapLibre API
  • presentation слой: Svelte компоненты
  • adapter слой: синхронизация событий карты и store

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