Mapbox GL JS и Vue.js часто используются совместно в интерактивных веб-приложениях, где требуется управляемая реактивность интерфейса и высокая производительность рендеринга карт. Интеграция этих технологий строится вокруг жизненного цикла компонентов Vue и императивного API Mapbox GL JS, что требует аккуратной синхронизации DOM, состояния и ресурсов карты.
Mapbox GL JS работает напрямую с DOM-элементом контейнера карты, в то время как Vue управляет виртуальным DOM. Это определяет базовый принцип интеграции: карта инициализируется только после монтирования компонента, а все изменения конфигурации проксируются через реактивные свойства.
Основная архитектура обычно включает:
refБазовая установка Mapbox GL JS:
npm install mapbox-gl
Подключение стилей обязательно, так как без них карта не отрисуется корректно:
import mapboxgl from 'mapbox-gl'
import 'mapbox-gl/dist/mapbox-gl.css'
Наиболее распространённый подход — создание изолированного компонента
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)
})
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
})
})
Список часто используемых событий:
movezoomclickloadrenderidleДля масштабируемых проектов логично вынести логику в 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)
При интеграции важно учитывать:
setCenter и
setZoomrequestIdleCallback для тяжёлых
операцийПример debounce:
let timeout
watch(() => props.center, (val) => {
clearTimeout(timeout)
timeout = setTimeout(() => {
map?.setCenter(val)
}, 100)
})
При использовании SSR (например, Nuxt) Mapbox GL JS должен инициализироваться только на клиенте.
Типичный паттерн:
if (process.client) {
map = new mapboxgl.Map({
container: mapContainer.value,
style: props.style
})
}
Либо использование onMounted, что полностью исключает
серверный рендеринг карты.
При работе с несколькими экземплярами важно изолировать состояние:
ref для каждого контейнераid источников и слоёв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 интеграция требует явного описания типов:
import type mapboxgl from 'mapbox-gl'
let map: mapboxgl.Map | null = null
Props:
interface MapProps {
center: [number, number]
zoom: number
style: string
}
Иногда требуется размещать Vue-элементы поверх карты (tooltip, popup
UI). Это реализуется через Popup:
const popup = new mapboxgl.Popup()
.setLngLat([30, 50])
.setHTML('<div>Info</div>')
.addTo(map)
Более сложный вариант — использование Vue-компонентов, рендерящихся в DOM, привязанном к popup контейнеру.
При сложных интерфейсах карта становится источником истины для геосостояния:
Пример синхронизации:
map.on('moveend', () => {
const center = map.getCenter()
const zoom = map.getZoom()
emit('update:state', { center, zoom })
})
Интеграция Vue и Mapbox GL JS часто используется для:
Пример обновления 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()onUnmounted(() => {
map?.remove()
map = null
})
Для масштабных проектов создаётся Vue plugin:
export default {
install(app, options) {
app.config.globalProperties.$mapboxToken = options.token
}
}
Это позволяет централизованно управлять конфигурацией Mapbox GL JS во всех компонентах.