Vue 3 composition API

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

Подключение библиотеки:

npm install mapbox-gl

Импорт стилей обязателен, иначе карта не отобразится корректно:

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

Создание базового компонента карты

Composition API позволяет строго контролировать инициализацию и уничтожение ресурсов через onMounted и onUnmounted.

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

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import mapboxgl from 'mapbox-gl'
import 'mapbox-gl/dist/mapbox-gl.css'

mapboxgl.accessToken = 'YOUR_MAPBOX_TOKEN'

const mapContainer = ref(null)
let map = null

onMounted(() => {
  map = new mapboxgl.Map({
    container: mapContainer.value,
    style: 'mapbox://styles/mapbox/streets-v12',
    center: [69.2401, 53.2145],
    zoom: 10
  })
})

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

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

Реактивная интеграция через Composition API

Vue 3 позволяет связывать состояние интерфейса с параметрами карты через watch. Это важно при изменении центра, масштаба или стиля.

import { ref, watch } from 'vue'

const center = ref([69.2401, 53.2145])
const zoom = ref(10)

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

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

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


Инкапсуляция логики карты в composable

Для масштабируемых приложений логика Mapbox выносится в отдельный composable. Это уменьшает связанность компонентов и упрощает повторное использование.

import { ref, onMounted, onUnmounted } from 'vue'
import mapboxgl from 'mapbox-gl'

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

  onMounted(() => {
    map.value = new mapboxgl.Map({
      container: containerRef.value,
      style: options.style || 'mapbox://styles/mapbox/streets-v12',
      center: options.center || [0, 0],
      zoom: options.zoom || 2
    })
  })

  onUnmounted(() => {
    map.value?.remove()
    map.value = null
  })

  return {
    map
  }
}

Использование composable:

<script setup>
import { ref } from 'vue'
import { useMapbox } from './useMapbox'

const container = ref(null)

const { map } = useMapbox(container, {
  center: [69.2401, 53.2145],
  zoom: 10
})
</script>

Работа с маркерами и реактивными данными

Mapbox GL JS использует императивное добавление объектов. В Vue 3 это удобно комбинировать с watch.

const markers = ref([
  { id: 1, coords: [69.24, 53.21] },
  { id: 2, coords: [69.25, 53.22] }
])

watch(markers, (newMarkers) => {
  if (!map) return

  newMarkers.forEach(m => {
    new mapboxgl.Marker()
      .setLngLat(m.coords)
      .addTo(map)
  })
}, { deep: true })

Важно учитывать, что Mapbox не управляет жизненным циклом маркеров автоматически, поэтому требуется хранить ссылки для удаления:

const markerInstances = []

function addMarker(coords) {
  const marker = new mapboxgl.Marker()
    .setLngLat(coords)
    .addTo(map)

  markerInstances.push(marker)
}

function clearMarkers() {
  markerInstances.forEach(m => m.remove())
  markerInstances.length = 0
}

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

Mapbox GL JS опирается на концепцию источников (sources) и слоёв (layers). В Vue 3 их добавление обычно выполняется после события загрузки карты.

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 updateSource(data) {
  const source = map.getSource('points')
  if (source) {
    source.setData(data)
  }
}

Связь Composition API и событий карты

Mapbox генерирует множество событий: движение карты, клики, загрузка стиля. Vue 3 позволяет обрабатывать их в рамках реактивной архитектуры.

const mapState = ref({
  lng: 0,
  lat: 0,
  zoom: 0
})

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

  mapState.value = {
    lng: center.lng,
    lat: center.lat,
    zoom: map.getZoom()
  }
})

Динамическая смена стилей карты

Mapbox поддерживает переключение стилей без пересоздания карты.

const style = ref('mapbox://styles/mapbox/streets-v12')

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

После смены стиля требуется повторная инициализация слоёв:

map.on('style.load', () => {
  map.addSource('points', {
    type: 'geojson',
    data: initialData
  })
})

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

Composition API требует явного контроля уничтожения объектов, созданных вне Vue.

onUnmounted(() => {
  markerInstances.forEach(m => m.remove())
  markerInstances.length = 0

  map?.remove()
  map = null
})

Игнорирование очистки приводит к утечкам памяти, особенно при частой навигации между страницами.


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

Mapbox позволяет добавлять кастомные элементы управления, которые легко интегрируются во Vue.

class CustomControl {
  onAdd(map) {
    this.map = map
    this.container = document.createElement('button')
    this.container.textContent = 'Reset'
    this.container.oncl ick = () => {
      map.setCenter([69.24, 53.21])
      map.setZoom(10)
    }
    return this.container
  }

  onRemove() {
    this.container.remove()
    this.map = null
  }
}

map.addControl(new CustomControl())

Работа с геоданными в реактивной модели

GeoJSON остаётся основным форматом передачи данных. В Vue 3 он удобно хранится в ref.

const geojson = ref({
  type: 'FeatureCollection',
  features: []
})

function addFeature(feature) {
  geojson.value.features.push(feature)
  updateSource(geojson.value)
}

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

Частые вызовы setData или добавление маркеров напрямую могут снижать производительность. Используются техники батчинга:

let queue = []

function queueUpdate(feature) {
  queue.push(feature)

  requestAnimationFrame(() => {
    if (queue.length === 0) return

    geojson.value.features.push(...queue)
    updateSource(geojson.value)

    queue = []
  })
}

Обработка асинхронных данных

Часто данные для карты приходят из API, что требует аккуратной синхронизации с состоянием карты.

async function fetchPoints() {
  const res = await fetch('/api/points')
  const data = await res.json()

  geojson.value = data

  if (map?.getSource('points')) {
    map.getSource('points').setData(data)
  }
}

Взаимодействие с Vue Router и динамическими страницами

При переходах между маршрутами карта должна корректно пересоздаваться или обновляться, особенно при использовании <keep-alive>.

import { onActivated, onDeactivated } from 'vue'

onActivated(() => {
  map?.resize()
})

onDeactivated(() => {
  // временная пауза взаимодействия
})

Масштабируемая архитектура приложения

При росте проекта логика карты разделяется на:

  • composables (инициализация, управление состоянием)
  • сервисы (работа с API и GeoJSON)
  • UI-компоненты (контролы, панели управления)
  • утилиты (работа с координатами)

Такое разделение позволяет поддерживать предсказуемую структуру при работе с Mapbox GL JS в рамках Vue 3 Composition API.