Young Kbt blog Young Kbt blog
首页
  • java基础

    • Java基础
    • Java集合
    • Java反射
    • JavaJUC
    • JavaJVM
  • Java容器

    • JavaWeb
  • Java版本新特性

    • Java新特性
  • SQL 数据库

    • MySQL
    • Oracle
  • NoSQL 数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • ActiveMQ
    • RabbitMQ
    • RocketMQ
    • Kafka
  • 进阶服务

    • Nginx
  • Spring
  • Spring Boot
  • Spring Security
  • 设计模式
  • 算法
  • 知识
  • 管理

    • Maven
    • Git
  • 部署

    • Linux
    • Docker
    • Jenkins
    • Kubernetes
  • 进阶

    • TypeScript
  • 框架

    • React
    • Vue2
    • Vue3
  • 轮子工具
  • 项目工程
  • 友情链接
  • 本站

    • 分类
    • 标签
    • 归档
  • 我的

    • 收藏
    • 关于
    • Vue2-Admin (opens new window)
    • Vue3-Admin(完善) (opens new window)
GitHub (opens new window)

Shp Liu

朝圣的使徒,正在走向编程的至高殿堂!
首页
  • java基础

    • Java基础
    • Java集合
    • Java反射
    • JavaJUC
    • JavaJVM
  • Java容器

    • JavaWeb
  • Java版本新特性

    • Java新特性
  • SQL 数据库

    • MySQL
    • Oracle
  • NoSQL 数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • ActiveMQ
    • RabbitMQ
    • RocketMQ
    • Kafka
  • 进阶服务

    • Nginx
  • Spring
  • Spring Boot
  • Spring Security
  • 设计模式
  • 算法
  • 知识
  • 管理

    • Maven
    • Git
  • 部署

    • Linux
    • Docker
    • Jenkins
    • Kubernetes
  • 进阶

    • TypeScript
  • 框架

    • React
    • Vue2
    • Vue3
  • 轮子工具
  • 项目工程
  • 友情链接
  • 本站

    • 分类
    • 标签
    • 归档
  • 我的

    • 收藏
    • 关于
    • Vue2-Admin (opens new window)
    • Vue3-Admin(完善) (opens new window)
GitHub (opens new window)
  • 关于 - 自我

  • 关于 - 本站

    • 本站 - 介绍
    • 本站 - 规划
    • 本站 - 搭建
    • 本站 - 主题
    • 本站 - 网站部署
    • 本站 - 服务器部署
    • 本站 - 评论模块
    • 本站 - 站点信息模块
      • 前言
      • 添加meta
      • 添加在线图标
      • Vue模板
      • 主题选择
      • 在线主题
        • 网站信息工具代码
        • 站点信息代码
        • 站点信息配置
      • 本地主题
        • 工具类
        • Vue组件创建
        • Vue组件引用
        • 核心配置文件
        • 配置站点信息
      • 属性配置
      • 结束语
    • 本站 - 自定义样式模块
    • 本站 - 记录曾阅读位置模块
    • 本站 - 私密文章模块
    • 本站 - 导航站模块
    • 本站 - 首页大图模块
    • 本站 - 代码块隐藏模块
    • 本站 - 全局时间提示模块
  • 关于 - 首页

  • 关于 - 技巧

  • 关于 - 随笔

  • 关于
  • 关于 - 本站
Young Kbt
2021-12-30
目录

本站 - 站点信息模块原创

笔记

本内容介绍如何搭建本站首页的站点信息,以及每篇文章的浏览量统计。

2021-12-30 @Young Kbt

  • 前言
  • 添加meta
  • 添加在线图标
  • Vue模板
  • 主题选择
  • 在线主题
    • 网站信息工具代码
    • 站点信息代码
    • 站点信息配置
  • 本地主题
    • 工具类
    • Vue组件创建
    • Vue组件引用
    • 核心配置文件
    • 配置站点信息
  • 属性配置
  • 结束语

# 前言

本内容将在首页和每篇的文章页加入了一些元素,目前适用版本是 Vdoing v1.x。

如果你想集成到其他 Vuepress 主题,那么要添加卡片样式,修改挂载元素即可(建议先按照步骤完成一次再考虑集成)。

  • 为什么添加卡片样式?本模块的站点信息是基于 Vdoing 自带的卡片样式,模块并没有添加任何卡片样式,所以想集成到其他主题,则需要参考 Vdoing 卡片样式进行添加,或者按照自己喜欢的样式进行添加
  • 为什么修改挂载元素?本模块的挂载元素是基于 Vdoing 标签提供的 class 或 id,而其他主题的标签不一样,所以自行进行调试

本模块的所有 功能 支持大部分 Vuepress 主题,但是如何将所有功能展示到其他主题页面合适的地方,以及展示的样式等 DOM 技术,需要自己适配。

效果如下:

image-20220102230719391

image-20220103180058191

本站的访问量和文章的浏览量使用了 不蒜子,本地启动的 localhost 有很多人访问过,但无需担心实际部署后的访问量。

不蒜子官网地址 (opens new window)

不蒜子文档地址 (opens new window)

注意

问题:本模块目前有一个功能依赖于 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' }],
1

如图:

image-20220103175843735

# 添加在线图标

这里使用的是阿里矢量库。

地址:https://www.iconfont.cn/ (opens new window)

添加了五个图标

image-20220104191943235

如果你不想使用自己的矢量库项目(不害怕我删图标跑路🤣),那么可以使用我的图标项目网址,当你发现图标失效了,就请来这里获取新的地址,如果还没有更新,请在评论区留言。

在 config.js 下的 head 中文件添加如下内容:

['link', { rel: 'stylesheet', href: 'https://at.alicdn.com/t/font_3077305_pt8umhrn4k9.css' }]
1

如图:(图片的内容不一定是最新的,以上方代码块为准)

image-20220104192105981

# 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>
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

# 主题选择

下面有两种配置方式可以选,分别为:

  • 在线主题:NPM 主题,采用监听路由、插入式的代码
  • 本地主题:站点信息模块与页面一起渲染出来,没有延迟

本地主题不好的一点就是版本升级后曾修改的内容被重置,所以需要记好修改位置、备份,比较麻烦。好处是根据自己的需求在基础上拓展。

在线主题具有通用性,即在任意环境(如本地主题)都有效果。

# 在线主题

建议:本内容代码块比较长,可以点击代码块的右侧箭头来折叠,然后点击复制图标进行复制即可。

不管使不使用本地主题,都可以配置在线主题的站点模块。

# 网站信息工具代码

添加网站信息需要的计算代码、获取字数代码等工具类。

首先进入 docs/.vuepress 目录,创建 webSiteInfo 文件夹

image-20220101192402636

然后在 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

然后创建 readFile.js 或者 readFile.ts 文件,这个文件用于 统计文章数目 和 网站总字数 等。

添加如下内容:

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

    接着继续在该目录下创建第三个文件 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

    目前就三个文件,最终效果如图:

    image-20220607184317549

    # 站点信息代码

    这一步的文件目录不能随便移动,因为该目录是 Vuepress 规定的。

    首先进入 docs/.vuepress 目录,创建 components 文件夹

    image-20220101193210129

    创建一个 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>
    
    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

    继续创建一个 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

    最终效果如图:

    image-20220607184354072

    创建好了两个 vue 组件,我们需要使用它们。

    使用 WebInfo.vue 组件

    打开 docs/index.md

    image-20220101193622169

    移到最下方,添加如下内容:

    <ClientOnly>
      <WebInfo/>
    </ClientOnly>
    
    1
    2
    3

    使用 PageInfo.vue 组件

    在 docs/.vuepress/config.js(新版是 config.ts)的 plugins 中添加配置。

      module.exports = {
          plugins: [
              {
                  name: 'custom-plugins',
                  globalUIComponents: ["PageInfo"] // 2.x 版本 globalUIComponents 改名为 clientAppRootComponentFiles
              }
          ]
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      import { UserPlugins } from 'vuepress/config'
      plugins: <UserPlugins>[
          [
          	{
              	name: 'custom-plugins',
              	globalUIComponents: ["PageInfo"] // 2.x 版本 globalUIComponents 改名为 clientAppRootComponentFiles
          	}
          ]
      ]
      
      1
      2
      3
      4
      5
      6
      7
      8
      9

      # 站点信息配置

      上面都按照步骤写好代码、使用组件了,那么就可以走最后一步配置我们的站点信息。

      进入到 docs/.vuepress/config.js(新版为 config.ts)文件。

      引入之前写好的工具代码文件:(路径要准确,这里仅仅是模板)

        const { readFileList, readTotalFileWords, readEachFileWords } = require('./webSiteInfo/readFile');
        
        1
        import { readFileList, readTotalFileWords, readEachFileWords } from './webSiteInfo/readFile';
        
        1

        如图(演示 JS 代码块):

        image-20220103124132339

        在 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
        },
        
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15

        如图(图片内容不一定是最新,最新的是代码块内容):

        image-20220104192821504

        属性配置的具体介绍请看 属性配置。

        # 本地主题

        如果已经看完了在线主题的内容,其实本内容的大小不变,只是位置变换、一些代码重组。

        配置了在线主题,就不需要配置本地主题,反之亦然。

        # 工具类

        在 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");
            });
          },
        };
        
        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

        # 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>
        
        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

        继续在 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>
        
        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

        # 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

        三个效果图:

        image-20220110215442265

        image-20220110215509546

        image-20220110215531954

        引入 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

        效果图:

        image-20220110220005822

        # 核心配置文件

        在 docs/.vuepress 目录下创建 webSiteInfo 文件夹,并在文件夹里创建 readFile.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,
          }
          
          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
          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

          # 配置站点信息

          最后一步,在 docs/.vuepress/config.js(新版为 config.ts)文件,引入写好的 readFile.js 文件(路径要准确,这里仅仅是模板)

            const { readFileList, readTotalFileWords, readEachFileWords } = require('./webSiteInfo/readFile');
            
            1
            import { readFileList, readTotalFileWords, readEachFileWords } from './webSiteInfo/readFile';
            
            1

            如图(演示 JS 代码块):

            image-20220103124132339

            在 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
            },
            
            1
            2
            3
            4
            5
            6
            7
            8
            9
            10
            11
            12
            13
            14
            15

            如图(图片内容不一定是最新):

            image-20220104192821504

            属性配置的具体介绍请看 属性配置。

            # 属性配置

            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 仓库查看源码。

            • GitHub (opens new window)

            • Gitee (opens new window)

            如果你有更好的方式,评论区留言告诉我,或者加入 Vdoing 主题的 QQ 群:694387113。谢谢!

            编辑此页 (opens new window)
            #本站
            更新时间: 2023/10/23, 10:58:52
            本站 - 评论模块
            本站 - 自定义样式模块

            ← 本站 - 评论模块 本站 - 自定义样式模块→

            最近更新
            01
            技术随笔 - Element Plus 修改包名 原创
            11-02
            02
            Reactor - 扩展性
            11-02
            03
            Reactor - 最佳实践
            11-02
            更多文章>
            Theme by Vdoing | Copyright © 2021-2024 Young Kbt | blog
            桂ICP备2021009994号
            • 跟随系统
            • 浅色模式
            • 深色模式
            • 阅读模式