Receitas de aplicativos offline





Bom dia amigos!



Apresento a sua atenção uma tradução do excelente artigo "Offline Cookbook" de Jake Archibald dedicado a vários casos de uso da API ServiceWorker e da API Cache.



Presume-se que você esteja familiarizado com os conceitos básicos dessas tecnologias, porque haverá muito código e poucas palavras.



Se não estiver familiarizado, comece com MDN e depois volte. Aqui está outro bom artigo sobre service workers, especificamente para recursos visuais.



Sem mais prefácio.



Quando economizar recursos?



O trabalhador permite que você processe solicitações independentemente do cache, portanto, iremos considerá-las separadamente.



A primeira pergunta é quando você deve armazenar recursos em cache?



Quando instalado como uma dependência






Um dos eventos que ocorre quando um trabalhador está em execução é o evento de instalação. Este evento pode ser usado para preparar o tratamento de outros eventos. Quando um novo trabalhador é instalado, o antigo continua a servir a página, portanto, lidar com o evento de instalação não deve interrompê-lo.



Adequado para armazenar estilos, imagens, scripts, modelos ... em geral, para quaisquer arquivos estáticos usados ​​na página.



Estamos falando daqueles arquivos sem os quais o aplicativo não pode funcionar como os arquivos incluídos no download inicial de aplicativos nativos.



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 aceita a promessa de determinar a duração e o resultado da instalação. Se a promessa for rejeitada, o trabalhador não será instalado. caches.open e cache.addAll retornam promessas. Se um dos recursos não estiver disponível, a

chamada para cache.addAll será rejeitada.



Quando instalado não como uma dependência






Isso é semelhante ao exemplo anterior, mas neste caso não esperamos que a instalação seja concluída, portanto, não a cancelará.



Adequado para grandes recursos que não são necessários agora, como recursos para os níveis posteriores do jogo.



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


Não passamos a promessa cache.addAll para event.waitUntil para os níveis 11-20, portanto, se ela for rejeitada, o jogo ainda será executado offline. Claro, você deve cuidar de possíveis problemas com o armazenamento em cache dos primeiros níveis e, por exemplo, tentar armazenar em cache novamente em caso de falha.



O trabalhador pode ser interrompido após o processamento de eventos antes que os níveis 11-20 sejam armazenados em cache. Isso significa que esses níveis não serão salvos. No futuro, está planejado adicionar uma interface de carregamento em segundo plano ao trabalhador para resolver esse problema, bem como baixar arquivos grandes, como filmes.



Aproximadamente. Por.: Esta interface foi implementada no final de 2018 e foi chamada de Background Fetch , mas até agora funciona apenas no Chrome e Opera (68% de acordo com CanIUse ).



Após a ativação






Adequado para excluir cache antigo e migrações.



Depois de instalar um novo trabalhador e interromper o antigo, o novo trabalhador é ativado e recebemos um evento de ativação. Esta é uma grande oportunidade para substituir recursos e excluir o cache antigo.



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


Durante a ativação, outros eventos como busca são enfileirados, portanto, uma ativação longa poderia teoricamente bloquear a página. Portanto, use este estágio apenas para coisas que o antigo trabalhador não pode fazer.



Quando ocorre um evento personalizado






Adequado quando o site inteiro não pode ser colocado offline. Nesse caso, damos ao usuário a capacidade de decidir o que armazenar em cache. Por exemplo, um vídeo do Youtube, uma página da Wikipedia ou uma galeria de imagens no Flickr.



Dê ao usuário um botão Ler mais tarde ou Salvar. Quando o botão for clicado, obtenha o recurso e grave-o no cache.



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)))
})


A interface de cache está disponível na página, assim como o próprio trabalhador, portanto, não precisamos chamar o último para economizar recursos.



Ao receber uma resposta






Adequado para recursos atualizados com frequência, como a caixa de correio do usuário ou o conteúdo do artigo. Também adequado para conteúdo secundário, como avatares, mas tenha cuidado neste caso.



Se o recurso solicitado não estiver no cache, nós o obtemos da rede, enviamos ao cliente e gravamos no cache.



Se você estiver solicitando vários URLs, como caminhos de avatar, certifique-se de que isso não sobrecarregue o armazenamento de origem (origem - protocolo, host e porta) - se o usuário precisar liberar espaço em disco, você não deve ser o primeiro. Tome cuidado ao remover recursos desnecessários.



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 a memória com eficiência, lemos o corpo da resposta apenas uma vez. O exemplo acima usa o método clone para criar uma cópia da resposta. Isso é feito para enviar simultaneamente uma resposta ao cliente e gravá-la no cache.



Durante a verificação de novidades






Adequado para atualizar recursos que não requerem as versões mais recentes. Isso pode se aplicar a avatares também.



Se o recurso estiver no cache, nós o usamos, mas obteremos uma atualização na próxima solicitação.



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
                    }))
    )
})


Quando você recebe uma notificação push






A API Push é uma abstração do trabalhador. Ele permite que o trabalhador seja executado em resposta a uma mensagem do sistema operacional. Além disso, isso acontece independentemente do usuário (quando a guia do navegador é fechada). Uma página normalmente envia uma solicitação ao usuário para permissão para executar certas ações.



Adequado para conteúdo que depende de notificações, como mensagens de bate-papo, notícias no feed, e-mails. Também é usado para sincronizar conteúdo, como tarefas em uma lista ou marcas de seleção em um calendário.



O resultado é uma notificação que, ao ser clicada, abre a página correspondente. Porém, é muito importante conservar recursos antes de enviar a notificação. O usuário está online quando a notificação é recebida, mas pode muito bem estar offline ao clicar nela, por isso é importante que o conteúdo esteja disponível offline naquele momento. O aplicativo móvel do Twitter faz isso um pouco errado.



Sem uma conexão de rede, o Twitter não fornece conteúdo relacionado a notificações. No entanto, clicar na notificação a exclui. Não faça isso!



O código a seguir atualiza o cache antes de enviar a notificação:



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/')
    }
})


Com sincronização em segundo plano






Sincronização em segundo plano é outra abstração sobre o trabalhador. Ele permite que você solicite uma sincronização de dados em segundo plano única ou periódica. Também é independente do usuário. No entanto, um pedido de permissão também é enviado a ele.



Adequado para atualização de recursos insignificantes, o envio regular de notificações sobre quais serão muito frequentes e, portanto, incômodos para o usuário, por exemplo, novos eventos na rede social ou novos artigos no feed de notícias.



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


Salvando cache



Sua fonte fornece uma certa quantidade de espaço livre. Este espaço é compartilhado entre todos os armazenamentos: local e sessão, banco de dados indexado, sistema de arquivos e, claro, cache.



Os tamanhos de armazenamento não são fixos e variam de acordo com o dispositivo e as condições de armazenamento. Você pode verificar assim:



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


Quando o tamanho deste ou daquele armazenamento atinge o limite, este armazenamento é limpo de acordo com certas regras que não podem ser alteradas neste momento.



Para resolver este problema, foi proposta a interface de envio de uma solicitação de permissão (requestPersistent):



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


Claro, o usuário deve conceder permissão para isso. O usuário deve fazer parte deste processo. Se a memória do dispositivo do usuário estiver cheia e a exclusão de dados secundários não resolver o problema, o usuário deve decidir quais dados manter e quais excluir.



Para que isso funcione, o sistema operacional deve tratar os armazenamentos do navegador como itens separados.



Responder a pedidos



Não importa quantos recursos você armazene em cache, o trabalhador não os usará até que você diga a ele quando e o que usar. Aqui estão alguns modelos para lidar com solicitações.



Apenas a dinheiro






Adequado para quaisquer recursos estáticos da versão atual da página. Você deve armazenar esses recursos em cache durante a fase de configuração do trabalhador para poder enviá-los em resposta às solicitações.



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


Apenas rede






Adequado para recursos que não podem ser armazenados em cache, como dados analíticos ou solicitações não GET.



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


Primeiro o cache, depois, em caso de falha, a rede






Adequado para lidar com a maioria das solicitações em aplicativos offline.



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


Os recursos salvos são retornados do cache, os recursos não salvos da rede.



Quem teve tempo, ele comeu






Adequado para pequenos recursos em busca de melhor desempenho para dispositivos com pouca memória.



A combinação de um disco rígido antigo, antivírus e uma conexão rápida com a Internet pode tornar a busca de dados da rede mais rápida do que a busca de dados do cache. No entanto, recuperar dados da rede enquanto os dados estão armazenados no dispositivo do usuário é um desperdício 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)
        ])
    )
})


Aproximadamente. Lane: Agora você pode usar Promise.allSettled para essa finalidade, mas seu suporte ao navegador é de 80%: -20% dos usuários provavelmente é demais.



Rede primeiro, então, em caso de falha, cache






Adequado para recursos que são atualizados com frequência e não afetam a versão atual do site, por exemplo, artigos, avatares, feeds de notícias em redes sociais, classificações de jogadores, etc.



Isso significa que você está servindo novo conteúdo para usuários online e conteúdo antigo para usuários offline. Se a solicitação de um recurso da rede for bem-sucedida, o cache provavelmente deve ser atualizado.



Essa abordagem tem uma desvantagem. Se o usuário tiver problemas de conexão ou for lento, ele terá que aguardar a conclusão ou falha da solicitação, em vez de buscar instantaneamente o conteúdo do cache. Essa espera pode ser muito longa, resultando em uma péssima experiência do usuário.



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


Primeiro o cache, depois a rede






Adequado para recursos atualizados com frequência.



Isso exige que a página envie duas solicitações, uma para o cache e outra para a rede. A ideia é retornar os dados do cache e atualizá-los ao receber dados da rede.



Às vezes, você pode substituir os dados atuais ao receber novos (por exemplo, a classificação dos jogadores), mas isso é problemático para grandes partes de conteúdo. Isso pode levar ao desaparecimento do que o usuário está lendo ou interagindo no momento.



O Twitter adiciona novo conteúdo acima do conteúdo existente, mantendo a rolagem: o usuário vê uma notificação de novos tweets no topo da tela. Isso é possível devido à ordem linear do conteúdo. Copiei este modelo para exibir o conteúdo do cache o mais rápido possível e adicionar novo conteúdo conforme ele obtém da web.



Código na 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 de trabalho:



acessamos a rede e atualizamos o cache.



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
                }))
    )
})


Internet Segura






Se as tentativas de obter o recurso do cache e a rede falharem, deve haver um fallback.



Adequado para placeholders (substituindo imagens por fictícios), solicitações POST com falha, páginas "Não disponível quando offline".



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


Se sua página enviar um email, o trabalhador pode salvá-lo em um banco de dados indexado antes de enviar e notificar a página que o envio falhou, mas o email foi salvo.



Criação de marcação no lado do trabalhador






Adequado para páginas que são renderizadas no lado do servidor e não podem ser armazenadas em cache.



A renderização de páginas do lado do servidor é um processo muito rápido, mas torna o armazenamento de conteúdo dinâmico no cache inútil, pois pode ser diferente para cada renderização. Se sua página for controlada por um trabalhador, você pode solicitar recursos e renderizar a página ali mesmo.



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


Você não precisa se limitar a um modelo. Você provavelmente terá que combiná-los, dependendo da solicitação. Por exemplo, treinado para emocionar usa o seguinte:



  • Cache de configuração do trabalhador para elementos de IU persistentes
  • Cache na resposta do servidor para imagens e dados do Flickr
  • Recuperando dados do cache e em caso de falha da rede para a maioria das solicitações
  • Recuperação de recursos do cache e da web para resultados de pesquisa do Flick


Basta olhar para a solicitação e decidir o que fazer com ela:



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 este artigo tenha sido útil para você. Obrigado pela atenção.



All Articles