更新日志
2020-11-24 根据参考内容起草教程
2020-11-25 完成进度60%
- 完成图标设计及manifest生成配置图文教程。
- 完成hexo-offline-popup教程。
2020-11-26 v1.0
- 完成workbox教程,修改了主题文档的更新通知引入方式,更符合原生主题体验。
- 完成Gulp教程。
- 经典bug归纳。
2021-01-16 v1.01
- 修复workbox的gulpfile.js的bug。
- 感谢@Nesxc的反馈。
2021-01-18 v1.02
- 修复了关于manifest.json必要项的描述。必须填写start_url才能看到地址栏的安装按钮。
- 恭喜@Nesxc双杀。
2021-02-01 v1.03
- 更新了butterfly_v3.6.1的适配提示
2022-01-15 v1.04
- 更新了workbox的配置内容
- 原本的缓存内容太多,直接注册成功后开始流量泄洪。
- 改成只缓存404页面和首页。
写在最前
PWA的全称是Progressive Web Apps,译为渐进式网络应用程序。装配了PWA以后,用户可以将网站作为WEB APP安装到自己的设备上,以原生应用般的方式浏览博客,同时借助PWA的缓存机制,能够更快速的浏览。本文讨论的是使用两种方案实现PWA。最终效果不尽相同,但是都可以实现原生应用体验和更新弹窗提示。其实还有个离线博客,但是视方案不同会有很多BUG,而且离线博客意义何在啊!
图标设计
在使用PWA之前,我们最好先行设计一个符合网站主题的图标。
本站使用的是brandmark
图标设计网站,访问 brandmark进行图标设计。下载需要收费,不过可以截图。建议截图的时候截成正方形。
图文教程
生成图标包及manifest
因为我们最终目的是要制作一个全平台的WEB APP,所以对于图标的大小、类型适配显得格外重要。可以访问realfavicongenerator进行图标制作及manifest
的生成。
图文教程
配置PWA
实现PWA的方式有许多种,本帖基于Butterfly主题文档进行详细拓展,所以只讨论两种方案。
- 使用
hexo-offline-popup
:这个插件配置较为简单,安装以后添加几行配置项即可。适合初学者。 - 使用
workbox
:这个插件需要配合gulp
插件,所以配置较为繁琐,好处是可以自定义适配弹窗提示,适合对前端有一定了解的用户。如果你还有使用pjax,恭喜你,BUG御三家马上就可以集齐了。
在博客根目录
[Blogroot]
下打开终端,输入以下指令安装hexo-offline-popup
插件。1
npm install hexo-offline-popup --save
修改站点配置文件
[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可自行参照上述格式进行配置。将之前生成的图标包移入相应的目录,例如我是
/img/siteicon/
,所以放到[Blogroot]/source/img/siteicon/
目录下。打开图标包内的
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": "Akilarの糖果屋",
"description": "Akilar.top",
"display": "standalone",
"short_name": "Aki~",
"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中不要添加任何注释,不然会报错。注意最后一条内容后面不用加逗号”,” 。
打开主题配置文件
[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运行
hexo clean
之后hexo generate
,使用hexo server
本地查看或者hexo deploy
部署到网站上。可以通过Chrome插件Lighthouse
检查PWA
配置是否生效以及配置是否正确。在Chrome浏览器中打开站点,按F12打开控制台,在右上角找到Lighthouse
,可能没显示出来,在>>
里找找。使用
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,最终影响离线观看博客的体验。
安装必要插件
既然要使用gulp配合workbox实现PWA,自然少不了安装这两个插件。1
2npm install --global gulp-cli # 全局安装gulp命令集
npm install workbox-build gulp --save # 安装workbox和gulp插件创建
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
20const 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"));创建在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
96const workboxVersion = '5.1.3';
importScripts(`https://storage.googleapis.com/workbox-cdn/releases/${workboxVersion}/workbox-sw.js`);
workbox.core.setCacheNameDetails({
prefix: "your name"
});
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();在
[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
}
}修改
[Blogroot]\themes\butterfly\layout\includes\additional-js.pug
,在文件底部添加以下内容,注意缩进。butterfly_v3.6.0
取消了缓存配置,转为完全默认,需要将{cache:theme.fragment_cache}
改为{cache: true}
:1
2
3
4
5
6
7if 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:theme.fragment_cache})将之前生成的图标包移入相应的目录,例如我是
/img/siteicon/
,所以放到[Blogroot]/source/img/siteicon/
目录下。打开图标包内的
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": "Akilarの糖果屋",
"description": "Akilar.top",
"display": "standalone",
"short_name": "Aki~",
"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中不要添加任何注释,不然会报错。注意最后一条内容后面不用加逗号”,” 。
打开主题配置文件
[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
2
3
4
5hexo clean # 清空缓存
hexo generate # 重新编译生成页面
gulp # hexo g之后必须运行gulp指令,不然PWA不会生效
hexo server # 打开本地预览
hexo deploy # 部署到网站上查看运行hexo g之后必须运行gulp指令,不然PWA不会生效!
可以通过Chrome插件
Lighthouse
检查PWA
配置是否生效以及配置是否正确。在Chrome浏览器中打开站点,按F12打开控制台,在右上角找到Lighthouse
,可能没显示出来,在>>
里找找。
拓展内容,使用GULP压缩静态资源
既然已经装了gulp
了,干脆把gulp
也配置好吧。gulp相关内容的详细教程可以看站内教程Gulp压缩全站静态资源
都说了是BUG御三家了,用不用取决于你的个人意志哦,现在回头用hexo-offline-popup
还来得及。
安装全套压缩插件
1
2
3
4
5
6
7
8
9
10
11
12
13# 压缩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-imagemin将
[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
96var gulp = require('gulp');
var cleanCSS = require('gulp-clean-css');
var htmlmin = require('gulp-htmlmin');
var htmlclean = require('gulp-htmlclean');
var imagemin = require('gulp-imagemin');
var workbox = require("workbox-build");
// 若使用babel压缩js,则取消下方注释,并注释terser的代码
// var uglify = require('gulp-uglify');
// var babel = require('gulp-babel');
// 若使用terser压缩js
var terser = require('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'])
.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'))
});
// 壓縮 public/uploads 目錄內圖片
gulp.task('minify-images', async () => {
gulp.src('./public/img/**/*.*')
.pipe(imagemin({
optimizationLevel: 5, //類型:Number 預設:3 取值範圍:0-7(優化等級)
progressive: true, //類型:Boolean 預設:false 無失真壓縮jpg圖片
interlaced: false, //類型:Boolean 預設:false 隔行掃描gif進行渲染
multipass: false, //類型:Boolean 預設:false 多次優化svg直到完全優化
}))
.pipe(gulp.dest('./public/img'));
});
// 執行 gulp 命令時執行的任務
gulp.task("default", gulp.series("generate-service-worker", gulp.parallel(
'compress','minify-html', 'minify-css', 'minify-images'
)));使用了
gulp-babel
压缩js以后,使用了冰卡诺老师的gitcalendar和本站的右键环形菜单教程的用户,会发现gitcalendar
和右键菜单
失效。原因是js加密压缩的算法存在问题。建议直接屏蔽对这两个js的压缩。修改[Blogroot]/gulpfile.js
,添加屏蔽项。(使用terser压缩就不会遇到这个问题,但是无法兼容IE浏览器现在真的还有人会去用IE吗)1
2
3
4
5
6
7
8
9
10
11
12//minify js babel
gulp.task('compress', () =>
- gulp.src(['./public/**/*.js', '!./public/**/*.min.js'])
+ gulp.src(['./public/**/*.js', '!./public/**/*.min.js','!./public/js/custom/galmenu.js','!./public/js/custom/gitcalendar.js'])
.pipe(babel({
presets: ['@babel/preset-env']
}))
.pipe(uglify().on('error', function(e){
console.log(e);
}))
.pipe(gulp.dest('./public'))
);运行gulp指令时报错:
1
2gulp-imagemin: Couldn't load default plugin "gifsicle"
gulp-imagemin: Couldn't load default plugin "optipng"这个众说纷纭,一个是说插件安装不对,一个是说和
nvm
版本不兼容,通过github action使用CI安装时并不会报这个错,推测是nvm
版本不兼容。
事实上这个只是作用于图片压缩,一般也就节省个5kB,而且这个报错不影响网站部署,可以无视。压缩图片还是得靠imagine
。
Use this card to join the candyhome and participate in a pleasant discussion together .
Welcome to Akilar's candyhome,wish you a nice day .