配置站点信息
配置站点信息
目录
[toc]
版权声明
警告
本着开源共享、共同学习的精神:
本文是在 博主《youngkbt》 文章:《本站 - 站点信息模块》https://notes.youngkbt.cn/about/website/info 基础上增加了一些自己的实际操作记录和修改,内容依旧属于原作者《youngkbt》 所有。转载无需和我联系,但请注明文章来源。如果侵权之处,请联系博主进行删除,谢谢~(这里万分感谢原作者的优质文章😜,感谢开源,拥抱开源💖)
本人测试环境
2024年12月26日测试
2024年12月23日从官方拉取的项目:
基于官方https://github.com/xugaoyi/vuepress-theme-vdoing
搭建的仓库。
前言
本内容介绍如何搭建本站首页的站点信息,以及每篇文章的浏览量统计。
本内容将在首页和每篇的文章页加入了一些元素,目前适用版本是 Vdoing v1.x。
如果你想集成到其他 Vuepress 主题,那么要添加卡片样式,修改挂载元素即可(建议先按照步骤完成一次再考虑集成)。
- 为什么添加卡片样式?本模块的站点信息是基于 Vdoing 自带的卡片样式,模块并没有添加任何卡片样式,所以想集成到其他主题,则需要参考 Vdoing 卡片样式进行添加,或者按照自己喜欢的样式进行添加
- 为什么修改挂载元素?本模块的挂载元素是基于 Vdoing 标签提供的 class 或 id,而其他主题的标签不一样,所以自行进行调试
本模块的所有 功能 支持大部分 Vuepress 主题,但是如何将所有功能展示到其他主题页面合适的地方,以及展示的样式等 DOM 技术,需要自己适配。
效果如下:
本站的访问量和文章的浏览量使用了 不蒜子,本地启动的 localhost 有很多人访问过,但无需担心实际部署后的访问量。
NOTE
注意
问题:本模块目前有一个功能依赖于 git 的
lastUpdated
功能,该功能已经内置 Vuepress,所以无需担心,唯一值得注意的是:在本地添加了新的文件,最后活动时间的数据可能为NaN
(无法获取的意思)。解决:只需要在博客项目部署的过程中执行
git commit
命令,因为该命令将会获取一个准确的时间代替NaN
,给本模块使用。2022-01-17 @Young Kbt
添加meta
为什么添加 meta 头信息呢,因为在 Chrome 85 版本中,为了保护用户的隐私,默认的 Referrer Policy 则变成了 strict-origin-when-cross-origin
。
所以必须添加 meta,否则文章统计访问量的数据则不正确。
在 docs/.vuepress/config.js 下的 head 中添加如下内容:
['meta', { name: 'referrer', content: 'no-referrer-when-downgrade' }],
如图:
自己配置:
添加在线图标
这里使用的是阿里矢量库。
地址:https://www.iconfont.cn/(opens new window)
添加了五个图标
如果你不想使用自己的矢量库项目(不害怕我删图标跑路🤣),那么可以使用我的图标项目网址,当你发现图标失效了,就请来这里获取新的地址,如果还没有更新,请在评论区留言。
在 config.js 下的 head 中文件添加如下内容:
['link', { rel: 'stylesheet', href: 'https://at.alicdn.com/t/font_3077305_pt8umhrn4k9.css' }]
如图:(图片的内容不一定是最新的,以上方代码块为准)
自己配置:
Vue模板
这里先提供一个在 Vue 里常用的模板代码,即通用代码(了解即可):
<template>
<div class="busuanzi">
<span id="busuanzi_container_site_pv" style="display:none">
本站总访问量
<span id="busuanzi_value_site_pv"></span>次
<span class="post-meta-divider">|</span>
</span>
<span id="busuanzi_container_site_uv" style="display:none">
本站访客数
<span id="busuanzi_value_site_uv"></span>人
</span>
</div>
</template>
<script>
let script;
export default {
mounted() {
script = require("busuanzi.pure.js");
},
// 监听,当路由发生变化的时候执行
watch: {
$route(to, from) {
if (to.path != from.path) {
script.fetch();
}
}
}
};
</script>
主题选择
下面有两种配置方式可以选,分别为:
- 在线主题:NPM 主题,采用监听路由、插入式的代码
- 本地主题:站点信息模块与页面一起渲染出来,没有延迟
本地主题不好的一点就是版本升级后曾修改的内容被重置,所以需要记好修改位置、备份,比较麻烦。好处是根据自己的需求在基础上拓展。
在线主题具有通用性,即在任意环境(如本地主题)都有效果。
本次个人使用在线主题配置方式。😜
在线主题
建议:本内容代码块比较长,可以点击代码块的右侧箭头来折叠,然后点击复制图标进行复制即可。
不管使不使用本地主题,都可以配置在线主题的站点模块。
网站信息工具代码
添加网站信息需要的计算代码、获取字数代码等工具类。
首先进入 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();
})
};
然后创建 readFile.js
或者 readFile.ts
文件,这个文件用于 统计文章数目 和 网站总字数 等。
添加如下内容:
注意:本次个人使用ts方式。
js
const fs = require('fs'); // 文件模块
const path = require('path'); // 路径模块
const matter = require('gray-matter'); // FrontMatter解析器 https://github.com/jonschlinkert/gray-matter
const chalk = require('chalk') // 命令行打印美化
const log = console.log
const docsRoot = path.join(__dirname, '..', '..', '..', 'docs'); // docs文件路径
/**
* 获取本站的文章数据
* 获取所有的 md 文档,可以排除指定目录下的文档
*/
function readFileList(excludeFiles = [''], dir = docsRoot, filesList = []) {
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);
var wordCount = 0;
filesList.forEach((item) => {
const content = getContent(item.filePath);
var len = counter(content);
wordCount += len[0] + len[1];
});
if (wordCount < 1000) {
return wordCount;
}
return Math.round(wordCount / 100) / 10 + 'k';
}
/**
* 获取每一个文章的字数
* 可以排除某个目录下的 md 文档字数
*/
function readEachFileWords(excludeFiles = [''], cn, en) {
const filesListWords = [];
const filesList = readFileList(excludeFiles);
filesList.forEach((item) => {
const content = getContent(item.filePath);
var len = counter(content);
// 计算预计的阅读时间
var readingTime = readTime(len, cn, en);
var wordsCount = 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, cn = 300, en = 160) {
var readingTime = len[0] / cn + len[1] / en;
if (readingTime > 60 && readingTime < 60 * 24) { // 大于一个小时,小于一天
let hour = parseInt(readingTime / 60);
let minute = parseInt((readingTime - hour * 60));
if (minute === 0) {
return hour + 'h';
}
return hour + 'h' + minute + 'm';
} else if (readingTime > 60 * 24) { // 大于一天
let day = parseInt(readingTime / (60 * 24));
let hour = parseInt((readingTime - day * 24 * 60) / 60);
if (hour === 0) {
return day + 'd';
}
return day + 'd' + hour + 'h';
}
return readingTime < 1 ? '1' : parseInt((readingTime * 10)) / 10 + 'm'; // 取一位小数
}
/**
* 读取文件内容
*/
function getContent(filePath) {
return fs.readFileSync(filePath, 'utf8');
}
/**
* 获取文件内容的字数
* cn:中文
* en:一整句英文(没有空格隔开的英文为 1 个)
*/
function counter(content) {
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];
}
module.exports = {
readFileList,
readTotalFileWords,
readEachFileWords,
}
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,
}
接着继续在该目录下创建第三个文件 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;
}
目前就三个文件,最终效果如图:
站点信息代码
这一步的文件目录不能随便移动,因为该目录是 Vuepress 规定的。
首先进入 docs/.vuepress 目录,创建 components 文件夹
创建一个 vue 文件: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>
继续创建一个 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>
最终效果如图:
创建好了两个 vue 组件,我们需要使用它们。
使用
WebInfo.vue
组件
打开 docs/index.md
移到最下方,添加如下内容:
<ClientOnly>
<WebInfo/>
</ClientOnly>
使用
PageInfo.vue
组件
在 docs/.vuepress/config.js(新版是 config.ts)的 plugins 中添加配置。
js
module.exports = {
plugins: [
{
name: 'custom-plugins',
globalUIComponents: ["PageInfo"] // 2.x 版本 globalUIComponents 改名为 clientAppRootComponentFiles
}
]
}
ts (本次使用这个)
import { UserPlugins } from 'vuepress/config'
plugins: <UserPlugins>[
[
{
name: 'custom-plugins',
globalUIComponents: ["PageInfo"] // 2.x 版本 globalUIComponents 改名为 clientAppRootComponentFiles
}
]
]
站点信息配置
上面都按照步骤写好代码、使用组件了,那么就可以走最后一步配置我们的站点信息。
进入到 docs/.vuepress/config.js(新版为 config.ts)文件。
引入之前写好的工具代码文件:(路径要准确,这里仅仅是模板)
js
const { readFileList, readTotalFileWords, readEachFileWords } = require('./webSiteInfo/readFile');
ts (本次)
import { readFileList, readTotalFileWords, readEachFileWords } from './webSiteInfo/readFile';
如图(演示 JS 代码块):
在 themeConfig 中添加如下内容:
// 站点配置(首页 & 文章页)
blogInfo: {
blogCreate: '2021-10-19', // 博客创建时间
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
},
如图(图片内容不一定是最新,最新的是代码块内容):
属性配置的具体介绍请看 属性配置。
本地主题
如果已经看完了在线主题的内容,其实本内容的大小不变,只是位置变换、一些代码重组。
配置了在线主题,就不需要配置本地主题,反之亦然。
工具类
在 vdoing/util 目录下创建 webSiteInfo.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;
}
/**
* 已运行时间低于一天显示时分秒
* 目前该函数没有使用,低于一天直接显示不到一天
*/
export function getTime(startDate, endDate) {
if (day < 0) {
let hour = parseInt(Math.abs(new Date(startDate) - new Date(endDate)) / (1000 * 60 * 60));
if (hour > 0) {
let minute = parseInt(Math.abs(new Date(startDate) - new Date(endDate) - hour * 60 * 60 * 1000) / (1000 * 60));
if (minute > 0) {
let second = parseInt(Math.abs(new Date(startDate) - new Date(endDate) - hour * 60 * 60 * 1000 - minute * 60 * 1000) / (1000));
if (second != 0) {
return hour + ' 小时 ' + minute + ' 分钟 ' + second + ' 秒';
} else {
return hour + ' 小时 ' + minute + ' 分钟 ';
}
} else {
return hour + ' 小时 ';
}
} else {
let minute = parseInt(Math.abs(new Date(startDate) - new Date(endDate) - hour * 60 * 60 * 1000) / (1000 * 60));
if (minute > 0) {
let second = parseInt(Math.abs(new Date(startDate) - new Date(endDate) - hour * 60 * 60 * 1000 - minute * 60 * 1000) / (1000));
if (second != 0) {
return + minute + ' 分钟 ' + second + ' 秒';
} else {
return minute + ' 分钟 ';
}
} else {
return parseInt(Math.abs(new Date(startDate) - new Date(endDate) - hour * 60 * 60 * 1000 - minute * 60 * 1000) / (1000)) + ' 秒 ';
}
}
}
}
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();
}
});
};
},
};
export function fetch() {
bszTag && bszTag.hides();
bszCaller.fetch("//busuanzi.ibruce.info/busuanzi?jsonpCallback=BusuanziCallback", function (t) {
bszTag.texts(t), bszTag.shows();
})
};
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");
});
},
};
Vue组件创建
需要两个 Vue 组件,分别是首页的站点信息模块和文章页信息模块。
在 vdoing/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, fetch } from "../util/webSiteInfo";
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);
},
},
};
</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>
继续在 vdoing/components 目录下创建 PageInfo.vue
文件,添加如下内容:
<template>
<div class="page-view">
<!-- 文章字数 -->
<div title="文章字数" class="book-words iconfont icon-book">
<a href="javascript:;" style="margin-left: 3px; color: #888">{{
wordsCount
}}</a>
</div>
<!-- 预阅读时长 -->
<div
v-if="readingTime"
title="预阅读时长"
class="reading-time iconfont icon-shijian"
>
<a href="javascript:;" style="margin-left: 3px; color: #888">{{
readingTime
}}</a>
</div>
<!-- 浏览量 -->
<div v-if="pageView" title="浏览量" class="page-view iconfont icon-view">
<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>
</div>
</div>
</template>
<script>
import { fetch } from "../util/webSiteInfo";
export default {
data() {
return {
// Young Kbt
wordsCount: 0,
readingTime: 0,
pageView: true,
pageIteration: 3000,
};
},
mounted() {
this.initPageInfo();
},
watch: {
$route(to, from) {
if (
to.path !== "/" &&
to.path != from.path &&
this.$themeConfig.blogInfo
) {
this.initPageInfo();
}
},
},
methods: {
/**
* 初始化页面信息
*/
initPageInfo() {
this.$filterPosts.forEach((itemPage) => {
if (itemPage.path == this.$route.path) {
const { eachFileWords, pageView, pageIteration, readingTime } =
this.$themeConfig.blogInfo;
this.pageIteration = pageIteration;
if (eachFileWords) {
eachFileWords.forEach((itemFile) => {
if (itemFile.permalink == itemPage.frontmatter.permalink) {
this.wordsCount = itemFile.wordsCount;
if (readingTime || readingTime == undefined) {
this.readingTime = itemFile.readingTime;
} else {
this.readingTime = false;
}
}
});
}
this.pageView = pageView == undefined ? true : pageView;
if (this.pageView) {
this.getPageViewCouter(this.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);
},
},
};
</script>
<style scoped>
.page-view > div {
float: left;
margin-left: 20px;
font-size: 0.8rem;
}
@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>
Vue组件引用
写好两个组件,那么我们需要使用它们。
引入
WebInfo.vue
组件
打开 vdoing/components/Home.vue
文件。
大概在 174 行处引入 WebInfo.vue
组件:
import WebInfo from './WebInfo.vue';
1
大概在 242 行处找到 components
注册该组件:
components: { ......, WebInfo },
1
大概在 153 行处(div 的 class 为 custom-html-box
的上方),添加如下内容:
<webInfo />
1
三个效果图:
引入
PageInfo.vue
组件
打开 vdoing/components/ArticleInfo.vue
文件。
大概在 67 行处引入 PagesView.vue
组件:
import PageInfo from './PageInfo.vue';
1
大概在 69 行处添加 components
注册该组件(data()
上方):
components: { PageInfo },
1
大概在 61 行处,添加如下内容:
<PageInfo style="margin-left: 0" />
1
效果图:
核心配置文件
在 docs/.vuepress 目录下创建 webSiteInfo 文件夹,并在文件夹里创建 readFile.js
文件。
添加如下内容:
js
const fs = require('fs'); // 文件模块
const path = require('path'); // 路径模块
const matter = require('gray-matter'); // FrontMatter解析器 https://github.com/jonschlinkert/gray-matter
const chalk = require('chalk') // 命令行打印美化
const log = console.log
const docsRoot = path.join(__dirname, '..', '..', '..', 'docs'); // docs文件路径
/**
* 获取本站的文章数据
* 获取所有的 md 文档,可以排除指定目录下的文档
*/
function readFileList(excludeFiles = [''], dir = docsRoot, filesList = []) {
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);
var wordCount = 0;
filesList.forEach((item) => {
const content = getContent(item.filePath);
var len = counter(content);
wordCount += len[0] + len[1];
});
if (wordCount < 1000) {
return wordCount;
}
return Math.round(wordCount / 100) / 10 + 'k';
}
/**
* 获取每一个文章的字数
* 可以排除某个目录下的 md 文档字数
*/
function readEachFileWords(excludeFiles = [''], cn, en) {
const filesListWords = [];
const filesList = readFileList(excludeFiles);
filesList.forEach((item) => {
const content = getContent(item.filePath);
var len = counter(content);
// 计算预计的阅读时间
var readingTime = readTime(len, cn, en);
var wordsCount = 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, cn = 300, en = 160) {
var readingTime = len[0] / cn + len[1] / en;
if (readingTime > 60 && readingTime < 60 * 24) { // 大于一个小时,小于一天
let hour = parseInt(readingTime / 60);
let minute = parseInt((readingTime - hour * 60));
if (minute === 0) {
return hour + 'h';
}
return hour + 'h' + minute + 'm';
} else if (readingTime > 60 * 24) { // 大于一天
let day = parseInt(readingTime / (60 * 24));
let hour = parseInt((readingTime - day * 24 * 60) / 60);
if (hour === 0) {
return day + 'd';
}
return day + 'd' + hour + 'h';
}
return readingTime < 1 ? '1' : parseInt((readingTime * 10)) / 10 + 'm'; // 取一位小数
}
/**
* 读取文件内容
*/
function getContent(filePath) {
return fs.readFileSync(filePath, 'utf8');
}
/**
* 获取文件内容的字数
* cn:中文
* en:一整句英文(没有空格隔开的英文为 1 个)
*/
function counter(content) {
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];
}
module.exports = {
readFileList,
readTotalFileWords,
readEachFileWords,
}
配置站点信息
最后一步,在 docs/.vuepress/config.js(新版为 config.ts)文件,引入写好的 readFile.js
文件(路径要准确,这里仅仅是模板)
const { readFileList, readTotalFileWords, readEachFileWords } = require('./webSiteInfo/readFile');
1
如图(演示 JS 代码块):
在 themeConfig 中添加如下内容:
// 站点配置(首页 & 文章页)
blogInfo: {
blogCreate: '2021-10-19', // 博客创建时间
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
},
如图(图片内容不一定是最新):
属性配置的具体介绍请看 属性配置。
属性配置
blogCreate
- 类型:
string
- 默认值:当前时间(
new Date()
) - 格式:yyyy-mm-dd
博客创建时间。如果不添加时间,页面上显示 0 天。
mdFileCountType
- 类型:
string
|readFileList()
- 参数:数组
- 默认值:archives
文章数目。如果不添加内容,页面上显示归档的文章数目。
readFileList
是一个 js 文件,需要引入,参数是 目录的全名,最终效果会 排除该目录里的文章数,可多选,逗号隔开。也可不传参数。
温馨提示:readFileList()
不传参数会获取 docs 下所有的 md 文档(除了 .vuepress
和 @pages
目录下的文档)。
totalWords
- 类型:
string
|readFileWords()
- 参数:数组
- 默认值:null
本站文档总字数。如果不添加内容,页面上显示 0 字。
string
仅支持 archives,并且使用该类型有条件:必须使用 eachFileWords,否则报错。
readFileWords
是一个 js 文件,需要引入,参数是目录的全名,最终效果会 排除该目录里的文章字数,可多选,逗号隔开。也可不传参数。
moutedEvent
- 类型:
string
- 默认值:.tags-wrapper
选择挂载的元素属性,支持多种选择器(id、class ......),该模块会挂载到该元素后面,形成兄弟元素。(仅支持首页的元素)。
温馨提示:.categories-wrapper
会挂载在文章分类下面;.blogger-wrapper
会挂载在头像模块下面;.icons
会挂载在头像下方、图标上方。
默认是热门标签 .tags-wrapper
下面。
indexView
- 类型:
boolean
- 默认值:true
开启首页的访问量和排名统计,默认 true(开启)。
pageView
- 类型
boolean
- 默认值:true
开启文章页的浏览量统计,默认 true(开启)。
eachFileWords
- 类型:
readEachFileWords()
- 参数:数组
- 默认值:null
开启每个文章页的字数。如果不添加内容,则不开启。
readEachFileWords(['xx'])
关闭 xx 目录(可多个,可不传参数)下的文章页字数和阅读时长。
readEachFileWords()
第一个参数是数组,后面两个参数分别是 1 分钟里能阅读的中文字数和英文字数,配合 readingTime 使用。
readEachFileWords()
方法默认排除了 article 为 false 的文章。
readingTime
- 类型:
boolean
- 默认值:true
- 条件:使用
eachFileWords
开启文章页的预计阅读时间。默认阅读中文 1 分钟 300 个字,英文 1 分钟 160 个字。如果想自定义阅读文字时长,请在 eachFileWords
的 readEachFileWords()
传入后面两个参数。分别为 1 分钟阅读的中文和英文个数。
indexIteration
- 类型:
number
- 默认值:3000
如果首页获取访问量失败,则每隔多少时间后获取一次访问量,直到获取成功或获取 10 次后。默认 3 秒。
注意:设置时间太低,可能导致访问量 + 2、+ 3 ......
pageIteration
- 类型:
number
- 默认值:3000
如果文章页获取访问量失败,则每隔多少时间后获取一次访问量,直到获取成功或获取 10 次后。默认 3 秒。
注意:设置时间太低,可能导致访问量 + 2、+ 3 ......
说明:成功获取一次访问量,访问量 + 1,所以第一次获取失败后,设置的每个隔段重新获取时间,将会影响访问量的次数。如 100 可能每次获取访问量 + 3。
结束语
如果你还有疑惑,可以去我的 GitHub 仓库或者 Gitee 仓库查看源码。
如果你有更好的方式,评论区留言告诉我,或者加入 Vdoing 主题的 QQ 群:694387113。谢谢!
自己配置完的效果
其它方案-自建busuanzi
2024年12月26日记录