Интеграция CesiumJS в Angular-проект требует согласования двух архитектурных моделей: императивного WebGL-движка Cesium и реактивной компонентной системы Angular. Основная сложность заключается в управлении жизненным циклом сцены, синхронизации обновлений и корректной работе с DOM через Angular Renderer и ViewChild.
Установка базовых зависимостей:
npm install cesium
npm install @types/cesium --save-dev
Дополнительно требуется настройка статических ресурсов Cesium
(workers, assets, widgets). В Angular CLI это решается через
конфигурацию angular.json:
"assets": [
"src/favicon.ico",
"src/assets",
{
"glob": "**/*",
"input": "node_modules/cesium/Build/Cesium",
"output": "assets/cesium"
}
]
В main.ts или в отдельном конфигурационном файле
задаются базовые пути:
import * as Cesium from "cesium";
(Cesium as any).buildModuleUrl.setBaseUrl("/assets/cesium/");
Основная точка интеграции — компонент, содержащий контейнер для WebGL-контекста.
<div #cesiumContainer class="cesium-container"></div>
import { Component, ElementRef, AfterViewInit, ViewChild, OnDestroy } from '@angular/core';
import * as Cesium from 'cesium';
@Component({
selector: 'app-globe',
templateUrl: './globe.component.html',
styleUrls: ['./globe.component.scss']
})
export class GlobeComponent implements AfterViewInit, OnDestroy {
@ViewChild('cesiumContainer', { static: true })
container!: ElementRef<HTMLDivElement>;
viewer!: Cesium.Viewer;
ngAfterViewInit(): void {
this.viewer = new Cesium.Viewer(this.container.nativeElement, {
animation: false,
timeline: false,
geocoder: false,
baseLayerPicker: true,
terrainProvider: Cesium.createWorldTerrain()
});
}
ngOnDestroy(): void {
if (this.viewer && !this.viewer.isDestroyed()) {
this.viewer.destroy();
}
}
}
Ключевым моментом является инициализация только после
AfterViewInit, когда DOM-контейнер гарантированно
существует.
Cesium создаёт WebGL-контекст, текстуры, буферы и воркеры. В Angular важно корректно освобождать ресурсы:
viewer.destroy() освобождает WebGL контекстОсобое внимание требуется при использовании маршрутизации Angular. При переходе между страницами компонент уничтожается, но глобальные ссылки на viewer могут сохраняться в сервисах.
Для масштабируемых приложений Viewer часто выносится в singleton-сервис.
import { Injectable } from '@angular/core';
import * as Cesium from 'cesium';
@Injectable({ providedIn: 'root' })
export class CesiumService {
private viewer?: Cesium.Viewer;
init(container: HTMLElement): Cesium.Viewer {
if (this.viewer) {
return this.viewer;
}
this.viewer = new Cesium.Viewer(container, {
terrainProvider: Cesium.createWorldTerrain()
});
return this.viewer;
}
getViewer(): Cesium.Viewer | undefined {
return this.viewer;
}
destroy(): void {
if (this.viewer && !this.viewer.isDestroyed()) {
this.viewer.destroy();
this.viewer = undefined;
}
}
}
Такой подход позволяет разделять логику отображения и управление сценой.
Cesium работает вне зоны Angular, поэтому изменения сцены не вызывают
автоматического обновления шаблонов. Для синхронизации используется
NgZone.
constructor(private zone: NgZone) {}
ngAfterViewInit(): void {
this.zone.runOutsideAngular(() => {
this.viewer = new Cesium.Viewer(this.container.nativeElement);
this.viewer.camera.moveEnd.addEventListener(() => {
const position = this.viewer.camera.positionWC;
this.zone.run(() => {
// обновление Angular состояния
this.cameraPosition = position;
});
});
});
}
Вынос Cesium-операций за пределы Angular зоны существенно снижает количество change detection циклов.
Добавление объектов в сцену часто привязывается к потокам RxJS:
import { BehaviorSubject } from 'rxjs';
private entities$ = new BehaviorSubject<Cesium.Entity[]>([]);
addPoint(lon: number, lat: number): void {
const entity = new Cesium.Entity({
position: Cesium.Cartesian3.fromDegrees(lon, lat),
point: {
pixelSize: 10,
color: Cesium.Color.RED
}
});
const current = this.entities$.value;
this.entities$.next([...current, entity]);
this.viewer.entities.add(entity);
}
Такой подход позволяет синхронизировать состояние сцены с состоянием приложения.
Cesium чувствителен к количеству объектов и частоте обновлений. В Angular-приложениях типичные проблемы возникают при:
Оптимизация включает:
runOutsideAngularPrimitive вместо Entity для
больших наборов данныхПри работе с маршрутизацией важно предотвращать повторную инициализацию WebGL:
{
path: 'globe',
component: GlobeComponent,
runGuardsAndResolvers: 'always'
}
Если требуется сохранение состояния сцены между переходами, Viewer выносится в сервис, а компонент только привязывает контейнер.
Cesium требует ручного уведомления о resize контейнера:
ngAfterViewInit(): void {
this.viewer = new Cesium.Viewer(this.container.nativeElement);
const resizeObserver = new ResizeObserver(() => {
this.viewer.resize();
});
resizeObserver.observe(this.container.nativeElement);
}
Без этого Canvas может некорректно масштабироваться при изменении layout Angular.
Cesium поддерживает различные источники карт:
this.viewer.imageryLayers.addImageryProvider(
new Cesium.OpenStreetMapImageryProvider({
url: 'https://a.tile.openstreetmap.org/'
})
);
Добавление слоёв должно выполняться после полной инициализации Viewer, иначе возможно неконсистентное состояние слоя.
Cesium имеет значительный вес, поэтому часто применяется динамический импорт:
async ngAfterViewInit(): Promise<void> {
const Cesium = await import('cesium');
this.viewer = new Cesium.Viewer(this.container.nativeElement);
}
В связке с Angular lazy modules это снижает начальный размер бандла и ускоряет первичную загрузку приложения.
Cesium зависит от WebGL и window, поэтому при серверном
рендеринге требуется защита:
if (typeof window !== 'undefined') {
this.viewer = new Cesium.Viewer(container);
}
На сервере Viewer не создаётся, а клиентская гидрация Angular должна учитывать отсутствие сцены.
Абстракция управления камерой часто выделяется в отдельный сервис:
setCameraHome(): void {
this.viewer.camera.flyHome(1.5);
}
flyToCoordinates(lon: number, lat: number): void {
this.viewer.camera.flyTo({
destination: Cesium.Cartesian3.fromDegrees(lon, lat, 15000)
});
}
Это разделяет UI-логику Angular и низкоуровневые операции Cesium.
Cesium предоставляет богатую систему событий, которая интегрируется через Angular сервисы:
this.viewer.screenSpaceEventHandler.setInputAction((movement: any) => {
const picked = this.viewer.scene.pick(movement.position);
if (picked) {
// обработка выбора объекта
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
Для сложных приложений события централизуются в отдельном обработчике, чтобы избежать дублирования логики между компонентами.
Основная архитектурная проблема заключается в расхождении между:
Решение заключается в введении промежуточного слоя:
Двунаправленная синхронизация требует строгого контроля, чтобы избежать циклических обновлений.
При отображении миллионов точек стандартные Entity становятся неэффективными. Используются:
PointPrimitiveCollectionCustomDataSource3D TilesИнтеграция в Angular требует минимизации перерисовок:
const collection = this.viewer.scene.primitives.add(
new Cesium.PointPrimitiveCollection()
);
collection.add({
position: Cesium.Cartesian3.fromDegrees(30, 50),
color: Cesium.Color.YELLOW,
pixelSize: 5
});
Контейнер Cesium должен занимать весь доступный viewport:
.cesium-container {
width: 100%;
height: 100vh;
position: relative;
overflow: hidden;
}
Angular layout напрямую влияет на размер WebGL canvas, поэтому любые flex/grid изменения должны учитывать пересчёт размеров сцены.
Cesium имеет частичную типизацию, поэтому часто требуется расширение типов:
declare module 'cesium' {
interface Viewer {
customState?: any;
}
}
Это позволяет интегрировать Cesium в строгую архитектуру Angular без потери типобезопасности.