Service Workers de origem cruzada - Experimentando com busca externa

Contexto

Os service workers oferecem aos desenvolvedores da Web a capacidade de responder às solicitações de rede feitas pelos aplicativos da Web, permitindo que continuem trabalhando mesmo off-line, lidam com o lie-fi e implementem interações complexas de cache, como desatualização durante a revalidação. No entanto, os service workers sempre estão 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 que o app da Web faz. Nesse modelo, cada service worker é responsável por processar até mesmo solicitações de origem cruzada, 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 comumente usado pudesse implantar o próprio service worker que tivesse a chance de lidar com solicitações feitas por outras origens para a origem? Os provedores podem implementar a própria lógica de rede personalizada e aproveitar uma única instância de cache autoritativa para armazenar as respostas. Agora, graças à busca externa, esse tipo de implantação de service workers de terceiros é uma realidade.

Implantar um service worker que implemente uma 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ê poderia fornecer uma versão independente de rede do seu serviço, em que os navegadores poderiam usar 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 análise
  • Provedores de hospedagem de imagem
  • Redes genéricas de fornecimento de conteúdo

Imagine, por exemplo, que você é um provedor de análises. Ao implantar um service worker de busca externa, é possível garantir que todas as solicitações para seu serviço com falha enquanto um usuário estiver off-line sejam enfileiradas e repetidas quando a conectividade retornar. Embora os clientes de um serviço possam implementar um comportamento semelhante usando service workers próprios, exigir que cada um deles escreva uma lógica personalizada para seu serviço não é tão escalonável quanto depender de um service worker de busca externa compartilhada que você implanta.

Pré-requisitos

Token de teste de origem

A busca externa ainda é considerada experimental. Para evitar a preparação prematura do design antes de ser totalmente especificado e acordado pelos fornecedores do navegador, ele foi implementado no Chrome 54 como um teste de origem. Enquanto a busca externa permanecer experimental, será necessário solicitar um token com escopo para a origem específica do seu serviço para usar esse novo recurso com o serviço hospedado. O token deve ser incluído como um cabeçalho de resposta HTTP em todas as solicitações de origem cruzada para os recursos que você quer processar por meio de busca externa, bem como na resposta para o 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 mudanças necessárias para estabilizar o recurso e, esperamos, ativá-lo por padrão. Se a busca externa não for ativada por padrão, a funcionalidade vinculada aos tokens de teste de origem atuais vai deixar de funcionar.

Para facilitar a experiência com a busca externa antes de se registrar para um token oficial do 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 sinalização "Recursos da plataforma experimental da Web". Isso precisa ser feito em todas as instâncias do Chrome que você quer usar nos experimentos locais. Com um token de teste de origem, o recurso estará disponível para todos os usuários do Chrome.

HTTPS

Assim como em todas as implantações de service worker, o servidor da Web que você usa para disponibilizar os recursos e o script do service worker precisa ser acessado via 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 sua implementação de busca externa.

Como usar a busca externa

Com os pré-requisitos definidos, vamos nos aprofundar nos detalhes técnicos necessários para colocar um service worker de busca externa em funcionamento.

Como registrar o service worker

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

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

Esse código JavaScript para o registro de um service worker primário faz sentido no contexto de um app da Web, acionado por um usuário que navega até um URL que você controla. Mas 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 sub-recurso específico, não uma navegação completa. Se o navegador solicitar, por exemplo, uma imagem de um servidor de CDN gerenciado por você, não será possível anexar esse snippet de JavaScript à sua resposta e esperar que ele será executado. É necessário um método diferente de registro do service worker, fora do contexto de execução normal do JavaScript.

Ela vem na forma de 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 nos componentes, cada um separado por um caractere ;.

  • </service-worker.js> é obrigatório e é usado para especificar o caminho para o arquivo do service worker (substitua /service-worker.js pelo caminho apropriado para o script). Isso corresponde diretamente à string scriptURL, que seria transmitida como o primeiro parâmetro para navigator.serviceWorker.register(). O valor precisa estar entre caracteres <> (conforme exigido pela especificação do cabeçalho Link) e, se um URL relativo e não absoluto for fornecido, ele será interpretado como relativo ao local da resposta.
  • rel="serviceworker" também é obrigatório e precisa ser incluído sem necessidade de personalização.
  • scope=/ é uma declaração de escopo opcional, equivalente à string options.scope que você pode transmitir como o segundo parâmetro para navigator.serviceWorker.register(). Em muitos casos de uso, você não tem problemas em usar o escopo padrão. Portanto, fique à vontade para deixar isso de fora, a menos que saiba que precisa dele. As mesmas restrições sobre o 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 service worker "tradicional", o uso do cabeçalho Link instala um service worker que é usado na próxima solicitação feita para o escopo registrado. O corpo da resposta que inclui o cabeçalho especial será usado no estado em que se encontra e fica disponível na página imediatamente, sem esperar que o service worker externo termine a instalação.

A busca externa está implementada atualmente como um teste de origem. Portanto, junto com o cabeçalho de resposta de link, é necessário incluir também 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

Depurar o registro

Durante o desenvolvimento, você provavelmente desejará confirmar se seu service worker de busca externa está instalado corretamente e processando solicitações. Para confirmar se tudo está funcionando como esperado, confira algumas coisas nas Ferramentas para Desenvolvedores do Chrome.

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

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

Cabeçalhos exibidos no painel &quot;Network&quot;.

O service worker de busca externa está registrado corretamente?

Também é possível confirmar o registro do service worker subjacente, incluindo o escopo dele, analisando a lista completa de service workers no painel Application do DevTools. Selecione a opção "Mostrar todos", já que, por padrão, você só verá service workers da origem atual.

O service worker de busca externa no painel &quot;Aplicativos&quot;.

O manipulador de eventos de instalação

Agora que você registrou o service worker de terceiros, ele poderá responder aos eventos install e activate, como qualquer outro service worker. Ele pode aproveitar esses eventos para, por exemplo, preencher caches com 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 obrigatória no manipulador de eventos install do service worker 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 necessárias:

  • scopes usa uma matriz de uma ou mais strings, cada uma representando um escopo para solicitações que acionarão um evento foreignfetch. Espere, você pode estar pensando: Já defina um escopo durante o registro do service worker. Isso é verdade e esse escopo geral ainda é relevante: cada escopo especificado aqui precisa ser igual ou subescopo do escopo geral do service worker. As restrições de escopo adicionais permitem que você implante um service worker para todas as finalidades que pode lidar com eventos fetch próprios (para solicitações feitas do seu próprio site) e eventos foreignfetch de terceiros (para solicitações feitas de outros domínios), e deixa claro que apenas um subconjunto do seu escopo maior precisa acionar foreignfetch. Na prática, se você estiver implantando um service worker dedicado a lidar apenas com eventos foreignfetch de terceiros, convém usar um escopo único e explícito igual ao escopo geral do service worker. Isso é o que o exemplo acima fará, usando o valor self.registration.scope.
  • origins também usa uma matriz de uma ou mais strings e permite que você restrinja o gerenciador foreignfetch para responder apenas a solicitações de domínios específicos. Por exemplo, se você permitir explicitamente "https://example.com", uma solicitação feita a partir de uma página hospedada em https://example.com/path/to/page.html para um recurso disponibilizado do seu escopo de busca externa acionará o gerenciador de busca externa, mas as solicitações feitas por https://random-domain.com/path/to/page.html não acionarão o gerenciador. A menos que você tenha um motivo específico para acionar apenas a lógica de busca externa 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 de externalfetch

Agora que você instalou o service worker de terceiros e ele foi configurado por registerForeignFetch(), ele terá a chance de interceptar solicitações de sub-recursos de origem cruzada para seu servidor que estejam no escopo de busca externa.

Em um service worker próprio tradicional, cada solicitação acionaria um evento fetch que poderia ser respondido pelo service worker. Nosso service worker terceirizado tem a chance de processar um evento um pouco diferente, chamado foreignfetch. Conceitualmente, os dois eventos são muito semelhantes e permitem inspecionar a solicitação recebida e, opcionalmente, fornecer uma resposta a ela via 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 uma ForeignFetchEvent. Em vez de apenas fornecer um Response (ou Promise que é resolvido com um Response) para respondWith(), como você faz com FetchEvent, é necessário transmitir um Promise que é resolvido com um objeto com propriedades específicas para 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 algo diferente de um Response válido, a solicitação do cliente será encerrada com um erro de rede. Diferente da chamada respondWith() dentro de um manipulador de eventos fetch, você precisa fornecer um Response aqui, não um Promise que é resolvido com um Response. É possível construir sua resposta usando uma cadeia de promessa e transmiti-la como o parâmetro para o respondWith() do foreignfetch, mas a cadeia precisa ser resolvida com um objeto que contenha a propriedade response definida como um objeto Response. Confira uma demonstração no exemplo de código acima.
  • origin é opcional e é usado para determinar se a resposta retornada é ou não opaca. Se você não incluir essa informação, a resposta vai ficar opaca, e o cliente vai ter acesso limitado ao corpo e aos cabeçalhos dela. 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 optar explicitamente por fornecer uma resposta com CORS ativada para o cliente.
  • headers também é opcional e útil apenas se você também especificar origin e retornar uma resposta do CORS. Por padrão, somente os cabeçalhos na lista de cabeçalhos de resposta da lista de segurança do CORS serão incluídos na resposta. Se você precisar filtrar ainda mais o que é retornado, os cabeçalhos vão usar uma lista com um ou mais nomes de cabeçalho e a usarão como uma lista de permissões de quais cabeçalhos expor na resposta. Isso permite que você ative o CORS e, ao mesmo tempo, impede que cabeçalhos de resposta potencialmente confidenciais sejam expostos diretamente ao cliente remoto.

É importante observar que, quando o gerenciador foreignfetch é executado, ele tem acesso a todas as credenciais e autoridade ambiente da origem que hospeda o service worker. Como desenvolvedor que implanta um service worker estrangeiro habilitado para busca, é sua responsabilidade garantir que você não vaze dados de resposta privilegiada que não estariam disponíveis devido a essas credenciais. Exigir a permissão das respostas do CORS é uma etapa para limitar a exposição acidental. No entanto, 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 considerações adicionais que afetam a forma como o service worker de busca externa lida com solicitações feitas por clientes do seu serviço.

Clientes que têm o próprio service worker

Talvez alguns clientes do seu serviço já tenham o próprio service worker primário, processando solicitações originadas do app da Web. O que isso significa para o service worker de busca externa terceirizada?

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 se houver um service worker de terceiros com foreignfetch ativado com um escopo que abranja a solicitação. Mas os clientes com service workers próprios ainda podem aproveitar o service worker de busca externa.

Em um service worker próprio, o uso de fetch() para recuperar recursos de origem cruzada vai acionar o service worker de busca externa apropriado. Isso significa que códigos como o seguinte podem 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 próprios, mas eles não chamarem event.respondWith() ao processar solicitações para seu recurso de origem cruzada, a solicitação vai passar automaticamente para o 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 gerenciador próprio fetch 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 service worker

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 externa, mesmo que eles ainda não estejam usando o próprio service worker. Os clientes não precisam fazer nada específico para aceitar o uso de um service worker de busca externa, desde que usem um navegador compatível. Isso significa que, ao implantar um service worker de busca externa, sua lógica de solicitação personalizada e seu cache compartilhado vão beneficiar muitos dos clientes do seu serviço imediatamente, sem que eles precisem tomar outras medidas.

Reunindo tudo: onde os clientes procuram uma resposta

Levando em consideração as informações acima, podemos montar uma hierarquia de fontes que um cliente usará para encontrar uma resposta para uma solicitação de origem cruzada.

  1. O gerenciador fetch de um service worker próprio (se houver)
  2. O gerenciador foreignfetch de um service worker terceirizado (se presente e somente para solicitações de origem cruzada)
  3. o cache HTTP do navegador (se houver uma nova resposta);
  4. A rede

O navegador começa de cima e, dependendo da implementação do service worker, seguirá pela lista até encontrar uma fonte para a resposta.

Saiba mais

Fique por dentro

A implementação do Chrome do teste de origem de busca externa está sujeita a alterações à medida que lidamos com o feedback dos desenvolvedores. Manteremos esta postagem atualizada com as alterações in-line e anotaremos as alterações específicas abaixo à medida que acontecerem. Também vamos compartilhar informações sobre as principais mudanças na conta do Twitter @chromiumdev.