更多

Service Worker缓存图片

使用Service Worker一般是为了缓存一些内容以便网站离线时使用,或者也可以将其作为一个缓存机制,减少对实际资源的访问,从而降低服务器成本流量成本等。

Service Worker的缓存按时机可分为两种,一种是预缓存,直接在开发时制定好需要缓存的资源链接,在用户打开页面时注册Service Worker时缓存,缓存完毕才算注册完成可以激活使用,随后浏览器会在合适的时机激活Service Worker。一般适用于一些静态的css、js文件等。

另一种则是在运行过程中,由开发者编写的代码主动缓存。这种主要是缓存在实际浏览过程中可能需要缓存的资源,例如一些不会发生更改的图片等。在使用Service Worker时,预缓存一般没有什么特别的地方,不做过多阐述,各种介绍Service Worker的博客教程都有详细讲解,这里我们重点关注在运行过程中需要缓存的图片处理。

首先描述一下具体场景,在页面中需要使用img标签加载图片,为了降低流量费用、减少服务器带宽占用,需要对img图片进行客户端侧缓存,这些加载的图片链接由接口下发,动态渲染在页面中,图片链接固定,不会发生改变(或者说不会频繁改变)。图片链接格式类似:/api/images/{imageId}。

直接给出ServiceWorker代码:

TypeScript
// 缓存名称,方便后续更新策略时清理旧缓存
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

留下评论

您的邮箱地址不会被公开。 必填项已用 * 标注

Captcha Code