Progressive Web Apps

Назначение Progressive Web Apps

Progressive Web App (PWA) — это веб-приложение, которое сочетает возможности традиционного сайта и нативного мобильного приложения. Такие приложения работают через браузер, но могут устанавливаться на устройство, функционировать в офлайн-режиме, отправлять уведомления и использовать локальное хранилище.

Для картографических приложений на базе Leaflet технология PWA особенно важна, поскольку позволяет:

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

Типичные примеры:

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

Архитектура PWA-приложения с Leaflet

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

Leaflet Application
│
├── index.html
├── app.js
├── styles.css
├── manifest.json
├── service-worker.js
│
├── tiles/
├── geojson/
└── images/

Каждый компонент выполняет отдельную задачу.

Manifest

Файл описывает параметры установки приложения.

Service Worker

Фоновый скрипт, отвечающий за:

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

Leaflet

Основная библиотека отображения карты и геоданных.


Web App Manifest

Файл manifest.json содержит сведения о приложении.

Пример:

{
  "name": "Leaflet Maps",
  "short_name": "Maps",
  "start_url": ".",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#3388ff",
  "icons": [
    {
      "src": "icons/icon-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "icons/icon-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

Основные параметры:

Параметр Назначение
name Полное название
short_name Короткое название
start_url Стартовая страница
display Режим запуска
theme_color Цвет интерфейса
icons Иконки приложения

Подключение Manifest

В документе необходимо добавить ссылку:

<link rel="manifest" href="manifest.json">

Дополнительно задаются параметры оформления:

<meta name="theme-color" content="#3388ff">

Регистрация Service Worker

Регистрация выполняется после загрузки страницы.

if ("serviceWorker" in navigator) {
    navigator.serviceWorker
        .register("service-worker.js")
        .then(() => {
            console.log("Service Worker зарегистрирован");
        })
        .catch(error => {
            console.error(error);
        });
}

После успешной регистрации браузер начинает использовать Service Worker для обработки запросов.


Основы работы Service Worker

Файл service-worker.js работает отдельно от основной страницы.

Основные события:

self.addEventListener("install", event => {

});

self.addEventListener("activate", event => {

});

self.addEventListener("fetch", event => {

});

Install

Выполняется при первой установке.

Activate

Запускается после установки.

Fetch

Перехватывает сетевые запросы.


Кэширование ресурсов приложения

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

const CACHE_NAME = "leaflet-v1";

const ASSETS = [
    "/",
    "/index.html",
    "/styles.css",
    "/app.js",
    "/manifest.json"
];

Сохранение в кэш:

self.addEventListener("install", event => {
    event.waitUntil(
        caches.open(CACHE_NAME)
            .then(cache => cache.addAll(ASSETS))
    );
});

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


Обработка запросов через кэш

Простейшая стратегия:

self.addEventListener("fetch", event => {
    event.respondWith(
        caches.match(event.request)
            .then(response => {
                return response || fetch(event.request);
            })
    );
});

Алгоритм:

  1. Поиск ресурса в кэше.
  2. Если найден — возврат из кэша.
  3. Если отсутствует — запрос в сеть.

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

Создание карты не отличается от обычного приложения.

const map = L.map("map").setView(
    [51.1694, 71.4491],
    10
);

Подключение слоя:

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

Однако в PWA появляется возможность сохранять полученные тайлы локально.


Кэширование тайлов карты

Наиболее важная задача картографического PWA — сохранение тайлов.

Пример обработки:

self.addEventListener("fetch", event => {

    const url = event.request.url;

    if (url.includes("tile.openstreetmap.org")) {

        event.respondWith(

            caches.open("tiles")
                .then(cache => {

                    return cache.match(event.request)
                        .then(response => {

                            if (response) {
                                return response;
                            }

                            return fetch(event.request)
                                .then(networkResponse => {

                                    cache.put(
                                        event.request,
                                        networkResponse.clone()
                                    );

                                    return networkResponse;
                                });
                        });
                })
        );
    }
});

Теперь каждый просмотренный тайл сохраняется локально.


Стратегии кэширования

Cache First

Сначала используется кэш.

Cache → Network

Преимущества:

  • высокая скорость;
  • работа без сети.

Недостаток:

  • данные могут устаревать.

Network First

Сначала сеть.

Network → Cache

Преимущества:

  • актуальная информация.

Недостаток:

  • зависимость от интернета.

Stale While Revalidate

Комбинированный подход.

Алгоритм:

  1. Возврат данных из кэша.
  2. Параллельное обновление через сеть.
  3. Перезапись кэша.

Это один из самых популярных вариантов для карт.


Работа с GeoJSON в офлайн-режиме

Часто требуется сохранять не только тайлы, но и данные объектов.

Например:

fetch("data/cities.geojson")
    .then(response => response.json())
    .then(data => {
        L.geoJSON(data).addTo(map);
    });

Файл можно предварительно добавить в список кэшируемых ресурсов:

const ASSETS = [
    "/",
    "/data/cities.geojson"
];

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


Хранение данных в IndexedDB

Для больших объёмов геоданных предпочтительнее использовать IndexedDB.

Причины:

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

Пример создания базы:

const request = indexedDB.open(
    "LeafletDB",
    1
);

request.onupgradenee ded = event => {

    const db = event.target.result;

    db.createObjectStore(
        "features",
        {
            keyPath: "id"
        }
    );
};

Сохранение GeoJSON в IndexedDB

Добавление объекта:

const transaction =
    db.transaction(
        ["features"],
        "readwrite"
    );

const store =
    transaction.objectStore(
        "features"
    );

store.add({
    id: 1,
    geometry: feature
});

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


Отображение данных после потери сети

Проверка соединения:

if (!navigator.onLine) {
    console.log("Офлайн");
}

События изменения состояния:

window.addEventListener(
    "online",
    () => console.log("Онлайн")
);

window.addEventListener(
    "offline",
    () => console.log("Офлайн")
);

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


Предварительное сохранение области карты

Для туристических и навигационных приложений часто реализуется загрузка региона заранее.

Общая схема:

Выбор региона
      ↓
Загрузка тайлов
      ↓
Кэширование
      ↓
Офлайн-доступ

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


Ограничение размера кэша

Тайлы способны быстро занимать значительный объём памяти.

Пример очистки старых кэшей:

self.addEventListener(
    "activate",
    event => {

        event.waitUntil(

            caches.keys()
                .then(keys => {

                    return Promise.all(

                        keys.map(key => {

                            if (key !== CACHE_NAME) {
                                return caches.delete(key);
                            }

                        })
                    );
                })
        );
    }
);

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


Обновление PWA-приложения

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

Например:

const CACHE_NAME =
    "leaflet-v2";

При следующем открытии браузер создаст новый кэш и удалит старый.

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


Push-уведомления в картографических системах

PWA поддерживает push-сообщения.

В геоинформационных приложениях они используются для:

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

Обработка уведомления:

self.addEventListener(
    "push",
    event => {

        event.waitUntil(

            self.registration.showNotification(
                "Новое событие",
                {
                    body: "Получены новые данные"
                }
            )
        );
    }
);

Фоновая синхронизация

Background Sync позволяет выполнять отложенную отправку данных.

Пример сценария:

Пользователь создал объект
        ↓
Нет интернета
        ↓
Данные сохранены локально
        ↓
Интернет появился
        ↓
Автоматическая отправка

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


Оптимизация производительности Leaflet PWA

Ключевые рекомендации:

  • минимизировать количество одновременно отображаемых объектов;
  • использовать кластеризацию маркеров;
  • загружать данные частями;
  • кэшировать тайлы;
  • применять IndexedDB для больших наборов данных;
  • использовать векторные тайлы при работе с крупными картами;
  • удалять устаревшие кэши;
  • ограничивать диапазон масштабов;
  • избегать повторных запросов к одним и тем же данным.

Типовая схема работы офлайн-карты

Первый запуск
      ↓
Установка PWA
      ↓
Регистрация Service Worker
      ↓
Кэширование ресурсов
      ↓
Загрузка тайлов
      ↓
Сохранение GeoJSON
      ↓
Отключение сети
      ↓
Работа из локального кэша
      ↓
Восстановление соединения
      ↓
Синхронизация данных

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