hexo-swpp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +395 -0
- package/package.json +26 -0
- package/sw-template.js +289 -0
package/index.js
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
// noinspection DuplicatedCode
|
|
2
|
+
|
|
3
|
+
"use strict"
|
|
4
|
+
|
|
5
|
+
const fs = require('fs')
|
|
6
|
+
const logger = require('hexo-log')()
|
|
7
|
+
const axios = require('axios')
|
|
8
|
+
const path = require('path')
|
|
9
|
+
|
|
10
|
+
const findScript = () => path.resolve('./', 'sw-cache')
|
|
11
|
+
|
|
12
|
+
// noinspection JSUnresolvedVariable
|
|
13
|
+
const config = hexo.config
|
|
14
|
+
const pluginConfig = config.version
|
|
15
|
+
const root = config.url + config.root
|
|
16
|
+
const { cacheList, replaceList } = require(findScript())
|
|
17
|
+
|
|
18
|
+
// 生成 update.json
|
|
19
|
+
// noinspection JSUnresolvedVariable
|
|
20
|
+
hexo.on('exit', async () => {
|
|
21
|
+
if (!fs.existsSync('public/index.html')) return logger.info('跳过生成')
|
|
22
|
+
const cachePath = 'cacheList.json'
|
|
23
|
+
const updatePath = 'update.json'
|
|
24
|
+
const oldCache = await getJsonFromNetwork(cachePath)
|
|
25
|
+
const oldUpdate = await getJsonFromNetwork(updatePath)
|
|
26
|
+
const newCache = buildNewJson(cachePath)
|
|
27
|
+
const dif = compare(oldCache, newCache)
|
|
28
|
+
buildUpdateJson(updatePath, dif, oldUpdate)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
// 生成 sw.js
|
|
32
|
+
// noinspection JSUnresolvedVariable
|
|
33
|
+
hexo.extend.generator.register('buildSw', () => {
|
|
34
|
+
// noinspection JSUnresolvedVariable
|
|
35
|
+
if (pluginConfig.customJS) return
|
|
36
|
+
const absPath = module.path + '/sw-template.js'
|
|
37
|
+
const rootPath = path.resolve('./')
|
|
38
|
+
const relativePath = path.relative(rootPath, absPath)
|
|
39
|
+
const template = fs.readFileSync(relativePath, 'utf8')
|
|
40
|
+
const cache = fs.readFileSync('sw-cache.js', 'utf8')
|
|
41
|
+
.replace('module.exports.cacheList', 'const cacheList')
|
|
42
|
+
.replace('module.exports.replaceList', 'const replaceList')
|
|
43
|
+
return {
|
|
44
|
+
path: 'sw.js',
|
|
45
|
+
data: template.replace('const { cacheList, replaceList } = require(\'../sw-cache\')', cache)
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// 生成注册 sw 的代码
|
|
50
|
+
// noinspection JSUnresolvedVariable
|
|
51
|
+
hexo.extend.injector.register('head_begin', () => {
|
|
52
|
+
// noinspection JSUnresolvedVariable
|
|
53
|
+
return `<script>
|
|
54
|
+
(() => {
|
|
55
|
+
const sw = navigator.serviceWorker
|
|
56
|
+
const error = () => ${pluginConfig.onError}
|
|
57
|
+
if (!sw?.register('/sw.js')?.then(() => {
|
|
58
|
+
if (!sw.controller) ${pluginConfig.onSuccess}
|
|
59
|
+
})?.catch(error)) error()
|
|
60
|
+
})()
|
|
61
|
+
</script>`
|
|
62
|
+
}, "default")
|
|
63
|
+
|
|
64
|
+
/** 遍历指定目录下的所有文件 */
|
|
65
|
+
const eachAllFile = (root, cb) => {
|
|
66
|
+
const stats = fs.statSync(root)
|
|
67
|
+
if (stats.isFile()) cb(root)
|
|
68
|
+
else {
|
|
69
|
+
const files = fs.readdirSync(root)
|
|
70
|
+
if (!root.endsWith('/')) root += '/'
|
|
71
|
+
files.forEach(it => eachAllFile(root + it, cb))
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 判断指定文件是否需要排除 */
|
|
76
|
+
const isExclude = pathname => {
|
|
77
|
+
for (let reg of pluginConfig.exclude) {
|
|
78
|
+
if (pathname.match(new RegExp(reg, 'i'))) return true
|
|
79
|
+
}
|
|
80
|
+
return false
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 构建 md5 缓存表并写入到发布目录中
|
|
85
|
+
*
|
|
86
|
+
* 格式为 `{"[path]": "[md5Value]"}`
|
|
87
|
+
*
|
|
88
|
+
* @param path 相对于根目录的路径
|
|
89
|
+
* @return {Object} 生成的 json 对象
|
|
90
|
+
*/
|
|
91
|
+
const buildNewJson = path => {
|
|
92
|
+
const crypto = require('crypto') // 用于计算 md5
|
|
93
|
+
const result = {} // 存储新的 MD5 表
|
|
94
|
+
const removeIndex = config.pretty_urls?.trailing_index
|
|
95
|
+
const removeHtml = config.pretty_urls?.trailing_html
|
|
96
|
+
eachAllFile(config.public_dir, path => {
|
|
97
|
+
if (!fs.existsSync(path)) return logger.error(`${path} 不存在!`)
|
|
98
|
+
let endIndex
|
|
99
|
+
if (removeIndex && path.endsWith('/index.html')) endIndex = path.length - 10
|
|
100
|
+
else if (removeHtml && path.endsWith('.html')) endIndex = path.length - 5
|
|
101
|
+
else endIndex = path.length
|
|
102
|
+
const url = new URL(root + path.substring(7, endIndex))
|
|
103
|
+
if (findCache(url) && !isExclude(url.pathname)) {
|
|
104
|
+
const content = fs.readFileSync(path)
|
|
105
|
+
const key = decodeURIComponent(url.pathname)
|
|
106
|
+
result[key] = crypto.createHash('md5').update(content).digest('hex')
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
let publicRoot = config.public_dir || 'public/'
|
|
110
|
+
if (!publicRoot.endsWith('/')) publicRoot += '/'
|
|
111
|
+
fs.writeFileSync(`${publicRoot}${path}`, JSON.stringify(result))
|
|
112
|
+
logger.info(`Generated: ${path}`)
|
|
113
|
+
return result
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 从网络拉取 json 文件
|
|
118
|
+
* @param path 文件路径(相对于根目录)
|
|
119
|
+
*/
|
|
120
|
+
const getJsonFromNetwork = async path => {
|
|
121
|
+
const url = root + path
|
|
122
|
+
try {
|
|
123
|
+
const result = await axios.get(url)
|
|
124
|
+
if (result.status < 200 || result.status >= 400 || !result.data)
|
|
125
|
+
// noinspection ExceptionCaughtLocallyJS
|
|
126
|
+
throw `拉取 ${url} 时出现异常(${result.status})`
|
|
127
|
+
return result.data
|
|
128
|
+
} catch (e) {
|
|
129
|
+
if (e.toString().includes('404'))
|
|
130
|
+
logger.error(`拉取 ${url} 时出现 404,如果您是第一次构建请忽略这个错误`)
|
|
131
|
+
else throw e
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 对比两个 md5 缓存表的区别
|
|
137
|
+
* @return [string] 需要更新的文件路径
|
|
138
|
+
*/
|
|
139
|
+
const compare = (oldCache, newCache) => {
|
|
140
|
+
const result = []
|
|
141
|
+
if (!oldCache) return result
|
|
142
|
+
for (let path in oldCache) {
|
|
143
|
+
if (newCache[path] !== oldCache[path]) result.push(path)
|
|
144
|
+
}
|
|
145
|
+
return result
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** 判断指定资源是否需要合并 */
|
|
149
|
+
const isMerge = (pathname, tidied) => {
|
|
150
|
+
// noinspection JSUnresolvedVariable
|
|
151
|
+
const optional = pluginConfig.merge
|
|
152
|
+
if (pathname.includes(`/${config.tag_dir}/`)) {
|
|
153
|
+
if (optional.tags ?? true) {
|
|
154
|
+
tidied.tags = true
|
|
155
|
+
return true
|
|
156
|
+
}
|
|
157
|
+
} else if (pathname.includes(`/${config.archive_dir}/`)) {
|
|
158
|
+
if (optional.archives ?? true) {
|
|
159
|
+
tidied.archives = true
|
|
160
|
+
return true
|
|
161
|
+
}
|
|
162
|
+
} else if (pathname.includes(`/${config.category_dir}/`)) {
|
|
163
|
+
if (optional.categories ?? true) {
|
|
164
|
+
tidied.categories = true
|
|
165
|
+
return true
|
|
166
|
+
}
|
|
167
|
+
} else if (pathname.startsWith('/page/') || pathname.length <= 1) {
|
|
168
|
+
if (optional.index ?? true) {
|
|
169
|
+
tidied.index = true
|
|
170
|
+
return true
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* 从一个字符串中提取最后两个 / 之间的内容
|
|
177
|
+
* @param it {string} 要操作的字符串
|
|
178
|
+
* @param keep {boolean} 是否保留最后一个 / 及其后面的内容
|
|
179
|
+
*/
|
|
180
|
+
const clipPageName = (it, keep) => {
|
|
181
|
+
const end = it.lastIndexOf('/')
|
|
182
|
+
let index = end - 1
|
|
183
|
+
for (; index > 0; --index) {
|
|
184
|
+
if (it[index] === '/') break
|
|
185
|
+
}
|
|
186
|
+
return it.substring(index + 1, keep ? it.length : end)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** 构建新的 update.json */
|
|
190
|
+
const buildUpdateJson = (name, dif, oldUpdate) => {
|
|
191
|
+
/** 将对象写入文件,如果对象为 null 或 undefined 则跳过写入 */
|
|
192
|
+
const writeJson = json => {
|
|
193
|
+
if (json) {
|
|
194
|
+
logger.info(`Generated: ${name}`)
|
|
195
|
+
fs.writeFileSync(`public/${name}`, JSON.stringify(json))
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// 读取拓展 json
|
|
199
|
+
const expand = fs.existsSync(name) ? JSON.parse(fs.readFileSync(name)) : undefined
|
|
200
|
+
// 获取上次最新的版本
|
|
201
|
+
let oldVersion = oldUpdate?.info[0]?.version ?? 0
|
|
202
|
+
if (typeof oldVersion !== 'number') {
|
|
203
|
+
// 当上次最新的版本号不是数字是尝试对其进行转换,如果无法转换则直接置零
|
|
204
|
+
if (oldVersion.match('\D')) oldVersion = 0
|
|
205
|
+
else oldVersion = Number.parseInt(oldVersion)
|
|
206
|
+
}
|
|
207
|
+
// 存储本次更新的内容
|
|
208
|
+
const newInfo = {
|
|
209
|
+
version: oldVersion + 1,
|
|
210
|
+
change: expand?.change ?? []
|
|
211
|
+
}
|
|
212
|
+
// 整理更新的数据
|
|
213
|
+
const tidied = tidyDiff(dif, expand)
|
|
214
|
+
// 如果没有更新的文件就直接退出
|
|
215
|
+
if (
|
|
216
|
+
tidied.page.size === 0 && tidied.file.size === 0 &&
|
|
217
|
+
!(tidied.archives || tidied.categories || tidied.tags || tidied.index)
|
|
218
|
+
) return writeJson(oldUpdate)
|
|
219
|
+
pushUpdateToInfo(newInfo, tidied)
|
|
220
|
+
const result = mergeUpdateWithOld(newInfo, oldUpdate, tidied)
|
|
221
|
+
return writeJson(result)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const mergeUpdateWithOld = (newInfo, oldUpdate, tidied) => {
|
|
225
|
+
const result = {
|
|
226
|
+
global: (oldUpdate?.global ?? 0) + (tidied.updateGlobal ? 1 : 0),
|
|
227
|
+
info: [newInfo]
|
|
228
|
+
}
|
|
229
|
+
// noinspection JSUnresolvedVariable
|
|
230
|
+
const charLimit = pluginConfig.charLimit ?? 1024
|
|
231
|
+
if (JSON.stringify(result).length > charLimit) {
|
|
232
|
+
return {
|
|
233
|
+
global: result.global,
|
|
234
|
+
info: [{version: newInfo.version}]
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (!oldUpdate) return result
|
|
238
|
+
for (let it of oldUpdate.info) {
|
|
239
|
+
if (it.change) it.change = zipInfo(newInfo, it)
|
|
240
|
+
result.info.push(it)
|
|
241
|
+
if (JSON.stringify(result).length > charLimit) {
|
|
242
|
+
result.info.pop()
|
|
243
|
+
break
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return result
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// 压缩相同项目
|
|
250
|
+
const zipInfo = (newInfo, oldInfo) => {
|
|
251
|
+
oldInfo = oldInfo.change
|
|
252
|
+
newInfo = newInfo.change
|
|
253
|
+
const result = []
|
|
254
|
+
for (let i = oldInfo.length - 1; i !== -1; --i) {
|
|
255
|
+
const value = oldInfo[i]
|
|
256
|
+
if (value.flag === 'page' && newInfo.find(it => it.flag === 'html')) continue
|
|
257
|
+
const newValue = newInfo.find(it => it.flag === value.flag)
|
|
258
|
+
if (!newValue) {
|
|
259
|
+
result.push(value)
|
|
260
|
+
continue
|
|
261
|
+
}
|
|
262
|
+
if (!value.value) continue
|
|
263
|
+
const isArray = Array.isArray(newValue.value)
|
|
264
|
+
if (Array.isArray(value.value)) {
|
|
265
|
+
const array = value.value
|
|
266
|
+
.filter(it => isArray ? !newValue.value.find(that => that === it) : it !== newValue.value)
|
|
267
|
+
if (array.length === 0) continue
|
|
268
|
+
result.push({flag: value.flag, value: array.length === 1 ? array[0] : array})
|
|
269
|
+
} else if (isArray) {
|
|
270
|
+
if (!newValue.value.find(it => it === value.value))
|
|
271
|
+
result.push(value)
|
|
272
|
+
} else {
|
|
273
|
+
if (newValue.value !== value.value)
|
|
274
|
+
result.push(value)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return result
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// 将更新推送到 info
|
|
281
|
+
const pushUpdateToInfo = (info, tidied) => {
|
|
282
|
+
// 推送页面更新
|
|
283
|
+
// noinspection JSUnresolvedVariable
|
|
284
|
+
if (tidied.page.size > (pluginConfig.maxHtml ?? 15)) {
|
|
285
|
+
// 如果 html 数量超过阈值就直接清掉所有 html
|
|
286
|
+
info.change.push({flag: 'html'})
|
|
287
|
+
} else {
|
|
288
|
+
const pages = [] // 独立更新
|
|
289
|
+
const merges = [] // 要合并的更新
|
|
290
|
+
tidied.page.forEach(it => pages.push(it))
|
|
291
|
+
if (tidied.tags) merges.push(config.tag_dir)
|
|
292
|
+
if (tidied.archives) merges.push(config.archive_dir)
|
|
293
|
+
if (tidied.categories) merges.push(config.category_dir)
|
|
294
|
+
if (tidied.index) {
|
|
295
|
+
pages.push(clipPageName(root, false))
|
|
296
|
+
merges.push('page')
|
|
297
|
+
}
|
|
298
|
+
if (merges.length > 0)
|
|
299
|
+
info.change.push({flag: 'str', value: merges.map(it => `/${it}/`)})
|
|
300
|
+
if (pages.length > 0)
|
|
301
|
+
info.change.push({flag: 'page', value: pages})
|
|
302
|
+
}
|
|
303
|
+
// 推送文件更新
|
|
304
|
+
if (tidied.file.size > 0) {
|
|
305
|
+
const list = []
|
|
306
|
+
tidied.file.forEach(it => list.push(it))
|
|
307
|
+
info.change.push({flag: 'file', value: list})
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// 将 diff 整理分类,并将 expand 整合到
|
|
312
|
+
const tidyDiff = (dif, expand) => {
|
|
313
|
+
const tidied = {
|
|
314
|
+
/** 所有 HTML 页面 */
|
|
315
|
+
page: new Set(),
|
|
316
|
+
/** 所有文件 */
|
|
317
|
+
file: new Set(),
|
|
318
|
+
/** 标记 tags 是否更新 */
|
|
319
|
+
tags: false,
|
|
320
|
+
/** 标记 archives 是否更新 */
|
|
321
|
+
archives: false,
|
|
322
|
+
/** 标记 categories 是否更新 */
|
|
323
|
+
categories: false,
|
|
324
|
+
/** 标记 index 是否更新 */
|
|
325
|
+
index: false,
|
|
326
|
+
/** 标记是否更新 global 版本号 */
|
|
327
|
+
updateGlobal: expand?.global
|
|
328
|
+
}
|
|
329
|
+
// noinspection JSUnresolvedVariable
|
|
330
|
+
const mode = pluginConfig.precisionMode
|
|
331
|
+
for (let it of dif) {
|
|
332
|
+
const url = new URL(root + it) // 当前文件的 URL
|
|
333
|
+
const cache = findCache(url) // 查询缓存
|
|
334
|
+
if (!cache) {
|
|
335
|
+
logger.error(`[buildUpdate] 指定 URL(${url.pathname}) 未查询到缓存规则!`)
|
|
336
|
+
continue
|
|
337
|
+
}
|
|
338
|
+
if (cache.clean) tidied.updateGlobal = true
|
|
339
|
+
if (it.match(/(\/|\.html)$/)) { // 判断缓存是否是 html
|
|
340
|
+
if (isMerge(it, tidied)) continue
|
|
341
|
+
if (mode.html ?? false) tidied.page.add(url.pathname)
|
|
342
|
+
else tidied.page.add(clipPageName(url.href, !it.endsWith('/')))
|
|
343
|
+
} else {
|
|
344
|
+
const extendedName = (it.includes('.') ? it.match(/[^.]+$/)[0] : null) ?? 'default'
|
|
345
|
+
const setting = mode[extendedName] ?? (mode.default ?? false)
|
|
346
|
+
if (setting) tidied.file.add(url.pathname)
|
|
347
|
+
else {
|
|
348
|
+
let name = url.href.match(/[^/]+$/)[0]
|
|
349
|
+
if (!name) throw `${url.href} 格式错误`
|
|
350
|
+
tidied.file.add(name)
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return tidied
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function findCache(url) {
|
|
358
|
+
url = new URL(replaceRequest(url.href))
|
|
359
|
+
for (let key in cacheList) {
|
|
360
|
+
const value = cacheList[key]
|
|
361
|
+
if (value.match(url)) return value
|
|
362
|
+
}
|
|
363
|
+
return null
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function replaceRequest(url) {
|
|
367
|
+
for (let key in replaceList) {
|
|
368
|
+
const value = replaceList[key]
|
|
369
|
+
for (let source of value.source) {
|
|
370
|
+
if (url.match(source)) {
|
|
371
|
+
url = url.replace(source, value.dist)
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return url
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** 对 hexo 的全局变量进行排序,以保证每次生成的结果一致 */
|
|
379
|
+
(() => {
|
|
380
|
+
// noinspection JSUnresolvedVariable
|
|
381
|
+
const locals = hexo.locals
|
|
382
|
+
const compare = (a, b) => a < b ? -1 : 1
|
|
383
|
+
const sort = (name, value) => locals.get(name).data.sort((a, b) => compare(a[value], b[value]))
|
|
384
|
+
const list = {
|
|
385
|
+
posts: 'title',
|
|
386
|
+
pages: 'title',
|
|
387
|
+
tags: 'name',
|
|
388
|
+
categories: 'name'
|
|
389
|
+
}
|
|
390
|
+
for (let key in list) sort(key, list[key])
|
|
391
|
+
locals.get('posts').forEach(it => {
|
|
392
|
+
it.tags.data.sort((a, b) => compare(a.name, b.name))
|
|
393
|
+
it.categories.data.sort((a, b) => compare(a.name, b.name))
|
|
394
|
+
})
|
|
395
|
+
})()
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hexo-swpp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "index.js",
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"axios": "^1.2.0",
|
|
7
|
+
"hexo-log": "^3.0.0"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"index.js",
|
|
11
|
+
"sw-template.js"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"hexo",
|
|
15
|
+
"sw",
|
|
16
|
+
"ServiceWorker",
|
|
17
|
+
"json",
|
|
18
|
+
"auto"
|
|
19
|
+
],
|
|
20
|
+
"author": "kmar",
|
|
21
|
+
"license": "AGPL-3.0",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/EmptyDreams/hexo-swpp.git"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/sw-template.js
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
// noinspection JSIgnoredPromiseFromCall
|
|
2
|
+
|
|
3
|
+
(() => {
|
|
4
|
+
/** 缓存库名称 */
|
|
5
|
+
const CACHE_NAME = 'kmarBlogCache'
|
|
6
|
+
/** 版本名称存储地址(必须以`/`结尾) */
|
|
7
|
+
const VERSION_PATH = 'https://id.v3/'
|
|
8
|
+
|
|
9
|
+
self.addEventListener('install', () => self.skipWaiting())
|
|
10
|
+
|
|
11
|
+
const { cacheList, replaceList } = require('../sw-cache')
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 删除指定缓存
|
|
15
|
+
* @param list 要删除的缓存列表
|
|
16
|
+
* @return {Promise<Array<string>>} 删除的缓存的URL列表
|
|
17
|
+
*/
|
|
18
|
+
const deleteCache = list => new Promise(resolve => {
|
|
19
|
+
caches.open(CACHE_NAME).then(cache => cache.keys()
|
|
20
|
+
.then(keys => Promise.all(keys.map(
|
|
21
|
+
it => new Promise(async resolve1 => {
|
|
22
|
+
const url = it.url
|
|
23
|
+
if (url !== VERSION_PATH && list.match(url)) {
|
|
24
|
+
await cache.delete(it)
|
|
25
|
+
resolve1(url)
|
|
26
|
+
} else resolve1(undefined)
|
|
27
|
+
})
|
|
28
|
+
)).then(removeList => resolve(removeList)))
|
|
29
|
+
)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
self.addEventListener('fetch', event => {
|
|
33
|
+
const replace = replaceRequest(event.request)
|
|
34
|
+
const request = replace || event.request
|
|
35
|
+
const url = new URL(request.url)
|
|
36
|
+
if (findCache(url)) {
|
|
37
|
+
event.respondWith(new Promise(async resolve => {
|
|
38
|
+
const key = new Request(`${url.protocol}//${url.host}${url.pathname}`)
|
|
39
|
+
let response = await caches.match(key)
|
|
40
|
+
if (!response) {
|
|
41
|
+
response = await fetchNoCache(request)
|
|
42
|
+
const status = response.status
|
|
43
|
+
if ((status > 199 && status < 400) || status === 0) {
|
|
44
|
+
const clone = response.clone()
|
|
45
|
+
caches.open(CACHE_NAME).then(cache => cache.put(key, clone))
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
resolve(response)
|
|
49
|
+
}))
|
|
50
|
+
} else if (replace !== null) {
|
|
51
|
+
event.respondWith(fetch(request))
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
self.addEventListener('message', event => {
|
|
56
|
+
const data = event.data
|
|
57
|
+
switch (data) {
|
|
58
|
+
case 'update':
|
|
59
|
+
updateJson().then(info => {
|
|
60
|
+
// noinspection JSUnresolvedVariable
|
|
61
|
+
event.source.postMessage({
|
|
62
|
+
type: 'update',
|
|
63
|
+
update: info.update,
|
|
64
|
+
version: info.version,
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
break
|
|
68
|
+
default:
|
|
69
|
+
const list = new VersionList()
|
|
70
|
+
list.push(new CacheChangeExpression({'flag': 'all'}))
|
|
71
|
+
deleteCache(list).then(() => {
|
|
72
|
+
if (data === 'refresh')
|
|
73
|
+
event.source.postMessage({type: 'refresh'})
|
|
74
|
+
})
|
|
75
|
+
break
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
/** 忽略浏览器HTTP缓存的请求指定request */
|
|
80
|
+
const fetchNoCache = request => fetch(request, {cache: "no-store"})
|
|
81
|
+
|
|
82
|
+
/** 判断指定url击中了哪一种缓存,都没有击中则返回null */
|
|
83
|
+
function findCache(url) {
|
|
84
|
+
for (let key in cacheList) {
|
|
85
|
+
const value = cacheList[key]
|
|
86
|
+
if (value.match(url)) return value
|
|
87
|
+
}
|
|
88
|
+
return null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 检查连接是否需要重定向至另外的链接,如果需要则返回新的Request,否则返回null<br/>
|
|
93
|
+
* 该函数会顺序匹配{@link replaceList}中的所有项目,即使已经有可用的替换项<br/>
|
|
94
|
+
* 故该函数允许重复替换,例如:<br/>
|
|
95
|
+
* 如果第一个匹配项把链接由"http://abc.com/"改为了"https://abc.com/"<br/>
|
|
96
|
+
* 此时第二个匹配项可以以此为基础继续进行修改,替换为"https://abc.net/"<br/>
|
|
97
|
+
* @return {Request|null}
|
|
98
|
+
*/
|
|
99
|
+
function replaceRequest(request) {
|
|
100
|
+
let url = request.url;
|
|
101
|
+
let flag = false
|
|
102
|
+
for (let key in replaceList) {
|
|
103
|
+
const value = replaceList[key]
|
|
104
|
+
for (let source of value.source) {
|
|
105
|
+
if (url.match(source)) {
|
|
106
|
+
url = url.replace(source, value.dist)
|
|
107
|
+
flag = true
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return flag ? new Request(url) : null
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 根据JSON删除缓存
|
|
116
|
+
* @returns {Promise<boolean>} 返回值用于标记当前页是否被刷新
|
|
117
|
+
*/
|
|
118
|
+
function updateJson() {
|
|
119
|
+
/**
|
|
120
|
+
* 解析elements,并把结果输出到list中
|
|
121
|
+
* @return boolean 是否刷新全站缓存
|
|
122
|
+
*/
|
|
123
|
+
const parseChange = (list, elements, version) => {
|
|
124
|
+
let biliFlag = false
|
|
125
|
+
let result = true
|
|
126
|
+
for (let element of elements) {
|
|
127
|
+
const ver = element['version']
|
|
128
|
+
if (ver === version) {
|
|
129
|
+
result = false
|
|
130
|
+
break
|
|
131
|
+
}
|
|
132
|
+
if (ver.endsWith('b')) biliFlag = true
|
|
133
|
+
const jsonList = element['change']
|
|
134
|
+
if (jsonList) {
|
|
135
|
+
for (let it of jsonList)
|
|
136
|
+
list.push(new CacheChangeExpression(it))
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (biliFlag) list.push(new CacheChangeExpression({flag: "str", value: "/bilibili/"}))
|
|
140
|
+
// resul=true时表明读取了已存在的所有版本信息后依然没有找到客户端当前的版本号
|
|
141
|
+
// 说明跨版本幅度过大,直接清理全站
|
|
142
|
+
return result
|
|
143
|
+
}
|
|
144
|
+
/** 解析字符串 */
|
|
145
|
+
const parseJson = json => new Promise(resolve => {
|
|
146
|
+
/** 版本号读写操作 */
|
|
147
|
+
const dbVersion = {
|
|
148
|
+
write: (id) => new Promise((resolve, reject) => {
|
|
149
|
+
caches.open(CACHE_NAME).then(function (cache) {
|
|
150
|
+
cache.put(
|
|
151
|
+
new Request(VERSION_PATH),
|
|
152
|
+
new Response(id)
|
|
153
|
+
).then(() => resolve())
|
|
154
|
+
}).catch(() => reject())
|
|
155
|
+
}), read: () => new Promise((resolve) => {
|
|
156
|
+
caches.match(new Request(VERSION_PATH))
|
|
157
|
+
.then(response => {
|
|
158
|
+
if (!response) resolve(null)
|
|
159
|
+
response.text().then(text => {
|
|
160
|
+
resolve(text)
|
|
161
|
+
})
|
|
162
|
+
}).catch(() => resolve(null)
|
|
163
|
+
)
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
let list = new VersionList()
|
|
167
|
+
dbVersion.read().then(oldData => {
|
|
168
|
+
const oldVersion = JSON.parse(oldData)
|
|
169
|
+
const elementList = json['info']
|
|
170
|
+
const global = json['global']
|
|
171
|
+
const newVersion = {global: global, local: elementList[0].version}
|
|
172
|
+
//新用户不进行更新操作
|
|
173
|
+
if (!oldVersion) {
|
|
174
|
+
dbVersion.write(JSON.stringify(newVersion))
|
|
175
|
+
return resolve(newVersion)
|
|
176
|
+
}
|
|
177
|
+
const refresh = parseChange(list, elementList, oldVersion.local)
|
|
178
|
+
dbVersion.write(JSON.stringify(newVersion))
|
|
179
|
+
//如果需要清理全站
|
|
180
|
+
if (refresh) {
|
|
181
|
+
if (global === oldVersion.global) {
|
|
182
|
+
list._list.length = 0
|
|
183
|
+
list.push(new CacheChangeExpression({'flag': 'all'}))
|
|
184
|
+
} else list.refresh = true
|
|
185
|
+
}
|
|
186
|
+
resolve({list: list, version: newVersion})
|
|
187
|
+
}).catch(() => dbVersion.write('{"global":0, "local":"-"}'))
|
|
188
|
+
})
|
|
189
|
+
const url = `/update.json` //需要修改JSON地址的在这里改
|
|
190
|
+
return new Promise(resolve => fetchNoCache(url)
|
|
191
|
+
.then(response => {
|
|
192
|
+
if (response.ok || response.status === 301 || response.status === 302)
|
|
193
|
+
response.json().then(json => {
|
|
194
|
+
parseJson(json).then(result => {
|
|
195
|
+
if (!result.list) return resolve({version: result})
|
|
196
|
+
deleteCache(result.list).then(list => resolve({
|
|
197
|
+
update: list.filter(it => it),
|
|
198
|
+
version: result.version
|
|
199
|
+
})
|
|
200
|
+
)
|
|
201
|
+
})
|
|
202
|
+
})
|
|
203
|
+
else console.error(`加载 update.json 时遇到异常,状态码:${response.status}`)
|
|
204
|
+
})
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** 版本列表 */
|
|
209
|
+
class VersionList {
|
|
210
|
+
|
|
211
|
+
_list = []
|
|
212
|
+
refresh = false
|
|
213
|
+
|
|
214
|
+
push(element) {
|
|
215
|
+
this._list.push(element)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
clean(element = null) {
|
|
219
|
+
this._list.length = 0
|
|
220
|
+
if (!element) this.push(element)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
match(url) {
|
|
224
|
+
if (this.refresh) return true
|
|
225
|
+
else {
|
|
226
|
+
for (let it of this._list) {
|
|
227
|
+
if (it.match(url)) return true
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return false
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* 缓存更新匹配规则表达式
|
|
237
|
+
* @param json 格式{"flag": ..., "value": ...}
|
|
238
|
+
* @see https://kmar.top/posts/bcfe8408/#JSON格式
|
|
239
|
+
*/
|
|
240
|
+
class CacheChangeExpression {
|
|
241
|
+
|
|
242
|
+
constructor(json) {
|
|
243
|
+
const checkCache = url => {
|
|
244
|
+
const cache = findCache(new URL(url))
|
|
245
|
+
return !cache || cache.clean
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* 遍历所有value
|
|
249
|
+
* @param action {function(string): boolean} 接受value并返回bool的函数
|
|
250
|
+
* @return {boolean} 如果value只有一个则返回`action(value)`,否则返回所有运算的或运算(带短路)
|
|
251
|
+
*/
|
|
252
|
+
const forEachValues = action => {
|
|
253
|
+
const value = json.value
|
|
254
|
+
if (Array.isArray(value)) {
|
|
255
|
+
for (let it of value) {
|
|
256
|
+
if (action(it)) return true
|
|
257
|
+
}
|
|
258
|
+
return false
|
|
259
|
+
} else return action(value)
|
|
260
|
+
}
|
|
261
|
+
switch (json['flag']) {
|
|
262
|
+
case 'all':
|
|
263
|
+
this.match = checkCache
|
|
264
|
+
break
|
|
265
|
+
case 'post':
|
|
266
|
+
this.match = url => url.endsWith('postsInfo.json') ||
|
|
267
|
+
forEachValues(post => url.endsWith(`posts/${post}/`))
|
|
268
|
+
break
|
|
269
|
+
case 'html':
|
|
270
|
+
this.match = url => cacheList.html.match(new URL(url)) || url.endsWith('postsInfo.json')
|
|
271
|
+
break
|
|
272
|
+
case 'file':
|
|
273
|
+
this.match = url => forEachValues(value => url.endsWith(value))
|
|
274
|
+
break
|
|
275
|
+
case 'new':
|
|
276
|
+
this.match = url => url.endsWith('postsInfo.json') || url.match(/\/archives\//)
|
|
277
|
+
break
|
|
278
|
+
case 'page':
|
|
279
|
+
this.match = url => forEachValues(value => url.match(new RegExp(`\/${value}(\/|)$`)))
|
|
280
|
+
break
|
|
281
|
+
case 'str':
|
|
282
|
+
this.match = url => forEachValues(value => url.includes(value))
|
|
283
|
+
break
|
|
284
|
+
default: throw `未知表达式:${JSON.stringify(json)}`
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
}
|
|
289
|
+
})()
|