🌟 Vitepress 侧边栏自动生成,让你更专注写作

3,549 阅读3分钟

我正在参加「掘金·启航计划」

前言

Vitepress 出来后,已经是我们博客、文档的不二之选。但是其功能还不是很完善,其中的一个痛点就是不能根据目录自动生成侧边栏,每次我们想增加一篇文章时都需要手动的将文章路径同步到 sidebar config。这个问题在 issues 里已经挂了很久了,但是优先级不是很高,于是有了个人开发的想法。

介绍

  1. 自动将目录转换成 sidebar,取文件的标题作为侧边栏名称。
# 目录结构

├── dir1
│   ├── dir1-1
│   │   ├── 1.md
│   │   ├── 2.md
│   │   └── dir1-1-1
│   │       └── 1.md
│   └── dir1-2
│       └── 1.md
  1. 当文件删除、标题修改时,自动同步到 sidebar
  2. 支持对产生的 sidebar 配置做自定义修改(改名称、排序...)

怎么使用

  1. 安装
# pnpm
pnpm i @iminu/vitepress-plugin-auto-sidebar
# yarn
yarn add @iminu/vitepress-plugin-auto-sidebar
# npm
npm install @iminu/vitepress-plugin-auto-sidebar
  1. 配置
// .vitepress/config.ts
import AutoSidebar from "@iminu/vitepress-plugin-auto-sidebar";
export default defineConfig({
  vite: {
    plugins: [
      AutoSidebar(),
    ],
  },
});

自定义配置

// .vitepress/config.ts
import AutoSidebar from "@iminu/vitepress-plugin-auto-sidebar";
export default defineConfig({
  vite: {
    plugins: [
      AutoSidebar({
        /**
         * 当插件将目录结构转换为 sidebar 配置后触发,
         * 方便我们去操作 sidebar,比如将目录排序、修改目录名称等
         */
        sidebarResolved(value) {
          // do sort
          value["/dir2/"][0].items?.sort((a, b) => a.text - b.text);
          // rename
          value["/dir2/"][0].text = "sorted";
        },
        // 忽略一些文件
        ignores: ["index.md"],
        // 指定我们要自动构建的文档目录,默认是 .vitepress 目录
        docs: path.resolve(process.cwd(), "docs/demo"),
        /**
         * 指定 .vitepress 目录,默认会通过 glob 匹配到,
         * 如果页面有多个 .vitepress 需要手动配置
         */
        root: path.resolve(process.cwd(), "docs"),,
      }),
    ],
  },
});

实现

vitepress 是基于 vite,因此我们的操作可以通过编写 Vite Plugin 实现。

1. 自动生成 sidebar 结构

思路是重写我们的配置文件。通过插件构建出 sidebar config 的数据结构,自动填充到 vitepress config 中,这里我们借助 config() 这个钩子。

export default function VitePluginAutoSidebar() {
  return {
    name: "VitePluginAutoSidebar",
    // 新增
    config(config) {
      config.vitepress.site.themeConfig.sidebar = getSidebarConfig(opts);
      return config;
    },
  };
}

getSidebarConfig()就是将目录结构转换成 sidebar config,那么我们怎么实现这个方法呢。

  1. 通过 glob,找到所有的 .md 文件。
const paths = glob.sync("**/*.md", {
    cwd: docsPath,
    ignore: opts.ignores,
});
  1. 拿到所有的文件 path 后,对 path 做分割,然后遍历组合成树,构建出符合 sidebar 的结构,具体见 源码
paths.forEach((fullPath) => {
    const segments = fullPath.split("/");
    const absolutePath = path.resolve(docsPath, fullPath);
    if (segments.length === 0) return;
    // { "/demo/dir1/":[]}
    const topLevel = basePath
      ? `/${basePath}/${segments.shift()}/`
      : `/${segments.shift()}/`;
    // 如果第一级是文件
    if (topLevel.endsWith(".md")) return;
    if (!sidebar[topLevel]) {
      sidebar[topLevel] = [];
    }
    let currentLevel = sidebar[topLevel];
    segments.forEach((segment) => {
      let curConfig = currentLevel.find((item) => item.text === segment);
      if (!curConfig) {
        const itemConfig: DefaultTheme.SidebarItem = {};
        // is file
        if (segment.endsWith(".md")) {
          const route = getRoute(opts.root, absolutePath);
          itemConfig.text = matchTitle(absolutePath);
          itemConfig.link = route;
          // cache title
          titleCache[route] = itemConfig.text;
        } else {
          itemConfig.text = segment;
          itemConfig.collapsed = false;
          itemConfig.items = [];
        }
        currentLevel.push(itemConfig);
        curConfig = itemConfig;
      }
      currentLevel = curConfig.items as DefaultTheme.SidebarItem[];
    });
  });

2. 文件变动时刷新 sidebar

  1. 利用 configureServer钩子拿到 ViteDevServer 对象。
  2. 通过 server.watcher 监听所有的 .md 文件变化
  3. 触发删除文件时重启 server
  4. 触发文件修改时,查看一级标题是否有变化,如有变化重启 server
export default function VitePluginAutoSidebar() {
  return {
    name: "VitePluginAutoSidebar",
    configureServer: ({ watcher, restart }: ViteDevServer) => {
      const fsWatcher = watcher.add("*.md");
      fsWatcher.on("all", (event, filePath) => {
        if (event === "addDir") return;
        if (event === "unlinkDir") return;
        if (event == "add") return;
        if (event === "unlink") {
          restart();
          return;
        }
        if (event === "change") {
          const title = matchTitle(filePath);
          const route = getRoute(opts.root, filePath);
          if (!route || !title) return;
          // 未更新 title
          if (title === titleCache[route]) return;
          restart();
          return;
        }
      });
    },
  };
}

最后

以上就是文章的全部内容了

如果你想给博客增加全文搜索,见 快来给你的博客添加全文搜索

如果对你有帮助,欢迎 Star 🌟 支持一下!