Deck.gl представляет собой высокопроизводительную библиотеку визуализации геопространственных данных на базе WebGL. При использовании совместно с Vue.js возникает задача корректного связывания реактивной модели данных Vue с системой слоёв и механизмом рендеринга Deck.gl.
В типичной архитектуре Vue отвечает за:
Deck.gl берёт на себя:
Схема взаимодействия выглядит следующим образом:
Vue State
↓
Computed Properties
↓
Deck.gl Layers
↓
Deck Instance
↓
WebGL Rendering
Главная задача интеграции заключается в том, чтобы изменения реактивных данных Vue автоматически приводили к обновлению слоёв Deck.gl без лишних пересозданий экземпляров визуализации.
Минимальный набор пакетов:
npm install deck.gl
npm install @deck.gl/core
npm install @deck.gl/layers
Для работы совместно с картографическими движками дополнительно используются:
npm install maplibre-gl
npm install @deck.gl/mapbox
или
npm install mapbox-gl
Для проектов на Vue 3 рекомендуется использовать Vite:
npm create vite@latest
После создания проекта устанавливаются необходимые зависимости.
Простейшая интеграция обычно реализуется через отдельный компонент.
<template>
<div ref="container" class="deck-container"></div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { Deck } from '@deck.gl/core';
const container = ref(null);
let deck = null;
onMounted(() => {
deck = new Deck({
parent: container.value,
initialViewState: {
longitude: 37.6173,
latitude: 55.7558,
zoom: 10
},
controller: true,
layers: []
});
});
onUnmounted(() => {
if (deck) {
deck.finalize();
}
});
</script>
<style>
.deck-container {
width: 100%;
height: 100vh;
}
</style>
После монтирования компонента создаётся экземпляр Deck,
который получает DOM-контейнер для вывода WebGL-сцены.
Основное преимущество Vue заключается в реактивности.
Предположим, имеется массив координат:
const points = ref([
{
position: [37.6173, 55.7558]
},
{
position: [37.6200, 55.7600]
}
]);
На основе этих данных можно формировать слой.
import { ScatterplotLayer } from '@deck.gl/layers';
const createLayers = () => [
new ScatterplotLayer({
id: 'points',
data: points.value,
getPosition: d => d.position,
getRadius: 100,
getFillColor: [255, 0, 0]
})
];
При изменении массива необходимо обновлять список слоёв:
watch(points, () => {
deck.setProps({
layers: createLayers()
});
}, { deep: true });
Таким образом данные Vue становятся источником состояния для Deck.gl.
Наиболее удобный подход основан на вычисляемых свойствах.
const layers = computed(() => [
new ScatterplotLayer({
id: 'points',
data: points.value,
getPosition: d => d.position,
getRadius: 100
})
]);
Обновление Deck.gl:
watch(layers, value => {
deck.setProps({
layers: value
});
});
Такой подход хорошо масштабируется при большом количестве слоёв.
При крупных проектах логика работы с Deck.gl выносится в composable.
import { Deck } from '@deck.gl/core';
import { ref } from 'vue';
export function useDeck() {
const deck = ref(null);
const createDeck = (container, options) => {
deck.value = new Deck({
parent: container,
...options
});
};
const updateLayers = layers => {
deck.value?.setProps({
layers
});
};
const destroyDeck = () => {
deck.value?.finalize();
};
return {
deck,
createDeck,
updateLayers,
destroyDeck
};
}
Использование:
const {
createDeck,
updateLayers,
destroyDeck
} = useDeck();
Подобная организация делает компоненты значительно чище.
Одним из самых распространённых сценариев является отображение слоёв поверх карты.
<template>
<div ref="mapContainer"></div>
</template>
<script setup>
import maplibregl from 'maplibre-gl';
import { onMounted, ref } from 'vue';
const mapContainer = ref();
onMounted(() => {
new maplibregl.Map({
container: mapContainer.value,
style: 'https://demotiles.maplibre.org/style.json',
center: [37.6173, 55.7558],
zoom: 10
});
});
</script>
Deck.gl может быть встроен непосредственно в карту.
import { MapboxOverlay } from '@deck.gl/mapbox';
Создание оверлея:
const overlay = new MapboxOverlay({
layers: []
});
map.addControl(overlay);
Обновление слоёв:
overlay.setProps({
layers
});
В результате карта и визуализация используют одну камеру и один набор параметров навигации.
Часто требуется включать и отключать слои через интерфейс.
const showPoints = ref(true);
const showHeatmap = ref(false);
Формирование списка слоёв:
const layers = computed(() => {
const result = [];
if (showPoints.value) {
result.push(createPointLayer());
}
if (showHeatmap.value) {
result.push(createHeatmapLayer());
}
return result;
});
Связь между состоянием интерфейса и картой становится полностью декларативной.
Deck.gl предоставляет систему событий для взаимодействия с объектами.
Пример обработки клика:
new ScatterplotLayer({
id: 'points',
data,
pickable: true,
onClick: info => {
console.log(info.object);
}
});
Для передачи данных в Vue:
const selectedObject = ref(null);
const layer = new ScatterplotLayer({
id: 'points',
data,
pickable: true,
onClick: info => {
selectedObject.value = info.object;
}
});
После этого выбранный объект может использоваться любыми компонентами приложения.
Типичный сценарий:
const tooltip = ref(null);
Создание слоя:
new ScatterplotLayer({
id: 'points',
data,
pickable: true,
onHover: info => {
tooltip.value = {
x: info.x,
y: info.y,
object: info.object
};
}
});
Шаблон Vue:
<div
v-if="tooltip"
class="tooltip"
:style="{
left: tooltip.x + 'px',
top: tooltip.y + 'px'
}"
>
{{ tooltip.object.name }}
</div>
Позиционирование и внешний вид полностью контролируются средствами Vue.
Получение данных с сервера обычно осуществляется через
fetch.
const points = ref([]);
onMounted(async () => {
const response = await fetch('/api/points');
points.value = await response.json();
});
После изменения массива автоматически пересчитываются слои и обновляется визуализация.
Создание новых экземпляров слоёв при каждом изменении может быть затратным.
Deck.gl использует механизм сравнения свойств.
new ScatterplotLayer({
id: 'points',
data,
updateTriggers: {
getFillColor: selectedCategory.value
},
getFillColor: d => {
return d.category === selectedCategory.value
? [255, 0, 0]
: [0, 0, 255];
}
});
updateTriggers позволяет пересчитывать только
необходимые атрибуты.
В крупных приложениях данные часто располагаются в централизованном хранилище.
export const useMapStore = defineStore('map', {
state: () => ({
points: [],
selectedPoint: null
})
});
Компонент:
const store = useMapStore();
Слой:
new ScatterplotLayer({
id: 'points',
data: store.points,
onClick: info => {
store.selectedPoint = info.object;
}
});
Такой подход обеспечивает единый источник данных для всей системы.
Положение камеры может храниться в реактивном состоянии.
const viewState = ref({
longitude: 37.6173,
latitude: 55.7558,
zoom: 10,
pitch: 0,
bearing: 0
});
Обновление:
watch(viewState, value => {
deck.setProps({
viewState: value
});
}, { deep: true });
Программное перемещение камеры:
viewState.value = {
...viewState.value,
zoom: 14
};
Deck.gl позволяет комбинировать различные типы визуализации.
const layers = [
scatterLayer,
heatmapLayer,
arcLayer,
geoJsonLayer
];
В Vue обычно используется единое вычисляемое свойство:
const layers = computed(() => [
pointLayer.value,
polygonLayer.value,
routeLayer.value
].filter(Boolean));
Такой подход упрощает поддержку сложных картографических интерфейсов.
Состояние карты может синхронизироваться с URL.
router.push({
query: {
zoom: viewState.value.zoom,
lat: viewState.value.latitude,
lng: viewState.value.longitude
}
});
Восстановление состояния:
const zoom = Number(route.query.zoom);
const lat = Number(route.query.lat);
const lng = Number(route.query.lng);
Это позволяет создавать ссылки на конкретные области карты.
Любой экземпляр Deck.gl должен корректно уничтожаться.
onUnmounted(() => {
deck.finalize();
});
Особенно важно это для:
Без вызова finalize() WebGL-ресурсы могут оставаться
занятыми.
Пример организации каталогов:
src
├── components
│ ├── DeckMap.vue
│ ├── Tooltip.vue
│ └── LayerControl.vue
│
├── composables
│ └── useDeck.js
│
├── stores
│ └── mapStore.js
│
├── layers
│ ├── pointLayer.js
│ ├── heatmapLayer.js
│ ├── routeLayer.js
│ └── polygonLayer.js
│
├── services
│ └── api.js
│
└── views
└── MapView.vue
Выделение слоёв в отдельные модули особенно полезно при работе с большим количеством визуализаций.
Для повышения переиспользуемости логика создания слоёв выносится в отдельные функции.
import { ScatterplotLayer } from '@deck.gl/layers';
export function createPointLayer(data) {
return new ScatterplotLayer({
id: 'points',
data,
getPosition: d => d.coordinates,
getRadius: 100
});
}
Использование:
const layers = computed(() => [
createPointLayer(points.value)
]);
Такой подход облегчает тестирование и поддержку визуализаций.
При использовании SSR необходимо учитывать, что Deck.gl зависит от браузерного окружения и WebGL.
Создание экземпляра должно происходить только после монтирования:
onMounted(() => {
initializeDeck();
});
Нельзя создавать объекты Deck.gl на этапе выполнения серверного кода:
// Ошибка для SSR
const deck = new Deck({...});
Корректный вариант:
let deck;
onMounted(() => {
deck = new Deck({...});
});
Это особенно важно при использовании Nuxt и других SSR-фреймворков на базе Vue.
Неправильно:
watch(data, () => {
deck = new Deck({...});
});
Правильно:
watch(data, () => {
deck.setProps({
layers: createLayers()
});
});
Неправильно:
onUnmounted(() => {});
Правильно:
onUnmounted(() => {
deck.finalize();
});
Неправильно:
watch(
hugeDataset,
callback,
{ deep: true }
);
Предпочтительнее:
watch(
() => hugeDataset.value,
callback
);
Неправильно:
<div>
{{ createLayer() }}
</div>
Правильно:
const layers = computed(() => {
return createLayers();
});
Разделение ответственности между Vue и Deck.gl позволяет сохранять высокую производительность даже при работе с сотнями тысяч и миллионами геопространственных объектов.