Service Workers de origen cruzado: cómo experimentar con la recuperación externa

Información general

Los service workers brindan a los desarrolladores web la capacidad de responder a las solicitudes de red realizadas por sus aplicaciones web, lo que les permite seguir trabajando incluso sin conexión, luchar en el lie-fi e implementar interacciones complejas de caché, como stale-while-revalid. Sin embargo, los service workers han estado históricamente vinculados a un origen específico; como propietario de una aplicación web, es tu responsabilidad escribir e implementar un service worker para interceptar todas las solicitudes de red que realiza tu aplicación web. En ese modelo, cada service worker es responsable de manejar incluso las solicitudes de origen cruzado, por ejemplo, a una API de terceros o fuentes web.

¿Qué pasaría si un proveedor externo de una API, fuentes web o algún otro servicio de uso común tuviera el poder de implementar su propio service worker que tuviera la oportunidad de manejar solicitudes realizadas por otros orígenes a sus orígenes? Los proveedores podrían implementar su propia lógica de herramientas de redes personalizada y aprovechar una única instancia de caché autorizada para almacenar sus respuestas. Ahora, gracias a la recuperación externa, ese tipo de implementación de service worker de terceros es una realidad.

Implementar un service worker que implemente la recuperación externa tiene sentido para cualquier proveedor de un servicio al que se accede a través de solicitudes HTTPS desde navegadores. Solo piensa en situaciones en las que podrías proporcionar una versión de tu servicio independiente de la red, en la que los navegadores podrían aprovechar una caché de recursos común. Los servicios que podrían beneficiarse de esto incluyen, entre otros:

  • Proveedores de API con interfaces RESTful
  • Proveedores de fuentes web
  • Proveedores de estadísticas
  • Proveedores de hosting de imágenes
  • Redes genéricas de distribución de contenidos

Imagina, por ejemplo, que eres un proveedor de servicios de analítica. Mediante la implementación de un service worker externo, puedes asegurarte de que todas las solicitudes a tu servicio que fallen mientras un usuario está sin conexión estén en cola y se vuelvan a reproducir una vez que se recupere la conectividad. Si bien es posible que los clientes de un servicio implementen un comportamiento similar a través de service workers propios, exigir que cada uno de ellos escriba una lógica personalizada para tu servicio no es tan escalable como depender de un service worker externo compartido que tú implementes.

Requisitos previos

Token de prueba de origen

La recuperación externa aún se considera experimental. Para evitar la integración prematura de este diseño antes de que esté completamente especificado y acordado por los proveedores de navegadores, se implementó en Chrome 54 como una prueba de origen. Siempre y cuando la recuperación externa siga siendo experimental, para usar esta nueva función con el servicio que alojas, deberás solicitar un token que se limite al origen específico de tu servicio. El token se debe incluir como un encabezado de respuesta HTTP en todas las solicitudes de origen cruzado de los recursos que desees controlar mediante la recuperación externa, así como en la respuesta del recurso JavaScript del service worker:

Origin-Trial: token_obtained_from_signup

La prueba finalizará en marzo de 2017. Para ese momento, esperamos haber descubierto los cambios necesarios para estabilizar la función y, con suerte, habilitarla de forma predeterminada. Si la recuperación externa no está habilitada de forma predeterminada para ese momento, la funcionalidad vinculada a los tokens de prueba de origen existentes dejará de funcionar.

Para facilitar la experimentación con la recuperación de datos extranjeros antes de registrarte para obtener un token de prueba de origen oficial, puedes omitir el requisito en Chrome para tu computadora local. Para ello, ve a chrome://flags/#enable-experimental-web-platform-features y habilita la marca "Funciones de la plataforma web experimental". Ten en cuenta que esto se debe hacer en cada instancia de Chrome que quieras usar en tus experimentos locales, mientras que con un token de prueba de origen, la función estará disponible para todos tus usuarios de Chrome.

HTTPS

Al igual que con todas las implementaciones de service worker, el servidor web que usas para entregar tus recursos y la secuencia de comandos del service worker debe tener acceso a través de HTTPS. Además, la intercepción de recuperación externa solo se aplica a las solicitudes que se originan en páginas alojadas en orígenes seguros, por lo que los clientes de tu servicio deben usar HTTPS para aprovechar tu implementación de recuperación externa.

Usar recuperación externa

Una vez que completes los requisitos previos, veamos los detalles técnicos necesarios para poner en funcionamiento un service worker extranjero de recuperación.

Cómo registrar tu service worker

Es probable que te encuentres con el primer desafío para registrar tu service worker. Si ya trabajaste con service workers, es probable que conozcas lo siguiente:

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

Este código JavaScript para el registro de un service worker propio tiene sentido en el contexto de una app web y se activa cuando un usuario navega a una URL que tú controlas. Sin embargo, no es un enfoque viable para registrar un service worker de terceros cuando la única interacción que tendrá el navegador con tu servidor es solicitar un subrecurso específico, no una navegación completa. Si el navegador solicita, por ejemplo, una imagen de un servidor CDN que tú mantienes, no puedes anteponer ese fragmento de JavaScript a tu respuesta y esperar que se ejecute. Se requiere un método diferente de registro de service worker, fuera del contexto normal de ejecución de JavaScript.

La solución se presenta como un encabezado HTTP que tu servidor puede incluir en cualquier respuesta:

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

Desglosemos ese encabezado de ejemplo en sus componentes, cada uno de los cuales está separado por un carácter ;.

  • </service-worker.js> es obligatorio y se usa para especificar la ruta de acceso al archivo de tu service worker (reemplaza /service-worker.js por la ruta de acceso adecuada a tu secuencia de comandos). Esto corresponde directamente a la cadena scriptURL que, de lo contrario, se pasaría como el primer parámetro a navigator.serviceWorker.register(). El valor debe encerrarse entre caracteres <> (como lo exige la especificación del encabezado Link). Si se proporciona una URL relativa en lugar de absoluta, se interpretará como relativa a la ubicación de la respuesta.
  • El parámetro rel="serviceworker" también es obligatorio y se debe incluir sin necesidad de personalización.
  • scope=/ es una declaración de alcance opcional, equivalente a la cadena options.scope que puedes pasar como el segundo parámetro a navigator.serviceWorker.register(). Para muchos casos de uso, está de acuerdo con usar el alcance predeterminado, así que no dudes en omitirlo, a menos que sepas que lo necesitas. Las mismas restricciones respecto del alcance máximo permitido, junto con la capacidad de flexibilizar las restricciones mediante el encabezado Service-Worker-Allowed, se aplican a los registros de encabezado Link.

Al igual que con un registro de service worker "tradicional", cuando se usa el encabezado Link, se instala un service worker que se usará para la próxima solicitud que se haga con el alcance registrado. El cuerpo de la respuesta que incluye el encabezado especial se utilizará tal como está y estará disponible en la página inmediatamente, sin esperar a que el service worker externo termine la instalación.

Recuerda que la recuperación externa se implementa actualmente como una prueba de origen, por lo que deberás incluir un encabezado Origin-Trial válido junto con el encabezado de respuesta de vínculo. El conjunto mínimo de encabezados de respuesta que debes agregar para registrar tu service worker externo es

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

Cómo depurar el registro

Durante el desarrollo, te recomendamos que confirmes que tu service worker externo está instalado correctamente y procesa solicitudes. Hay algunos elementos que puedes revisar en las herramientas para desarrolladores de Chrome para confirmar que todo funciona según lo esperado.

¿Se envían los encabezados de respuesta adecuados?

Para registrar el service worker externo, debes establecer un encabezado de vínculo en una respuesta a un recurso alojado en tu dominio, como se describió anteriormente en esta publicación. Durante el período de prueba de origen, y suponiendo que no estableciste chrome://flags/#enable-experimental-web-platform-features, también debes establecer un encabezado de respuesta Origin-Trial. Puedes confirmar que tu servidor web está configurando esos encabezados. Para ello, consulta la entrada del panel de red de Herramientas para desarrolladores:

Encabezados que se muestran en el panel Red.

¿El service worker de recuperación externa está registrado correctamente?

También puedes confirmar el registro del service worker subyacente, incluido su alcance, consultando la lista completa de estos procesos en el panel Application de Herramientas para desarrolladores. Asegúrate de seleccionar la opción "Mostrar todo", ya que, de forma predeterminada, solo verás service workers para el origen actual.

El service worker externo de recuperación en el panel Aplicaciones.

El controlador de eventos de instalación

Ahora que registraste el service worker de terceros, podrás responder a los eventos install y activate, tal como lo haría cualquier otro service worker. Puedes aprovechar esos eventos para, por ejemplo, propagar cachés con los recursos necesarios durante el evento install o reducir las cachés desactualizadas en el evento activate.

Más allá de las actividades normales de almacenamiento en caché de eventos install, hay un paso adicional que es obligatorio dentro del controlador de eventos install del service worker de terceros. Tu código debe llamar a registerForeignFetch(), como en el siguiente ejemplo:

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

Hay dos opciones de configuración, ambas obligatorias:

  • scopes toma un array de una o más cadenas, cada una de las cuales representa un alcance para las solicitudes que activarán un evento foreignfetch. Pero espera, quizás estés pensando: Ya definí un alcance durante el registro del service worker. Eso es cierto, y el alcance general sigue siendo relevante; cada alcance que especifiques aquí debe ser igual o inferior al alcance general del service worker. Las restricciones de alcance adicionales que aquí se incluyen te permiten implementar un service worker multipropósito que puede controlar eventos fetch propios (para solicitudes realizadas desde tu propio sitio) y eventos foreignfetch de terceros (para solicitudes realizadas desde otros dominios), y dejar en claro que solo un subconjunto de tu alcance más amplio debería activar foreignfetch. En la práctica, si implementas un service worker dedicado a controlar solo eventos foreignfetch de terceros, te conviene usar un permiso único y explícito que sea igual al alcance general de tu service worker. Eso es lo que hará el ejemplo anterior, con el valor self.registration.scope.
  • origins también toma un array de una o más strings y te permite restringir el controlador foreignfetch para que solo responda a las solicitudes de dominios específicos. Por ejemplo, si permites "https://example.com" de manera explícita, una solicitud realizada desde una página alojada en https://example.com/path/to/page.html para un recurso entregado desde tu alcance de recuperación externo activará el controlador de recuperación externo, pero las solicitudes realizadas desde https://random-domain.com/path/to/page.html no activarán el controlador. A menos que tengas un motivo específico para activar solo tu lógica de recuperación externa para un subconjunto de orígenes remotos, puedes especificar '*' como el único valor en el array, y se permitirán todos los orígenes.

El controlador de eventos externalfetch

Ahora que instalaste el service worker de terceros y se configuró a través de registerForeignFetch(), podrás interceptar solicitudes de subrecursos de origen cruzado a tu servidor que se encuentren dentro del alcance de recuperación externo.

En un service worker tradicional tradicional, cada solicitud activaría un evento fetch al que el service worker pudo responder. Nuestro service worker de terceros tiene la oportunidad de controlar un evento ligeramente diferente, llamado foreignfetch. Conceptualmente, los dos eventos son bastante similares y te brindan la oportunidad de inspeccionar la solicitud entrante y, opcionalmente, proporcionar una respuesta a ella a través de 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']
        };
    })
    );
});

A pesar de las similitudes conceptuales, existen algunas diferencias en la práctica cuando se llama a respondWith() en un ForeignFetchEvent. En lugar de solo proporcionar una Response (o Promise que se resuelve con un Response) a respondWith(), como lo haces con una FetchEvent, debes pasar una Promise que se resuelva con un objeto con propiedades específicas al respondWith() de ForeignFetchEvent:

  • response es obligatorio y se debe configurar en el objeto Response que se mostrará al cliente que realizó la solicitud. Si proporcionas algo que no sea un Response válido, la solicitud del cliente finalizará con un error de red. A diferencia de cuando llamas a respondWith() dentro de un controlador de eventos fetch, debes proporcionar un Response aquí, no un Promise que se resuelve con un Response. Puedes crear tu respuesta a través de una cadena de promesas y pasar esa cadena como parámetro a respondWith() de foreignfetch, pero la cadena debe resolverse con un objeto que contenga la propiedad response establecida en un objeto Response. Puedes ver una demostración de esto en la muestra de código anterior.
  • origin es opcional y se usa para determinar si la respuesta que se muestra es opaca o no. Si omites esto, la respuesta será opaca, y el cliente tendrá acceso limitado al cuerpo y los encabezados de la respuesta. Si la solicitud se realizó con mode: 'cors', devolver una respuesta opaca se considerará un error. Sin embargo, si especificas un valor de string igual al origen del cliente remoto (que se puede obtener a través de event.origin), aceptas de forma explícita proporcionar al cliente una respuesta habilitada para CORS.
  • headers también es opcional y solo es útil si también especificas origin y muestras una respuesta de CORS. De forma predeterminada, en tu respuesta solo se incluirán los encabezados de la lista de encabezados de respuesta incluidos en la lista de entidades seguras de CORS. Si necesitas filtrar aún más lo que se muestra, los encabezados toman una lista de uno o más nombres de encabezados que usarán como una lista de entidades permitidas de los encabezados que deben exponerse en la respuesta. Esto te permite habilitar CORS y, al mismo tiempo, evitar que los encabezados de respuesta potencialmente sensibles se expongan directamente al cliente remoto.

Es importante tener en cuenta que, cuando se ejecuta el controlador foreignfetch, tiene acceso a todas las credenciales y la autoridad ambiente del origen que aloja el service worker. Como desarrollador que implementa un service worker externo habilitado para la recuperación, es tu responsabilidad asegurarte de no filtrar datos de respuesta con privilegios que no estarían disponibles de otra forma gracias a esas credenciales. Requerir la aceptación de respuestas de CORS es un paso para limitar la exposición involuntaria, pero como desarrollador puedes realizar solicitudes fetch() de forma explícita dentro de tu controlador foreignfetch que no usen las credenciales implícitas a través de lo siguiente:

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}))
    );
});

Consideraciones del cliente

Existen algunas consideraciones adicionales que afectan la manera en que tu service worker externo maneja las solicitudes que realizan los clientes de tu servicio.

Clientes que tienen su propio service worker propio

Es posible que algunos clientes de tu servicio ya tengan su propio service worker propio que administre solicitudes que se originan en su app web. ¿Qué significa esto para tu service worker externo externo?

Los controladores fetch de un service worker propio obtienen la primera oportunidad de responder a todas las solicitudes que realiza la app web, incluso si existe un service worker de terceros con foreignfetch habilitado con un alcance que abarca la solicitud. Sin embargo, los clientes con service workers propios pueden aprovechar el service worker externo.

En un service worker propio, el uso de fetch() para recuperar recursos de origen cruzado activará el service worker externo de recuperación correspondiente. Eso significa que un código como el siguiente puede aprovechar tu controlador 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));
});

Del mismo modo, si hay controladores de recuperación propios, pero no llaman a event.respondWith() cuando se manejan solicitudes para tu recurso de origen cruzado, la solicitud "caerá" automáticamente en el controlador 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.
});

Si un controlador fetch propio llama a event.respondWith(), pero no usa fetch() para solicitar un recurso en el alcance de recuperación externa, el service worker de recuperación externo no tendrá oportunidad de procesar la solicitud.

Clientes que no tienen su propio service worker

Todos los clientes que realizan solicitudes a un servicio de terceros pueden beneficiarse cuando el servicio implementa un service worker externo, incluso si todavía no usan su propio service worker. No hay nada específico que los clientes deban hacer para optar por usar un service worker externo, siempre que usen un navegador que lo admita. Esto significa que, si implementas un service worker externo, tu lógica de solicitud personalizada y tu caché compartida beneficiarán a muchos de los clientes de tu servicio de inmediato, sin que ellos realicen ninguna acción adicional.

Revisión general: dónde los clientes buscan una respuesta

Teniendo en cuenta la información anterior, podemos armar una jerarquía de fuentes que un cliente usará para encontrar una respuesta a una solicitud de origen cruzado.

  1. Un controlador fetch de un service worker propio (si está presente)
  2. El controlador foreignfetch de un service worker de terceros (si está presente y solo para solicitudes de origen cruzado)
  3. La caché HTTP del navegador (si existe una respuesta nueva)
  4. La red

El navegador se inicia desde la parte superior y, según la implementación del service worker, avanzará por la lista hasta que encuentre una fuente para la respuesta.

Más información

Mantente al día

La implementación de Chrome de la prueba de origen de recuperación externa está sujeta a cambios a medida que abordamos los comentarios de los desarrolladores. Mantendremos esta publicación actualizada mediante cambios intercalados e incluiremos los cambios específicos más abajo a medida que se realicen. También compartiremos información sobre cambios importantes a través de la cuenta de Twitter @chromiumdev.