Um guia abrangente para aplicativos da Web progressivos

Publicados: 2022-03-10
Resumo rápido ↬ Neste artigo, veremos os pontos problemáticos dos usuários que estão navegando em sites antigos sem PWA e a promessa dos PWAs de tornar a web excelente. Você aprenderá a maioria das tecnologias importantes que criam PWAs interessantes, como service workers, notificações push da Web e IndexedDB.

Era aniversário do meu pai, e eu queria pedir um bolo de chocolate e uma camisa para ele. Fui até o Google para procurar bolos de chocolate e cliquei no primeiro link nos resultados da pesquisa. Houve uma tela em branco por alguns segundos; Eu não entendia o que estava acontecendo. Depois de alguns segundos olhando pacientemente, a tela do meu celular se encheu de bolos deliciosos. Assim que cliquei em um deles para verificar seus detalhes, recebi um pop-up feio e gordo, me pedindo para instalar um aplicativo Android para que eu pudesse ter uma experiência suave e sedosa ao pedir um bolo.

Isso foi decepcionante. Minha consciência não me permitiu clicar no botão “Instalar”. Tudo o que eu queria fazer era pedir um pequeno bolo e seguir meu caminho.

Cliquei no ícone de cruz à direita do pop-up para sair dele o mais rápido possível. Mas então o pop-up de instalação ficou na parte inferior da tela, ocupando um quarto do espaço. E com a interface do usuário escamosa, rolar para baixo era um desafio. De alguma forma consegui encomendar um bolo holandês.

Depois dessa experiência terrível, meu próximo desafio foi encomendar uma camisa para o meu pai. Como antes, procuro camisas no Google. Cliquei no primeiro link e, em um piscar de olhos, todo o conteúdo estava bem na minha frente. A rolagem foi suave. Nenhum banner de instalação. Senti como se estivesse navegando em um aplicativo nativo. Houve um momento em que minha terrível conexão com a internet desistiu, mas ainda consegui ver o conteúdo em vez de um jogo de dinossauro. Mesmo com a internet ruim, consegui encomendar uma camisa e uma calça jeans para o meu pai. O mais surpreendente de tudo, eu estava recebendo notificações sobre meu pedido.

Eu chamaria isso de uma experiência suave e sedosa. Essas pessoas estavam fazendo algo certo. Todo site deve fazer isso para seus usuários. É chamado de aplicativo da web progressivo.

Como Alex Russell afirma em uma de suas postagens no blog:

“Acontece na web de tempos em tempos que tecnologias poderosas passam a existir sem o benefício de departamentos de marketing ou embalagens elegantes. Eles permanecem e crescem nas periferias, tornando-se velhos para um pequeno grupo enquanto permanecem quase invisíveis para todos os outros. Até que alguém os nomeie.”

Uma experiência suave e sedosa na Web, às vezes conhecida como um aplicativo da Web progressivo

Os aplicativos da Web progressivos (PWAs) são mais uma metodologia que envolve uma combinação de tecnologias para criar aplicativos da Web poderosos. Com uma experiência de usuário aprimorada, as pessoas passarão mais tempo em sites e verão mais anúncios. Eles tendem a comprar mais e, com atualizações de notificação, são mais propensos a visitar com frequência. O Financial Times abandonou seus aplicativos nativos em 2011 e construiu um aplicativo da web usando as melhores tecnologias disponíveis na época. Agora, o produto se tornou um PWA completo.

Mas por que, depois de todo esse tempo, você criaria um aplicativo da Web quando um aplicativo nativo faz o trabalho bem o suficiente?

Vejamos algumas das métricas compartilhadas no Google IO 17.

Mais depois do salto! Continue lendo abaixo ↓

Cinco bilhões de dispositivos estão conectados à web, tornando a web a maior plataforma da história da computação. Na web móvel, 11,4 milhões de visitantes únicos mensais vão para as 1.000 principais propriedades da web e 4 milhões vão para os mil principais aplicativos. A web móvel reúne cerca de quatro vezes mais usuários do que os aplicativos nativos. Mas esse número cai drasticamente quando se trata de engajamento.

Um usuário gasta em média 188,6 minutos em aplicativos nativos e apenas 9,3 minutos na web móvel. Os aplicativos nativos aproveitam o poder dos sistemas operacionais para enviar notificações push para fornecer aos usuários atualizações importantes. Eles oferecem uma melhor experiência ao usuário e inicializam mais rapidamente do que sites em um navegador. Em vez de digitar uma URL no navegador da web, os usuários só precisam tocar no ícone de um aplicativo na tela inicial.

É improvável que a maioria dos visitantes na web volte, então os desenvolvedores criaram a solução alternativa de mostrar a eles banners para instalar aplicativos nativos, na tentativa de mantê-los profundamente engajados. Mas então, os usuários teriam que passar pelo processo cansativo de instalar o binário de um aplicativo nativo. Forçar os usuários a instalar um aplicativo é irritante e reduz ainda mais a chance de que eles o instalem em primeiro lugar. A oportunidade para a web é clara.

Leitura recomendada : Native And PWA: Choices, Not Challengers!

Se os aplicativos da web vierem com uma rica experiência do usuário, notificações push, suporte offline e carregamento instantâneo, eles podem conquistar o mundo. Isso é o que um aplicativo da Web progressivo faz.

Um PWA oferece uma experiência de usuário rica porque possui vários pontos fortes:

  • Rápido
    A interface do usuário não é esquisita. A rolagem é suave. E o aplicativo responde rapidamente à interação do usuário.

  • De confiança
    Um site normal força os usuários a esperar, sem fazer nada, enquanto está ocupado fazendo viagens para o servidor. Enquanto isso, um PWA carrega dados instantaneamente do cache. Um PWA funciona perfeitamente, mesmo em uma conexão 2G. Cada solicitação de rede para buscar um ativo ou parte de dados passa por um service worker (mais sobre isso posteriormente), que primeiro verifica se a resposta para uma solicitação específica já está no cache. Quando os usuários obtêm conteúdo real quase instantaneamente, mesmo com uma conexão ruim, eles confiam mais no aplicativo e o consideram mais confiável.

  • Noivando
    Um PWA pode ganhar um lugar na tela inicial do usuário. Ele oferece uma experiência nativa semelhante a um aplicativo, fornecendo uma área de trabalho em tela cheia. Ele faz uso de notificações push para manter os usuários envolvidos.

Agora que sabemos o que os PWAs trazem para a mesa, vamos entrar nos detalhes do que dá aos PWAs uma vantagem sobre os aplicativos nativos. Os PWAs são criados com tecnologias como service workers, manifestos de aplicativos da Web, notificações push e estrutura de dados IndexedDB/local para armazenamento em cache. Vejamos cada um em detalhes.

Trabalhadores de serviço

Um service worker é um arquivo JavaScript executado em segundo plano sem interferir nas interações do usuário. Todas as solicitações GET para o servidor passam por um service worker. Ele age como um proxy do lado do cliente. Ao interceptar solicitações de rede, ele assume controle total sobre a resposta que está sendo enviada de volta ao cliente. Um PWA é carregado instantaneamente porque os service workers eliminam a dependência da rede respondendo com dados do cache.

Um service worker só pode interceptar uma solicitação de rede que esteja em seu escopo. Por exemplo, um service worker com escopo raiz pode interceptar todas as solicitações de busca provenientes de uma página da Web. Um service worker opera como um sistema orientado a eventos. Ele entra em um estado inativo quando não é necessário, conservando assim a memória. Para usar um service worker em uma aplicação web, primeiro temos que registrá-lo na página com JavaScript.

 (function main () { /* navigator is a WEB API that allows scripts to register themselves and carry out their activities. */ if ('serviceWorker' in navigator) { console.log('Service Worker is supported in your browser') /* register method takes in the path of service worker file and returns a promises, which returns the registration object */ navigator.serviceWorker.register('./service-worker.js').then (registration => { console.log('Service Worker is registered!') }) } else { console.log('Service Worker is not supported in your browser') } })()

Primeiro verificamos se o navegador suporta service workers. Para registrar um service worker em uma aplicação web, fornecemos sua URL como parâmetro para a função register , disponível em navigator.serviceWorker ( navigator é uma API web que permite que scripts se registrem e realizem suas atividades). Um service worker é registrado apenas uma vez. O registro não acontece a cada carregamento de página. O navegador faz download do arquivo do service worker ( ./service-worker.js ) somente se houver uma diferença de bytes entre o service worker ativado existente e o mais recente ou se sua URL tiver sido alterada.

O service worker acima interceptará todas as solicitações provenientes da raiz ( / ). Para limitar o escopo de um service worker, passaríamos um parâmetro opcional com uma das chaves como escopo.

 if ('serviceWorker' in navigator) { /* register method takes in an optional second parameter as an object. To restrict the scope of a service worker, the scope should be provided. scope: '/books' will intercept requests with '/books' in the url. */ navigator.serviceWorker.register('./service-worker.js', { scope: '/books' }).then(registration => { console.log('Service Worker for scope /books is registered', registration) }) }

O service worker acima interceptará solicitações que tenham /books na URL. Por exemplo, ele não interceptará solicitações com /products , mas poderia muito bem interceptar solicitações com /books/products .

Como mencionado, um service worker opera como um sistema orientado a eventos. Ele escuta eventos (instalar, ativar, buscar, enviar) e, consequentemente, chama o respectivo manipulador de eventos. Alguns desses eventos fazem parte do ciclo de vida de um service worker, que passa por esses eventos em sequência para ser ativado.

Instalação

Depois que um service worker é registrado com sucesso, um evento de instalação é acionado. Este é um bom lugar para fazer o trabalho de inicialização, como configurar o cache ou criar armazenamentos de objetos no IndexedDB. (IndexedDB fará mais sentido para você quando entrarmos em seus detalhes. Por enquanto, podemos apenas dizer que é uma estrutura de par chave-valor.)

 self.addEventListener('install', (event) => { let CACHE_NAME = 'xyz-cache' let urlsToCache = [ '/', '/styles/main.css', '/scripts/bundle.js' ] event.waitUntil( /* open method available on caches, takes in the name of cache as the first parameter. It returns a promise that resolves to the instance of cache All the URLS above can be added to cache using the addAll method. */ caches.open(CACHE_NAME) .then (cache => cache.addAll(urlsToCache)) ) })

Aqui, estamos armazenando em cache alguns dos arquivos para que o próximo carregamento seja instantâneo. self refere-se à instância do service worker. event.waitUntil faz o service worker esperar até que todo o código dentro dele termine a execução.

Ativação

Depois que um service worker é instalado, ele ainda não pode atender solicitações de busca. Em vez disso, um evento de activate é acionado. Se nenhum service worker ativo estiver operando no site no mesmo escopo, o service worker instalado será ativado imediatamente. No entanto, se um site já tiver um service worker ativo, a ativação de um novo service worker será adiada até que todas as guias que operam no antigo service worker sejam fechadas. Isso faz sentido porque o antigo service worker pode estar usando a instância do cache que agora foi modificada na mais nova. Portanto, a etapa de ativação é um bom lugar para se livrar de caches antigos.

 self.addEventListener('activate', (event) => { let cacheWhitelist = ['products-v2'] // products-v2 is the name of the new cache event.waitUntil( caches.keys().then (cacheNames => { return Promise.all( cacheNames.map( cacheName => { /* Deleting all the caches except the ones that are in cacheWhitelist array */ if (cacheWhitelist.indexOf(cacheName) === -1) { return caches.delete(cacheName) } }) ) }) ) })

No código acima, estamos excluindo o cache antigo. Se o nome de um cache não corresponder ao cacheWhitelist , ele será excluído. Para pular a fase de espera e ativar imediatamente o service worker, usamos skip.waiting() .

 self.addEventListener('activate', (event) => { self.skipWaiting() // The usual stuff })

Depois que o service worker é ativado, ele pode ouvir solicitações de busca e eventos de push.

Buscar Manipulador de Eventos

Sempre que uma página da Web dispara uma solicitação de busca para um recurso na rede, o evento de busca do service worker é chamado. O manipulador de eventos fetch primeiro procura o recurso solicitado no cache. Se estiver presente no cache, ele retornará a resposta com o recurso armazenado em cache. Caso contrário, ele inicia uma solicitação de busca para o servidor e, quando o servidor envia de volta a resposta com o recurso solicitado, ele a coloca no cache para solicitações subsequentes.

 /* Fetch event handler for responding to GET requests with the cached assets */ self.addEventListener('fetch', (event) => { event.respondWith( caches.open('products-v2') .then (cache => { /* Checking if the request is already present in the cache. If it is present, sending it directly to the client */ return cache.match(event.request).then (response => { if (response) { console.log('Cache hit! Fetching response from cache', event.request.url) return response } /* If the request is not present in the cache, we fetch it from the server and then put it in cache for subsequent requests. */ fetch(event.request).then (response => { cache.put(event.request, response.clone()) return response }) }) }) ) })

event.respondWith permite que o service worker envie uma resposta personalizada ao cliente.

O primeiro off-line agora é uma coisa. Para qualquer solicitação não crítica, devemos servir a resposta do cache, em vez de fazer uma carona até o servidor. Se algum ativo não estiver presente no cache, nós o pegamos do servidor e o armazenamos em cache para solicitações subsequentes.

Os service workers só funcionam em sites HTTPS porque têm o poder de manipular a resposta de qualquer solicitação de busca. Alguém com intenção maliciosa pode adulterar a resposta de uma solicitação em um site HTTP. Portanto, hospedar um PWA em HTTPS é obrigatório. Os service workers não interrompem o funcionamento normal do DOM. Eles não podem se comunicar diretamente com a página da web. Para enviar qualquer mensagem para uma página da web, ele faz uso de mensagens de postagem.

Notificações Web Push

Vamos supor que você está ocupado jogando um jogo no seu celular e uma notificação aparece informando sobre um desconto de 30% em sua marca favorita. Sem mais delongas, você clica na notificação e compra sem fôlego. Obter atualizações ao vivo sobre, digamos, uma partida de críquete ou futebol ou receber e-mails e lembretes importantes como notificações é um grande negócio quando se trata de envolver os usuários com um produto. Esse recurso estava disponível apenas em aplicativos nativos até o surgimento do PWA. Um PWA usa notificações push da Web para competir com esse poderoso recurso que os aplicativos nativos fornecem imediatamente. Um usuário ainda receberia uma notificação por push da Web mesmo que o PWA não estivesse aberto em nenhuma das guias do navegador e mesmo que o navegador não estivesse aberto.

Um aplicativo da web precisa pedir permissão ao usuário para enviar notificações push.

Prompt do navegador para pedir permissão para notificações Web Push
Prompt do navegador para pedir permissão para notificações Web Push. (Visualização grande)

Assim que o usuário confirmar clicando no botão “Permitir”, um token de assinatura exclusivo é gerado pelo navegador. Este token é exclusivo para este dispositivo. O formato do token de assinatura gerado pelo Chrome é o seguinte:

 { "endpoint": "https://fcm.googleapis.com/fcm/send/c7Veb8VpyM0:APA91bGnMFx8GIxf__UVy6vJ-n9i728CUJSR1UHBPAKOCE_SrwgyP2N8jL4MBXf8NxIqW6NCCBg01u8c5fcY0kIZvxpDjSBA75sVz64OocQ-DisAWoW7PpTge3SwvQAx5zl_45aAXuvS", "expirationTime": null, "keys": { "p256dh": "BJsj63kz8RPZe8Lv1uu-6VSzT12RjxtWyWCzfa18RZ0-8sc5j80pmSF1YXAj0HnnrkyIimRgLo8ohhkzNA7lX4w", "auth": "TJXqKozSJxcWvtQasEUZpQ" } }

O endpoint contido no token acima será exclusivo para cada assinatura. Em um site comum, milhares de usuários concordariam em receber notificações push e, para cada um deles, esse endpoint seria único. Assim, com a ajuda desse endpoint , o aplicativo pode direcionar esses usuários no futuro enviando notificações push. O expirationTime é a quantidade de tempo que a assinatura é válida para um determinado dispositivo. Se o expirationTime for 20 dias, significa que a assinatura push do usuário expirará após 20 dias e o usuário não poderá receber notificações push na assinatura mais antiga. Nesse caso, o navegador gerará um novo token de assinatura para esse dispositivo. As chaves auth e p256dh são usadas para criptografia.

Agora, para enviar notificações push para esses milhares de usuários no futuro, primeiro precisamos salvar seus respectivos tokens de assinatura. É o trabalho do servidor de aplicativos (o servidor back-end, talvez um script Node.js) enviar notificações push para esses usuários. Isso pode parecer tão simples quanto fazer uma solicitação POST para a URL do endpoint com os dados de notificação na carga útil da solicitação. No entanto, deve-se observar que, se um usuário não estiver online quando uma notificação por push destinada a ele for acionada pelo servidor, ele ainda deverá receber essa notificação assim que voltar a ficar online. O servidor teria que cuidar desses cenários, além de enviar milhares de solicitações aos usuários. Um servidor que acompanha a conexão do usuário parece complicado. Então, algo no meio seria responsável por rotear as notificações push da web do servidor para o cliente. Isso é chamado de serviço push e cada navegador tem sua própria implementação de um serviço push. O navegador deve informar as seguintes informações ao serviço push para enviar qualquer notificação:

  1. A hora de viver
    Este é o tempo que uma mensagem deve ficar na fila, caso não seja entregue ao usuário. Após esse tempo, a mensagem será removida da fila.
  2. Urgência da mensagem
    Isso é para que o serviço push preserve a bateria do usuário enviando apenas mensagens de alta prioridade.

O serviço push roteia as mensagens para o cliente. Como o push deve ser recebido pelo cliente, mesmo que seu respectivo aplicativo da Web não esteja aberto no navegador, os eventos de push precisam ser ouvidos por algo que monitora continuamente em segundo plano. Você adivinhou: esse é o trabalho do service worker. O service worker escuta eventos push e faz o trabalho de mostrar notificações ao usuário.

Então, agora sabemos que o navegador, o serviço push, o service worker e o servidor de aplicativos trabalham em harmonia para enviar notificações push para o usuário. Vejamos os detalhes de implementação.

Cliente Web Push

Pedir permissão ao usuário é uma coisa única. Se um usuário já concedeu permissão para receber notificações push, não devemos perguntar novamente. O valor da permissão é salvo em Notification.permission .

 /* Notification.permission can have one of these three values: default, granted or denied. */ if (Notification.permission === 'default') { /* The Notification.requestPermission() method shows a notification permission prompt to the user. It returns a promise that resolves to the value of permission*/ Notification.requestPermission().then (result => { if (result === 'denied') { console.log('Permission denied') return } if (result === 'granted') { console.log('Permission granted') /* This means the user has clicked the Allow button. We're to get the subscription token generated by the browser and store it in our database. The subscription token can be fetched using the getSubscription method available on pushManager of the serviceWorkerRegistration object. If subscription is not available, we subscribe using the subscribe method available on pushManager. The subscribe method takes in an object. */ serviceWorkerRegistration.pushManager.getSubscription() .then (subscription => { if (!subscription) { const applicationServerKey = ' ' serviceWorkerRegistration.pushManager.subscribe({ userVisibleOnly: true, // All push notifications from server should be displayed to the user applicationServerKey // VAPID Public key }) } else { saveSubscriptionInDB(subscription, userId) // A method to save subscription token in the database } }) } }) } /* Notification.permission can have one of these three values: default, granted or denied. */ if (Notification.permission === 'default') { /* The Notification.requestPermission() method shows a notification permission prompt to the user. It returns a promise that resolves to the value of permission*/ Notification.requestPermission().then (result => { if (result === 'denied') { console.log('Permission denied') return } if (result === 'granted') { console.log('Permission granted') /* This means the user has clicked the Allow button. We're to get the subscription token generated by the browser and store it in our database. The subscription token can be fetched using the getSubscription method available on pushManager of the serviceWorkerRegistration object. If subscription is not available, we subscribe using the subscribe method available on pushManager. The subscribe method takes in an object. */ serviceWorkerRegistration.pushManager.getSubscription() .then (subscription => { if (!subscription) { const applicationServerKey = ' ' serviceWorkerRegistration.pushManager.subscribe({ userVisibleOnly: true, // All push notifications from server should be displayed to the user applicationServerKey // VAPID Public key }) } else { saveSubscriptionInDB(subscription, userId) // A method to save subscription token in the database } }) } }) }

No método de subscribe acima, estamos passando userVisibleOnly e applicationServerKey para gerar um token de assinatura. A propriedade userVisibleOnly deve sempre ser verdadeira porque informa ao navegador que qualquer notificação push enviada pelo servidor será mostrada ao cliente. Para entender o propósito de applicationServerKey , vamos considerar um cenário.

Se alguma pessoa obtiver seus milhares de tokens de assinatura, ela poderá muito bem enviar notificações para os terminais contidos nessas assinaturas. Não há como o endpoint ser vinculado à sua identidade exclusiva. Para fornecer uma identidade exclusiva aos tokens de assinatura gerados em seu aplicativo da web, usamos o protocolo VAPID. Com o VAPID, o servidor de aplicativos se identifica voluntariamente para o serviço push enquanto envia notificações push. Geramos duas chaves assim:

 const webpush = require('web-push') const vapidKeys = webpush.generateVAPIDKeys()

web-push é um módulo npm. vapidKeys terá uma chave pública e uma chave privada. A chave do servidor de aplicativos usada acima é a chave pública.

Servidor Web Push

O trabalho do servidor web push (servidor de aplicativos) é simples. Ele envia uma carga de notificação para os tokens de assinatura.

 const options = { TTL: 24*60*60, //TTL is the time to live, the time that the notification will be queued in the push service vapidDetails: { subject: '[email protected]', publicKey: ' ', privateKey: ' ' } } const data = { title: 'Update', body: 'Notification sent by the server' } webpush.sendNotification(subscription, data, options) const options = { TTL: 24*60*60, //TTL is the time to live, the time that the notification will be queued in the push service vapidDetails: { subject: '[email protected]', publicKey: ' ', privateKey: ' ' } } const data = { title: 'Update', body: 'Notification sent by the server' } webpush.sendNotification(subscription, data, options) const options = { TTL: 24*60*60, //TTL is the time to live, the time that the notification will be queued in the push service vapidDetails: { subject: '[email protected]', publicKey: ' ', privateKey: ' ' } } const data = { title: 'Update', body: 'Notification sent by the server' } webpush.sendNotification(subscription, data, options)

Ele usa o método sendNotification da biblioteca de push da web.

Trabalhadores de serviço

O service worker mostra a notificação ao usuário da seguinte forma:

 self.addEventListener('push', (event) => { let options = { body: event.data.body, icon: 'images/example.png', } event.waitUntil( /* The showNotification method is available on the registration object of the service worker. The first parameter to showNotification method is the title of notification, and the second parameter is an object */ self.registration.showNotification(event.data.title, options) ) })

Até agora, vimos como um service worker usa o cache para armazenar solicitações e torna um PWA rápido e confiável, e vimos como as notificações push da Web mantêm os usuários envolvidos.

Para armazenar um monte de dados no lado do cliente para suporte offline, precisamos de uma estrutura de dados gigante. Vamos dar uma olhada no PWA do Financial Times. Você tem que testemunhar o poder dessa estrutura de dados por si mesmo. Carregue a URL em seu navegador e desligue sua conexão com a internet. Recarregue a página. Nossa! Ainda está funcionando? Isto é. (Como eu disse, offline é o novo preto.) Os dados não estão vindo dos fios. Está sendo servido da casa. Vá para a guia "Aplicativos" das Ferramentas do desenvolvedor do Chrome. Em “Armazenamento”, você encontrará “IndexedDB”.

IndexedDB armazena os dados dos artigos no Financial Times PWA
IndexedDB no Financial Times PWA. (Visualização grande)

Confira a loja de objetos "Artigos" e expanda qualquer um dos itens para ver a mágica por si mesmo. O Financial Times armazenou esses dados para suporte offline. Essa estrutura de dados que nos permite armazenar uma grande quantidade de dados é chamada IndexedDB. IndexedDB é um banco de dados orientado a objetos baseado em JavaScript para armazenar dados estruturados. Podemos criar diferentes armazenamentos de objetos neste banco de dados para vários propósitos. Por exemplo, como podemos ver na imagem acima que “Resources”, “ArticleImages” e “Articles” são chamados de armazenamentos de objetos. Cada registro em um armazenamento de objetos é identificado exclusivamente com uma chave. IndexedDB pode até ser usado para armazenar arquivos e blobs.

Vamos tentar entender IndexedDB criando um banco de dados para armazenar livros.

 let openIdbRequest = window.indexedDB.open('booksdb', 1)

Se o banco de dados booksdb ainda não existir, o código acima criará um banco de dados booksdb . O segundo parâmetro para o método aberto é a versão do banco de dados. A especificação de uma versão cuida das alterações relacionadas ao esquema que podem ocorrer no futuro. Por exemplo, booksdb agora tem apenas uma tabela, mas quando a aplicação crescer, pretendemos adicionar mais duas tabelas a ela. Para garantir que nosso banco de dados esteja sincronizado com o esquema atualizado, especificaremos uma versão superior à anterior.

Chamar o método open não abre o banco de dados imediatamente. É uma solicitação assíncrona que retorna um objeto IDBOpenDBRequest . Este objeto possui propriedades de sucesso e erro; teremos que escrever manipuladores apropriados para essas propriedades para gerenciar o estado de nossa conexão.

 let dbInstance openIdbRequest.onsuccess = (event) => { dbInstance = event.target.result console.log('booksdb is opened successfully') } openIdbRequest.onerror = (event) => { console.log('There was an error in opening booksdb database') } openIdbRequest.onupgradeneeded = (event) => { let db = event.target.result let objectstore = db.createObjectStore('books', { keyPath: 'id' }) }

Para gerenciar a criação ou modificação de armazenamentos de objetos (armazenamentos de objetos são análogos a tabelas baseadas em SQL — eles têm uma estrutura de valor-chave), o método onupgradeneeded é chamado no objeto openIdbRequest . O método onupgradeneeded será invocado sempre que a versão for alterada. No snippet de código acima, estamos criando uma loja de objetos de livros com uma chave exclusiva como ID.

Digamos que, após a implantação desse código, tenhamos que criar mais um armazenamento de objetos, chamado de users . Então, agora a versão do nosso banco de dados será 2 .

 let openIdbRequest = window.indexedDB.open('booksdb', 2) // New Version - 2 /* Success and error event handlers remain the same. The onupgradeneeded method gets called when the version of the database changes. */ openIdbRequest.onupgradeneeded = (event) => { let db = event.target.result if (!db.objectStoreNames.contains('books')) { let objectstore = db.createObjectStore('books', { keyPath: 'id' }) } let oldVersion = event.oldVersion let newVersion = event.newVersion /* The users tables should be added for version 2. If the existing version is 1, it will be upgraded to 2, and the users object store will be created. */ if (oldVersion === 1) { db.createObjectStore('users', { keyPath: 'id' }) } }

dbInstance em cache no manipulador de eventos de sucesso da solicitação aberta. Para recuperar ou adicionar dados no IndexedDB, usaremos dbInstance . Vamos adicionar alguns registros de livros em nossa loja de objetos de livros.

 let transaction = dbInstance.transaction('books') let objectstore = transaction.objectstore('books') let bookRecord = { id: '1', name: 'The Alchemist', author: 'Paulo Coelho' } let addBookRequest = objectstore.add(bookRecord) addBookRequest.onsuccess = (event) => { console.log('Book record added successfully') } addBookRequest.onerror = (event) => { console.log('There was an error in adding book record') }

Fazemos uso de transactions , especialmente ao gravar registros em armazenamentos de objetos. Uma transação é simplesmente um wrapper em torno de uma operação para garantir a integridade dos dados. Se alguma das ações em uma transação falhar, nenhuma ação será executada no banco de dados.

Vamos modificar um registro de livro com o método put :

 let modifyBookRequest = objectstore.put(bookRecord) // put method takes in an object as the parameter modifyBookRequest.onsuccess = (event) => { console.log('Book record updated successfully') }

Vamos recuperar um registro de livro com o método get :

 let transaction = dbInstance.transaction('books') let objectstore = transaction.objectstore('books') /* get method takes in the id of the record */ let getBookRequest = objectstore.get(1) getBookRequest.onsuccess = (event) => { /* event.target.result contains the matched record */ console.log('Book record', event.target.result) } getBookRequest.onerror = (event) => { console.log('Error while retrieving the book record.') }

Adicionando ícone na tela inicial

Agora que quase não há distinção entre um PWA e um aplicativo nativo, faz sentido oferecer uma posição privilegiada ao PWA. Se o seu site atende aos critérios básicos de um PWA (hospedado em HTTPS, integra-se com service workers e possui um manifest.json ) e após o usuário ter passado algum tempo na página da web, o navegador invocará um prompt na parte inferior, perguntando que o usuário adicione o aplicativo à tela inicial, conforme mostrado abaixo:

Solicitar a adição do Financial Times PWA na tela inicial
Solicitar a adição do Financial Times PWA na tela inicial. (Visualização grande)

Quando um usuário clica em “Adicionar FT à tela inicial”, o PWA consegue colocar o pé na tela inicial, bem como na gaveta de aplicativos. Quando um usuário pesquisa qualquer aplicativo em seu telefone, todos os PWAs que correspondem à consulta de pesquisa serão listados. Eles também serão vistos nas configurações do sistema, o que torna mais fácil para os usuários gerenciá-los. Nesse sentido, um PWA se comporta como um aplicativo nativo.

Os PWAs usam manifest.json para fornecer esse recurso. Vamos examinar um arquivo manifest.json simples.

 { "name": "Demo PWA", "short_name": "Demo", "start_url": "/?standalone", "background_color": "#9F0C3F", "theme_color": "#fff1e0", "display": "standalone", "icons": [{ "src": "/lib/img/icons/xxhdpi.png?v2", "sizes": "192x192" }] }

O short_name aparece na tela inicial do usuário e nas configurações do sistema. O name aparece no prompt do Chrome e na tela inicial. A tela inicial é o que o usuário vê quando o aplicativo está se preparando para iniciar. O start_url é a tela principal do seu aplicativo. É o que os usuários obtêm quando tocam em um ícone na tela inicial. O background_color é usado na tela inicial. O theme_color define a cor da barra de ferramentas. O valor standalone para o modo de display diz que o aplicativo deve ser operado no modo de tela cheia (ocultando a barra de ferramentas do navegador). Quando um usuário instala um PWA, seu tamanho é meramente em kilobytes, em vez dos megabytes de aplicativos nativos.

Service workers, notificações push da Web, IndexedDB e a posição da tela inicial compensam o suporte offline, a confiabilidade e o engajamento. Deve-se notar que um service worker não ganha vida e começa a fazer seu trabalho na primeira carga. O primeiro carregamento ainda será lento até que todos os ativos estáticos e outros recursos tenham sido armazenados em cache. Podemos implementar algumas estratégias para otimizar o primeiro carregamento.

Agrupamento de ativos

Todos os recursos, incluindo HTML, folhas de estilo, imagens e JavaScript, devem ser buscados no servidor. Quanto mais arquivos, mais solicitações HTTPS são necessárias para buscá-los. Podemos usar empacotadores como o WebPack para agrupar nossos ativos estáticos, reduzindo assim o número de solicitações HTTP para o servidor. O WebPack faz um ótimo trabalho ao otimizar ainda mais o pacote usando técnicas como divisão de código (por exemplo, agrupando apenas os arquivos necessários para o carregamento da página atual, em vez de agrupar todos eles) e agitação de árvore (por exemplo, removendo dependências duplicadas ou dependências que são importadas, mas não usadas no código).

Redução de viagens de ida e volta

Uma das principais razões para a lentidão na web é a latência da rede. O tempo que leva para um byte viajar de A para B varia com a conexão de rede. Por exemplo, uma viagem de ida e volta específica por Wi-Fi leva 50 milissegundos e 500 milissegundos em uma conexão 3G, mas 2500 milissegundos em uma conexão 2G. Essas solicitações são enviadas usando o protocolo HTTP, o que significa que, enquanto uma determinada conexão está sendo usada para uma solicitação, ela não pode ser usada para outras solicitações até que a resposta da solicitação anterior seja atendida. Um site pode fazer seis solicitações HTTP assíncronas por vez porque seis conexões estão disponíveis para um site fazer solicitações HTTP. Um site médio faz cerca de 100 solicitações; assim, com um máximo de seis conexões disponíveis, um usuário pode acabar gastando cerca de 833 milissegundos em uma única viagem de ida e volta. (O cálculo é 833 milissegundos - 1006 = 1666 . Temos que dividir 1666 por 2 porque estamos calculando o tempo gasto em uma viagem de ida e volta.) Com HTTP2 em vigor, o tempo de resposta é drasticamente reduzido. O HTTP2 não bloqueia o cabeçote de conexão, portanto, várias solicitações podem ser enviadas simultaneamente.

A maioria das respostas HTTP contém cabeçalhos last-modified e etag . O cabeçalho da last-modified é a data em que o arquivo foi modificado pela última vez, e uma etag é um valor exclusivo com base no conteúdo do arquivo. Ele só será alterado quando o conteúdo de um arquivo for alterado. Ambos os cabeçalhos podem ser usados ​​para evitar o download do arquivo novamente se uma versão em cache já estiver disponível localmente. Se o navegador tiver uma versão desse arquivo disponível localmente, ele poderá adicionar qualquer um desses dois cabeçalhos na solicitação da seguinte forma:

Adicione cabeçalhos ETag e Last-Modified para impedir o download de ativos em cache válidos
ETag e cabeçalhos modificados pela última vez. (Visualização grande)

O servidor pode verificar se o conteúdo do arquivo foi alterado. If the contents of the file have not changed, then it responds with a status code of 304 ( not modified ).

If-None-Match Header to prevent downloading of valid cached assets
If-None-Match Header. (Visualização grande)

This indicates to the browser to use the locally available cached version of the file. By doing all of this, we've prevented the file from being downloaded.

Faster responses are in now place, but our job is not done yet. We still have to parse the HTML, load the style sheets and make the web page interactive. It makes sense to show some empty boxes with a loader to the user, instead of a blank screen. While the HTML document is getting parsed, when it comes across <script src='asset.js'></script> , it will make a synchronous HTTP request to the server to fetch asset.js , and the whole parsing process will be paused until the response comes back. Imagine having a dozen of synchronous static asset references. These could very well be managed just by making use of the async keyword in script references, like <script src='asset.js' async></script> . With the introduction of the async keyword here, the browser will make an asynchronous request to fetch asset.js without hindering the parsing of the HTML. If a script file is required at a later stage, we can defer the downloading of that file until the entire HTML has been parsed. A script file can be deferred by using the defer keyword, like <script src='asset.js' defer></script> .

Conclusão

We've learned a lot of many new things that make for a cool web application. Here's a summary of all of the things we've explored in this article:

  1. Service workers make good use of the cache to speed up the loading of assets.
  2. Web push notifications work under the hood.
  3. We use IndexedDB to store a massive amount of data.
  4. Some of the optimizations for instant first load, like using HTTP2 and adding headers like Etag , last-modified and If-None-Match , prevent the downloading of valid cached assets.

That's all, folks!