VitePress 扩展首页内容 
在 VitePress 1.0 之后,官方提供了 createContentLoader API,可以更方便的收集文档信息
介绍 
createContentLoader API 是 VitePress 提供的辅助方法,接受相对于 srcDir 的 glob 匹配表达式,以及选项参数。执行后会返回一个 { watch, load } 的加载器对象,VitePress 要求默认导出该对象,在执行时 VitePress 内部会自动处理加载器,以 data 具名导出数据的方式使用
WARNING
使用 createContentLoader 的文件要求以 .data.js 或 .data.ts 结尾
glob 匹配表达式仅支持 md 文件,其他类型文件将被跳过
加载器得到的数据为 ContentData[] 格式数组:
interface ContentData {
  // 页面路径映射. 例如. /posts/hello.html (不会包括 base 路径)
  url: string;
  frontmatter: Record<string, any>;
  // 仅当启用了相关选项时,才会出现以下内容
  // md 源码
  src: string | undefined;
  // HTML 代码
  html: string | undefined;
  // 摘录信息
  excerpt: string | undefined;
}2
3
4
5
6
7
8
9
10
11
12
13
TIP
加载器获取的数据会内联到文档包中,所以需要注意数据大小,这也是默认仅返回 url 和 frontmatter 的原因
要返回额外的信息,需要配置第二个选项参数:
import { createContentLoader } from "vitepress";
// 排除掉不需要的 md 文件
export default createContentLoader("**/*.md", {
  includeSrc: true, // 是否返回 md 源码
  render: true, // 是否返回 HTML 源码
  excerpt: true, // 是否返回摘录
  transform(rawData) {
    // 这里可以处理加载器获取的数据,并将处理后的数据作为 data 导出
    return rawData.map(({ url, frontmatter, src, html, excerpt }) => {
      return {
        /* ... */
      };
    });
  },
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
获取需要的数据 
在 .vitepress 目录下创建 create.data.ts 文件,并默认导出 createContentLoader API,glob 表达式仅匹配我们需要展示的文章(可以排除掉功能目录以及未完成的文章)
TIP
因为最终的导出是 VitePress 处理后的 data 数据,为了避免编辑器导入时报错,可以自定义一个 data 并导出
// create.data.ts
let data;
// 避免在导入时报没有导出 data 的错误
export { data };
// createContentLoader会默认忽略'**/node_modules/**', '**/dist/**'
export default createContentLoader(
  [
    "!(.vitepress|public|images|.guthub|components|snippets)/**/!(index|README).md",
  ],
  {
    // ...
  }
);2
3
4
5
6
7
8
9
10
11
12
13
14
我们还需要展示文章发布或修改时间,直接使用 fs.stat 模块读取文件信息会存在不同设备上文件信息不一致的情况。更好的办法是通过 git 读取提交记录,使用 fs.stat 作为提交记录获取失败时的备选方案:
// 获取文件提交时间
function getGitTimestamp(filePath: string) {
  return new Promise<[number, number]>((resolve) => {
    let output: number[] = [];
    // 开启子进程执行git log命令
    const child = spawn("git", [
      "--no-pager",
      "log",
      '--pretty="%ci"',
      filePath,
    ]);
    // 监听输出流
    child.stdout.on("data", (d) => {
      const data = String(d)
        .split("\n")
        .map((item) => +new Date(item))
        .filter((item) => item);
      output.push(...data);
    });
    // 输出接受后返回
    child.on("close", () => {
      if (output.length) {
        // 返回[发布时间,最近更新时间]
        resolve([+new Date(output[output.length - 1]), +new Date(output[0])]);
      } else {
        // 没有提交记录时获取文件时间
        const { birthtimeMs, mtimeMs } = fs.statSync(filePath);
        resolve([birthtimeMs, mtimeMs]);
      }
    });
    child.on("error", () => {
      // 获取失败时使用文件时间
      const { birthtimeMs, mtimeMs } = fs.statSync(filePath);
      resolve([birthtimeMs, mtimeMs]);
    });
  });
}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
还需要展示文章简要介绍,所以开启 includeSrc 属性得到文章源码,之后就可以处理加载器得到的数据
export default createContentLoader(
  [
    "!(.vitepress|public|images|.guthub|components|snippets)/**/!(index|README).md",
  ],
  {
    includeSrc: true,
    async transform(data) {
      const promises: Promise<any>[] = [];
      data.forEach(({ frontmatter, src, url }) => {
        // 用页面的一级标题作为文章标题(因为sidebar中可能是精简的标题)
        let title =
          src?.match(/^\s*#\s+(.*)\s*$/m)?.[1] ||
          basename(url).replace(extname(url), "");
        // 标题可能用到了变量,需要替换
        const regexp = /\{\{\s*\$frontmatter\.(\S+?)\s*\}\}/g;
        let match;
        while ((match = regexp.exec(title)) !== null) {
          const replaceReg = new RegExp(
            "\\{\\{\\s*\\$frontmatter\\." + match[1] + "\\s*\\}\\}",
            "g"
          );
          title = title.replace(replaceReg, frontmatter[match[1]]);
        }
        // 链接去掉项目名
        const link = normalize(url)
          .split(sep)
          .filter((item) => item)
          .join(sep);
        // 获取发布时间
        const task = getGitTimestamp(link.replace(/\.html$/i, ".md")).then(
          (fileTimeInfo) => ({
            title,
            details: src
              // 去除html标签
              ?.replace(/<[^>]+?>/g, "")
              // 去除frontmatter
              .replace(/^---[\s\S]*?---/, "")
              // 去除标题
              .replace(/^#+\s+.*?$/gm, "")
              // 去除引用
              .replace(/^\>/gm, "")
              // 只保留反引号内部内容
              .replace(/`(.+?)`/g, "$1")
              // 只保留加粗、倾斜符号中的内容
              .replace(/\*{1,3}(.+?)\*{1,3}/g, "$1")
              // 只保留跳转内容
              .replace(/\[(.+?)\]\(.+?\)/g, "$1")
              // 去除提示块
              .replace(/^:::.*$/gm, "")
              // 统一空白字符为一个空格
              .replace(/\s/g, " ")
              // 仅保留可能显示的部分,减小数据大小
              .slice(0, 200),
            link,
            // linkText 可以显示更新时间
            linkText: new Date(fileTimeInfo[1]).toLocaleDateString(),
            // 存储时间信息用于排序
            fileTimeInfo,
          })
        );
        promises.push(task);
      });
      const pages = await Promise.all(promises);
      // 更新时间降序排列
      return pages.sort((a, b) => b.fileTimeInfo[1] - a.fileTimeInfo[1]);
      // 发布时间降序排列
      // return pages.sort((a, b) => b.fileTimeInfo[0] - a.fileTimeInfo[0]);
    },
  }
);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
创建展示组件 
数据获取完毕后我们在自定义首页中展示数据即可,创建 Home.vue 组件,我们可以直接使用 VitePress 默认主题提供的组件来展示数据
<template>
  <VPHero
    name="XaviDocs"
    text="个人技术文档"
    :image="image"
    :actions="actions"
  />
  <VPFeatures :features="data" />
</template>
<script setup lang="ts">
import VPHero from "vitepress/dist/client/theme-default/components/VPHero.vue";
import VPFeatures from "vitepress/dist/client/theme-default/components/VPFeatures.vue";
// 导入加载器获取的数据
import { data } from "../.vitepress/create.data";
const image = { light: "/pic1.svg", dark: "/pic2.svg" };
const actions = [
  {
    text: "随便逛逛",
    link: randomPage(),
  },
];
// 随机访问一篇文章功能
function randomPage(): string {
  const length = data.length - 1;
  return data[Math.floor(Math.random() * length)]?.link;
}
</script>
<style scoped>
/* 修改默认主题提供的组件样式 */
:deep(.details) {
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  word-break: break-all;
}
</style>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
使用自定义首页组件 
因为 VitePress md 文件中能够直接引用 Vue 组件,所以我们直接在 index.md 文件中引入并使用 Home 组件即可完成展示
---
layout: home
---
<script setup>
  import Home from './components/Home.vue'
</script>
<Home />2
3
4
5
6
7
8
9