# Hexo:使用 Gulp 为你的博客配置 PWA

本文以 Butterfly 主题为范例。

使用这个方法之前,请先卸载掉其它的 PWA 插件,并安装 Gulp 和 WorkBox。

npm install gulp-cli -g
npm install workbox-build gulp --save-dev

本文参考利用 Workbox 实现博客的 PWA Butterfly 进阶教程

# 写在前面

渐进式网络应用程式(英语:Progressive Web Apps,简称:PWA)是一种普通网页或网站架构起来的网络应用程式,但它可以以传统应用程式或原生移动应用程式形式展示给用户。这种应用程式形态视图将目前最为现代化的浏览器提供的功能与行动装置的体验优势相结合。

当你的网站实现了 PWA,那就代表了

  • 用户可以添加你的博客到电脑 / 手机的桌面,以原生应用般的方式浏览你的博客
  • 用户本地可以自动生成缓存,二次访问速度大大加快
  • 用户可以离线浏览你的博客

下面的 PWA 实现方法借助了 Gulp 插件,在站点有内容更新时,可以弹窗提醒用户刷新页面。

# 开启主题相关设置

以 Butterfly 主题为例,在 butterfly.yml 中开启 PWA 选项

实例:

pwa:
  enable: true
  manifest: /manifest.json # 清单文件,下文将介绍如何生成
  theme_color: "#fff" # 应用程序顶栏的背景色
  apple_touch_icon: /img/pwa/apple-touch-icon.png # 添加至苹果移动设备的主屏幕后显示的图标,尽量使用 png 格式
  favicon_32_32: /img/pwa/32.png # 32 * 32 像素,网页图标
  favicon_16_16: /img/pwa/16.png # 16 * 16 像素,网页图标
  mask_icon: /img/pwa/safari-pinned-tab.svg # 苹果电脑 Touch Bar 区域显示的收藏栏封面图,须使用 svg 格式

图片尺寸要求:

  • apple_touch_icon:192 * 192 像素
  • mask_icon:viewBox 的值必须是 0 0 16 16

如果你的主题没有内置 PWA,下面附有 Butterfly 主题的 PWA 部分,可以根据你所用的主题调整下面的 Pug 模板,编译后插入到 html 生成模板的 head 处。

link(rel="manifest" href=url_for(theme.pwa.manifest))
if(theme.pwa.theme_color) 
  meta(name="theme-color" content=theme.pwa.theme_color)
if(theme.pwa.theme_color) 
  meta(name="msapplication-TileColor" content=theme.pwa.theme_color)
if(theme.pwa.apple_touch_icon) 
  link(rel="apple-touch-icon" sizes="180x180" href=url_for(theme.pwa.apple_touch_icon))
if(theme.pwa.favicon_32_32) 
  link(rel="icon" type="image/png" sizes="32x32" href=url_for(theme.pwa.favicon_32_32))
if(theme.pwa.favicon_16_16)
  link(rel="icon" type="image/png" sizes="16x16" href=url_for(theme.pwa.favicon_16_16))
if(theme.pwa.mask_icon)
  link(rel="mask-icon" href=url_for(theme.pwa.mask_icon) color="#5bbad5")

# 配置 manifest.json

创建 manifest.json ,路径应与配置文件中所填路径相同。

实例及配置说明:

{
  "name": "Serok's Blog", // 应用全称
  "short_name": "Seeker", // 应用简称
  "theme_color": "#49b1f5", // 应用主题色
  "background_color": "#49b1f5", // 加载应用时的背景色
  "display": "minimal-ui", // 首選顯示模式
  // 更多顯示模式:"fullscreen", "standalone", "browser"
  "scope": "/",
  "start_url": "/",
  "icons": [ 
    // 指定 icons 參數,用來適配不同設備
    // 需為 png 格式,至少包含一个 192 * 192 像素的圖標
    { 
      "src": "https://snow.js.org/image/pwaicons/192.png", // 建议采用绝对路径
      "sizes": "192x192",
      "type": "image/png"
    }
  ],
  "splash_pages": null // 自定義启动动画
}

default

Hexo 采用的是严格 Json 规范,因此 manifest.json 文件配置好后,需删除文件中的所有注释。

# 安装插件

在命令行中输入

npm install workbox-build gulp --save-dev

# 创建 gulpfile.js 文件

在博客的根目录下,创建一个 gulpfile.js 文件

const gulp = require("gulp");
const workbox = require("workbox-build");
gulp.task('generate-service-worker', () => {
    return workbox.injectManifest({
        swSrc: './sw-template.js',
        swDest: './public/sw.js',
        globDirectory: './public',
        globPatterns: [
            "**/*.{html,css,js,json,woff2}"
        ],
        modifyURLPrefix: {
            "": "./"
        }
    });
});
gulp.task("build", gulp.series("generate-service-worker"));

# 创建 sw-template.js 文件

在博客的根目录下,创建一个 sw-template.js 文件

const workboxVersion = '5.1.3';
importScripts(`https://storage.googleapis.com/workbox-cdn/releases/${workboxVersion}/workbox-sw.js`);
workbox.core.setCacheNameDetails({
    prefix: "Serok's Blog"
});
workbox.core.skipWaiting();
workbox.core.clientsClaim();
workbox.precaching.precacheAndRoute(self.__WB_MANIFEST,{
    directoryIndex: null
});
workbox.precaching.cleanupOutdatedCaches();
// Images
workbox.routing.registerRoute(
    /\.(?:png|jpg|jpeg|gif|bmp|webp|svg|ico)$/,
    new workbox.strategies.CacheFirst({
        cacheName: "images",
        plugins: [
            new workbox.expiration.ExpirationPlugin({
                maxEntries: 1000,
                maxAgeSeconds: 60 * 60 * 24 * 30
            }),
            new workbox.cacheableResponse.CacheableResponsePlugin({
                statuses: [0, 200]
            })
        ]
    })
);
// Fonts
workbox.routing.registerRoute(
    /\.(?:eot|ttf|woff|woff2)$/,
    new workbox.strategies.CacheFirst({
        cacheName: "fonts",
        plugins: [
            new workbox.expiration.ExpirationPlugin({
                maxEntries: 1000,
                maxAgeSeconds: 60 * 60 * 24 * 30
            }),
            new workbox.cacheableResponse.CacheableResponsePlugin({
                statuses: [0, 200]
            })
        ]
    })
);
// Google Fonts
workbox.routing.registerRoute(
    /^https:\/\/fonts\.googleapis\.com/,
    new workbox.strategies.StaleWhileRevalidate({
        cacheName: "google-fonts-stylesheets"
    })
);
workbox.routing.registerRoute(
    /^https:\/\/fonts\.gstatic\.com/,
    new workbox.strategies.CacheFirst({
        cacheName: 'google-fonts-webfonts',
        plugins: [
            new workbox.expiration.ExpirationPlugin({
                maxEntries: 1000,
                maxAgeSeconds: 60 * 60 * 24 * 30
            }),
            new workbox.cacheableResponse.CacheableResponsePlugin({
                statuses: [0, 200]
            })
        ]
    })
);
// Static Libraries
workbox.routing.registerRoute(
    /^https:\/\/cdn\.jsdelivr\.net/,
    new workbox.strategies.CacheFirst({
        cacheName: "static-libs",
        plugins: [
            new workbox.expiration.ExpirationPlugin({
                maxEntries: 1000,
                maxAgeSeconds: 60 * 60 * 24 * 30
            }),
            new workbox.cacheableResponse.CacheableResponsePlugin({
                statuses: [0, 200]
            })
        ]
    })
);
workbox.googleAnalytics.initialize();

注意:把 prefix 修改为你博客的名字(最好用英文)。

上面的文件涵盖了大多数资源的缓存策略。如果你想缓存其他类型的资源(例如一些国内的镜像 CDN 库),或者想使用其他的缓存方式,请自行查看相关文档并添加。

# 添加 js 进主题

配置 butterfly.yml , 添加需要的 css 和 js

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 .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> <a href="javascript:void(0)" onclick="location.reload()">点击刷新</a> </div></div><script>function showNotification(){if(GLOBAL_CONFIG.Snackbar){var t="light"===document.documentElement.getAttribute("data-theme")?GLOBAL_CONFIG.Snackbar.bgLight:GLOBAL_CONFIG.Snackbar.bgDark,e=GLOBAL_CONFIG.Snackbar.position;Snackbar.show({text:"已更新最新版本",backgroundColor:t,duration:5e5,pos:e,actionText:"点击刷新",actionTextColor:"#fff",onActionClick:function(t){location.reload()}})}else{var o=`top: 0; background: ${"light"===document.documentElement.getAttribute("data-theme")?"#49b1f5":"#1f1f1f"};`;document.getElementById("app-refresh").style.cssText=o}}"serviceWorker"in navigator&&(navigator.serviceWorker.controller&&navigator.serviceWorker.addEventListener("controllerchange",function(){showNotification()}),window.addEventListener("load",function(){navigator.serviceWorker.register("/sw.js")}));</script>'

同样,如果你使用的不是 Butterfly 主题,可以在所示代码的基础上修改以适配你的主题。以下是展开后的代码,便于修改调试。

# 以下代码请插入到头部 </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 span {
    color: #fff;
    text-decoration: underline;
    cursor: pointer;
  }
</style>

# 以下代码请插入到底部 </body> 之前:

<div class="app-refresh" id="app-refresh">
  <div class="app-refresh-wrap">
    <label>✨ 网站已更新最新版本 👉</label>
    <a href="javascript:void(0)" onclick="location.reload()">点击刷新</a>
  </div>
</div>
<script>
  if ('serviceWorker' in navigator) {
    if (navigator.serviceWorker.controller) {
      navigator.serviceWorker.addEventListener('controllerchange', function () {
        showNotification()
      })
    }
    window.addEventListener('load', function () {
      navigator.serviceWorker.register('/sw.js')
    })
  }
  function showNotification() {
    if (GLOBAL_CONFIG.Snackbar) {
      var snackbarBg =
        document.documentElement.getAttribute('data-theme') === 'light'
          ? GLOBAL_CONFIG.Snackbar.bgLight
          : GLOBAL_CONFIG.Snackbar.bgDark
      var snackbarPos = GLOBAL_CONFIG.Snackbar.position
      Snackbar.show({
        text: '已更新最新版本',
        backgroundColor: snackbarBg,
        duration: 500000,
        pos: snackbarPos,
        actionText: '点击刷新',
        actionTextColor: '#fff',
        onActionClick: function (e) {
          location.reload()
        },
      })
    } else {
      var showBg =
        document.documentElement.getAttribute('data-theme') === 'light'
          ? '#49b1f5'
          : '#1f1f1f'
      var cssText = `top: 0; background: ${showBg};`
      document.getElementById('app-refresh').style.cssText = cssText
    }
  }
</script>

# 运行

为了方便每次部署,可以在你的博客根目录下新建 deploy.sh ,添加以下内容:

#!/bin/sh
echo "Start"
hexo clean
hexo generate
gulp
hexo deploy
echo "Finish"
echo 按任意键继续
read -n 1

然后每次部署就可以直接运行这个脚本了。

更新于