Перейти к содержанию

Синхронизация данных приложения в фоновом режиме

изображение названия и автора

Добро пожаловать на неделю 2 день 6 из серии 30 Days of PWA! В сегодняшней статье мы расскажем о фоновых сервисах современных браузеров и о том, как использовать их для различных видов синхронизации.

Фоновые сервисы

Современные браузеры и API сервис-воркеров привнесли в веб новые интересные возможности. Теперь некоторые части кода фронтенда можно запускать совершенно независимо от жизненного цикла основного приложения. Это означает, что вы больше не ограничены в проектировании и создании приложения как чего-то, что начинается только тогда, когда пользователь открывает соответствующий URL, и заканчивается сразу после закрытия вкладки. Теперь части вашего кода могут выполняться даже тогда, когда окна браузера с открытым в нем приложением нет, т.е. в фоновом режиме. Причем это может происходить как с уведомлением пользователя, так и без него. Решение принимаете вы, как разработчик.

В браузерах на базе Chromium существует множество API, формирующих функцию Background Services, которая позволяет выполнять код в фоновом режиме. Сегодня мы подробно рассмотрим два из них, позволяющих синхронизировать данные между приложением и браузером: Background Sync API и Periodic Background Sync API.

Однократная синхронизация при восстановлении соединения

Веб зависит от подключения к Интернету, что не очень хорошо для платформы приложений. Загрузив и установив приложение, вы ожидаете, что оно будет работать независимо от того, находитесь вы в сети или нет. О готовности PWA к работе в автономном режиме заботится комбинация основных событий API сервис-воркера — install, activate, fetch — и Cache Storage. Но как быть с запросами, которые приложение выполняет во время работы? Как обеспечить бесперебойную работу приложения при отсутствии подключения к Интернету?

На помощь приходит API Background Sync. Он позволяет разработчикам рассматривать взаимодействие внешнего приложения с сервером как набор синхронизаций. Эти "синхронизации" будут происходить сразу после их создания ("регистрации" в терминах Background Sync API) при наличии соединения или, если соединения нет, позже, когда пользователь снова выйдет в Интернет. Отличие от всех других подходов к pre-PWA заключается в том, что "позже" может быть после того, как пользователь закрыл вкладку приложения и/или видимую часть браузера — благодаря сервису-воркеру, который всегда "дежурит" в фоновом режиме.

Обратите внимание, что данный API предоставляет только событие sync для сервиса-воркера. Вам все равно придется сохранять данные, которые вы хотите отправить (на случай, если устройство находится в автономном режиме), и отправлять их. Если вы хотите поддерживать несколько транзакций за одну синхронизацию, то вам потребуется хранить данные и реализовать постановку их в очередь. Для хранения данных можно использовать API IndexedDB — он асинхронный, поэтому доступ к нему можно получить из сервис-воркера.

Как реализовать

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

В основном коде приложения после того, как пользователь создал новый пост и нажал кнопку "Опубликовать", вы проверяете, не завершился ли запрос к серверу. Если да, то вы создаете синхронизацию для этой попытки. Для этого необходима активная регистрация сервис-воркера.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
async function publishPostOnConnected(post) {
    const registration =
        await navigator.serviceWorker.ready;
    try {
        // First, you should write a code to save to IndexedDB the data you want to send later. It's a good idea to have this function ready for multiple posts published while offline.
        await savePost(post);

        // Then, the sync registration itself. We give it a name because we can have multiple syncs for various parts of the app functionality:
        await registration.sync.register('sync-post');

        // Finally, you can inform the user about it:
        showNotification(
            'Your post will be published automatically right after connection is restored. It is safe to close the app.',
        );
    } catch {
        console.error(
            'Background Sync registration failed',
        );
    }
}

Очень хорошей идеей будет обернуть этот код функцией обнаружения:

1
2
3
4
5
6
7
8
if (
    'serviceWorker' in navigator &&
    'SyncManager' in window
) {
    publishPostOnConnected(post);
} else {
    console.log('Background Sync is not supported');
}

В сервисе-воркере вы слушаете и реагируете на событие sync с именем sync-post:

1
2
3
4
5
6
7
8
9
self.addEventListener('sync', event => {
    if (event.tag === 'sync-post') {
      event.waitUntil(

        // You have to implement the function that iterates over the preserved posts and sends them:
        publishSavedPosts();
      );
    }
});

Здесь publishSavedPosts() должна возвращать промис, указывающий на успех/неуспех отправки данных. Если он отклонен, то при следующей синхронизации будет запланирована повторная попытка.

Повторные попытки неудачных запросов могут быть автоматизированы и реализованы декларативно с помощью модуля Workbox's Background Sync.

Периодическая синхронизация

Большинство прикладных платформ имеют API, позволяющие приложениям периодически выполнять некоторые задачи в фоновом режиме без участия пользователя. Например, приложение для социальных сетей может время от времени получать и сохранять новые сообщения с сервера — чтобы отображать актуальную ленту при каждом запуске приложения.

А как насчет веба? Можно ли поддерживать PWA в актуальном состоянии? Да, с помощью API периодической фоновой синхронизации. С помощью этого API мы можем попросить фоновую службу браузера регулярно запускать части нашего кода, независимо от того, используется ли PWA в данный момент. Как и в случае с Background Sync, этот API только предоставляет сервису-воркеру событие, а сами задачи вы должны реализовывать самостоятельно. Например, можно получить текущую версию приложения, его содержимое и все обновленные ресурсы, добавив их в кэш браузера.

Рассмотрим, как это работает для веб-клиента социальных сетей, который может периодически обновлять содержимое своей основной ленты в фоновом режиме.

Как сделать

Неплохо было бы предоставить пользователю возможность самому решать, использовать эту функцию или нет. Используя интерфейс PeriodicSyncManager зарегистрированного сервиса-воркера, можно перечислить существующие "периодические синхронизации" для установки значения (например, включения/выключения) элементов управления пользовательского интерфейса (например, переключателей, флажков), таких как "Обновлять мою ленту в фоновом режиме". Когда пользователь включает эту функцию, вы регистрируете новую периодическую синхронизацию:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
async function registerPeriodicFeedUpdate() {
  const registration = await navigator.serviceWorker.ready;

  // Query and check permission. See "Privacy and resource utilization considerations" section below.
  const status = await navigator.permissions.query({
    name: 'periodic-background-sync',
  });
  if (status.state !== 'granted') {
    console.log('Periodic Background Sync is not granted.');
    return;
  }

  try {
    await registration.periodicSync.register('update-feed-content', {
      minInterval: 24 * 60 * 60 * 1000, // We ask browser to run the sync no more than once a day
    });
    showNotification('Success! Feed will be updated in the background.');

  } catch() {
    console.error('Periodic Background Sync registration failed');
  }
}

Как и всегда в PWA, обнаружение особенностей является лучшей практикой:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
if (
    'serviceWorker' in navigator &&
    'PeriodicSyncManager' in window
) {
    registerPeriodicFeedUpdate();
} else {
    console.log(
        'Periodic Background Sync is not supported',
    );
}

В сервисе-воркере необходимо прослушать и отреагировать на событие periodicsync с именем update-feed-content:

1
2
3
4
5
6
7
8
9
self.addEventListener('periodicsync', event => {
  if (event.tag === 'update-feed-content') {
      event.waitUntil(

        // You have to implement the function that fetches the latest posts and updates the storage:
        updateFeedContent();
      );
  }
});

Следует отметить, что в функции updateFeedContent можно не получать новые данные. Перед отправкой запроса следует убедиться, что пользователь находится на безлимитном соединении (с помощью Network Information API) и что в хранилище браузера достаточно места (с помощью Storage Manager API).

Соображения конфиденциальности и использования ресурсов

API Background Sync и Periodic Background Sync предназначены для выполнения некоторого пользовательского кода (который может отправлять сетевые запросы) в фоновом режиме в любое время без уведомления пользователя. Это вызывает как минимум две проблемы: конфиденциальность и использование ресурсов. Любой сетевой запрос — это потенциальная возможность утечки истории и отслеживания местоположения. А выполнение любого кода потребляет ресурсы процессора, памяти, аккумулятора и, при необходимости, сетевые ресурсы. Чтобы избежать этих проблем, обе спецификации требуют:

  • Наличие разрешения Background Sync для источника. По умолчанию оно имеет состояние allow, но пользователи могут его запретить.

    Разрешение фоновой синхронизации

  • Ограничение количества повторных попыток фоновой синхронизации и длительности периодической синхронизации. Это не определено в спецификации API, но для периодической синхронизации браузеры используют индекс Site Engagement (количество и продолжительность взаимодействий с пользователем) для конкретного источника — и конкретного устройства — для расчета точных интервалов. В лучшем случае это несколько часов. Если вам интересно, вы можете самостоятельно проверить индекс вовлеченности сайта, зайдя на сайт about://site-engagement/ в браузере на базе Chromium.

  • Время выполнения кода ограничено временем жизни сервис-воркера, которое составляет несколько секунд. Поэтому злоупотреблять аппаратными средствами устройства за счет тяжелых вычислений или перерасхода сетевых ресурсов достаточно сложно.
  • События sync и periodicsync выполняются только в режиме онлайн.

К API периодической фоновой синхронизации предъявляется несколько дополнительных требований:

  • periodicsync будет происходить только в известной сети (той, к которой пользователь был подключен ранее).
  • Он работает только для установленных веб-приложений.

Тестирование и отладка

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

Перейдите в панель Application → Service Workers, чтобы отправить события sync и periodicsync на сервис-воркер вручную:

Отправка событий вручную

В разделе Background Services вкладки Application можно щелкнуть на отлаживаемом API, чтобы увидеть события (как "реальные", так и "ручные"), которые были отправлены сервису-воркеру. Чтобы "поймать" и отладить реальные (распределенные по часам и дням, если речь идет о периодических синхронизациях), можно воспользоваться пиктограммой "Запись" и вести журнал событий на срок до трех дней.

Протоколирование событий фоновой службы

Подробнее о фоновой синхронизации

Комментарии