Связка OpenLayers и Vue строится вокруг идеи изоляции императивного API карты внутри жизненного цикла компонента. OpenLayers работает напрямую с DOM и WebGL/Canvas, тогда как Vue управляет реактивностью и виртуальным DOM. Основная архитектурная задача — отделить управление картой от реактивного состояния, сохранив синхронизацию слоёв, центра, зума и объектов.
Ключевой принцип: карта и её instance живут вне реактивной системы Vue, но управляются через неё через refs и watchers.
OpenLayers устанавливается как обычная npm-библиотека:
npm install ol
Vue-проект обычно создаётся через Vite:
npm create vite@latest my-map-app -- --template vue
cd my-map-app
npm install
npm install ol
Минимальная структура для карты:
src/
components/
MapView.vue
composables/
useMap.js
main.js
Компонент карты строится вокруг DOM-элемента, в который OpenLayers монтирует своё отображение.
<template>
<div ref="mapContainer" class="map"></div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import Map from 'ol/Map'
import View from 'ol/View'
import TileLayer from 'ol/layer/Tile'
import OSM from 'ol/source/OSM'
const mapContainer = ref(null)
let map = null
onMounted(() => {
map = new Map({
target: mapContainer.value,
layers: [
new TileLayer({
source: new OSM()
})
],
view: new View({
center: [0, 0],
zoom: 2
})
})
})
onBeforeUnmount(() => {
if (map) {
map.setTarget(null)
map = null
}
})
</script>
<style>
.map {
width: 100%;
height: 100vh;
}
</style>
target должен быть привязан только после монтирования
компонентаsetTarget(null)
предотвращает утечки памятиVue-реактивность не должна напрямую «оборачивать» объект карты.
Вместо этого используется связка watch + imperative API
OpenLayers.
import { ref, watch } from 'vue'
const center = ref([0, 0])
const zoom = ref(3)
watch(center, (newCenter) => {
if (map) {
map.getView().setCenter(newCenter)
}
})
watch(zoom, (newZoom) => {
if (map) {
map.getView().setZoom(newZoom)
}
})
map.getView().on('change:center', () => {
center.value = map.getView().getCenter()
})
map.getView().on('change:zoom', () => {
zoom.value = map.getView().getZoom()
})
Vue хранит состояние, OpenLayers его исполняет
Для масштабируемости логика карты выносится в composable.
// composables/useMap.js
import { ref } from 'vue'
import Map from 'ol/Map'
import View from 'ol/View'
export function useMap() {
const map = ref(null)
const target = ref(null)
function initMap() {
map.value = new Map({
target: target.value,
view: new View({
center: [0, 0],
zoom: 2
})
})
}
function destroyMap() {
if (map.value) {
map.value.setTarget(null)
map.value = null
}
}
return {
map,
target,
initMap,
destroyMap
}
}
Использование в компоненте:
<script setup>
import { onMounted, onBeforeUnmount } from 'vue'
import { useMap } from '@/composables/useMap'
const { map, target, initMap, destroyMap } = useMap()
onMounted(initMap)
onBeforeUnmount(destroyMap)
</script>
<template>
<div ref="target" class="map"></div>
</template>
Слои OpenLayers должны храниться либо в массиве состояния, либо в отдельном store.
import { ref, watch } from 'vue'
import TileLayer from 'ol/layer/Tile'
import OSM from 'ol/source/OSM'
const layers = ref([])
watch(layers, (newLayers) => {
if (!map) return
map.getLayers().clear()
newLayers.forEach(layer => {
map.addLayer(layer)
})
})
layers.value.push(
new TileLayer({
source: new OSM()
})
)
OpenLayers не предназначен для полного пересоздания слоёв при каждом изменении. Более оптимально:
setVisible,
setOpacity, setSource)Работа с векторами требует отдельного подхода, так как источники
(VectorSource) обновляются часто.
import VectorLayer from 'ol/layer/Vector'
import VectorSource from 'ol/source/Vector'
import Feature from 'ol/Feature'
import Point from 'ol/geom/Point'
const source = new VectorSource()
const vectorLayer = new VectorLayer({
source
})
function addPoint(coord) {
const feature = new Feature({
geometry: new Point(coord)
})
source.addFeature(feature)
}
import { watch } from 'vue'
watch(featuresList, (list) => {
source.clear()
list.forEach(item => {
source.addFeature(
new Feature({
geometry: new Point(item.coords)
})
)
})
})
OpenLayers использует собственную систему событий.
map.on('click', (event) => {
const coordinate = event.coordinate
console.log('click:', coordinate)
})
import { defineEmits } from 'vue'
const emit = defineEmits(['map-click'])
map.on('click', (event) => {
emit('map-click', event.coordinate)
})
Оверлеи позволяют размещать Vue-компоненты поверх карты.
import Overlay from 'ol/Overlay'
const overlay = new Overlay({
element: document.getElementById('popup'),
positioning: 'bottom-center'
})
map.addOverlay(overlay)
<template>
<div ref="popup" class="popup">
{{ text }}
</div>
</template>
Основные узкие места:
setCenterОптимизационные подходы:
let pending = false
function scheduleUpdate(fn) {
if (pending) return
pending = true
requestAnimationFrame(() => {
fn()
pending = false
})
}
При использовании Pinia карта становится глобальным сервисом.
import { defineStore } from 'pinia'
export const useMapStore = defineStore('map', {
state: () => ({
center: [0, 0],
zoom: 2
}),
actions: {
setCenter(c) {
this.center = c
}
}
})
OpenLayers слушает store, но не хранится в нём напрямую.
Map в reactive()Типовая организация:
map/
core/
createMap.js
layers.js
composables/
useMap.js
useLayers.js
components/
MapView.vue
MapControls.vue
Разделение ответственности:
map.on('moveend', () => {
const view = map.getView()
console.log(view.getCenter(), view.getZoom())
})
UI-компоненты реагируют только на состояние, а не на внутреннюю логику карты.