博客搭建-站点信息模块
# 博客搭建-站点信息模块
# 一、概述
站点信息是根据主题作者 (opens new window)推荐,看一位博主 (opens new window)进行搭建,更加具体的信息,请参考前面说的博主文章,主要用来显示博客运行时间访问量等信息,效果如下:
# 二、meta修改
在docs/.vuepress/config.js 下的 head 中添加如下内容:
['meta', { name: 'referrer', content: 'no-referrer-when-downgrade' }],
// head 部分完整内容
head: [
['link', { rel: 'icon', href: '/img/favicon.ico' }], //favicons,资源放在public文件夹
['meta', { name: 'keywords',content: '运维博客,个人技术博客,云原生,运维开发,技术文档,学习,面试,Kubertenes,Docker,Linux,ddevops,vue,python,go',},],
['meta', { name: 'baidu-site-verification', content: '7F55weZDDc' }], // 百度统计的站长验证(你可以去掉)
['meta', { name: 'theme-color', content: '#11a8cd' }], // 移动浏览器主题颜色
['meta', { name: 'referrer', content: 'no-referrer-when-downgrade' }],
],
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
# 三、添加在线图标
在docs/.vuepress/config.js 下的 head 中添加如下内容:
['link', { rel: 'stylesheet', href: 'https://at.alicdn.com/t/font_3077305_pt8umhrn4k9.css' }]
1
2
2
# 四、站点信息目录创建
创建docs/.vuepress/webSiteInfo 目录
在webSiteInfo目录下创建busuanzi.js,该文件用来获取访问量。
var bszCaller, bszTag, scriptTag, ready;
var t,
e,
n,
a = !1,
c = [];
// 修复Node同构代码的问题
if (typeof document !== "undefined") {
(ready = function (t) {
return (
a ||
"interactive" === document.readyState ||
"complete" === document.readyState
? t.call(document)
: c.push(function () {
return t.call(this);
}),
this
);
}),
(e = function () {
for (var t = 0, e = c.length; t < e; t++) c[t].apply(document);
c = [];
}),
(n = function () {
a ||
((a = !0),
e.call(window),
document.removeEventListener
? document.removeEventListener("DOMContentLoaded", n, !1)
: document.attachEvent &&
(document.detachEvent("onreadystatechange", n),
window == window.top && (clearInterval(t), (t = null))));
}),
document.addEventListener
? document.addEventListener("DOMContentLoaded", n, !1)
: document.attachEvent &&
(document.attachEvent("onreadystatechange", function () {
/loaded|complete/.test(document.readyState) && n();
}),
window == window.top &&
(t = setInterval(function () {
try {
a || document.documentElement.doScroll("left");
} catch (t) {
return;
}
n();
}, 5)));
}
bszCaller = {
fetch: function (t, e) {
var n = "BusuanziCallback_" + Math.floor(1099511627776 * Math.random());
t = t.replace("=BusuanziCallback", "=" + n);
(scriptTag = document.createElement("SCRIPT")),
(scriptTag.type = "text/javascript"),
(scriptTag.defer = !0),
(scriptTag.src = t),
document.getElementsByTagName("HEAD")[0].appendChild(scriptTag);
window[n] = this.evalCall(e);
},
evalCall: function (e) {
return function (t) {
ready(function () {
try {
e(t),
scriptTag &&
scriptTag.parentElement &&
scriptTag.parentElement.removeChild &&
scriptTag.parentElement.removeChild(scriptTag);
} catch (t) {
console.log(t), bszTag.hides();
}
});
};
},
};
bszTag = {
bszs: ["site_pv", "page_pv", "site_uv"],
texts: function (n) {
this.bszs.map(function (t) {
var e = document.getElementById("busuanzi_value_" + t);
e && (e.innerHTML = n[t]);
});
},
hides: function () {
this.bszs.map(function (t) {
var e = document.getElementById("busuanzi_container_" + t);
e && (e.style.display = "none");
});
},
shows: function () {
this.bszs.map(function (t) {
var e = document.getElementById("busuanzi_container_" + t);
e && (e.style.display = "inline");
});
},
};
export default () => {
bszTag && bszTag.hides();
bszCaller.fetch("//busuanzi.ibruce.info/busuanzi?jsonpCallback=BusuanziCallback", function (t) {
bszTag.texts(t), bszTag.shows();
})
};
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
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
- 创建
readFile.ts
文件,这个文件用于 统计文章数目 和 网站总字数 等
import fs from 'fs'; // 文件模块
import path from 'path'; // 路径模块
import matter from 'gray-matter'; // FrontMatter解析器 https://github.com/jonschlinkert/gray-matter
import chalk from 'chalk' // 命令行打印美化
const log = console.log
const docsRoot = path.join(__dirname, '..', '..', '..', 'docs'); // docs文件路径
/**
* 获取本站的文章数据
* 获取所有的 md 文档,可以排除指定目录下的文档
*/
function readFileList(excludeFiles: Array<string> = [''], dir: string = docsRoot, filesList: Array<Object> = []) {
const files = fs.readdirSync(dir);
files.forEach((item, index) => {
let filePath = path.join(dir, item);
const stat = fs.statSync(filePath);
if (!(excludeFiles instanceof Array)) {
log(chalk.yellow(`error: 传入的参数不是一个数组。`))
}
excludeFiles.forEach((excludeFile) => {
if (stat.isDirectory() && item !== '.vuepress' && item !== '@pages' && item !== excludeFile) {
readFileList(excludeFiles, path.join(dir, item), filesList); //递归读取文件
} else {
if (path.basename(dir) !== 'docs') { // 过滤 docs目录级下的文件
const fileNameArr = path.basename(filePath).split('.')
let name = null, type = null;
if (fileNameArr.length === 2) { // 没有序号的文件
name = fileNameArr[0]
type = fileNameArr[1]
} else if (fileNameArr.length === 3) { // 有序号的文件
name = fileNameArr[1]
type = fileNameArr[2]
} else { // 超过两个‘.’的
log(chalk.yellow(`warning: 该文件 "${filePath}" 没有按照约定命名,将忽略生成相应数据。`))
return
}
if (type === 'md') { // 过滤非 md 文件
filesList.push({
name,
filePath
});
}
}
}
});
});
return filesList;
}
/**
* 获取本站的文章总字数
* 可以排除某个目录下的 md 文档字数
*/
function readTotalFileWords(excludeFiles = ['']) {
const filesList = readFileList(excludeFiles);
let wordCount = 0;
filesList.forEach((item: any) => {
const content = getContent(item.filePath);
let len = counter(content);
wordCount += len[0] + len[1];
});
if (wordCount < 1000) {
return wordCount;
}
return Math.round(wordCount / 100) / 10 + 'k';
}
/**
* 获取每一个文章的字数
* 可以排除某个目录下的 md 文档字数
*/
function readEachFileWords(excludeFiles: Array<string> = [''], cn: number, en: number) {
const filesListWords = [];
const filesList = readFileList(excludeFiles);
filesList.forEach((item: any) => {
const content = getContent(item.filePath);
let len = counter(content);
// 计算预计的阅读时间
let readingTime = readTime(len, cn, en);
let wordsCount: any = 0;
wordsCount = len[0] + len[1];
if (wordsCount >= 1000) {
wordsCount = Math.round(wordsCount / 100) / 10 + 'k';
}
// fileMatterObj => {content:'剔除frontmatter后的文件内容字符串', data:{<frontmatter对象>}, ...}
const fileMatterObj = matter(content, {});
const matterData = fileMatterObj.data;
filesListWords.push({ ...item, wordsCount, readingTime, ...matterData });
});
return filesListWords;
}
/**
* 计算预计的阅读时间
*/
function readTime(len: Array<number>, cn: number = 300, en: number = 160) {
let readingTime = len[0] / cn + len[1] / en;
if (readingTime > 60 && readingTime < 60 * 24) { // 大于一个小时,小于一天
let hour = Math.trunc(readingTime / 60);
let minute = Math.trunc(readingTime - hour * 60);
if (minute === 0) {
return hour + 'h';
}
return hour + 'h' + minute + 'm';
} else if (readingTime > 60 * 24) { // 大于一天
let day = Math.trunc(readingTime / (60 * 24));
let hour = Math.trunc((readingTime - day * 24 * 60) / 60);
if (hour === 0) {
return day + 'd';
}
return day + 'd' + hour + 'h';
}
return readingTime < 1 ? '1' : Math.trunc(readingTime * 10) / 10 + 'm'; // 取一位小数
}
/**
* 读取文件内容
*/
function getContent(filePath: string) {
return fs.readFileSync(filePath, 'utf8');
}
/**
* 获取文件内容的字数
* cn:中文
* en:一整句英文(没有空格隔开的英文为 1 个)
*/
function counter(content: string) {
const cn = (content.match(/[\u4E00-\u9FA5]/g) || []).length;
const en = (content.replace(/[\u4E00-\u9FA5]/g, '').match(/[a-zA-Z0-9_\u0392-\u03c9\u0400-\u04FF]+|[\u4E00-\u9FFF\u3400-\u4dbf\uf900-\ufaff\u3040-\u309f\uac00-\ud7af\u0400-\u04FF]+|[\u00E4\u00C4\u00E5\u00C5\u00F6\u00D6]+|\w+/g) || []).length;
return [cn, en];
}
export {
readFileList,
readTotalFileWords,
readEachFileWords,
}
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
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
- 创建第三个文件
utils.js
,该文件用于计算 已运行时间 和 最后活动时间。
// 日期格式化(只获取年月日)
export function dateFormat(date) {
if (!(date instanceof Date)) {
date = new Date(date);
}
return `${date.getUTCFullYear()}-${zero(date.getUTCMonth() + 1)}-${zero(date.getUTCDate())}`;
}
// 小于10补0
export function zero(d) {
return d.toString().padStart(2, '0');
}
/**
* 计算最后活动时间
*/
export function lastUpdatePosts(posts) {
posts.sort((prev, next) => {
return compareDate(prev, next);
});
return posts;
}
// 获取时间的时间戳
export function getTimeNum(post) {
let dateStr = post.lastUpdated || post.frontmatter.date;
let date = new Date(dateStr);
if (date == "Invalid Date" && dateStr) { // 修复new Date()在Safari下出现Invalid Date的问题
date = new Date(dateStr.replace(/-/g, '/'));
}
return date.getTime();
}
// 比对时间
export function compareDate(a, b) {
return getTimeNum(b) - getTimeNum(a);
}
/**
* 获取两个日期相差多少天
*/
export function dayDiff(startDate, endDate) {
if (!endDate) {
endDate = startDate;
startDate = new Date();
}
startDate = dateFormat(startDate);
endDate = dateFormat(endDate);
let day = parseInt(Math.abs(new Date(startDate) - new Date(endDate)) / (1000 * 60 * 60 * 24));
return day;
}
/**
* 计算相差多少年/月/日/时/分/秒
*/
export function timeDiff(startDate, endDate) {
if (!endDate) {
endDate = startDate;
startDate = new Date();
}
if (!(startDate instanceof Date)) {
startDate = new Date(startDate);
}
if (!(endDate instanceof Date)) {
endDate = new Date(endDate);
}
// 计算时间戳的差
const diffValue = parseInt((Math.abs(endDate - startDate) / 1000));
if (diffValue == 0) {
return '刚刚';
} else if (diffValue < 60) {
return diffValue + ' 秒';
} else if (parseInt(diffValue / 60) < 60) {
return parseInt(diffValue / 60) + ' 分';
} else if (parseInt(diffValue / (60 * 60)) < 24) {
return parseInt(diffValue / (60 * 60)) + ' 时';
} else if (parseInt(diffValue / (60 * 60 * 24)) < getDays(startDate.getMonth, startDate.getFullYear)) {
return parseInt(diffValue / (60 * 60 * 24)) + ' 天';
} else if (parseInt(diffValue / (60 * 60 * 24 * getDays(startDate.getMonth, startDate.getFullYear))) < 12) {
return parseInt(diffValue / (60 * 60 * 24 * getDays(startDate.getMonth, startDate.getFullYear))) + ' 月';
} else {
return parseInt(diffValue / (60 * 60 * 24 * getDays(startDate.getMonth, startDate.getFullYear) * 12)) + ' 年';
}
}
/**
* 判断当前月的天数(28、29、30、31)
*/
export function getDays(mouth, year) {
let days = 30;
if (mouth === 2) {
days = year % 4 === 0 ? 29 : 28;
} else if (mouth === 1 || mouth === 3 || mouth === 5 || mouth === 7 || mouth === 8 || mouth === 10 || mouth === 12) {
// 月份为:1,3,5,7,8,10,12 时,为大月.则天数为 31;
days = 31;
}
return days;
}
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
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
# 五、站点信息代码组件配置
在docs/.vuepress/components目录下创建WebInfo.vue,这就是站点信息模块。
<template>
<!-- Young Kbt -->
<div class="web-info card-box">
<div class="webinfo-title">
<i
class="iconfont icon-award"
style="font-size: 0.875rem; font-weight: 900; width: 1.25em"
></i>
<span>站点信息</span>
</div>
<div class="webinfo-item">
<div class="webinfo-item-title">文章数目:</div>
<div class="webinfo-content">{{ mdFileCount }} 篇</div>
</div>
<div class="webinfo-item">
<div class="webinfo-item-title">已运行时间:</div>
<div class="webinfo-content">
{{ createToNowDay != 0 ? createToNowDay + " 天" : "不到一天" }}
</div>
</div>
<div class="webinfo-item">
<div class="webinfo-item-title">本站总字数:</div>
<div class="webinfo-content">{{ totalWords }} 字</div>
</div>
<div class="webinfo-item">
<div class="webinfo-item-title">最后活动时间:</div>
<div class="webinfo-content">
{{ lastActiveDate == "刚刚" ? "刚刚" : lastActiveDate + "前" }}
</div>
</div>
<div v-if="indexView" class="webinfo-item">
<div class="webinfo-item-title">本站被访问了:</div>
<div class="webinfo-content">
<span id="busuanzi_value_site_pv" class="web-site-pv"
><i title="正在获取..." class="loading iconfont icon-loading"></i>
</span>
次
</div>
</div>
<div v-if="indexView" class="webinfo-item">
<div class="webinfo-item-title">您的访问排名:</div>
<div class="webinfo-content busuanzi">
<span id="busuanzi_value_site_uv" class="web-site-uv"
><i title="正在获取..." class="loading iconfont icon-loading"></i>
</span>
名
</div>
</div>
</div>
</template>
<script>
import { dayDiff, timeDiff, lastUpdatePosts } from "../webSiteInfo/utils";
import fetch from "../webSiteInfo/busuanzi"; // 统计量
export default {
data() {
return {
// Young Kbt
mdFileCount: 0, // markdown 文档总数
createToNowDay: 0, // 博客创建时间距今多少天
lastActiveDate: "", // 最后活动时间
totalWords: 0, // 本站总字数
indexView: true, // 开启访问量和排名统计
};
},
computed: {
$lastUpdatePosts() {
return lastUpdatePosts(this.$filterPosts);
},
},
mounted() {
// Young Kbt
if (Object.keys(this.$themeConfig.blogInfo).length > 0) {
const {
blogCreate,
mdFileCountType,
totalWords,
moutedEvent,
eachFileWords,
indexIteration,
indexView,
} = this.$themeConfig.blogInfo;
this.createToNowDay = dayDiff(blogCreate);
if (mdFileCountType != "archives") {
this.mdFileCount = mdFileCountType.length;
} else {
this.mdFileCount = this.$filterPosts.length;
}
if (totalWords == "archives" && eachFileWords) {
let archivesWords = 0;
eachFileWords.forEach((itemFile) => {
if (itemFile.wordsCount < 1000) {
archivesWords += itemFile.wordsCount;
} else {
let wordsCount = itemFile.wordsCount.slice(
0,
itemFile.wordsCount.length - 1
);
archivesWords += wordsCount * 1000;
}
});
this.totalWords = Math.round(archivesWords / 100) / 10 + "k";
} else if (totalWords == "archives") {
this.totalWords = 0;
console.log(
"如果 totalWords = 'archives',必须传入 eachFileWords,显然您并没有传入!"
);
} else {
this.totalWords = totalWords;
}
// 最后一次活动时间
this.lastActiveDate = timeDiff(this.$lastUpdatePosts[0].lastUpdated);
this.mountedWebInfo(moutedEvent);
// 获取访问量和排名
this.indexView = indexView == undefined ? true : indexView;
if (this.indexView) {
this.getIndexViewCouter(indexIteration);
}
}
},
methods: {
/**
* 挂载站点信息模块
*/
mountedWebInfo(moutedEvent = ".tags-wrapper") {
let interval = setInterval(() => {
const tagsWrapper = document.querySelector(moutedEvent);
const webInfo = document.querySelector(".web-info");
if (tagsWrapper && webInfo) {
if (!this.isSiblilngNode(tagsWrapper, webInfo)) {
tagsWrapper.parentNode.insertBefore(
webInfo,
tagsWrapper.nextSibling
);
clearInterval(interval);
}
}
}, 200);
},
/**
* 挂载在兄弟元素后面,说明当前组件是 siblingNode 变量
*/
isSiblilngNode(element, siblingNode) {
if (element.siblingNode == siblingNode) {
return true;
} else {
return false;
}
},
/**
* 首页的统计量
*/
getIndexViewCouter(iterationTime = 3000) {
fetch();
var i = 0;
var defaultCouter = "9999";
// 如果只需要第一次获取数据(可能获取失败),可注释掉 setTimeout 内容,此内容是第一次获取失败后,重新获取访问量
// 可能会导致访问量再次 + 1 原因:取决于 setTimeout 的时间(需求调节),setTimeout 太快导致第一个获取的数据没返回,就第二次获取,导致结果返回 + 2 的数据
setTimeout(() => {
let indexUv = document.querySelector(".web-site-pv");
let indexPv = document.querySelector(".web-site-uv");
if (
indexPv &&
indexUv &&
indexPv.innerText == "" &&
indexUv.innerText == ""
) {
let interval = setInterval(() => {
// 再次判断原因:防止进入 setInterval 的瞬间,访问量获取成功
if (
indexPv &&
indexUv &&
indexPv.innerText == "" &&
indexUv.innerText == ""
) {
i += iterationTime;
if (i > iterationTime * 5) {
indexPv.innerText = defaultCouter;
indexUv.innerText = defaultCouter;
clearInterval(interval); // 5 次后无法获取,则取消获取
}
if (indexPv.innerText == "" && indexUv.innerText == "") {
// 手动获取访问量
fetch();
} else {
clearInterval(interval);
}
} else {
clearInterval(interval);
}
}, iterationTime);
// 绑定 beforeDestroy 生命钩子,清除定时器
this.$once("hook:beforeDestroy", () => {
clearInterval(interval);
interval = null;
});
}
}, iterationTime);
},
beforeMount() {
let webInfo = document.querySelector(".web-info");
webInfo && webInfo.parentNode.removeChild(webInfo);
},
},
};
</script>
<style scoped>
.web-info {
font-size: 0.875rem;
padding: 0.95rem;
}
.webinfo-title {
text-align: center;
color: #888;
font-weight: bold;
padding: 0 0 10px 0;
}
.webinfo-item {
padding: 8px 0 0;
margin: 0;
}
.webinfo-item-title {
display: inline-block;
}
.webinfo-content {
display: inline-block;
float: right;
}
@keyframes turn {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.loading {
display: inline-block;
animation: turn 1s linear infinite;
-webkit-animation: turn 1s linear infinite;
}
</style>
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
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
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
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
- 创建一个 vue 文件:
PageInfo.vue
,这就是文章页的信息模块:文章浏览量、字数代码、预阅读时间
<template></template>
<script>
import fetch from "../webSiteInfo/busuanzi";
export default {
mounted() {
// 首页不初始页面信息
if (this.$route.path != "/") {
this.initPageInfo();
}
},
watch: {
$route(to, from) {
// 如果页面是非首页,# 号也会触发路由变化,这里要排除掉
if (
to.path !== "/" &&
to.path !== from.path &&
this.$themeConfig.blogInfo
) {
this.initPageInfo();
}
},
},
methods: {
/**
* 初始化页面信息
*/
initPageInfo() {
if (this.$frontmatter.article == undefined || this.$frontmatter.article) {
// 排除掉 article 为 false 的文章
const { eachFileWords, pageView, pageIteration, readingTime } =
this.$themeConfig.blogInfo;
// 下面两个 if 可以调换位置,从而让文章的浏览量和字数交换位置
if (eachFileWords) {
try {
eachFileWords.forEach((itemFile) => {
if (itemFile.permalink == this.$frontmatter.permalink) {
// this.addPageWordsCount 和 if 可以调换位置,从而让文章的字数和预阅读时间交换位置
this.addPageWordsCount(itemFile.wordsCount);
if (readingTime || readingTime == undefined) {
this.addReadTimeCount(itemFile.readingTime);
}
throw new Error();
}
});
} catch (error) {}
}
if (pageView || pageView == undefined) {
this.addPageView();
this.getPageViewCouter(pageIteration);
}
return;
}
},
/**
* 文章页的访问量
*/
getPageViewCouter(iterationTime = 3000) {
fetch();
let i = 0;
var defaultCouter = "9999";
// 如果只需要第一次获取数据(可能获取失败),可注释掉 setTimeout 内容,此内容是第一次获取失败后,重新获取访问量
// 可能会导致访问量再次 + 1 原因:取决于 setTimeout 的时间(需求调节),setTimeout 太快导致第一个获取的数据没返回,就第二次获取,导致结果返回 + 2 的数据
setTimeout(() => {
let pageView = document.querySelector(".view-data");
if (pageView && pageView.innerText == "") {
let interval = setInterval(() => {
// 再次判断原因:防止进入 setInterval 的瞬间,访问量获取成功
if (pageView && pageView.innerText == "") {
i += iterationTime;
if (i > iterationTime * 5) {
pageView.innerText = defaultCouter;
clearInterval(interval); // 5 次后无法获取,则取消获取
}
if (pageView.innerText == "") {
// 手动获取访问量
fetch();
} else {
clearInterval(interval);
}
} else {
clearInterval(interval);
}
}, iterationTime);
// 绑定 beforeDestroy 生命钩子,清除定时器
this.$once("hook:beforeDestroy", () => {
clearInterval(interval);
interval = null;
});
}
}, iterationTime);
},
/**
* 添加浏览量元素
*/
addPageView() {
let pageView = document.querySelector(".page-view");
if (pageView) {
pageView.innerHTML =
'<a style="color: #888; margin-left: 3px" href="javascript:;" id="busuanzi_value_page_pv" class="view-data"><i title="正在获取..." class="loading iconfont icon-loading"></i></a>';
} else {
// 创建访问量的元素
let template = document.createElement("div");
template.title = "浏览量";
template.className = "page-view iconfont icon-view";
template.style.float = "left";
template.style.marginLeft = "20px";
template.style.fontSize = "0.8rem";
template.innerHTML =
'<a style="color: #888; margin-left: 3px" href="javascript:;" id="busuanzi_value_page_pv" class="view-data"><i title="正在获取..." class="loading iconfont icon-loading"></i></a>';
// 添加 loading 效果
let style = document.createElement("style");
style.innerHTML = `@keyframes turn {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.loading {
display: inline-block;
animation: turn 1s linear infinite;
-webkit-animation: turn 1s linear infinite;
}`;
document.head.appendChild(style);
this.mountedView(template);
}
},
/**
* 添加当前文章页的字数元素
*/
addPageWordsCount(wordsCount = 0) {
let words = document.querySelector(".book-words");
if (words) {
words.innerHTML = `<a href="javascript:;" style="margin-left: 3px; color: #888">${wordsCount}</a>`;
} else {
let template = document.createElement("div");
template.title = "文章字数";
template.className = "book-words iconfont icon-book";
template.style.float = "left";
template.style.marginLeft = "20px";
template.style.fontSize = "0.8rem";
template.innerHTML = `<a href="javascript:;" style="margin-left: 3px; color: #888">${wordsCount}</a>`;
this.mountedView(template);
}
},
/**
* 添加预计的阅读时间
*/
addReadTimeCount(readTimeCount = 0) {
let reading = document.querySelector(".reading-time");
if (reading) {
reading.innerHTML = `<a href="javascript:;" style="margin-left: 3px; color: #888">${readTimeCount}</a>`;
} else {
let template = document.createElement("div");
template.title = "预阅读时长";
template.className = "reading-time iconfont icon-shijian";
template.style.float = "left";
template.style.marginLeft = "20px";
template.style.fontSize = "0.8rem";
template.innerHTML = `<a href="javascript:;" style="margin-left: 3px; color: #888">${readTimeCount}</a>`;
this.mountedView(template);
}
},
/**
* 挂载目标到页面上
*/
mountedView(
template,
mountedIntervalTime = 100,
moutedParentEvent = ".articleInfo-wrap > .articleInfo > .info"
) {
let i = 0;
let parentElement = document.querySelector(moutedParentEvent);
if (parentElement) {
if (!this.isMountedView(template, parentElement)) {
parentElement.appendChild(template);
}
} else {
let interval = setInterval(() => {
parentElement = document.querySelector(moutedParentEvent);
if (parentElement) {
if (!this.isMountedView(template, parentElement)) {
parentElement.appendChild(template);
clearInterval(interval);
}
} else if (i > 1 * 10) {
// 10 秒后清除
clearInterval(interval);
}
}, mountedIntervalTime);
// 绑定 beforeDestroy 生命钩子,清除定时器
this.$once("hook:beforeDestroy", () => {
clearInterval(interval);
interval = null;
});
}
},
/**
* 如果元素存在,则删除
*/
removeElement(selector) {
var element = document.querySelector(selector);
element && element.parentNode.removeChild(element);
},
/**
* 目标是否已经挂载在页面上
*/
isMountedView(element, parentElement) {
if (element.parentNode == parentElement) {
return true;
} else {
return false;
}
},
},
// 防止重写编译时,导致页面信息重复出现问题
beforeMount() {
clearInterval(this.interval);
this.removeElement(".page-view");
this.removeElement(".book-words");
this.removeElement(".reading-time");
},
};
</script>
<style></style>
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
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
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
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# 六、站点信息代码组件注册
打开 docs/index.md,添加以下内容
<ClientOnly>
<WebInfo/>
</ClientOnly>
1
2
3
2
3
# 七、使用PageInfo.vue 组件
在 docs/.vuepress/config.ts 的 plugins 中添加配置。
[
{
name: 'custom-plugins',
globalUIComponents: ["PageInfo"] // 2.x 版本 globalUIComponents 改名为 clientAppRootComponentFiles
},
],
1
2
3
4
5
6
7
2
3
4
5
6
7
# 八、站点信息配置
在docs/.vuepress/config/themeConfig.ts文件中导入,并使用组件。
注意
这里之所以在themeConfig.ts文件中配置,是因为之前已经把文件按照模块分离,具体请《博客搭建-目录调整 (opens new window)》。
import { readFileList, readTotalFileWords, readEachFileWords } from '../webSiteInfo/readFile';
// 站点配置(首页 & 文章页)
blogInfo: {
blogCreate: '2023-02-01', // 博客创建时间
indexView: true, // 开启首页的访问量和排名统计,默认 true(开启)
pageView: true, // 开启文章页的浏览量统计,默认 true(开启)
readingTime: true, // 开启文章页的预计阅读时间,条件:开启 eachFileWords,默认 true(开启)。可在 eachFileWords 的 readEachFileWords 的第二个和第三个参数自定义,默认 1 分钟 300 中文、160 英文
eachFileWords: readEachFileWords([''], 300, 160), // 开启每个文章页的字数。readEachFileWords(['xx']) 关闭 xx 目录(可多个,可不传参数)下的文章页字数和阅读时长,后面两个参数分别是 1 分钟里能阅读的中文字数和英文字数。无默认值。readEachFileWords() 方法默认排除了 article 为 false 的文章
mdFileCountType: 'archives', // 开启文档数。1. archives 获取归档的文档数(默认)。2. 数组 readFileList(['xx']) 排除 xx 目录(可多个,可不传参数),获取其他目录的文档数。提示:readFileList() 获取 docs 下所有的 md 文档(除了 `.vuepress` 和 `@pages` 目录下的文档)
totalWords: 'archives', // 开启本站文档总字数。1. archives 获取归档的文档数(使用 archives 条件:传入 eachFileWords,否则报错)。2. readTotalFileWords(['xx']) 排除 xx 目录(可多个,可不传参数),获取其他目录的文章字数。无默认值
moutedEvent: '.tags-wrapper', // 首页的站点模块挂载在某个元素后面(支持多种选择器),指的是挂载在哪个兄弟元素的后面,默认是热门标签 '.tags-wrapper' 下面,提示:'.categories-wrapper' 会挂载在文章分类下面。'.blogger-wrapper' 会挂载在博客头像模块下面
// 下面两个选项:第一次获取访问量失败后的迭代时间
indexIteration: 2500, // 如果首页获取访问量失败,则每隔多少时间后获取一次访问量,直到获取成功或获取 10 次后。默认 3 秒。注意:设置时间太低,可能导致访问量 + 2、+ 3 ......
pageIteration: 2500, // 如果文章页获取访问量失败,则每隔多少时间后获取一次访问量,直到获取成功或获取 10 次后。默认 3 秒。注意:设置时间太低,可能导致访问量 + 2、+ 3 ......
// 说明:成功获取一次访问量,访问量 + 1,所以第一次获取失败后,设置的每个隔段重新获取时间,将会影响访问量的次数。如 100 可能每次获取访问量 + 3
},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
编辑 (opens new window)
上次更新: 2023/02/21, 14:35:28