Интерактивные карты часто работают с данными, которые изменяются непрерывно: координаты транспорта, положение курьеров, телеметрия датчиков, состояние объектов инфраструктуры, перемещение пользователей, события мониторинга и многое другое. При использовании обычных HTTP-запросов обновление информации выполняется через периодический опрос сервера (polling), что приводит к лишней нагрузке на сеть и увеличивает задержки.
Технология WebSocket обеспечивает постоянное двустороннее соединение между браузером и сервером. После установки соединения сервер способен самостоятельно отправлять новые данные клиенту без дополнительных запросов.
В связке с Leaflet WebSocket позволяет создавать карты с обновлением объектов в режиме реального времени:
Классическая схема HTTP выглядит следующим образом:
Клиент → Запрос
Сервер → Ответ
Клиент → Запрос
Сервер → Ответ
Каждое обновление требует нового обращения к серверу.
С WebSocket схема меняется:
Клиент ↔ Сервер
Постоянное соединение
После подключения обе стороны могут отправлять сообщения в любой момент времени.
Типичный поток данных:
GPS-трекер
↓
Сервер
↓
WebSocket
↓
Leaflet-карта
Подключение выполняется стандартным API браузера.
const socket = new WebSocket(
"ws://localhost:8080"
);
Для защищённых соединений используется протокол:
const socket = new WebSocket(
"wss://example.com/socket"
);
Где:
ws:// — обычное соединение;wss:// — защищённое соединение через TLS.Создадим карту, которая будет получать координаты через 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);
При клике пользователь увидит сведения об объекте.
Если данные изменяются динамически, можно обновлять содержимое окна.
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()
);
Если координаты приходят редко, перемещение маркера выглядит скачкообразным.
Для сглаживания используются специализированные плагины:
Пример:
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-объекты.
Пример:
{
"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
);
};
Ошибки могут возникать из-за:
Событие закрытия:
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
Разделение ответственности позволяет:
Инициализация:
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 кадров в секунду.
Применять кластеризацию для больших наборов маркеров.
Это существенно снижает нагрузку на браузер и ускоряет работу карты при масштабировании.