Service Workers: cache offline e background sync
1. Fundamentos do Service Worker no ecossistema JS/Node/React
O Service Worker é um script que o navegador executa em segundo plano, separado da página web, permitindo funcionalidades como cache offline e sincronização em segundo plano. No ecossistema React, o Service Worker atua como uma camada de rede programável entre o frontend e o servidor.
Ciclo de vida
O Service Worker possui três eventos principais:
// service-worker.js
self.addEventListener('install', (event) => {
console.log('Service Worker instalado');
self.skipWaiting(); // Ativa imediatamente
});
self.addEventListener('activate', (event) => {
console.log('Service Worker ativado');
clients.claim(); // Assume controle das páginas abertas
});
self.addEventListener('fetch', (event) => {
console.log('Interceptando requisição:', event.request.url);
});
Registro no frontend React
// src/serviceWorkerRegistration.js
export function register() {
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker
.register('/service-worker.js')
.then((registration) => {
console.log('SW registrado:', registration.scope);
})
.catch((error) => {
console.error('Falha no registro:', error);
});
});
}
}
2. Estratégias de Cache Offline para SPAs React
Existem três estratégias principais de cache para aplicações React:
Cache-first (para assets estáticos)
// service-worker.js
self.addEventListener('fetch', (event) => {
if (event.request.url.includes('/static/')) {
event.respondWith(
caches.match(event.request).then((cachedResponse) => {
return cachedResponse || fetch(event.request);
})
);
}
});
Network-first (para dados de API)
self.addEventListener('fetch', (event) => {
if (event.request.url.includes('/api/')) {
event.respondWith(
fetch(event.request)
.then((response) => {
return caches.open('api-cache').then((cache) => {
cache.put(event.request, response.clone());
return response;
});
})
.catch(() => caches.match(event.request))
);
}
});
Stale-while-revalidate (para conteúdo dinâmico)
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cachedResponse) => {
const fetchPromise = fetch(event.request).then((response) => {
caches.open('dynamic-cache').then((cache) => {
cache.put(event.request, response.clone());
});
return response;
});
return cachedResponse || fetchPromise;
})
);
});
3. Implementando Cache Offline com Workbox + React
O Workbox simplifica a implementação de Service Workers em aplicações React.
Configuração do Workbox webpack plugin
// craco.config.js ou webpack.config.js
const { InjectManifest } = require('workbox-webpack-plugin');
module.exports = {
webpack: {
plugins: {
add: [
new InjectManifest({
swSrc: './src/service-worker.js',
swDest: 'service-worker.js',
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
}),
],
},
},
};
Service Worker com Workbox
// src/service-worker.js
import { precacheAndRoute } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { StaleWhileRevalidate, CacheFirst } from 'workbox-strategies';
precacheAndRoute(self.__WB_MANIFEST);
// Cache de assets estáticos
registerRoute(
({ request }) => request.destination === 'script' ||
request.destination === 'style',
new CacheFirst({ cacheName: 'static-assets' })
);
// Cache de requisições de API
registerRoute(
({ url }) => url.pathname.startsWith('/api/'),
new StaleWhileRevalidate({ cacheName: 'api-responses' })
);
Fallback offline com UI React
// src/components/OfflineFallback.jsx
import React from 'react';
const OfflineFallback = () => {
const [isOnline, setIsOnline] = React.useState(navigator.onLine);
React.useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
if (!isOnline) {
return (
<div className="offline-banner">
<p>Você está offline. Exibindo dados em cache.</p>
</div>
);
}
return null;
};
export default OfflineFallback;
4. Background Sync: Sincronizando Dados em Segundo Plano
O Background Sync permite enfileirar requisições quando o usuário está offline e reenviá-las automaticamente quando a conexão for restabelecida.
Registro de eventos sync no Service Worker
// service-worker.js
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-posts') {
event.waitUntil(syncPendingPosts());
}
});
async function syncPendingPosts() {
const db = await openIndexedDB();
const pendingPosts = await db.getAll('pending-posts');
for (const post of pendingPosts) {
try {
const response = await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(post.data)
});
if (response.ok) {
await db.delete('pending-posts', post.id);
}
} catch (error) {
console.error('Falha na sincronização:', error);
}
}
}
Enfileiramento de requisições no frontend
// src/hooks/useOfflineSync.js
export async function queuePostForSync(postData) {
const db = await openIndexedDB();
await db.add('pending-posts', {
id: Date.now(),
data: postData,
timestamp: new Date().toISOString()
});
if ('serviceWorker' in navigator && 'SyncManager' in window) {
const registration = await navigator.serviceWorker.ready;
await registration.sync.register('sync-posts');
}
}
5. Sincronização com Backend Node.js
API REST para receber dados sincronizados
// server/routes/sync.js
const express = require('express');
const router = express.Router();
router.post('/api/posts', async (req, res) => {
const { title, content, clientTimestamp } = req.body;
// Validação
if (!title || !content) {
return res.status(400).json({ error: 'Campos obrigatórios faltando' });
}
// Deduplicação baseada em timestamp
const existingPost = await Post.findOne({
clientTimestamp,
userId: req.user.id
});
if (existingPost) {
return res.status(200).json({
message: 'Post já sincronizado',
id: existingPost._id
});
}
const post = new Post({ title, content, clientTimestamp });
await post.save();
res.status(201).json({ id: post._id });
});
module.exports = router;
6. Gerenciamento de Estado e UI no React
Hook personalizado useOnlineStatus
// src/hooks/useOnlineStatus.js
import { useState, useEffect } from 'react';
export function useOnlineStatus() {
const [onlineStatus, setOnlineStatus] = useState({
isOnline: navigator.onLine,
pendingSync: 0
});
useEffect(() => {
const checkPendingSync = async () => {
if ('serviceWorker' in navigator) {
const registration = await navigator.serviceWorker.ready;
const tags = await registration.sync.getTags();
setOnlineStatus(prev => ({
...prev,
pendingSync: tags.length
}));
}
};
const handleOnline = () => {
setOnlineStatus(prev => ({ ...prev, isOnline: true }));
checkPendingSync();
};
const handleOffline = () => {
setOnlineStatus(prev => ({ ...prev, isOnline: false }));
};
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
// Verificar sincronizações pendentes a cada 30 segundos
const interval = setInterval(checkPendingSync, 30000);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
clearInterval(interval);
};
}, []);
return onlineStatus;
}
Indicador visual de fila de sincronização
// src/components/SyncIndicator.jsx
import React from 'react';
import { useOnlineStatus } from '../hooks/useOnlineStatus';
const SyncIndicator = () => {
const { isOnline, pendingSync } = useOnlineStatus();
if (isOnline && pendingSync === 0) return null;
return (
<div className={`sync-indicator ${isOnline ? 'online' : 'offline'}`}>
{isOnline ? (
<span>Sincronizando {pendingSync} itens...</span>
) : (
<span>Offline - {pendingSync} itens pendentes</span>
)}
</div>
);
};
export default SyncIndicator;
Atualização otimista com rollback
// src/hooks/useOptimisticUpdate.js
export function useOptimisticUpdate(apiEndpoint) {
const [data, setData] = useState([]);
const [pendingUpdates, setPendingUpdates] = useState([]);
const addItem = async (newItem) => {
// Atualização otimista
const tempId = `temp-${Date.now()}`;
setData(prev => [...prev, { ...newItem, id: tempId }]);
try {
const response = await fetch(apiEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newItem)
});
if (!response.ok) throw new Error('Falha na requisição');
const savedItem = await response.json();
// Substituir item temporário pelo real
setData(prev => prev.map(item =>
item.id === tempId ? savedItem : item
));
} catch (error) {
// Rollback
setData(prev => prev.filter(item => item.id !== tempId));
// Enfileirar para sincronização posterior
await queuePostForSync(newItem);
}
};
return { data, addItem, pendingUpdates };
}
7. Testes e Boas Práticas
Simulação de cenários offline com Cypress
// cypress/e2e/offline.cy.js
describe('Testes offline', () => {
it('deve exibir conteúdo em cache quando offline', () => {
cy.visit('/');
cy.contains('Dados carregados').should('be.visible');
// Simular modo offline
cy.intercept('GET', '/api/data', {
forceNetworkError: true
}).as('offlineRequest');
cy.reload();
cy.contains('Exibindo dados em cache').should('be.visible');
});
it('deve enfileirar requisições quando offline', () => {
cy.visit('/');
cy.contains('Criar Post').click();
// Desconectar
cy.window().then((win) => {
win.dispatchEvent(new Event('offline'));
});
cy.get('input[name="title"]').type('Post offline');
cy.get('button[type="submit"]').click();
cy.contains('1 itens pendentes').should('be.visible');
});
});
Versionamento de cache
// service-worker.js
const CACHE_VERSION = 'v2';
const STATIC_CACHE = `static-${CACHE_VERSION}`;
const API_CACHE = `api-${CACHE_VERSION}`;
self.addEventListener('activate', (event) => {
const validCaches = [STATIC_CACHE, API_CACHE];
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (!validCaches.includes(cacheName)) {
return caches.delete(cacheName);
}
})
);
})
);
});
Referências
- Service Workers: An Introduction - Google Developers — Guia oficial da Google sobre fundamentos de Service Workers, incluindo ciclo de vida e estratégias de cache
- Workbox Documentation - Chrome Developers — Documentação completa do Workbox para implementação de Service Workers em aplicações React
- Background Sync API - MDN Web Docs — Documentação oficial da Mozilla sobre a API de sincronização em segundo plano
- Using Service Workers with React - LogRocket — Tutorial prático sobre integração de Service Workers em aplicações React
- Offline-First Apps with React and Workbox - CSS-Tricks — Guia detalhado sobre estratégias offline-first para SPAs React
- Testing Service Workers with Cypress - Cypress Documentation — Como testar Service Workers usando o framework Cypress
- Node.js Service Worker Implementation - Node.js Official Docs — Implementação de Service Workers no backend Node.js para sincronização