Vue.js integration

Mapbox GL JS и Vue.js часто используются совместно в интерактивных веб-приложениях, где требуется управляемая реактивность интерфейса и высокая производительность рендеринга карт. Интеграция этих технологий строится вокруг жизненного цикла компонентов Vue и императивного API Mapbox GL JS, что требует аккуратной синхронизации DOM, состояния и ресурсов карты.


Mapbox GL JS работает напрямую с DOM-элементом контейнера карты, в то время как Vue управляет виртуальным DOM. Это определяет базовый принцип интеграции: карта инициализируется только после монтирования компонента, а все изменения конфигурации проксируются через реактивные свойства.

Основная архитектура обычно включает:

  • отдельный компонент карты
  • входные props для конфигурации (центр, zoom, стиль)
  • эмит событий (move, click, load)
  • управление экземпляром карты через ref
  • очистку ресурсов при размонтировании

Установка и подключение зависимостей

Базовая установка Mapbox GL JS:

npm install mapbox-gl

Подключение стилей обязательно, так как без них карта не отрисуется корректно:

import mapboxgl from 'mapbox-gl'
import 'mapbox-gl/dist/mapbox-gl.css'

Базовый компонент карты (Vue 3)

Наиболее распространённый подход — создание изолированного компонента MapView.vue.

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

<script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue'
import mapboxgl from 'mapbox-gl'

mapboxgl.accessToken = 'YOUR_TOKEN'

const props = defineProps({
  center: {
    type: Array,
    default: () => [0, 0]
  },
  zoom: {
    type: Number,
    default: 2
  },
  style: {
    type: String,
    default: 'mapbox://styles/mapbox/streets-v12'
  }
})

const emit = defineEmits(['load', 'move'])

const mapContainer = ref(null)
let map = null

onMounted(() => {
  map = new mapboxgl.Map({
    container: mapContainer.value,
    style: props.style,
    center: props.center,
    zoom: props.zoom
  })

  map.on('load', () => emit('load', map))
  map.on('move', () => {
    emit('move', {
      center: map.getCenter(),
      zoom: map.getZoom()
    })
  })
})

onUnmounted(() => {
  if (map) {
    map.remove()
    map = null
  }
})
</script>

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

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

Ключевая задача интеграции — синхронизация Vue props с состоянием карты. Mapbox GL JS не является реактивным, поэтому изменения нужно применять вручную через watch.

Обновление центра карты

watch(() => props.center, (newCenter) => {
  if (!map) return
  map.setCenter(newCenter)
})

Обновление zoom

watch(() => props.zoom, (newZoom) => {
  if (!map) return
  map.setZoom(newZoom)
})

Обновление стиля карты

Изменение стиля является более тяжёлой операцией, так как перезагружает ресурсы:

watch(() => props.style, (newStyle) => {
  if (!map) return
  map.setStyle(newStyle)
})

Работа с маркерами

Маркер в Mapbox GL JS — императивный объект, который нужно хранить отдельно.

let marker = null

function addMarker(coords) {
  if (marker) marker.remove()

  marker = new mapboxgl.Marker()
    .setLngLat(coords)
    .addTo(map)
}

Реактивная интеграция:

watch(() => props.marker, (coords) => {
  if (!map || !coords) return
  addMarker(coords)
})

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

Mapbox GL JS использует модель “source + layer”. В Vue важно добавлять их после события 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': '#ff0000'
    }
  })
})

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

function updatePoints(data) {
  if (!map) return
  const source = map.getSource('points')
  if (source) {
    source.setData(data)
  }
}

Интеграция событий карты

Mapbox генерирует большое количество событий: click, move, zoom, idle.

Пример проброса событий в Vue:

map.on('click', (e) => {
  emit('click', {
    lng: e.lngLat.lng,
    lat: e.lngLat.lat
  })
})

Список часто используемых событий:

  • move
  • zoom
  • click
  • load
  • render
  • idle

Композиционный подход (Vue composables)

Для масштабируемых проектов логично вынести логику в composable.

// useMapbox.js
import mapboxgl from 'mapbox-gl'
import { ref } from 'vue'

export function useMapbox(containerRef, options) {
  const map = ref(null)

  function init() {
    map.value = new mapboxgl.Map({
      container: containerRef.value,
      ...options
    })
  }

  function destroy() {
    if (map.value) {
      map.value.remove()
      map.value = null
    }
  }

  return {
    map,
    init,
    destroy
  }
}

Использование в компоненте:

const { map, init, destroy } = useMapbox(mapContainer, {
  center: [0, 0],
  zoom: 3,
  style: 'mapbox://styles/mapbox/dark-v11'
})

onMounted(init)
onUnmounted(destroy)

Работа с Vue 3 reactivity и performance

При интеграции важно учитывать:

  • избегание частых вызовов setCenter и setZoom
  • debounce для пользовательских input
  • минимизация перерисовок источников
  • использование requestIdleCallback для тяжёлых операций

Пример debounce:

let timeout

watch(() => props.center, (val) => {
  clearTimeout(timeout)
  timeout = setTimeout(() => {
    map?.setCenter(val)
  }, 100)
})

SSR и Nuxt особенности

При использовании SSR (например, Nuxt) Mapbox GL JS должен инициализироваться только на клиенте.

Типичный паттерн:

if (process.client) {
  map = new mapboxgl.Map({
    container: mapContainer.value,
    style: props.style
  })
}

Либо использование onMounted, что полностью исключает серверный рендеринг карты.


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

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

  • отдельный ref для каждого контейнера
  • уникальные id источников и слоёв
  • предотвращение глобальных side effects
const maps = new Map()

function createMap(id, container) {
  const instance = new mapboxgl.Map({
    container,
    style: 'mapbox://styles/mapbox/light-v11'
  })

  maps.set(id, instance)
}

Динамическая загрузка компонентов карты

Для оптимизации bundle size используется lazy loading:

import { defineAsyncComponent } from 'vue'

const MapView = defineAsyncComponent(() =>
  import('./MapView.vue')
)

Это позволяет не загружать Mapbox GL JS до момента фактического отображения карты.


Типизация (TypeScript)

В TypeScript интеграция требует явного описания типов:

import type mapboxgl from 'mapbox-gl'

let map: mapboxgl.Map | null = null

Props:

interface MapProps {
  center: [number, number]
  zoom: number
  style: string
}

Работа с пользовательскими оверлеями Vue поверх карты

Иногда требуется размещать Vue-элементы поверх карты (tooltip, popup UI). Это реализуется через Popup:

const popup = new mapboxgl.Popup()
  .setLngLat([30, 50])
  .setHTML('<div>Info</div>')
  .addTo(map)

Более сложный вариант — использование Vue-компонентов, рендерящихся в DOM, привязанном к popup контейнеру.


Синхронизация состояния приложения и камеры карты

При сложных интерфейсах карта становится источником истины для геосостояния:

  • координаты центра
  • bounding box
  • zoom level

Пример синхронизации:

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

  emit('update:state', { center, zoom })
})

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

Интеграция Vue и Mapbox GL JS часто используется для:

  • трекинга объектов в реальном времени
  • визуализации кластеров
  • отображения heatmap
  • маршрутизации

Пример обновления GeoJSON в реальном времени:

function updateRealtime(features) {
  const source = map.getSource('realtime')
  source.setData({
    type: 'FeatureCollection',
    features
  })
}

Кластеризация данных

map.addSource('clusters', {
  type: 'geojson',
  data: geojsonData,
  cluster: true,
  clusterMaxZoom: 14,
  clusterRadius: 50
})

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

Корректное уничтожение включает:

  • map.remove()
  • очистку маркеров
  • удаление listeners
  • сброс ссылок
onUnmounted(() => {
  map?.remove()
  map = null
})

Паттерн plugin для Vue

Для масштабных проектов создаётся Vue plugin:

export default {
  install(app, options) {
    app.config.globalProperties.$mapboxToken = options.token
  }
}

Это позволяет централизованно управлять конфигурацией Mapbox GL JS во всех компонентах.