各位大佬早上好、中午好、晚上好!
在进行组件库维护的时候,需要进行组件库文档的编写,需要获取到贡献者或者维护者。vitePress的方式是通过在文档的最上面初始化页面的frontmatter
---
author:xxx,
avatar: xxx,
---
然后通过useData获取到这个数据,然后通过一个Contributor.vue的组件渲染贡献者列表。
<script setup>
import { useData } from 'vitepress'
const { theme } = useData()
</script>
<template>
<h1>{{ theme.footer.copyright }}</h1>
</template>
本文将通过另一种方式简化此步骤,让文档维护者无感的更新贡献者列表。
以下示例环境:
- 代码托管:私有化的gitlab
- 组件库搭建框架:vitePress
第一步:获取gitlab秘钥
秘钥的作用是用于通过http接口请求gitlab服务。
以下是如何在GitLab中生成一个新的Personal Access Token的步骤:
- 登录到你的GitLab实例。
- 在右上角点击你的头像,然后选择 Settings。
- 在左侧菜单中,找到并点击 Access Tokens。
- 在 New Access Token 部分,填写以下信息:
- Name: 给你的Token一个描述性的名称,例如“API Access”。
- Expires at: 选择一个过期时间,或者让Token不过期。如果你担心安全问题,建议设置一个合理的过期时间。
- Scopes: 选择Token的作用域。为了获取用户信息,我们选择read_user scope。如果你打算进行更复杂的操作,可以选择勾选其他scopes。
- 点击 Create personal access token。
- 在这个页面的下边,你会看到你的新Token。请确保你立即复制这个Token并将其保存在一个安全的地方。GitLab不会再次显示这个Token,所以如果你丢失了它,你将需要生成一个新的Token。
第二步:获取用户信息
gitlab提供了api用于操作数据,我们可以利用用户api来获取所有用户的详细信息。
gitlab:http://xxx/api/v4/users?per_page=${pageSize}&page=${page} 。如果代码部署到其他平台,如gitHub也有相应的开放api
import { join, resolve } from 'node:path'
import fs from 'fs-extra'
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const DIR_ROOT = resolve(__dirname, '..')
async function getUsers(page, pageSize) {
return new Promise((resolve, reject) => {
// 私有化部署的域名
fetch(`http:/私有化部署的域名/api/v4/users?per_page=${pageSize}&page=${page}`, {
headers: {
'PRIVATE-TOKEN': 'xxx', // 这里填上你刚刚获取的Access Tokens
}
})
.then(response => response.json())
.then(data => {
// console.log("🚀 ~ data:", data)
if (data) {
const users = data.reduce((users, item) => {
users.push({
id: item.id,
name: item.name,
username: item.username,
avatar_url: item.avatar_url,
web_url: item.web_url,
})
return users;
}, [])
console.log('users.length >>:', users.length);
resolve(users)
}
})
.catch(error => {
console.error('Error:', error)
reject([])
});
})
}
async function updateUsers() {
const users = await getUsers(1, 1000)
if (users.length > 0) {
await fs.writeFile(join(DIR_ROOT, './users.json'), `${JSON.stringify(users, null, 2)}\n`, 'utf8')
}
}
updateUsers()
这段代码的目的是从GitLab API获取用户列表,并将这些用户信息保存到本地的一个JSON文件中:
- 定义了一个异步函数
getUsers,它通过发送HTTP请求到一个私有化部署的GitLab API端点来获取用户列表。这个函数接收两个参数:page和pageSize,分别代表请求的页码和每页的用户数量。 - 在
getUsers函数中,使用fetch函数发送请求,并设置请求头中的PRIVATE-TOKEN用于身份验证。 - 请求成功后,解析JSON响应,并将用户信息提取到一个新的数组中。
- 定义了一个异步函数
updateUsers,它调用getUsers函数获取用户数据,如果获取到的用户数量大于0,则将这些用户信息写入到项目根目录下的users.json文件中。 - 最后,调用
updateUsers函数执行整个过程。
然后在根目录新建一个users.json文件用于保存请求回来的数据
写个sh用于执行此脚本文件
"update-users": "cd ./scripts && node users.js",
第三步:获取每个文件的提交信息
这里我们使用了node模块child_process用于动态执行git来获取提交信息;
child_process是Node.js的一个内置模块,它提供了一种方法来创建子进程,允许你执行操作系统上的命令,并与这些子进程进行交互。这个模块在需要对操作系统进行交互,或者执行需要访问系统资源的任务时非常有用。在此脚本中我们主要使用 exec。它启动一个shell,然后在该shell中执行命令,并且缓存命令的输出,当命令执行完成时,则将输出以回调函数参数的形式返回。
获取你写的md文档的所有地址:
function getMarkdownPath(dir) {
let files = [];
const items = fs.readdirSync(dir);
items.forEach(item => {
const itemPath = path.join(dir, item);
const stats = fs.statSync(itemPath);
if (stats.isDirectory()) {
files = files.concat(getMarkdownPath(itemPath)); // 递归获取子文件夹中的文件
} else if (path.extname(item) === '.md') {
files.push(itemPath.replace(/\\/g, '/'));
}
});
return files;
}
通过文件地址获取提交记录
async function getCommitByPath(path) {
return new Promise((resolve, reject) => {
exec(`git log --pretty=format:"%h %an %ad" -- ${path}`, (error, stdout, stderr) => {
if (error) {
console.error(`Error: ${error.message}`);
reject(error.message)
return;
}
if (stderr) {
reject(stderr)
console.error(`Stderr: ${stderr}`);
return;
}
// 解析 Git log 输出
const gitLogLines = stdout.split('\n');
const contributors = new Set();
gitLogLines.forEach(line => {
const [hash, author] = line.split(' ')
contributors.add(author);
});
// 输出贡献者列表
resolve(Array.from(contributors));
});
})
}
将获取到的提交记录写入到文件中
async function main() {
const markdownFiles = getMarkdownPath('../docs')
let pageCommit = {};
if (markdownFiles.length > 0) {
for (let path of markdownFiles) {
const users = await getCommitByPath(path)
pageCommit[path] = users
}
}
console.log('pageCommit >>:', pageCommit);
if (!isEmpty(pageCommit)) {
await fs.writeFile(join(DIR_ROOT, './page-commit.json'), `${JSON.stringify(pageCommit, null, 2)}\n`, 'utf8')
}
}
这是全部的获取文件提交记录的代码:
import { exec } from "child_process";
import { join, resolve } from 'node:path'
import fs from 'fs-extra'
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const DIR_ROOT = resolve(__dirname, '..')
function getMarkdownPath(dir) {
let files = [];
const items = fs.readdirSync(dir);
items.forEach(item => {
const itemPath = path.join(dir, item);
const stats = fs.statSync(itemPath);
if (stats.isDirectory()) {
files = files.concat(getMarkdownPath(itemPath)); // 递归获取子文件夹中的文件
} else if (path.extname(item) === '.md') {
files.push(itemPath.replace(/\\/g, '/'));
}
});
return files;
}
async function getCommitByPath(path) {
return new Promise((resolve, reject) => {
exec(`git log --pretty=format:"%h %an %ad" -- ${path}`, (error, stdout, stderr) => {
if (error) {
console.error(`Error: ${error.message}`);
reject(error.message)
return;
}
if (stderr) {
reject(stderr)
console.error(`Stderr: ${stderr}`);
return;
}
// 解析 Git log 输出
const gitLogLines = stdout.split('\n');
const contributors = new Set();
gitLogLines.forEach(line => {
const [hash, author] = line.split(' ')
contributors.add(author);
});
// 输出贡献者列表
resolve(Array.from(contributors));
});
})
}
function isEmpty(obj) {
for (const key of Object.keys(obj)) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
return false;
}
}
return true;
}
async function main() {
const markdownFiles = getMarkdownPath('../docs')
let pageCommit = {};
if (markdownFiles.length > 0) {
for (let path of markdownFiles) {
const users = await getCommitByPath(path)
pageCommit[path] = users
}
}
console.log('pageCommit >>:', pageCommit);
if (!isEmpty(pageCommit)) {
await fs.writeFile(join(DIR_ROOT, './page-commit.json'), `${JSON.stringify(pageCommit, null, 2)}\n`, 'utf8')
}
}
main()
-
定义了一个函数
getMarkdownPath,它递归地搜索指定目录及其子目录,收集所有Markdown文件的路径,并将这些路径返回。 -
定义了一个异步函数
getCommitByPath,它通过执行git log命令获取指定文件的提交历史,并解析输出以提取贡献者列表(即作者),然后返回这些作者的名字。 -
定义了一个辅助函数
isEmpty,用于检查一个对象是否为空。 -
定义了一个异步函数
main,它是整个脚本的入口点。main函数首先调用getMarkdownPath来获取所有Markdown文件的路径列表。- 然后,对于每个Markdown文件,调用
getCommitByPath来获取其贡献者列表,并将这些信息存储在一个对象pageCommit中,该对象的键是文件路径,值是贡献者数组。 - 最后,如果
pageCommit不为空,将其内容写入到项目根目录下的page-commit.json文件中。
总的来说就是:遍历一个目录及其子目录中的所有Markdown文件,获取每个文件的Git提交历史和贡献者列表,并将这些信息保存到一个JSON文件中。
将提交信息拼接到对应文件的末尾
我们已经拿到了用户信息,和每个文件的提交信息。vitePress提供了布局插槽用于将一些信息显示在页面中:
<!--.vitepress/theme/MyLayout.vue-->
<script setup>
import DefaultTheme from 'vitepress/theme'
const { Layout } = DefaultTheme
</script>
<template>
<Layout>
<template #doc-footer-before`> // 这里使用了doc-footer-before插槽
My custom sidebar top content
</template>
</Layout>
</template>
不过这种方式不太好用,它是静态的,无法根据每个页面的不同元数据动态设置,而且提交者不能显示在右侧的锚点导航中。
不用怕,vitePress灵活性非常高,我们可以利用插件的形式来动态修改文章的字符串,拼接贡献者到文章的末尾,具体怎么做呢?
- 在
.vitepress下新建plugins文件夹用于放置自定义的插件 - 新建
MDPreprocessor.js文件,代码逻辑很简单,code就是文件的源码,在code的尾部添加了<Contributors/>组件,然后返回。
export function MDPreprocessor() {
return {
name: 'md-preprocessor',
transform(code, id) {
if (!id.endsWith('.md')) return null
const [filename, i] = id.split('/').slice(-2)
// 是首页的 index.md直接跳过,这里可以根据项目定义其他的判断条件
if (filename === 'xxx' && i === 'index.md') return code
// 在正文末尾插入“Contributors”标题以及贡献者组件
code += '\n\n## 贡献者\n<Contributors/>'
return code
}
}
}
- 新建一个vite.config.js配置文件
import { defineConfig } from 'vite'
import { MDPreprocessor } from './.vitepress/plugins/MDPreprocessor'
export default defineConfig(async () => {
return {
plugins: [
MDPreprocessor()
]
}
})
完成以上步骤,每个你定义的md格式的文件都会在文件的末尾添加Contributors组件了,下一步就是要实现这个Contributors组件。
实现Contributors组件
- 在
.vitepress\theme\components\新建Contributors.vue组件并写入以下内容:
<script setup>
import { useData } from 'vitepress'
import users from '../../../users.json'
import pageCommit from '../../../page-commit.json'
const { page } = useData();
const { relativePath } = page.value;
const pageContributors = pageCommit['../' + relativePath]
let pageContributorInfos = [];
if (pageContributors) {
pageContributorInfos = users.filter(item => {
return pageContributors.includes(item.username) || pageContributors.includes(item.name)
})
}
</script>
<template>
<div class="contributor-wraper" v-for="user of pageContributorInfos" :key="user.id">
<div class="contributor">
<img :src="user.avatar_url" :alt="user.name" :title="user.name">
<a :href="user.web_url" target="_blank">{{ user.name }}</a>
</div>
</div>
</template>
<style scoped>
.contributor-wraper {
display: flex;
flex-wrap: wrap;
}
.contributor {
display: flex;
font-size: 16px;
line-height: 30px;
margin-right: 10px;
}
img {
width: 30px;
height: 30px;
line-height: 30px;
vertical-align: middle;
margin-right: 5px;
border-radius: 50%;
border: 1px solid rgba(0, 0, 0, 0.3);
}
</style>
在.vitepress\theme\index.js里注册这个组件为全局组件
export default {
extends: DefaultTheme,
Layout: () => {
return h(DefaultTheme.Layout, null, {
// https://vitepress.dev/guide/extending-default-theme#layout-slots
})
},
enhanceApp({ app, router, siteData }) {
app.component('Contributors', Contributors)
// ...
},
}
完成以上代码的编写,不出意外的话你的贡献者将会显示到每个文件的末尾
并且文件的提交者都会显示到此列表里。nice!!
最后我们在package.json里:
"scripts": {
"dev": "vitepress dev",
"build": "pnpm update-users && pnpm update-page-commit && vitepress build",
"preview": "vitepress preview",
"preinstall": "npx only-allow pnpm",
"update-users": "cd ./scripts && node users.js",
"update-page-commit": "cd ./scripts && node page-commit.js"
}
当我们执行pnpm build的时候先更新用户列表然后更新每个文件的提交信息,最后打包。done!!