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

Segundo plano

Los service workers les brindan a los desarrolladores web la capacidad de responder a las solicitudes de red que realizan sus aplicaciones web, lo que les permite seguir trabajando incluso sin conexión, luchar contra la falta de conexión e implementar interacciones complejas de almacenamiento en caché, como stale-while-revalidate. Pero, históricamente, los service workers están vinculados a un origen específico; como propietario de una app web, es tu responsabilidad escribir e implementar un service worker para interceptar todas las solicitudes de red que realiza tu app 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 a fuentes web.

¿Qué pasaría si un proveedor externo de una API, fuentes web o algún otro servicio de uso general tuviera el poder de implementar su propio servicio de trabajador que tuviera la oportunidad de controlar las solicitudes que otros orígenes realizan a su origen? Los proveedores pueden implementar su propia lógica de red 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 trabajador de servicio 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 independiente de la red de tu servicio, en la que los navegadores podrían aprovechar una caché de recursos común. Entre los servicios que podrían beneficiarse de esto, se incluyen los siguientes:

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

Imagina, por ejemplo, que eres un proveedor de herramientas de análisis. Si implementas un service worker de recuperación extranjero, puedes asegurarte de que todas las solicitudes a tu servicio que fallan mientras un usuario está sin conexión se pongan en cola y se vuelvan a reproducir una vez que se restablezca la conectividad. Si bien los clientes de un servicio pueden implementar un comportamiento similar a través de trabajadores del servicio propios, exigir que cada cliente escriba una lógica personalizada para tu servicio no es tan escalable como depender de un trabajador del servicio de recuperación externo compartido que implementes.

Requisitos previos

Token de prueba de origen

La recuperación de fuentes externas aún se considera experimental. Para evitar implementar este diseño antes de tiempo, antes de que los proveedores de navegadores lo especifiquen y aprueben por completo, se implementó en Chrome 54 como una prueba de origen. Mientras la recuperación externa siga siendo experimental, para usar esta nueva función con el servicio que alojas, deberás solicitar un token que esté centrado en el origen específico de tu servicio. El token se debe incluir como un encabezado de respuesta HTTP en todas las solicitudes entre orígenes de recursos que deseas controlar a través de la recuperación externa, así como en la respuesta del recurso de JavaScript de tu trabajador del servicio:

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 en ese momento, la funcionalidad vinculada a los tokens de Origin Trial existentes dejará de funcionar.

Para facilitar la experimentación con la recuperación externa antes de registrarte para obtener un token oficial de la Prueba de origen, 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 experimentales de la plataforma web". Ten en cuenta que esto se debe hacer en cada instancia de Chrome que quieras usar en tus experimentaciones locales. En cambio, con un token de prueba de origen, la función estará disponible para todos los usuarios de Chrome.

HTTPS

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

Usa la recuperación externa

Ahora que ya no incluiremos los requisitos previos, veamos los detalles técnicos necesarios para poner en marcha un service worker externo.

Registra tu service worker

El primer desafío que es probable que encuentres es cómo 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, que se activa cuando un usuario navega a una URL que controlas. Sin embargo, no es un enfoque viable para registrar un service worker externo, cuando la única interacción que el navegador tendrá con tu servidor será solicitando un subrecurso específico, no una navegación completa. Si el navegador solicita, por ejemplo, una imagen de un servidor de CDN que mantienes, no puedes anteponer ese fragmento de JavaScript a tu respuesta y esperar que se ejecute. Se requiere un método diferente de registro del trabajador de servicio, fuera del contexto de ejecución normal de JavaScript.

La solución se presenta en forma de 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 trabajador de servicio (reemplaza /service-worker.js por la ruta de acceso correcta a tu secuencia de comandos). Esto corresponde directamente a la cadena scriptURL que, de otro modo, se pasaría como primer parámetro a navigator.serviceWorker.register(). El valor debe encerrarse entre caracteres <> (como lo exige la especificación del encabezado Link) y, si se proporciona una URL relativa en lugar de absoluta, se interpretará como relativa a la ubicación de la respuesta.
  • rel="serviceworker" también es obligatorio y debe incluirse sin necesidad de personalización.
  • scope=/ es una declaración de alcance opcional, equivalente a la cadena options.scope que puedes pasar como segundo parámetro a navigator.serviceWorker.register(). En muchos casos de uso, no es necesario usar el alcance predeterminado, así que no dudes en omitir este paso, a menos que sepas que lo necesitas. Las mismas restricciones sobre el alcance máximo permitido, junto con la capacidad de flexibilizarlas a través del encabezado Service-Worker-Allowed, se aplican a los registros del encabezado Link.

Al igual que con un registro de service worker "tradicional", el uso del encabezado Link instalará un service worker que se usará para la siguiente solicitud realizada en el alcance registrado. El cuerpo de la respuesta que incluye el encabezado especial se usará tal como está y estará disponible para la página de inmediato, sin esperar a que el trabajador del servicio externo termine la instalación.

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

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

Registro de depuración

Durante el desarrollo, es probable que desees confirmar que tu service worker de recuperación externa esté instalado correctamente y esté procesando las solicitudes. Hay algunos aspectos que puedes verificar en las Herramientas para desarrolladores de Chrome para confirmar que todo funcione según lo esperado.

¿Se están enviando los encabezados de respuesta adecuados?

Para registrar el service worker de recuperación externo, debes establecer un encabezado Link en una respuesta a un recurso alojado en tu dominio, como se describió anteriormente en esta publicación. Durante el período de la prueba de origen, y si no tienes configurado chrome://flags/#enable-experimental-web-platform-features, también debes configurar un encabezado de respuesta Origin-Trial. Para confirmar que tu servidor web configura esos encabezados, consulta la entrada en el panel de red de DevTools:

Encabezados que se muestran en el panel Red

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

También puedes confirmar el registro subyacente del service worker, incluido su alcance, si consultas la lista completa de service workers en el panel Application de DevTools. Asegúrate de seleccionar la opción “Mostrar todo”, ya que, de forma predeterminada, solo verás los trabajadores del servicio para el origen actual.

El servicio de trabajador de recuperación externo en el panel Aplicaciones

El controlador de eventos de instalación

Ahora que registraste tu service worker de terceros, tendrá la oportunidad de responder a los eventos install y activate, como lo haría cualquier otro service worker. Puedes aprovechar esos eventos, por ejemplo, para 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']
    });
});

Existen 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, es posible que 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 al alcance general del service worker o un subalcance del service worker. Las restricciones de alcance adicionales que se incluyen aquí te permiten implementar un trabajador de servicio multipropósito que pueda controlar eventos fetch propios (para solicitudes realizadas desde tu propio sitio) y eventos fetch de terceros (para solicitudes realizadas desde otros dominios), y dejar en claro que solo un subconjunto de tu alcance más amplio debe activar foreignfetch.foreignfetch En la práctica, si implementas un service worker dedicado a controlar solo eventos de foreignfetch de terceros, solo querrás usar un alcance explícito único 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 cadenas y te permite restringir tu controlador foreignfetch para que solo responda a solicitudes de dominios específicos. Por ejemplo, si permites de forma explícita "https://example.com", una solicitud realizada desde una página alojada en https://example.com/path/to/page.html para un recurso que se entrega desde tu alcance de recuperación externa activará tu controlador de recuperación externa, pero las solicitudes realizadas desde https://random-domain.com/path/to/page.html no activarán tu 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 del array y se permitirán todos los orígenes.

El controlador de eventos externalfetch

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

En un service worker tradicional de terceros, cada solicitud activaría un evento fetch al que tu service worker podría responder. Nuestro service worker externo 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, de manera opcional, proporcionarle una respuesta 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 proporcionar solo un Response (o un Promise que se resuelve con un Response) a respondWith(), como haces con un FetchEvent, debes pasar un Promise que se resuelva con un objeto con propiedades específicas al respondWith() de ForeignFetchEvent:

  • response es obligatorio y se debe establecer en el objeto Response que se mostrará al cliente que realizó la solicitud. Si proporcionas cualquier elemento que no sea un Response válido, la solicitud del cliente se 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 resuelva con un Response. Puedes construir 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. Si no lo haces, 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', se considerará un error mostrar una respuesta opaca. Sin embargo, si especificas un valor de cadena igual al origen del cliente remoto (que se puede obtener a través de event.origin), aceptas de forma explícita proporcionarle al cliente una respuesta habilitada para CORS.
  • headers también es opcional y solo es útil si también especificas origin y devuelves 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, headers toma una lista de uno o más nombres de encabezados y la usará como una lista de entidades permitidas de los encabezados que se mostrarán en la respuesta. Esto te permite habilitar el 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 ambiental del origen que aloja el trabajador de servicio. Como desarrollador que implementa un trabajador de servicio habilitado para la recuperación externa, es tu responsabilidad asegurarte de no filtrar ningún dato de respuesta con privilegios que, de otro modo, no estaría disponible debido a esas credenciales. Requerir una habilitación para las 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 forma en que el trabajador del servicio de recuperación externo controla 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 y se ocupe de las solicitudes que se originan en su app web. ¿Qué significa esto para tu service worker externo de recuperación de datos?

Los controladores fetch de un service worker propio tienen la primera oportunidad de responder a todas las solicitudes que realiza la app web, incluso si hay un service worker externo con foreignfetch habilitado con un alcance que cubra la solicitud. Pero los clientes con service workers propios aún pueden aprovecharse del service worker de recuperación externa.

Dentro de un servicio de trabajo propio, usar fetch() para recuperar recursos de varios orígenes activará el servicio de trabajo de recuperación externo adecuado. 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 manejan solicitudes para tu recurso de origen cruzado, la solicitud "fallará" automáticamente en tu 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 dentro de tu alcance de recuperación externa, tu service worker de recuperación externa no tendrá la oportunidad de controlar la solicitud.

Clientes que no tienen su propio trabajador de servicio

Todos los clientes que realizan solicitudes a un servicio de terceros pueden beneficiarse cuando el servicio implementa un service worker de recuperación externo, incluso si aún no usan su propio service worker. No hay nada específico que los clientes deban hacer para aceptar el uso de un service worker de recuperación externo, siempre y cuando usen un navegador compatible. Esto significa que, si implementas un trabajador de servicio de recuperación externo, tu lógica de solicitud personalizada y la caché compartida beneficiarán a muchos de los clientes de tu servicio de inmediato, sin que deban seguir otros pasos.

Revisión general: donde los clientes buscan una respuesta

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

  1. El controlador fetch de un service worker propio (si está presente)
  2. El controlador foreignfetch de un trabajador del servicio de terceros (si está presente y solo para solicitudes entre orígenes)
  3. La caché HTTP del navegador (si existe una respuesta reciente)
  4. La red

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

Más información

Mantente al día

La implementación de la prueba de origen de recuperación externa de Chrome está sujeta a cambios a medida que respondemos los comentarios de los desarrolladores. Mantendremos esta publicación actualizada con cambios intercalados y tomaremos nota de los cambios específicos a continuación a medida que ocurran. También compartiremos información sobre los cambios importantes a través de la cuenta de Twitter @chromiumdev.