LV04-自动侧边栏
一、概述
buildDirectoryStructure 函数是 VitePress 侧边栏插件 plugins\vitepress-plugin-sidebar-resolve\src\rewritesToSidebar.ts 中的一个功能组件。该函主要是将 VitePress 的 rewrites 配置(一个扁平化的记录对象)转换为嵌套的目录结构树。
二、函数签名与类型定义
1. 函数签名
typescript
const buildDirectoryStructure = (rewrites: Record<string, string>): DirectoryStructure => {
// 函数实现
};2. 参数详解
2.1 rewrites 参数
- 类型:
Record<string, string> - 描述: VitePress 的重写配置对象
- 键: 源文件路径(如
'01.指南/01.简介/01.简介.md') - 值: 重写后的目标路径(如
'guide/intro.md')
2.2 输入示例
javascript
{
'01.指南/01.简介/01.简介.md': 'guide/intro.md',
'01.指南/01.简介/10.快速开始.md': 'guide/quickstart.md',
'01.指南/10.使用/05.Markdown 拓展.md': 'guide/markdown.md'
}3. 返回值类型
3.1 DirectoryStructure 接口
定义在 plugins\vitepress-plugin-sidebar-resolve\src\types.ts:
typescript
export interface DirectoryStructure {
[key: string]: DirectoryStructure | string;
}3.2 输出示例
javascript
{
"01.指南": {
"01.简介": {
"01.简介.md": "guide/intro.md",
"10.快速开始.md": "guide/quickstart.md"
},
"10.使用": {
"05.Markdown 拓展.md": "guide/markdown.md"
}
}
}三、核心算法实现解析
1. 整体处理流程
函数采用分层构建策略,通过以下四个阶段完成转换:
1.1 初始化阶段
typescript
const structure: DirectoryStructure = {};创建空的根目录结构对象。
1.2 遍历处理阶段
typescript
Object.entries(rewrites).forEach(([key, value]) => {
// 处理每个路径映射
});遍历所有重写规则,逐个处理。
1.3 路径解析阶段
typescript
const parts = key.split("/");将文件路径分割为组件数组。
1.4 层级构建阶段
通过循环逐层构建嵌套目录结构。
2. 详细算法步骤
2.1 步骤一:初始化结构
typescript
const structure: DirectoryStructure = {};
let currentLevel = structure;2.2 步骤二:路径分割
typescript
const parts = key.split("/");
// 示例: '01.指南/01.简介/01.简介.md' → ['01.指南', '01.简介', '01.简介.md']2.3 步骤三:层级遍历构建
typescript
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
const isLast = i === parts.length - 1;
if (isLast) {
// 文件处理
currentLevel[part] = value;
} else {
// 目录处理
if (!currentLevel[part]) currentLevel[part] = {};
currentLevel = currentLevel[part] as DirectoryStructure;
}
}四、运行过程详细模拟
1. 测试数据准备
使用以下 rewrites 配置进行模拟:
javascript
const rewrites = {
"01.指南/01.简介/01.简介.md": "guide/intro.md",
"01.指南/01.简介/10.快速开始.md": "guide/quickstart.md",
};2. 第一次处理过程
2.1 处理条目
'01.指南/01.简介/01.简介.md': 'guide/intro.md'
2.2 路径分割
javascript
const parts = "01.指南/01.简介/01.简介.md".split("/");
// 结果: ['01.指南', '01.简介', '01.简介.md']2.3 层级构建详细步骤
2.3.1 初始化状态
javascript
structure = {};
currentLevel = structure(指向根目录);2.3.2 处理第一层 (i=0, part='01.指南')
- 不是最后一部分 → 目录处理
currentLevel['01.指南']不存在 → 创建空目录- 更新
currentLevel指向新创建的目录 - 当前结构:
{ '01.指南': {} }
2.3.3 处理第二层 (i=1, part='01.简介')
- 不是最后一部分 → 目录处理
currentLevel['01.简介']不存在 → 创建空目录- 更新
currentLevel指向新创建的目录 - 当前结构:
{ '01.指南': { '01.简介': {} } }
2.3.4 处理第三层 (i=2, part='01.简介.md')
- 是最后一部分 → 文件处理
- 设置
currentLevel['01.简介.md'] = 'guide/intro.md' - 最终结构:
{ '01.指南': { '01.简介': { '01.简介.md': 'guide/intro.md' } } }
3. 第二次处理过程
3.1 处理条目
'01.指南/01.简介/10.快速开始.md': 'guide/quickstart.md'
3.2 路径分割
javascript
const parts = "01.指南/01.简介/10.快速开始.md".split("/");
// 结果: ['01.指南', '01.简介', '10.快速开始.md']3.3 层级构建详细步骤
3.3.1 初始化状态
javascript
structure = { "01.指南": { "01.简介": { "01.简介.md": "guide/intro.md" } } };
currentLevel = structure(指向根目录);3.3.2 处理第一层 (i=0, part='01.指南')
- 不是最后一部分 → 目录处理
currentLevel['01.指南']已存在 → 直接使用- 更新
currentLevel指向已存在的目录 - 当前结构: 保持不变
3.3.3 处理第二层 (i=1, part='01.简介')
- 不是最后一部分 → 目录处理
currentLevel['01.简介']已存在 → 直接使用- 更新
currentLevel指向已存在的目录 - 当前结构: 保持不变
3.3.4 处理第三层 (i=2, part='10.快速开始.md')
- 是最后一部分 → 文件处理
- 设置
currentLevel['10.快速开始.md'] = 'guide/quickstart.md' - 最终结构:
javascript
{
"01.指南": {
"01.简介": {
"01.简介.md": "guide/intro.md",
"10.快速开始.md": "guide/quickstart.md"
}
}
}五、关键技术特性分析
1. 递归数据结构设计
1.1 递归类型定义
typescript
interface DirectoryStructure {
[key: string]: DirectoryStructure | string;
}支持任意深度的嵌套结构。
1.2 设计优势
- 灵活性: 支持无限层级目录嵌套
- 一致性: 统一处理目录和文件节点
- 可扩展性: 易于添加新的节点类型
2. 引用传递与指针技术
2.1 currentLevel 指针机制
typescript
let currentLevel = structure;
// ...
currentLevel = currentLevel[part] as DirectoryStructure;2.2 技术原理
- 引用传递: 通过变量引用直接操作对象
- 动态导航: 实时跟踪当前处理层级
- 内存高效: 避免不必要的对象复制
3. 路径解析算法
3.1 分割策略
typescript
const parts = key.split("/");3.2 节点类型判断
typescript
const isLast = i === parts.length - 1;- 中间部分: 目录节点
- 最后部分: 文件节点
4. 惰性初始化策略
4.1 按需创建
typescript
if (!currentLevel[part]) currentLevel[part] = {};4.2 优化效果
- 资源节约: 只创建必要的目录节点
- 性能提升: 避免预先创建所有可能目录
- 内存优化: 减少不必要的对象分配
5. 扁平化到层次化转换
5.1 转换模式
从线性键值对到树形结构的智能转换。
5.2 应用场景
- 文件系统路径处理
- URL路由解析
- 导航菜单生成
六、从目录结构到侧边栏的转换过程
1. createSidebarItems 函数解析
1.1 函数作用
createSidebarItems 函数是将 buildDirectoryStructure 生成的目录结构转换为 VitePress 侧边栏配置的核心函数。
1.2 输入输出
- 输入: DirectoryStructure 目录结构
- 输出: DefaultTheme.SidebarItem[] 侧边栏项数组
1.3 核心处理逻辑
typescript
const createSidebarItems = (
structure: DirectoryStructure,
root: string,
option: SidebarOption,
prefix = "/",
onlyScannerRootMd = false
): DefaultTheme.SidebarItem[] => {
// 处理逻辑
};2. 处理流程详解
2.1 初始化阶段
typescript
let sidebarItems: DefaultTheme.SidebarItem[] = [];
const sidebarItemsNoIndex: DefaultTheme.SidebarItem[] = [];
const entries = Object.entries(structure);2.2 遍历处理每个节点
typescript
entries.forEach(([dirOrFilename, dirOrFileInfo]) => {
// 处理每个目录或文件
});2.3 目录节点处理
当遇到目录节点时(typeof dirOrFileInfo === "object"):
- 递归调用
createSidebarItems处理子目录 - 创建侧边栏分组项
- 设置标题、折叠状态等属性
2.4 文件节点处理
当遇到文件节点时(typeof dirOrFileInfo === "string"):
- 读取 Markdown 文件内容
- 解析 frontmatter 获取标题等信息
- 创建侧边栏链接项
- 设置链接路径为 rewrites 中的目标路径
3. 标题生成机制
3.1 标题获取优先级
- frontmatter.title: Markdown 文件中的 frontmatter 标题
- 一级标题: 从 Markdown 内容中提取的第一个一级标题
- 文件名: 去除序号前缀后的文件名
3.2 前缀后缀处理
支持通过 frontmatter 或配置选项添加前缀后缀:
typescript
const finalSidebarPrefix = (sidebarPrefix && (prefixTransform?.(sidebarPrefix) ?? sidebarPrefix)) ?? "";
const finalSidebarSuffix = (sidebarSuffix && (suffixTransform?.(sidebarSuffix) ?? sidebarSuffix)) ?? "";
const text = finalSidebarPrefix + (frontmatterTitle || mdTitle || title) + finalSidebarSuffix;4. 排序机制
4.1 排序策略
- 文件序号排序: 根据文件名前缀的序号进行排序
- frontmatter 排序: 通过
sidebarSort属性指定排序值 - 默认排序: 未指定排序的文件放在最后
4.2 排序实现
typescript
if (sort) {
sidebarItems = sidebarItems
.sort((a: any, b: any) => (a.sort || defaultSortNum) - (b.sort || defaultSortNum))
.map(item => {
delete (item as any).sort;
return item;
});
}5. 完整的转换流程
5.1 数据流
rewrites配置
→ buildDirectoryStructure
→ 目录结构树
→ createSidebarItems
→ VitePress侧边栏配置5.2 示例转换(基于前面的模拟数据)
基于第四章节的运行过程模拟,我们有以下目录结构:
javascript
{
"01.指南": {
"01.简介": {
"01.简介.md": "guide/intro.md",
"10.快速开始.md": "guide/quickstart.md"
}
}
}createSidebarItems 处理过程:
处理 "01.指南" 目录:
typescript// 是目录节点,递归处理子目录 const childSidebarItems = createSidebarItems( { "01.简介": { "01.简介.md": "guide/intro.md", "10.快速开始.md": "guide/quickstart.md" } }, "/path/to/docs/01.指南/", option, "/01.指南/" );处理 "01.简介" 子目录:
typescript// 继续递归处理文件 const grandChildItems = createSidebarItems( { "01.简介.md": "guide/intro.md", "10.快速开始.md": "guide/quickstart.md" }, "/path/to/docs/01.指南/01.简介/", option, "/01.指南/01.简介/" );处理文件节点:
typescript// 处理 "01.简介.md" const content = readFileSync("/path/to/docs/01.指南/01.简介/01.简介.md", "utf-8"); const { data: { title: frontmatterTitle }, } = matter(content); // 获取标题:frontmatter.title > 一级标题 > 文件名 const text = frontmatterTitle || "简介"; // 创建侧边栏项 const sidebarItem = { text, link: "/guide/intro.md", // 使用 rewrites 中的目标路径 collapsed: false, };最终生成的侧边栏配置:
javascript
[
{
text: "指南",
collapsed: false,
items: [
{
text: "简介",
collapsed: false,
items: [
{
text: "简介", // 来自 frontmatter 或文件名
link: "/guide/intro.md",
},
{
text: "快速开始",
link: "/guide/quickstart.md",
},
],
},
],
},
];关键代码对应关系:
- 文件读取和解析:readFileSync + matter 解析
- 标题生成:frontmatterTitle || mdTitle || title 优先级获取
- 链接设置:使用 rewrites 中的目标路径作为链接
- 递归调用:createSidebarItems 递归处理子目录