Service Worker 如同应用内部的代理服务器,负责处理网络请求。它可以让应用离线运行。即使你不需要离线支持,或者应用本身不适合实现离线功能,预先缓存构建后的 JavaScript 和 CSS 通常也能加快页面导航。
在 SvelteKit 中,只要存在 src/service-worker/index.ts 文件,它就会被打包并自动注册。
在 Service Worker 内部
要让 Service Worker 发挥作用,通常需要导入以下内容:
$app/service-worker导出self,即标注为ServiceWorkerGlobalScope类型的globalThis。按照这些步骤配置后,fetch事件便具有正确的类型。$app/env导出version,可用于创建与当前部署版本关联的缓存。$app/manifest导出immutable构建文件、assets资源以及所有prerendered内容,可用来填充缓存。
一个典型的 Service Worker 如下:
import { self } from '$app/service-worker';
import { version } from '$app/env';
import { immutable, assets } from '$app/manifest';
import { resolve } from '$app/paths';
// Create a unique cache name for this deployment
const CACHE = `cache-${version}`;
// `immutable`/`assets` paths from `$app/manifest` are relative to the
// base path, so resolve them to absolute pathnames that can be matched
// against `url.pathname` in the `fetch` handler
const ASSETS = [
...immutable.map((asset) => resolve(asset.path)), // the Vite output
...assets.map((asset) => resolve(asset.path)) // everything in `static`
];
self.addEventListener('install', (event) => {
// Create a new cache and add all files to it
async function addFilesToCache() {
const cache = await caches.open(CACHE);
await cache.addAll(ASSETS);
}
event.waitUntil(addFilesToCache());
});
self.addEventListener('activate', (event) => {
// Remove previous cached data from disk
async function deleteOldCaches() {
for (const key of await caches.keys()) {
if (key !== CACHE) await caches.delete(key);
}
}
event.waitUntil(deleteOldCaches());
});
self.addEventListener('fetch', (event) => {
// ignore POST requests etc
if (event.request.method !== 'GET') return;
async function respond() {
const url = new URL(event.request.url);
const cache = await caches.open(CACHE);
// `immutable`/`assets` can always be served from the cache
if (ASSETS.includes(url.pathname)) {
const response = await cache.match(url.pathname);
if (response) {
return response;
}
}
// for everything else, try the network first...
try {
const response = await fetch(event.request);
if (response.status === 200 && !response.headers.get('cache-control')?.includes('no-store')) {
// ...and cache responses in the background for next time....
void cache.put(event.request, response.clone());
}
return response;
} catch (error) {
// ...otherwise fall back to previously cached data if it exists...
const response = await cache.match(event.request);
if (response) {
return response;
}
// ...or throw the error
throw error;
}
}
event.respondWith(respond());
});
类型安全
Service Worker 的运行上下文与应用其余部分不同,因此需要不同的类型。请确保项目根目录的 tsconfig.json 排除 Service Worker 代码:
{
"extends": "$app/tsconfig",
"include": ["src", "test"],
"exclude": ["src/service-worker"]
}
然后在 src/service-worker/index.ts 所在目录放置独立的 tsconfig.json,通过继承 $app/tsconfig/service-worker 配置正确的类型:
{
"extends": "$app/tsconfig/service-worker"
}
手动注册
如果需要使用自己的逻辑注册 Service Worker,可以禁用自动注册。默认注册代码会注入服务端渲染的 HTML,其形式大致如下:
if ('serviceWorker' in navigator) {
const script_url = './service-worker.js';
const policy = globalThis?.window?.trustedTypes?.createPolicy(
'sveltekit-trusted-url',
{ createScriptURL(url) { return url; } }
);
const sanitised = policy?.createScriptURL(script_url) ?? script_url;
addEventListener('load', function () {
navigator.serviceWorker.register(sanitised, { type: 'module' });
});
}
更新 Service Worker
当作用域内发生整页导航,以及 push、sync 等功能事件结束后,浏览器会检查 Service Worker 是否有更新。客户端导航不属于这两种情况,因此在应用内切换页面本身不会让浏览器发现新部署的 Service Worker。
SvelteKit 只在错误恢复流程中调用 registration.update():如果路由模块加载失败,或导航产生错误状态,同时版本轮询检测到应用已重新部署,SvelteKit 会先更新 Service Worker,再回退到整页导航。
如果希望更及时地发现新部署,可以自行触发更新检查。例如,在根布局中,每次客户端导航后检查一次:
import { afterNavigate } from '$app/navigation';
afterNavigate(async () => {
if ('serviceWorker' in navigator) {
const registration = await navigator.serviceWorker.getRegistration();
await registration?.update();
}
});
即使存在新的 Service Worker,这也不会让它立即接管当前页面。它会先在后台安装,等旧 Service Worker 管理的标签页数量降为零后再接管。
其他方案
SvelteKit 的 Service Worker 实现以易用为目标,通常适合大多数用户。不过,在 SvelteKit 之外,许多 PWA 应用使用 Workbox。如果你习惯使用 Workbox,也可以选择 Vite PWA 插件。
参考资料
有关 Service Worker 的更多通用信息,请参阅 MDN Web 文档。











暂无评论内容