Vue.js интеграция

Интеграция CesiumJS в Vue строится вокруг прямого управления DOM-элементом, в который монтируется WebGL-контекст. Основная идея заключается в том, что Vue отвечает за жизненный цикл компонента, а Cesium — за рендеринг сцены и управление 3D-графикой.

Типовая структура компонента сводится к созданию контейнера и инициализации Viewer после монтирования:

<template>
  <div ref="cesiumContainer" class="cesium-container"></div>
</template>

<script setup>
import { onMounted, onBeforeUnmount, ref } from "vue";
import * as Cesium from "cesium";

const cesiumContainer = ref(null);
let viewer = null;

onMounted(() => {
  viewer = new Cesium.Viewer(cesiumContainer.value, {
    terrainProvider: Cesium.createWorldTerrain(),
    animation: false,
    timeline: false,
  });
});

onBeforeUnmount(() => {
  if (viewer && !viewer.isDestroyed()) {
    viewer.destroy();
    viewer = null;
  }
});
</script>

<style>
.cesium-container {
  width: 100%;
  height: 100vh;
}
</style>

Ключевым моментом выступает строгая привязка инициализации к onMounted, поскольку до этого момента DOM-узел недоступен для WebGL-контекста.


Управление реактивностью Vue и объектами Cesium

Cesium использует императивную модель, тогда как Vue — реактивную. Прямое связывание объектов Cesium с реактивными состояниями приводит к избыточным пересозданиям объектов сцены и снижению производительности.

Корректная модель взаимодействия строится через явные эффекты обновления:

import { watch } from "vue";

const zoomLevel = ref(1.0);

watch(zoomLevel, (value) => {
  if (viewer) {
    viewer.camera.zoomIn(value);
  }
});

Объекты Cesium не должны попадать в reactive() или ref(), так как они содержат сложные структуры WebGL и внутренние ссылки, несовместимые с проксированием Vue.


Компонентный подход к архитектуре сцены

При построении сложных приложений Cesium логика разделяется на изолированные компоненты: карта, слои, сущности, управление камерой.

Пример выделения слоя в отдельную композиционную функцию:

// useCesiumEntities.js
import * as Cesium from "cesium";

export function useCesiumEntities(viewer) {
  const addPoint = (lon, lat) => {
    return viewer.entities.add({
      position: Cesium.Cartesian3.fromDegrees(lon, lat),
      point: {
        pixelSize: 10,
        color: Cesium.Color.YELLOW,
      },
    });
  };

  const clear = () => {
    viewer.entities.removeAll();
  };

  return {
    addPoint,
    clear,
  };
}

Использование внутри компонента:

const { addPoint } = useCesiumEntities(viewer);
addPoint(71.4304, 51.1284);

Такой подход уменьшает связность между UI-слоем Vue и графическим ядром Cesium.


Асинхронная загрузка Cesium и оптимизация бандла

Cesium обладает значительным размером, поэтому динамический импорт снижает нагрузку на старт приложения.

onMounted(async () => {
  const Cesium = await import("cesium");

  viewer = new Cesium.Viewer(cesiumContainer.value, {
    terrainProvider: Cesium.createWorldTerrain(),
  });
});

При использовании Vite или Webpack важно учитывать корректную обработку статических ресурсов Cesium (Workers, Assets, Widgets).

Для Vite часто требуется настройка:

import { defineConfig } from "vite";
import cesium from "vite-plugin-cesium";

export default defineConfig({
  plugins: [cesium()],
});

Работа с Web Workers и ресурсами Cesium

Cesium использует Web Workers для рендеринга и обработки тайлов. Некорректная настройка путей приводит к ошибкам загрузки.

Базовая конфигурация включает определение базового URL:

window.CESIUM_BASE_URL = "/cesium";

Статические ресурсы должны быть доступны в публичной директории приложения. Это включает:

  • Workers
  • Assets
  • Widgets
  • Third-party dependencies Cesium

Управление камерой через Vue Composition API

Камера Cesium является центральным объектом управления сценой. Интеграция через Vue обычно строится через отдельные composables.

export function useCamera(viewer) {
  const flyTo = (lon, lat, height = 1000) => {
    viewer.camera.flyTo({
      destination: Cesium.Cartesian3.fromDegrees(lon, lat, height),
    });
  };

  const setTopDownView = () => {
    viewer.camera.setView({
      destination: Cesium.Cartesian3.fromDegrees(0, 0, 20000000),
      orientation: {
        heading: 0,
        pitch: -Math.PI / 2,
        roll: 0,
      },
    });
  };

  return {
    flyTo,
    setTopDownView,
  };
}

Обработка событий Cesium внутри Vue

Cesium генерирует низкоуровневые события, которые необходимо аккуратно адаптировать к Vue-архитектуре.

onMounted(() => {
  const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);

  handler.setInputAction((movement) => {
    const picked = viewer.scene.pick(movement.position);
    if (picked) {
      console.log("Selected object:", picked);
    }
  }, Cesium.ScreenSpaceEventType.LEFT_CLICK);
});

При разрушении компонента обработчики должны очищаться вручную:

onBeforeUnmount(() => {
  handler?.destroy();
});

Интеграция тайловых слоёв и 3D Tiles

Cesium активно использует 3D Tiles для потоковой загрузки больших наборов данных.

const tileset = new Cesium.Cesium3DTileset({
  url: "https://assets.cesium.com/tileset.json",
});

viewer.scene.primitives.add(tileset);

tileset.readyPromise.then(() => {
  viewer.zoomTo(tileset);
});

В Vue-архитектуре такие объекты не должны попадать в реактивное состояние, так как их обновление управляется исключительно Cesium.


Resize и адаптация к контейнеру Vue

Изменение размеров компонента требует синхронизации с WebGL canvas.

import { onMounted } from "vue";

onMounted(() => {
  const resizeObserver = new ResizeObserver(() => {
    if (viewer) {
      viewer.resize();
    }
  });

  resizeObserver.observe(cesiumContainer.value);
});

Без этого механизма сцена теряет корректное соотношение сторон при изменении layout.


Использование Provide/Inject для доступа к Viewer

В сложных приложениях Cesium Viewer передаётся через dependency injection, что позволяет избегать prop drilling.

// provider
import { provide } from "vue";

provide("cesiumViewer", viewer);
// consumer
import { inject } from "vue";

const viewer = inject("cesiumViewer");

Данный механизм особенно эффективен при наличии нескольких слоёв интерфейса, взаимодействующих с одной сценой.


Управление жизненным циклом сцены в KeepAlive

При использовании <KeepAlive> Vue компоненты не уничтожаются полностью, что требует отдельного управления ресурсами Cesium.

import { onActivated, onDeactivated } from "vue";

onDeactivated(() => {
  viewer?.scene.requestRender();
});

onActivated(() => {
  viewer?.resize();
});

Cesium продолжает удерживать WebGL-контекст, поэтому контроль активности сцены становится критичным для предотвращения утечек памяти.


Интеграция с внешним состоянием приложения

Cesium часто используется как визуализация слоя поверх глобального состояния (Pinia или Vuex). При этом состояние хранит только сериализуемые данные, а Cesium служит их визуальным представлением.

Пример синхронизации:

watch(
  () => store.points,
  (points) => {
    viewer.entities.removeAll();

    points.forEach((p) => {
      viewer.entities.add({
        position: Cesium.Cartesian3.fromDegrees(p.lon, p.lat),
        point: { pixelSize: 8 },
      });
    });
  },
  { deep: true }
);

Такой подход обеспечивает разделение ответственности: состояние управляет данными, Cesium — визуализацией.


Обработка контекста WebGL и утечек памяти

Cesium активно использует GPU-ресурсы, поэтому корректное уничтожение объектов критично.

onBeforeUnmount(() => {
  if (viewer && !viewer.isDestroyed()) {
    viewer.entities.removeAll();
    viewer.destroy();
    viewer = null;
  }
});

Игнорирование очистки приводит к накоплению WebGL-контекстов, что особенно заметно при частой навигации между страницами Vue Router.


Динамическая подгрузка модулей Cesium

Разделение Cesium на модули позволяет уменьшить начальный размер бандла:

const Cesium = await import("cesium/Source/Cesium.js");
import "cesium/Source/Widgets/widgets.css";

Такой подход требует ручной настройки сборщика, но даёт контроль над тем, какие части библиотеки загружаются в конкретный момент.


Синхронизация маршрутизации Vue Router и сцены

При смене маршрута часто требуется переключение контекста сцены или её пересоздание.

import { watch } from "vue";
import { useRoute } from "vue-router";

const route = useRoute();

watch(
  () => route.name,
  () => {
    viewer?.camera.flyHome(1);
  }
);

В более сложных сценариях применяется полное уничтожение и повторная инициализация Viewer для каждого маршрута.