Cómo sincronizar periódicamente datos en segundo plano

Cecilia Cong
Cecilia Cong

A la manera moderna

La sincronización periódica en segundo plano te permite mostrar contenido actualizado cuando se inicia una app web progresiva o una página respaldada por un service worker. Para ello, descarga los datos en segundo plano cuando la app o la página no se usa.

Cómo usar la API de Periodic Background Sync

Después de instalar el service worker, usa la API de Permissions para consultar periodic-background-sync. Puedes hacerlo desde un contexto de ventana o de service worker.

const status = await navigator.permissions.query({
  name: 'periodic-background-sync',
});
if (status.state === 'granted') {
  // Periodic background sync can be used.
} else {
  // Periodic background sync cannot be used.
}

Para registrar una sincronización periódica, se requiere una etiqueta y un intervalo de sincronización mínimo (minInterval). La etiqueta identifica la sincronización registrada y se pueden registrar varias sincronizaciones. En el siguiente ejemplo, el nombre de la etiqueta es 'content-sync' y el minInterval es un día.

navigator.serviceWorker.ready.then(async registration => {
  try {
    await registration.periodicSync.register('get-cats', { minInterval: 24 * 60 * 60 * 1000 });
    console.log(Periodic background sync registered.');
  } catch (err) {
    console.error(err.name, err.message);
  }
});

Llama a periodicSync.getTags() para recuperar un array de etiquetas de registro. En el siguiente ejemplo, se usan nombres de etiquetas para confirmar que la actualización de la caché esté activa y evitar que se vuelva a actualizar.

const registration = await navigator.serviceWorker.ready;
if ('periodicSync' in registration) {
  const tags = await registration.periodicSync.getTags();
  // Only update content if sync isn't set up.
  if (!tags.includes('content-sync')) {
    updateContentOnPageLoad();
  }
} else {
  // If periodic background sync isn't supported, always update.
  updateContentOnPageLoad();
}

Para responder a un evento periódico de sincronización en segundo plano, agrega un controlador de eventos periodicsync a tu service worker. El objeto de evento que se le pase contendrá un parámetro de etiqueta que coincide con el valor utilizado durante el registro. Por ejemplo, si se registró una sincronización periódica en segundo plano con el nombre 'content-sync', event.tag será 'content-sync'.

self.addEventListener('periodicsync', (event) => {
  if (event.tag === 'content-sync') {
    event.waitUntil(syncContent());
  }
});

Compatibilidad del navegador

Navegadores compatibles

  • 80
  • 80
  • x
  • x

Origen

El método clásico

En lugar de actualizar los datos en segundo plano para que estén listos cuando el usuario cargue la app, la forma clásica simplemente consiste en actualizar los datos en la carga.

Lecturas adicionales

Demostración

HTML

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link
      rel="icon"
      href=""
    />
    <link rel="manifest" href="./manifest.json" />
    <title>How to periodically synchronize data in the background</title>
    <link rel="stylesheet" href="/style.css" />
    <!-- TODO: Devsite - Removed inline handlers -->
    <!-- <script src="/script.js" defer></script> -->
  </head>
  <body>
    <h1>How to periodically synchronize data in the background</h1>
    <p class="available">Periodic background sync can be used. Install the app first.</p>
    <p class="not-available">Periodic background sync cannot be used.</p>
    <h2>Last updated</h2>
    <p class="last-updated">Never</p>
    <h2>Registered tags</h2>
    <ul>
      <li>None yet.</li>
    </ul>
  </body>
</html>

CSS


        html {
  box-sizing: border-box;
  font-family: system-ui, sans-serif;
  color-scheme: dark light;
}

*,
*:before,
*:after {
  box-sizing: inherit;
}

body {
  margin: 1rem;
}
        

JS


        const available = document.querySelector('.available');
const notAvailable = document.querySelector('.not-available');
const ul = document.querySelector('ul');
const lastUpdated = document.querySelector('.last-updated');

const updateContent = async () => {
  const data = await fetch(
    'https://worldtimeapi.org/api/timezone/Europe/London.json'
  ).then((response) => response.json());
  return new Date(data.unixtime * 1000);
};

const registerPeriodicBackgroundSync = async (registration) => {
  const status = await navigator.permissions.query({
    name: 'periodic-background-sync',
  });
  if (status.state === 'granted' && 'periodicSync' in registration) {
    try {
      // Register the periodic background sync.
      await registration.periodicSync.register('content-sync', {
        // An interval of one day.
        minInterval: 24 * 60 * 60 * 1000,
      });
      available.hidden = false;
      notAvailable.hidden = true;

      // List registered periodic background sync tags.
      const tags = await registration.periodicSync.getTags();
      if (tags.length) {
        ul.innerHTML = '';
      }
      tags.forEach((tag) => {
        const li = document.createElement('li');
        li.textContent = tag;
        ul.append(li);
      });

      // Update the user interface with the last periodic background sync data.
      const backgroundSyncCache = await caches.open('periodic-background-sync');
      if (backgroundSyncCache) {
        const backgroundSyncResponse =
          backgroundSyncCache.match('/last-updated');
        if (backgroundSyncResponse) {
          lastUpdated.textContent = `${await fetch('/last-updated').then(
            (response) => response.text()
          )} (periodic background-sync)`;
        }
      }

      // Listen for incoming periodic background sync messages.
      navigator.serviceWorker.addEventListener('message', async (event) => {
        if (event.data.tag === 'content-sync') {
          lastUpdated.textContent = `${await updateContent()} (periodic background sync)`;
        }
      });
    } catch (err) {
      console.error(err.name, err.message);
      available.hidden = true;
      notAvailable.hidden = false;
      lastUpdated.textContent = 'Never';
    }
  } else {
    available.hidden = true;
    notAvailable.hidden = false;
    lastUpdated.textContent = `${await updateContent()} (manual)`;
  }
};

if ('serviceWorker' in navigator) {
  window.addEventListener('load', async () => {
    const registration = await navigator.serviceWorker.register('./sw.js');
    console.log('Service worker registered for scope', registration.scope);

    await registerPeriodicBackgroundSync(registration);
  });
}