Интеграция с Vue

Интеграция 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

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

Карта сама по себе не является реактивным объектом 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

Для масштабируемых приложений логика карты выносится в 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)
    }
  }
}

Интеграция с Vuex или Pinia

При использовании 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 })

Асинхронная загрузка API и обработка ошибок

Загрузка 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()
})