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