# # 整合 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，当然，开发环境例外。`localhost` 和 `127.0.0.1` 可以在不启用 HTTPS 访问的情况下使用 ServiceWorker。

## # ServiceWorker 注册和配置

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

`messageSyncServiceWorker` 有两种用法：

1. 方法 1: 指定 `url` 和 `options`:

   - `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`:

   - `registration` 为 `navigator.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();
           }
       });

## # ServiceWorker 的使用场景

Foxit PDF SDK for Web 使用 ServiceWorker 来优化用户的交互体验并解决一些交互相关的问题。目前，ServiceWorker 已被用于处理访问本地字体授权, 填表功能和执行表单、动态 XFA 的一些 JavaScript 脚本的 UI。