javascript - Active ServiceWorker 并不总是拦截请求

标签 javascript reactjs typescript service-worker

我有一个带有 ServiceWorker 的 React 应用程序,它的目的是拦截某些请求(API 调用和资源获取)并添加一个带有适当 token 的 Authorization header 。在大多数情况下,它工作得很好,但在某些情况下,service worker 不处理获取事件,导致 401 响应。


react 方面 ⚛️

我们有以下函数来注册 service worker 并等待它被激活:

function initializeServiceWorker() {
  return new Promise((resolve, reject) => {
    navigator
      .serviceWorker
      .register("/tom.js")
      .then((registration) => {

        // checks the state of the service worker until it's activated
        // and move on with the then-chain
        return pollUntil(
          (registration) => registration.active?.state === "activated",
          POLL_EVERY,      // 40ms
          registration,
          POLL_TIMEOUT     // 6000ms
        );

      })
      .then((registration) => {

        // here we send the authentication token to the service worker
        // to be stored in the IndexedDB overriding the old one
        // and persist across termination/reactivation
        registration.postMessage(<NewAuthToken>{ type: "AUTH_TOKEN", token });

      })
      .catch(reject);
  });
}

Service Worker 端 🤖

我们的 service worker 为 active、install、message 和 fetch 事件设置处理程序如下:


// eslint-disable-next-line no-restricted-globals
const worker = self as any as ServiceWorkerGlobalScope;
// NOTE: we refer to self as `worker` after performing a couple of type casts


worker.addEventListener("install", () => {
  // this ensure that updates to the underlying service worker take effect
  // immediately for both the current client and all other active clients
  worker.skipWaiting();
});

worker.addEventListener("activate", async (event: ExtendableEvent) => {
  // https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle#activate
  // The first activation does not connect / intercept already running clients. 
  // "claim" claims any already running clients.
  event.waitUntil(worker.clients.claim());
});

worker.addEventListener("message", (event: ExtendableMessageEvent) => {
  // here we store the token to IndexedDB
});

worker.addEventListener("fetch", (event: FetchEvent) => {
  event.respondWith(
    (async function respond() {
      try {
        const token = await DB.getToken();

        // no token - no party
        // just send the request as is
        if (!token) return fetch(event.request);

        // going somewhere else?
        // no need to add the token to the request
        const sourceHost = new URL(worker.location.origin).host;
        const destinationHost = new URL(event.request.url).host;
        if (destinationHost !== sourceHost) {
          return fetch(event.request);
        }

        // already authorized?
        // ready to go as it is
        if (event.request.headers.get("Authorization")) {
          return fetch(event.request);
        }

        /**
         * When the `destination` property of the request is empty, it means that the
         * request is an API call, otherwise it would have contained the DOM element
         * that initialized the request (image, script, style, etc.)
         *
         * In this case, we can smoothly add the token in the Headers.
         */
        if (event.request.destination === "") {
          const headers = new Headers(event.request.headers);
          headers.append("Authorization", token);
          const enhancedRequest = new Request(event.request, { headers });

          return await fetch(enhancedRequest);
        }

        // when this get executed it means the browser wants
        // the content of an <img> tag or some script or styling
        // ... but this is not the point of this SO question ...
        return requestResource(event.request, token);

      } catch (error) {
        // something horrible happen?
        // let the original request to reach it's destination freely
        return fetch(event.request);
      }
    })()
  );
});



那么,问题是什么? 🤔

在执行我们的第一个 API 调用之前,我们等待 initializeServiceWorker 方法,如下所示:


// in an async function...

await initializeServiceWorker();

const homeModel = await api.environment.getClientSettings();

在某些情况下(谢天谢地,比例很小,如 <5%),getClientSettings api 调用以 401 结束,没有添加 Authorization header 。

那么,这怎么可能呢? initializeServiceWorker promise 在调用 api 之前解决并且 token 仍然不存在怎么可能?

这个初始化/service worker 代码有意义吗?你有没有看到任何已经从 6 只眼睛中漏过的重大或微妙的错误?我们是不是在做一些疯狂的事情? 🤷‍♂️

让我知道🤞


你可能想知道的奇怪事情 🤨

为什么要等到 service worker 被激活?

我们注意到,有时激活需要的时间比必要的多一点,我们确实需要等待它准备就绪,然后再继续我们的代码。

--

你怎么知道它根本没有拦截?

为了确定我们是否没有添加 Authorization header 或者我们根本没有拦截请求,我们决定始终添加一个额外的 X-SW header 并在后端跟踪失败与此新“标记” header 的存在之间的关系。

我们注意到,当我们收到 401 时,没有 X-SW 标记 header ,因此 service worker 根本没有触及这些请求。

注意:为清楚起见,已从之前的代码片段中删除了添加的标记 header 。

--

在 IndexedDB 中存储 token 和首次 API 调用之间是否存在竞争条件?

虽然这可能是个问题,但这里不是这种情况,因为请求甚至没有 X-SW 标记 header 。让我们假设 token 还没有出现在服务 worker 端,请求无论如何都应该有标记头,但我们跟踪了这一点,但事实并非如此……

--

您怎么知道 initializeServiceWorker 正在解析?

该调用以及 api 调用位于一个 try catch block 中,该 block 根据失败情况区分用户在失败时看到的内容。然后,我们知道初始化进行得很顺利,因为我们看到了 401 相关的错误消息。

最佳答案

据我所知,您的代码中有两个竞争条件。

首先,initializeServiceWorker() 的第一部分中的逻辑并未等待正确的事情。您正在轮询以试图找出已注册的服务 worker 何时被激活,但即使您调用 clients.claim(),服务 worker 被激活和被激活之间也存在时间间隔当它控制范围内的所有客户端时。

如果您想确保所有网络请求都将触发服务 worker 的 fetch 处理程序,您应该等待的是有一个服务 worker 控制当前页面。还有更多背景in this issue ,它包含您可以使用的以下代码段:

const controlledPromise = new Promise((resolve) => {
  if (navigator.serviceWorker.controller) {
    resolve();
  } else {
    navigator.serviceWorker.addEventListener(
      'controllerchange', () => resolve());
  }
});

一旦 controlledPromise 解析,您就会知道来自当前页面的网络请求将触发受控制的 service worker 的 fetch 处理程序。

第二个问题是,一旦 message 事件在 service worker 上触发,您的 postMessage() 调用就会完成,但在 IndexedDB 操作实际完成之前。

您需要从服务 worker 向客户端页面发回一条消息,指示 IndexedDB 写入已完成。您可以使用 MessagePorts 使用“vanilla”JavaScript 执行此操作, 但我建议使用 Comlink library相反,因为这将包含使用 postMessage() 和在更好的、基于 promise 的界面中协调 MessagePort 的细节。

关于javascript - Active ServiceWorker 并不总是拦截请求,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/69796320/

相关文章:

javascript - 哪个规范定义了就绪事件?

javascript - 什么是 gatsbyContentfulFluid?

class - 从字符串创建 React 类的实例

javascript - 将 Redux 与 React v0.12.2 结合使用

node.js - (node.js 模块)sharp 图像处理器保持源文件打开,调整大小后无法取消链接原始文件

javascript - 如何根据窗口大小更改DIV的高度

javascript - Firefox 不会触发动态嵌套 iframe 的 onload

angular - 从 FirebaseListObservables 数组到字符串数组

typescript - 从 Angular 2 typescript 编辑 SASS 变量

javascript - 从外部链接调用 JavaScript