Skip to content

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"):

  1. 递归调用 createSidebarItems 处理子目录
  2. 创建侧边栏分组项
  3. 设置标题、折叠状态等属性

2.4 文件节点处理

当遇到文件节点时(typeof dirOrFileInfo === "string"):

  1. 读取 Markdown 文件内容
  2. 解析 frontmatter 获取标题等信息
  3. 创建侧边栏链接项
  4. 设置链接路径为 rewrites 中的目标路径

3. 标题生成机制

3.1 标题获取优先级

  1. frontmatter.title: Markdown 文件中的 frontmatter 标题
  2. 一级标题: 从 Markdown 内容中提取的第一个一级标题
  3. 文件名: 去除序号前缀后的文件名

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 处理过程:

  1. 处理 "01.指南" 目录

    typescript
    // 是目录节点,递归处理子目录
    const childSidebarItems = createSidebarItems(
      { "01.简介": { "01.简介.md": "guide/intro.md", "10.快速开始.md": "guide/quickstart.md" } },
      "/path/to/docs/01.指南/",
      option,
      "/01.指南/"
    );
  2. 处理 "01.简介" 子目录

    typescript
    // 继续递归处理文件
    const grandChildItems = createSidebarItems(
      { "01.简介.md": "guide/intro.md", "10.快速开始.md": "guide/quickstart.md" },
      "/path/to/docs/01.指南/01.简介/",
      option,
      "/01.指南/01.简介/"
    );
  3. 处理文件节点

    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,
    };
  4. 最终生成的侧边栏配置

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 递归处理子目录