Skip to content

LV100-自动侧边栏总览

自动侧边栏(自动导航栏)是 mist 主题文档体系里最核心的自动化插件之一:它在 VitePress 启动时扫描 sdoc 目录结构,自动生成 themeConfig.nav(顶部导航栏)和 themeConfig.sidebar(侧边栏)两份数据,免去手写配置、也免去文档增删后手动同步的工作。

本文是自动侧边栏系列的第一篇总览,讲清楚三件事:插件家族里三个包是什么关系、插件是怎么注册进 VitePress 的、以及内部两种解析模式(filePath / rewrites)的分野。后续四篇分工如下:

一、插件定位与家族

1. 解决什么问题

VitePress 原生的导航栏和侧边栏都写在 .vitepress/config.tsthemeConfig.navthemeConfig.sidebar 里。文档一多,这份手写配置会持续膨胀:每新增一篇文档要改一次配置、目录调整要重排结构、标题想改顺序只能挪数组。自动侧边栏插件把这件事交给代码——启动时扫描文档目录,按目录层级和文件名序号直接生成这两份配置数据。

2. 三个插件包的关系

仓库里与"自动导航/侧边栏"相关的插件包有三个,它们是同一条演进线上的三代产物:

(1)@docs-site/vitepress-nav-sidebarplugins/vitepress-nav-sidebar):最早的函数版。只导出 getSidebarData()getNavData() 两个普通函数,用户在自己的 config.mts 里调用后把返回值手动赋给 themeConfig.sidebar / themeConfig.nav。它是独立可用的轻量方案,mist 主题并不内置它。

(2)@docs-site/vitepress-plugin-sidebar-resolveplugins/vitepress-plugin-sidebar-resolve):从 vitepress-theme-teek fork 的纯侧边栏 Vite 插件。以 Vite 插件形式在 config() 钩子里生成并注入侧边栏,功能最全(国际化、文件监听重启、回调钩子等),但只管侧边栏不管导航栏。

(3)@docs-site/vitepress-auto-nav-sidebarplugins/vitepress-auto-nav-sidebar):mist 主题实际内置的合并版。在 sidebar-resolve 的算法基础上合并了导航栏生成,一次扫描同时产出 nav 和 sidebar。下文不做特殊说明时,"自动侧边栏插件"指的就是它。

3. 功能对比

能力nav-sidebar(函数版)sidebar-resolve(Teek 版)auto-nav-sidebar(内置版)
形态普通函数,手动赋值Vite 插件,自动注入Vite 插件,自动注入
导航栏有(简单目录树)有(filePath / rewrites 两套)
侧边栏有(简单目录树)有(filePath / rewrites 两套)有(filePath / rewrites 两套)
模式选择无(等价 filePath)resolveRule 手动配置__create__ 标记自动判定
国际化 locales
md 增删自动重启有(restart无(配置项存在但未实现)
序号排序 / frontmatter 定制弱(仅两位数字前缀剥离)全量(sidebarSort 等)全量(同 sidebar-resolve)
调试落盘debugPrint / saveToFile

二、注册链路:插件如何挂到 VitePress

这是"插件怎么注册到 VitePress"的完整答案,链路共四跳,全部发生在配置解析阶段。

1. 用户配置入口

文档站在 docs/src/.vitepress/config.mts 里通过 defineMistConfig 使用主题,自动侧边栏的配置就写在其中的 vitePlugins.navSidebarOption

typescript
// docs/src/.vitepress/config.mts
import { defineMistConfig } from "../../../packages/config";

const myThemeConfig = defineMistConfig({
  useTheme: true,
  themeName: "vitepress-theme-mist",
  vitePlugins: {
    navSidebarOption: {
      path: "sdoc",
      navOption: { maxLevel: 2 },
      sideBarOption: { type: "object", collapsed: true },
    },
  },
});

export default defineConfig({ extends: myThemeConfig /* ... */ });

2. 主题注册插件

packages/config/index.tsdefineMistConfig 会把默认配置(含 defaultConfig/vitePlugins.ts 里的 navSidebarOption 默认值)与用户传入的 vitePlugins 浅合并,然后调用 registerPluginAndGet

packages/config/vitePlugins.ts 把插件分两类注册:

(1)弱依赖插件registerLoosePlugins):docAnalysis、demo、permalink、mdH1,可通过配置项关闭;

(2)强依赖插件registerTightPlugins):catalogue、file-content-loader、auto-nav-sidebar。只要 useTheme !== false 就会注册,无法单独关闭——侧边栏数据是主题渲染的硬依赖。

typescript
// packages/config/vitePlugins.ts
export const registerTightPlugins = (vitePlugins: Plugins, ignoreDir: Record<string, any[]>) => {
  const plugins: any[] = [];
  // ...catalogue、fileContentLoader...
  // 自动侧边栏插件
  plugins.push(VitePluginVitePressAutoNavSidebar(navSidebarOption));
  return plugins;
};

3. 进入 Vite 插件体系

defineMistConfig 返回的配置里带着 vite.plugins 数组(见 packages/config/index.ts),VitePress 加载站点配置后,这些插件就进入了 Vite 的插件管线。VitePress 自己会在 config.vitepress 上挂一份站点上下文(sitesrcDirrewritesuserConfig 等),供插件读取。

4. config 钩子读取站点上下文并注入

插件的入口 plugins/vitepress-auto-nav-sidebar/src/index.ts 只实现了一个 config() 钩子:

typescript
// plugins/vitepress-auto-nav-sidebar/src/index.ts
config(config: any) {
  const {
    site: { themeConfig = {} },
    srcDir,
    rewrites: rewritesObj,
    userConfig,
  } = config.vitepress;

  // ...生成 navData 与 sideBarData...
  setNavBar(themeConfig, navData); // 设置导航栏
  setSideBar(themeConfig, sideBarData, sideBarOption?.type, createRule); // 设置侧边栏
},

生成结果通过 setNavBar / setSideBar 直接写进 config.vitepress.site.themeConfig,VitePress 后续序列化站点数据时就把它们当成用户配置的一部分交给主题渲染。

5. 注入时的合并规则

(1)导航栏:自动生成的排在前面,用户手写的 themeConfig.nav 追加在后(themeConfig.nav = [...autoNav, ...userNav]);

(2)侧边栏:type: "object" 时做对象浅合并,用户手写的同名 key 会覆盖自动生成的;type: "array" 时自动项在前、用户项在后;

(3)类型不符会告警:object 模式下手写的 sidebar 是数组(或反之),控制台提示"自定义 Sidebar 必须是对象形式 / 数组形式",且该手写数据不生效。

6. 脱离主题独立使用

不走 mist 主题时,也可以在任意 VitePress 项目里单独安装 @docs-site/vitepress-auto-nav-sidebar,手动挂进 vite.plugins

typescript
// .vitepress/config.ts
import AutoNavSidebar from "@docs-site/vitepress-auto-nav-sidebar";

export default defineConfig({
  vite: {
    plugins: [
      AutoNavSidebar({
        path: "sdoc",
        navOption: { maxLevel: 2 },
        sideBarOption: { type: "object", collapsed: true },
      }),
    ],
  },
});

三、核心原理

1. 启动时一次性生成

生成动作发生在 Vite config() 钩子里,即配置解析阶段只跑一次:dev 启动时一次、build 时一次。插件用闭包变量 isExecute 防止 build 流程中钩子被多次触发导致重复注入。代价是:运行期间新增、删除、移动 md 文档不会反映到侧边栏,必须重启服务(详见 LV112-自动侧边栏的限制与避坑)。

2. 扫描 → 生成 → 注入的数据流

整个插件可以概括为一条单向数据流:

text
sdoc 目录(文件系统)
        │  readdirSync / statSync 递归扫描

目录名 + 文件名(序号、标题)
        │  resolveFileName 解析 + matter() 读 frontmatter

NavItem[] / SidebarMulti 树形数据
        │  setNavBar / setSideBar

config.vitepress.site.themeConfig.nav / sidebar
        │  VitePress 序列化站点数据

主题渲染导航栏与侧边栏

其中"扫描→树形数据"这一步有两条独立实现路径,就是下一节的两种模式。

自动侧边栏与 LV070-永久链接 介绍的 vitepress-plugin-permalink 是一对搭档:permalink 插件负责让文档拥有稳定的英文永久链接,自动侧边栏负责让导航/侧边栏里的链接与永久链接保持一致。两者的协作方式(__create__ 标记)在下一节和 LV109-自动侧边栏之rewrites模式 中展开。

四、两种解析模式

README 里把 filePath 称为"本地文件路径"、rewrites 称为"运行文件路径",本质区别是侧边栏链接指向哪种 URL

1. filePath 模式

直接扫描文件系统,链接按文件真实路径生成,如 /sdoc/01-开发/LV001-pnpm工作区。适用于没有启用永久链接、路由就是文件路径的站点。当前文档站就是这种状态config.mtsrewrites: createRewrites(...) 一行被注释掉)。

2. rewrites 模式

以 VitePress 的 rewrites 路由重写表为数据源,链接按重写后的运行路径生成,如 /sdoc/develop/126d5cf36f2f354a。适用于通过 createRewrites() 把 frontmatter permalink 转成路由的站点,链接与永久链接一致。

3. 自动判定,而非手动配置

与 sidebar-resolve 用 resolveRule 手动选择不同,auto-nav-sidebar 不读这个配置项,而是在 config 钩子里自动判定(见 src/index.ts):

typescript
// plugins/vitepress-auto-nav-sidebar/src/index.ts
let createRule: "filePath" | "rewrites" = "filePath";
const rewrites = rewritesObj.map || {};
const rewritesLength = Object.keys(rewrites).length;
if (userConfig?.rewrites?.__create__ === "vitepress-plugin-permalink" && rewritesLength !== 0) {
  createRule = "rewrites";
}

判定条件有两个,缺一不可:

(1)用户原始配置里的 rewrites 对象带 __create__: "vitepress-plugin-permalink" 标记——即它必须由 permalink 插件的 createRewrites() 生成,手写的 rewrites 不认;

(2)重写表非空。

不满足时一律走 filePath 模式。createRewrites() 返回值的第一位就是 __create__ 标记,因此"启用了 createRewrites"天然意味着"标记存在"。

五、生成效果实测

对本站 docs/src/sdoc 目录(2026-09-20 时的结构)分别模拟两种模式的 config 钩子,得到的真实输出如下。

1. filePath 模式输出

导航栏(maxLevel: 2,一级目录为 link、二级目录为下拉):

json
[
  { "text": "开发", "link": "/sdoc/01-开发/" },
  {
    "text": "组件",
    "items": [
      { "text": "公共组件", "link": "/sdoc/02-组件/01-公共组件/" },
      { "text": "主题组件", "link": "/sdoc/02-组件/02-主题组件/" }
    ]
  },
  {
    "text": "插件",
    "items": [
      { "text": "独立插件包", "link": "/sdoc/03-插件/01-独立插件包/" },
      { "text": "内置插件", "link": "/sdoc/03-插件/02-内置插件/" }
    ]
  },
  { "text": "使用", "link": "/sdoc/04-使用/" }
]

侧边栏的 key 是 sdoc 下的一级目录(中文原样),目录树往下递归:

json
{
  "/sdoc/01-开发/": [
    { "text": "LV001-pnpm工作区", "collapsed": true, "link": "/sdoc/01-开发/LV001-pnpm工作区" },
    { "text": "LV004-TS路径映射与包作用域", "collapsed": true, "link": "/sdoc/01-开发/LV004-TS路径映射与包作用域" }
  ],
  "/sdoc/03-插件/": [
    {
      "text": "01-独立插件包",
      "collapsed": true,
      "items": [
        {
          "text": "LV010-file-content-loader",
          "collapsed": true,
          "link": "/sdoc/03-插件/01-独立插件包/LV010-file-content-loader"
        },
        {
          "text": "LV040-catalogue分析文档",
          "collapsed": true,
          "link": "/sdoc/03-插件/01-独立插件包/LV040-catalogue分析文档"
        }
      ]
    },
    {
      "text": "02-内置插件",
      "collapsed": true,
      "items": [{ "text": "LV001-img懒加载", "collapsed": true, "link": "/sdoc/03-插件/02-内置插件/LV001-img懒加载" }]
    }
  ]
}

2. rewrites 模式输出

同一目录启用 createRewrites() 后,导航栏链接变成各目录 index.md 的永久链接(注意结尾带 .md,VitePress 会自动处理):

json
[
  { "text": "开发", "link": "/sdoc/develop/126d5cf36f2f354aaaf41792.md" },
  {
    "text": "组件",
    "items": [
      { "text": "公共组件", "link": "/sdoc/component/common-component/126d5cf36f3029e123c119dd.md" },
      { "text": "主题组件", "link": "/sdoc/component/theme-component/126d5cf36f310519d37deafe.md" }
    ]
  },
  { "text": "使用", "link": "/sdoc/use/126d5cf36f331d41132d0c0c.md" }
]

侧边栏的 key 变成永久链接的英文别名段,文件链接变成各自的 permalink:

json
{
  "/sdoc/plugin/": [
    {
      "text": "01-独立插件包",
      "collapsed": true,
      "items": [
        {
          "text": "LV010-file-content-loader",
          "collapsed": true,
          "link": "/sdoc/plugin/standalone-plugin/126d5cf36f313a80a26194d1.md"
        },
        {
          "text": "LV040-catalogue分析文档",
          "collapsed": true,
          "link": "/sdoc/plugin/standalone-plugin/126d5cf36f313a9467f65843.md"
        }
      ]
    }
  ]
}

3. 两种模式对照

维度filePath 模式rewrites 模式
数据源文件系统目录扫描VitePress rewrites 表(来自 frontmatter permalink)
侧边栏 key/sdoc/<中文目录名>//sdoc/<英文别名>/
文章链接中文文件路径永久链接(/sdoc/<别名>/<24 位 hex>
URL 观感中文 + LV 编号全暴露纯英文 + hex,简洁稳定
路由要求未启用 rewrites(路由 = 文件路径)已启用 createRewrites()(路由 = permalink)
链接与路由一致性一致一致

可以看到不管哪种模式,链接形态都与当站路由一致——这正是自动判定的意义:路由怎么走,侧边栏就怎么生成。模式内部各自的生成过程,分别见 LV106LV109