观前提示:JS改造计划与本人编写的SW一起食用效果最佳

更新记录

2022/08/13

  • 将手动调用pjax.loadUrl改为调用pjax.refresh
  • 修复多处调用kms.readPostsJson时可能重复发起网络请求的问题

2022/07/31

补充缺少的内容

2022/07/30

发布初版

介绍

  JS改造是指为了配合SW缓存策略而将部分内嵌在HTML文件中的内容改为通过JS生成。

  优劣对比:

  • 将多个页面中的重复数据提取到一个文件中
  • 降低缓存刷新频率
  • 减小HTML文件大小
  • 博文数量达到一定程度时JSON文件体积会非常庞大
  • 不利于SEO
  • 不兼容不支持JS的浏览器

生成 Json

注意:本系列教程均建立在用户已经使用了 abbrlink 的前提下,如果没有使用该插件,请注意修改代码中的相关片段!!!

注:本系列教程假设用户的博文URL路径为/posts/[abbrlink],如果你的URL不是这个格式,请自行修改代码中的相关片段。

  在博客根目录中创建scripts文件夹(已有的话不需要删除原有内容),然后在其中创建posts.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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
const logger = require('hexo-log')()

hexo.extend.generator.register('buildPostJson', async () => {
const resultJson = {config: {}}
const config = hexo.theme.config
const list = hexo.locals.get('posts').data

/** 构建博文的信息 */
const buildAbbrlinkInfo = () => {
const json = {}
for (let post of list) {
const leftIndex = post.cover.indexOf('bg/b') + 4
const rightIndex = post.cover.lastIndexOf('.')
const img = post.cover.substring(leftIndex, rightIndex)
json[post.abbrlink] = {
title: post.title, // 标题
img, // 封面图片
date: post.date, // 发布日期
updated: post.updated, // 更新日期
categories: post.categories.data.map(it => it.name), // 分类列表
tags: post.tags.data.map(it => it.name), // 标签列表
des: post.description // 描述
}
// 如果存在subtitle则写入
if (post.subtitle) json[post.abbrlink].subtitle = post.subtitle
// 如果存在keywords则写入
if (post.keywords) json[post.abbrlink].keywords = post.keywords
}
resultJson.info = json
}

/** 构建相关推荐信息 */
const buildRelatedJsonInfo = () => {
if (!config.related_post.enable) return
resultJson.config.related = config.related_post.date_type
const maxCount = config.related_post.limit
const categories = hexo.locals.get('categories').data
const tags = hexo.locals.get('tags').data
// 查找对象
const findObj = (src, dist) => {
for (let value of src) {
if (value.name === dist) return value.posts.data
}
return []
}
// 获取指定标签的文章列表
const getPostsByTags = tag => {
const result = []
for (let value of findObj(tags, tag.name)) result.push(value)
return result
}
// 获取指定分类的文章列表
const getPostsByCategories = cat => {
const result = []
for (let value of findObj(categories, cat.name)) result.push(value)
return result
}
/**
* 获取和指定文章相关的文章列表,根据有关程度从大到小排序
* @param post
* @return {[{post, value}]} 其中value是有关程度,post是文章对象
*/
const handle = post => {
const map = new Map()
const plusValue = (post, plus = 1) => {
if (map.has(post)) map.set(post, map.get(post) + plus)
else map.set(post, plus)
}
for (let tag of post.tags.data) {
const list = getPostsByTags(tag)
for (let value of list) plusValue(value)
}
for (let cat of post.categories.data) {
const list = getPostsByCategories(cat)
for (let value of list) plusValue(value, 2)
}
const result = []
map.forEach((value, key) => result.push({post: key, value: value}))
result.sort((a, b) => b.value - a.value)
return result
}

for (let post of list) {
const info = handle(post)
const json = []
for (let value of info) {
if (value.post.abbrlink === post.abbrlink) continue
if (json.length === maxCount) break
json.push(value.post.abbrlink.toString())
}
//如果相关推荐数量不够就随机推一些文章上去
for (; json.length !== maxCount;) {
const index = Math.floor(Math.random() * list.length)
const abbrlink = list[index].abbrlink.toString()
if (abbrlink === post.abbrlink.toString() || json.indexOf(abbrlink) > -1) continue
json.push(abbrlink)
}
resultJson.info[post.abbrlink].related = json
}
}

/** 构建公告 */
const buildAnnouncement = () => {
if (config.aside.enable && config.aside.card_announcement.enable)
resultJson.config.doc = config.aside.card_announcement.content
}

const tasks = [buildAbbrlinkInfo, buildRelatedJsonInfo, buildAnnouncement]
await Promise.all(tasks.map(it => new Promise(resolve => resolve(it()))))
logger.info(`文章JSON构建成功(${list.length})`)
return {
path: 'postsInfo.json',
data: JSON.stringify(resultJson)
}
})

文件描述

  该方法构建出的JSON格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"config": {
"related": "[相关推荐日期样式]",
"doc": "[公告]"
},
"info": {
"[博文abbrlink]": {
"title": "[标题]",
"date": "[发布日期]",
"updated": "[更新日期]",
"img": "[封面]",
"categories": ["[分类列表]"],
"tags": ["标签列表"],
"des": "[描述]",
"subtitle": "[副标题]",
"keywords": "[关键字]"
}
}
}

  生成的文件里面包含后面所有功能所需的全部数据,如果有部分功能你没有采用,这里可能会多出一部分数据,可以根据自己需要进行删减。

  目前还不支持配置,如果需要修改json的生成路径的话还请自行更改。

文件读取

  现在我们成功的将文件生成了,现在我们需要在前端加载并读取该文件。

文中需要引入JS时,如果内容以`kms`包围,说明是将代码添加到之前引入的JS中,而非再重新声明一个`kms`,重新声明一个的话会覆盖掉前面声明的内容!

  引入下面的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
const kms = {
/**
* 读取 postsJson
* @return {Promise<*>} json对象
*/
readPostsJson: () => new Promise(async resolve => {
const cache = kms._cache.postsJson
if (cache) {
if (cache.info) return resolve(cache)
else {
const id = setInterval(() => {
const cache = kms._cache.postsJson
if (cache.info) {
clearInterval(id)
resolve(cache)
}
}, 100)
}
} else {
kms._cache.postsJson = {}
const json = kms._cache.postsJson = await (await fetch(`/postsInfo.json`)).json()
const info = json.info
// noinspection JSMismatchedCollectionQueryUpdate
const dateSort = json.dateSort = []
// noinspection JSMismatchedCollectionQueryUpdate
const updatedSort = json.updatedSort = []
for (let abbrlink in info) {
const value = info[abbrlink]
value.img = `https://image.kmar.top/bg/b${value.cover}.jpg`
dateSort.push(abbrlink)
updatedSort.push(abbrlink)
}
dateSort.sort((a, b) => info[a].date < info[b].date ? 1 : -1)
updatedSort.sort((a, b) => info[a].updated < info[b].updated ? 1 : -1)
resolve(json)
}
}),
/**
* 读取文章信息
* @param json JSON对象
* @param abbrlink 博文的`abbrlink`
* @return {JSON} 博文信息
*/
readPostInfo: (json, abbrlink) => json.info[abbrlink]
}

  接下来,当需要访问这个文件的时候直接调用kms.readPostsJson()即可。值得注意的是,外部调用不应当为了减少网络占用而缓存返回结果,因为函数内部已经通过sessionStorage对结果进行了缓存。不过目前我没有找到监听用户主动刷新页面的方法,所以没有添加缓存自动删除的功能,如果有小伙伴知道解决方案的话可以分享一下。

数据收集

  现在我们获取到的数据非常少,还需要再对其进行扩充:

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
const kms = {

/**
* 收集博文中的指定信息
* @param pageNames {string} 名称
* @return {Promise<JSON>} 收集后的结果保存在`json.cache[pageName]`中
*/
collectJInfo: (pageNames) => new Promise(async resolve => {
const json = await kms.readPostsJson()
let flag = false
for (let pageName of pageNames) {
if (json.cache && json.cache[pageName]) continue
flag = true
const list = {}
for (let abbrlink in json.info) {
const info = json.info[abbrlink]
for (let name of info[pageName]) {
const value = list[name] || []
value.push(abbrlink)
list[name] = value
}
}
if (!json.cache) json.cache = {}
json.cache[pageName] = list
}
if (flag) sessionStorage.setItem('postsJson', JSON.stringify(json))
resolve(json)
})

}

代码执行 & pjax兼容

事件

  为了通过异步的方式加载JS,所以对于所有JS代码,我们都选择通过类似于DOMContentLoaded的事件将代码的执行推迟到页面加载完毕后执行。

  没有启用pjax的小伙伴可以直接这么写:

1
2
3
document.addEventListener('DOMContentLoaded', () => {
// 这里写要执行的代码
})

  如果启用了pjax则这么写:

1
2
3
4
5
6
7
8
9
10
(() => {
const task = () => {
// 这里写要执行的代码
}
document.addEventListener('DOMContentLoaded', task)
document.addEventListener('pjax:success', function fun() {
//document.removeEventListener('pjax:loaded', fun)
task()
})
})()

  其中有一行代码被注释了,这行代码的目的是保证事件执行后被删除,也就是在保证不论这段代码执行几次,事件同一时间内只会被注册一次。

  这行代码在代码会在每一次加载页面都会执行或想让代码只在当前页面执行时应当使用,以保证事件的唯一性,下文同理。

  这段话看不懂没关系,后面用到的时候会详细解释。

简易封装

  如果每个地方都这么写未免有些太过费时,所以也可以选择自己发布一个事件,在任意一个可以在任何页面都会加载的JS中添加以下代码:

1
2
3
4
5
document.addEventListener('DOMContentLoaded', () => {
const pushEvent = () => document.dispatchEvent(new Event('kms:loaded'))
pushEvent()
document.addEventListener('pjax:loaded', pushEvent)
})

  接下来,需要执行代码的地方这么写即可:

1
2
3
4
document.addEventListener('kms:loaded', function fun() {
//document.removeEventListener('kms:loaded', fun)
// 这里写要执行的代码
})

  后续教程中,我们都会使用这个事件,没有pjax的小伙伴可以替换为DOMContentLoaded事件。

链接跳转

  没有启用pjax的小伙伴链接跳转时无需做额外处理,但是启用了pjax的小伙伴如果不做额外处理的话我们用js生成的元素用户点击时pjax并不会生效。

  为了让pjax对于我们新加入的链接也有效果,我们需要调用pjax.refresh函数,具体内容见:《Pjax README》