# 整合 ServiceWorker

# 目的

本节旨在为开发者提供一个全面的指南,用于在应用层集成和整合 SDK 内置的 ServiceWorker 功能。并解决 ServiceWorker 冲突问题和常见问题。

虽然浏览器允许在同一个域中注册多个 ServiceWorker, 但这些 ServiceWorker 必须拥有不同的作用域 (scope)。这样,每个 ServiceWorker 才可以独立控制不同作用域下的资源。然而,MessageWorker.js 占用了一个作用域(默认为 '/'), 这将导致应用层的 ServiceWorker 无法在相同的作用域下注册成功。因此,当应用层的 ServiceWorker 需要与 SDK 内置的 MessageWorker.js 共享同一个作用域时,就必须将其合并为同一个文件。

本节将介绍如何合并和自定义与 ServiceWorker 配置相关的参数。

# ServiceWorker 简介

ServiceWorker 是在浏览器后台运行的脚本,可以用来实现离线功能、推送通知、后台同步等功能。在网页完全加载后,其在后台运作,可以拦截网络请求、缓存资源,并提供其他功能。

ServiceWorker 的主要功能:

  • 离线功能: ServiceWorker 可以缓存资源,使用户在断网时仍能访问网站。
  • 推送通知: ServiceWorker 可以从服务器接收推送通知并向用户展示消息。
  • 后台同步: ServiceWorker 可以在后台执行任务,如上传数据或更新数据。
  • 网络拦截: ServiceWorker 可以拦截网络请求并按需处理,例如缓存数据或修改请求头。

# MessageWorker.js 概述

MessageWorker.js 是 SDK 提供的一个 ServiceWorker 脚本,用于在 Web Worker 层实现同步通信。由于 MessageWorker.js 占用了独立的作用域,当应用层的 ServiceWorker 需要与其使用同一个作用域时,就需要将两者合并到一个文件中。

# 环境设置

在开始集成 ServiceWorker 之前,请确保您的开发环境满足以下要求:

  • 支持 ServiceWorker 的浏览器: 确保您的目标浏览器支持 ServiceWorker,例如 Chrome,Firefox 和 Edge。如需更具体的信息,可以参考 Can I use (opens new window)

  • @foxitsoftware/foxit-pdf-sdk-for-web-library: 版本必须是 10.0.0 或更高,且 lib 目录应包含 MessageWorker.js 文件。

  • HTTPS 支持: 您的网站必须通过 HTTPS 访问才能使用 ServiceWorker,当然,开发环境例外。localhost127.0.0.1 可以在不启用 HTTPS 访问的情况下使用 ServiceWorker。

# ServiceWorker 注册和配置

10.0.0 版本开始, PDFViewer 构造函数参数中新增了 messageSyncServiceWorker (opens new window) 参数, 用于指定 ServiceWorker 的注册方式。

messageSyncServiceWorker 有两种用法:

  1. 方法 1: 指定 urloptions:

    • url 为 ServiceWorker 的注册路径;
    • options 为 ServiceWorker 的注册选项,可以参考 MDN (opens new window) 中的具体描述。
    const viewer = new PDFViewer({
        messageSyncServiceWorker: {
            url: "/your-service-worker.js",
            options: {
                // Optional
                scope: "/foxit-lib/",
            },
        },
        // ... Other parameters
    });
    
  2. 方法 2: 指定 registration:

    • registrationnavigator.serviceWorker.register() 方法返回的 Promise<ServiceWorkerRegistration> 对象。
    const viewer = new PDFViewer({
        messageSyncServiceWorker: {
            registration: navigator.serviceWorker.register(
                "/your-service-worker.js",
                {
                    scope: "/foxit-lib/",
                }
            ),
        },
        // ... Other parameters
    });
    

方法 1 将由 SDK 内部决定何时注册 ServiceWorker。如果您需要手动控制 ServiceWorker 的注册,必须使用方法 2。

在 SDK 发布包中,我们提供了一个完整的示例 (/examples/PDFViewCtrl/integrate-service-worker)。您可以直接参考其实现,并根据 README.md 文档运行和查看效果。

# Service-Worker-Allowed 响应头

默认情况下,ServiceWorker 允许的最大作用范围由其脚本位置决定。具体来说,ServiceWorker 的作用范围只能覆盖其脚本所在的目录及其子目录。例如,如果 ServiceWorker 脚本位于 https://example.com/sub/worker.js,则默认情况下,它只能控制 https://example.com/sub/ 及其子路径下的资源。如果将 scope 参数强行指定为更大作用范围,将导致 ServiceWorker 注册失败,并且报错: The path of the provided scope ('/') is not under the max scope allowed ('/sub/')

然而,在某些情况下,您可能希望扩大 ServiceWorker 的作用范围,以便其能够控制更大范围内的资源。这时候,Service-Worker-Allowed 响应头就显得尤为重要。通过配置此响应头,您可以指定更广泛的路径,允许 ServiceWorker 在更大的范围内生效。

# 配置 Service-Worker-Allowed 响应头

要使用 Service-Worker-Allowed 响应头,您需要在 ServiceWorker 脚本的 HTTP 响应头中添加以下字段。其值为允许的最大作用范围路径:

Service-Worker-Allowed /;

# Nginx 配置示例

如果您使用 Nginx 作为服务器,可以通过修改 Nginx 配置文件来添加 Service-Worker-Allowed 响应头。以下是一个配置示例:

server {
    location /sw.js {
        add_header Service-Worker-Allowed /;
    }
}

# Webpack Dev Server 配置示例

如果您使用 Webpack Dev Server 进行本地开发,可以通过配置 devServer 添加 Service-Worker-Allowed 响应头。以下是一个配置示例:

// webpack.config.js
module.exports = {
    //  Other configurations
    devServer: {
        headers: {
            "Service-Worker-Allowed": "/",
        },
    },
};

# vue.config.js 配置示例

如果您使用 Vue CLI,可以通过修改 vue.config.js 来调整 Webpack Dev Server。以下是一个配置示例:

// vue.config.js
module.exports = {
    devServer: {
        headers: {
            "Service-Worker-Allowed": "/",
        },
    },
};

# 特殊的请求地址

在应用层的 ServiceWorker 监听的 fetch 事件中,如果请求地址匹配 __foxitwebsdk-syncmsg__,请直接忽略此请求。这也在我们的示例代码中有提到(examples/PDFViewCtrl/integrate-service-worker/src/service-worker.js)。

# 常见问题和故障排查

  1. ServiceWorker 未注册

    • 问题描述: 在开发者工具中无法找到 ServiceWorker 的注册结果。

    • 可能原因:

      1. 路径设置不正确,ServiceWorker js 请求返回 404 或其他错误。
      2. 浏览器不支持 ServiceWorker。
      3. 出于安全考虑,ServiceWorker 只能在 HTTPS 协议下使用。这里有个例外,localhost 和 127.0.0.1 不需要 HTTPS。
    • 解决方法:

      1. 检查 ServiceWorker 的注册代码和路径设置。
      2. 检查浏览器兼容性,ServiceWorker 兼容性请参考 Can I use (opens new window)
      3. 开启 HTTPS 协议。
  2. ServiceWorker 注册失败

    • 问题描述:注册时提示作用域超出最大允许范围。错误信息示例:
        register a ServiceWorker for scope ('http://localhost:9899/') with script ('http://localhost:9899/lib/MessageWorker.js?b=http://localhost:9899/__foxitwebsdk-syncmsg__'): The path of the provided scope ('/') is not under the max scope allowed ('/lib/'). Adjust the scope, move the Service Worker script, or use the Service-Worker-Allowed HTTP header to allow the scope.
    
    • 可能原因: ServiceWorker 的注册路径或作用域设置不正确。需要注意的是,ServiceWorker 允许的最大作用范围取决于 ServiceWorker 脚本本身的位置 (参考: MDN (opens new window))。

    • 解决方法:

      1. 理解 ServiceWorker 的作用域规则。
      2. 检查 ServiceWorker 的注册代码和作用域设置。
      3. 调整 ServiceWorker 的作用域,即修改 scope (opens new window) 参数。
      4. 调整 Service-Worker-Allowed HTTP 响应头以允许更大的作用域。
      5. 检查 ServiceWorker 脚本的路径和服务器配置。
  3. ServiceWorker 注册提示 MIME 不支持

    • 问题描述: 注册时提示 unsupported MIME type ('/')。错误信息示例:
    Failed to register a ServiceWorker for scope ('http://localhost:5173/assets/') with script ('http://localhost:5173/assets/service-worker.js'): The script has an unsupported MIME type ('text/html').
    
    • 可能原因: ServiceWorker 的注册路径错误或目标文件不存在。
    • 解决方法:
      1. 检查 ServiceWorker 的注册路径。
      2. 检查目标文件是否存在。
      3. 检查服务器配置。
  4. 浏览器强制刷新后, ServiceWorker 无法拦截请求

    • 问题描述:参考 MDN: ServiceWorkerContainer: controller property (opens new window) 文档描述,当用户强制刷新(Shift + Refresh)页面后,service worker 会失去页面的控制权,并且 navigator.serviceWorker.controller 属性为 null。
    • 可能原因: SDK 的内置 MessageWorker.js 已经自动处理这个问题,如果您的 ServiceWorker 代码没有复用 MessageWorker.js, 并且未处理此情况,则可能出现这个现象。
    • 解决方法:
      1. 在成功注册 serviceWorker 后检查 navigator.serviceWorker.controller 是否为 null。
      2. 如果为 null, 则通过 registration (opens new window).active.postMessage 方法向 ServiceWorker 发送消息, 比如我们发送的消息为: {id: 'clientsClaim'}
      3. 在 ServiceWorker 上下文中监听此消息,并在收到消息后调用 clients.claim() 方法。

    下面是一个简单的示例:

    main.js:

    const registration = await navigator.serviceWorker.register(
        "service-worker.js" /* ... more options */
    );
    if (!navigator.serviceWorker.controller && registration.active) {
        function onControllerChange() {
            navigator.serviceWorker.removeEventListener(
                "controllerchange",
                onControllerChange
            );
            if (navigator.serviceWorker.controller) {
                resolve(true);
            }
        }
        navigator.serviceWorker.addEventListener(
            "controllerchange",
            onControllerChange
        );
        registration.active.postMessage({ id: "clientsClaim" });
    }
    const pdfViewer =  new PDFViewCtrl.PDFViewer({
       messageSyncServiceWorker: {
           registration: registration,
       }
    }
    

    service-worker.js

    // ......
    self.addEventListener("message", (e) => {
        if (e.data?.id === "clientsClaim") {
            // 将当前 service worker 设置为其 scope 内所有 clients 的 controller
            clients.claim();
        }
    });