Recetas de aplicaciones sin conexión





¡Buen dia amigos!



Presento a su atención una traducción del excelente artículo de Jake Archibald "Libro de recetas sin conexión" dedicado a varios casos de uso de la API de ServiceWorker y la API de caché.



Se supone que está familiarizado con los conceptos básicos de estas tecnologías, porque habrá mucho código y pocas palabras.



Si no está familiarizado, comience con MDN y luego regrese. Aquí hay otro buen artículo sobre trabajadores de servicios específicamente para imágenes.



Sin más prefacio.



¿En qué momento ahorrar recursos?



El trabajador te permite procesar solicitudes independientemente del caché, por lo que las consideraremos por separado.



La primera pregunta es ¿cuándo debería almacenar en caché los recursos?



Cuando se instala como una dependencia






Uno de los eventos que ocurre cuando un trabajador se está ejecutando es el evento de instalación. Este evento se puede utilizar para prepararse para manejar otros eventos. Cuando se instala un nuevo trabajador, el anterior continúa sirviendo la página, por lo que manejar el evento de instalación no debería romperla.



Adecuado para almacenar en caché estilos, imágenes, scripts, plantillas ... en general, para cualquier archivo estático utilizado en la página.



Estamos hablando de aquellos archivos sin los cuales la aplicación no puede funcionar como los archivos incluidos en la descarga inicial de aplicaciones nativas.



self.addEventListener('install', event => {
    event.waitUntil(
        caches.open('mysite-static-v3')
            .then(cache => cache.addAll([
                '/css/whatever-v3.css',
                '/css/imgs/sprites-v6.png',
                '/css/fonts/whatever-v8.woff',
                '/js/all-min-v4.js'
                //  ..
            ]))
    )
})


event.waitUntil acepta la promesa de determinar la duración y el resultado de la instalación. Si se rechaza la promesa, el trabajador no se instalará. caches.open y cache.add Todas devuelven promesas. Si uno de los recursos no está disponible, la

llamada a cache.addAll será rechazada.



Cuando se instala no como una dependencia






Esto es similar al ejemplo anterior, pero en este caso no esperamos a que se complete la instalación, por lo que no cancelará la instalación.



Adecuado para grandes recursos que no se requieren en este momento, como recursos para los niveles posteriores del juego.



self.addEventListener('install', event => {
    event.waitUntil(
        caches.open('mygame-core-v1')
            .then(cache => {
                cache.addAll(
                    //  11-20
                )
                return cache.addAll(
                    //     1-10
                )
            })
    )
})


No pasamos la promesa cache.addAll a event.waitUntil para los niveles 11-20, por lo que si se rechaza, el juego seguirá funcionando sin conexión. Por supuesto, debe ocuparse de los posibles problemas con el almacenamiento en caché de los primeros niveles y, por ejemplo, intentar almacenar en caché nuevamente en caso de falla.



El trabajador se puede detener después de procesar eventos antes de que los niveles 11-20 se almacenen en caché. Esto significa que estos niveles no se guardarán. En el futuro, se planea agregar una interfaz de carga en segundo plano al trabajador para resolver este problema, así como para descargar archivos grandes como películas.



Aprox. Per .: Esta interfaz se implementó a finales de 2018 y se denominó Background Fetch , pero hasta ahora funciona solo en Chrome y Opera (68% según CanIUse ).



Tras la activación






Adecuado para eliminar cachés antiguos y migraciones.



Después de instalar un nuevo trabajador y detener el anterior, el nuevo trabajador se activa y recibimos un evento de activación. Esta es una gran oportunidad para reemplazar recursos y eliminar el caché antiguo.



self.addEventListener('activate', event => {
    event.waitUntil(
        caches.keys()
            .then(cacheNames => Promise.all(
                cacheNames.filter(cacheName => {
                    //  true, ,     ,
                    //  ,      
                }).map(cacheName => caches.delete(cacheName))
            ))
    )
})


Durante la activación, otros eventos, como la búsqueda, se ponen en cola, por lo que una activación prolongada podría bloquear teóricamente la página. Por lo tanto, use esta etapa solo para cosas que no puede hacer con el trabajador anterior.



Cuando ocurre un evento personalizado






Adecuado cuando no se puede desconectar todo el sitio. En este caso, le damos al usuario la posibilidad de decidir qué almacenar en caché. Por ejemplo, un video de Youtube, una página de Wikipedia o una galería de imágenes en Flickr.



Proporcione al usuario un botón Leer más tarde o Guardar. Cuando se hace clic en el botón, obtenga el recurso y escríbalo en el caché.



document.querySelector('.cache-article').addEventListener('click', event => {
    event.preventDefault()

    const id = event.target.dataset.id
    caches.open(`mysite-article ${id}`)
        .then(cache => fetch(`/get-article-urls?id=${id}`)
            .then(response => {
                // get-article-urls     JSON
                //  URL   
                return response.json()
            }).then(urls => cache.addAll(urls)))
})


La interfaz de almacenamiento en caché está disponible en la página, al igual que el trabajador mismo, por lo que no es necesario llamar a este último para ahorrar recursos.



Mientras recibe una respuesta






Adecuado para recursos que se actualizan con frecuencia, como el buzón de correo de un usuario o el contenido de un artículo. También es adecuado para contenido menor como avatares, pero tenga cuidado en este caso.



Si el recurso solicitado no está en la caché, lo obtenemos de la red, lo enviamos al cliente y lo escribimos en la caché.



Si solicita varias URL, como rutas de avatar, asegúrese de que no se desborde del almacén de origen (origen: protocolo, host y puerto); si el usuario necesita liberar espacio en el disco, no debería ser el primero. Ocúpate de eliminar los recursos innecesarios.



self.addEventListener('fetch', event => {
    event.respondWith(
        caches.open('mysite-dynamic')
            .then(cache => cache.match(event.request)
                .then(response => response || fetch(event.request)
                    .then(response => {
                        cache.put(event.request, response.clone())
                        return response
                    })))
    )
})


Para usar la memoria de manera eficiente, solo leemos el cuerpo de la respuesta una vez. El ejemplo anterior usa el método de clonación para crear una copia de la respuesta. Esto se hace para enviar simultáneamente una respuesta al cliente y escribirla en la caché.



Durante el cheque de novedad






Adecuado para actualizar recursos que no requieren las últimas versiones. Esto también se puede aplicar a los avatares.



Si el recurso está en la caché, lo usamos, pero obtenemos una actualización en la siguiente solicitud.



self.addEventListener('fetch', event => {
    event.respondWith(
        caches.open('mysite-dynamic')
            .then(cache => cache.match(event.request)
                .then(response => {
                    const fetchPromise = fetch(event.request)
                        .then(networkResponse => {
                            cache.put(event.request, networkResponse.clone())
                            return networkResponse
                        })
                        return response || fetchPromise
                    }))
    )
})


Cuando recibe una notificación push






La API Push es una abstracción sobre el trabajador. Permite que el trabajador se ejecute en respuesta a un mensaje del sistema operativo. Además, esto sucede independientemente del usuario (cuando la pestaña del navegador está cerrada). Normalmente, una página envía una solicitud al usuario de permiso para realizar determinadas acciones.



Adecuado para contenido que depende de notificaciones, como mensajes de chat, feeds de noticias, correos electrónicos. También se utiliza para sincronizar contenido, como tareas en una lista o marcas de verificación en un calendario.



El resultado es una notificación que, al hacer clic, abre la página correspondiente. Sin embargo, es muy importante conservar los recursos antes de enviar la notificación. El usuario está en línea cuando se recibe la notificación, pero bien puede estar fuera de línea al hacer clic en ella, por lo que es importante que el contenido esté disponible sin conexión en ese momento. La aplicación móvil de Twitter hace esto un poco mal.



Sin una conexión de red, Twitter no proporciona contenido relacionado con notificaciones. Sin embargo, hacer clic en la notificación la elimina. ¡No hagas eso!



El siguiente código actualiza la caché antes de enviar la notificación:



self.addEventListener('push', event => {
    if (event.data.text() === 'new-email') {
        event.waitUntil(
            caches.open('mysite-dynamic')
                .then(cache => fetch('/inbox.json')
                    .then(response => {
                        cache.put('/inbox.json', response.clone())
                        return response.json()
                    })).then(emails => {
                        registration.showNotification('New email', {
                            body: `From ${emails[0].from.name}`,
                            tag: 'new-email'
                        })
                    })
        )
    }
})

self.addEventListener('notificationclick', event => {
    if (event.notification.tag === 'new-email') {
        // ,   ,    /inbox/  ,
        // ,   
        new WindowClient('/inbox/')
    }
})


Con sincronización en segundo plano






Background Sync es otra abstracción sobre el trabajador. Le permite solicitar una sincronización de datos en segundo plano única o periódica. También es independiente del usuario. Sin embargo, también se le envía una solicitud de permiso.



Adecuado para actualizar recursos insignificantes, cuyo envío regular de notificaciones será demasiado frecuente y, por tanto, molesto para el usuario, por ejemplo, nuevos eventos en una red social o nuevos artículos en el feed de noticias.



self.addEventListener('sync', event => {
    if (event.id === 'update-leaderboard') {
        event.waitUntil(
            caches.open('mygame-dynamic')
                .then(cache => cache.add('/leaderboard.json'))
        )
    }
})


Guardando caché



Su fuente proporciona una cierta cantidad de espacio libre. Este espacio se comparte entre todos los almacenamientos: local y de sesión, base de datos indexada, sistema de archivos y, por supuesto, caché.



Los tamaños de almacenamiento no son fijos y varían según el dispositivo y las condiciones de almacenamiento. Puedes comprobarlo así:



navigator.storageQuota.queryInfo('temporary').then(info => {
    console.log(info.quota)
    // : <  >
    console.log(info.usage)
    //  <    >
})


Cuando el tamaño de este o aquel almacenamiento alcanza el límite, este almacenamiento se borra de acuerdo con ciertas reglas que no se pueden cambiar en este momento.



Para solucionar este problema, se propuso la interfaz para enviar una solicitud de permiso (requestPersistent):



navigator.storage.requestPersistent().then(granted => {
    if (granted) {
        // ,       
    }
})


Por supuesto, el usuario debe conceder permiso para ello. El usuario debe formar parte de este proceso. Si la memoria del dispositivo del usuario está llena y la eliminación de datos menores no resuelve el problema, el usuario debe decidir qué datos conservar y cuáles eliminar.



Para que esto funcione, el sistema operativo debe tratar las tiendas del navegador como elementos separados.



Responder solicitudes



No importa cuántos recursos guarde en caché, el trabajador no los usará hasta que usted le diga cuándo y qué usar. Aquí hay algunas plantillas para manejar solicitudes.



Solamente efectivo






Adecuado para cualquier recurso estático de la versión actual de la página. Debe almacenar en caché estos recursos durante la fase de configuración del trabajador para poder enviarlos en respuesta a las solicitudes.



self.addEventListener('fetch', event => {
    //     ,
    //      
    event.respondWith(caches.match(event.request))
})


Solo red






Adecuado para recursos que no se pueden almacenar en caché, como datos analíticos o solicitudes que no son GET.



self.addEventListener('fetch', event => {
    event.respondWith(fetch(event.request))
    //     event.respondWith
    //      
})


Primero el caché, luego, en caso de falla, la red






Adecuado para manejar la mayoría de solicitudes en aplicaciones fuera de línea.



self.addEventListener('fetch', event => {
    event.respondWith(
        caches.match(event.request)
            .then(response => response || fetch(event.request))
    )
})


Los recursos guardados se devuelven de la caché, los recursos no guardados de la red.



Quien tuvo tiempo, comió






Adecuado para pequeños recursos en busca de un mejor rendimiento para dispositivos con poca memoria.



La combinación de un disco duro antiguo, antivirus y una conexión rápida a Internet puede hacer que la obtención de datos de la red sea más rápida que la obtención de datos de la caché. Sin embargo, recuperar datos de la red mientras se almacenan en el dispositivo del usuario es una pérdida de recursos.



// Promise.race   ,   
//       .
//   
const promiseAny = promises => new Promise((resolve, reject) => {
    //  promises   
    promises = promises.map(p => Promise.resolve(p))
    //   ,    
    promises.forEach(p => p.then(resolve))
    //     ,   
    promises.reduce((a, b) => a.catch(() => b))
        .catch(() => reject(Error('  ')))
})

self.addEventListener('fetch', event => {
    event.respondWith(
        promiseAny([
            caches.match(event.request),
            fetch(event.request)
        ])
    )
})


Aprox. Lane: ahora puede usar Promise.allSettled para este propósito, pero su compatibilidad con el navegador es del 80%: -20% de los usuarios probablemente sea demasiado.



Red primero, luego, en caso de falla, caché






Adecuado para recursos que se actualizan con frecuencia y no afectan la versión actual del sitio, por ejemplo, artículos, avatares, noticias en redes sociales, calificaciones de jugadores, etc.



Esto significa que está ofreciendo contenido nuevo a usuarios en línea y contenido antiguo a usuarios sin conexión. Si la solicitud de un recurso de la red se realiza correctamente, probablemente debería actualizarse la caché.



Este enfoque tiene un inconveniente. Si el usuario tiene problemas de conexión o es lento, debe esperar a que la solicitud se complete o falle en lugar de obtener instantáneamente el contenido de la caché. Esta espera puede ser muy larga, lo que resulta en una experiencia de usuario terrible.



self.addEventListener('fetch', event => {
    event.respondWith(
        fetch(event.request).catch(() => caches.match(event.request))
    )
})


Primero el caché, luego la red






Adecuado para recursos actualizados con frecuencia.



Esto requiere que la página envíe dos solicitudes, una para el caché y otra para la red. La idea es devolver los datos de la caché y luego actualizarlos al recibir datos de la red.



A veces, puede reemplazar los datos actuales cuando recibe otros nuevos (por ejemplo, la calificación de los jugadores), pero esto es problemático para grandes piezas de contenido. Esto puede llevar a la desaparición de lo que el usuario está leyendo o interactuando actualmente.



Twitter agrega contenido nuevo por encima del contenido existente mientras mantiene el desplazamiento: el usuario ve una notificación de nuevos tweets en la parte superior de la pantalla. Esto es posible debido al orden lineal del contenido. Copié esta plantilla para mostrar el contenido de la caché lo más rápido posible y agregar contenido nuevo a medida que llega de la web.



Código en la página:



const networkDataReceived = false

startSpinner()

//   
const networkUpdate = fetch('/data.json')
    .then(response => response.json())
        .then(data => {
            networkDataReceived = true
            updatePage(data)
        })

//   
caches.match('/data.json')
    .then(response => {
        if (!response) throw Error(' ')
        return response.json()
    }).then(data => {
        //      
        if (!networkDataReceived) {
            updatePage(data)
        }
    }).catch(() => {
        //      ,  -   
        return networkUpdate
    }).catch(showErrorMessage).then(stopSpinner)


Código trabajador:



Accedemos a la red y actualizamos la caché.



self.addEventListener('fetch', event => {
    event.respondWith(
        caches.open('mysite-dynamic')
            .then(cache => fetch(event.request)
                .then(response => {
                    cache.put(event.request, response.clone())
                    return response
                }))
    )
})


Red de seguridad






Si los intentos de obtener el recurso de la caché y la red fallan, debe haber una reserva.



Adecuado para marcadores de posición (reemplazando imágenes con ficticias), solicitudes POST fallidas, páginas "No disponible sin conexión".



self.addEventListener('fetch', event => {
    event.respondWith(
        //     
        //   ,   
        caches.match(event.request)
            .then(response => response || fetch(event.request))
            .catch(() => {
                //    ,  
                return caches.match('/offline.html')
                //       
                //    URL   
            })
    )
})


Si su página envía un correo electrónico, el trabajador puede guardarlo en una base de datos indexada antes de enviar y notificar a la página que el envío falló, pero el correo electrónico se guardó.



Creando marcado en el lado del trabajador






Adecuado para páginas que se procesan en el lado del servidor y no se pueden almacenar en caché.



El procesamiento de páginas del lado del servidor es un proceso muy rápido, pero hace que almacenar contenido dinámico en la caché sea inútil, ya que puede ser diferente para cada procesamiento. Si su página está controlada por un trabajador, puede solicitar recursos y representar la página allí mismo.



import './templating-engine.js'

self.addEventListener('fetch', event => {
    const requestURL = new URL(event.request.url)

    event.respondWith(
        Promise.all([
            caches.match('/article-template.html')
                .then(response => response.text()),
            caches.match(`${requestURL.path}.json`)
                .then(response => response.json())
        ]).then(responses => {
            const template = responses[0]
            const data = responses[1]

            return new Response(renderTemplate(template, data), {
                headers: {
                    'Content-Type': 'text/html'
                }
            })
        })
    )
})


Juntos


No tiene que estar limitado a una plantilla. Lo más probable es que tenga que combinarlos según la solicitud. Por ejemplo, entrenado para la emoción utiliza lo siguiente:



  • Almacenamiento en caché de configuración de trabajador para elementos de IU persistentes
  • Almacenamiento en caché en la respuesta del servidor para imágenes y datos de Flickr
  • Recuperar datos del caché y en caso de falla de la red para la mayoría de las solicitudes
  • Recuperar recursos de la caché y luego de la web para los resultados de búsqueda de Flick


Solo mire la solicitud y decida qué hacer con ella:



self.addEventListener('fetch', event => {
    //  URL
    const requestURL = new URL(event.request.url)

    //       
    if (requestURL.hostname === 'api.example.com') {
        event.respondWith(/*    */)
        return
    }

    //    
    if (requestURL.origin === location.origin) {
        //   
        if (/^\/article\//.test(requestURL.pathname)) {
            event.respondWith(/*    */)
            return
        }
        if (/\.webp$/.test(requestURL.pathname)) {
            event.respondWith(/*    */)
            return
        }
        if (request.method == 'POST') {
            event.respondWith(/*     */)
            return
        }
        if (/cheese/.test(requestURL.pathname)) {
            event.respondWith(
                // . .:    -   ?
                new Response('Flagrant cheese error', {
                //    
                status: 512
                })
            )
            return
        }
    }

    //  
    event.respondWith(
        caches.match(event.request)
            .then(response => response || fetch(event.request))
    )
})


Espero que el artículo te haya sido útil. Gracias por su atención.



All Articles