feat(pwa): app instalável com service worker e shell offline
- manifest.webmanifest: standalone, theme/background color, ícones 192/512 any+maskable+monochrome (PNGs em web/icons) e atalho 'Nova nota' (?new=1) - sw.js: cache versionado (notas-shell-v1); navegação network-first com fallback ao shell (offline), estáticos stale-while-revalidate, /api/* e SSE sempre passthrough; activate limpa caches e notifica janelas - app.js: registro do SW, beforeinstallprompt com item 'Instalar Aplicativo…' no menu Ajuda (escondido em standalone/após instalar), toast de atualização, aviso offline/online, deep-link ?new=1 - index.html: meta tags theme-color/mobile-web-app/apple-touch-icon/manifest - server: staticHandler com MIME application/manifest+json e Cache-Control no-cache para /sw.js; embed inclui manifest, sw e ícones - README: seção PWA + orientação de serving (Go atual e equivalente Rust)
This commit is contained in:
1 parent
63fd024f68
commit
18d33ed141
12 files changed
+243
-3
No files matched your search
@@ -0,0 +1,79 @@
|
||||
/* Service Worker do Notas — caches versionados.
|
||||
* BUMP de CACHE em todo deploy que mude shell/assets (o activate limpa os antigos).
|
||||
*/
|
||||
const CACHE = 'notas-shell-v1';
|
||||
const SHELL = [
|
||||
'/',
|
||||
'/css/app.css',
|
||||
'/js/app.js',
|
||||
'/manifest.webmanifest',
|
||||
'/icons/icon-192.png',
|
||||
'/icons/icon-512.png',
|
||||
'/icons/maskable-192.png',
|
||||
'/icons/maskable-512.png',
|
||||
'/icons/monochrome-512.png',
|
||||
];
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
event.waitUntil(
|
||||
caches.open(CACHE)
|
||||
.then((cache) => cache.addAll(SHELL))
|
||||
.then(() => self.skipWaiting()),
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(
|
||||
caches.keys()
|
||||
.then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))))
|
||||
.then(() => self.clients.claim())
|
||||
.then(() => self.clients.matchAll({ type: 'window' }))
|
||||
.then((clients) => {
|
||||
for (const client of clients) client.postMessage('notas:updated');
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('message', (event) => {
|
||||
if (event.data === 'notas:skip-waiting') self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
const req = event.request;
|
||||
const url = new URL(req.url);
|
||||
|
||||
// 1. API e SSE: NUNCA interceptar/cache — dados devem ser sempre frescos
|
||||
// (interceptar SSE quebraria o stream de tempo real).
|
||||
if (url.origin !== self.location.origin || url.pathname.startsWith('/api/')) return;
|
||||
|
||||
// 2. Navegação (abrir o app): network-first com fallback ao shell em cache (offline).
|
||||
if (req.mode === 'navigate' || req.destination === 'document') {
|
||||
event.respondWith(
|
||||
fetch(req)
|
||||
.then((resp) => {
|
||||
const copy = resp.clone();
|
||||
caches.open(CACHE).then((c) => c.put('/', copy));
|
||||
return resp;
|
||||
})
|
||||
.catch(() => caches.match('/')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Estáticos (css/js/ícones/fontes): stale-while-revalidate —
|
||||
// mostra o cache na hora e atualiza em background.
|
||||
event.respondWith(
|
||||
caches.match(req).then((hit) => {
|
||||
const refresh = fetch(req)
|
||||
.then((resp) => {
|
||||
if (resp && resp.ok) {
|
||||
const copy = resp.clone();
|
||||
caches.open(CACHE).then((c) => c.put(req, copy));
|
||||
}
|
||||
return resp;
|
||||
})
|
||||
.catch(() => hit);
|
||||
return hit || refresh;
|
||||
}),
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user