Vue интеграция

Архитектурная модель интеграции

Интеграция MapLibre GL JS в Vue опирается на связку императивного API рендеринга карты и реактивной модели управления состоянием Vue. Карта создаётся как внешний объект, не входящий в систему реактивности напрямую, но управляется через жизненный цикл компонентов и наблюдатели за состоянием.

Ключевая особенность архитектуры заключается в разделении ответственности:

  • Vue управляет состоянием интерфейса и параметрами карты
  • MapLibre GL JS отвечает за отрисовку WebGL сцены
  • связующий слой синхронизирует состояние и карту

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


Установка и базовая инициализация

Библиотека MapLibre GL JS подключается как зависимость проекта:

npm install maplibre-gl

В компоненте Vue создаётся контейнер для карты:

<template>
  <div ref="mapContainer" class="map"></div>
</template>

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

import maplibregl from "maplibre-gl";

export default {
  data() {
    return {
      map: null
    };
  },
  mounted() {
    this.map = new maplibregl.Map({
      container: this.$refs.mapContainer,
      style: "https://demotiles.maplibre.org/style.json",
      center: [0, 0],
      zoom: 2
    });
  }
};

Важно учитывать, что объект карты создаётся только после появления DOM-узла.


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

Карта требует явного уничтожения при размонтировании компонента:

beforeUnmount() {
  if (this.map) {
    this.map.remove();
    this.map = null;
  }
}

Отсутствие очистки приводит к утечкам памяти из-за WebGL-контекста и подписок на события.


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

Центр и масштаб

Реактивные данные Vue связываются с состоянием карты через наблюдатели:

watch: {
  center(newVal) {
    if (this.map) {
      this.map.setCenter(newVal);
    }
  },
  zoom(newVal) {
    if (this.map) {
      this.map.setZoom(newVal);
    }
  }
}

При этом изменения карты также могут синхронизироваться обратно:

mounted() {
  this.map.on("move", () => {
    const center = this.map.getCenter();
    this.center = [center.lng, center.lat];
    this.zoom = this.map.getZoom();
  });
}

Возникает двунаправленная синхронизация, требующая защиты от циклических обновлений через флаги или throttling.


Composition API и современный подход

В Composition API интеграция становится более явной:

import { ref, onMounted, onBeforeUnmount, watch } from "vue";
import maplibregl from "maplibre-gl";

export default {
  setup() {
    const mapContainer = ref(null);
    const map = ref(null);

    const center = ref([0, 0]);
    const zoom = ref(2);

    onMounted(() => {
      map.value = new maplibregl.Map({
        container: mapContainer.value,
        style: "https://demotiles.maplibre.org/style.json",
        center: center.value,
        zoom: zoom.value
      });

      map.value.on("move", () => {
        const c = map.value.getCenter();
        center.value = [c.lng, c.lat];
        zoom.value = map.value.getZoom();
      });
    });

    watch(center, (val) => {
      if (map.value) map.value.setCenter(val);
    });

    watch(zoom, (val) => {
      if (map.value) map.value.setZoom(val);
    });

    onBeforeUnmount(() => {
      if (map.value) map.value.remove();
    });

    return { mapContainer, center, zoom };
  }
};

Composition API упрощает изоляцию логики карты в переиспользуемые composables.


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

MapLibre использует модель sources + layers.

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

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

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

Реактивное обновление GeoJSON

watch: {
  geojson(newVal) {
    if (this.map && this.map.getSource("points")) {
      this.map.getSource("points").setData(newVal);
    }
  }
}

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


Интеграция маркеров в Vue

Маркер — отдельный DOM-объект, управляемый MapLibre:

const marker = new maplibregl.Marker()
  .setLngLat([30, 50])
  .addTo(this.map);

При использовании Vue-компонентов внутри маркеров создаётся мост через createApp:

import { createApp } from "vue";

const el = document.createElement("div");

createApp({
  template: "<div>Маркер</div>"
}).mount(el);

new maplibregl.Marker(el)
  .setLngLat([30, 50])
  .addTo(this.map);

Это позволяет внедрять полноценные Vue-интерфейсы в карту.


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

MapLibre предоставляет событийную модель:

this.map.on("click", (e) => {
  console.log(e.lngLat);
});

Интеграция с Vue через эмиты:

this.map.on("click", (e) => {
  this.$emit("map-click", e.lngLat);
});

События полезны для построения интерактивных слоёв, выбора объектов и редактирования геометрии.


Адаптация размера карты

Изменение размеров контейнера требует вызова:

this.map.resize();

В Vue это связывается с наблюдением за layout:

watch: {
  sidebarOpen() {
    this.$nextTick(() => {
      this.map.resize();
    });
  }
}

Без вызова resize() WebGL-карта часто отображается некорректно после изменения размеров контейнера.


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

Стили MapLibre загружаются асинхронно:

this.map.on("load", () => {
  console.log("style loaded");
});

При динамическом переключении стилей:

watch(styleUrl, (url) => {
  this.map.setStyle(url);
});

После смены стиля необходимо повторно добавлять слои и источники, так как они очищаются.


Использование в Nuxt и SSR

MapLibre требует доступа к window и WebGL, что исключает серверный рендеринг.

В SSR-средах применяется условная инициализация:

if (process.client) {
  this.map = new maplibregl.Map({...});
}

Компонент карты должен быть отключён от SSR через динамический импорт:

const MapComponent = defineAsyncComponent(() =>
  import("./Map.vue")
);

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

При работе с Vue и MapLibre ключевые оптимизации включают:

  • минимизацию реактивных данных, связанных с картой
  • использование requestAnimationFrame для частых обновлений
  • батчинг обновлений GeoJSON
  • отключение ненужных слоёв
  • избегание глубоких watch на объектах карты

Особое внимание требуется к частым событиям move и render, которые способны перегружать реактивную систему.


Паттерн composable для карты

Вынос логики в composable упрощает масштабирование:

import maplibregl from "maplibre-gl";
import { ref, onMounted, onBeforeUnmount } from "vue";

export function useMap(containerRef) {
  const map = ref(null);

  onMounted(() => {
    map.value = new maplibregl.Map({
      container: containerRef.value,
      style: "https://demotiles.maplibre.org/style.json"
    });
  });

  onBeforeUnmount(() => {
    map.value?.remove();
  });

  return { map };
}

Такой подход отделяет UI-логику Vue от инфраструктуры карты.


Синхронизация нескольких карт

При наличии нескольких экземпляров карты важно изолировать состояние:

  • отдельные ref для каждого экземпляра
  • независимые источники данных
  • разделённые event handlers

Нарушение изоляции приводит к перекрёстным обновлениям и конфликтам WebGL-контекстов.


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

MapLibre поддерживает плавные переходы:

this.map.flyTo({
  center: [10, 10],
  zoom: 5,
  duration: 2000
});

Vue может управлять анимациями через реактивные триггеры состояния, например переключение режимов навигации или выбор объектов.


Взаимодействие с пользовательским интерфейсом

Карты часто используются как фон для UI-слоёв Vue:

  • панели фильтров
  • списки объектов
  • модальные окна
  • инструменты рисования

Связь реализуется через общий store (например Pinia), где изменения фильтров влияют на GeoJSON источники карты.


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

При построении приложений с высокой интерактивностью возникает необходимость:

  • блокировки событий карты при открытых модальных окнах
  • временного отключения drag/zoom
  • переключения режимов редактирования
this.map.dragPan.disable();
this.map.scrollZoom.disable();

и обратное включение при выходе из режима.


Масштабирование архитектуры интеграции

При увеличении сложности приложения MapLibre выделяется в отдельный слой инфраструктуры:

  • MapService (инициализация и управление экземпляром)
  • MapComposable (реактивный интерфейс)
  • MapComponents (UI-обвязка Vue)

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