Criando um PWA com Blazor WASM
Um PWA com Blazor permite entregar aplicações web com sensação de app nativo, usando C# no lugar de JavaScript. PWA, ou Progressive Web App, é um conjunto de práticas para criar apps web mais responsivos e competitivos com o mercado mobile, usando HTML, CSS e JavaScript ou, neste caso, Blazor.
O Blazor é um framework web criado e mantido pela Microsoft. Com ele, criamos diferentes tipos de aplicações web, entre elas SPAs com suporte a WebAssembly (WASM) e PWA.
O que é um PWA
Como vimos, PWAs são aplicações web com sensação nativa. Mas o que de fato define essa sensação, e por que devemos nos preocupar com ela?
Com a invasão dos dispositivos móveis e a popularização das aplicações mobile, surgiu a questão da navegação e da usabilidade nesses aparelhos. Por isso, houve uma fase inicial em que se falava muito de design responsivo, algo que hoje é obrigatório.
Se você quer reforçar a base de layout antes de seguir, vale conferir uma indicação de conteúdo:
O curso Crie Sites Responsivos, com Henrique Giaretta, é gratuito. Ele cobre justamente a base de layout que todo PWA precisa.
De qualquer forma, precisamos adaptar nossas aplicações para dispositivos móveis, entre outros. E o PWA nos ajuda exatamente nisso.
Pilares do PWA
O primeiro pilar é o design responsivo. Embora não esteja na especificação oficial, ter um layout que se adapta a qualquer tela é indispensável para qualquer aplicação hoje em dia.
O segundo é o manifest, um arquivo JSON na raiz do site. Ele descreve comportamentos como cores do tema, ícone, nome da aplicação e até orientação da tela.
A partir desse arquivo, conseguimos instalar nossos apps web nos dispositivos, como se fossem nativos. A especificação completa está no MDN, em Web App Manifest.
O terceiro é o service worker. Uma das capacidades especiais das aplicações nativas é rodar em segundo plano, e os service workers permitem isso e muito mais.
Com eles, podemos cachear arquivos, trabalhar offline e até acessar outros recursos do dispositivo. Inclusive, alguns desses recursos funcionam com o app em segundo plano.
O quarto é trabalhar offline. Por meio dos service workers, sua aplicação deve ser capaz de abrir sem depender da internet.
Com esses quatro pilares implementados, você já tem um PWA, inclusive um PWA com Blazor. Além disso, isso independe de framework: pode ser PHP, ASP.NET, Java, Node ou qualquer outra tecnologia web.
Vantagens e desvantagens do PWA
Entre as vantagens, estão:
- Um único codebase.
- Um único deployment.
- Nenhuma dependência de aprovação nas lojas.
- Reaproveitamento do conhecimento web que o time já tem.
Por outro lado, há desvantagens:
- Recursos reduzidos, já que o PWA não tem tudo o que o nativo oferece.
- O app fica de fora do marketing das lojas.
O que é o Blazor
O Blazor é um framework web criado e mantido pela própria Microsoft. Com ele, criamos aplicações web modernas e muito performáticas.
Atualmente, ele é a recomendação da Microsoft para frontends .NET e, portanto, para quem quer um PWA com Blazor, no lugar da antiga recomendação do Razor Pages. Para uma visão mais ampla, leia também uma visão geral do Blazor.
O Blazor usa o Razor, o motor de renderização que permite interpolar C# com HTML. É através do Razor que fazemos um foreach no HTML, algo impossível no HTML puro, já que ele é uma linguagem de marcação e não de programação.
<ul>
@foreach (var item in items)
{
<li>@item</li>
}
</ul>
Por fim, o Blazor roda em cima do ASP.NET, o framework web principal da Microsoft para qualquer aplicação web. Assim, o ecossistema web dentro do .NET fica organizado como na imagem abaixo.
Sabores do Blazor
O Blazor se divide em diversos sabores. Eles permitem criar aplicações web estáticas (sem comportamento), server side (Blazor Server) ou client side (Blazor WASM).
Blazor Server
É a versão do Blazor que roda somente no servidor. Ela fica conectada ao cliente por um WebSocket, que no .NET é implementado pelo SignalR.
Blazor Auto
É a versão do Blazor que permite misturar Blazor Server e Blazor WASM no mesmo projeto. Os detalhes de cada modo estão no artigo sobre render modes do Blazor.
Quer conhecer mais sobre Blazor? O eBook que escrevi há alguns anos traz uma introdução com o .NET 8, então confira o eBook sobre Blazor.
WebAssembly
WebAssembly é a possibilidade de rodar um binário em uma máquina virtual diretamente no navegador. Com isso, linguagens como C# e Rust, que possuem target para WebAssembly, podem substituir o JavaScript.
Ainda assim, um PWA com Blazor roda no navegador e não deixa de ser HTML e CSS. Inclusive, ele interopera com JavaScript sempre que necessário.
Para quem vai construir um PWA com Blazor, na plataforma também temos a gravação de um encontro premium sobre os fundamentos do WebAssembly. Vale a pena para quem quer ir mais fundo no tema.
Blazor WebAssembly e PWA com Blazor
Blazor WebAssembly, ou Blazor WASM para os mais íntimos, é a implementação do WebAssembly no .NET para a web. Por isso, ele é a base natural para um PWA com Blazor.
Como resultado da compilação, temos arquivos .wasm que contêm a aplicação. Vale destacar que esses arquivos não são JavaScript traduzido do C#, e sim binários que executam no browser.
Isso traz vários benefícios, como usar C# no lugar do JavaScript. Mas o principal deles é a performance.
Importante: não é porque o WASM é muito performático que ele deve ser sempre a única escolha. Ele tem cenários em que se destaca e outros em que não se encaixa.
Aproveitando, reunimos todo o nosso conteúdo de Blazor em um só lugar. Confira a carreira ASP.NET / Blazor Developer no balta.io.
Como criar um PWA com Blazor WASM
Chega de conceito e vamos para a prática. Agora, vamos criar um PWA com Blazor do zero e instalá-lo pelo navegador.
Criando o projeto
Vamos começar criando um projeto do tipo blazorwasm, que traz tudo o que precisamos para começar:
dotnet new blazorwasm -o BlazorPwaSample
Note que, dentro da pasta wwwroot, o template trouxe alguns arquivos estáticos. É nessa pasta que vamos trabalhar.
Vale saber que o template também aceita a opção --pwa, que já gera manifest e service workers prontos. Neste artigo, porém, vamos montar cada peça à mão para entender o papel de cada uma.
Importante: esses recursos também se aplicam ao ASP.NET MVC, Razor Pages, PHP e qualquer outro framework web.
Ícones do PWA com Blazor
Embora não seja obrigatório, é interessante criar um ícone para a aplicação. Afinal, vamos instalá-la no computador ou no telefone.
O projeto que criamos traz três imagens:
- favicon.png
- icon-192.png
- icon-512.png
Essas imagens são padrão e servem de base para o que queremos fazer. Então, sinta-se à vontade para editá-las.
Como resultado, as imagens já aparecem referenciadas no arquivo wwwroot/index.html:
<link rel="icon" type="image/png" href="favicon.png" />
...
<link rel="apple-touch-icon" sizes="512x512" href="icon-512.png" />
<link rel="apple-touch-icon" sizes="192x192" href="icon-192.png" />
Para gerar ícones em vários tamanhos e outros itens do PWA, uma boa ferramenta é o PWA Builder. Falaremos dele com mais calma em breve.
Criando o manifest do PWA com Blazor
O manifest é um JSON que descreve detalhes da aplicação, como cores do tema, título e ícone. A estrutura geral é esta:
{
"name": "YOUR APP NAME",
"short_name": "SHORT NAME SHOWN ON THE HOME SCREEN",
"id": "IDENTIFIER",
"start_url": "START URL",
"display": "DISPLAY MODE",
"background_color": "BACKGROUND COLOR",
"theme_color": "THEME COLOR",
"prefer_related_applications": false,
"icons": []
}
Aqui vai um exemplo de manifest preenchido corretamente e funcionando para o nosso PWA com Blazor:
{
"name": "Blazor PWA",
"short_name": "Blazor PWA",
"id": "./",
"start_url": "./",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#03173d",
"prefer_related_applications": false,
"icons": [
{
"src": "icon-512.png",
"type": "image/png",
"sizes": "512x512"
},
{
"src": "icon-192.png",
"type": "image/png",
"sizes": "192x192"
}
]
}
O valor standalone em display faz o app abrir em janela própria, sem a barra de endereço. Já prefer_related_applications como false diz ao navegador que não existe um app nativo preferido nas lojas.
Salve o arquivo com o nome manifest.webmanifest na pasta wwwroot. Em seguida, referencie-o na página index.html:
<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="css/app.css" />
<link href="BlazorPwaSample.styles.css" rel="stylesheet" />
<!-- referência ao manifest -->
<link href="manifest.webmanifest" rel="manifest" />
<link rel="icon" type="image/png" href="favicon.png" />
<link rel="apple-touch-icon" sizes="512x512" href="icon-512.png" />
<link rel="apple-touch-icon" sizes="192x192" href="icon-192.png" />
Execute a aplicação para ver o ícone no topo da página, como na imagem abaixo.
Se o ícone não aparecer, ou se você alterou o ícone e a mudança não refletiu, segure CTRL (CMD no Mac) e aperte F5. Outra opção é usar SHIFT + clique no botão atualizar do navegador.
Se mesmo assim o cache persistir, abra uma nova aba anônima com a mesma URL.
Inspecionando o manifest
No Chrome ou no Edge, aperte F12 para abrir o DevTools. Em seguida, vá até a aba Application e selecione Manifest para ver os detalhes do arquivo.
Service worker no PWA com Blazor
Os service workers adicionam comportamentos mais parecidos com os nativos aos nossos PWAs, incluindo cache e trabalho offline. Para começar, crie um arquivo chamado service-worker.js na pasta wwwroot:
self.addEventListener('fetch', () => { });
Esse código é bem simples e apenas escuta o evento fetch, ou seja, as requisições da página. Ele serve só como placeholder para testarmos o funcionamento do service worker.
Em seguida, referencie o arquivo no index.html:
<script src="_framework/blazor.webassembly.js"></script>
<!-- registro do service worker -->
<script>navigator.serviceWorker.register('service-worker.js');</script>
</body>
A documentação da Microsoft também sugere registrar com { updateViaCache: 'none' }. Assim, o navegador não usa versões cacheadas do próprio script do service worker.
Inspecionando o service worker
Assim como fizemos com o manifest, podemos inspecionar o service worker para saber se tudo funciona corretamente.
Cache no PWA com Blazor
Agora vamos implementar um service worker que cacheia os arquivos da aplicação. Vale destacar que, depois que esse script executa, ele mantém os arquivos em cache.
Isso significa que novas mudanças podem demorar para aparecer. Portanto, não recomendo usar este exemplo em desenvolvimento, apenas em produção.
self.importScripts('./service-worker-assets.js');
self.addEventListener('install', event => event.waitUntil(onInstall(event)));
self.addEventListener('activate', event => event.waitUntil(onActivate(event)));
self.addEventListener('fetch', event => event.respondWith(onFetch(event)));
const cacheNamePrefix = 'offline-cache-';
const cacheName = `${cacheNamePrefix}${self.assetsManifest.version}`;
const offlineAssetsInclude = [/\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/, /\.ico$/, /\.blat$/, /\.dat$/];
const offlineAssetsExclude = [/^service-worker\.js$/];
const base = "/";
const baseUrl = new URL(base, self.origin);
const manifestUrlList = self.assetsManifest.assets.map(asset => new URL(asset.url, baseUrl).href);
async function onInstall(event) {
console.info('Service worker: Install');
// baixa e guarda no cache os arquivos listados no manifest de assets
const assetsRequests = self.assetsManifest.assets
.filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url)))
.filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url)))
.map(asset => new Request(asset.url, { integrity: asset.hash, cache: 'no-cache' }));
await caches.open(cacheName).then(cache => cache.addAll(assetsRequests));
}
async function onActivate(event) {
console.info('Service worker: Activate');
// remove caches de versões anteriores
const cacheKeys = await caches.keys();
await Promise.all(cacheKeys
.filter(key => key.startsWith(cacheNamePrefix) && key !== cacheName)
.map(key => caches.delete(key)));
}
async function onFetch(event) {
let cachedResponse = null;
if (event.request.method === 'GET') {
// navegações devolvem o index.html do cache
const shouldServeIndexHtml = event.request.mode === 'navigate'
&& !manifestUrlList.some(url => url === event.request.url);
const request = shouldServeIndexHtml ? 'index.html' : event.request;
const cache = await caches.open(cacheName);
cachedResponse = await cache.match(request);
}
return cachedResponse || fetch(event.request);
}
O arquivo service-worker-assets.js é gerado pelo build e lista todos os recursos estáticos do app com seus hashes. Para isso, o projeto precisa da propriedade ServiceWorkerAssetsManifest no .csproj, que o template com --pwa já inclui.
O evento install baixa os arquivos, o activate limpa caches antigos e o fetch responde primeiro com o que está em cache. Essa é a estratégia cache first, a mesma do template oficial descrita na documentação de PWA do Blazor.
Substitua o conteúdo do service-worker.js anterior por este, pare todas as aplicações e execute novamente, de preferência em modo anônimo. Você não vai notar nada diferente na tela, mas algo muito legal aconteceu.
PWA com Blazor offline
Com o service worker criado e registrado, os arquivos principais do PWA com Blazor foram para o cache. Ou seja, depois de baixados, eles permanecem na máquina.
Isso melhora a performance e também permite navegar offline, isso mesmo, desconectado. Podemos testar esse comportamento no DevTools, seguindo os passos da imagem abaixo e apertando algumas vezes o botão atualizar.
Instalando o PWA com Blazor
Com os pilares do PWA implementados, podemos instalar o app localmente no Windows, no Mac e no Linux. Testei no Edge, no Chrome e no Safari, mas não nos outros navegadores.
Basta acessar a URL do seu PWA com Blazor, que pode ser o localhost. Se quiser testar um que eu fiz, acesse o PassPad.
Em seguida, o ícone de instalação aparece na barra de endereço com a mensagem "App available. Install".
Ao clicar no ícone, temos a opção de instalar o app, junto com algumas observações do navegador.
Depois da instalação, o app fica na pasta de aplicativos do navegador. Assim, você o acessa pelo File Explorer, pelo Finder ou pelas URLs edge://apps e chrome://apps.
Por fim, ao executar a aplicação, ela aparece na barra de tarefas, como se fosse nativa.
Inclusive, ela aparece no menu Iniciar do Windows e no Launchpad do Mac.
Legal, não é mesmo? No celular, o processo é parecido: no Safari do iOS, use Compartilhar e Adicionar à Tela de Início; no Chrome do Android, use o menu e Adicionar à tela inicial.
Cuidados com um PWA com Blazor em produção
Como o service worker adota cache first, o usuário pode ver uma versão antiga do app até a atualização em segundo plano terminar. Por isso, evite publicar mudanças que quebrem a compatibilidade com suas APIs.
Além disso, o service worker tenta cachear tudo o que está listado no manifest de assets. Portanto, não coloque arquivos muito grandes ou numerosos na pasta wwwroot sem filtrá-los na função onInstall.
Se o app usa autenticação, lembre que o usuário offline não consegue fazer login nem obter tokens. Então, desenhe as telas para funcionar de forma útil sem rede, ou para falhar de forma elegante.
Por fim, se algumas rotas precisam de HTML renderizado no servidor, ajuste a condição shouldServeIndexHtml. Caso contrário, o service worker devolve o index.html para todas as navegações.
Jornada PWA
Você está sabendo da Jornada PWA? Nela, você aprende a criar Progressive Web Apps, uma solução moderna, fácil de manter e com alcance em diversas lojas de aplicativos.
Conclusão
Criar um PWA com Blazor WASM exige poucas peças: ícones, um manifest, um service worker e uma estratégia de cache. Com elas, o app abre offline e pode ser instalado como se fosse nativo.
O melhor é que todo o conhecimento de C# e .NET continua valendo. Assim, você entrega uma experiência próxima da nativa sem manter um codebase separado para cada plataforma.
FAQ
Preciso montar o PWA à mão no Blazor?
Não, porque o comando dotnet new blazorwasm --pwa já gera manifest, ícones e service workers. Montar à mão, porém, ajuda a entender cada peça.
O Blazor Server pode virar PWA?
O template de PWA da Microsoft é voltado ao Blazor WebAssembly, que roda no navegador e pode funcionar offline. O Blazor Server depende de conexão constante com o servidor.
Por que minhas mudanças não aparecem depois de ativar o cache?
Isso acontece porque o service worker serve os arquivos do cache primeiro. Use uma aba anônima, limpe o armazenamento no DevTools ou atualize o service worker.
Qual navegador permite instalar um PWA no desktop?
Navegadores baseados em Chromium, como Edge e Chrome, exibem o botão de instalação na barra de endereço. O Safari também permite adicionar o app em versões recentes do macOS.
Qual a diferença entre service-worker.js e service-worker.published.js?
No template oficial, o primeiro é usado em desenvolvimento e o segundo, com cache offline, é usado depois da publicação.