Promise обработка

Большая часть современных приложений на базе CesiumJS работает с удалёнными источниками данных. Трёхмерные тайлы, модели, изображения, данные рельефа, GeoJSON-файлы и различные сетевые ресурсы загружаются асинхронно. По этой причине обработка объектов Promise занимает важное место в архитектуре приложений.

Promise представляет собой объект, содержащий результат асинхронной операции. В момент создания результат ещё может быть неизвестен, однако после завершения операции объект переходит в одно из состояний:

  • pending — ожидание выполнения;
  • fulfilled — успешное завершение;
  • rejected — завершение с ошибкой.

Во многих API CesiumJS методы возвращают именно Promise, поскольку загрузка данных происходит через сеть и требует времени.


Promise при загрузке 3D Tiles

Одним из наиболее распространённых примеров является загрузка набора трёхмерных тайлов.

const tileset = await Cesium.Cesium3DTileset.fromUrl(
    "https://example.com/tileset.json"
);

viewer.scene.primitives.add(tileset);

Метод Cesium3DTileset.fromUrl() возвращает Promise.

Эквивалентный вариант через then():

Cesium.Cesium3DTileset.fromUrl(
    "https://example.com/tileset.json"
)
.then(function(tileset) {

    viewer.scene.primitives.add(tileset);

})
.catch(function(error) {

    console.error(error);

});

После успешной загрузки объект tileset передаётся в обработчик then().


Цепочки Promise

Несколько асинхронных операций могут выполняться последовательно.

Cesium.Cesium3DTileset.fromUrl(
    "https://example.com/tileset.json"
)
.then(function(tileset) {

    viewer.scene.primitives.add(tileset);

    return viewer.zoomTo(tileset);

})
.then(function() {

    console.log("Камера перемещена");

})
.catch(function(error) {

    console.error(error);

});

В данном примере:

  1. Загружается набор тайлов.
  2. Добавляется в сцену.
  3. Выполняется масштабирование камеры.
  4. После завершения выполняется следующий обработчик.

Каждый вызов then() возвращает новый Promise, что позволяет формировать длинные последовательности действий.


Обработка ошибок через catch

Сетевые операции всегда потенциально подвержены ошибкам:

  • отсутствие доступа к серверу;
  • неверный URL;
  • ошибки авторизации;
  • повреждённые данные;
  • тайм-аут соединения.

Для обработки подобных ситуаций используется метод catch().

Cesium.GeoJsonDataSource.load(
    "data/countries.geojson"
)
.then(function(dataSource) {

    viewer.dataSources.add(dataSource);

})
.catch(function(error) {

    console.error("Ошибка загрузки:", error);

});

Если ошибка возникнет на любом этапе цепочки Promise, управление будет передано в блок catch().


Использование async/await

Современный JavaScript предоставляет синтаксис async/await, который значительно упрощает работу с Promise.

Без async/await:

Cesium.GeoJsonDataSource.load(url)
    .then(function(dataSource) {

        viewer.dataSources.add(dataSource);

        return viewer.zoomTo(dataSource);

    })
    .catch(function(error) {

        console.error(error);

    });

С использованием async/await:

async function loadGeoJson() {

    try {

        const dataSource =
            await Cesium.GeoJsonDataSource.load(url);

        viewer.dataSources.add(dataSource);

        await viewer.zoomTo(dataSource);

    }
    catch(error) {

        console.error(error);

    }

}

Такой код легче читать, особенно когда количество асинхронных операций увеличивается.


Promise при загрузке GeoJSON

GeoJSON является одним из наиболее часто используемых форматов пространственных данных.

async function addCountries() {

    try {

        const source =
            await Cesium.GeoJsonDataSource.load(
                "countries.geojson"
            );

        viewer.dataSources.add(source);

    }
    catch(error) {

        console.error(error);

    }

}

Пока файл не будет полностью загружен и обработан, выполнение остановится на операторе await.


Promise при загрузке KML

Загрузка KML выполняется аналогичным образом.

async function loadKml() {

    try {

        const dataSource =
            await Cesium.KmlDataSource.load(
                "map.kml"
            );

        viewer.dataSources.add(dataSource);

    }
    catch(error) {

        console.error(error);

    }

}

Метод возвращает Promise, содержащий объект KmlDataSource.


Promise при загрузке CZML

CZML используется для описания динамических объектов и временных интервалов.

async function loadCzml() {

    try {

        const source =
            await Cesium.CzmlDataSource.load(
                "aircraft.czml"
            );

        viewer.dataSources.add(source);

    }
    catch(error) {

        console.error(error);

    }

}

После завершения Promise появляется доступ к объекту источника данных.


Параллельная загрузка через Promise.all

Во многих проектах необходимо загружать сразу несколько ресурсов.

Например:

  • карту дорог;
  • административные границы;
  • населённые пункты;
  • маршруты движения.

Для подобных задач используется Promise.all().

const promises = [

    Cesium.GeoJsonDataSource.load("roads.geojson"),

    Cesium.GeoJsonDataSource.load("cities.geojson"),

    Cesium.GeoJsonDataSource.load("regions.geojson")

];

Promise.all(promises)
.then(function(results) {

    results.forEach(function(dataSource) {

        viewer.dataSources.add(dataSource);

    });

})
.catch(function(error) {

    console.error(error);

});

Все запросы запускаются одновременно.

Promise завершится успешно только тогда, когда будут успешно загружены все ресурсы.


Promise.all и async/await

Более современная реализация выглядит следующим образом:

async function loadLayers() {

    try {

        const [
            roads,
            cities,
            regions
        ] = await Promise.all([

            Cesium.GeoJsonDataSource.load(
                "roads.geojson"
            ),

            Cesium.GeoJsonDataSource.load(
                "cities.geojson"
            ),

            Cesium.GeoJsonDataSource.load(
                "regions.geojson"
            )

        ]);

        viewer.dataSources.add(roads);
        viewer.dataSources.add(cities);
        viewer.dataSources.add(regions);

    }
    catch(error) {

        console.error(error);

    }

}

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


Promise.allSettled

Иногда необходимо получить результаты всех операций независимо от ошибок отдельных запросов.

Для этого используется Promise.allSettled().

const results = await Promise.allSettled([

    Cesium.GeoJsonDataSource.load("roads.geojson"),

    Cesium.GeoJsonDataSource.load("cities.geojson"),

    Cesium.GeoJsonDataSource.load("regions.geojson")

]);

Каждый элемент массива будет содержать:

{
    status: "fulfilled",
    value: ...
}

или

{
    status: "rejected",
    reason: ...
}

Это особенно полезно для приложений мониторинга и геопорталов, где отказ одного слоя не должен блокировать отображение остальных данных.


Promise.race

Метод Promise.race() возвращает результат первого завершившегося Promise.

const result = await Promise.race([

    requestA(),
    requestB()

]);

На практике подобный механизм может использоваться для:

  • резервных серверов;
  • зеркал данных;
  • проверки доступности источников.

Promise.any

Метод Promise.any() завершится успешно, если хотя бы один Promise выполнится без ошибки.

const source = await Promise.any([

    loadFromServer1(),
    loadFromServer2(),
    loadFromServer3()

]);

Подход удобен при наличии нескольких независимых поставщиков пространственных данных.


Загрузка пользовательских ресурсов

Часто требуется предварительно получить данные через Fetch API, а затем передать их в CesiumJS.

async function loadData() {

    try {

        const response =
            await fetch("/api/objects");

        const json =
            await response.json();

        console.log(json);

    }
    catch(error) {

        console.error(error);

    }

}

Каждый вызов await фактически ожидает завершения соответствующего Promise.


Проверка HTTP-статусов

Fetch не генерирует исключение при ошибках HTTP.

Поэтому рекомендуется выполнять дополнительную проверку.

async function loadData() {

    const response =
        await fetch("/api/data");

    if (!response.ok) {

        throw new Error(
            "HTTP Error " + response.status
        );

    }

    return response.json();

}

После генерации исключения управление перейдёт в блок catch.


Создание собственных Promise

Иногда необходимо обернуть старый асинхронный код в Promise.

function wait(ms) {

    return new Promise(function(resolve) {

        setTimeout(resolve, ms);

    });

}

Использование:

await wait(3000);

console.log("Прошло 3 секунды");

Подобный механизм может применяться при управлении анимациями и сценариями отображения данных.


Последовательная загрузка данных

Иногда следующий ресурс зависит от предыдущего.

async function loadScenario() {

    const buildings =
        await Cesium.GeoJsonDataSource.load(
            "buildings.geojson"
        );

    viewer.dataSources.add(buildings);

    const routes =
        await Cesium.GeoJsonDataSource.load(
            "routes.geojson"
        );

    viewer.dataSources.add(routes);

}

В этом случае загрузка выполняется строго по порядку.


Параллельная и последовательная модели

Последовательная схема:

await loadA();
await loadB();
await loadC();

Параллельная схема:

await Promise.all([
    loadA(),
    loadB(),
    loadC()
]);

Параллельный вариант обычно работает значительно быстрее, поскольку запросы отправляются одновременно.


Асинхронная инициализация Viewer

Крупные приложения нередко выполняют полную подготовку сцены до начала работы.

async function initialize() {

    const terrain =
        await Cesium.CesiumTerrainProvider.fromIonAssetId(
            1
        );

    const viewer = new Cesium.Viewer(
        "cesiumContainer",
        {
            terrainProvider: terrain
        }
    );

    const buildings =
        await Cesium.Cesium3DTileset.fromUrl(
            "tileset.json"
        );

    viewer.scene.primitives.add(buildings);

    await viewer.zoomTo(buildings);

    return viewer;

}

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


Типичные ошибки при работе с Promise

Отсутствие await

const dataSource =
    Cesium.GeoJsonDataSource.load(url);

viewer.dataSources.add(dataSource);

Ошибка заключается в том, что dataSource содержит Promise, а не объект GeoJsonDataSource.

Правильный вариант:

const dataSource =
    await Cesium.GeoJsonDataSource.load(url);

viewer.dataSources.add(dataSource);

Игнорирование catch

Cesium.GeoJsonDataSource.load(url);

При возникновении ошибки информация о ней может быть потеряна.

Корректный вариант:

Cesium.GeoJsonDataSource.load(url)
    .catch(console.error);

Смешивание стилей

Нежелательно одновременно использовать длинные цепочки then() и await.

Плохо:

await loadData()
    .then(processData)
    .then(renderData);

Лучше придерживаться одного подхода:

const data =
    await loadData();

const processed =
    processData(data);

renderData(processed);

Рекомендации по организации асинхронного кода

Использовать async/await как основной стиль разработки.

Оборачивать асинхронные операции в try/catch.

Выполнять независимые запросы через Promise.all().

Проверять сетевые ошибки и HTTP-статусы.

Не блокировать загрузкой одного ресурса загрузку остальных без необходимости.

Разделять этапы получения данных, обработки данных и отображения данных.

Логировать ошибки загрузки тайлов, моделей и пространственных слоёв.

Грамотное использование Promise позволяет строить масштабируемые приложения CesiumJS, эффективно работающие с большими объёмами геопространственных данных, сетевыми сервисами и сложными сценариями визуализации в реальном времени.