Os service workers estão em alta. Em março de 2018, o Safari do iOS começou a incluir os service workers, de modo que todos os principais navegadores, nesse momento, oferecem suporte a opções off-line. E isso é mais importante do que nunca: 20% dos adultos nos Estados Unidos não têm Internet em casa, o que faz com que essas pessoas dependam exclusivamente de um celular para acessar a maioria das informações. Isso pode incluir algo tão simples como verificar o saldo bancário ou algo tão tedioso como procurar um emprego ou até mesmo pesquisar doenças.

Os aplicativos com suporte off-line são uma necessidade, e a inclusão de um funcionário de serviço é um ótimo começo. No entanto, os service workers por si só só levarão alguém a uma experiência on-line para off-line realmente perfeita. Armazenar ativos em cache é ótimo, mas sem uma conexão com a Internet o senhor ainda não pode acessar novos dados nem enviar solicitações.

O ciclo de vida das solicitações

Atualmente, uma solicitação pode ter a seguinte aparência:

O ciclo de vida da solicitação

Um usuário aperta um botão e uma solicitação é disparada para um servidor em algum lugar. Se houver Internet, tudo deve ocorrer sem problemas. Se não houver Internet… bem, as coisas não são tão simples. A solicitação não será enviada, e talvez o usuário perceba que a solicitação não chegou a ser enviada, ou talvez ele não esteja ciente. Felizmente, há uma maneira melhor.

Entre: sincronização em segundo plano.

Sincronização em segundo plano

O ciclo de vida da sincronização em segundo plano

O ciclo de vida da sincronização em segundo plano é um pouco diferente. Primeiro, um usuário faz uma solicitação, mas em vez de a solicitação ser tentada imediatamente, o service worker entra em ação. O service worker verificará se o usuário tem acesso à Internet – se tiver, ótimo. A solicitação será enviada. Caso contrário, o service worker aguardará até que o usuário faça tem Internet e, nesse momento, envia a solicitação, depois de buscar os dados no IndexedDB. O melhor de tudo é que a sincronização em segundo plano continuará e enviará a solicitação mesmo que o usuário tenha navegado para fora da página original.

Suporte à sincronização em segundo plano

Embora a sincronização em segundo plano seja totalmente compatível apenas com o Chrome, o Firefox e o Edge estão trabalhando atualmente para implementá-la. Felizmente, com o uso de detecção de recursos e onLine e offLine eventospodemos usar com segurança a sincronização em segundo plano em qualquer aplicativo e, ao mesmo tempo, incluir um fallback.

Um aplicativo simples de assinatura de boletim informativo

(Se o senhor quiser acompanhar a demonstração, o código pode ser encontrado aqui e a demonstração em si pode ser encontrada aqui.)

Vamos supor que temos um formulário muito simples de assinatura de boletim informativo. Queremos que o usuário possa se inscrever em nosso boletim informativo, independentemente de ter ou não acesso à Internet no momento. Vamos começar com a implementação da sincronização em segundo plano.

(Este tutorial pressupõe que o senhor esteja familiarizado com service workers. Se o senhor não estiver, este é um bom lugar para começar. Se o senhor não estiver familiarizado com o IndexedDB, recomendo que o senhor comece por aqui.)

Ao configurar um service worker pela primeira vez, o senhor terá de registrá-lo no arquivo JavaScript do aplicativo. Isso pode se parecer com o seguinte:

if(navigator.serviceWorker) {
      navigator.serviceWorker.register('serviceworker.js');
}

Observe que estamos usando a detecção de recursos mesmo quando registramos o service worker. Não há quase nenhuma desvantagem em usar a detecção de recursos e isso impedirá o surgimento de erros em navegadores mais antigos, como o Internet Explorer 11, quando o service worker não estiver disponível. De modo geral, é um bom hábito manter-se atualizado, mesmo que nem sempre seja necessário.

Quando configuramos a sincronização em segundo plano, nossa função de registro é alterada e pode ter a seguinte aparência:

if(navigator.serviceWorker) {
        navigator.serviceWorker.register('./serviceworker.js')
        .then(function() {
            return navigator.serviceWorker.ready
        })
        .then(function(registration) {
            document.getElementById('submitForm').addEventListener('click', (event) => {
                registration.sync.register('example-sync')
                .catch(function(err) {
                    return err;
                })
            })
        })
        .catch( /.../ )
    }

Isso é muito mais código, mas vamos dividi-lo em uma linha de cada vez.

Primeiro, estamos registrando o service worker como antes, mas agora estamos aproveitando o fato de que o register retorna uma promessa. A próxima parte que o senhor vê é navigator.serviceWorker.ready. Essa é uma propriedade somente leitura de um service worker que, basicamente, permite que o senhor saiba se o service worker está pronto ou não. Essa propriedade nos permite atrasar a execução das seguintes funções até que o service worker esteja realmente pronto.

Em seguida, temos uma referência ao registro do service worker. Colocaremos um ouvinte de eventos em nosso botão de envio e, nesse ponto, registraremos um evento de sincronização e passaremos uma string. Essa string será usada no lado do service worker mais tarde.

Vamos reescrever isso rapidamente para incluir a detecção de recursos, pois sabemos que a sincronização em segundo plano ainda não tem amplo suporte.

if(navigator.serviceWorker) {
        navigator.serviceWorker.register('./serviceworker.js')
        .then(function() {
            return navigator.serviceWorker.ready
        })
        .then(function(registration) {
            document.getElementById('submitForm').addEventListener('click', (event) => {
                if(registration.sync) {
                    registration.sync.register('example-sync')
                    .catch(function(err) {
                        return err;
                    })
                }
            })
        })
    }

Agora vamos dar uma olhada no lado do service worker.

self.onsync = function(event) {
    if(event.tag == 'example-sync') {
        event.waitUntil(sendToServer());
    }
}

Anexamos uma função ao onsync, o ouvinte de eventos para sincronização em segundo plano. Queremos observar a string que passamos para a função de registro no JavaScript do aplicativo. Estamos observando essa string usando event.tag.

Também estamos usando event.waitUntil. Como um service worker não está em execução contínua – ele “acorda” para fazer uma tarefa e depois “volta a dormir” – queremos usar o event.waitUntil para manter o service worker ativo. Essa função aceita um parâmetro de função. A função que passamos retornará uma promessa, e o event.waitUntil manterá o service worker “acordado” até que essa função seja resolvida. Se não tivermos usado event.waitUntil a solicitação talvez nunca chegasse ao servidor porque o service worker executaria a função onsync e, em seguida, voltaria imediatamente a dormir.

Observando o código acima, o senhor notará que não precisamos fazer nada para verificar o status da conexão de Internet do usuário ou enviar a solicitação novamente se a primeira tentativa falhar. A sincronização em segundo plano está cuidando de tudo isso para nós. Vamos dar uma olhada em como acessamos os dados no service worker.

Como um service worker é isolado em seu próprio worker, não poderemos acessar nenhum dado diretamente do DOM. Dependeremos do IndexedDB para obter os dados e depois enviá-los para o servidor.

O IndexedDB utiliza retornos de chamada, enquanto um service worker é baseado em promessas, portanto, teremos de levar isso em conta em nossa função. (Existem wrappers em torno do IndexedDB que tornam esse processo um pouco mais simples. Recomendo que dê uma olhada no IDB ou clipe de dinheiro.)

Aqui está a aparência de nossa função:

return new Promise(function(resolve, reject) {
    var db = indexedDB.open('newsletterSignup');
    db.onsuccess = function(event) {
        this.result.transaction("newsletterObjStore").objectStore("newsletterObjStore").getAll().onsuccess = function(event) {
            resolve(event.target.result);
        }
    }
    db.onerror = function(err) {
        reject(err);
    }
});

Ao analisar a função, estamos retornando uma promessa e usaremos a função resolve e reject para tornar essa função mais baseada em promessas e manter tudo alinhado com o service worker.

Abriremos um banco de dados e usaremos a função getAll para extrair todos os dados do armazenamento de objetos especificado. Quando isso for bem-sucedido, resolveremos a função com os dados. Se houver um erro, rejeitaremos. Isso mantém nosso tratamento de erros funcionando da mesma forma que todas as outras promessas e garante que tenhamos os dados antes de enviá-los ao servidor.

Depois de obtermos os dados, basta fazer uma solicitação de busca como faríamos normalmente.

fetch('https://www.mocky.io/v2/5c0452da3300005100d01d1f', {
    method: 'POST',
    body: JSON.stringify(response),
    headers:{
        'Content-Type': 'application/json'
    }
})

É claro que tudo isso só será executado se o usuário tiver acesso à Internet. Se o usuário não tiver acesso à Internet, o service worker aguardará o retorno da conexão. Se, após o retorno da conexão, a solicitação de busca falhar, o service worker tentará no máximo três vezes antes de parar de tentar enviar a solicitação definitivamente.

Agora que configuramos a sincronização em segundo plano, estamos prontos para configurar nosso fallback para navegadores que não são compatíveis com a sincronização em segundo plano.

Suporte para navegadores antigos

Infelizmente, os service workers não são compatíveis com os navegadores antigos e o recurso de sincronização em segundo plano só é compatível com o Chrome até o momento. Nesta publicação, vamos nos concentrar na utilização de outros recursos off-line para imitar a sincronização em segundo plano e oferecer uma experiência semelhante.

Eventos on-line e off-line

Começaremos com eventos on-line e off-line. Nosso código para registrar o trabalho de serviço da última vez ficou assim:

if(navigator.serviceWorker) {
    navigator.serviceWorker.register('./serviceworker.js')
    .then(function() {
        return navigator.serviceWorker.ready
    })
    .then(function(registration) {
        document.getElementById('submitForm').addEventListener('click', (event) => {
            event.preventDefault();
            saveData().then(function() {
                if(registration.sync) {
                    registration.sync.register('example-sync')
                    .catch(function(err) {
                        return err;
                    })
                }
            });
        })
    })
}

Vamos fazer uma rápida recapitulação desse código. Depois de registrarmos o service worker, usamos a promessa retornada de navigator.serviceWorker.ready para garantir que o service worker esteja de fato pronto para funcionar. Quando o service worker estiver pronto, anexaremos um ouvinte de eventos ao botão submit e salvaremos imediatamente os dados no IndexedDB. Para nossa sorte, o IndexedDB é compatível com praticamente todos os navegadores, portanto, podemos confiar nele.

Depois de salvarmos os dados, usamos a detecção de recursos para garantir que podemos usar a sincronização em segundo plano. Vamos em frente e adicionar nosso plano de fallback no else.

if(registration.sync) {
    registration.sync.register('example-sync')
    .catch(function(err) {
        return err;
    })
} else {
    if(navigator.onLine) {
        sendData();
    } else {
        alert("You are offline! When your internet returns, we'll finish up your request.");
    }
}

Suporte adicional

Estamos usando navigator.onLine para verificar a conexão do usuário com a Internet. Se ele tiver uma conexão, isso retornará true. Se o usuário tiver uma conexão com a Internet, enviaremos os dados. Caso contrário, exibiremos um alerta informando ao usuário que seus dados não foram enviados.

Vamos adicionar alguns eventos para monitorar a conexão com a Internet. Primeiro, adicionaremos um evento para observar se a conexão está off-line.

window.addEventListener('offline', function() {
    alert('You have lost internet access!');
});

Se o usuário perder a conexão com a Internet, ele verá um alerta. Em seguida, adicionaremos um ouvinte de eventos para observar se o usuário está novamente on-line.

window.addEventListener('online', function() {
    if(!navigator.serviceWorker && !window.SyncManager) {
        fetchData().then(function(response) {
            if(response.length > 0) {
                return sendData();
            }
        });
    }
});

Quando a conexão do usuário com a Internet retornar, faremos uma verificação rápida para ver se um service worker está disponível e também se a sincronização está disponível. Queremos verificar isso porque, se o navegador tiver sincronização disponível, não precisaremos contar com nosso fallback, pois isso resultaria em duas buscas. No entanto, se usarmos nosso fallback, primeiro extrairemos os dados do IndexedDB da seguinte forma:

var myDB = window.indexedDB.open('newsletterSignup');

myDB.onsuccess = function(event) {
    this.result.transaction("newsletterObjStore").objectStore("newsletterObjStore").getAll().onsuccess = function(event) {
        return event.target.result;
    };
};

myDB.onerror = function(err) {
    reject(err);
}

Em seguida, verificaremos se a resposta do IndexedDB realmente tem dados e, se tiver, a enviaremos ao nosso servidor.

Esse fallback não substituirá totalmente a sincronização em segundo plano por alguns motivos. Em primeiro lugar, estamos verificando se há eventos on-line e off-line, o que não precisamos fazer com a sincronização em segundo plano, pois a sincronização em segundo plano cuida de tudo isso para nós. Além disso, a sincronização em segundo plano continuará tentando enviar solicitações mesmo que o usuário tenha saído da página.

Nossa solução não poderá enviar a solicitação mesmo que o usuário saia da página, mas podemos verificar preventivamente o IndexedDB assim que a página for carregada e enviar imediatamente todos os dados armazenados em cache. Essa solução também observa qualquer alteração na conexão de rede e envia dados em cache assim que a conexão retorna.

Próximas etapas do suporte off-line

Os navegadores Edge e Firefox estão trabalhando atualmente na implementação da sincronização em segundo plano, o que é fantástico. É um dos melhores recursos para proporcionar uma experiência mais empática para os usuários que transitam entre a conexão com a Internet e a perda de conexão. Felizmente, com uma pequena ajuda dos eventos on-line e off-line e do IndexedDB, podemos começar a oferecer uma experiência melhor para os usuários hoje mesmo.

Se o senhor quiser saber mais sobre técnicas off-line, visite meu blog: carmalou.com ou me siga no Twitter.

Carmen Bourlon

Sobre Carmen Bourlon

Carmen é uma desenvolvedora de software que vive e trabalha em Oklahoma City, com interesse especial em tecnologia off-line. Ela falou em várias conferências sobre Offline First e organiza um grupo local de tecnologia para mulheres. Em seu tempo livre, Carmen defende Hyrule do malvado Ganon e escreve bots para o Twitter.