Google Maps JavaScript API используется как императивная библиотека,
работающая с глобальным объектом google.maps. Vue.js,
напротив, строится на реактивности и декларативном описании UI. Основная
сложность интеграции заключается в согласовании императивной модели
карты с реактивной моделью состояния компонентов.
Типовая архитектура Vue-обёртки над картой включает три слоя:
google.maps.Map)Ключевое правило архитектуры — карта не должна пересоздаваться при каждом изменении состояния, изменяются только её параметры и дочерние сущности.
API подключается через загрузку внешнего скрипта. В Vue-проектах это обычно реализуется через singleton-загрузчик.
let googleMapsPromise = null;
export function loadGoogleMaps(apiKey) {
if (googleMapsPromise) return googleMapsPromise;
googleMapsPromise = new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places`;
script.async = true;
script.defer = true;
script.onl oad = () => resolve(window.google);
script.oner ror = reject;
document.head.appendChild(script);
});
return googleMapsPromise;
}
Особенность подхода заключается в предотвращении повторной загрузки API при переходе между страницами и повторном использовании компонента.
Компонент карты является контейнером для google.maps.Map
и управляет жизненным циклом экземпляра карты.
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
import { loadGoogleMaps } from './loadGoogleMaps';
export default {
props: {
apiKey: String,
center: Object,
zoom: Number
},
setup(props) {
const mapContainer = ref(null);
const map = ref(null);
onMounted(async () => {
const google = await loadGoogleMaps(props.apiKey);
map.value = new google.maps.Map(mapContainer.value, {
center: props.center,
zoom: props.zoom
});
});
onBeforeUnmount(() => {
map.value = null;
});
watch(() => props.center, (newCenter) => {
if (map.value) {
map.value.setCenter(newCenter);
}
}, { deep: true });
watch(() => props.zoom, (newZoom) => {
if (map.value) {
map.value.setZoom(newZoom);
}
});
return { mapContainer };
}
};
Vue-реактивность не распространяется на объекты Google Maps. Любое изменение состояния должно быть явно синхронизировано.
Типовые сценарии синхронизации:
Изменения простых параметров обрабатываются через
watch:
watch(() => props.center, (val) => {
map.value?.panTo(val);
});
watch(() => props.zoom, (val) => {
map.value?.setZoom(val);
});
Использование panTo предпочтительнее
setCenter, так как обеспечивает плавное перемещение.
Маркерная система требует синхронизации массива объектов Vue с
экземплярами google.maps.Marker.
props: {
markers: {
type: Array,
default: () => []
}
}
const markerInstances = new Map();
watch(() => props.markers, (newMarkers) => {
const google = window.google;
// удаление старых
markerInstances.forEach((marker, id) => {
const exists = newMarkers.find(m => m.id === id);
if (!exists) {
marker.setMap(null);
markerInstances.delete(id);
}
});
// добавление новых
newMarkers.forEach(m => {
if (!markerInstances.has(m.id)) {
const marker = new google.maps.Marker({
position: m.position,
map: map.value,
title: m.title
});
markerInstances.set(m.id, marker);
}
});
}, { deep: true });
События Google Maps являются императивными и должны преобразовываться в Vue-события.
map.value.addListener('click', (e) => {
emit('map-click', {
lat: e.latLng.lat(),
lng: e.latLng.lng()
});
});
Типовая схема:
Для масштабируемых систем карта часто становится провайдером контекста.
// MapProvider
provide('googleMap', map);
Дочерние компоненты:
const map = inject('googleMap');
Это позволяет создавать модульные сущности:
Каждый дочерний компонент регистрируется в контексте карты без проп-дриллинга.
export default {
props: {
position: Object,
title: String
},
setup(props) {
const map = inject('googleMap');
let marker = null;
onMounted(() => {
marker = new google.maps.Marker({
position: props.position,
map: map.value,
title: props.title
});
});
watch(() => props.position, (pos) => {
marker?.setPosition(pos);
});
onBeforeUnmount(() => {
marker?.setMap(null);
});
}
};
При работе с картами критично учитывать:
Избыточные watch вызывают каскад перерисовок.
Особенно для drag-событий:
function debounce(fn, delay) {
let t;
return (...args) => {
clearTimeout(t);
t = setTimeout(() => fn(...args), delay);
};
}
Вместо поэлементного обновления предпочтительно diff-алгоритмы по ID.
Автоматическая подгонка карты под набор точек:
const bounds = new google.maps.LatLngBounds();
markers.forEach(m => {
bounds.extend(m.position);
});
map.value.fitBounds(bounds);
При реактивном обновлении важно избегать постоянного
fitBounds, иначе пользователь не сможет управлять
картой.
Google Maps позволяет создавать пользовательские DOM-оверлеи через
OverlayView.
class CustomOverlay extends google.maps.OverlayView {
constructor(position, el) {
super();
this.position = position;
this.el = el;
}
onAdd() {
this.getPanes().overlayLayer.appendChild(this.el);
}
draw() {
const projection = this.getProjection();
const point = projection.fromLatLngToDivPixel(this.position);
this.el.style.left = point.x + 'px';
this.el.style.top = point.y + 'px';
}
onRemove() {
this.el.remove();
}
}
Во Vue такие оверлеи обычно создаются внутри onMounted и
уничтожаются в onBeforeUnmount.
При использовании Nuxt или SSR-режимов важно предотвращать выполнение API на сервере:
if (typeof window === 'undefined') return;
Инициализация карты должна происходить строго в
onMounted.
Типичные источники утечек:
addListener)setMap(null)Рекомендуемая схема очистки:
onBeforeUnmount(() => {
markerInstances.forEach(m => m.setMap(null));
markerInstances.clear();
google.maps.event.clearInstanceListeners(map.value);
});
Для крупных приложений формируется компонентная иерархия:
MapContainer
MarkerLayer
PolylineLayer
OverlayLayer
Каждый слой отвечает за свой тип объектов и не смешивает ответственность с другими слоями.
Координаты карты часто связываются с формами:
watch(() => mapState.center, (val) => {
emit('update:center', val);
});
И обратная связь:
watch(() => props.center, (val) => {
map.value.panTo(val);
});
Это формирует двустороннюю синхронизацию состояния.
Вынос логики в переиспользуемый composable:
export function useGoogleMap(containerRef, options) {
const map = ref(null);
onMounted(async () => {
const google = await loadGoogleMaps(options.apiKey);
map.value = new google.maps.Map(containerRef.value, options);
});
return { map };
}
Такой подход отделяет UI от логики и облегчает тестирование и повторное использование компонентов.