原教程:

生成图标包及manifest

因为我们最终目的是要制作一个全平台的WEB APP,所以对于图标的大小、类型适配显得格外重要。可以访问realfavicongenerator进行图标制作及manifest的生成。

图文教程(可省略)

这一步可以省略,这个是店长的教程图片链接,我只是为了记录我的博客美化记录,后续出问题可以重新部署,各位请理解

选择图片

选择图片

创建所有图标

创建所有图标

调整Windows磁贴图标配色

调整Windows磁贴图标配色

设置图片相对于source目录的存放路径

设置图片相对于source目录的存放路径

设置Web App名称

设置Web App名称

生成README.md

生成README.md

选择生成

选择生成

下载资源包

下载资源包

获取图标文件和manifest

获取图标文件和manifest

配置PWA

  1. 在博客根目录[Blogroot]下打开终端,输入以下指令安装hexo-offline-popup插件。
1
npm install hexo-offline-popup --save
  1. 修改站点配置文件[Blogroot]/_config.yml,在站点配置文件_config.yml中增加以下内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# hexo-offline-popup.
service_worker:
maximumFileSizeToCacheInBytes: 3145728 # 缓存的最大文件大小,以字节为单位,此处设置为3MB。
staticFileGlobs:
- public/**/*.{js,html,xml,css,png,jpg,gif,svg,webp,eot,ttf,woff,woff2}
# - public/**/*.{html,xml} #精简版使用这行即可
# 静态文件合集,如果你的站点使用了例如webp格式的文件,请将文件类型添加进去。。
# 注意,此处的文件类型就是会缓存下来的所有文件类型,如果不需要缓存那么多,
# 而只是想判断网页更新与否,缓存html和xml即可。
stripPrefix: public
verbose: false
runtimeCaching:
# CDNs - should be cacheFirst, since they should be used specific versions so should not change
- urlPattern: /* # 如果你需要加载CDN資源,请配置该选项,如果沒有,可以不配置。
handler: cacheFirst
options:
origin: unpkg.com # 又拍云
- urlPattern: /*
handler: cacheFirst
options:
origin: cdn.jsdelivr.net # jsdelivr
# 更多cdn可自行参照上述格式进行配置。
  1. 将之前生成的图标包移入相应的目录,例如我是/img/siteicon/,所以放到[Blogroot]/source/img/siteicon/目录下。
  2. 打开图标包内的site.webmanifest,建议修改文件名为manifest.json并将其放到[Blogroot]/source目录下,此时还不能直接用,需要添加一些内容,以下是我的manifest.json配置内容,权且作为参考,其中的theme_color建议用取色器取设计的图标的主色调,同时务必配置start_url和name的配置项,这关系到你之后能否看到浏览器的应用安装按钮。:
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
{   "lang": "en",
"dir": "ltr",
"name": "冰刻无痕",
"description": "bm158.tk",
"display": "standalone",
"short_name": "冰梦",
"scope": "/",
"start_url": "/",
"theme_color": "#212121",
"background_color": "#212121",
"icons": [
{
"src": "/img/siteicon/android-chrome-36x36.png",
"sizes": "36x36",
"type": "image/png"
},
{
"src": "/img/siteicon/android-chrome-48x48.png",
"sizes": "48x48",
"type": "image/png"
},
{
"src": "/img/siteicon/android-chrome-72x72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "/img/siteicon/android-chrome-96x96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "/img/siteicon/android-chrome-144x144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "/img/siteicon/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/img/siteicon/android-chrome-256x256.png",
"sizes": "256x256",
"type": "image/png"
},
{
"src": "/img/siteicon/android-chrome-384x384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "/img/siteicon/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}

json中不要添加任何注释,不然会报错。注意最后一条内容后面不用加逗号”,” 。

  1. 打开主题配置文件[Blogroot]/_config.butterfly.yml,找到PWA配置项。添加图标路径。这里的theme_color建议改成你图标的主色调,包括manifest.json中的theme_color也是如此。
1
2
3
4
5
6
7
8
9
# PWA
pwa:
enable: true
manifest: /manifest.json
theme_color: '#212121'
apple_touch_icon: /img/siteicon/apple-touch-icon.png
favicon_32_32: /img/siteicon/favicon-32x32.png
favicon_16_16: /img/siteicon/favicon-16x16.png
mask_icon: /img/siteicon/safari-pinned-tab.svg
  1. 运行hexo clean之后hexo generate,使用hexo server本地查看或者hexo deploy部署到网站上。可以通过Chrome插件Lighthouse检查PWA配置是否生效以及配置是否正确。在Chrome浏览器中打开站点,按F12打开控制台,在右上角找到Lighthouse,可能没显示出来,在>>里找找。

  2. 使用hexo-offline-popup以后,如果还开启了pjax,可能遇到页面URL带着长长的后缀。形似index.html?_sw-precache=fff6559539ab8f2d6043bcfa832ce38f。此处感谢Android(矩阵)大佬提供的方案,把以下js引入即可,实质是劫持了pjax,并对其链接进行重定向:

1
2
3
4
5
6
7
8
//重定向浏览器地址
pjax.site_handleResponse = pjax.handleResponse;
pjax.handleResponse = function(responseText, request, href, options){
Object.defineProperty(request,'responseURL',{
value: href
});
pjax.site_handleResponse(responseText,request,href,options);
}

workbox是通过设置 directoryIndex:null来去掉index.html的。这会导致PWA无法加载索引文件,也就是说无法从PWA加载index.html,最终影响离线观看博客的体验。

  1. 安装必要插件
    既然要使用gulp配合workbox实现PWA,自然少不了安装这两个插件。

    1
    2
    npm install --global gulp-cli # 全局安装gulp命令集
    npm install workbox-build gulp --save # 安装workbox和gulp插件
  2. 创建gulpfile.js
    在Hexo的根目录,创建一个gulpfile.js文件,打开[Blogroot]/gulpfile.js,
    输入

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    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,xml}"
    // 推荐只缓存404,主页和主要样式和脚本。
    "404.html","index.html","js/main.js","css/index.css"
    ],
    modifyURLPrefix: {
    "": "./"
    }
    });
    });
    gulp.task("default", gulp.series("generate-service-worker"));
  3. 创建在Hexo的根目录,创建一个sw-template.js文件,打开[Blogroot]/sw-template.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
    const workboxVersion = '5.1.3';

    importScripts(`https://storage.googleapis.com/workbox-cdn/releases/${workboxVersion}/workbox-sw.js`);

    workbox.core.setCacheNameDetails({
    prefix: "冰梦"
    });

    workbox.core.skipWaiting();

    workbox.core.clientsClaim();

    // 注册成功后要立即缓存的资源列表
    // 具体缓存列表在gulpfile.js中配置,见下文
    workbox.precaching.precacheAndRoute(self.__WB_MANIFEST,{
    directoryIndex: null
    });

    // 清空过期缓存
    workbox.precaching.cleanupOutdatedCaches();

    // 图片资源(可选,不需要就注释掉)
    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]
    })
    ]
    })
    );

    // 字体文件(可选,不需要就注释掉)
    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]
    })
    ]
    })
    );

    // 谷歌字体(可选,不需要就注释掉)
    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]
    })
    ]
    })
    );

    // jsdelivr的CDN资源(可选,不需要就注释掉)
    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();
  4. [Blogroot]\themes\butterfly\layout\includes\third-party\目录下新建pwanotice.pug文件,
    打开[Blogroot]\themes\butterfly\layout\includes\third-party\pwanotice.pug,输入:

    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
    #app-refresh.app-refresh(style='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(style=' display: flex;color: #fff;height: 100%;align-items: center;justify-content: center;')
    label ✨ 冰梦更新啦! 👉
    a(href='javascript:void(0)' onclick='location.reload()')
    span(style='color: #fff;text-decoration: underline;cursor: pointer;') 🍭查看新品🍬
    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
    }
    }
  5. 修改[Blog]\themes\butterfly\layout\includes\additional-js.pug,在文件底部添加以下内容。

1
2
3
4
5
6
7
      if theme.pjax.enable
!=partial('includes/third-party/pjax', {}, {cache:theme.fragment_cache})

!=partial('includes/third-party/baidu_push', {}, {cache:theme.fragment_cache})

+ if theme.pwa.enable
+ !=partial('includes/third-party/pwanotice', {}, {cache: true})
  1. 将之前生成的图标包移入相应的目录,例如我是/img/siteicon/,所以放到[Blog]/source/img/siteicon/目录下。

  2. 打开图标包内的site.webmanifest,建议修改文件名为manifest.json并将其放到[Blog]/source目录下,此时还不能直接用,需要添加一些内容,以下是我的manifest.json配置内容,权且作为参考,其中的theme_color建议用取色器取设计的图标的主色调,同时务必配置start_url和name的配置项,这关系到你之后能否看到浏览器的应用安装按钮

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
{   "lang": "en",
"dir": "ltr",
"name": "冰刻无痕",
"description": "bm158.tk",
"display": "standalone",
"short_name": "冰梦",
"scope": "/",
"start_url": "/",
"theme_color": "#212121",
"background_color": "#212121",
"icons": [
{
"src": "/img/siteicon/android-chrome-36x36.png",
"sizes": "36x36",
"type": "image/png"
},
{
"src": "/img/siteicon/android-chrome-48x48.png",
"sizes": "48x48",
"type": "image/png"
},
{
"src": "/img/siteicon/android-chrome-72x72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "/img/siteicon/android-chrome-96x96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "/img/siteicon/android-chrome-144x144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "/img/siteicon/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/img/siteicon/android-chrome-256x256.png",
"sizes": "256x256",
"type": "image/png"
},
{
"src": "/img/siteicon/android-chrome-384x384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "/img/siteicon/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}

json中不要添加任何注释,不然会报错。注意最后一条内容后面不用加逗号”,” 。

  1. 运行以下指令

    1
    2
    3
    4
    5
    hexo clean # 清空缓存
    hexo generate # 重新编译生成页面
    gulp # hexo g之后必须运行gulp指令,不然PWA不会生效
    hexo server # 打开本地预览
    hexo deploy # 部署到网站上查看

运行hexo g之后必须运行gulp指令,不然PWA不会生效!

可以通过Chrome插件Lighthouse检查PWA配置是否生效以及配置是否正确。在Chrome浏览器中打开站点,按F12打开控制台,在右上角找到Lighthouse,可能没显示出来,在>>里找找。

拓展内容,使用gulp压缩静态资源

BUG御三家,考虑好再斟酌使用,用不用取决于你,现在回头用hexo-offline-popup还来得及。

  1. 安装全套压缩插件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 上面有安装过,不需要重复安装
npm install --global gulp-cli # 全局安装gulp命令集
npm install workbox-build gulp --save # 安装workbox和gulp插件

# 压缩html插件
npm install gulp-htmlclean --save-dev
npm install --save gulp-htmlmin
# 压缩css插件
npm install gulp-clean-css --save-dev
# 压缩js插件
# 使用babel压缩js,与terser二选一
npm install --save-dev gulp-uglify
npm install --save-dev gulp-babel @babel/core @babel/preset-env
# 使用terser压缩js,与babel二选一
npm install gulp-terser --save-dev
npm install --save-dev gulp-babel @babel/core @babel/preset-env
# 压缩图片插件
npm install --save-dev gulp-imagemin
# 压缩字体插件(font-min仅支持压缩ttf格式的字体包)
npm install gulp-fontmin --save-dev

关于 font-min 的补充说明,在本文中,是通过读取所有编译好的 html 文件(./public/*/.html)中的字符,然后匹配原有字体包内./public/fonts/.ttf 字体样式,输出压缩后的字体包到./public/fontsdest/目录。所以最终引用字体的相对路径应该是/fontsdest/.ttf。而本地测试时,如果没有运行 gulp,自然也就不会输出压缩字体包到 public 目录,也就看不到字体样式。

gulp-terser 只会直接压缩 js 代码,所以不存在因为语法变动导致的错误。事实上,当我们使用 jsdelivrCDN 服务时,只需要在 css 或者 js 的后缀前添加.min,例如 example.js->example.min.js,JsDelivr 就会自动使用 terser 帮我们压缩好代码。

package.json 中添加

1
"type": "module",

  1. [Blogroot]/gulpfile.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
    import gulp from "gulp";
    import cleanCSS from "gulp-clean-css";
    import htmlmin from "gulp-htmlmin";
    import htmlclean from "gulp-htmlclean";
    import workbox from "workbox-build";
    import fontmin from "gulp-fontmin";

    // 若使用babel压缩js,则取消下方注释,并注释terser的代码
    // var uglify = require('gulp-uglify');
    // var babel = require('gulp-babel');

    // 若使用terser压缩js
    import terser from "gulp-terser";

    //pwa
    gulp.task("generate-service-worker", () => {
    return workbox.injectManifest({
    swSrc: "./sw-template.js",
    swDest: "./public/sw.js",
    globDirectory: "./public",
    globPatterns: [
    // 缓存所有以下类型的文件,极端不推荐
    // "**/*.{html,css,js,json,woff2,xml}"
    // 推荐只缓存404,主页和主要样式和脚本。
    "404.html",
    "index.html",
    "js/main.js",
    "css/index.css",
    ],
    modifyURLPrefix: {
    "": "./",
    },
    });
    });

    //minify js babel
    // 若使用babel压缩js,则取消下方注释,并注释terser的代码
    // gulp.task('compress', () =>
    // gulp.src(['./public/**/*.js', '!./public/**/*.min.js'])
    // .pipe(babel({
    // presets: ['@babel/preset-env']
    // }))
    // .pipe(uglify().on('error', function(e){
    // console.log(e);
    // }))
    // .pipe(gulp.dest('./public'))
    // );

    // minify js - gulp-tester
    // 若使用terser压缩js
    gulp.task("compress", () =>
    gulp
    .src([
    "./public/**/*.js",
    "!./public/**/*.min.js",
    "!./public/js/custom/galmenu.js",
    "!./public/js/custom/gitcalendar.js",
    ])
    .pipe(terser())
    .pipe(gulp.dest("./public"))
    );

    //css
    gulp.task("minify-css", () => {
    return gulp
    .src("./public/**/*.css")
    .pipe(
    cleanCSS({
    compatibility: "ie11",
    })
    )
    .pipe(gulp.dest("./public"));
    });

    // 压缩 public 目录内 html
    gulp.task("minify-html", () => {
    return gulp
    .src("./public/**/*.html")
    .pipe(htmlclean())
    .pipe(
    htmlmin({
    removeComments: true, //清除 HTML 註释
    collapseWhitespace: true, //压缩 HTML
    collapseBooleanAttributes: true, //省略布尔属性的值 <input checked="true"/> ==> <input />
    removeEmptyAttributes: true, //删除所有空格作属性值 <input id="" /> ==> <input />
    removeScriptTypeAttributes: true, //删除 <script> 的 type="text/javascript"
    removeStyleLinkTypeAttributes: true, //删除 <style> 和 <link> 的 type="text/css"
    minifyJS: true, //压缩页面 JS
    minifyCSS: true, //压缩页面 CSS
    minifyURLs: true,
    })
    )
    .pipe(gulp.dest("./public"));
    });

    //压缩字体
    function minifyFont(text, cb) {
    gulp
    .src("./public/fonts/*.ttf") //原字体所在目录
    .pipe(
    fontmin({
    text: text,
    })
    )
    .pipe(gulp.dest("./public/fontsdest/")) //压缩后的输出目录
    .on("end", cb);
    }

    gulp.task("mini-font", cb => {
    var buffers = [];
    gulp
    .src(["./public/**/*.html"]) //HTML文件所在目录请根据自身情况修改
    .on("data", function (file) {
    buffers.push(file.contents);
    })
    .on("end", function () {
    var text = Buffer.concat(buffers).toString("utf-8");
    minifyFont(text, cb);
    });
    });

    // 执行 gulp 命令时执行的任务
    gulp.task(
    "default",
    gulp.series("generate-service-worker", gulp.parallel("compress", "minify-html", "minify-css", "mini-font"))
    );