📖 本教程更新於 2021 年 03 月 30 日,教程的內容針對最新穩定版而更新(如果你是舊版,教程會有些出入,請留意)

🦋 Butterfly 已經更新到 3.7.1

如果有安裝這兩個插件的,請卸載掉,會導致主題報錯。
hexo-injecthexo-neat


建議

  1. 不要把個人需要的文件/圖片放在主題source文件夾裏,因為在升級主題的過程中,可能會把文件覆蓋刪除了。
    在Hexo根目錄的source文件夾裏,創建一個文件夾來放置個人文件/圖片。
    引用文件直接為/文件夾名稱/文件名

音樂

音樂界面使用了插件 hexo-tag-aplayer
使用方法請參考插件文檔

音樂頁面只是普通的page頁,按普通頁面操作生成就行。

以下內容可供選擇配置

注意: 仍需要安裝插件hexo-tag-aplayer

插件會在每一個文件都插入 js 和 css,為了避免這一情況,3.0 版本內置了 aplayer 需要的 css 和 js。

首先在Hexo根目錄_config裏配置asset_injectfalse

1
2
aplayer:
asset_inject: false

然後在你需要使用aplayer的頁面Front-matter添加

1
aplayer: true

這樣只會在需要aplayer的頁面插入js和css。

如何添加全局 Aplayer 播放,請參考 這篇文章

電影

電影界面使用了插件 hexo-butterfly-douban
使用方法請參考插件的文檔

注意:

  1. hexo-butterfly-douban 會主動生成頁面,所以不需要自己創建。
  2. 如遇到無法抓取問題,顯示 INFO 0 movies have been loaded in xxx ms, because you are offline or your network is bad
    請過段時間再試試,這我也無能為力。

説説

Artitalk

安裝插件 hexo-butterfly-artitalk

具體配置查看插件文檔

HexoPlusPlus Talk

安裝插件 hexo-butterfly-hpptalk

具體配置查看插件文檔

自定義代碼配色

點擊前往

自定義側邊欄

點擊前往

添加全局吸底Aplayer教程

點擊前往

jQuery 加載

主題已於 3.4.0 去除 jQuery 的引用,但是部分功能仍需要加載 jQuery(Justified Galleryfancybox)。

如果你仍需要使用 jQuery,可以調用主題提供的 function,防止 jQuery 多次加載。

btf.isJqueryLoad(fn)

function 會判斷是否加載了 jQuery ,如果沒有,加載 jQuery 後運行 fn。如果有,直接運行 fn。

使用方法

1
2
3
4
5
6
7
8
9
10
// 方法 1
btf.isJqueryLoad(function() {
你的function
}}

// 方法 2
function myFn () {
你的function
}
btf.isJqueryLoad(myFn)

Gulp壓縮

Gulp 是一款自動化構建的工具,擁有眾多的插件。而我們只需要使用到幾個插件來壓縮Html/css/js。

安裝Gulp

1
npm install --global gulp-cli

插件安裝

壓縮HTML

可以使用gulp-htmlcleangulp-html-minifier-terser來壓縮HTML

1
2
npm install gulp-htmlclean --save-dev
npm install --save gulp-html-minifier-terser

壓縮CSS

可以使用gulp-clean-css來壓縮CSS

1
npm install gulp-clean-css --save-dev

壓縮JS

由於Butterfly主題中的JS使用到了部分ES6語法,因此不能只使用 gulp-uglify 來壓縮,還需要搭配其它的插件。兩種方法都可以有效的壓縮JS代碼,選一種適合自己的就行。

gulp-terser 是直接壓縮 js 代碼,不會進行轉換

gulp-babel是一個JavaScript轉換編譯器,可以把ES6轉換成ES5

gulp-terser

1
npm install gulp-terser --save-dev

gulp-uglify + gulp-babel

1
2
npm install --save-dev gulp-uglify
npm install --save-dev gulp-babel @babel/core @babel/preset-env

壓縮圖片

可以使用gulp-imagemin來壓縮圖片

1
npm install --save-dev gulp-imagemin

創建 gulpfile 文件

在Hexo的根目錄,創建一個 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
const gulp = require('gulp')
const cleanCSS = require('gulp-clean-css')
const htmlmin = require('gulp-html-minifier-terser')
const htmlclean = require('gulp-htmlclean')
const imagemin = require('gulp-imagemin')
// gulp-tester (如果使用 gulp-tester,把下面的//去掉)
// const terser = require('gulp-terser');

// babel (如果不是使用bebel,把下面兩行註釋掉)
const uglify = require('gulp-uglify')
const babel = require('gulp-babel')

// minify js - babel( 如果不是使用bebel,把下面註釋掉)
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 (如果使用 gulp-tester,把下面前面的//去掉)
// 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())
.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.parallel(
'compress', 'minify-css', 'minify-html', 'minify-images'
))

運行

hexo g之後運行gulp就行。

1
gulp

PWA

這是另一種實現PWA的方法,使用這個方法之前,請先卸載掉其它的PWA插件。

據維基百科介紹

漸進式網絡應用程式(英語:Progressive Web Apps,簡稱:PWA)是一種普通網頁網站架構起來的網絡應用程式,但它可以以傳統應用程式或原生移動應用程式形式展示給用户。這種應用程式形態視圖將目前最為現代化的瀏覽器提供的功能與行動裝置的體驗優勢相結合。

當你的網站實現了PWA,那就代表了

  1. 用户可以添加你的博客到電腦╱手機的桌面,以原生應用般的方式瀏覽你的博客
  2. 用户可以更快速地瀏覽你的博客
  3. 用户可以離線瀏覽你的博客

Hexo已經有很多插件可以實現PWA,下面是另一種實現方法,需要有Gulp就行。這種方法也可實現彈窗提醒用户刷新網站(當網站有更新時)

image-20200528222826867

此方法是使用 Service Worker。我們使用 Workbox 這個工具生成 sw.js 以快速實現 Service Worker ,並實現頁面的預緩存和頁面更新後的提醒功能。

開啟設置和配置manifest.json

主題配置文件中中開啟 pwa 選項

1
2
3
4
5
6
7
8
pwa:
enable: true
manifest: /img/pwa/manifest.json
theme_color: "#fff"
apple_touch_icon: /img/pwa/apple-touch-icon.png
favicon_32_32: /img/pwa/32.png
favicon_16_16: /img/pwa/16.png
mask_icon: /img/pwa/safari-pinned-tab.svg

在Hexo的source目錄中創建manifest.json文件。

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
{
"name": "string",
"short_name": "Junzhou",
"theme_color": "#49b1f5",
"background_color": "#49b1f5",
"display": "standalone",
"scope": "/",
"start_url": "/",
"icons": [
{
"src": "images/pwaicons/36.png",
"sizes": "36x36",
"type": "image/png"
},
{
"src": "images/pwaicons/48.png",
"sizes": "48x48",
"type": "image/png"
},
{
"src": "images/pwaicons/72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "images/pwaicons/96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "images/pwaicons/144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "images/pwaicons/192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "images/pwaicons/512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"splash_pages": null
}

你也可以通過 Web App Manifest快速創建manifest.json。(Web App Manifest 要求至少包含一個 512*512 像素的圖標)

安裝插件

在命令行中輸入安裝插件

1
npm install workbox-build gulp --save-dev

創建gulpfile.js 文件

在Hexo的根目錄,創建一個 gulpfile.js文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 文件

在Hexo的根目錄,創建一個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
const 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();

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 修改為你博客的名字(英文),如果你想用其它緩存策略,請自行查看相關文檔

添加js進主題

在主題配置文件中,添加需要的css和js

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 .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>'

上面的代碼是壓縮過的,具體相關代碼如下,可供理解。

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
<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>
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
<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>

運行

在你運行hexo g後,記得要運行gulp這樣才會生效


以下是結合了上面提到的Gulp壓縮和PWA的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
const gulp = require('gulp')
const cleanCSS = require('gulp-clean-css')
const htmlmin = require('gulp-html-minifier-terser')
const htmlclean = require('gulp-htmlclean')
const imagemin = require('gulp-imagemin')
const workbox = require("workbox-build");
// gulp-tester (如果使用 gulp-tester,把下面的//去掉)
// const terser = require('gulp-terser');

// babel
const uglify = require('gulp-uglify');
const babel = require('gulp-babel');

//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}"
],
modifyURLPrefix: {
"": "./"
}
});
});

//minify js babel
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 (如果使用 gulp-tester,把下面前面的//去掉)
// 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'
)));

Icon

Butterfly主題內置了Font Awesome V5 圖標,目前已更新到 5.13.0,總共有1,588個免費圖標。由於是國外的圖標網站,對於國內的一些網站Icon並不支持。如有需要,你可以引入其它的圖標服務商。

iconfont

國內最出名的莫過於iconfont,功能很強大且圖標內容很豐富的矢量圖標庫。很多Font Awesome不支持的圖標都可以在這裏找到。同時,iconfont支持選擇需要的圖標生成css鏈接,減少不必要的CSS加載。

註冊賬號

打開iconfont的網站,點擊導航欄的人像圖標,會跳出註冊界面,按要求註冊賬號。

Snipaste_2020-05-28_21-12-01

添加圖標入庫

選擇自己需要的圖標,把鼠標移到圖標上,會顯示三個按鈕(依次是添加入庫、收藏和下載),而我們需要的是把圖標添加入庫

image-20200528205401440

添加入庫後,你可以看到網站右上角購物車圖標顯示了1字,代表圖標已經添加入庫,點擊購物車圖標,會彈出側邊欄顯示詳情。

image-20200528205925258

image-20200528210120442

已選擇的圖標會顯示在上面,你可以重複上面的操作,把需要的圖標添加入庫,然後點擊添加到項目

接下來會要求選擇項目名稱,沒有的自己創建一個。

image-20200528211624459

生成CSS鏈接

在添加到項目之後,會跳到項目的詳情界面。點擊Font class,然後再點擊暫無代碼,點擊生成文字。網站會自動生成CSS鏈接,我們只需要複製鏈接就行。

image-20200528212301786

添加鏈接進主題配置文件

打開主題配置文件,找到inject配置,按要求把鏈接填入

image-20200528212440743

在我們需要使用的地方填入icon,例如Menu,圖片使用格式為iconfont icon名字

image-20200528213151304

運行Butterfly之後,你就可以看到menu的圖標生效了

image-20200528213346338

其他添加方法

除了通過引入CSS鏈接使用圖標,iconfont也支持通過其它方法使用圖標,具體可查看iconfont官方使用文檔

其它圖標提供商

除了iconfont,還有RemixIconFlaticon等等提供商,很多圖標可以選擇,具體使用方法請參考各自的文檔。

圖片壓縮

Butterfly主題需要使用到很多圖片。如果圖片太大,會嚴重拖慢網站的加載速度。

圖片壓縮能夠有效的緩解這個問題。

除了通過gulp-imagemin來壓縮圖片,還可以通過在綫壓縮網站和軟件來進行壓縮。以下兩款是我自己正在使用的工具。網上有很多這樣的工具,挑選一款適合自己的就行。

  • tinypng

    一個在綫壓縮的網站。壓縮後的圖片也保留了很高的質量,在知乎上很多人推薦,不過免費版有限制。

    image-20200526173511503

  • caesium

    開源軟件,支持Windows和macOS。可以批量壓縮軟件,無限制。

    image-20200526173316278

  • imgbot

imgbot 是一款 Github 插件。

安裝後,你上傳圖片到 Github 去,imgbot 會自動壓縮圖片並推送 PR,我們只需要合併 PR 就行

你可以配置 imgbot 的偵測方法、壓縮方法(有損/無損),具體可以查看插件的文檔

image-20200830231742951

插件推薦

參考

利用 Workbox 實現博客的 PWA

漸進式網絡應用程式

✨ Butterfly 安裝文檔(七) 更新日誌