Организация кода в приложениях на Deck.gl напрямую влияет на масштабируемость, производительность и удобство сопровождения. При увеличении количества слоёв, источников данных и интерактивных сценариев монолитная структура быстро становится узким местом. Базовый принцип — разделение ответственности между слоями, данными, состоянием камеры и логикой взаимодействия.
Типичная архитектура строится вокруг следующих сущностей:
Deck или DeckGLlayers)Разделение этих частей в разные модули позволяет избежать переполнения основного файла и упростить тестирование.
Один из ключевых паттернов — группировка слоёв по смысловым доменам. Вместо хранения всех слоёв в одном массиве они распределяются по отдельным модулям.
Пример структуры:
/layers
roadsLayer.js
buildingsLayer.js
vehiclesLayer.js
weatherLayer.js
Каждый модуль экспортирует фабрику слоя:
import { GeoJsonLayer } from '@deck.gl/layers';
export function createRoadsLayer(data) {
return new GeoJsonLayer({
id: 'roads-layer',
data,
stroked: true,
getLineColor: [120, 120, 120],
getLineWidth: 2
});
}
Такой подход даёт несколько преимуществ:
Использование фабрик слоёв является центральным паттерном организации кода. Вместо того чтобы создавать слои внутри компонента или основного файла, выносится функция-конструктор.
Фабрика может принимать не только данные, но и параметры конфигурации:
export function createBuildingsLayer(data, options = {}) {
return new ColumnLayer({
id: 'buildings',
data,
diskResolution: options.resolution || 12,
radius: options.radius || 50,
getPosition: d => d.position,
getElevation: d => d.height,
getFillColor: d => d.color || [200, 200, 200]
});
}
Ключевая идея — слой становится чистой функцией от данных и конфигурации.
В сложных сценах Deck.gl слои редко существуют изолированно. Они комбинируются в композиции, где каждый слой отвечает за свой аспект визуализации.
Организация композиции часто реализуется через функцию сборки:
import { createRoadsLayer } from './layers/roadsLayer';
import { createBuildingsLayer } from './layers/buildingsLayer';
import { createVehiclesLayer } from './layers/vehiclesLayer';
export function createLayers(state) {
return [
createRoadsLayer(state.roads),
createBuildingsLayer(state.buildings, state.buildingOptions),
createVehiclesLayer(state.vehicles, state.time)
];
}
Такая структура позволяет:
Частая ошибка — смешивание загрузки данных и визуализации внутри слоя. Более устойчивый подход заключается в выделении слоя данных отдельно от слоя представления.
Пример организации:
/data
fetchRoads.js
transformBuildings.js
normalizeVehicles.js
/layers
roadsLayer.js
buildingsLayer.js
vehiclesLayer.js
Функции обработки данных:
export async function fetchRoads() {
const response = await fetch('/api/roads');
const json = await response.json();
return json.features.map(f => ({
coordinates: f.geometry.coordinates,
type: f.properties.type
}));
}
Слои при этом получают уже подготовленные структуры, не занимаясь парсингом или фильтрацией.
В динамических приложениях слой зависит не только от данных, но и от состояния интерфейса: фильтров, времени, зума, выбранных объектов.
Организация через чистые функции состояния:
export function createFilteredLayer(data, filters) {
return new ScatterplotLayer({
id: 'points',
data: data.filter(d => d.value > filters.minValue),
getPosition: d => d.position,
getRadius: d => d.value
});
}
При изменении состояния пересоздаётся только нужный слой, а не вся сцена.
Состояние камеры (viewState) часто становится источником
дублирования логики. Для больших приложений используется
централизованный модуль управления видом.
/state
viewStateManager.js
export function createViewStateManager(initialState) {
let state = initialState;
function setViewState(newState) {
state = {
...state,
...newState
};
}
function getViewState() {
return state;
}
return {
setViewState,
getViewState
};
}
Это позволяет:
Интерактивность в Deck.gl строится вокруг picking-системы. При усложнении логики события лучше выносить в отдельный модуль.
/interactions
hoverHandler.js
clickHandler.js
selectionManager.js
Пример обработки hover:
export function onHover(info, state) {
if (info.object) {
return {
hoveredId: info.object.id,
tooltip: {
x: info.x,
y: info.y,
content: info.object.name
}
};
}
return {
hoveredId: null,
tooltip: null
};
}
Слой становится пассивным, а логика взаимодействия концентрируется отдельно.
При работе с разнородными источниками данных удобно вводить слой-адаптер, который приводит данные к единому формату.
export function adaptApiResponse(response) {
return response.items.map(item => ({
position: [item.lon, item.lat],
value: Number(item.metric),
timestamp: item.time
}));
}
Это снижает связанность между API и визуализацией.
Deck.gl часто работает с большими массивами, и повторные вычисления становятся дорогими. Используется кэширование промежуточных результатов.
const cache = new Map();
export function getProcessedData(rawData) {
const key = rawData.length;
if (cache.has(key)) {
return cache.get(key);
}
const processed = rawData.map(d => ({
position: d.position,
weight: d.value * 2
}));
cache.set(key, processed);
return processed;
}
В более сложных сценариях ключом становится hash данных или комбинация параметров фильтрации.
При росте приложения удобно разделять состояние на уровни:
Каждый уровень управляется отдельно и не смешивается с другими.
Пример структуры:
/state
globalState.js
dataState.js
layerState.js
В больших приложениях слои могут подключаться динамически в зависимости от контекста.
const layerRegistry = new Map();
export function registerLayer(name, factory) {
layerRegistry.set(name, factory);
}
export function buildLayers(context) {
return Array.from(layerRegistry.values()).map(factory =>
factory(context)
);
}
Такой подход полезен для:
Deck.gl слой должен оставаться максимально декларативным. Все побочные эффекты — логирование, аналитика, запросы — выносятся наружу.
Пример неправильного подхода:
new ScatterplotLayer({
data,
onHover: info => {
fetch('/log', { method: 'POST', body: JSON.stringify(info) });
}
});
Корректная организация:
function handleHover(info) {
logEvent(info);
updateTooltip(info);
}
new ScatterplotLayer({
data,
onHover: handleHover
});
Конфигурация слоёв может храниться отдельно от кода. Это особенно важно для больших визуализационных систем.
/config
layersConfig.js
mapConfig.js
export const roadsConfig = {
stroked: true,
lineWidthMinPixels: 1,
getLineColor: [80, 80, 80]
};
Исполнение конфигурации:
import { GeoJsonLayer } from '@deck.gl/layers';
import { roadsConfig } from '../config/layersConfig';
export function createRoadsLayer(data) {
return new GeoJsonLayer({
id: 'roads',
data,
...roadsConfig
});
}
Для уменьшения дублирования создаются функции-компоновщики:
export function withHighlight(layer, conditionFn) {
return layer.clone({
getFillColor: d =>
conditionFn(d) ? [255, 0, 0] : layer.props.getFillColor(d)
});
}
Такой паттерн позволяет расширять поведение слоёв без изменения исходного кода.
При росте приложения структура часто эволюционирует в следующую форму:
/src
/layers
/data
/state
/interactions
/services
/utils
/config
deckInstance.js
createScene.js
deckInstance.js отвечает за инициализацию Deck.gl, а
createScene.js — за композицию всех слоёв и состояния.
Такой подход минимизирует связность и делает систему предсказуемой при увеличении сложности визуализаций.