文章

解决Jekyll博客Service Worker缓存问题

在使用Jekyll Chirpy搭建的本博客中,我遇到了一个很奇怪的缓存问题。使用Ctrl+R刷新页面后页面内容没有更新;在新设备访问页面,内容可以更新;使用Ctrl+Shift+R强制刷新后页面内容更新了,然而此后使用Ctrl+R竟然回退到了旧版。

这个问题很早就存在了,然而我一直没有深入研究,并认为和Cloudflare缓存有关。但是这无法解释这种奇怪的行为,更无法解释为什么新设备上第一次打开可以看到最新内容。

按照我之前的理解,Ctrl+Shift+R会忽略缓存进行刷新,同时更新本地的缓存。因此按理来说不可能遇到回退情况。后来经过研究,发现是Service Worker引起的问题。

Service Worker是一种运行在浏览器后台的脚本,可以拦截网络请求并进行缓存管理。Chirpy博客使用了Service Worker来实现离线浏览和加速页面加载。这与一个名为PWA的选项有关。

PWA的全称是Progressive Web App,也就是渐进式网页应用。启用PWA后,在手机浏览器上访问网站可以选择将网站作为应用安装。为了允许手机上离线阅读,博客会注册一个Service Worker来缓存页面资源。这个Service Worker太喜欢缓存了,以至于即使按下Ctrl+R也无法更新内容。

sw.js就是Service Worker的脚本文件。它定义了如何缓存资源以及如何响应网络请求。Chirpy的sw.js中缓存的核心逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
event.respondWith(
  caches.match(event.request).then((response) => {
    if (response) {
      return response;  // 缓存命中 → 直接返回缓存版本(即使可能已过时)
    }

    return fetch(event.request).then((response) => {
      // ... 条件检查 ...
      if (purge || event.request.method !== 'GET' || !verifyUrl(url)) {
        return response;  // 不缓存,直接返回网络响应
      }

      // 网络获取成功 → 克隆并放入缓存,供下次使用
      let responseToCache = response.clone();
      caches.open(swconf.cacheName).then((cache) => {
        cache.put(event.request, responseToCache);
      });
      return response;
    });
  })
);

而purge之类的选项由swconf.js提供:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
---
layout: compress
permalink: '/:path/swconf.js'
# Note that this file will be fetched by the ServiceWorker, so it will not be cached.
---

const swconf = {
  {% if site.pwa.cache.enabled %}
    cacheName: 'chirpy-{{ "now" | date: "%s" }}',

    {%- comment -%} Resources added to the cache during PWA installation. {%- endcomment -%}
    resources: [
      '{{ "/assets/css/:THEME.css" | replace: ':THEME', site.theme | relative_url }}',
      '{{ "/" | relative_url }}',
      {% for tab in site.tabs %}
        '{{- tab.url | relative_url -}}',
      {% endfor %}

      {% assign cache_list = site.static_files | where: 'swcache', true %}
      {% for file in cache_list %}
        '{{ file.path | relative_url }}'{%- unless forloop.last -%},{%- endunless -%}
      {% endfor %}
    ],

    interceptor: {
      {%- comment -%} URLs containing the following paths will not be cached. {%- endcomment -%}
      paths: [
        {% for path in site.pwa.cache.deny_paths %}
          {% unless path == empty %}
            '{{ path | relative_url }}'{%- unless forloop.last -%},{%- endunless -%}
          {% endunless  %}
        {% endfor %}
      ],

      {%- comment -%} URLs containing the following prefixes will not be cached. {%- endcomment -%}
      urlPrefixes: [
        {% if site.analytics.goatcounter.id != nil and site.pageviews.provider == 'goatcounter' %}
          'https://{{ site.analytics.goatcounter.id }}.goatcounter.com/counter/'
        {% endif %}
      ]
    },

    purge: false
  {% else %}
    purge: true
  {% endif %}
};

关键在于swconf.purge。如果启用了PWA缓存(site.pwa.cache.enabled为true),purge就会被设置为false。这样一来,Service Worker在拦截请求时总是优先返回缓存版本,导致Ctrl+R无法更新内容。

于是为了解决问题,我们可以在_config.yml中禁用PWA缓存:

1
2
3
pwa:
  cache:
    enabled: false

这样移动端的用户仍然可以安装PWA,但Service Worker不会缓存页面资源,从而避免了缓存过度的问题。

本文由作者按照 CC BY 4.0 进行授权