VitePress 自动路由配置实现
1. 功能介绍
本文介绍了如何实现 VitePress 的自动路由配置,通过文件系统自动生成侧边栏,避免手动维护路由配置的繁琐工作。
1.1 主要功能
- 自动扫描文档目录结构
- 生成多层级侧边栏配置
- 支持文件夹和文件的智能排序
- 自动处理特殊文件(如 index.md)
1.2 技术特点
- 使用缓存机制提高性能
- 支持深层嵌套目录
- 智能识别文档标题
- 可扩展的配置系统
2. 核心实现
2.1 完整代码实现
首先在 .vitepress/utils
目录下创建 generateSidebar.js
文件:
javascript
import fs from 'fs'
import path from 'path'
export function generateSidebar() {
const docsPath = path.resolve(__dirname, '../../')
const sidebar = {}
// 读取docs目录下的所有文件夹
const dirs = fs.readdirSync(docsPath).filter(file => {
const stat = fs.statSync(path.join(docsPath, file))
return stat.isDirectory() && !file.startsWith('.') // 排除.vitepress等隐藏目录
})
// 为每个文件夹生成配置
dirs.forEach(dir => {
const items = []
generateSidebarItems(path.join(docsPath, dir), items, '')
if (items.length) {
sidebar[`/${dir}/`] = [
{
text: dir,
items: items
}
]
}
})
return sidebar
}
function generateSidebarItems(dirPath, items, baseUrl) {
const files = fs.readdirSync(dirPath)
files.forEach(file => {
const fullPath = path.join(dirPath, file)
const stat = fs.statSync(fullPath)
if (stat.isDirectory()) {
// 如果是目录,递归处理
const subItems = []
generateSidebarItems(fullPath, subItems, `${baseUrl}${file}/`)
if (subItems.length) {
items.push({
text: file,
items: subItems
})
}
} else if (file.endsWith('.md')) {
// 如果是md文件,添加到items中
const text = file === 'index.md' ? '介绍' : file.replace('.md', '')
items.push({
text: text,
link: `/${path.relative(path.resolve(__dirname, '../../'), dirPath)}/${file}`
})
}
})
}
generateSidebar 函数
这是主函数,负责生成整个侧边栏配置:
- 获取文档根目录路径
- 过滤所有非隐藏文件夹
- 为每个文件夹生成配置项
- 返回完整的侧边栏配置对象
generateSidebarItems 函数
这是递归函数,用于处理目录结构:
- 读取当前目录下的所有文件
- 对文件夹进行递归处理
- 将 Markdown 文件转换为侧边栏项
- 支持无限层级的目录结构
在 .vitepress/config.js
文件中,引入 generateSidebar
函数:
javascript
import { generateSidebar } from './utils/generateSidebar'
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
sidebar: generateSidebar(),
}
})
3.目录结构示例
docs/
├── guide/
│ ├── index.md # 指南首页
│ ├── getting-started.md
│ └── advanced/
│ └── configuration.md
├── api/
│ ├── index.md # API 文档首页
│ └── references.md
└── examples/
└── basic-usage.md
4. 注意事项
文件命名规范
- 推荐使用英文命名
- 避免使用特殊字符
- 保持文件名简洁明了
目录结构建议
- 保持层级结构清晰
- 避免过深的嵌套
- 建议最多不超过三层目录
性能考虑
- 大型项目中注意文件数量
- 可以考虑增加缓存机制
- 建议定期清理未使用的文档
但以上代码消耗性能,需要优化。
优化如下:
- 使用缓存 使用 sidebarCache 缓存最终的侧边栏配置 使用 fileCache 缓存文件系统的读取结果
- 减少文件系统操作 通过 getAllFiles 一次性读取所有文件信息 避免重复的 fs.statSync 调用
- 优化数据处理 使用 groupFilesByDirectory 将文件按目录分组 采用树形结构处理目录层级,减少递归次数
- 内存优化 使用对象存储数据,减少内存占用,减少中间数据的生成
优化后代码示例:
javascript
import fs from 'fs'
import path from 'path'
// 使用 Map 对象创建缓存,提高性能
// sidebarCache: 存储最终生成的侧边栏配置
// fileCache: 存储文件系统的读取结果,避免重复读取
const sidebarCache = new Map()
const fileCache = new Map()
/**
* 生成 VitePress 侧边栏配置的主函数
* 返回一个对象,格式为 { '/路径/': [{ text: '标题', items: [...] }] }
*/
export function generateSidebar() {
// 获取文档根目录的绝对路径
const docsPath = path.resolve(__dirname, '../../')
// 检查是否存在缓存,如果有则直接返回缓存的结果
const cacheKey = docsPath
if (sidebarCache.has(cacheKey)) {
return sidebarCache.get(cacheKey)
}
// 初始化侧边栏配置对象
const sidebar = {}
// 获取所有 Markdown 文件的路径
const allFiles = getAllFiles(docsPath)
// 将文件按照顶级目录进行分组
const filesByDir = groupFilesByDirectory(allFiles, docsPath)
// 处理每个顶级目录,生成对应的侧边栏配置
for (const [dir, files] of Object.entries(filesByDir)) {
// 跳过以点开头的隐藏目录
if (dir.startsWith('.')) continue
// 处理该目录下的所有文件,生成侧边栏项
const items = processFiles(files, docsPath)
if (items.length) {
sidebar[`/${dir}/`] = [{
text: dir,
items: items
}]
}
}
// 将生成的配置存入缓存
sidebarCache.set(cacheKey, sidebar)
return sidebar
}
/**
* 递归获取指定目录下的所有 Markdown 文件
* @param {string} dir - 要扫描的目录路径
* @returns {string[]} - 文件路径数组
*/
function getAllFiles(dir) {
// 检查缓存中是否已存在该目录的文件列表
if (fileCache.has(dir)) {
return fileCache.get(dir)
}
const results = []
// 递归遍历目录的辅助函数
function traverse(currentPath) {
// 读取当前目录下的所有文件和文件夹
const files = fs.readdirSync(currentPath)
for (const file of files) {
const fullPath = path.join(currentPath, file)
const stat = fs.statSync(fullPath)
// 如果是目录且不是隐藏目录,则递归处理
if (stat.isDirectory() && !file.startsWith('.')) {
traverse(fullPath)
}
// 如果是 Markdown 文件,则添加到结果数组
else if (file.endsWith('.md')) {
results.push(fullPath)
}
}
}
traverse(dir)
// 将结果存入缓存
fileCache.set(dir, results)
return results
}
/**
* 将文件按照顶级目录进行分组
* @param {string[]} files - 文件路径数组
* @param {string} basePath - 基础路径
* @returns {Object} - 按目录分组的文件对象
*/
function groupFilesByDirectory(files, basePath) {
const groups = {}
for (const file of files) {
// 获取相对于基础路径的相对路径
const relativePath = path.relative(basePath, file)
// 获取顶级目录名
const dir = relativePath.split(path.sep)[0]
if (!groups[dir]) {
groups[dir] = []
}
groups[dir].push(file)
}
return groups
}
/**
* 处理文件列表,生成侧边栏配置项
* @param {string[]} files - 文件路径数组
* @param {string} basePath - 基础路径
* @returns {Array} - 侧边栏配置项数组
*/
function processFiles(files, basePath) {
// 用于存储最终的侧边栏项
const items = []
// 用于构建目录树的临时对象
const dirStructure = {}
// 遍历所有文件,构建目录树结构
files.forEach(file => {
const relativePath = path.relative(basePath, file)
const parts = relativePath.split(path.sep)
let current = dirStructure
// 遍历路径的每一部分,构建嵌套的目录结构
for (let i = 1; i < parts.length; i++) {
const part = parts[i]
// 处理文件
if (i === parts.length - 1 && part.endsWith('.md')) {
// 如果是 index.md,显示为"介绍",否则使用文件名(去掉.md后缀)
const text = part === 'index.md' ? '介绍' : part.replace('.md', '')
current.files = current.files || []
current.files.push({
text,
link: `/${relativePath}`
})
}
// 处理目录
else {
current.dirs = current.dirs || {}
current.dirs[part] = current.dirs[part] || {}
current = current.dirs[part]
}
}
})
/**
* 将目录树结构转换为 VitePress 侧边栏配置数组
* @param {Object} structure - 目录树结构对象
* @returns {Array} - 侧边栏配置数组
*/
function convertToItems(structure) {
const result = []
// 先添加文件
if (structure.files) {
result.push(...structure.files)
}
// 再处理子目录
if (structure.dirs) {
for (const [dirName, content] of Object.entries(structure.dirs)) {
const subItems = convertToItems(content)
if (subItems.length) {
result.push({
text: dirName,
items: subItems
})
}
}
}
return result
}
// 将构建好的目录树转换为最终的侧边栏配置
return convertToItems(dirStructure)
}
5. 常见问题与解决方案
5.1 性能问题
- 问题: 大量文件导致生成速度慢
- 解决: 使用缓存机制,增量更新
5.2 路径问题
- 问题: Windows 系统路径分隔符不一致
- 解决: 使用 path.sep 统一处理
5.3 特殊字符问题
- 问题: 文件名包含特殊字符导致路由错误
- 解决: 添加文件名验证和转换逻辑
6. 总结
通过这种自动化配置方式,我们实现了:
- 零配置的文档管理
- 高效的文件组织
- 优秀的性能表现
- 灵活的扩展性
6.1 后续优化方向
- 添加文件变更监听
- 实现热重载功能
- 支持自定义排序规则
- 优化缓存策略
6.2 注意事项
- 定期检查缓存有效性
- 监控内存占用情况
- 及时更新依赖版本
- 做好错误处理机制