Skip to content

VitePress 自动生成导航和侧边栏

.vitepress/navSidebarUtil.ts

typescript
import { resolve, join, sep } from 'path'
import { readdirSync, statSync } from 'fs'
import { DefaultTheme } from 'vitepress'

interface SidebarGenerateConfig {
  /**
   * 需要遍历的目录. 默认:articles
   */
  dirName?: string
  /**
   * 忽略的文件名. 默认: index.md
   */
  ignoreFileName?: string
  /**
   * 忽略的文件夹名称. 默认: ['demo','asserts']
   */
  ignoreDirNames?: string[]
}

interface SideBarItem {
  text: string
  collapsible?: boolean
  collapsed?: boolean
  items?: SideBarItem[]
  link?: string
}

interface NavGenerateConfig {
  /**
   * 需要遍历的目录. 默认:articles
   */
  dirName?: string
  /**
   * 最大遍历层级. 默认:1
   */
  maxLevel?: number
}

/**
 * 判断是否为markdown文件
 * @param fileName 文件名
 * @returns 有返回值则表示是markdown文件,否则不是
 */
function isMarkdownFile(fileName: string) {
  return !!fileName.match(/.+\.md$/)
}

// 获取docs目录的完整名称(从根目录一直到docs目录)
const docsDirFullPath = join(__dirname, '../')
// 获取docs目录的完整长度
const docsDirFullPathLen = docsDirFullPath.length

/**
 * 获取dirOrFileFullName中第一个/docs/后的所有内容
 *  如:
 * /a-root/docs/test 则 获取到 /test
 * /a-root-docs/docs/test 则 获取到 /test
 * /a-root-docs/docs/docs/test 则 获取到 /docs/test
 * @param dirOrFileFullName 文件或者目录名
 * @returns
 */
function getDocsDirNameAfterStr(dirOrFileFullName: string) {
  // 使用docsDirFullPathLen采用字符串截取的方式,避免多层目录都叫docs的问题
  return `${sep}${dirOrFileFullName.substring(docsDirFullPathLen)}`
}

export function getSidebarData(sidebarGenerateConfig: SidebarGenerateConfig = {}) {
  const {
    dirName = 'articles',
    ignoreFileName = 'index.md',
    ignoreDirNames = ['demo', 'asserts'],
  } = sidebarGenerateConfig

  // 获取目录的绝对路径
  const dirFullPath = resolve(__dirname, `../${dirName}`)
  const allDirAndFileNameArr = readdirSync(dirFullPath)
  const obj = {}

  allDirAndFileNameArr.map(dirName => {
    let subDirFullName = join(dirFullPath, dirName)

    const property = getDocsDirNameAfterStr(subDirFullName).replace(/\\/g, '/') + '/'
    const arr = getSideBarItemTreeData(subDirFullName, 1, 3, ignoreFileName, ignoreDirNames)

    obj[property] = arr
  })

  return obj
}

function getSideBarItemTreeData(
  dirFullPath: string,
  level: number,
  maxLevel: number,
  ignoreFileName: string,
  ignoreDirNames: string[]
): SideBarItem[] {
  // 获取所有文件名和目录名
  const allDirAndFileNameArr = readdirSync(dirFullPath)
  const result: SideBarItem[] = []
  allDirAndFileNameArr.map((fileOrDirName: string, idx: number) => {
    const fileOrDirFullPath = join(dirFullPath, fileOrDirName)
    const stats = statSync(fileOrDirFullPath)
    if (stats.isDirectory()) {
      if (!ignoreDirNames.includes(fileOrDirName)) {
        const text = fileOrDirName.match(/^[0-9]{2}-.+/) ? fileOrDirName.substring(3) : fileOrDirName
        // 当前为文件夹
        const dirData: SideBarItem = {
          text,
          collapsed: false,
        }
        if (level !== maxLevel) {
          dirData.items = getSideBarItemTreeData(fileOrDirFullPath, level + 1, maxLevel, ignoreFileName, ignoreDirNames)
        }
        if (dirData.items) {
          dirData.collapsible = true
        }
        result.push(dirData)
      }
    } else if (isMarkdownFile(fileOrDirName) && ignoreFileName !== fileOrDirName) {
      // 当前为文件
      const matchResult = fileOrDirName.match(/(.+)\.md/)
      let text = matchResult ? matchResult[1] : fileOrDirName
      text = text.match(/^[0-9]{2}-.+/) ? text.substring(3) : text

      const fileData: SideBarItem = {
        text,
        link: getDocsDirNameAfterStr(fileOrDirFullPath).replace('.md', '').replace(/\\/g, '/'),
      }

      result.push(fileData)
    }
  })

  return result
}

export function getNavData(navGenerateConfig: NavGenerateConfig = {}) {
  const { dirName = 'articles', maxLevel = 2 } = navGenerateConfig
  const dirFullPath = resolve(__dirname, `../${dirName}`)
  const result = getNavDataArr(dirFullPath, 1, maxLevel)

  return result
}

/**
 * 获取顶部导航数据
 *
 * @param   {string}     dirFullPath  当前需要遍历的目录绝对路径
 * @param   {number}     level        当前层级
 * @param   {number[]}   maxLevel     允许遍历的最大层级
 * @return  {NavItem[]}               导航数据数组
 */
function getNavDataArr(dirFullPath: string, level: number, maxLevel: number): DefaultTheme.NavItem[] {
  // 获取所有文件名和目录名
  const allDirAndFileNameArr = readdirSync(dirFullPath)
  const result: DefaultTheme.NavItem[] = []

  allDirAndFileNameArr.map((fileOrDirName: string, idx: number) => {
    const fileOrDirFullPath = join(dirFullPath, fileOrDirName)
    const stats = statSync(fileOrDirFullPath)
    const link = getDocsDirNameAfterStr(fileOrDirFullPath).replace('.md', '').replace(/\\/g, '/')

    const text = fileOrDirName.match(/^[0-9]{2}-.+/) ? fileOrDirName.substring(3) : fileOrDirName

    if (stats.isDirectory()) {
      // 当前为文件夹
      const dirData: any = {
        text,
        link: `${link}/`,
      }

      if (level !== maxLevel) {
        const arr = getNavDataArr(fileOrDirFullPath, level + 1, maxLevel).filter(v => v.text !== 'index.md')
        if (arr.length > 0) {
          // @ts-ignore
          dirData.items = arr
          delete dirData.link
        }
      }

      dirData.activeMatch = link + '/'
      result.push(dirData)
    } else if (isMarkdownFile(fileOrDirName)) {
      // 当前为文件
      const fileData: DefaultTheme.NavItem = {
        text,
        link,
      }
      fileData.activeMatch = link + '/'
      result.push(fileData)
    }
  })

  return result
}

.vitepress/config.ts

typescript
import { defineConfig } from 'vitepress'
import { getSidebarData, getNavData } from './navSidebarUtil'

export default defineConfig({
  // ...
  themeConfig: {
    nav: getNavData(),
    sidebar: getSidebarData(),
  },
})

Released under the MIT License.