Skip to content

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 函数

这是主函数,负责生成整个侧边栏配置:

  1. 获取文档根目录路径
  2. 过滤所有非隐藏文件夹
  3. 为每个文件夹生成配置项
  4. 返回完整的侧边栏配置对象

generateSidebarItems 函数

这是递归函数,用于处理目录结构:

  1. 读取当前目录下的所有文件
  2. 对文件夹进行递归处理
  3. 将 Markdown 文件转换为侧边栏项
  4. 支持无限层级的目录结构

.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. 注意事项

  1. 文件命名规范

    • 推荐使用英文命名
    • 避免使用特殊字符
    • 保持文件名简洁明了
  2. 目录结构建议

    • 保持层级结构清晰
    • 避免过深的嵌套
    • 建议最多不超过三层目录
  3. 性能考虑

    • 大型项目中注意文件数量
    • 可以考虑增加缓存机制
    • 建议定期清理未使用的文档

但以上代码消耗性能,需要优化。

优化如下:

  • 使用缓存 使用 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 注意事项

  • 定期检查缓存有效性
  • 监控内存占用情况
  • 及时更新依赖版本
  • 做好错误处理机制