在 Service Worker 的 fetch 事件中,我们往往会从本地缓存中构建请求结果从而加速响应,然而有些时候我们又需要通过网络请求获取最新的数据,那么如何决定缓存的使用时机呢?本章将介绍一些常见的请求策略,以便大家能够更容易地控制缓存的使用时机。
# 常见策略
# 缓存优先

首先从缓存中进行匹配,如果存在相关请求的响应,返回该响应,否则通过网络获取。基本实现如下:
async function fetchFromNetwork(event) {
const response = await fetch(event.request);
if (response) {
const cloneResponse = response.clone();
event.waitUntil((async () => {
const cache = await caches.open('cache-name');
await cache.put(event.request, cloneResponse);
})());
}
return response;
}
self.addEventListener('fetch', event => {
if (request.method.toLowerCase() === 'get') {
event.respondWith((async () => {
const cachedResponse = await caches.match(event.request);
if (cachedResponse) {
return cachedResponse;
}
return await fetchFromNetwork(event);
})());
}
});
该策略主要适用于请求资源不经常变更的情况,比如:Shell 文件、图片、脚本等。
# 网络优先

首先通过网络获取,如果请求异常,则从缓存中获取。基本实现如下:
self.addEventListener('fetch', event => {
if (request.method.toLowerCase() === 'get') {
event.respondWith((async () => {
try {
return await fetchFromNetwork(event);
} catch {
return await caches.match(event.request);
}
})());
}
});
该策略主要用于需要频繁更新的资源,比如:资讯、排行榜等。
该策略的主要优势是,如果用户处于离线状态,依旧可以为其提供服务,从而为用户提供更好的使用体验。
# 仅使用缓存

所有请求都从缓存中获取,基本实现如下:
self.addEventListener('fetch', event => {
if (request.method.toLowerCase() === 'get') {
event.respondWith(caches.match(event.request));
}
});
该策略的使用场景与缓存优先类似,相对于后者,该策略的主要问题是,如果缓存中不存在相关请求的响应,它将与传统的网络请求一样抛出异常,这可能会导致令人失望的用户体验。
# 仅使用网络

- 所有请求都从网络中获取,这是浏览器的默认行为,无需在
Service Worker中做任何特殊处理。 - 该策略的使用场景与网络优先类似,相对于后者,该策略的主要问题是,如果请求出现异常,这可能会导致令人失望的用户体验。
# 先缓存后网络

该策略为缓存优先的升级版,它与后者的唯一区别是,如果在缓存中匹配到相关请求的响应,在返回该响应的同时依旧会发起网络请求,并更新相关缓存。基本实现如下:
self.addEventListener('fetch', event => {
if (request.method.toLowerCase() === 'get') {
event.respondWith((async () => {
const cachedResponse = await caches.match(event.request);
if (cachedResponse) {
try {
event.waitUntil((async () => {
await fetchFromNetwork(event);
})());
} catch {
}
return cachedResponse;
}
return await fetchFromNetwork(event);
})());
}
});
该策略适用于任何类型的资源,其最为常见的一个场景是,假设一个处于滚动的列表,为了不让用户感觉到因请求最新数据而导致的间断,我们可以使用该策略快速返回缓存版本的数据,当滚动停止时,便可以用得到的最新数据替换展示在用户面前的内容。
# 总结
上文中,我们讨论了常见的请求策略,它为我们如何决定使用缓存提供了理论基础。很多情况下,我们不必为某一个请求选择一个具体的策略,而是根据其特点综合使用多种策略,比如示例:
async function fetchPageContent(cacheKey, event) {
try {
const response = await fetch(cacheKey, {
headers: {
'only_content': 1
}
});
if (response) {
const cloneResponse = response.clone();
event.waitUntil((async () => {
await setCache(runtimeCacheName, cacheKey, cloneResponse);
})());
}
return response;
} catch {
return await getCache(runtimeCacheName, cacheKey);
}
}
function fetchPage(cacheKey, event) {
//... 根据 cacheKey 获取 shell 类型
const stream = new ReadableStream({
start(controller) {
//... pushStream 函数定义
(async () => {
const top = await getCache(precacheName, `/shell/${shellType}_top.html`);
await pushStream(top.body);
const content = await fetchPageContent(cacheKey, event);
await pushStream(content.body);
const bottom = await getCache(precacheName, `/shell/${shellType}_bottom.html`);
await pushStream(bottom.body);
controller.close();
})();
}
});
return new Response(stream, {
headers: { 'Content-Type': 'text/html' }
});
}
在
fetchPage中,shell 文件(top和bottom)的获取使用了仅使用缓存策略,正文信息(content)的获取使用了网络优先策略(通过调用fetchPageContent)