WebSocket для real-time обновлений

Интерактивные карты часто работают с данными, которые изменяются непрерывно: координаты транспорта, положение курьеров, телеметрия датчиков, состояние объектов инфраструктуры, перемещение пользователей, события мониторинга и многое другое. При использовании обычных HTTP-запросов обновление информации выполняется через периодический опрос сервера (polling), что приводит к лишней нагрузке на сеть и увеличивает задержки.

Технология WebSocket обеспечивает постоянное двустороннее соединение между браузером и сервером. После установки соединения сервер способен самостоятельно отправлять новые данные клиенту без дополнительных запросов.

В связке с Leaflet WebSocket позволяет создавать карты с обновлением объектов в режиме реального времени:

  • отслеживание транспорта;
  • диспетчерские системы;
  • GPS-мониторинг;
  • карты событий;
  • логистические сервисы;
  • системы безопасности;
  • IoT-панели мониторинга;
  • онлайн-игры с географической привязкой.

Принцип работы WebSocket

Классическая схема HTTP выглядит следующим образом:

Клиент → Запрос
Сервер → Ответ

Клиент → Запрос
Сервер → Ответ

Каждое обновление требует нового обращения к серверу.

С WebSocket схема меняется:

Клиент ↔ Сервер
Постоянное соединение

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

Типичный поток данных:

GPS-трекер
      ↓
Сервер
      ↓
WebSocket
      ↓
Leaflet-карта

Создание WebSocket-соединения

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

const socket = new WebSocket(
    "ws://localhost:8080"
);

Для защищённых соединений используется протокол:

const socket = new WebSocket(
    "wss://example.com/socket"
);

Где:

  • ws:// — обычное соединение;
  • wss:// — защищённое соединение через TLS.

Базовая карта Leaflet

Создадим карту, которая будет получать координаты через WebSocket.

<div id="map"></div>
#map {
    width: 100%;
    height: 600px;
}
const map = L.map("map").setView(
    [55.751244, 37.618423],
    12
);

L.tileLayer(
    "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
    {
        maxZoom: 19
    }
).addTo(map);

После этого карта готова к отображению объектов.


Обработка открытия соединения

После успешного подключения возникает событие open.

socket.addEventListener("open", () => {
    console.log("Соединение установлено");
});

Альтернативный синтаксис:

socket.ono pen = () => {
    console.log("Подключено");
};

В этот момент обычно выполняется:

  • авторизация;
  • запрос начальных данных;
  • регистрация подписок;
  • передача идентификатора пользователя.

Пример:

socket.ono pen = () => {

    socket.send(
        JSON.stringify({
            type: "subscribe",
            channel: "vehicles"
        })
    );

};

Получение сообщений

Основная логика реального времени строится вокруг события message.

Предположим, сервер отправляет данные в формате JSON:

{
  "id": 12,
  "lat": 55.752,
  "lng": 37.621
}

Получение сообщения:

socket.onmess age = (event) => {

    const data = JSON.parse(
        event.data
    );

    console.log(data);

};

Теперь можно использовать координаты для обновления объектов на карте.


Создание маркера при первом сообщении

Для одного объекта достаточно создать маркер после получения координат.

let marker = null;

socket.onmess age = (event) => {

    const data = JSON.parse(
        event.data
    );

    if (!marker) {

        marker = L.marker([
            data.lat,
            data.lng
        ]).addTo(map);

    }

};

Такой подход подходит для одного отслеживаемого объекта.


Обновление позиции маркера

На практике сервер регулярно присылает новые координаты.

let marker = null;

socket.onmess age = (event) => {

    const data = JSON.parse(
        event.data
    );

    if (!marker) {

        marker = L.marker([
            data.lat,
            data.lng
        ]).addTo(map);

    } else {

        marker.setLatLng([
            data.lat,
            data.lng
        ]);

    }

};

Метод setLatLng() изменяет положение существующего маркера без его пересоздания.

Это значительно эффективнее, чем удалять и создавать объект заново.


Отображение нескольких объектов

Часто сервер передаёт данные о множестве объектов.

Пример сообщения:

{
  "id": 101,
  "lat": 55.75,
  "lng": 37.61
}
{
  "id": 102,
  "lat": 55.76,
  "lng": 37.63
}

Удобно хранить маркеры в коллекции.

const markers = {};

Обработка сообщений:

socket.onmess age = (event) => {

    const data = JSON.parse(
        event.data
    );

    const id = data.id;

    if (!markers[id]) {

        markers[id] = L.marker([
            data.lat,
            data.lng
        ]).addTo(map);

    } else {

        markers[id].setLatLng([
            data.lat,
            data.lng
        ]);

    }

};

Теперь каждый объект обновляется независимо.


Добавление всплывающих окон

Дополнительная информация может приходить вместе с координатами.

{
  "id": 7,
  "name": "Автобус 24",
  "lat": 55.75,
  "lng": 37.61
}

Создание маркера:

markers[id] = L.marker([
    data.lat,
    data.lng
])
.bindPopup(data.name)
.addTo(map);

При клике пользователь увидит сведения об объекте.


Обновление содержимого Popup

Если данные изменяются динамически, можно обновлять содержимое окна.

markers[id].setPopupContent(`
    ${data.name}<br>
    Скорость: ${data.speed}
`);

Так отображаются:

  • скорость транспорта;
  • уровень заряда;
  • температура датчиков;
  • статус оборудования.

Отображение маршрута движения

Для визуализации трека используется Polyline.

const route = [];

Получение координат:

socket.onmess age = (event) => {

    const data = JSON.parse(
        event.data
    );

    route.push([
        data.lat,
        data.lng
    ]);

};

Создание линии:

const polyline = L.polyline(
    [],
    {
        color: "red"
    }
).addTo(map);

Обновление маршрута:

socket.onmess age = (event) => {

    const data = JSON.parse(
        event.data
    );

    route.push([
        data.lat,
        data.lng
    ]);

    polyline.setLatLngs(
        route
    );

};

Маршрут будет постепенно удлиняться по мере поступления данных.


Автоматическое центрирование карты

Иногда необходимо следить за перемещающимся объектом.

marker.setLatLng([
    data.lat,
    data.lng
]);

map.panTo([
    data.lat,
    data.lng
]);

Метод panTo() перемещает карту плавно.

Для мгновенного перемещения используется:

map.setView(
    [data.lat, data.lng],
    map.getZoom()
);

Анимация движения объекта

Если координаты приходят редко, перемещение маркера выглядит скачкообразным.

Для сглаживания используются специализированные плагины:

  • Leaflet.Marker.SlideTo
  • MovingMarker

Пример:

marker.slideTo(
    [data.lat, data.lng],
    {
        duration: 1000
    }
);

Маркер будет плавно двигаться между точками.


Удаление объектов

Иногда сервер сообщает об исчезновении объекта.

Сообщение:

{
  "type": "remove",
  "id": 25
}

Обработка:

if (data.type === "remove") {

    map.removeLayer(
        markers[data.id]
    );

    delete markers[data.id];

}

Это позволяет поддерживать актуальное состояние карты.


Использование разных типов сообщений

Хорошей практикой является введение поля type.

Пример сообщений:

{
  "type": "vehicle"
}
{
  "type": "alarm"
}
{
  "type": "sensor"
}

Обработка:

switch(data.type) {

    case "vehicle":
        updateVehicle(data);
        break;

    case "sensor":
        updateSensor(data);
        break;

    case "alarm":
        showAlarm(data);
        break;

}

Подобная архитектура хорошо масштабируется.


Работа с GeoJSON через WebSocket

Сервер может отправлять готовые GeoJSON-объекты.

Пример:

{
  "type": "Feature",
  "geometry": {
    "type": "Point",
    "coordinates": [37.61, 55.75]
  }
}

Отображение:

socket.onmess age = (event) => {

    const geojson = JSON.parse(
        event.data
    );

    L.geoJSON(
        geojson
    ).addTo(map);

};

Такой формат особенно удобен при интеграции ГИС-систем.


Работа со слоями событий

Предположим, поступают сигналы тревоги.

Создаётся отдельный слой:

const alarmsLayer =
    L.layerGroup().addTo(map);

Добавление событий:

L.marker(
    [data.lat, data.lng]
).addTo(
    alarmsLayer
);

После этого слой можно включать и отключать независимо от остальных данных.


Обработка ошибок соединения

Для контроля состояния используется событие error.

socket.oner ror = (error) => {

    console.error(
        "Ошибка WebSocket",
        error
    );

};

Ошибки могут возникать из-за:

  • недоступности сервера;
  • проблем сети;
  • неверного адреса;
  • ошибок SSL-сертификата.

Обработка закрытия соединения

Событие закрытия:

socket.oncl ose = () => {

    console.log(
        "Соединение закрыто"
    );

};

Полезно отображать пользователю информацию о потере связи.

Например:

statusElement.textContent =
    "Нет соединения";

Автоматическое переподключение

В реальных приложениях соединение может периодически обрываться.

Базовая схема переподключения:

function connect() {

    const socket =
        new WebSocket(
            "ws://localhost:8080"
        );

    socket.oncl ose = () => {

        setTimeout(
            connect,
            5000
        );

    };

}

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


Ограничение количества обновлений

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

Пример:

100 сообщений/сек

Если каждое сообщение вызывает обновление карты, производительность начинает снижаться.

Применяется буферизация.

let latestData = null;

Получение:

socket.onmess age = (event) => {

    latestData =
        JSON.parse(event.data);

};

Обновление карты:

setInterval(() => {

    if (!latestData) {
        return;
    }

    marker.setLatLng([
        latestData.lat,
        latestData.lng
    ]);

}, 100);

В этом случае карта обновляется не чаще десяти раз в секунду.


Кластеризация динамических маркеров

При большом количестве объектов используется плагин MarkerCluster.

Создание группы:

const clusterGroup =
    L.markerClusterGroup();

map.addLayer(
    clusterGroup
);

Добавление маркера:

clusterGroup.addLayer(
    marker
);

Это особенно важно при работе с тысячами объектов.


Архитектура реального времени

Типичная структура проекта выглядит следующим образом:

WebSocket
    ↓
Message Handler
    ↓
Data Store
    ↓
Leaflet Layers
    ↓
Map Rendering

Разделение ответственности позволяет:

  • уменьшить связанность кода;
  • упростить тестирование;
  • облегчить поддержку проекта;
  • масштабировать систему без переписывания карты.

Практический пример GPS-мониторинга

Инициализация:

const markers = {};

const socket = new WebSocket(
    "wss://example.com/tracking"
);

Получение координат:

socket.onmess age = (event) => {

    const vehicle =
        JSON.parse(event.data);

    const id = vehicle.id;

    if (!markers[id]) {

        markers[id] = L.marker([
            vehicle.lat,
            vehicle.lng
        ]).addTo(map);

    }

    markers[id].setLatLng([
        vehicle.lat,
        vehicle.lng
    ]);

};

Сервер отправляет сообщения:

{
  "id": 1,
  "lat": 55.751,
  "lng": 37.618
}
{
  "id": 2,
  "lat": 55.763,
  "lng": 37.645
}

Каждое новое сообщение мгновенно отражается на карте.


Рекомендации по производительности

Не создавать маркеры повторно.

Использовать:

marker.setLatLng(...)

вместо:

removeLayer(...)
addLayer(...)

Группировать объекты по слоям.

L.layerGroup()

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

Минимизировать количество перерисовок.

Обновление карты желательно выполнять пакетами.

Использовать GeoJSON для сложных структур.

Формат хорошо интегрирован с Leaflet и большинством ГИС-платформ.

Ограничивать частоту обновлений интерфейса.

Даже если сервер отправляет данные очень часто, визуальная часть редко нуждается в обновлении чаще 10–30 кадров в секунду.

Применять кластеризацию для больших наборов маркеров.

Это существенно снижает нагрузку на браузер и ускоряет работу карты при масштабировании.