ServiceWorker
service Worker
是一个运行于浏览器后台的独立线程。可以用于处理离线缓存、推送通知和网络请求拦截等功能,Service Worker可以在浏览器关闭后继续后台运行,并且可以在网络连接不可用时(脱机)提供离线体验。还可以拦截和处理来自网页的网络请求,将请求转发到网络,也可以从缓存中直接返回响应(配合Cache Storage)。出于安全原因,Service worker
仅限在 HTTPS 或localhost上运行。此外,Service Worker是完全异步的。
同时,它也是PWA(渐进式Web应用)的关键技术之一
作用域
我们把页面、workers、shared workers称为clients
,SW(Service Worker
)只能对作用域内的clients有效。对于被控制的clients,他的所有请求都会经过这个SW。SW的作用域是由他的URL决定的,URL决定了他的作用域最大的控制区域。并且同一个URL最多只能注册一个Worker。我们可以通过navigator.serviceWorker.register
这个API来进行注册SW,例如navigator.serviceWorker.register('./worker/sw.js')
,那么他的作用域为https://$hostname/worker/
,这个worker能控制的最大范围也就是https://$hostname/worker/
目录下的所有页面,可以通过第二个参数来修改作用域范围。
可以通过navigator.serviceWorker.controller
是否为 null 来查看一个client
是否被 SW 控制。
生命周期
- 在主线程成功注册 Service Worker 之后,开始下载并解析执行 Service Worker 文件,执行过程中开始安装 Service Worker,在此过程中会触发 worker 线程的 install 事件。
- 如果 install 事件回调成功执行(在 install 回调中通常会做一些缓存读写的工作,可能会存在失败的情况),则开始激活 Service Worker,在此过程中会触发 worker 线程的 activate 事件,如果 install 事件回调执行失败,则生命周期进入 Error 终结状态,终止生命周期。
- 完成激活之后,Service Worker 就能够控制作用域下的页面的资源请求,可以监听 fetch 事件。
- 如果在激活后 Service Worker 被 unregister 或者有新的 Service Worker 版本更新,则当前 Service Worker 生命周期完结,进入 Terminated 终结状态。
执行流程
// sw.js
console.log('service worker 注册成功')
self.addEventListener('install', () => {
// 安装回调的逻辑处理
console.log('service worker 安装成功')
})
self.addEventListener('activate', () => {
// 激活回调的逻辑处理
console.log('service worker 激活成功')
})
self.addEventListener('fetch', event => {
console.log('service worker 抓取请求成功: ' + event.request.url)
})
当我们首次进入页面时,会发现会打印service worker 注册成功
、service worker 安装成功
、service worker 激活成功
第二次进入页面时,会打印service worker 抓取请求成功: url
从这个执行结果来看,初步能够说明以下几点:
- Service Worker 文件只在首次注册的时候执行了一次。
- 安装、激活流程也只是在首次执行 Service Worker 文件的时候进行了一次。
- 首次注册成功的 Service Worker 没能拦截当前页面的请求。
- 非首次注册的 Service Worker 可以控制当前的页面并能拦截请求。
实际上 Service Worker 首次注册或者有新版本触发更新的时候,才会重新创建一个 worker 工作线程并解析执行 Service Worker 文件,在这之后并进入 Service Worker 的安装和激活生命周期。
而在首次注册、安装、激活之后,Service Worker 已经拿到了当前页面的控制权了,但为什么首次刷新却没有拦截到网络请求呢?主要是因为在 Service Worker 的注册是一个异步的过程,在激活完成后当前页面的请求都已经发送完成,因为时机太晚,此时是拦截不到任何请求的,只能等待下次访问再进行。
发现机制总结起来更多的是cv,所以不再记录了,后续想了解打算直接翻阅参考链接。第一个参考链接详细介绍了service worker。
缓存资源DEMO
const CACHE_NAME = 'harver_blog_version_' + 5;
const cacheList = ['https://cdn.jsdelivr.net/npm/katex@0.16.0/dist/katex.min.css'];
const tacticRules = [
{
name: 'image',
tacticType: 'cacheFirst',
},
{
name: 'style',
tacticType: 'networkFirst',
},
{
name: 'script',
tacticType: 'networkFirst',
},
{
name: 'document',
tacticType: 'networkFirst',
},
{
name: 'manifest',
tacticType: 'networkFirst',
},
];
self.addEventListener('install', e => {
e.waitUntil(
(async () => {
const cache = await caches.open(CACHE_NAME); // 创建一个缓存空间
await cache.addAll(cacheList);
await self.skipWaiting();
})()
);
});
self.addEventListener('activate', e => {
e.waitUntil(
(async () => {
const keys = await caches.keys();
await Promise.all(
keys.map(key => {
if (key !== CACHE_NAME) return caches.delete(key);
return;
})
);
await self.clients.claim();
})()
);
});
const cacheFirstHandle = async request => {
const cache = await caches.open(CACHE_NAME);
const responseCache = await cache.match(request);
if (responseCache) return responseCache;
return fetch(request.clone())
.then(async res => {
if (res.status === 200) {
const cloneRes = res.clone();
caches.open(CACHE_NAME).then(async cache => {
await cache.put(request, cloneRes);
});
}
return res;
})
.catch(err => {
console.log('资源加载失败:', request.url, err?.message);
return err;
});
};
const cacheOnlyHandle = async request => {
const cache = await caches.open(CACHE_NAME);
return await cache.match(request);
};
const networkFirstHandle = async request => {
return fetch(request.clone())
.then(async res => {
if (res.status === 200) {
const cloneRes = res.clone();
caches.open(CACHE_NAME).then(async cache => {
await cache.put(request, cloneRes);
});
return res;
} else {
const cacheResponse = await cacheOnlyHandle(request.clone());
if (cacheResponse) return cacheResponse;
}
return res;
})
.catch(async err => {
console.log('资源加载失败:', request.url, err?.message);
const cacheResponse = await cacheOnlyHandle(request.clone());
if (cacheResponse) return cacheResponse;
return err;
});
};
self.addEventListener('fetch', e => {
const tactic = tacticRules.find(tactic => tactic.name === e.request.destination);
const isInCacheList = cacheList.includes(e.request.url);
if (tactic || isInCacheList) {
e.respondWith(
(async () => {
if (isInCacheList) {
return cacheFirstHandle(e.request);
} else if (tactic.tacticType === 'networkFirst') {
return networkFirstHandle(e.request);
} else if (tactic.tacticType === 'cacheFirst') {
return cacheFirstHandle(e.request);
} else if (tactic.tacticType === 'cacheOnly') {
return cacheOnlyHandle(e.request);
}
})()
);
}
});
export const useServiceWorker = () => {
if (typeof window === 'undefined') return;
if ('serviceWorker' in navigator) {
navigator.serviceWorker.getRegistrations().then(swList => {
for (const sw of swList) {
sw.unregister();
}
navigator.serviceWorker.register('/mysw.js', { scope: '/' });
});
}
};
对于除开静态资源的文件都采用网络优先,并在请求之后将其缓存下来,用于用户脱机状态下正常访问
对于静态资源,直接采用缓存优先
参考链接
第4章 Service Worker · PWA 应用实战 (lavas-project.github.io)
service-worker - 【Service Worker】生命周期那些事儿 - 郑 farmer 的一亩三分地 - SegmentFault 思否
Service Worker初探(工作原理讲解,附发送message和cache缓存demo代码) - 掘金 (juejin.cn)