Интеграция Google Maps JavaScript API с Vue начинается с подготовки
окружения и выбора способа загрузки API. На практике используются два
подхода: подключение через официальный загрузчик
@googlemaps/js-api-loader либо ручная вставка скрипта в
index.html. Первый вариант предпочтительнее из-за
управляемости и корректной асинхронной инициализации.
Установка загрузчика:
npm install @googlemaps/js-api-loader
Ключ API хранится в переменных окружения:
VITE_GOOGLE_MAPS_API_KEY=your_api_key
Доступ к ключу в коде:
const apiKey = import.meta.env.VITE_GOOGLE_MAPS_API_KEY;
Vue 3 предоставляет удобную реактивную модель, которая хорошо сочетается с жизненным циклом Google Maps.
Создание компонента карты:
<template>
<div ref="mapContainer" class="map-container"></div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { Loader } from '@googlemaps/js-api-loader'
const mapContainer = ref(null)
let map = null
const loader = new Loader({
apiKey: import.meta.env.VITE_GOOGLE_MAPS_API_KEY,
version: 'weekly',
})
Инициализация карты после монтирования компонента:
onMounted(async () => {
await loader.load()
map = new google.maps.Map(mapContainer.value, {
center: { lat: 51.1605, lng: 71.4704 },
zoom: 10,
disableDefaultUI: true,
})
})
Карта сама по себе не является реактивным объектом Vue, поэтому все
изменения состояния должны синхронизироваться вручную через
watch или вычисляемые свойства.
Пример привязки центра карты к реактивным данным:
const center = ref({ lat: 51.1605, lng: 71.4704 })
watch(center, (newCenter) => {
if (map) {
map.setCenter(newCenter)
}
})
Изменение center.value автоматически приводит к
обновлению положения карты.
Маркер представляет собой отдельный объект Google Maps API и требует явного управления жизненным циклом.
Создание маркера:
let marker = null
function createMarker(position) {
marker = new google.maps.Marker({
position,
map,
draggable: true,
})
}
Использование реактивного списка точек:
const points = ref([
{ lat: 51.16, lng: 71.47 },
{ lat: 51.17, lng: 71.50 },
])
Отрисовка маркеров:
const markers = []
function renderMarkers() {
markers.forEach(m => m.setMap(null))
markers.length = 0
points.value.forEach(point => {
const m = new google.maps.Marker({
position: point,
map,
})
markers.push(m)
})
}
Синхронизация с Vue:
watch(points, () => {
if (map) renderMarkers()
}, { deep: true })
Google Maps использует собственную систему событий, которая
подключается через addListener.
Пример обработки клика по карте:
onMounted(async () => {
await loader.load()
map = new google.maps.Map(mapContainer.value, {
center: { lat: 51.1605, lng: 71.4704 },
zoom: 10,
})
map.addListener('click', (e) => {
points.value.push({
lat: e.latLng.lat(),
lng: e.latLng.lng(),
})
})
})
Обработка событий маркера:
function attachMarkerEvents(marker) {
marker.addListener('dragend', () => {
const position = marker.getPosition()
console.log(position.lat(), position.lng())
})
}
Для масштабируемых приложений логика карты выносится в composable-функцию.
import { ref, onMounted } from 'vue'
import { Loader } from '@googlemaps/js-api-loader'
export function useGoogleMap(containerRef) {
const map = ref(null)
const loader = new Loader({
apiKey: import.meta.env.VITE_GOOGLE_MAPS_API_KEY,
version: 'weekly',
})
const initMap = async (options) => {
await loader.load()
map.value = new google.maps.Map(containerRef.value, options)
}
return {
map,
initMap,
}
}
Использование в компоненте:
const mapContainer = ref(null)
const { map, initMap } = useGoogleMap(mapContainer)
onMounted(() => {
initMap({
center: { lat: 51.1605, lng: 71.4704 },
zoom: 10,
})
})
Инфоокна позволяют отображать HTML-контент над маркерами.
const infoWindow = new google.maps.InfoWindow()
function openInfo(marker, content) {
infoWindow.setContent(content)
infoWindow.open(map, marker)
}
Привязка к маркеру:
marker.addListener('click', () => {
openInfo(marker, '<div>Точка интереса</div>')
})
Для уменьшения нагрузки важно избегать повторной загрузки API. Используется единый экземпляр Loader.
let loaderInstance = null
export function getLoader() {
if (!loaderInstance) {
loaderInstance = new Loader({
apiKey: import.meta.env.VITE_GOOGLE_MAPS_API_KEY,
version: 'weekly',
})
}
return loaderInstance
}
Vue-компоненты могут размонтироваться, поэтому важно освобождать ресурсы:
onUnmounted(() => {
if (marker) marker.setMap(null)
markers.forEach(m => m.setMap(null))
markers.length = 0
map = null
})
OverlayView позволяет создавать произвольные элементы поверх карты.
class CustomOverlay extends google.maps.OverlayView {
constructor(position, element) {
super()
this.position = position
this.element = element
}
onAdd() {
this.getPanes().overlayLayer.appendChild(this.element)
}
draw() {
const projection = this.getProjection()
const point = projection.fromLatLngToDivPixel(this.position)
this.element.style.left = point.x + 'px'
this.element.style.top = point.y + 'px'
}
onRemove() {
if (this.element.parentNode) {
this.element.parentNode.removeChild(this.element)
}
}
}
При использовании Pinia состояние карты и точек выносится в store:
import { defineStore } from 'pinia'
export const useMapStore = defineStore('map', {
state: () => ({
points: [],
selectedPoint: null,
}),
actions: {
addPoint(point) {
this.points.push(point)
},
},
})
Связь store и карты:
const store = useMapStore()
watch(() => store.points, (points) => {
renderMarkers(points)
}, { deep: true })
Загрузка Google Maps может быть нестабильной, поэтому требуется обработка ошибок:
try {
await loader.load()
} catch (e) {
console.error('Ошибка загрузки Google Maps API', e)
}
При работе с сотнями или тысячами точек используется кластеризация:
import MarkerClusterer from '@googlemaps/markerclusterer'
const clusterer = new MarkerClusterer({ map, markers })
Кластеризация снижает нагрузку на DOM и повышает отзывчивость интерфейса.
Маршруты строятся через DirectionsService:
const service = new google.maps.DirectionsService()
const renderer = new google.maps.DirectionsRenderer()
renderer.setMap(map)
service.route(
{
origin: start,
destination: end,
travelMode: google.maps.TravelMode.DRIVING,
},
(result) => {
renderer.setDirections(result)
}
)
Связывание с Vue:
watch([start, end], () => {
if (start && end) calculateRoute()
})