使用Service Worker一般是为了缓存一些内容以便网站离线时使用,或者也可以将其作为一个缓存机制,减少对实际资源的访问,从而降低服务器成本流量成本等。
Service Worker的缓存按时机可分为两种,一种是预缓存,直接在开发时制定好需要缓存的资源链接,在用户打开页面时注册Service Worker时缓存,缓存完毕才算注册完成可以激活使用,随后浏览器会在合适的时机激活Service Worker。一般适用于一些静态的css、js文件等。
另一种则是在运行过程中,由开发者编写的代码主动缓存。这种主要是缓存在实际浏览过程中可能需要缓存的资源,例如一些不会发生更改的图片等。在使用Service Worker时,预缓存一般没有什么特别的地方,不做过多阐述,各种介绍Service Worker的博客教程都有详细讲解,这里我们重点关注在运行过程中需要缓存的图片处理。
首先描述一下具体场景,在页面中需要使用img标签加载图片,为了降低流量费用、减少服务器带宽占用,需要对img图片进行客户端侧缓存,这些加载的图片链接由接口下发,动态渲染在页面中,图片链接固定,不会发生改变(或者说不会频繁改变)。图片链接格式类似:/api/images/{imageId}。
直接给出ServiceWorker代码:
// 缓存名称,方便后续更新策略时清理旧缓存
const CACHE_NAME = "cache-v1";
// 安装阶段:打开缓存空间
self.addEventListener("install", event => {
(event as ExtendableEvent).waitUntil(caches.open(CACHE_NAME));
});
const putInCache = async (request: Request, response: Response) => {
const cache = await caches.open(CACHE_NAME);
await cache.put(request, response);
};
interface Cacher {
canCache: (request: Request) => boolean;
cache: (request: Request, response: Response) => Promise<void>;
}
const cachers: Cacher[] = [];
const thumbnailCacher: Cacher = {
canCache: (request: Request) => {
if (request.method !== "GET") {
return false;
}
const url = new URL(request.url);
return /^\/api\/images\/[0-9]+$/i.test(url.pathname);
},
cache: async (request: Request, response: Response) => {
if (response.type !== "opaque" && response.ok === false) {
console.warn("Resource not available");
return;
}
await putInCache(request, response);
},
};
cachers.push(thumbnailCacher);
const tryCache = async (request: Request, response: Response) => {
for (const cacher of cachers) {
if (cacher.canCache(request)) {
console.log("Caching response for:", request.url);
await cacher.cache(request, response);
return;
}
}
};
const cacheFirst = async (request: Request) => {
const responseFromCache = await caches.match(request);
if (responseFromCache) {
console.warn("Serving from cache:", request.url);
return responseFromCache;
}
const responseFromNetwork = await fetch(request, {
redirect: "follow",
});
const url = new URL(request.url);
tryCache(request, responseFromNetwork.clone());
return responseFromNetwork;
};
// 拦截所有 fetch 请求
self.addEventListener("fetch", e => {
const event = e as FetchEvent;
event.respondWith(cacheFirst(event.request));
});
这里需要注意的是29-33行的处理。一般我们认为一个请求若是成功,则其状态码为200或者响应的ok属性为true,但在面对no-cors的简单请求时,情况就不一样了。根据MDN描述,当发送的请求模式为“no-cors”时,response的类型就为“opaque”,在规范中称作“opaque filtered response”,这种响应type=”opaque”,URL列表为空,响应头为空,状态码为0,body也为空,对应的响应的ok属性也为false。
而no-cors请求是什么呢?这就涉及到跨域问题了,跨域问题这里不展开,具体请自行查阅学习。浏览器一般发出的请求都是跨域请求,但有些情况是例外,不需要严格遵循跨域限制,例如页面中的图像,其加载也是要通过网络请求发送的,浏览器默认对img不做跨域限制,除非设置了crossorigin属性为use-credentials
。
到这里就可以明白为什么上述代码这样编写了,需要缓存的请求或者说响应是由img标签发出的no-cors请求,在判断发出的请求是否成功时需要按照opaque filtered response模式判断而非传统的response.ok为true或response.status为200判断。
参考:
1. Service-worker to prefetch remote images (with expiration) and respond with fallback one when image cannot be fetched
2. Response.type – Web API | MDN
3. HTML 属性:crossorigin – HTML(超文本标记语言) | MDN