Featured image of post 基于 Workbox 实现 Hugo 渐进式 Web 应用

基于 Workbox 实现 Hugo 渐进式 Web 应用

本文讲述了如何利用 Workbox 为 Hugo 静态网站添加 PWA 功能,通过 Service Worker 提升加载速度和用户体验。PWA 的优势包括快速加载、离线访问、推送通知和安装到主屏幕。文章详细介绍了注册 Service Worker、使用 Workbox 的缓存策略,并提供了详细的配置步骤和示例代码。

# 基于 Workbox 实现 Hugo PWA

最近给基于 Hugo 搭建的博客添加了 PWA 功能,显著提升了加载速度和用户体验,甚至实现了离线访问。至于如何实现,那么你需要了解 Progressive Web Apps (PWA)

# 什么是 PWA

渐进式 Web 应用(Progressive Web Apps,简称 PWA)利用现代 Web API 和传统的渐进式增强策略,打造出跨平台的 Web 应用程序。这些应用无处不在,功能丰富,为用户带来媲美原生应用的体验。

PWA 的优势:

  • ⚡️ 更快的加载速度: PWA 可以缓存重要资源,即使网络状况不佳也能快速加载。
  • ✈️ 离线访问: PWA 可以缓存内容,让用户即使离线也能访问内容。
  • 🔔 推送通知: 像原生应用一样,PWA 可以向用户发送推送通知,提高用户参与度。
  • 📱 安装到主屏幕: 用户可以将你的应用添加到电脑或手机桌面,像原生应用一样浏览你的 Web 应用。

PWA 的实现原理是 Service WorkerService Worker 是一种特殊的 JavaScript 资源,在浏览器后台独立运行,充当着网络浏览器和 Web 服务器之间的代理。它可以拦截和处理网络请求、缓存资源以及推送通知

主流的前端框架 Vue、React、Angular 都提供了相应的 PWA 插件。而对于 Hugo 这样的静态网站生成器,我们可以通过手动添加 Workbox 来实现 PWA 功能。

# Workbox

Workbox 是由 Google Chrome 团队开发的一套模块,旨在简化常见的 Service Worker 路由和缓存操作。每个模块都针对 Service Worker 开发的特定方面进行了优化。Workbox 的目标是尽可能简化 Service Worker 的使用,同时在需要时灵活地满足复杂应用的需求。

如果没有 Workbox,我们需要手动编写 Service Worker 来监听 fetch 事件、缓存资源并实现离线访问等功能。而 Workbox 提供了一套工具,可以帮助我们自动生成 Service Worker,并且内置了一些常用的缓存策略,使我们能够更加专注于业务逻辑。

# 配置 PWA

在上一节中,我们了解了 PWA 的概念和优势,以及 Workbox 如何简化 Service Worker 的开发。接下来将一步步地给 Hugo 博客配置 PWA 功能。

# 注册 Service Worker

首先,我们需要在页面中注册 Service Worker。将以下代码段添加到你的 Hugo 主题的 layouts/partials/footer/custom.html 文件中(其他主题可能需要根据文件结构进行调整):

<script>
    // Check that service workers are registered
    if ('serviceWorker' in navigator) {
        // Use the window load event to keep the page load performant
        window.addEventListener('load', () => {
            navigator.serviceWorker.register('/sw.js').then(reg => {
                console.log('Service worker registered with scope: ', reg.scope);
            }, err => {
                console.log('Service worker registration failed: ', err);
            });
        });
    }
</script>

注释

注意: 在注册 Service Worker 之前,你需要先创建 sw.js 文件,我们将在下一小节中完成这一步骤。

完成注册后,你可以在浏览器的开发者工具 (F12) 中的 “Application” -> “Service Workers” 面板中查看 Service Worker 的注册状态。

Service Worker

Service Worker

# 导入 Workbox

在你的 Hugo 网站根目录下的 static 文件夹中创建 sw.js 文件。然后,在 sw.js 文件中添加以下代码,使用 CDN 导入 Workbox:

importScripts('https://cdn.bootcdn.net/ajax/libs/workbox-sw/7.1.0/workbox-sw.js');

# 缓存策略

Workbox 提供了一些常用的缓存策略,如 CacheFirstNetworkFirstStaleWhileRevalidate 等。这里先介绍几种常用的策略。

# CacheOnly 仅缓存

CacheOnly

CacheOnly

强制响应来自缓存。

workbox.routing.registerRoute(
    new RegExp(regex),
    new workbox.strategies.CacheOnly()
);

# NetworkOnly 仅网络

NetworkOnly

NetworkOnly

这种缓存策略强制要求所有请求都从网络获取最新数据,完全绕过缓存。

workbox.routing.registerRoute(
    new RegExp(regex),
    new workbox.strategies.NetworkOnly()
);

# CacheFirst 优先缓存

CacheFirst

CacheFirst

这种缓存策略以速度为优先,会首先尝试从缓存中获取响应,以尽快向用户显示内容。如果缓存中没有所需数据,它才会向网络发起请求获取数据。

workbox.routing.registerRoute(
    new RegExp(regex),
    new workbox.strategies.CacheFirst()
);

# NetworkFirst 优先网络

NetworkFirst

NetworkFirst

这种缓存策略优先使用最新数据,因此会首先尝试从网络获取响应。如果网络请求失败,例如用户离线或网络连接不稳定,它会回退使用缓存中的数据,确保用户仍然可以访问内容。

workbox.routing.registerRoute(
    new RegExp(regex),
    new workbox.strategies.NetworkFirst()
);

# StaleWhileRevalidate 读取缓存,同时发起网络请求

StaleWhileRevalidate

StaleWhileRevalidate

这种缓存策略优先返回缓存内容(如果有)。即使缓存内容有效,它也会在后台发起网络请求以获取最新数据,保证用户最终能看到最新内容。虽然这种策略能确保用户定期更新缓存,但也意味着每次请求都会产生网络流量,即使数据没有变化,也比较浪费带宽。

workbox.routing.registerRoute(
    new RegExp(regex),
    new workbox.strategies.StaleWhileRevalidate()
);

# 策略配置

Workbox 不仅提供上述策略,还允许通过 cacheName、plugins 和 expiration 等配置项进行自定义。你可以通过定义要使用的插件来自定义路由行为。例如,你可以配置缓存名称、缓存有效期以及可缓存的响应状态码,如下所示:

workbox.routing.registerRoute(
    new RegExp(regex),
    new workbox.strategies.CacheFirst({
        cacheName: 'my-cache',
        plugins: [
            new workbox.expiration.ExpirationPlugin({
                maxEntries: 60,
                maxAgeSeconds: 30 * 24 * 60 * 60, // 30 Days
            }),
            new workbox.cacheableResponse.CacheableResponsePlugin({
                statuses: [0, 200],
            }),
        ],
    })
);

# 本站配置

# 全局配置

以下是全局缓存配置:

// 缓存版本号
let cacheVersion = '-240619';
// 最大条目数
const maxEntries = 100;

# Twitto 配置

为了确保用户即使在离线状态下也能查看评论,Twitto 评论 API 采用了 NetworkFirst 缓存策略。这意味着浏览器会优先尝试从网络获取最新数据,如果网络不可用,则使用缓存中的数据。

workbox.routing.registerRoute(
    new RegExp('^https://comment\.cuterwrite\.top'),
    new workbox.strategies.NetworkFirst({
        plugins: [
            new workbox.expiration.ExpirationPlugin({
                maxEntries: maxEntries,
                maxAgeSeconds: 30 * 24 * 60 * 60,
            }),
            new workbox.cacheableResponse.CacheableResponsePlugin({
                statuses: [0, 200],
            }),
        ],
    })
);

# RSS 与 Sitemap 配置

为了确保用户始终获取最新的 RSS 和 Sitemap 数据,这些页面配置为仅使用网络策略 (NetworkOnly),不进行缓存。

workbox.routing.registerRoute(
    new RegExp('^https://cuterwrite\.top/(index|sitemap)\.xml'),
    new workbox.strategies.NetworkOnly()
);

# HTML 配置

为了在保证用户快速加载页面的同时,也能获取到最新内容,网站对 HTML 页面采用了 StaleWhileRevalidate 缓存策略。这意味着浏览器会优先使用缓存中的页面进行展示,同时在后台向服务器发起请求,获取最新版本,并在下次请求时使用。

workbox.routing.registerRoute(
    new RegExp('.*\.html'),
    new workbox.strategies.StaleWhileRevalidate({
        cacheName: 'html-cache' + cacheVersion,
        plugins: [
            new workbox.expiration.ExpirationPlugin({
                maxEntries: maxEntries,
                maxAgeSeconds: 30 * 24 * 60 * 60,
            }),
            new workbox.cacheableResponse.CacheableResponsePlugin({
                statuses: [0, 200],
            }),
        ],
    })
);

# Google Fonts 配置

为了在保证字体文件更新的同时,也能利用缓存加速页面加载速度,网站对 Google Fonts 资源采用了 CacheFirst 缓存策略,并设置了较长的缓存过期时间。

workbox.routing.registerRoute(
    new RegExp('.*\.(?:woff|woff2|ttf|otf|eot)'),
    new workbox.strategies.StaleWhileRevalidate({
        cacheName: 'google-fonts' + cacheVersion,
        plugins: [
            // 使用 expiration 插件实现缓存条目数目和时间控制
            new workbox.expiration.ExpirationPlugin({
                // 最大缓存条目数
                maxEntries: maxEntries,
                // 最长缓存时间 30 天
                maxAgeSeconds: 30 * 24 * 60 * 60,
            }),
            // 使用 cacheableResponse 插件缓存状态码为 0 的请求
            new workbox.cacheableResponse.CacheableResponsePlugin({
                statuses: [0, 200],
            }),
        ],
    })
);

# CDN 配置

为了最大程度地利用缓存加速页面加载速度,网站对来自常用 CDN 的资源采用了 CacheFirst 缓存策略,并设置了较长的缓存过期时间。

workbox.routing.registerRoute(
    new RegExp('^https://(?:cdn\.bootcdn\.net|unpkg\.com|cdn\.jsdelivr\.net)'),
    new workbox.strategies.CacheFirst({
        cacheName: 'cdn' + cacheVersion,
        fetchOptions: {
            mode: 'cors',
            credentials: 'omit',
        },
        plugins: [
            new workbox.expiration.ExpirationPlugin({
                maxEntries: maxEntries,
                maxAgeSeconds: 30 * 24 * 60 * 60,
            }),
        ],
    })
);

# Umani 网站统计配置

为了确保网站统计数据的准确性,网站对 Umani 网站统计请求采用了 NetworkOnly 策略,并使用 BackgroundSyncPlugin 插件来实现即使在网络离线的情况下也能保证数据最终发送成功。

workbox.routing.registerRoute(
    new RegExp('^https://analytics\.cuterwrite\.top/uma'),
    new workbox.strategies.NetworkOnly({
        plugins: [
            // 使用 background sync 插件实现后台同步
            new workbox.backgroundSync.BackgroundSyncPlugin('Optical_Collect', {
                maxRetentionTime: 12 * 60,
            }),
        ],
    })
);

# 图片配置

为了加速图片加载速度,并减少网络请求次数,网站对图片资源采用了 CacheFirst 缓存策略,并设置了较长的缓存过期时间。

workbox.routing.registerRoute(
    new RegExp('^(https://cuterwrite-1302252842\.file\.myqcloud\.com|https://s2\.loli\.net)'),
    new workbox.strategies.CacheFirst({
        cacheName: 'image-cache' + cacheVersion,
        plugins: [
            new workbox.expiration.ExpirationPlugin({
                maxEntries: maxEntries,
                maxAgeSeconds: 30 * 24 * 60 * 60,
            }),
            new workbox.cacheableResponse.CacheableResponsePlugin({
                statuses: [0, 200],
            }),
        ],
    })
);

# 后缀匹配配置

为了兼顾加载速度和内容更新,网站对未被域名匹配到的静态文件(例如图片、CSS 和 JavaScript 文件)采用了 StaleWhileRevalidate 缓存策略。

workbox.routing.registerRoute(
    new RegExp('.*\.(?:png|jpg|jpeg|svg|gif|webp|ico)'),
    new workbox.strategies.StaleWhileRevalidate()
);
workbox.routing.registerRoute(
    new RegExp('.*\.(css|js)'),
    new workbox.strategies.StaleWhileRevalidate()
);

# 默认行为配置

为了处理未被任何自定义路由规则匹配到的请求,网站配置了默认缓存行为,使用 NetworkFirst 策略并设置了网络超时时间,以兼顾资源获取速度和离线可用性。

workbox.routing.setDefaultHandler(
    // 优先使用缓存,缓存没有则使用网络请求
    new workbox.strategies.NetworkFirst({
        networkTimeoutSeconds: 3,
    })
);

# 完整配置

sw.js
importScripts('https://cdn.bootcdn.net/ajax/libs/workbox-sw/7.1.0/workbox-sw.js');

// 缓存版本号
let cacheVersion = '-240619';
// 最大条目数
const maxEntries = 100;

if (workbox) {
    console.log(`Yay! Workbox is loaded 🎉`);
    // 评论缓存
    workbox.routing.registerRoute(
        new RegExp('^https://comment\.cuterwrite\.top'),
        new workbox.strategies.NetworkFirst({
            plugins: [
                new workbox.expiration.ExpirationPlugin({
                    maxEntries: maxEntries,
                    maxAgeSeconds: 30 * 24 * 60 * 60,
                }),
                new workbox.cacheableResponse.CacheableResponsePlugin({
                    statuses: [0, 200],
                }),
            ],
        })
    );
    // rss 、sitemap 不缓存
    workbox.routing.registerRoute(
        new RegExp('^https://cuterwrite\.top/(index|sitemap)\.xml'),
        new workbox.strategies.NetworkOnly()
    );
    // 缓存 HTML
    workbox.routing.registerRoute(
        new RegExp('.*\.html'),
        new workbox.strategies.StaleWhileRevalidate({
            cacheName: 'html-cache' + cacheVersion,
            plugins: [
                new workbox.expiration.ExpirationPlugin({
                    maxEntries: maxEntries,
                    maxAgeSeconds: 30 * 24 * 60 * 60,
                }),
                new workbox.cacheableResponse.CacheableResponsePlugin({
                    statuses: [0, 200],
                }),
            ],
        })
    );
    // 缓存 Google Fonts
    workbox.routing.registerRoute(
        new RegExp('.*\.(?:woff|woff2|ttf|otf|eot)'),
        new workbox.strategies.StaleWhileRevalidate({
            cacheName: 'google-fonts' + cacheVersion,
            plugins: [
                // 使用 expiration 插件实现缓存条目数目和时间控制
                new workbox.expiration.ExpirationPlugin({
                    // 最大缓存条目数
                    maxEntries: maxEntries,
                    // 最长缓存时间 30 天
                    maxAgeSeconds: 30 * 24 * 60 * 60,
                }),
                // 使用 cacheableResponse 插件缓存状态码为 0 的请求
                new workbox.cacheableResponse.CacheableResponsePlugin({
                    statuses: [0, 200],
                }),
            ],
        })
    );
    // 缓存 bootcdn、unpkg、jsdelivr 等公共库,用正则匹配
    workbox.routing.registerRoute(
        new RegExp('^https://(?:cdn\.bootcdn\.net|unpkg\.com|cdn\.jsdelivr\.net)'),
        new workbox.strategies.CacheFirst({
            cacheName: 'cdn' + cacheVersion,
            fetchOptions: {
                mode: 'cors',
                credentials: 'omit',
            },
            plugins: [
                new workbox.expiration.ExpirationPlugin({
                    maxEntries: maxEntries,
                    maxAgeSeconds: 30 * 24 * 60 * 60,
                }),
            ],
        })
    );
    // 自建 UMA 统计脚本: https://analytics.cuterwrite.top/uma
    workbox.routing.registerRoute(
        new RegExp('^https://analytics\.cuterwrite\.top/uma'),
        new workbox.strategies.NetworkOnly({
            plugins: [
                // 使用 background sync 插件实现后台同步
                new workbox.backgroundSync.BackgroundSyncPlugin('Optical_Collect', {
                    maxRetentionTime: 12 * 60,
                }),
            ],
        })
    );
    // 缓存存储桶图片 https://cloud.cuterwrite.fun/
    workbox.routing.registerRoute(
        new RegExp('^(https://cuterwrite-1302252842\.file\.myqcloud\.com|https://s2\.loli\.net)'),
        new workbox.strategies.CacheFirst({
            cacheName: 'image-cache' + cacheVersion,
            plugins: [
                new workbox.expiration.ExpirationPlugin({
                    maxEntries: maxEntries,
                    maxAgeSeconds: 30 * 24 * 60 * 60,
                }),
                new workbox.cacheableResponse.CacheableResponsePlugin({
                    statuses: [0, 200],
                }),
            ],
        })
    );

    // 后缀匹配,针对其余没有被域名匹配到的静态文件
    workbox.routing.registerRoute(
        new RegExp('.*\.(?:png|jpg|jpeg|svg|gif|webp|ico)'),
        new workbox.strategies.StaleWhileRevalidate()
    );
    workbox.routing.registerRoute(
        new RegExp('.*\.(css|js)'),
        new workbox.strategies.StaleWhileRevalidate()
    );

    // 默认匹配剩下的请求
    workbox.routing.setDefaultHandler(
        // 优先使用缓存,缓存没有则使用网络请求
        new workbox.strategies.NetworkFirst({
            networkTimeoutSeconds: 3,
        })
    );

} else {
    console.log(`Boo! Workbox didn't load 😬`);
}

# manifest.json

  1. 创建 manifest.json 文件

在你的 Hugo 博客的根目录 static 文件夹下创建 manifest.json 文件,该文件包含了关于你的博客的元数据,例如名称、图标和显示选项。

{
    "name": "你的博客名称",
    "short_name": "博客简称",
    "start_url": "/",
    "display": "standalone",
    "background_color": "#ffffff",
    "theme_color": "#000000",
    "icons": [{
            "src": "/icon-192x192.png",
            "sizes": "192x192",
            "type": "image/png"
        },
        {
            "src": "/icon-512x512.png",
            "sizes": "512x512",
            "type": "image/png"
        }
    ]
}

注释

注意:将 icon-192x192.png 和 icon-512x512.png 替换为你自己的图标文件名。并确保将这两个图标文件放置在 Hugo 博客的 static 文件夹中。如果你想修改主题颜色和背景颜色,可以修改 theme_color 和 background_color 字段。

  1. 链接 manifest.json 文件

在你的 Hugo 博客的 layouts/partials/head/custom.html 文件中添加以下代码,将 manifest.json 文件链接到你的网站:

<link rel="manifest" href="/manifest.json">

完成以上步骤后,你的 Hugo 博客就具备了 PWA 功能,用户可以像使用原生应用程序一样访问你的网站。

# 参考资料

Licensed under CC BY-NC-SA 4.0
本博客已稳定运行
总访客数: Loading
总访问量: Loading
发表了 73 篇文章 · 总计 323.73k

使用 Hugo 构建
主题 StackJimmy 设计
基于 v3.27.0 分支版本修改