封面

更新日志

2.3+

新增功能:

  • 允许部分 URL 使用 cors 而部分 URL 不使用 快速跳转
  • 允许部分 URL 开启请求合并而部分 URL 不开启 快速跳转

漏洞修复:

  • 修复部分请求在进行 cors 检查时报错的问题 [2.3.0]
  • 修复部分请求 cors 和其余功能冲突的问题 [2.3.1]
  • 修复缺省 DOM JS 每次刷新都触发更新事件的问题 [2.3.2]
  • 修复触发逃生门后不同步版本号的问题 [2.3.2]
  • 修复缺省 DOM JS 连续刷新时没有更新版本但仍会触发更新事件的问题 [2.3.3]
  • 修复 URL 竞速在非缓存资源上不起作用的问题 [2.3.4]
  • 修复 URL 竞速不可利用外部构建好的列表的问题 [2.3.5]
  • 修复生成 SW 时字符串替换失败的问题 [2.3.7]
  • 修复配置合并时错误地忽略了0!vtrue的缺省项的问题 [2.3.8]
  • 修复配置项中dom.onsuccess值为空时生成的 DOM JS 文件包含无效代码的问题 [2.3.9]
  • 修复配置项中dom.onsuccess值为空时生成的 DOM JS 文件中变量缺失的问题 [2.3.11]

其它修改:

  • 版本号改为自动从package.json读取 [2.3.10]

2.2+

新增功能:

漏洞修复:

  • 修复没有声明skipRequest时前端报错的问题 [2.2.1]
  • 修复请求图像数据时可能出现跨域错误的问题 [2.2.2]

2.1+

新增功能:

  • 合并请求
  • 允许自定义版本文件拉取失败时的动作 [2.1.2]
  • 版本更新后向 DOM 端返回的信息添加old字段 [2.1.3]

漏洞修复:

  • 修复 URL 拉取失败时后台报两个错误的问题 [2.1.3]
  • 修复创建Response对象时报错的漏洞 [2.1.4]
  • 修改逃生门逻辑 [2.1.7]
  • 修复缺省 DOM JS 更新后不自动刷新的问题 [2.1.8]

2.0+

新增功能:

  • 添加Variant系列 API
  • 拉取外部文件支持队列等待 快速跳转
  • 支持自动构建 DOM 端 JS
  • 内置updateextraListenedUrls支持 快速跳转

漏洞修复:

  • 修复主题文件夹内地规则文件错误地覆盖了用户规则的问题 [2.0.0]
  • 修复没有处理 CSS 中以./开头的 URL 的问题 [2.0.0]
  • 修复执行 swpp 指令时报错崩溃的漏洞 [2.0.1]
  • 修复 eject 导出的变量名称错误的问题 [2.0.2]
  • 修复内置 DOM JS 更新事件不能正常触发的问题 [2.0.3]
  • 修复 SW 有时会报未知表达式 {flag: all}的问题 [2.0.4]
  • 修复版本文件中不记录update.flag的问题 [2.0.5]
  • 修复一系列前端缓存匹配错误的漏洞 [2.0.6]
  • 修复 escape 为 0 时逃生门未禁用的严重漏洞 [2.0.7]
  • 修复清除全部缓存时无法正常清除的严重漏洞 [2.0.8]
  • 修复刷新 HTML 缓存时报错的漏洞 [2.0.9]

其它修改:

  • 修改了部分函数的名称 [2.0.0]

1.2+

漏洞修复:

  • 修复 CSS 文件中字符串包含/**/时无法正常提取 URL 的问题 [1.2.1]
  • 修复解析 JS 文件时不能正确计算headtail规则字符串长度的问题 [1.2.1]
  • 修复处理 CSS 时死循环的漏洞 [1.2.3]

功能调整:

  • 处理 CSS 文件时不再解析文件内容,使用正则表达式提取 URL [1.2.0]
  • 导出的对象中添加version字段 [1.2.0]

其他修改:

  • 修正类型信息 [1.2.2]

1.1+

漏洞修复:

  • 修复备用 URL 工作异常的问题 [1.1.1]

其它修改:

  • 阻塞响应的状态码由 208 修改为 204 [1.1.2]
  • 当规则文件完成加载后,不再允许修改规则内容 [1.1.2]

介绍

  swpp-backends(以下简称 swpp)插件的功能是为网站生成一个高度可用的 ServiceWorker(以下简称 SW),为网站优化二次加载、提供离线体验、提高可靠性,并为此附带了一些其它的功能。

  swpp 的全拼为“Service Worker Plus Plus”(或“ServiceWorker++”),但是其与已有的插件并没有关系,插件中所有代码均为我个人开发,这一点请不要误解。

  swpp 生成的 SW 与其它插件的对比:

swpphexo-offline
本地缓存✔️✔️
缓存增量更新✔️
缓存过期时间1✔️
缓存大小限制2✔️
预缓存✔️
Request 篡改✔️
URL 竞速✔️
备用 URL✔️
204 阻塞响应✔️
逃生门✔️
请求合并✔️
跨平台✔️
高度自由✔️
更新非常频繁超过两年没有更新
  • ✔️:支持
  • ❌:不支持
  1. 因为有增量更新,所以没提供过期的实现,没必要
  2. 该功能用处不是很大暂不提供,一般情况下通过缓存规则手动控制缓存大小即可

  注:上面提到的跨平台是指跨越框架(比如 Hexo、WordPress 等)。

  目前支持的平台:

平台插件名文档作者
hexohexo-swppgithub空梦

  如果你为某一个平台做了适配,可以在 gh 上发布 issue 或者在该页面发布评论~

功能介绍

  文档不再介绍 sw,使用 swpp 前请确保您已经知道了 SW 的基本原理,以免在使用过程中遇到一些不必要的麻烦。(Service Worker API - MDN

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

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

简而言之,就是版本更新文件必须与需要缓存的资源共享同样的 CDN 缓存周期或者让版本更新文件延后更新缓存。但是目前市面上我知道的 CDN 无法做到这一点,所以只能从下列选项中二选一:

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

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

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

本地缓存

  本地缓存功能将在网站(sw 生效后)第一次加载需要缓存的资源时,将其永久的缓存到硬盘上,下一次对缓存过的资源发起访问时,将直接从硬盘中读取而非从网络下载。

  本地缓存对用户二次访问地体验有较大地提升(提升程度取决于缓存的资源的数量以及其它相关的指标),在旧客比较多时,还可以极大程度地降低服务器地负载。

  即使用户暂时没有网络连接,其仍然可以浏览网站中已经被缓存的资源,所以您可以将浏览网页所必需的资源缓存下来,这样用户就可以在没有网络的情况下获得良好的体验(不过这个用途目前似乎没有什么价值)。

  插件没有支持预缓存,因为预缓存容易拖延 SW 的注册时间,为了尽快地完成注册我没有添加预缓存的功能,但是用户可以在 DOM 端自己实现该功能(在 SW 注册成功后选择一个时机使用fetch或其它网络接口向你想缓存的内容发起请求即可)。

Request 篡改

  插件允许在真正发起网络请求前篡改 Request 中的各项数据(包括 url,但不能修改 referer 和 ua),可以用这项功能来替换掉一些需要修改但是不便于修改的链接。

  注意:Request 篡改只会在发起网络请求时修改请求内容,而不会修改 HTML、CSS 等文件的内容。

URL 竞速

  URL 竞速功能会在发起一个请求的时候同时向多个不同的 URL 发起请求,选择速度最快的一个作为最终响应。这些不同的 URL 需要用户自己使用代码生成,一般这些 URL 都会返回相同的数据。

  需要注意的是,开启 URL 竞速后由于会同时向多个 URL 发起请求,所以在没有缓存的时候会增大网络和 CPU 压力,尤其是使用流量访问网站的用户比较多时请谨慎开启该功能。

  请注意,“URL 竞速”与“备用 URL”功能相互冲突,仅允许启用一个。

备用 URL

  备用 URL 在发起网络请求时会尝试获取一个 URL 列表,然后向列表中第一个 URL 发起请求,如果这个请求失败或者超过限定时间,则同时向剩余所有的 URL 发起请求。

  备用 URL 的主要目的是在某个 CDN 无法访问后,有一个或多个备用的 URL 可以临时替代,网站不至于直接整体挂掉。或者是当站内存在链接是仅国内可用且网站主要目标群体是国内用户时,在国外访问网站不至于无法加载。

  需要注意的是,只要不是第一个 URL 就访问成功,那么网络请求一定会产生额外的延迟,所以务必把成功率最高、速度最快的请求放在首位,如果首位的 URL 无法访问了要及时替换,否则会影响用户体验。

204 阻塞响应

  这项功能是当网站内出现了一些不希望出现的网络请求或者想在特定条件下阻断指定请求时不发起网络请求,直接向 DOM 端返回空 body 的 204(No Content)响应。

增量更新

  插件通过扫描站内所有文件(仅限支持的类型:HTML、CSS、JS),从中提取出可能在前端被缓存的 URL,然后记录他们的 MD5 值并存储到一个文件中,这个文件需要连同网站文件一同发布到网络上。

  在下一次更新时,插件会将在本地生成的新的 MD5 表与线上的 MD5 表进行对比,以此确定哪些 URL 需要更新缓存,然后将结果输出到一个描述文件更新的 json 中。

  由于一些限制,插件可能无法提取到所有链接,如果有部分链接脱离了监控,在这部分链接内容更新后则需要用户手动刷新缓存。

逃生门

  由于缓存的更新需要 DOM 端发送一个请求到 SW 端,如果你自定义了一个 DOM 但是其无法工作并且你缓存了 JS 文件,那么此时将陷入一个死局:

  • 修复 JS 需要更新缓存
  • 更新缓存需要修复 JS

  为了解决这一死局,插件支持逃生门选项,当用户浏览器中存储的逃生门的值与您设置的不同时会强制刷新所有缓存,这样就能打破僵局,加载到正确的内容了。

请求合并

  有时我们在前端可能会同时对一个文件发起多个 GET 请求,此时这些请求虽然都会收到同样的内容,但是后续的请求仍然会向浏览器发起网络请求。

  swpp 支持将 URL(除网络协议和 hash 之外)相同的请求合并为一个,以此减少客户端网络压力,提升性能。

规则文件

  插件将从一个指定的 EJS 文件(以下称之为“规则文件”)中读取所有的配置项,本节将讲述该文件的构成。

观前提醒:

  我给出的代码中基本都写了臭长臭长的注释,这些注释是为了在文档中解释这些代码的用途、用法,请不要把这些臭长臭长的注释放到你的文件中。

  同时对于配置项,我为了解释所有配置项的功能、缺省值,列出了所有支持的配置项,对于你想保持缺省的选项,不要在你的文件中重复缺省值,直接删掉不写就行。

  不要让你的规则文件变得臭长臭长的!对于规则文件臭长臭长的求助我一律无视!

加载规则文件

  规则文件的文件名和路径是可以自定义的,我们可以通过下面的代码加载规则文件:

1
2
3
4
5
// 这个代码是给开发者演示的,非开发者不需要写这个代码

const swpp = require('swpp-backends')

swpp.loader.loadRules(root, fileName, selects)
  • root参数用于声明优先级最高的规则文件所在的目录(不包括文件名),插件将优先使用这个文件中导出的值。
  • fileName参数则用于声明规则文件的文件名(不包括拓展名),插件支持读取ejsjs两种拓展名,如果同一个目录下同时存在xxx.ejsxxx.js,则会使用前者。
  • selects参数用于声明一个备选的路径列表,插件会从这个列表中首个包含规则文件的路径中读取规则文件,并与root中的规则文件合并(如果存在的话)。

  如上所述,如果rootselects同时存在规则文件,插件会合并两者而非只有一个生效,合并的规则如下:

  1. 如果一个属性root有而selects没有,则root生效,反之亦然
  2. 如果一个属性rootselects两者同时存在且属性名为config,则进行深度拷贝(下文具体举例)
  3. 如果一个属性rootselects两者同时存在且属性名不为config,则root中的属性覆盖selects中的

  为了方便理解,我们给出一个实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
module.export.cacheRules = {
simple: {
clean: true,
match: url => /(\/|\.html)$/.test(url.pathname)
}
}

module.exports.blockRequest = url => {
return false
}

module.exports.custom = {
flag: {
type: true,
value: false
}
}

module.exports.config = {
serviceWorker: {
escape: 114514,
debug: false
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module.exports.cacheRules = {
css: {
clean: true,
match: url => url.pathname.endsWith('.css')
}
}

module.exports.blockRequest = url => true

module.exports.config = {
serviceWorker: {
debug: true
},
register: {
onerror: () => console.log(114514)
}
}
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
module.export.cacheRules = {
simple: {
clean: true,
match: url => /(\/|\.html)$/.test(url.pathname)
}
}

module.exports.blockRequest = url => {
return false
}

module.exports.custom = {
flag: {
type: true,
value: false
}
}

module.exports.config = {
serviceWorker: {
escape: 114514,
debug: false
},
register: {
onerror: () => console.log(114514)
}
}

配置项

  在规则文件中声明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
// ⚠ 警 告:该代码块中所有注释均是为了解释数据类型所写,请勿在部署的配置项中复制我的注释

// 这里的 import 就是我方便你可以在 IDE 中快速找到类型的声明位置
// 自己不要写到文件里面,没法执行
import {
ServiceWorkerConfig, RegisterConfig, DomConfig, VersionJsonConfig, ExternalMonitorConfig, SwppConfig
} from 'swpp-backends'

module.exports.config = {
/** @type {?ServiceWorkerConfig|boolean} */
serviceWorker: {
/** @type {number} */
escape: 0,
/** @type {string} */
cacheName: 'kmarBlogCache'
},
/** @type {?RegisterConfig|boolean} */
register: {
/** @type {?VoidFunction} */
onsuccess: undefined,
/** @type {VoidFunction} */
onerror: () => console.error('Service Worker 注册失败!可能是由于您的浏览器不支持该功能!'),
/**
* @param root {string} 网页根目录的 URL
* @param framework {Object} 框架对象
* @param pluginConfig {SwppConfig} swpp 配置项
* @return {string} 一个 HTML 标签的字符串形式
*/

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

}
},
/** @type {?DomConfig|boolean} */
dom: {
/** @type {?VoidFunction} */
onsuccess: undefined
},
/** @type {?VersionJsonConfig|boolean} */
json: {
/** @type {number} */
maxHtml: 15,
/** @type {number} */
charLimit: 1024,
/** @type {string[]} */
merge: [],
exclude: {
/** @type {RegExp[]} */
localhost: [],
/** @type {RegExp[]} */
other: []
}
},
/** @type {?ExternalMonitorConfig|boolean} */
external: {
/** @type {number} */
timeout: 5000,
/** 拉取文件时地并发限制 */
concurrencyLimit: 100,
/** @type {({head: string, tail: string}|function(string):string[])[]} */
js: [],
/** @type {RegExp[]} */
stable: [],
/**
* @param srcUrl {string} 原始 URL
* @return {string[]|string}
*/

replacer: srcUrl => srcUrl
}
}

详细解释

  将 serviceWorker、register、dom、json 或 external 设置为 false 即可关闭对应的功能,默认开启所有功能。

serviceWorker

  该配置项内的内容均为和生成 SW 有关的配置项。

  • escape - 逃生门
    具体介绍见 #逃生门
  • cacheName - 缓存库名称
    swpp 将所有缓存的数据存储在同一个缓存存储中,该缓存存储可以通过浏览器的开发者工具查看、编辑。
    当已经将 swpp 部署到线上后,请勿修改名称,否则 SW 将无法对旧有的缓存库进行管理。
    如果的确需要修改缓存库名称,请自行在 SW 中插入兼容代码,因为 swpp 并不推荐这种行为,文档不再给出提示。
register

  该配置项内的内容均为和注册 SW 有关的配置项。

  • onsuccess - 注册成功事件
    在 DOM 端完成 SW 的注册后会触发该事件,值得注意的是,该事件并非 SW 修改后才会触发,如果用户 SW 已经注册成功再次打开网页仍然会触发该事件。
    在触发事件时页面并不一定完成加载!
  • onerror - 注册失败事件
    当 SW 注册失败后触发该事件,注册失败一般是由于浏览器不支持 SW 或 SW 中存在浏览器无法解析的代码。
    在触发事件时页面并不一定完成加载!
  • builder - 注册代码构建器
    用于生成注册 SW 所用的 HTML 结构,生成出来的结果将被插入到<head>顶部,参数类型和含义在注释中给出。
dom

  该配置项内的内容均为和 DOM 端更新操作有关的配置项。

  • onsuccess - 更新成功事件
    SW 端完成更新操作后,在 DOM 端触发该事件,缓存更新后会刷新页面,该事件将在页面刷新后触发。
    该事件在DOMContentLoaded事件中触发。
json

  该配置项内的内容均为与生成版本文件有关的配置项。

  • maxHtml - 最大 HTML 数量限制
    一般情况下,版本文件中仅记录需要更新缓存的文件以做到精准更新,但是如果一次性更新了太多的 HTML 文件(比如改动了一个所有 HTML 文件都有的内容),容易导致版本文件体积膨胀,所以 swpp 通过该配置项限制一次更新的 HTML 文件的数量,当数量超过限制后将直接清除所有 HTML 缓存,不再记录具体页面。
  • charLimit - 字符数量限制
    swpp 每次都会保留之前生成的版本文件的内容(会进行压缩),时间长了之后难免会导致文件体积膨胀,swpp 通过该配置项限制版本文件的字符数量,当文件字符处理超过该阈值后将会删除一部分内容从而控制文件体积。
  • merge - 更新合并
    网站中部分文件的更新具有连锁性,最典型的就是静态博客的首页、归档页,每次有新的博文都会导致一整个目录下所有文件一起更新,通过该配置项可以将某个目录下的所有更新合并为一个更新,从而减小版本文件的体积。
    匹配规则:
     匹配时将会匹配所有以设置项目开头的目录。
    例如:
    1
        merge: ['page', 'first/sec']
     将会匹配/page//first/sec/目录,注意插件将会自动在输入的值两侧添加斜杠,不需要手动添加。
  • exclude - 忽略项
    网站中部分文件或者网站使用的部分文件可能会存在虽然与缓存规则中的某一条相匹配但其实永远不会被前端访问的情况,为了尽可能地减小 SW 的体积,我们不必在缓存规则中排除这些文件,在exclude中将这些文件排除后插件将在构建时彻底忽略这些文件。
    exclude中非为两类:localhostother,分别表示本地文件和非本地文件,本地文件匹配时仅匹配 pathname 部分,非本地文件会使用完整的 URL(带协议)进行匹配。
external

  该配置项内的内容均为与外部文件监听有关的配置项。

  • timeout - 超时时间
    为插件拉取外部文件设置超时时间,避免无法访问的文件卡死构建,填0表示禁用超时(不建议)。

  • concurrencyLimit - 并发限制
    部分用户响应拉取外部文件时经常出现超时错误,经测试后怀疑问题可能是由于并发过多造成,所以添加该选项用于控制并发量,该选项不一定能够解决问题,如果设置后仍然出现大量超时错误请向我反馈。

  • js - JS 代码匹配
    swpp 无法自主的从 js 代码中提取 URL,所以需要用户自己添加一些规则来进行 URL 提取。
    该项中支持两种类型:对象和函数。前者(不建议使用)是按照插件内置的特定的匹配规则从 JS 文件中提取 URL,后者是用户自己编写代码提取。
    内置的匹配规则如下:

    • head - 起始头
    • tail - 结束头

     swpp 将提取从起始头和结束头之间的内容(单行匹配),注意由于起始头和结束头会被直接插入到正则表达式当中,所以括号之类的带有特殊含义的字符需要使用反斜杠转义。
    自定义函数接收一个字符串参数,表示一个 js 文件的内容,返回一个字符串数组,表示从中提取的 URL。

  • stable - 稳定 URL
    部分 URL 可能 URL 没有变化其指向的文件内容就一定不会变化,该选项通过正则表达式匹配这些 URL。被判定为稳定 URL 的文件将只在第一次访问时拉取其文件内容,用来记录其中出现了哪些 URL。

  • replacer - URL 替换器
    部分 URL 可能存在国内可以访问国外不能访问的情况,这对于通过 netlify、github action 等远程构建工具构建网站的用户来说是致命的,所以 swpp 提供了替换器,可以在构建期将一个 URL 替换为另一个或多个 URL。
    函数返回字符串或字符串数组,如果返回了字符串数组,插件将以 URL 竞速的形式拉取文件。

eject

  插件支持用户通过ejectValues向 SW 中插入变量和常量,用法如下:

1
2
3
4
5
6
7
/**
* @param framework {Object} 平台对象
* @param rules {SwppRules} 规则文件对象
*/

module.exports.ejectValues = (framework, rules) => {

}

  该函数应当返回一个对象,对象中的键的名称代表变量名(仅允许使用英文字母和阿拉伯数字),值的类型格式如下(注意返回的 value 中不要包含不能在 SW 中运行的内容):

1
2
3
4
5
const simple = {
/** @type {string} */
prefix: '',
value: ''
}

  比如对于 hexo 平台,我们可以使用如下代码在 SW 中插入域名:

1
2
3
4
5
6
7
8
module.exports.ejectValues = (hexo, _) => {
return {
domain: {
prefix: 'const',
value: new URL(hexo.config.url).host
}
}
}

  对于我的博客,这段代码将在 SW 中插入如下语句:

1
const ejectDomain = 'kmar.top'

  可以发现在值被插入后标识符加入了eject前缀,并且自己设置的变量名的首字母被大写了,这是为了避免用户设置的变量名与插件中的一些字段名称重复。所以在使用时引用 eject 插入的值注意名称转换的问题,部分内置函数中添加了$eject参数,在这些函数中可以直接使用$eject.domain的形式(不能使用方括号,也不能使用 forin 之类的形式遍历,$eject是一个虚拟参数,在实际运行时是不存在的)引用 eject 导出的变量,使用时需要注意,函数必须使用箭头函数不能使用function,同时不能修改$eject参数的变量名,否则将无法识别。

  使用示范:

1
2
3
4
5
6
7
8
9
10
11
12
module.exports.cacheRules = {
simple: {
clean: true,
match: (url, $eject) => {
const domain0 = ejectDomain // 非法!对于支持 $eject 参数的函数,必须使用 $eject.xxx 的形式引用变量
const domain1 = $eject['domain'] // 非法!必须使用 $eject.xxx 的形式
Object.getOwnPropertyNames($eject) // 非法!实际运行时 $eject 并不存在,将会报错
for (let key in $eject) {} // 非法!原因同上
const domain2 = $eject.domain // 合法
}
}
}

跳过请求

  在规则文件中声明skipRequest即可设置跳过规则,返回true的请求 swpp 将不会对其做任何处理。

1
2
3
4
5
6
7
/**
* @param request {Request}
* @return {boolean}
*/

module.exports.skipRequest = request => {
// do something...
}

  注意:对于使用referrerpolicy来规避 referer 检查的资源,务必使用skipRequest跳过处理,否则 swpp 将为请求添加 referer 头导致资源返回 403 状态码。

  比如 Bilibili 追番页番剧封面图所用的图床就带有防盗链,不过其允许空 referer 访问,所以可以通过referrerpolicy来实现在自己网页中加载图像,但是安装并启用 swpp 后会导致图像全部加载失败,此时在规则文件中添加如下代码即可解决问题:

1
module.exports.skipRequest = request => request.url.startsWith('https://i0.hdslb.com')

添加跨域校验

  如果某个请求需要启用 CORS 检查,则在规则文件中声明isCors即可:

1
2
3
4
5
6
7
8
/**
* 判断指定请求是否使用 CORS(必须启用 CORS 的请求将不通过该函数判断)
* @param request {Request} 当前请求的 request
* @return {boolean}
*/

module.exports.isCors = request => {
// do something...
}

  注意:有些请求必须开启 CORS,否则 SW 无法通过 status 判断请求是否成功,这些请求将绕过isCors函数强制启用 CORS。

启用请求合并

  在规则文件中声明isMemoryQueue即可启用请求合并功能:

1
2
3
4
5
6
7
8
/**
* 判断指定请求是否开启请求合并(合并时通过 URL 判断请求是否相同)
* @param request {Request}
* @return {boolean}
*/

module.exports.isMemoryQueue = request => {
// do something...
}

缓存规则

  在规则文件中声明cacheRules即可定义缓存规则,这个值是一个对象,在对象中添加键值对即可添加缓存规则:

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.cacheRules = {
/**
* 键值是规则名称<br/>
* 规则名只是给人看的,不影响匹配,起一个能够表明该项规则的用途的名称即可,不要太长
*/

simple: {
/**
* 符合该规则的缓存在进行全局清理时是否清除
* 如果你无法确定是否需要声明为 false 的话写 true 即可
* @type boolean
*/

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

search: false,
/**
* 匹配缓存
* @param url {URL} 链接的 URL 对象(对象包括 hash 和 search,但不要使用 hash,search 为 false 时不要使用 search)
* @param $eject {Object} 用于访问通过 [ejectValues] 函数插入的变量(该参数可忽略不写)
* @return boolean
*/

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

  cacheRules中可以同时声明多个缓存规则,在进行匹配时会按照声明顺序依次匹配,只要有一个匹配成功即视为成功。

Request 篡改

  在规则文件中添加modifyRequest函数即可启用 Request 篡改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 修改 Request
* @param request {Request} 原始 Request
* @param $eject {Object} 用于访问通过 [ejectValues] 函数插入的变量(这个参数可以忽略不写)
* @return {Request} 修改后的 Request,不修改的话返回 null 或不返回数据
*/

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

URL 竞速

  在规则文件中添加getRaceUrls函数即可启用 URL 竞速。

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.getRaceUrls = 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

  在规则文件中添加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}`
]
}
}
}

阻塞响应

  在规则文件中添加blockRequest函数即可启用阻塞响应。

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

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

手动提交更新

  如果部分文件插件无法监听到更新,可以通过update字段手动向插件提交更新:

1
2
3
4
5
6
7
8
module.exports.update = {
flag: true,
force: false,
/** @type string[] */
refresh: [],
/** @type ChangeExpression[] */
change: []
}
  • flag 是更新标记,只有当本次更新标记与上次值不相同时 update 的内容才会生效,所以修改 update 后没有必要手动清空 update 的内容。
  • force 为强制更新标记,当设置为 true 时会清除前端所有缓存。
  • refresh 为 URL 刷新列表,填写想要刷新缓存的 URL。
  • change 为刷新列表,填写要提交的缓存刷新表达式,表达式写法见 swpp-backends 中的 ChangeExpression 类型。

  同时 swpp 支持用户通过extraListenedUrls项向插件添加需要监听的 URL,按照如下格式填写:

1
module.exports.extraListenedUrls = []

  extraListenedUrls 的值不一定要是数组,只要是包含 forEach 函数的对象就可以,元素类型必须是 string

  注意填写该项后,swpp 会认为该项内的所有 URL 全部存在,如果哪一个 URL 需要删除,需要从这个列表中手动移除。

自定义 fetch

  在规则文件中定义fetchFile即可自定义前端 SW 拉取文件的行为:

1
2
3
4
5
6
7
8
/**
* @param request {Request} 请求信息
* @param banCache {boolean} 是否禁用缓存
* @param spare {?SpareUrls} 备用 URL 列表,传 null 表示需要自己获取
*/

module.exports.fetchFile = (request, banCache, spare = null) => {
// do something...
}

  注意,自定义 fetch 后插件内置的备用 URL 和 URL 竞速功能都将失效。

导出代码

  在规则文件中定义external即可向 SW 中导出代码:

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

module.exports.external = ['customFunction']

  这将在 SW 中插入如下代码:

1
2
3
const customFunction = () => {
// do something...
}

文档剩余内容为 API 文档,仅供开发者使用,普通用户不须要查看

SW API 文档

  插件内置的Service Worker提供了一个 API,用于前端在加载页面时通知 SW 进行版本更新检查:

1
navigator.serviceWorker.controller.postMessage('update')

  这个代码调用后 SW 就会进行一系列版本更新检查的操作,如果需要接受更新完毕的消息,则需要添加如下事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
navigator.serviceWorker.addEventListener('message', event => {
const data = event.data
switch (data.type) {
case 'update':
/*
这里代表已经更新完毕,data 中包含如下字段

+ new: {escape: number, global: number, local: number}
+ old?: {escape?: number, global: number, local: number}
+ list?: string[]

其中 new 表示更新后的版本号,old 是更新前的版本号,list 是本次更新删除的缓存的 URL
*/

break
case 'escape':
/* 这里代表通过逃生门更新缓存完毕 */
break
}
})

  需要自定义 DOM 端代码的话可以参考插件内置的 DOM 端的代码。

获取缓存版本号

  SW 没有内置获取缓存版本号的代码,使用如下模板即可获取版本号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
caches.match('https://id.v3/')
.then(response => response?.json())
.then(json => {
if (json) {
/*
版本号:
escape: 逃生门
global: 全局版本号
local: 局部版本号
*/

} else {
// 新用户可能这里为空
}
// 示例:
// const version = optionals.querySelector('.panel[type=time] p.version')
// version.textContent = json ? `${json.escape}#${json.global}.${json.local}` : 'latest'
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
async () => {
const response = await caches.match('https://id.v3/')
if (!response) {
// 版本号不存在
// do something...
return
}
/*
版本号:
escape: 逃生门
global: 全局版本号
local: 局部版本号
*/

const json = await response.json()
// 示例:
// const version = optionals.querySelector('.panel[type=time] p.version')
// version.textContent = json ? `${json.escape}#${json.global}.${json.local}` : 'latest'
}

插件 API 文档

  可以使用version字段获取 swpp 的版本号:

1
2
const swpp = require('swpp-backends')
console.log(swpp.version)

类型

  如果你正在使用 typescript,那么可以通过如下语句导入 swpp 的类型信息:

1
import {xxx} from 'swpp-backends'

  其中xxx为类型名称,swpp 对外公开了下列的类型:

  • SwppRules - 合并后的规则文件对象
  • SwppConfig - 插件配置项
  • SwppConfigTemplate - 插件配置项模板
  • ServiceWorkerConfig - SW 配置项
  • RegisterConfig - SW 注册配置项
  • DomConfig - DOM 端配置项
  • VersionJsonConfig - 版本文件配置项
  • ExternalMonitorConfig - 外部文件监听配置项
  • CacheRules - 缓存规则
  • SpareURLs - 备用 URL 列表
  • EjectValue - eject 导出元素
  • EjectCache - eject 缓存
  • VersionJson - 版本信息
  • VersionMap - 版本列表
  • UpdateJson - 更新信息
  • UpdateVersionInfo - 更新信息中的版本信息
  • FlagStr - 更新标记
  • ChangeExpression - 更新表达式
  • AnalyzeResult - 版本信息分析结果

函数

  swpp 按照一定规则对 API 进行了分类,一共有五大类:cachebuilderloadereventutils

  • builder - 构建器,用于生成一些必要的信息

    • buildServiceWorker - 生成 sw.js 的代码
    • buildDomJs - 生成 DOM 端 JS 代码
    • buildUpdateJson - 生成新的版本信息
    • buildVersionJson - 生成版本更新信息
    • analyzeVersion - 对比分析新旧版本信息的差异
    • calcEjectValues - 计算 eject
  • loader - 加载器,用来一些已有的数据

    • loadRules - 加载规则文件
    • loadVersionJson - 加载上一次的版本信息
    • loadUpdateJson - 加载上一次的版本更新文件
  • event - 事件

    • addRulesMapEvent - 添加一个规则映射事件
    • registryFileHandler - 注册一个文件处理器
    • submitCacheInfo - 提交一个要存储在版本信息中的值
    • submitExternalUrl - 提交一个需要监听的外链
    • refreshUrl - 刷新指定 URL 的缓存
    • submitChange - 向版本更新文件提交一个更新语句
  • cache - 缓存,用来存储已经计算过的信息

    • readEjectData - 读取 eject 数据
    • readRules - 读取规则文件数据
    • readOldVersionJson - 读取上一次的版本信息
    • readNewVersionJson - 读取当前的版本信息
    • readMergeVersionMap - 读取新旧版本信息合并后的版本列表
    • readUpdateJson - 读取上一次的版本更新信息
    • readAnalyzeResult - 读取最后一次版本信息差异分析结果
  • utils - 工具函数

    • getSource - 获取指定值的 js 源码表达式
    • findCache - 查询指定 URL 对应的缓存规则
    • fetchFile - 拉取一个文件(内部自动调用replaceDevRequest
    • replaceDevRequest - 按照replacer的内容替换一个 URL
    • replaceRequest - 按照规则文件中的内容替换一个 URL
    • isStable - 判断一个 URL 是否是 stable 的
    • isExclude - 判断一个 URL 是否应该被忽略
    • findFileHandler - 查询指定 URL 对应的文件处理器
    • eachAllLinkInUrl - 遍历一个 URL 对应的文件中的所有 URL(包括其自身)
    • getShorthand - 计算一个 URL 在版本更新文件中的缩写形式(只适用于end表达式)
    • deepFreeze - 深度冻结对象,冻结后对象将无法修改(直接在原有对象上进行操作)
    • writeVariant - 写入一个全局变量
    • readVariant - 读取一个全局变量
    • deleteVariant - 删除一个全局变量

一般顺序

  在构建 SW 时一般按照如下顺序调用 API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const swpp = require('swpp-backends')
const path = require('path')
const fs = require('fs')

// 加载规则文件
const rules = swpp.loader.loadRules(args)
// 计算 eject
swpp.builder.calcEjectValues(args)

/*
1. 下面的代码没有顺序关系
2. 注意下面这些代码只生成了文件的内容,并没有创建文件,具体创建文件的方式不同平台可能不一样
3. 注意处理配置项关闭功能的情况
*/


// 生成 SW
swpp.builder.buildServiceWorker()
// 生成注册 SW 的代码
rules.config.builder(args)
// 读取 DOM 端 JS 代码样例
swpp.builder.buildDomJs()

  在生成版本信息时一般使用如下顺序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const swpp = require('swpp-backends')

// 加载规则文件
const rules = swpp.loader.loadRules(args)
// 计算 eject
swpp.builder.calcEjectValues(args)

// 加载上一次的版本信息
await swpp.loader.loadUpdateJson(args)
// 加载上一次的版本更新信息
await swpp.loader.loadVersionJson(args)
// 构建版本信息,注意这里只返回了数据没有进行写入,需要自行写入文件
await swpp.builder.buildVersionJson(args)
// 分析版本差异
const dif = swpp.builder.analyzeVersion()
// 构建版本更新信息,注意同样需要自行写入文件
await swpp.builder.buildVersionJson(args)

  上面的代码中描述了一些基本函数的调用顺序,并没有使用event,如果想要实现event可以参考hexo-swpp@3的代码。