Интеграция MapLibre GL JS в Vue опирается на связку императивного API рендеринга карты и реактивной модели управления состоянием Vue. Карта создаётся как внешний объект, не входящий в систему реактивности напрямую, но управляется через жизненный цикл компонентов и наблюдатели за состоянием.
Ключевая особенность архитектуры заключается в разделении ответственности:
Такой подход исключает попытки реактивизации самого объекта карты, что критично для производительности.
Библиотека MapLibre GL JS подключается как зависимость проекта:
npm install maplibre-gl
В компоненте Vue создаётся контейнер для карты:
<template>
<div ref="mapContainer" class="map"></div>
</template>
Инициализация выполняется после монтирования компонента:
import maplibregl from "maplibre-gl";
export default {
data() {
return {
map: null
};
},
mounted() {
this.map = new maplibregl.Map({
container: this.$refs.mapContainer,
style: "https://demotiles.maplibre.org/style.json",
center: [0, 0],
zoom: 2
});
}
};
Важно учитывать, что объект карты создаётся только после появления DOM-узла.
Карта требует явного уничтожения при размонтировании компонента:
beforeUnmount() {
if (this.map) {
this.map.remove();
this.map = null;
}
}
Отсутствие очистки приводит к утечкам памяти из-за WebGL-контекста и подписок на события.
Реактивные данные Vue связываются с состоянием карты через наблюдатели:
watch: {
center(newVal) {
if (this.map) {
this.map.setCenter(newVal);
}
},
zoom(newVal) {
if (this.map) {
this.map.setZoom(newVal);
}
}
}
При этом изменения карты также могут синхронизироваться обратно:
mounted() {
this.map.on("move", () => {
const center = this.map.getCenter();
this.center = [center.lng, center.lat];
this.zoom = this.map.getZoom();
});
}
Возникает двунаправленная синхронизация, требующая защиты от циклических обновлений через флаги или throttling.
В Composition API интеграция становится более явной:
import { ref, onMounted, onBeforeUnmount, watch } from "vue";
import maplibregl from "maplibre-gl";
export default {
setup() {
const mapContainer = ref(null);
const map = ref(null);
const center = ref([0, 0]);
const zoom = ref(2);
onMounted(() => {
map.value = new maplibregl.Map({
container: mapContainer.value,
style: "https://demotiles.maplibre.org/style.json",
center: center.value,
zoom: zoom.value
});
map.value.on("move", () => {
const c = map.value.getCenter();
center.value = [c.lng, c.lat];
zoom.value = map.value.getZoom();
});
});
watch(center, (val) => {
if (map.value) map.value.setCenter(val);
});
watch(zoom, (val) => {
if (map.value) map.value.setZoom(val);
});
onBeforeUnmount(() => {
if (map.value) map.value.remove();
});
return { mapContainer, center, zoom };
}
};
Composition API упрощает изоляцию логики карты в переиспользуемые composables.
MapLibre использует модель sources + layers.
Добавление источника:
this.map.on("load", () => {
this.map.addSource("points", {
type: "geojson",
data: {
type: "FeatureCollection",
features: []
}
});
this.map.addLayer({
id: "points-layer",
type: "circle",
source: "points",
paint: {
"circle-radius": 6,
"circle-color": "#ff0000"
}
});
});
watch: {
geojson(newVal) {
if (this.map && this.map.getSource("points")) {
this.map.getSource("points").setData(newVal);
}
}
}
При больших объёмах данных важно использовать минимальные обновления
через setData, избегая пересоздания источников.
Маркер — отдельный DOM-объект, управляемый MapLibre:
const marker = new maplibregl.Marker()
.setLngLat([30, 50])
.addTo(this.map);
При использовании Vue-компонентов внутри маркеров создаётся мост
через createApp:
import { createApp } from "vue";
const el = document.createElement("div");
createApp({
template: "<div>Маркер</div>"
}).mount(el);
new maplibregl.Marker(el)
.setLngLat([30, 50])
.addTo(this.map);
Это позволяет внедрять полноценные Vue-интерфейсы в карту.
MapLibre предоставляет событийную модель:
this.map.on("click", (e) => {
console.log(e.lngLat);
});
Интеграция с Vue через эмиты:
this.map.on("click", (e) => {
this.$emit("map-click", e.lngLat);
});
События полезны для построения интерактивных слоёв, выбора объектов и редактирования геометрии.
Изменение размеров контейнера требует вызова:
this.map.resize();
В Vue это связывается с наблюдением за layout:
watch: {
sidebarOpen() {
this.$nextTick(() => {
this.map.resize();
});
}
}
Без вызова resize() WebGL-карта часто отображается
некорректно после изменения размеров контейнера.
Стили MapLibre загружаются асинхронно:
this.map.on("load", () => {
console.log("style loaded");
});
При динамическом переключении стилей:
watch(styleUrl, (url) => {
this.map.setStyle(url);
});
После смены стиля необходимо повторно добавлять слои и источники, так как они очищаются.
MapLibre требует доступа к window и WebGL, что исключает
серверный рендеринг.
В SSR-средах применяется условная инициализация:
if (process.client) {
this.map = new maplibregl.Map({...});
}
Компонент карты должен быть отключён от SSR через динамический импорт:
const MapComponent = defineAsyncComponent(() =>
import("./Map.vue")
);
При работе с Vue и MapLibre ключевые оптимизации включают:
requestAnimationFrame для частых
обновленийОсобое внимание требуется к частым событиям move и
render, которые способны перегружать реактивную
систему.
Вынос логики в composable упрощает масштабирование:
import maplibregl from "maplibre-gl";
import { ref, onMounted, onBeforeUnmount } from "vue";
export function useMap(containerRef) {
const map = ref(null);
onMounted(() => {
map.value = new maplibregl.Map({
container: containerRef.value,
style: "https://demotiles.maplibre.org/style.json"
});
});
onBeforeUnmount(() => {
map.value?.remove();
});
return { map };
}
Такой подход отделяет UI-логику Vue от инфраструктуры карты.
При наличии нескольких экземпляров карты важно изолировать состояние:
ref для каждого экземпляраНарушение изоляции приводит к перекрёстным обновлениям и конфликтам WebGL-контекстов.
MapLibre поддерживает плавные переходы:
this.map.flyTo({
center: [10, 10],
zoom: 5,
duration: 2000
});
Vue может управлять анимациями через реактивные триггеры состояния, например переключение режимов навигации или выбор объектов.
Карты часто используются как фон для UI-слоёв Vue:
Связь реализуется через общий store (например Pinia), где изменения фильтров влияют на GeoJSON источники карты.
При построении приложений с высокой интерактивностью возникает необходимость:
this.map.dragPan.disable();
this.map.scrollZoom.disable();
и обратное включение при выходе из режима.
При увеличении сложности приложения MapLibre выделяется в отдельный слой инфраструктуры:
Такое разделение снижает связанность и упрощает поддержку больших геоинформационных приложений.