Офлайн-режим в OpenLayers строится вокруг идеи отделения источников данных от слоя отображения карты. Основная сложность заключается не в самой библиотеке, а в организации локального хранения тайлов, векторных данных и стратегии их подгрузки при отсутствии сетевого соединения.
OpenLayers изначально не предоставляет единой «кнопки офлайн-режима»,
вместо этого он предлагает набор инструментов: источники
(source), слои (layer), форматтеры
(format) и механизмы перехвата запросов. Комбинация этих
компонентов позволяет построить устойчивую систему работы без сети.
Ключевые элементы офлайн-архитектуры:
Наиболее распространённый сценарий офлайн-карт — работа с тайлами
XYZ. OpenLayers использует ol/source/XYZ, который делает
HTTP-запросы к тайловому серверу.
import Map from 'ol/Map';
import View from 'ol/View';
import TileLayer from 'ol/layer/Tile';
import XYZ from 'ol/source/XYZ';
const layer = new TileLayer({
source: new XYZ({
url: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
crossOrigin: 'anonymous'
})
});
const map = new Map({
target: 'map',
layers: [layer],
view: new View({
center: [0, 0],
zoom: 2
})
});
В онлайн-режиме всё работает напрямую, однако для офлайн-доступа требуется перехват сетевых запросов и их сохранение.
Service Worker становится центральным компонентом системы кеширования. Он перехватывает запросы тайлов и решает, отдавать ли их из сети или локального хранилища.
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js');
}
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
if (url.pathname.includes('/tile/')) {
event.respondWith(
caches.match(event.request).then((cached) => {
if (cached) {
return cached;
}
return fetch(event.request).then((response) => {
const clone = response.clone();
caches.open('tile-cache').then((cache) => {
cache.put(event.request, clone);
});
return response;
});
})
);
}
});
В данном подходе реализуется стратегия cache-first, где приоритет отдаётся локальным данным.
Офлайн-карты требуют заранее подготовленного набора данных. Подход зависит от масштаба:
function generateTileUrls(z, xStart, xEnd, yStart, yEnd) {
const urls = [];
for (let x = xStart; x <= xEnd; x++) {
for (let y = yStart; y <= yEnd; y++) {
urls.push(`https://tile.openstreetmap.org/${z}/${x}/${y}.png`);
}
}
return urls;
}
Далее эти URL можно прогреть через fetch, что позволит
service worker сохранить их в кеш.
Cache Storage API обеспечивает долговременное хранение ресурсов. В контексте OpenLayers это чаще всего тайлы, стили и GeoJSON.
async function cacheTile(url) {
const cache = await caches.open('tile-cache');
const response = await fetch(url);
await cache.put(url, response);
}
async function getCachedTile(url) {
const cache = await caches.open('tile-cache');
const match = await cache.match(url);
return match || null;
}
Cache Storage используется как второй уровень после service worker, обеспечивая устойчивость даже при повторных перезапусках приложения.
Помимо тайлов, OpenLayers активно используется для отображения
векторных данных через ol/source/Vector.
import VectorSource from 'ol/source/Vector';
import GeoJSON from 'ol/format/GeoJSON';
const source = new VectorSource({
format: new GeoJSON(),
url: '/data/regions.geojson'
});
Для офлайн-режима данные должны быть заранее сохранены.
function saveGeoJSON(key, data) {
const request = indexedDB.open('map-db', 1);
request.onupgradenee ded = () => {
const db = request.result;
db.createObjectStore('geojson');
};
request.onsucc ess = () => {
const db = request.result;
const tx = db.transaction('geojson', 'readwrite');
tx.objectStore('geojson').put(data, key);
};
}
function loadGeoJSON(key) {
return new Promise((resolve) => {
const request = indexedDB.open('map-db', 1);
request.onsucc ess = () => {
const db = request.result;
const tx = db.transaction('geojson', 'readonly');
const store = tx.objectStore('geojson');
const getRequest = store.get(key);
getRequest.onsucc ess = () => {
resolve(getRequest.result);
};
};
});
}
После загрузки данные могут быть переданы в OpenLayers:
loadGeoJSON('regions').then((data) => {
const features = new GeoJSON().readFeatures(data);
source.addFeatures(features);
});
Офлайн-режим требует корректного поведения при частичной доступности данных.
async function fetchTile(url) {
const cached = await caches.match(url);
if (cached) return cached;
try {
const response = await fetch(url);
const cache = await caches.open('tile-cache');
cache.put(url, response.clone());
return response;
} catch (e) {
return null;
}
}
Если используется ol/style/Style и динамические ресурсы
(иконки, SVG), они также должны быть закешированы.
import Icon from 'ol/style/Icon';
import Style from 'ol/style/Style';
const style = new Style({
image: new Icon({
src: '/offline/icons/marker.png'
})
});
Service Worker должен перехватывать запрос
/offline/icons/* и обеспечивать их локальную доставку.
Для сложных приложений используется стратегия сборки офлайн-пакета:
function buildOfflineManifest(region) {
const manifest = [];
for (let z = region.minZoom; z <= region.maxZoom; z++) {
const tiles = generateTileUrls(
z,
region.xMin[z],
region.xMax[z],
region.yMin[z],
region.yMax[z]
);
manifest.push(...tiles);
}
return manifest;
}
Офлайн-данные устаревают, поэтому вводится версия кеша.
const CACHE_VERSION = 'v3';
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(`tile-cache-${CACHE_VERSION}`)
);
});
Удаление старых кешей:
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(
keys.map((key) => {
if (!key.includes(CACHE_VERSION)) {
return caches.delete(key);
}
})
)
)
);
});
При работе без сети сохраняются структурные ограничения:
Эти ограничения формируют архитектурный компромисс между автономностью и актуальностью данных.