Service Workers de origem cruzada - Experimentando com busca externa

Contexto

Os service workers permitem que os desenvolvedores da Web respondam a solicitações de rede feitas pelos aplicativos da Web, permitindo que eles continuem trabalhando mesmo off-line, combatendo a falta de sinal e implementando interações complexas de cache, como stale-while-revalidate. No entanto, os service workers sempre foram vinculados a uma origem específica. Como proprietário de um app da Web, é sua responsabilidade escrever e implantar um service worker para interceptar todas as solicitações de rede feitas pelo app. Nesse modelo, cada service worker é responsável por processar até mesmo solicitações entre origens, por exemplo, para uma API de terceiros ou para fontes da Web.

E se um provedor terceirizado de uma API, fontes da Web ou outro serviço comum tivesse o poder de implantar o próprio worker de serviço que processa solicitações feitas por outras origens? Os provedores podem implementar a própria lógica de rede personalizada e aproveitar uma única instância de cache confiável para armazenar as respostas. Agora, graças à busca externa, esse tipo de implantação de service worker de terceiros é uma realidade.

Implantar um worker de serviço que implementa a busca externa faz sentido para qualquer provedor de um serviço acessado por solicitações HTTPS de navegadores. Basta pensar em cenários em que você pode fornecer uma versão independente da rede do seu serviço, em que os navegadores podem aproveitar um cache de recursos comum. Os serviços que podem se beneficiar disso incluem, entre outros:

  • Provedores de API com interfaces RESTful
  • Provedores de fontes da Web
  • Provedores de dados de análise
  • Provedores de hospedagem de imagens
  • Redes genéricas de fornecimento de conteúdo

Imagine, por exemplo, que você seja um provedor de análises. Ao implantar um service worker de busca externo, você garante que todas as solicitações ao serviço que falharem enquanto um usuário estiver off-line sejam colocadas na fila e reproduzidas quando a conectividade retornar. Embora seja possível que os clientes de um serviço implementem um comportamento semelhante usando service workers próprios, exigir que cada cliente escreva uma lógica personalizada para o serviço não é tão escalonável quanto depender de um service worker de busca externo compartilhado que você implanta.

Pré-requisitos

Token de teste de origem

A busca externa ainda é considerada experimental. Para evitar a preparação prematura desse design antes de ser totalmente especificado e aceito pelos fornecedores do navegador, ele foi implementado no Chrome 54 como um teste de origem. Enquanto a busca externa continuar sendo experimental, para usar esse novo recurso com o serviço que você hospeda, será necessário solicitar um token com escopo para a origem específica do serviço. O token precisa ser incluído como um cabeçalho de resposta HTTP em todas as solicitações entre origens de recursos que você quer processar por busca externa, bem como na resposta do recurso JavaScript do service worker:

Origin-Trial: token_obtained_from_signup

O teste termina em março de 2017. A essa altura, esperamos ter descoberto as alterações necessárias para estabilizar o recurso e (espero) ativá-lo por padrão. Se a busca externa não for ativada por padrão nesse período, a funcionalidade vinculada aos tokens de teste de origem atuais vai parar de funcionar.

Para facilitar os testes com a busca externa antes de se registrar para um token oficial de teste de origem, você pode ignorar o requisito no Chrome para seu computador local acessando chrome://flags/#enable-experimental-web-platform-features e ativando a flag "Recursos experimentais da plataforma Web". Isso precisa ser feito em todas as instâncias do Chrome que você quer usar nas suas experimentações locais. Já com um token de teste do Origin, o recurso fica disponível para todos os usuários do Chrome.

HTTPS

Como em todas as implantações de service workers, o servidor da Web usado para veicular os recursos e o script do service worker precisa ser acessado por HTTPS. Além disso, a interceptação de busca externa só se aplica a solicitações originadas de páginas hospedadas em origens seguras. Portanto, os clientes do seu serviço precisam usar HTTPS para aproveitar a implementação de busca externa.

Como usar a busca externa

Agora que você já tem os pré-requisitos, vamos mergulhar nos detalhes técnicos necessários para configurar e executar um worker de serviço de busca externo.

Como registrar o service worker

O primeiro desafio que você provavelmente vai encontrar é como registrar seu service worker. Se você já trabalhou com service workers antes, provavelmente tem familiaridade com o seguinte:

// You can't do this!
if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('service-worker.js');
}

Este código JavaScript para um registro de service worker próprio faz sentido no contexto de um app da Web, acionado por um usuário que navega para um URL controlado por você. No entanto, não é uma abordagem viável para registrar um service worker de terceiros, quando a única interação que o navegador terá com seu servidor é solicitar um subrecurso específico, não uma navegação completa. Se o navegador solicitar, por exemplo, uma imagem de um servidor CDN que você mantém, não será possível anexar esse snippet de JavaScript à sua resposta e esperar que ele seja executado. É necessário um método diferente de registro de service worker, fora do contexto normal de execução de JavaScript.

A solução é um cabeçalho HTTP que o servidor pode incluir em qualquer resposta:

Link: </service-worker.js>; rel="serviceworker"; scope="/"

Vamos dividir esse cabeçalho de exemplo em componentes, cada um separado por um caractere ;.

  • </service-worker.js> é obrigatório e é usado para especificar o caminho para o arquivo do worker de serviço (substitua /service-worker.js pelo caminho adequado para o script). Isso corresponde diretamente à string scriptURL que seria transmitida como o primeiro parâmetro para navigator.serviceWorker.register(). O valor precisa ser colocado entre caracteres <> (conforme exigido pela especificação do cabeçalho Link). Se um URL relativo em vez de absoluto for fornecido, ele será interpretado como relativo ao local da resposta.
  • rel="serviceworker" também é obrigatório e precisa ser incluído sem qualquer personalização.
  • scope=/ é uma declaração de escopo opcional, equivalente à string options.scope que pode ser transmitida como o segundo parâmetro para navigator.serviceWorker.register(). Para muitos casos de uso, é possível usar o escopo padrão. Portanto, sinta-se à vontade para deixar isso de fora, a menos que você saiba que precisa dele. As mesmas restrições em relação ao escopo máximo permitido, além da capacidade de relaxar essas restrições pelo cabeçalho Service-Worker-Allowed, se aplicam aos registros de cabeçalho Link.

Assim como em um registro de worker de serviço "tradicional", o uso do cabeçalho Link instala um worker de serviço que será usado para a solicitação seguinte feita no escopo registrado. O corpo da resposta que inclui o cabeçalho especial será usado como está e estará disponível para a página imediatamente, sem esperar que o worker de serviço externo termine a instalação.

Lembre-se de que o fetch externo está implementado atualmente como um teste de origem. Portanto, além do cabeçalho de resposta de link, você também precisa incluir um cabeçalho Origin-Trial válido. O conjunto mínimo de cabeçalhos de resposta a serem adicionados para registrar o service worker de busca externa é

Link: </service-worker.js>; rel="serviceworker"
Origin-Trial: token_obtained_from_signup

Como depurar o registro

Durante o desenvolvimento, é recomendável confirmar se o service worker de busca externa está instalado e processando solicitações corretamente. Há algumas coisas que você pode verificar nas Ferramentas para Desenvolvedores do Chrome para confirmar se tudo está funcionando como esperado.

Os cabeçalhos de resposta corretos estão sendo enviados?

Para registrar o service worker de busca externo, é necessário definir um cabeçalho Link em uma resposta a um recurso hospedado no seu domínio, conforme descrito anteriormente nesta postagem. Durante o período de teste de origem, e supondo que você não tenha definido chrome://flags/#enable-experimental-web-platform-features, também é necessário definir um cabeçalho de resposta Origin-Trial. É possível confirmar se o servidor da Web está definindo esses cabeçalhos analisando a entrada no painel de rede do DevTools:

Cabeçalhos exibidos no painel Network.

O service worker de Busca externa está devidamente registrado?

Também é possível confirmar o registro do service worker subjacente, incluindo o escopo dele, consultando a lista completa de service workers no painel do aplicativo do DevTools. Selecione a opção "Mostrar tudo", já que, por padrão, você só vai encontrar service workers para a origem atual.

O service worker de busca externo no painel &quot;Applications&quot;.

O gerenciador de eventos de instalação

Agora que você registrou o service worker de terceiros, ele vai ter a chance de responder aos eventos install e activate, assim como qualquer outro service worker. Ele pode aproveitar esses eventos para, por exemplo, preencher caches com os recursos necessários durante o evento install ou remover caches desatualizados no evento activate.

Além das atividades normais de armazenamento em cache de eventos install, há uma etapa adicional que é necessária no manipulador de eventos install do worker de serviço de terceiros. Seu código precisa chamar registerForeignFetch(), como no exemplo a seguir:

self.addEventListener('install', event => {
    event.registerForeignFetch({
    scopes: [self.registration.scope], // or some sub-scope
    origins: ['*'] // or ['https://example.com']
    });
});

Há duas opções de configuração, ambas obrigatórias:

  • scopes recebe uma matriz de uma ou mais strings, cada uma representando um escopo para solicitações que acionarão um evento foreignfetch. Mas espere, você pode estar pensando: Eu já defini um escopo durante o registro do service worker! Isso é verdade, e esse escopo geral ainda é relevante. Cada escopo especificado aqui precisa ser igual ou um subestágio do escopo geral do service worker. As restrições de escopo adicionais permitem implantar um worker de serviço multiuso que pode processar eventos fetch próprios (para solicitações feitas no seu próprio site) e eventos foreignfetch de terceiros (para solicitações feitas em outros domínios) e deixar claro que apenas um subconjunto do seu escopo maior deve acionar foreignfetch. Na prática, se você estiver implantando um worker de serviço dedicado a processar apenas eventos foreignfetch de terceiros, use um escopo único e explícito igual ao escopo geral do worker de serviço. É isso que o exemplo acima vai fazer, usando o valor self.registration.scope.
  • origins também recebe uma matriz de uma ou mais strings e permite que você restrinja o gerenciador foreignfetch para que ele responda apenas a solicitações de domínios específicos. Por exemplo, se você permitir explicitamente "https://example.com", uma solicitação feita em uma página hospedada em https://example.com/path/to/page.html para um recurso veiculado no seu escopo de busca externo vai acionar o gerenciador de busca externo, mas as solicitações feitas em https://random-domain.com/path/to/page.html não vão acionar o gerenciador. A menos que você tenha um motivo específico para acionar sua lógica de busca externa apenas para um subconjunto de origens remotas, basta especificar '*' como o único valor na matriz e todas as origens serão permitidas.

O manipulador de eventos foreignfetch

Agora que você instalou o service worker de terceiros e ele foi configurado com registerForeignFetch(), ele terá a chance de interceptar solicitações de subrecursos de origem cruzada para o servidor que estão dentro do escopo de busca externo.

Em um service worker tradicional de primeira parte, cada solicitação aciona um evento fetch, ao qual o service worker pode responder. Nosso service worker de terceiros tem a chance de processar um evento um pouco diferente, chamado foreignfetch. Conceitualmente, os dois eventos são bastante semelhantes e oferecem a oportunidade de inspecionar a solicitação recebida e, opcionalmente, fornecer uma resposta por respondWith():

self.addEventListener('foreignfetch', event => {
    // Assume that requestLogic() is a custom function that takes
    // a Request and returns a Promise which resolves with a Response.
    event.respondWith(
    requestLogic(event.request).then(response => {
        return {
        response: response,
        // Omit to origin to return an opaque response.
        // With this set, the client will receive a CORS response.
        origin: event.origin,
        // Omit headers unless you need additional header filtering.
        // With this set, only Content-Type will be exposed.
        headers: ['Content-Type']
        };
    })
    );
});

Apesar das semelhanças conceituais, há algumas diferenças na prática ao chamar respondWith() em um ForeignFetchEvent. Em vez de apenas fornecer um Response (ou Promise que se resolve com um Response) para respondWith(), como fazer com um FetchEvent, é necessário transmitir um Promise que se resolva com um objeto com propriedades específicas para o respondWith() do ForeignFetchEvent:

  • response é obrigatório e precisa ser definido como o objeto Response que será retornado ao cliente que fez a solicitação. Se você fornecer qualquer coisa que não seja um Response válido, a solicitação do cliente será encerrada com um erro de rede. Ao contrário de quando você chama respondWith() dentro de um gerenciador de eventos fetch, é necessário fornecer um Response aqui, não um Promise que seja resolvido com um Response. Você pode construir sua resposta usando uma cadeia de promessas e transmiti-la como parâmetro para respondWith() de foreignfetch, mas a cadeia precisa ser resolvida com um objeto que contenha a propriedade response definida como um objeto Response. Confira um exemplo no código acima.
  • origin é opcional e é usado para determinar se a resposta retornada é opaco. Se você deixar isso de fora, a resposta será opaca, e o cliente terá acesso limitado ao corpo e aos cabeçalhos da resposta. Se a solicitação tiver sido feita com mode: 'cors', o retorno de uma resposta opaca será tratado como um erro. No entanto, se você especificar um valor de string igual à origem do cliente remoto (que pode ser obtido por event.origin), vai ativar explicitamente o envio de uma resposta com CORS ao cliente.
  • headers também é opcional e só é útil se você também especificar origin e retornar uma resposta do CORS. Por padrão, apenas os cabeçalhos na lista de cabeçalhos de resposta listados no CORS serão incluídos na resposta. Se você precisar filtrar ainda mais o que é retornado, a propriedade headers recebe uma lista de um ou mais nomes de cabeçalho e a usa como uma lista de permissões de quais cabeçalhos serão expostos na resposta. Isso permite a ativação do CORS e, ao mesmo tempo, impede que cabeçalhos de resposta potencialmente sensíveis sejam expostos diretamente ao cliente remoto.

É importante observar que, quando o gerenciador foreignfetch é executado, ele tem acesso a todas as credenciais e à autoridade ambiental da origem que hospeda o service worker. Como desenvolvedor que implanta um worker de serviço habilitado para busca externa, é sua responsabilidade garantir que não haja vazamento de dados de resposta privilegiados que não estariam disponíveis devido a essas credenciais. Exigir uma ativação para respostas do CORS é uma etapa para limitar a exposição inadvertida, mas, como desenvolvedor, você pode fazer solicitações fetch() explicitamente no gerenciador foreignfetch que não usam as credenciais implícitas usando:

self.addEventListener('foreignfetch', event => {
    // The new Request will have credentials omitted by default.
    const noCredentialsRequest = new Request(event.request.url);
    event.respondWith(
    // Replace with your own request logic as appropriate.
    fetch(noCredentialsRequest)
        .catch(() => caches.match(noCredentialsRequest))
        .then(response => ({response}))
    );
});

Considerações do cliente

Há algumas outras considerações que afetam a forma como o worker de serviço de busca externo processa as solicitações feitas por clientes do serviço.

Clientes que têm o próprio service worker primário

Alguns clientes do seu serviço podem já ter o próprio worker de serviço próprio, processando solicitações originadas do app da Web. O que isso significa para o worker de serviço de busca de terceiros?

Os gerenciadores fetch em um service worker próprio têm a primeira oportunidade de responder a todas as solicitações feitas pelo app da Web, mesmo que haja um service worker de terceiros com foreignfetch ativado com um escopo que abranja a solicitação. No entanto, os clientes com service workers próprios ainda podem aproveitar seu service worker de busca externo.

Em um service worker próprio, o uso de fetch() para recuperar recursos de várias origens vai acionar o service worker de busca externo adequado. Isso significa que um código como o seguinte pode aproveitar o gerenciador foreignfetch:

// Inside a client's first-party service-worker.js:
self.addEventListener('fetch', event => {
    // If event.request is under your foreign fetch service worker's
    // scope, this will trigger your foreignfetch handler.
    event.respondWith(fetch(event.request));
});

Da mesma forma, se houver gerenciadores de busca primários, mas eles não chamarem event.respondWith() ao processar solicitações para seu recurso de origem cruzada, a solicitação "cairá" automaticamente para seu gerenciador foreignfetch:

// Inside a client's first-party service-worker.js:
self.addEventListener('fetch', event => {
    if (event.request.mode === 'same-origin') {
    event.respondWith(localRequestLogic(event.request));
    }

    // Since event.respondWith() isn't called for cross-origin requests,
    // any foreignfetch handlers scoped to the request will get a chance
    // to provide a response.
});

Se um manipulador fetch de primeira parte chamar event.respondWith(), mas não usar fetch() para solicitar um recurso no escopo de busca externa, o service worker de busca externa não terá a chance de processar a solicitação.

Clientes que não têm o próprio worker de serviço

Todos os clientes que fazem solicitações a um serviço de terceiros podem se beneficiar quando o serviço implanta um service worker de busca externo, mesmo que ainda não estejam usando o próprio service worker. Não há nada específico que os clientes precisem fazer para ativar o uso de um worker de serviço de busca externo, desde que estejam usando um navegador compatível. Isso significa que, ao implantar um worker de serviço de busca externo, a lógica de solicitação personalizada e o cache compartilhado vão beneficiar muitos dos clientes do serviço imediatamente, sem que eles precisem fazer outras etapas.

Juntando tudo: onde os clientes procuram uma resposta

Levando em conta as informações acima, podemos montar uma hierarquia de origens que um cliente vai usar para encontrar uma resposta para uma solicitação entre origens.

  1. O gerenciador fetch de um service worker próprio (se houver)
  2. Um manipulador foreignfetch de um worker de serviço de terceiros (se presente e apenas para solicitações entre origens)
  3. O cache HTTP do navegador (se houver uma resposta nova)
  4. A rede

O navegador é iniciado de cima para baixo e, dependendo da implementação do service worker, continua pela lista até encontrar uma origem para a resposta.

Saiba mais

Fique por dentro

A implementação do teste de origem de busca externa do Chrome está sujeita a mudanças conforme abordamos o feedback dos desenvolvedores. Vamos manter esta postagem atualizada com mudanças inline e anotar as mudanças específicas abaixo à medida que elas forem feitas. Também vamos compartilhar informações sobre as principais mudanças na conta do Twitter @chromiumdev.