Animación de hoja de papel al pasar página y URLs versionadas para saltar la caché del proxy

- Lector por páginas: animaciones Pasar hoja (la hoja se curva al levantarla, con sombra), deslizar y
  fundido; hacia atrás la página anterior vuelve y se coloca encima.
- app.js, style.css y sw.js se sirven con ?v=<hash> para que ni el proxy ni el navegador den una copia vieja.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-01 12:41:22 -06:00
co-authored by Claude Opus 5.5
parent 86999b5725
commit 29af893917
5 changed files with 237 additions and 14 deletions
+17 -3
View File
@@ -4,8 +4,20 @@ const SHELL = 'shell-v1'
const OFFLINE = 'offline-v1'
const SHELL_FILES = ['/', '/app.js', '/style.css', '/icon.svg', '/manifest.webmanifest', '/icon-192.png', '/icon-512.png']
// Se guarda la portada tal como la sirve el servidor y justo las versiones de app.js y style.css que
// enlaza (?v=…); cache: 'reload' para no copiar lo que tenga la caché HTTP (un proxy puede alargarla horas).
async function installShell() {
const c = await caches.open(SHELL)
const html = await fetch('/', { cache: 'reload' })
const text = await html.clone().text()
const linked = [...text.matchAll(/(?:src|href)="(\/(?:app\.js|style\.css)\?v=[^"]+)"/g)].map(m => m[1])
const files = [...linked, ...SHELL_FILES.filter(f => f !== '/' && !linked.some(l => l.startsWith(f + '?')))]
for (const f of files) await c.delete(f, { ignoreSearch: true })
await c.addAll(files.map(u => new Request(u, { cache: 'reload' })))
await c.put('/', html)
}
self.addEventListener('install', e => {
e.waitUntil(caches.open(SHELL).then(c => c.addAll(SHELL_FILES)).then(() => self.skipWaiting()))
e.waitUntil(installShell().then(() => self.skipWaiting()))
})
self.addEventListener('activate', e => {
e.waitUntil(caches.keys()
@@ -25,11 +37,13 @@ async function fromCache(res) {
headers.set('x-from-cache', '1')
return new Response(await res.blob(), { status: res.status, statusText: res.statusText, headers })
}
async function networkFirst(req, cacheName, { store = true, ignoreSearch = false, fallback } = {}) {
// single: guardar solo la última versión (app.js?v=… sustituye a la anterior).
async function networkFirst(req, cacheName, { store = true, ignoreSearch = false, fallback, single = false } = {}) {
const cached = () => caches.match(req, { ignoreSearch, cacheName }).then(hit => hit || (fallback && caches.match(fallback)))
const net = fetch(req).then(async res => {
if (store && res.ok) {
const c = await caches.open(cacheName)
if (single) await c.delete(req, { ignoreSearch: true })
if (store === true || (await c.match(req, { ignoreSearch }))) await c.put(req, res.clone())
}
return res
@@ -54,7 +68,7 @@ self.addEventListener('fetch', e => {
if (url.origin !== location.origin) return
const p = url.pathname
if (req.mode === 'navigate') return e.respondWith(networkFirst(req, SHELL, { fallback: '/' }))
if (SHELL_FILES.includes(p) || p === '/sw.js') return e.respondWith(networkFirst(req, SHELL))
if (SHELL_FILES.includes(p) || p === '/sw.js') return e.respondWith(networkFirst(req, SHELL, { ignoreSearch: true, single: true }))
if (p === '/api/me') return e.respondWith(networkFirst(req, SHELL))
// Ficha y capítulo: solo se guardan si el usuario los guardó (store: 'existing' = refrescar si ya estaban).
if (p.startsWith('/api/manga/')) return e.respondWith(networkFirst(req, OFFLINE, { store: 'existing', ignoreSearch: true }))