最近给基于 Hugo
搭建的博客添加了 PWA 功能,显著提升了加载速度和用户体验,甚至实现了离线访问。至于如何实现,那么你需要了解 Progressive Web Apps (PWA) 。
什么是 PWA 渐进式 Web 应用(Progressive Web Apps,简称 PWA)利用现代 Web API 和传统的渐进式增强策略,打造出跨平台的 Web 应用程序。这些应用无处不在,功能丰富,为用户带来媲美原生应用的体验。
PWA 的优势:
⚡️ 更快的加载速度 : PWA 可以缓存重要资源,即使网络状况不佳也能快速加载。 ✈️ 离线访问 : PWA 可以缓存内容,让用户即使离线也能访问内容。 🔔 推送通知 : 像原生应用一样,PWA 可以向用户发送推送通知,提高用户参与度。 📱 安装到主屏幕 : 用户可以将你的应用添加到电脑或手机桌面,像原生应用一样浏览你的 Web 应用。 PWA 的实现原理是 Service Worker 。Service 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
文件中(其他主题可能需要根据文件结构进行调整):
1
2
3
4
5
6
7
8
9
10
11
12
13
< 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
导入 Workbox 在你的 Hugo 网站根目录下的 static
文件夹中创建 sw.js
文件。然后,在 sw.js
文件中添加以下代码,使用 CDN 导入 Workbox:
1
importScripts ( 'https://cdn.bootcdn.net/ajax/libs/workbox-sw/7.1.0/workbox-sw.js' );
缓存策略 Workbox 提供了一些常用的缓存策略,如 CacheFirst
、NetworkFirst
、StaleWhileRevalidate
等。这里先介绍几种常用的策略。
CacheOnly 仅缓存 CacheOnly 强制响应来自缓存。
1
2
3
4
workbox . routing . registerRoute (
new RegExp ( regex ),
new workbox . strategies . CacheOnly ()
);
NetworkOnly 仅网络 NetworkOnly 这种缓存策略强制要求所有请求都从网络获取最新数据,完全绕过缓存。
1
2
3
4
workbox . routing . registerRoute (
new RegExp ( regex ),
new workbox . strategies . NetworkOnly ()
);
CacheFirst 优先缓存 CacheFirst 这种缓存策略以速度为优先,会首先尝试从缓存中获取响应,以尽快向用户显示内容。如果缓存中没有所需数据,它才会向网络发起请求获取数据。
1
2
3
4
workbox . routing . registerRoute (
new RegExp ( regex ),
new workbox . strategies . CacheFirst ()
);
NetworkFirst 优先网络 NetworkFirst 这种缓存策略优先使用最新数据,因此会首先尝试从网络获取响应。如果网络请求失败,例如用户离线或网络连接不稳定,它会回退使用缓存中的数据,确保用户仍然可以访问内容。
1
2
3
4
workbox . routing . registerRoute (
new RegExp ( regex ),
new workbox . strategies . NetworkFirst ()
);
StaleWhileRevalidate 读取缓存,同时发起网络请求 StaleWhileRevalidate 这种缓存策略优先返回缓存内容(如果有)。即使缓存内容有效,它也会在后台发起网络请求以获取最新数据,保证用户最终能看到最新内容。虽然这种策略能确保用户定期更新缓存,但也意味着每次请求都会产生网络流量,即使数据没有变化,也比较浪费带宽。
1
2
3
4
workbox . routing . registerRoute (
new RegExp ( regex ),
new workbox . strategies . StaleWhileRevalidate ()
);
策略配置 Workbox 不仅提供上述策略,还允许通过 cacheName、plugins 和 expiration 等配置项进行自定义。你可以通过定义要使用的插件来自定义路由行为。例如,你可以配置缓存名称、缓存有效期以及可缓存的响应状态码,如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 ],
}),
],
})
);
本站配置
全局配置 以下是全局缓存配置:
1
2
3
4
// 缓存版本号
let cacheVersion = '-240619' ;
// 最大条目数
const maxEntries = 100 ;
Twitto 配置 为了确保用户即使在离线状态下也能查看评论,Twitto 评论 API 采用了 NetworkFirst
缓存策略。这意味着浏览器会优先尝试从网络获取最新数据,如果网络不可用,则使用缓存中的数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 数据,这些页面配置为仅使用网络策略 (NetworkOnly
),不进行缓存。
1
2
3
4
workbox . routing . registerRoute (
new RegExp ( '^https://cuterwrite\.top/(index|sitemap)\.xml' ),
new workbox . strategies . NetworkOnly ()
);
HTML 配置 为了在保证用户快速加载页面的同时,也能获取到最新内容,网站对 HTML 页面采用了 StaleWhileRevalidate
缓存策略。这意味着浏览器会优先使用缓存中的页面进行展示,同时在后台向服务器发起请求,获取最新版本,并在下次请求时使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
缓存策略,并设置了较长的缓存过期时间。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
缓存策略,并设置了较长的缓存过期时间。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
插件来实现即使在网络离线的情况下也能保证数据最终发送成功。
1
2
3
4
5
6
7
8
9
10
11
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
缓存策略,并设置了较长的缓存过期时间。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
缓存策略。
1
2
3
4
5
6
7
8
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
策略并设置了网络超时时间,以兼顾资源获取速度和离线可用性。
1
2
3
4
5
6
workbox . routing . setDefaultHandler (
// 优先使用缓存,缓存没有则使用网络请求
new workbox . strategies . NetworkFirst ({
networkTimeoutSeconds : 3 ,
})
);
完整配置 sw.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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
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 创建 manifest.json 文件 在你的 Hugo 博客的根目录 static
文件夹下创建 manifest.json
文件,该文件包含了关于你的博客的元数据,例如名称、图标和显示选项。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"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 字段。
链接 manifest.json 文件 在你的 Hugo 博客的 layouts/partials/head/custom.html
文件中添加以下代码,将 manifest.json
文件链接到你的网站:
1
< link rel = "manifest" href = "/manifest.json" >
完成以上步骤后,你的 Hugo 博客就具备了 PWA 功能,用户可以像使用原生应用程序一样访问你的网站。
参考资料