请注意:本教程基于hexo-swpp@2.8+编写,不适用于 V3,V3 文档参考下面的链接:

请注意:如果版本号a.b.c中的ab发生变化,请务必重新阅读一遍文档!!!

请务必使用最新的稳定版本!旧版本很可能包含某些功能漏洞!!!

若版本号后方带有beta.*字样,请勿使用!

更新日志

  每行最后的方括号中的值表示该条更新所在的具体版本号,括号中的值为快速跳转链接,若链接以#开头则是 github issue 链接。

2.8+

功能性更新:

  • 支持通过ejectValues向 sw 插入值(快速跳转
  • 允许修改 rules 文件的名称(不建议修改)
  • rules 文件的拓展名可以为 cjs 或 js 两者其一

BUG 修复:

  • DOM 插入onsuccess时错误的执行了代码【2.8.4】
  • 缓存的 key 协议名称后缺少冒号【2.8.7】
  • ServiceWorker 中匹配 HTML 的正则表达式存在缺陷【2.8.8】
  • 生成 ServiceWorker 时报错【2.8.9】
  • 当 eject 导出的字符串中包含单引号时无法正确导出【2.8.10】
  • 当 escape 设置为 0 时逃生门未禁用【2.8.11】(严重漏洞!

细微功能调整:

  • 缓存匹配时统一 URL 风格(快速跳转)【2.8.5】
  • cacheList 中支持通过search区分不同的 URL【2.8.6】
  • 缓存的 key 不再区分 http 与 https 协议【2.8.6】
  • 遇到加载错误将直接中断 hexo 构建而非跳过 swpp 的执行【2.8.8】

2.7+

功能性更新:

  • 将配置项从配置文件迁移到 sw-rules.js 中(快速跳转

细微功能调整:

  • 不再对分类进行排序(#1)【2.7.1】
  • 优化错误输出【2.7.2】

2.6+

功能新更新:

  • sw 激活后直接生效,无需刷新页面
  • 支持从主题目录下加载 sw-rules.js(快速跳转
  • 修改代码插入方式(快速跳转

BUG 修复:

  • sw 不响应 http 请求【2.6.2】
  • 向 sw 复制 sw-rules.js 中的代码时格式错误【2.6.2】

2.5+

功能性更新:

BUG 修复:

  • 构建时报错【2.5.1】
  • 当 css 中存在任意属性的值包含url且没有后跟()时构建报错【2.5.2】

细微功能调整:

  • 不再相应非 http 请求(比如浏览器拓展)【2.5.3】
  • 支持缓存 301、302、307、308 重定向的相应【2.5.3】

older

之前没有写更新日志,省略不写。

介绍

  该插件用于在 hexo 中自动构建可用的 ServiceWorker,插件内自带了一个缺省的 sw.js,也支持使用自定义的 sw。

  本插件的特性:

  • 本地缓存(增量更新)
  • Request 篡改
  • CDN 竞速
  • 备用 URL
  • 208 阻塞响应
  • 兼容 MoOx-Pjax
  • 功能强大,高度自由

  我的另一篇博文已经介绍了这个 SW 的工作原理,具体介绍不再赘述,详情见:

如果你的网站使用了 CDN 且启用了 CDN 端缓存,请务必将 CDN 缓存时间调整至最大值,然后每次更新网页内容后手动刷新 CDN 缓存。

因为本插件的更新方案要求update.json更新时,其它所有需要更新的资源均已更新,否则客户端拉取时会误以为拉取到了最新的内容,从而导致部分资源“错过”更新。

简而言之,就是update.json必须与需要缓存的资源共享同样的 CDN 缓存周期。但是目前市面上我知道的 CDN 无法做到这一点,所以只能从下列选项中二选一

  • 把所有资源的 CDN 缓存时间拉满,每次更新网站时刷新 CDN 缓存
  • CDN 不缓存所有需要在客户端缓存的资源

Netlify 构建后自动刷新 CDN 缓存的教程见:《全自动博客部署方案》

请务必注意 CDN 缓存的问题!!!

  详细的更新日志见:GitHub Commits

如果文档中有不懂的地方,记得在评论区中提出你的问题,不要怕问题太简单哟~~

教程

  首先需要安装插件(参数使用--save--save-dev均可):

1
npm install hexo-swpp --save

  然后添加配置项(在 hexo 配置或主题配置中添加均可):

1
2
3
4
5
swpp:
enable: true
# rules 文件的名称(缺省值为 'sw-rules')
# 没有需求不建议更改,当主题和用户配置项不同时会导致主题和插件无法正常配合工作
# rules:

  接着还需要在博客根目录创建一个 JS 文件——sw-rules.cjs(拓展名为 cjs 或 js 均可,若两个不同拓展名的文件同时存在优先使用 cjs,主题开发者可在主题根目录下创建该文件)。

sw-rules.cjs必须要创建,且其中必须包含configcacheList,就算对象内容都是空的也要写上,不然无法构建!

配置项

  config是插件的配置项,插件的所有功能的配置均在该项中进行修改,以下是插件支持的所有配置(给出值的为插件的缺省配置,必填项和特殊情况在注释中说明):

温馨提示:不要被这么长的配置吓到,其中绝大多数配置都是非必填项,看不懂的功能不启用即可。

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
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
module.exports.config = {
/**
* 与 ServiceWorker 有关的配置项
* 若想禁止插件自动生成 sw,此项填 false 即可
* @type {?Object|boolean}
*/

serviceWorker: {
/**
* 逃生门
* @type number
* @see https://kmar.top/posts/73014407/#6c7c33f0
*/

escape: 0,
/**
* 缓存库名称
* 发布网站后 **切勿修改** 该配置项!
* @type string
*/

cacheName: 'kmarBlogCache',
/**
* 是否启用调试,启用后会在 sw 中插入一些辅助调试的代码,不建议开启
* @type boolean
*/

debug: false
},
/**
* 与 SW 注册有关的配置项
* 若想禁止插件向 html 中插入注册代码,此项填 false 即可
* @type {Object|boolean}
*/

register: {
/**
* sw 注册成功时的动作
* @type ?VoidFunction
* */

onsuccess: undefined,
/**
* sw 注册失败时的动作
* ~若没有禁用 register,则该项为必填项,该项没有缺省值~
* @type ?VoidFunction
*/

onerror: undefined,
/**
* 生成注册 SW 的 HTML 代码片段
* @param root {string} 网页根目录的 URL
* @param hexoConfig {Object} Hexo 配置项
* @param pluginConfig {Object} SW 配置项
* @return {string} 一个 HTML 标签的字符串形式
*/

builder: (root, hexoConfig, pluginConfig) => {
const {onerror, onsuccess} = pluginConfig.register
return `<script>
(() => {
const sw = navigator.serviceWorker
const error =
${onerror.toString()}
if (!sw?.register('
${new URL(root).pathname}sw.js')
${onsuccess ? '?.then(' + onsuccess + ')' : ''}
?.catch(error)
) error()
})()
</script>
`

}
},
/**
* 与 DOM 端有关的配置
* 若想禁止插件自动生成 DOM 端 JS,此项填 false 即可
* @type {Object|boolean}
*/

dom: {
/**
* 缓存更新成功后触发的操作
* @type VoidFunction
*/

onsuccess: () => {}
},
/**
* 与插件生成的版本文件相关的配置项
* 该功能目前无法关闭
*/

json: {
/**
* 更新缓存时允许更新的最大 HTML 页面数量,需要更新的 HTML 文件数量超过这个值后会清除所有 HTML 缓存
* @type number
*/

maxHtml: 15,
/**
* 版本文件(update.json)字符数量限制,插件将保证版本文件的字符数量不超过该值
* @type number
*/

charLimit: 1024,
/**
* 文件缓存匹配采取精确模式
* 关闭时更新缓存时仅匹配文件名称,如 https://kmar.top/simple/a/index.html 仅匹配 /a/index.html
* 开启后更新缓存时将会匹配完整名称,如 https://kmar.top/simple/a/index.html 将匹配 /simple/a/index.html
* 两种方式各有优劣,开启后会增加 update.json 的空间占用,但会提升精确度
* 如果网站内没有多级目录结构,就可以放心大胆的关闭了
* key 值为文件拓展名,default 用于指代所有未列出的拓展名以及没有拓展名的文件
*/

precisionMode: {
default: false
},
/**
* 是否合并指定项目
* 例如当 tags 为 true 时(假设标签目录为 https://kmar.top/tags/...)
* 如果标签页存在更新,则直接匹配 https://kmar.top/tags/ 目录下的所有文件
* **推荐将此项开启**
*/

merge: {
index: true,
tags: true,
archives: true,
categories: true,
/**
* 这里填写目录名称列表(不带两边的斜杠)
* @type string[]
*/

custom: []
},
/**
* 生成版本文件时忽略的文件
* 注:匹配的时候不附带域名,只有 pathname,匹配的内容一定是博客本地的文件
* @type RegExp[]
*/

exclude: [
/sw\.js$/
]
},
/**
* 外部文件更新监听
* 该项缺省值为 false,若想允许插件监听外部文件的更新至少将值改为 `{}`
* @type {Object|boolean}
* @see https://kmar.top/posts/73014407/#c60b3060
*/

external: {
/**
* 拉取网络文件时的超时时间
* @type number
*/

timeout: 1500,
/**
* 匹配 JS 代码中的 URL
* 注意:字符串中的内容将被直接嵌入到正则表达式中,括号等特殊字符前需添加反斜杠,不允许使用括号
* 该项的缺省值为 `[]`,下方的值仅用于标明填写格式
* @see https://kmar.top/posts/73014407/#c60b3060
*/

js: [
{
head: 'getScript\(',
tail: '\)'
}
],
/**
* 某些外链只要 URL 不变其内容就一定不会变
* 可以通过正则表达式排除这些外链的文件内容监控,加快构建速度
* 注意:当某一个文件被跳过拉取后,这个文件中包含的 URL 也会被跳过
* @type RegExp[]
*/

skip: [],
/**
* 在构建过程中替换部分链接,该替换结果不会影响文件内容
* 该设置项是为了应对构建服务器在国外,但是网站内部分缓存资源无法在国外访问导致拉取时超时的问题
* 该项的缺省值为 `[]`,下方的值仅用于标明填写格式
* @type Object[]
* @see https://kmar.top/posts/73014407/#4ea71e00
*/

replace: [
{
source: ['source0', 'source1'],
dist: 'dist'
}
]
},
/**
* 对 Hexo 中的变量进行排序
* 默认插件对 posts、tags、pages 三个变量进行排序
* 排序规则为优先按照字符串长度排序,若长度一致按照字典序排序
*
* 格式为 `name: value`
* value 的可能值为:字符串、非负整数、false
* 假定 Array<obj> 为要被排序的数据
* 当 value 为字符串和非负整数时,插件会以 `obj[value]` 的格式读取关键字
* 当 value 为 false 时,插件会直接以 `obj` 为关键字
* 注意:关键字必须为含有 length 属性且支持 < 操作符的类型
* 插件内置的 posts 规则如果用上面的格式写应该为:
* posts: 'title'
* 插件支持使用配置项覆盖插件内置规则
*
* 该项缺省值为 `{}`,下方的值仅用于标明填写格式
*/

sort: {
keywords: false
}
}
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
module.exports.config = {
serviceWorker: {
escape: 0,
cacheName: 'kmarBlogCache',
debug: false
},
register: {
onsuccess: undefined,
onerror: undefined,
builder: (root, hexoConfig, pluginConfig) => {
const {onerror, onsuccess} = pluginConfig.register
return `<script>
(() => {
const sw = navigator.serviceWorker
const error =
${onerror.toString()}
if (!sw?.register('
${new URL(root).pathname}sw.js')
${onsuccess ? '?.then(' + onsuccess + ')' : ''}
?.catch(error)
) error()
})()
</script>
`

}
},
dom: {
onsuccess: () => {}
},
json: {
maxHtml: 15,
charLimit: 1024,
precisionMode: {
default: false
},
merge: {
index: true,
tags: true,
archives: true,
categories: true,
custom: []
},
exclude: [ /sw\.js$/ ]
},
external: {
timeout: 1500,
js: [],
skip: [],
replace: []
},
sort: { }
}

缓存规则

  cacheList用于确定运行时哪些 URL 应当被缓存哪些 URL 不缓存,此项的代码会被复制到 sw.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
/** 缓存列表 */
module.exports.cacheList = {
// 这个 [simple] 就是规则的名称,该对象下可以包含多个规则,名称不影响缓存匹配
// 缓存匹配时按声明顺序进行匹配
simple: {
/**
* 符合该规则的缓存在进行全局清理时是否清除
* 如果你无法确定是否需要声明为 false 的话写 true 即可
* @type boolean
*/

clean: true,
/**
* 标记当前规则是否依据 search(URL 中问号及问号之后的部分)的不同而做出不同的响应
* 该项可以不填,不需要依据参数变化的 URL 均建议不填该项
* 插件**不易监测**带参数的 URL 的更新,需要更新缓存时很可能需要手动刷新缓存,该项慎填
* @type {boolean|undefined}
*/

search: false,
/**
* 匹配缓存,第二个参数可以不写
* @param url {URL} 链接的 URL 对象(对象包括 hash 和 search,但不要使用 hash,search 为 false 时不要使用 false)
* @param $eject {Object} 用于访问通过 [ejectValues] 函数插入的变量
* @return boolean
* @see https://kmar.top/posts/73014407/#eee25160
*/

match: (url, $eject) => url.host === 'kmar.top' && url.pathname.match(/\.(woff2|woff|ttf|cur)$/)
}
}

  注意!所有匹配缓存的 URL 都将进行一次格式化,所有以/index.html结尾的 URL 都将被替换为/,比如:

  • https://kmar.top/index.html -> https://kmar.top/
  • /abc/def/index.html -> /abc/def/
  • /abc/def.html -> abc/def.html

  如果你只是想快速预览一下 sw,可以使用如下的代码(未列出的项目均可不填):

1
2
3
4
5
6
7
module.exports.cacheList = {}

module.exports.config = {
register: {
onerror: () => console.error("ServiceWorker 注册失败,可能是您的浏览器不支持该功能。")
}
}

  插件会在生成网站的过程中自动生成 sw.js 和 sw-dom.js 并插入注册代码,所以本地预览时支持对 sw 和 sw-dom.js 进行调试。而 json 文件则需要通过hexo swpp指令手动生成,在执行hexo g && hexo swpp后启用本地预览时 sw.js 可以访问到update.json,如果没有提前生成发布目录并运行指令,本地预览时浏览器后台报404错误属于正常现象,无需担心。(一般不建议在本地中启用本地文件的缓存,不方便本地调试。)

  请在发布网站及压缩文件前执行hexo swpp,否则可能会因为压缩导致文件md5频繁变动。如果你的博客需要使用gulp一类的脚本替换一部分链接,请在替换链接后执行hexo swpp,在替换之前执行只能监听到替换前的链接。

  请注意:在缓存数据时不会处理hash和参数,所以如果请求内容会根据 URL 参数不同而不同的话请勿缓存,否则会导致拉取到错误的内容!!!

主题支持

  插件支持主题自带 sw-rules.js 文件,将该文件放置在主题根目录下一并上传即可。插件运行时会尝试在/themes/[name]/sw-rules.js/node_modules/hexo-theme-[name]/sw-rules.js两个位置查找 js。

  主题中自带 sw-rules.js 后,用户仍然可以在博客根目录创建 js 文件。用户 js 中声明的内容的优先级高于主题 js,若两者中包含同名的 export,将使用用户 js 的内容;若主题 js 中包含了指定内容但用户 js 中没有,将使用主题 js 中的内容。

逃生门

  该配置我们可以称之为“逃生门”。这个配置后应填写一个整数(缺省为 0),在 sw 检测到用户没有加载过这个版本时,就会强制刷新用户所有缓存。这个选项主要是提供给想要自定义 DOM 端 JS 的用户使用的,因为缓存的更新是需要 DOM 端发送更新请求到 SW 端的,所以有些时候因为 DOM 端代码有误会导致永远无法触发缓存更新,这时候就可以通过逃生门强制更新缓存。

  填写逃生门时,随意填写一个未填写过的正整数即可,不填或填0表示不启用逃生门。

Request 篡改

  如果你需要在发起网络请求前修改请求头的信息,直接在sw-rules.js中添加modifyRequest函数即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 修改 Request
* @param request {Request} 原始 Request
* @param $eject {Object} 用于访问通过 [ejectValues] 函数插入的变量
* @return {Request} 修改后的 Request,不修改的话返回 null 或不返回数据
* @see https://kmar.top/posts/73014407/#eee25160
*/

module.exports.modifyRequest = (request, $eject) => {
// 下面是一个示例
// 如果不需要该功能,可直接删除该函数
const url = request.url
const source = '/gh/EmptyDreams/resources/icon'
if (url.includes(source)) {
return new Request(url.replace(source, '/gh/EmptyDreams/twikoo-emoji'), request)
}
}

CDN 竞速

  CDN 竞速开启后会同时向多个 URL 发起网络请求,使用速度最快的一个 URL 返回的数据。开启该功能后,将增加客户端的网络和 CPU 压力,请酌情开启。

  想要启用 CDN 竞速,需要在sw-rules.js中添加如下语句:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 获取一个 URL 对应的多个 CDN 的 URL
*
* 竞速时除 URL 外的所有参数均保持一致
*
* @param srcUrl 原始 URL
* @return {string[]} URL数组,需要包含原始 URL,不包含则表示去除原始 URL 的访问,返回 null 或不返回数据表示该 URL 不启用竞速
*/

module.exports.getCdnList = srcUrl => {
if (srcUrl.startsWith('https://npm.elemecdn.com')) {
const url = new URL(srcUrl)
return [
srcUrl,
`https://cdn.jsdelivr.net/npm` + url.pathname,
`https://cdn1.tianli0.top/npm` + url.pathname,
`https://fastly.jsdelivr.net/npm` + url.pathname
]
}
}

  这个函数体只是给出了一个样例,读者可自行编写。

备用 URL

  备用 URL 和 CDN 竞速有异曲同工之妙,不过备用 URL 是在上一个链接下载时长超过指定时间后再发起对下一个 URL 的访问。在发起新的访问时不会中断对上一个 URL 的访问,只有在某一个访问成功拉取内容后才会中断对其它 URL 的访问。

  如果需要开启此功能,在 sw-rules.js 中添加getSpareUrls声明即可。开启时,建议将速度最快、受众群体最大的 URL 放在开头,否则会严重影响加载速度。

  下面给出一个代码样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 获取一个 URL 对应的备用 URL 列表,访问顺序按列表顺序,所有 URL 访问时参数一致
* @param srcUrl {string} 原始 URL
* @return {{list: string[], timeout: number}} 返回 null 或不返回表示对该 URL 不启用该功能。timeout 为超时时间(ms),list 为 URL 列表,列表不包含原始 URL 表示去除原始访问
*/

module.exports.getSpareUrls = srcUrl => {
if (srcUrl.startsWith('https://npm.elemecdn.com')) {
return {
timeout: 3000,
list: [
srcUrl,
`https://cdn.jsdelivr.net/npm${new URL(srcUrl).pathname}`
]
}
}
}

阻塞响应

  有时候,我们可能希望屏蔽一些 URL 的访问,这时候可以通过直接返回 208 来阻塞这些 URL。

  注意:当 URL 被屏蔽后,拉取文件的过程中并不会抛出异常,如果拉取文件的代码没有做错误处理,在处理文件内容时可能会报错。

  在sw-rules.js中添加如下代码即可启用阻塞响应功能:

1
2
3
4
5
6
7
8
/**
* 判断是否阻塞响应
* @param url {URL} URL
* @return {boolean} true 表示阻塞,false 表示不阻塞
*/

modelus.exports.blockRequest = url => {
// ...
}

外部文件监听

  插件支持监听一下文件内引入的外部文件的更新:

  • html 文档中使用scriptlink标签引入的 URL
  • css 文件及 html 中内嵌的 css 中使用url(...)语法引入的 URL(排除注释)
  • js 文件中符合预定要求的 URL

  当插件尝试监听 URL 时,如果 URL 没有附带通讯协议,会默认使用http协议而非https协议。

  html 和 css 内 URL 的监听就不再多说,我们重点说 js 文件的监听。从上面配置文件的示例可以看到监听 js 内 URL 的格式如下:

1
2
3
4
5
js:
- head: #...
tail: #...
- head: #...
tail: #...

  headtail项的值都为字符串,字符串的内容会直接内嵌到正则表达式中,所以如果你使用了一些特殊字符,记得使用反斜线,否则无法正常匹配。

  每一对headtail都会被拼接为如下格式的正则表达式:

1
new RegExp(`${head}(['"\`])(.*?)\\1${tail}`, 'mg')

  该正则表达式的作用是匹配被headtail包裹起来的字符串(不忽略注释),同时会向head后和tail前添加一个引号(通用匹配三种引号)

  注意,插件不支持拼接字符串,同时如果你使用模板字符串,也不支持使用${},如果被检查到字符串中包含三种引号中任意一种或多种或者$符号,都会被视为非法 URL 跳过监听。

  遇到非法 URL 时会自动跳过,不用担心正则表达式匹配到不是 URL 的内容。如果你是 butterfly 主题的用户,可以使用如下配置来捕获使用getScript()函数引入的文件:

1
2
3
js:
- head: 'getScript\('
tail: `\)'

replace

  如果部署服务器在国外,但是网站中部分链接只有国内才能访问,可以选择取消对这些 URL 的监控或者使用 replace 配置项将 URL 改为国外可访问的 URL。

  替换时会把source中写出的字符串替换为dist中的内容,且一次替换完成后不会终止,会一直完成整个列表的匹配。

  替换 URL 的代码如下:

1
2
3
4
5
6
7
8
9
10
function replaceDevRequest(url) {
for (let value of external.replace) {
for (let source of value.source) {
if (url.match(source)) {
url = url.replace(source, value.dist)
}
}
}
return url
}

手动更新缓存

  插件捕获外部文件更新的能力有限,如果部分文件无法使用插件自动捕获更新,则需要在更新时手动添加。

  当需要手动向版本文件添加信息时在 hexo 根目录中创建update.json即可,格式如下:

1
2
3
4
5
6
7
{
"flag": true,
"global": false,
"all": false,
"change": [
]
}

  其中:

  • flag 用于标记文件版本,只有当前值与上一次的值不同时该文件内容才有效
  • global用于标记常驻缓存是否发生更新
  • all用于标记是否清除所有缓存(如果global没变就不会清除常驻缓存)
  • change中填写你要刷新的缓存,具体格式下面再描述,如果all标记为了truechange写不写都没有区别

  change的格式为{"flag": [name], value: [args]},其中flag为规则名称,value为匹配值,value可以为数组也可以为单个值。

  目前支持以下几种规则:

flagvalue描述示例
html清除所有 html 缓存{"flag": "html"}
file文件名称(带拓展名)清除指定文件的缓存(如果不同目录下有重名文件并且想要准确清理的话需要带上部分路径名){"flag": "file", "value": ["main.js", "/css/index.css"]}
str任意字符串清除所有完整路径中包含指定字符串的缓存{"flag": "str", "value": "fonts"}
reg正则表达式(不带两边的斜杠,不区分大小写)清除所有完整路径与指定正则表达式存在匹配的缓存{"flag": "reg", "value": "\.js$"}

插入代码

  插件自 2.6.0 开始不再将 sw-rules.js 中的所有内容复制到 sw 中,而是以白名单的形式,只有在白名单中的内容才会复制到 sw 中。

  插件内置了一个白名单,这个白名单中包括了插件自身支持的几个基本的函数和对象,如果想要添加白名单,可以通过如下接口实现:

1
2
/** @type {string[]} */
module.exports.external = []

  字符串数组的值就是要复制的内容的 key。(注意:要导出的内容必须使用module.exports声明,否则将无法导出!)

  同时还需要注意,不要在 sw-rules.js 的全局域中写只能在 sw 端执行的代码,因为在构建期间 node 也会执行这些代码,如果需要向 sw 添加一些自动执行的代码,可以使用afterJoinafterTheme接口,使用方法如下:

1
2
3
4
5
6
7
module.exports.afterJoin = () => {
// do something...
}

module.exports.afterTheme = () => {
// do something...
}

  插入到 sw 后会被转换为如下形式:

1
2
3
4
5
6
7
8
9
// afterJoin
(() => {
// do something...
})()

// afterTheme
(() => {
// do something...
})()

  afterJoin主题和用户都可以使用,若主题和用户同时声明了该函数,则用户声明将完全覆盖主题声明。afterTheme是用户端独享的接口,主题禁止声明该接口。

  如果想要插入一些需要根据 hexo 或 swpp 配置变化的值可以使用如下的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 获取要插入到 sw 中的变量或常量
* @param hexo hexo 对象
* @param rules 合并后的 sw-rules 对象
* @return {Object} 要插入的键值对
*/

module.exports.ejectValues = (hexo, rules) => {
return {
/** 这里的 key 为变量名,变量名仅允许包含英文字母和阿拉伯数字 */
domain: {
// 值的前缀,一般填 'const 或 let'
prefix: 'const',
// 要插入的值,只支持 string、number、boolean 和 bigint
value: new URL(hexo.config.url).host
}
}
}

  以我的网站为例,上面的代码将在 sw 顶部插入如下代码:

1
const ejectDomain = 'kmar.top'

如果你在cacheListmodifyRequest中使用了$eject,请务必注意以下事项:

  • 第二个参数的变量名必须为$eject不得修改!!!
  • 必须使用箭头函数,不得使用function关键字声明cacheListmodifyRequest!!!

运行机制及避坑指南

  插件内置的 SW 实现增量更新的方式是在网站内生成了一个描述各个版本差异的文件(下称作“版本文件”),更新时通过读取这个文件来计算需要更新哪些内容,插件的主要工作就是在生成网站时自动创建这个版本文件。

  如果你使用1.3.0之前的版本(请务必更新到最新版本),需要注意下面这种情况:

  假如我们缓存了https://[npm]/hexo-swpp@1.0.2/index.js,然后在本站中将链接的版本号替换为了1.1.0,这时候插件是不能检测到需要删除index.js这个文件的缓存的。客户端访问网页时虽然可以正确地访问到1.1.0index.js,但是1.0.2版本的 JS 仍然存在于缓存之中,但是永远不会被使用,这时候需要手动输入更新,告诉插件这个缓存需要删除。

  当然新版本该问题也没有彻底解决,只是很大程度上的缓解了该问题,如果你是用插件无法监听到的方式引入的文件,仍然需要手动告知插件更新缓存。

  那么插件是如何实现监听文件更新的呢?答案就是通过计算并存储每个文件的md5

  插件会扫描配置文件中设置的输出目录(一般为public)下的所有文件以及能够扫描到的你引入的文件,并从中找出需要缓存的文件,计算并存储它们的md5值,然后存储在public/cacheList.json当中。

  那么插件是如何获取上一次生成的结果呢?为了兼容netlifygithub action等非本地的自动构建方案,插件没有在本地存储cacheList.json,而是直接输出到public目录中。在下一次构建网站时会使用node-fetch从网络拉取cacheList.json。所以一定要保证在构建网站时网站可以被访问(请求的时候会仿造refererUser-Agent),否则拉取文件时会失败。

  第一次构建网站的时候拉取文件的时候会出现404警告,这是正常现象直接无视即可,但是第二次构建如果仍然出现404则需要检查是否是哪里出了问题。

  这里还有一个优化生成性能的小技巧,cacheList.json中会存储所有满足缓存条件的文件信息,但是可能并不是所有文件都会被用户访问和缓存。比如sw.js可能就满足你的缓存规则,但是sw.js本身并不会走 sw,在exclude中排除这些文件/文件夹可以优化生成性能(对用户无影响)。

  同时注意maxHtml不宜调的过大,charLimit不宜过小,否则有可能会导致当你修改的 html 数量很多时一条更新信息就超过了charLimit的限制,直接清掉了全站缓存,得不偿失。

  反过来,charLimit也不宜过大,否则当版本号积累后会使update.json文件体积过大,从而拖慢客户端更新速度,还会加大服务器负担。

  另外还需要注意,虽然插件内对 hexo 内置的全局变量进行了排序,但是部分主题会在页面内添加一些随机的数据(不修改网页内容连续构建得出不同的结果),这些数据会导致插件认为该页面需要刷新缓存。

  此时需要用户手动解决这个问题,比如butterfly主题会在页面内插入时间戳来实现过期文章提示的功能,我们可以选择修改主题源代码,将文章过期提示的功能移除(深度修改也可以),或者将插入时间戳的页面放入exclude列表中,然后需要更新时手动在update.json文件中写出更新条目。

QA

  • Q:逃生门必须填吗?
    A:该选项不是必填项。
      如果你想要自定义 DOM-JS,则在 DOM-JS 出现不可逆的故障时需要填写逃生门;
      逃生门留空或不设置默认为 0。
  • Q:如何控制缓存大小?
    A:当前 SW 不支持通过代码控制缓存大小,需要自行通过缓存规则控制缓存规模。
  • Q:插件执行过程中 NodeJs 提示语法错误是怎么回事?
    A:版本过低,升级 NodeJs 版本即可,推荐升级到 STL 版本。
  • Q:如何在线上查看 sw 的工作情况?
    A:开发者工具的“应用程序”和“网络”都可以查看相关信息。

创作不易,扫描下方打赏二维码支持一下吧ヾ(≧▽≦*)o