跨域 Service Worker - 使用外部提取进行实验

背景

借助服务工件,Web 开发者能够响应其 Web 应用发出的网络请求,从而让应用即使在离线状态下也能继续运行、防范网络欺骗,以及实现在重新验证时过时等复杂的缓存交互。但服务工件历来都与特定来源相关联。作为 Web 应用的所有者,您有责任编写和部署服务工件,以拦截 Web 应用发出的所有网络请求。在该模型中,每个服务工件都负责处理跨源请求,例如对第三方 API 或 Web 字体的请求。

如果 API、Web 字体或其他常用服务的第三方提供商能够部署自己的服务工件,并且该服务工件有机会处理其他来源向其来源发出的请求,该怎么办?提供程序可以实现自己的自定义网络逻辑,并利用单个权威性缓存实例来存储其响应。现在,得益于外部提取功能,这种类型的第三方服务工件部署已成为现实。

对于通过浏览器的 HTTPS 请求访问的服务的任何提供商,部署实现外部提取的服务工件都是有意义的。只需考虑您可以提供哪些场景中可独立于网络的服务版本,浏览器可以在其中利用公共资源缓存。受益于此的服务包括但不限于:

  • 提供RESTful 接口的 API 提供方
  • 网页字体提供商
  • 分析工具提供商
  • 图片托管服务提供商
  • 通用内容分发网络

例如,假设您是一名分析服务提供商。通过部署外部提取服务 worker,您可以确保在用户离线时对您的服务失败的所有请求都会加入队列,并在连接恢复后重放。虽然服务的客户端可以通过第一方服务工件实现类似行为,但要求每个客户端都为您的服务编写专用逻辑,其可扩展性不如依赖于您部署的共享外部提取服务工件。

前提条件

来源试用令牌

外部提取功能仍处于实验阶段。为了避免在浏览器供应商全面指定并同意此设计之前过早将其纳入,我们在 Chrome 54 中将其作为源试用进行了实现。只要外部提取功能仍处于实验阶段,如果您想将此新功能与您托管的服务搭配使用,就需要请求令牌,该令牌的范围应限定为您服务的特定来源。您应在要通过外部提取处理的资源的所有跨源请求以及服务工件 JavaScript 资源的响应中,将令牌作为 HTTP 响应标头包含在内:

Origin-Trial: token_obtained_from_signup

该试用期将于 2017 年 3 月结束。届时,我们预计已确定稳定该功能所需的所有更改,并(希望)默认启用该功能。如果到那时默认未启用外部提取功能,与现有 Origin 试用令牌相关联的功能将停止运行。

为了便于您在注册官方 Origin 试用令牌之前对外部提取进行实验,您可以在 Chrome 中前往 chrome://flags/#enable-experimental-web-platform-features 并启用“实验性 Web 平台功能”标志,从而绕过对本地计算机的要求。请注意,您需要在要用于本地实验的每个 Chrome 实例中执行此操作,而使用 Origin Trial 令牌时,所有 Chrome 用户都可以使用此功能。

HTTPS

与所有服务工件部署一样,您用于提供资源和服务工件脚本的 Web 服务器需要通过 HTTPS 访问。此外,外部提取拦截仅适用于源自在安全来源上托管的网页的请求,因此您的服务客户端需要使用 HTTPS 才能利用您的外部提取实现。

使用外部提取

前置条件已解决,接下来我们来深入了解启动和运行外部提取服务工作器所需的技术细节。

注册 Service Worker

您可能会遇到的第一个难题是如何注册 Service Worker。如果您之前使用过服务工件,可能熟悉以下内容:

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

在 Web 应用上下文中,此用于注册第一方服务工件的 JavaScript 代码非常有用,它会由用户导航到您控制的网址触发。但是,如果浏览器与您的服务器的唯一互动是请求特定子资源(而非完整导航),则无法通过这种方式注册第三方服务工件。如果浏览器请求从您维护的 CDN 服务器获取图片,您无法将该 JavaScript 代码段附加到响应开头,并期望它会运行。您需要在正常 JavaScript 执行上下文之外使用其他服务工件注册方法。

解决方案采用 HTTP 标头的形式,您的服务器可以在任何响应中添加此标头:

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

我们将该示例标头拆解为其组成部分,每个组成部分由 ; 字符分隔。

  • </service-worker.js> 是必需的,用于指定服务工件文件的路径(将 /service-worker.js 替换为脚本的适当路径)。这直接对应于 scriptURL 字符串,否则该字符串会作为第一个参数传递给 navigator.serviceWorker.register()。该值需要用 <> 字符括起来(如 Link 标头规范所要求),如果提供的是相对网址(而非绝对网址),则系统会将其解读为相对于响应位置
  • rel="serviceworker" 也是必需的,并且应包含在内,无需任何自定义。
  • scope=/ 是一个可选的范围声明,相当于您可以作为第二个参数传入 navigator.serviceWorker.register()options.scope 字符串。对于许多用例,使用默认范围即可,因此除非您知道自己需要此参数,否则可以随意忽略。Link 标头注册同样受到与允许的最大范围相关的限制,并且可以通过 Service-Worker-Allowed 标头放宽这些限制。

与“传统”服务工件注册一样,使用 Link 标头会安装一个服务工件,该服务工件将用于针对已注册的范围发出的下一个请求。包含特殊标头的响应正文将按原样使用,并立即可供页面使用,而无需等待外部服务工件完成安装。

请注意,外部提取目前是作为源代码试用实现的,因此除了 Link 响应标头之外,您还需要添加有效的 Origin-Trial 标头。若要注册外部提取服务 worker,至少需要添加以下一组响应标头:

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

调试注册

在开发过程中,您可能需要确认外部提取服务 worker 是否已正确安装并在处理请求。您可以在 Chrome 的开发者工具中检查一些内容,以确认一切是否按预期运行。

是否发送了正确的响应标头?

如需注册外部提取服务 worker,您需要在对您网域上托管的资源的响应中设置 Link 标头,如本文前面所述。在源站试用期间,假设您未设置 chrome://flags/#enable-experimental-web-platform-features,则还需要设置 Origin-Trial 响应标头。您可以通过查看开发者工具的“网络”面板中的条目,确认您的 Web 服务器是否设置了这些标头:

“网络”面板中显示的标头。

Foreign Fetch Service Worker 是否已正确注册?

您还可以查看 DevTools 的“应用”面板中服务工件的完整列表,以确认底层服务工件注册(包括其作用域)。请务必选择“全部显示”选项,因为默认情况下,您只会看到当前源的服务工件。

“应用”面板中的外部提取 Service Worker。

安装事件处理脚本

现在,您已注册第三方服务工件,它将有机会响应 installactivate 事件,就像任何其他服务工件一样。例如,它可以利用这些事件在 install 事件期间使用所需资源填充缓存,或在 activate 事件中修剪过时缓存。

除了正常的 install 事件缓存 activity 之外,第三方服务工件的 install 事件处理脚本中还有一个必需的额外步骤。您的代码需要调用 registerForeignFetch(),如以下示例所示:

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

有两个配置选项,这两个选项都是必需的:

  • scopes 接受一个或多个字符串的数组,每个字符串都代表将触发 foreignfetch 事件的请求的范围。不过,您可能会想,我在注册 Service Worker 时已经定义了作用域!没错,该总体作用域仍然适用 - 您在此处指定的每个作用域都必须等于或为服务工件的总体作用域的子作用域。借助此处的额外范围限制,您可以部署一个通用服务工件,该工件既可以处理第一方 fetch 事件(针对从您自己的网站发出的请求),也可以处理第三方 foreignfetch 事件(针对从其他网域发出的请求),并明确说明只有更大范围的一部分应触发 foreignfetch。在实践中,如果您要部署一个仅用于处理第三方 foreignfetch 事件的服务工件,只需使用与服务工件的整体作用域相同的单个显式作用域即可。上面的示例将使用值 self.registration.scope 执行此操作。
  • origins 还接受一个或多个字符串的数组,可让您将 foreignfetch 处理脚本限制为仅响应来自特定网域的请求。例如,如果您明确允许“https://example.com”,那么从托管在 https://example.com/path/to/page.html 的网页发出针对从您的外部提取作用域提供的资源的请求将触发您的外部提取处理脚本,但从 https://random-domain.com/path/to/page.html 发出的请求不会触发您的处理脚本。除非您有特定原因需要仅针对部分远程来源触发外部提取逻辑,否则只需将 '*' 指定为数组中的唯一值,系统便会允许所有来源。

foreignfetch 事件处理脚本

现在,您已安装第三方 Service Worker 并通过 registerForeignFetch() 对其进行了配置,它将有机会拦截属于外部提取范围的针对您服务器的跨源子资源请求

在传统的第一方 Service Worker 中,每个请求都会触发 fetch 事件,您的 Service Worker 有机会对其做出响应。我们的第三方服务工件有机会处理一个略有不同的事件,名为 foreignfetch。从概念上讲,这两种事件非常相似,它们让您有机会检查传入请求,并可选择通过 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']
        };
    })
    );
});

尽管在概念上有相似之处,但在实践中,对 ForeignFetchEvent 调用 respondWith() 时还是存在一些差异。您需要将可解析为具有特定属性的对象的 Promise 传递给 ForeignFetchEventrespondWith(),而不是像使用 FetchEvent 那样,仅向 respondWith() 提供 Response(或可解析为 ResponsePromise)。

  • response 是必需的,必须设置为将返回给发出请求的客户端的 Response 对象。如果您提供的不是有效的 Response,客户端的请求将会因网络错误而终止。与在 fetch 事件处理程序内调用 respondWith() 不同,您必须在此处提供 Response,而不是使用 Promise(会解析为 Response)!您可以通过 promise 链构建响应,并将该链作为参数传递给 foreignfetchrespondWith(),但该链必须解析为包含 response 属性(已设置为 Response 对象)的对象。您可以在上面的代码示例中查看此示例。
  • origin 是可选的,用于确定返回的响应是否为不透明响应。如果您不添加此属性,响应将是不可透的,并且客户端对响应的正文和标头的访问权限将受到限制。如果请求是使用 mode: 'cors' 发出的,则返回不透明响应将被视为错误。不过,如果您指定的字符串值等于远程客户端的源(可通过 event.origin 获取),则表示您明确选择向客户端提供启用了 CORS 的响应。
  • headers 也是可选项,只有在您还指定了 origin 并返回 CORS 响应时,它才有用。默认情况下,只有 CORS 安全名单中的响应标头列表中的标头才会包含在响应中。如果您需要进一步过滤返回的内容,则可以使用“headers”接受一个或多个标头名称的列表,并将其用作在响应中公开哪些标头的许可名单。这样,您就可以选择启用 CORS,同时仍能防止可能敏感的响应标头直接公开给远程客户端。

请务必注意,运行 foreignfetch 处理程序时,它可以访问托管服务工件的来源的所有凭据和环境权限。作为部署启用了外部提取功能的服务工件的开发者,您有责任确保不会泄露任何凭据无法提供的特权响应数据。要求用户选择启用 CORS 响应是限制无意中泄露信息的措施之一,但作为开发者,您可以通过以下方式在 foreignfetch 处理程序中明确发出 fetch() 请求,不使用隐含的凭据

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

客户端注意事项

还有一些其他注意事项会影响外部提取服务 worker 处理来自服务客户端的请求的方式。

具有自己的第一方服务工件的客户端

您服务的某些客户端可能已经有自己的第一方服务工件,用于处理来自其 Web 应用的请求。这对您的第三方外部提取服务工件有何影响?

第一方服务工件中的 fetch 处理脚本有机会优先响应 Web 应用发出的所有请求,即使存在已启用 foreignfetch 且范围涵盖相应请求的第三方服务工件也是如此。不过,具有第一方 Service Worker 的客户端仍然可以使用您的外部提取 Service Worker!

在第一方服务工件中,使用 fetch() 检索跨源资源会触发相应的外部提取服务工件。这意味着,以下代码可以利用 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));
});

同样,如果存在第一方提取处理脚本,但在处理跨源资源的请求时未调用 event.respondWith(),则请求会自动“回传”到 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.
});

如果第一方 fetch 处理脚本调用 event.respondWith(),但使用 fetch() 请求外部提取范围下的资源,则外部提取服务工作器将无法处理该请求。

没有自己的服务工件的客户端

当第三方服务部署外部提取服务工作器时,向该服务发出请求的所有客户都会受益,即使他们尚未使用自己的服务工作器也是如此。客户端无需执行任何特定操作即可选择使用外部提取服务 worker,前提是他们使用的是支持该 worker 的浏览器。这意味着,通过部署外部提取服务 worker,您的自定义请求逻辑和共享缓存将立即为服务的许多客户端带来好处,而无需他们采取进一步措施。

总结:客户在哪里查找响应

考虑到上述信息,我们可以组合出客户端将用来查找跨源请求响应的来源层次结构。

  1. 第一方服务工件的 fetch 处理脚本(如果有)
  2. 第三方服务工件的 foreignfetch 处理脚本(如果有,仅适用于跨源请求)
  3. 浏览器的 HTTP 缓存(如果存在新鲜响应)
  4. 广告联盟

浏览器会从顶部开始,并根据服务工件实现继续向下遍历列表,直到找到响应的来源。

了解详情

随时掌握最新动态

随着我们解决开发者反馈的问题,Chrome 对外部提取源试用版的实现可能会发生变化。我们会通过内嵌更改及时更新这篇文章,并在有具体更改时在下方注明。我们还会通过 @chromiumdev Twitter 账号分享有关重大变更的信息。