Offline функциональность

Offline-функциональность является важной частью современных веб-приложений. Она позволяет пользователю работать с приложением даже при отсутствии подключения к сети и синхронизировать данные при повторном подключении. В Riot.js это достигается сочетанием возможностей фреймворка для управления состоянием компонентов и встроенных веб-технологий, таких как Service Workers и IndexedDB.


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

Service Workers обеспечивают перехват сетевых запросов и кэширование ресурсов, что является ключевым для оффлайн-доступа.

// Регистрация Service Worker
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js')
    .then(registration => {
      console.log('Service Worker зарегистрирован с областью:', registration.scope);
    })
    .catch(error => {
      console.error('Ошибка регистрации Service Worker:', error);
    });
}

sw.js — основной скрипт Service Worker:

const CACHE_NAME = 'riot-app-cache-v1';
const URLS_TO_CACHE = [
  '/',
  '/index.html',
  '/bundle.js',
  '/style.css'
];

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

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

Ключевые моменты:

  • install — этап, когда происходит кэширование критических ресурсов.
  • fetch — перехватывает запросы, возвращая кэшированные версии при оффлайн-доступе.
  • Возможность обновления кэша через activate и очистку старых версий.

Работа с IndexedDB для хранения данных

Для динамических данных используется IndexedDB, так как локальныйStorage ограничен по объему и не подходит для сложных структур.

let db;
const request = indexedDB.open('riotDB', 1);

request.onupgradenee ded = event => {
  db = event.target.result;
  db.createObjectStore('tasks', { keyPath: 'id', autoIncrement: true });
};

request.onsucc ess = event => {
  db = event.target.result;
};

function saveTask(task) {
  const transaction = db.transaction(['tasks'], 'readwrite');
  const store = transaction.objectStore('tasks');
  store.add(task);
}

function getTasks(callback) {
  const transaction = db.transaction(['tasks'], 'readonly');
  const store = transaction.objectStore('tasks');
  const request = store.getAll();
  request.onsucc ess = () => callback(request.result);
}

Особенности работы:

  • IndexedDB работает асинхронно, что предотвращает блокировку интерфейса.
  • Использование transactions обеспечивает целостность данных.
  • Поддержка сложных структур данных с индексами и поиском.

Интеграция с Riot.js

Компоненты Riot.js обладают реактивной системой, что позволяет автоматически обновлять интерфейс при изменении данных, даже при оффлайн-режиме.

<task-list>
  <ul>
    <li each={task in tasks}>{task.name}</li>
  </ul>

  this.on('mount', () => {
    getTasks(fetchedTasks => {
      this.tasks = fetchedTasks;
      this.update();
    });
  });
</task-list>

Ключевые моменты:

  • each используется для итерации по массиву данных и автоматического рендеринга списка.
  • update() — метод, вызывающий перерендеринг компонента при изменении состояния.
  • Хранение данных локально через IndexedDB позволяет компоненту отображать актуальные данные без сети.

Синхронизация с сервером

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

let offlineQueue = [];

function enqueueTask(task) {
  offlineQueue.push(task);
  saveTask(task);
}

function syncTasks() {
  if (navigator.onLine) {
    while (offlineQueue.length) {
      const task = offlineQueue.shift();
      fetch('/api/tasks', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(task)
      });
    }
  }
}

window.addEventListener('online', syncTasks);

Особенности:

  • Очередь offlineQueue хранит задачи, добавленные оффлайн.
  • При событии online автоматически инициируется синхронизация.
  • Комбинация IndexedDB и очереди обеспечивает надежное хранение данных до их успешной отправки на сервер.

Кэширование динамических данных

Можно кэшировать ответы API с помощью Service Workers для ускорения повторного доступа:

self.addEventListener('fetch', event => {
  if (event.request.url.includes('/api/')) {
    event.respondWith(
      caches.open('api-cache').then(cache => 
        fetch(event.request)
          .then(response => {
            cache.put(event.request, response.clone());
            return response;
          })
          .catch(() => cache.match(event.request))
      )
    );
  }
});

Важные моменты:

  • Используется кэш отдельного хранилища для API.
  • При отсутствии сети возвращается последняя кэшированная версия.
  • Можно внедрять стратегии кэширования: stale-while-revalidate, cache-first и другие.

Отслеживание состояния сети

Для улучшения UX важно информировать компоненты о состоянии подключения:

this.onl ine = navigator.onLine;

window.addEventListener('online', () => {
  this.onl ine = true;
  this.update();
});

window.addEventListener('offline', () => {
  this.onl ine = false;
  this.update();
});
  • Переменная online может использоваться для условного отображения кнопок синхронизации или предупреждений.
  • Обновление компонента через update() гарантирует реактивное изменение интерфейса.

Практические рекомендации

  • Разделять кэш статики и данных API для более гибкого управления.
  • Использовать небольшие транзакции IndexedDB для минимизации риска потери данных.
  • Комбинировать стратегии кэширования с системой уведомлений о синхронизации.
  • Тестировать оффлайн-функциональность через эмуляцию отсутствия сети в браузере.

Эта структура позволяет создавать полнофункциональные оффлайн-приложения на Riot.js, обеспечивая устойчивую работу интерфейса и надежное хранение данных при любых условиях сети.