0xc*_*m1z 4 javascript typescript reactjs service-worker
我有一个带有 ServiceWorker 的 React 应用程序,其目的是拦截某些请求(API 调用和资源获取)并添加Authorization带有适当令牌的标头。在大多数情况下,它工作得很好,但在某些情况下,Service Worker 不会处理 fetch 事件,从而导致 401 响应。
反应侧\xe2\x9a\x9b\xef\xb8\x8f
\n我们有以下函数来注册 Service Worker 并等待它激活:
\nfunction initializeServiceWorker() {\n return new Promise((resolve, reject) => {\n navigator\n .serviceWorker\n .register("/tom.js")\n .then((registration) => {\n\n // checks the state of the service worker until it\'s activated\n // and move on with the then-chain\n return pollUntil(\n (registration) => registration.active?.state === "activated",\n POLL_EVERY, // 40ms\n registration,\n POLL_TIMEOUT // 6000ms\n );\n\n })\n .then((registration) => {\n\n // here we send the authentication token to the service worker\n // to be stored in the IndexedDB overriding the old one\n // and persist across termination/reactivation\n registration.postMessage(<NewAuthToken>{ type: "AUTH_TOKEN", token });\n\n })\n .catch(reject);\n });\n}\nRun Code Online (Sandbox Code Playgroud)\nService Worker 端
\n我们的服务工作人员为活动、安装、消息和获取事件设置处理程序,如下所示:
\n\n// eslint-disable-next-line no-restricted-globals\nconst worker = self as any as ServiceWorkerGlobalScope;\n// NOTE: we refer to self as `worker` after performing a couple of type casts\n\n\nworker.addEventListener("install", () => {\n // this ensure that updates to the underlying service worker take effect\n // immediately for both the current client and all other active clients\n worker.skipWaiting();\n});\n\nworker.addEventListener("activate", async (event: ExtendableEvent) => {\n // https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle#activate\n // The first activation does not connect / intercept already running clients. \n // "claim" claims any already running clients.\n event.waitUntil(worker.clients.claim());\n});\n\nworker.addEventListener("message", (event: ExtendableMessageEvent) => {\n // here we store the token to IndexedDB\n});\n\nworker.addEventListener("fetch", (event: FetchEvent) => {\n event.respondWith(\n (async function respond() {\n try {\n const token = await DB.getToken();\n\n // no token - no party\n // just send the request as is\n if (!token) return fetch(event.request);\n\n // going somewhere else?\n // no need to add the token to the request\n const sourceHost = new URL(worker.location.origin).host;\n const destinationHost = new URL(event.request.url).host;\n if (destinationHost !== sourceHost) {\n return fetch(event.request);\n }\n\n // already authorized?\n // ready to go as it is\n if (event.request.headers.get("Authorization")) {\n return fetch(event.request);\n }\n\n /**\n * When the `destination` property of the request is empty, it means that the\n * request is an API call, otherwise it would have contained the DOM element\n * that initialized the request (image, script, style, etc.)\n *\n * In this case, we can smoothly add the token in the Headers.\n */\n if (event.request.destination === "") {\n const headers = new Headers(event.request.headers);\n headers.append("Authorization", token);\n const enhancedRequest = new Request(event.request, { headers });\n\n return await fetch(enhancedRequest);\n }\n\n // when this get executed it means the browser wants\n // the content of an <img> tag or some script or styling\n // ... but this is not the point of this SO question ...\n return requestResource(event.request, token);\n\n } catch (error) {\n // something horrible happen?\n // let the original request to reach it\'s destination freely\n return fetch(event.request);\n }\n })()\n );\n});\n\n\nRun Code Online (Sandbox Code Playgroud)\n那么,问题出在哪里呢?
\n我们initializeServiceWorker在执行第一个 API 调用之前等待该方法,如下所示:
\n// in an async function...\n\nawait initializeServiceWorker();\n\nconst homeModel = await api.environment.getClientSettings();\n\nRun Code Online (Sandbox Code Playgroud)\n在某些情况下(幸运的是,比例很小,例如 <5%),getClientSettingsapi 调用最终会出现 401,而不会添加授权标头。
那么,这怎么可能呢?怎么可能initializeServiceWorker在调用 api 之前就解决了承诺,而令牌仍然不存在?
这个初始化/服务工作者代码有意义吗?你是否发现六只眼睛漏过的任何巨大或微妙的错误?我们在这里做一些有点疯狂的事情吗?\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f
\n让我知道
\n您可能想知道的奇怪事情
\n为什么要等到 Service Worker 被激活?
\n我们注意到,有时激活所需的时间比所需的时间要多一些,并且我们确实需要等待它准备好才能继续我们的代码。
\n--
\n你怎么知道它根本没有拦截?
\n为了弄清楚我们是否没有添加Authorization标头或者我们根本没有拦截请求,我们决定始终添加一个额外的X-SW标头并在后端跟踪失败与这个新“标记”的存在之间的关系” 标题。
我们注意到,当我们收到 401 时,没有X-SW标记标头,因此 Service Worker 根本不会触及这些请求。
注意:为了清楚起见,已从之前的代码片段中删除了添加的标记标头。
\n--
\nIndexedDB 中令牌的存储与第一个 API 调用之间是否存在竞争条件?
\n虽然这可能是一个问题,但这里的情况并非如此,因为请求甚至没有X-SW标记标头。让我们假设令牌尚未出现在服务工作者方面,请求无论如何都应该具有标记标头,但我们跟踪了这一点,但情况并非如此......
--
\n你怎么知道问题initializeServiceWorker已经解决了?
该调用以及 api 调用都位于 try catch 块中,该块根据失败情况区分用户在失败时看到的内容。然后,我们知道初始化进展顺利,因为我们看到了 401 相关错误消息。
\n据我所知,您的代码中有两个竞争条件。
首先,你的第一部分的逻辑initializeServiceWorker()并没有等待正确的事情。您正在进行轮询,试图找出已注册的 Service Worker 何时被激活,但即使您调用clients.claim(),Service Worker 被激活和它控制范围内的所有客户端之间也存在时间间隔。
如果您想确保所有网络请求都会触发 Service Worker 的fetch处理程序,您应该等待的是有一个 Service Worker 控制当前页面。本期有更多背景信息,其中包括您可以使用的以下代码片段:
const controlledPromise = new Promise((resolve) => {
if (navigator.serviceWorker.controller) {
resolve();
} else {
navigator.serviceWorker.addEventListener(
'controllerchange', () => resolve());
}
});
Run Code Online (Sandbox Code Playgroud)
一旦controlledPromise解析,您就会知道来自当前页面的网络请求将触发fetch受控制的 Service Worker 的处理程序。
第二个问题是,postMessage()一旦message在服务工作线程上触发事件,但在 IndexedDB 操作实际完成之前,您的调用就会完成。
您需要从 Service Worker 向客户端页面发回一条消息,指示 IndexedDB 写入已完成。您可以使用MessagePorts使用“普通”JavaScript 来完成此操作,但我建议使用Comlink 库postMessage(),因为这会将使用和协调s的细节包含MessagePort在一个更好的、基于承诺的界面中。
| 归档时间: |
|
| 查看次数: |
901 次 |
| 最近记录: |