参考文章:

Butterfly 主题通过 hexo-offline 实现 PWA,之前一直使用的是这种方法,根据 hexo-offline 作者的说法,默认策略 sw-precache 是缓存优先,所以网站总是不能及时更新,而且我也想要更新弹窗,于是就换 gulp & workbox 方法来实现 PWA。

但是同时我也使用了 pjax,万恶之源的开端,pjax 与 PWA 同时使用,视方案不同出现的异常不同。

以上内容详见参考文章

更新弹窗样式的引入

这里我并没有采用新建 \themes\butterfly\layout\includes\third-party\pwanotice.pug 的方法,也就不需要修改 \themes\butterfly\layout\includes\additional-js.pug

通过 \_config.butterfly.ymlinject 项引入:

1
2
3
4
5
inject:
head:
- '<style type="text/css">.app-refresh{position:fixed;top:-2.2rem;left:0;right:0;z-index:99999;padding:0 1rem;font-size:15px;height:2.2rem;transition:all 0.3s ease}.app-refresh-wrap{display:flex;color:#fff;height:100%;align-items:center;justify-content:center}.app-refresh-wrap a{color:#fff;text-decoration:underline;cursor:pointer}</style>'
bottom:
- '<div class="app-refresh" id="app-refresh"><div class="app-refresh-wrap"><label>⭐ 已接收到更新 ➡️</label>&nbsp;<a href="javascript:void(0)" onclick="location.reload()">点击刷新</a></div></div><script>(function(){function showNotification(){if(GLOBAL_CONFIG.Snackbar){var theme=document.documentElement.getAttribute("data-theme");var bgColor=theme==="light"?GLOBAL_CONFIG.Snackbar.bgLight:GLOBAL_CONFIG.Snackbar.bgDark;var position=GLOBAL_CONFIG.Snackbar.position;Snackbar.show({text:"已接收到更新",backgroundColor:bgColor,duration:10000,pos:position,actionText:"点击刷新",actionTextColor:"#fff",onActionClick:function(elem){location.reload();}});}else{var theme=document.documentElement.getAttribute("data-theme");var bgColor=theme==="light"?"#b5caa0":"#1f1f1f";document.getElementById("app-refresh").style.cssText=`top: 0; background: ${bgColor};`;}}if("serviceWorker" in navigator){navigator.serviceWorker.controller&&navigator.serviceWorker.addEventListener("controllerchange",showNotification);window.addEventListener("load",function(){navigator.serviceWorker.register("/sw.js");});}})();</script>'

这样就会在浏览器接收并安装完新的 Service Worker 后弹出提醒。

具体样式如描述和颜色等根据自己喜好修改,最终效果如下:

弹窗展示

sw-template 修改

以下提到修改的原始文件来自 Butterfly 主题的 PWA 实现方案

  • workboxVersion 可根据发布版本自行修改
  • prefix 记得改成自己站名
  • workbox.precaching.precacheAndRoute 删除 directoryIndex: null 一项(逗号和大括号记得删),这步很重要,参考文章中 workbox 方式是采用这种办法隐藏 index.html,根据 workbox 相关文档,这将不会处理 index.html 传入的请求,也就是说离线无法加载主页
  • 如果使用的是其他 CDN,可将 /^https:\/\/cdn\.jsdelivr\.net/ 替换

优化不透明响应的缓存

在实现 PWA 以后,我发现我的缓存占额异常,动不动就几百 M,而 Response-Type 为 opaque (不透明)。

异常缓存展示

于是查阅了 Chrome Developers 的文档 Beware of opaque responses!,其中有一条解释——为安全考虑,浏览器会给每个不透明响应至少 7 M 的空间。

A common source of unexpectedly high quota usage is due to runtime caching of opaque responses, which is to say, cross-origin responses to requests made without CORS enabled.

Browsers automatically inflate the quota impact of those opaque responses as a security consideration. In Chrome, for instance, even an opaque response of a few kilobytes will end up contributing around 7 megabytes towards your quota usage.

You can quickly use up much more quota than you’d anticipate once you start caching opaque responses, so the best practice is to use ExpirationPlugin with maxEntries, and potentially purgeOnQuotaError, configured appropriately.

在 sw-template 中,我设置的 statuses: [0, 200] 会缓存所有不透明响应(0),这直接导致了缓存占额的激增,所以接下来需要优化一下这些不透明响应的缓存。

hexo 有插件 hexo-filter-crossorigin 可以自动为元素添加 crossorigin 属性,将不透明响应变为透明响应,但是配置完了以后发现,仍然有部分资源响应类型为不透明,比如背景图片:

不透明响应类型

可以将 sw-template 中 statuses: [0, 200] 改为 statuses: [200] 而不缓存剩余的这些不透明响应。

插件的作者还提供了另外一种方法,我们也可以只修改 sw-template 添加 fetchOptions 来允许跨域请求的响应:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  workbox.routing.registerRoute(
/^https:\/\/cdn\.jsdelivr\.net/,
new workbox.strategies.CacheFirst({
cacheName: "static-libs",
+ fetchOptions: {
+ mode: "cors",
+ credentials: "omit",
+ },
plugins: [
new workbox.expiration.ExpirationPlugin({
maxEntries: 5,
maxAgeSeconds: 60 * 60 * 3,
}),
new workbox.cacheableResponse.CacheableResponsePlugin({
statuses: [0, 200],
}),
],
})
);

我觉得添加 fetchOptions 的方法好一些,能确保所有跨域资源都被正常缓存,配合 service worker 也更灵活一些。

使用 gulp 构建时替换

根据 workbox 相关文档,可以使用 manifestTransforms 选项并提供一个函数来修改清单中的条目,函数使用 Android 大佬提供的解决方法,但是我们是通过 gulp 来生成 service worker,所以在 gulpfile.js 中引入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// PWA
gulp.task("generate-service-worker", () => {
return workbox.injectManifest({
//其他配置...
+ manifestTransforms: [removeIndex],
});
});
+ async function removeIndex(manifestEntries) {
+ const manifest = manifestEntries.map((entry) => {
+ entry.url = entry.url.replace(/(^|\/)index\.html$/, "/");
+ return entry;
+ });
+ return {
+ manifest,
+ };
+ }

现在在构建 sw.jsindex.html 会被替换成 /,达到了隐藏的目的,同时 index.html 传入的请求也会被正常处理,也就是说,在不影响离线体验的情况下,拥有了一个完美的 PWA Blog

workbox 与 gulp 压缩

如果同时使用 gulp 来压缩文件,参考使用 gulp 压缩博客静态资源,任务执行可以这么写:

1
2
3
4
5
6
7
gulp.task(
"default",
gulp.series(
"generate-service-worker",
gulp.parallel("compress", "minify-css", "minify-html", "mini-font")
)
);

series 串行处理,生成 sw.js 后执行压缩,parallel 并行处理压缩任务。


Luminosite Eternelle

Image Source : Azomo