Os primeiros passos para tornar uma aplicação web offline
Melhorar a performance e usabilidade de webapps é minha maior motivação como desenvolvedor web. Nos meus estudos e pesquisas sobre como tornar aplicações web mais leves e agradáveis, me deparei com Services Workers (SW) e como essa tecnologia pode ajudar nessa missão. No meu post de estreia aqui no Medium, vou mostrar como configurar Services Workers e apresentar algumas estratégias simples para dar o start offline em webapps ou websites e ainda assim melhorar sua performance.
Services workers
Service worker é um script que roda em background no browser, numa thread separada das páginas web, como um proxy entre a web e o servidor. Entre suas principais utilidades estão: web push notifications, sincronismo em background e cache, a feature explicada nesse post. Os únicos requisitos para sua utilização é que o browser ofereça suporte e que a aplicação responda em um servidor https. Então mãos à obra:
1 — Registro
O primeiro passo é criar um arquivo register-sw.js na raiz do projeto e importá-lo no index.html. Esse arquivo inicia o processo de registro do service worker, apontando onde o browser deve encontrá-lo.
Como se trata de uma funcionalidade progressiva, o código confere se o navegador oferece suporte aos service workers. Se sim, é só chamar o método register passando a localização do arquivo de script. Eu imagino que muitos desenvolvedores podem se perguntar se não seria um problema registrar todas vez que a página é carregada, mas a vantagem é que o próprio browser se encarrega de conferir e registrar uma única vez.
2 — Instalação
Agora vamos criar o arquivo sw.js, onde deve estar o código de instalação do service worker.
self.addEventListener('install', async event => {
// Perform install steps
});
A forma mais simples de testar o sucesso dessa etapa é capturar o evento install e conferir o resultado no browser. Se tudo ocorreu bem, já é possível ver o service worker em ação no dev tools do browser. No caso do Google Chrome, é só ir em Application > Service Workers. É bem provável que lá você encontre também outros service workers de aplicações que você usa no dia a dia.
Agora que o SW foi instalado, vamos aplicar a estratégia mais simples para deixar uma webapp offline e pré carregar os arquivos estáticos em cache.
O próximo passo é criar um simples array com a localização dos arquivos:
const CACHE_NAME = 'app-cache-v1';const urlsToCache = [
'/',
'./index.html',
'./page2.html',
'./css/styles.css',
'./dist/bundle.min.js',
'./img/1.webp',
'./img/2.webp',
'./data/backup.json'
];
Para carregar e armazenar os arquivos, é preciso escutar o evento install, abrir o cache através da api cache e, no callback, adicionar o array criado antes com os caminhos dos arquivos:
self.addEventListener('install', async event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => {
return cache.addAll(urlsToCache);
})
);
});
Note que assim que o SW for instalado, o browser começa a carregar os arquivos e colocá-los em cache, mesmo antes da aplicação precisar deles. Isso pode reduzir consideravelmente o loading time da aplicação, pois todo esse carregamento está acontecendo em background.
3— Interceptando requisições
Para interceptar requisições e devolver nossos arquivos do cache, precisamos escutar o evento fetch, verificar se o arquivo requisitado está no cache e então responder com o arquivo armazenado. Essa estratégia é conhecida como "Cache only".
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response)
);
});
Se por algum motivo o arquivo não estiver no cache, é possível adotar outra estratégia, executando a requisição no servidor (Cache then Network).
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => response || fetch(event.request))
);
});
Um complemento à estratégia anterior é executar a requisição para o servidor, após responder a requisição interceptada com o arquivo do cache, e então atualizá-lo(Cache and Update) para que futuras requisições respondam com arquivos atualizados.
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(response)
);
event.waitUntil(update(event.request));
});const update = (request) => {
caches.open(CACHE_NAME).then(cache => {
return fetch(request).then(response => {
return cache.put(request, response);
});
});
}
Eu particularmente sou um entusiasta dos services workers pelo controle reativo que eles me possibilitam. Imagine ter uma aplicação preparada para qualquer situação, menos vulnerável às condições de conectividade e suas consequências. Por exemplo, se a internet do usuário cair, ao invés de apresentar uma mensagem de erro ou tela em branco, a aplicação pode responder com arquivos e mensagens customizadas ou trabalhar offline, até a internet voltar.
Combinar essas estratégias melhora a performance da aplicação e garante uma experiência mais agradável ao usuário. Com poucos passos é possível responder requisições com arquivos do cache. Essas mesmas estratégias podem ser complementadas com ferramentas mais poderosas como IndexedDB e aumentar ainda mais a capacidade offline de sua aplicação, mas isso já é assunto para um outro post…