Files
blog-press/docs/Others/VitePress.md
2025-12-21 17:17:42 +08:00

13 KiB
Raw Blame History

title, date
title date
VitePress简介和使用 2025-12-18

一、简介

VitePress是由 Vue 团队开发的静态网站生成器 (SSG),基于 Vite前端构建工具和 Vue 3 构建,专门用于快速搭建文档网站、博客或个人主页。它继承了 Vite 的极速开发体验,同时具备 Vue 的组件化能力,语法上兼容 Markdown 并支持扩展 Vue 组件,是替代 VuePress 的新一代工具。

二、快速开始

2.1 安装

pnpm add -D vitepress@next

2.2 初始化

pnpm vitepress init

::: tip 建议按照官网配置,文档目录设置为./docs
:::

2.3 运行

pnpm run docs:dev

2.4 使用

  在docs文件夹新建md文档,并在.vitepress/config.mts中配置navsidebar路径。

三、优化

3.1 路由组件

  每次新建文档时,都需要在.vitepress/config.mts重复navsidebar路径,有时还需要在文档目录的index.md配置导航链接每次都要重复写3次很麻烦因此可以构造一个路由配置清单和路由组件每次只需更新一次路由配置清单即可。
  在.vitepress/theme/router文件夹下新建路由清单文件:

export const routers = [
  { 
    text: '🌐 Web前端', 
    items: [
      { 
        text: '🌿 Vue',
        items: [
          { text: 'Test', link: '/Web-Front/Vue/Test' }
        ]
      },
      { 
        text: '🔄 其他',
        items: [
          { text: 'Test', link: '/Web-Front/Others/Test' }    
        ]
      },
    ]  
  },
  { 
    text: '🖥️ Web后端', 
    items: [
      { 
        text: '🍃 SpringBoot',
        items: [
          { text: 'Test', link: '/Web-Backend/SpringBoot/Test' }
        ]
      },
      {
        text: '🐍 FastAPI',
        items: [
          { text: 'Test', link: '/Web-Backend/FastAPI/Test' }
        ]
      },
      { 
        text: '🔄 其他',
        items: [
          { text: 'Test', link: '/Web-Backend/Others/Test' }
        ]
      },
    ]
  },
  { 
    text: '🚀 DevOps', 
    items: [ 
      { text: 'Test', link: '/DevOps/Test' }
    ]
  }
];

  这里列举了有二级菜单和只有一级菜单的两种情况。
  在.vitepress/theme/components文件夹下新建路由组件MenuList.vue

<template>
  <div v-if="isNested">
    <div v-for="category in props.routers.items" :key="category.text">
      <h3>{{ category.text }}</h3>
      <ul>
        <li v-for="item in category.items" :key="item.text">
            <a :href="withBase(item.link)">{{ item.text }}</a>
        </li>
      </ul>
    </div>
  </div>

  <div v-else>
    <ul>
      <li v-for="item in props.routers.items" :key="item.text">
          <a :href="withBase(item.link)">{{ item.text }}</a>
        </li>
    </ul>
  </div>
</template>

<script setup>
import { withBase } from 'vitepress'
import { defineProps } from 'vue'

const props = defineProps({
  routers: {
    required: true,
    type: Object
  },
  isNested: {
    type: Boolean,
    default: false
  }
})  
</script>

  这里需要使用withBase拼接路由,防止存在设置了base根路径的情况。通过isNested来控制是否存在二级菜单。
::: warning 这里只考虑了最多存在二级菜单的情况。
:::

  在.vitepress/theme/index.ts注册组件:

import MenuList from './components/MenuList.vue'
import type { EnhanceAppContext } from 'vitepress'

export default {
  enhanceApp({ app }: EnhanceAppContext) {
    app.component("MenuList", MenuList);
  },
};

  在.vitepress/config.mts中配置navsidebar

export default defineConfig({
  themeConfig: {
    nav: [
      { text: '🏠 Home', link: '/' },
      ...routers
    ],
    sidebar: {
      '/Web-Front/': [routers[0]],
      '/Web-Backend/': [routers[1]],
      '/DevOps/': [routers[2]]
    },
  },

  lastUpdated: true,
})

  在文档所在目录的index.md中配置:

---
layout: doc
title: Web前端
description: Web前端开发技术文档
---

<script setup>
import { routers } from '../.vitepress/theme/router'
</script>

<MenuList :routers=routers[0] :isNested=true />

  这样后续只需要维护路由配置清单文件即可自动生成路由菜单。

3.2 博客元数据

  可以通过在每个博客中添加frontmatter元数据,在最顶部添加博客属性,例如创建日期、字数、时长和是否精品等信息。

3.2.1 定义元数据

  在每个博客最上面添加元数据:

---
  title: Docker简介和安装
  date: 2025-12-15
  isGreat: true
---

  这里可以自定义添加任何key: value形式的字段信息。
::: tip 建议手动加上每个博客的创建日期 :::

3.2.2 博客信息组件

  在.vitepress/theme/components文件夹下新建博客信息组件ArticleMetadata.vue

<template>
    <div class="title">
        <span>{{ page.frontmatter.title }}</span>
    </div>

    <div v-if="!page.filePath.includes('index.md')" class="blog-stats">
        <div v-if="page.frontmatter.isGreat" class="item">
            </svg>            
            精品
        </div>

        <div class="item">
            </svg>
            日期: {{ page.frontmatter.date.split('T')[0] }}
        </div>

        <div class="item">
            </svg>
            字数: {{ formatNumberUnit(wordCount)  }} 
        </div>

        <div>
            </svg>
            时长: {{ readTime }} 分钟
        </div>
    </div>
</template>

<script lang="ts" setup>
import { computed, ref, onMounted, watch } from 'vue'
import { countWord, formatNumberUnit } from '../utils'
import { useData } from 'vitepress'

const wordCount = ref(0)
const imageCount = ref(0)

// 获取页面数据
const { page } = useData()

// 文字阅读时间
const wordTime = computed(() => {
    return ((wordCount.value / 275) * 60)
})

// 图片阅读时间
const imageTime = computed(() => {
    const n = imageCount.value
    if (imageCount.value <= 10) {
        // 等差数列求和
        return n * 13 + (n * (n - 1)) / 2
    }
    return 175 + (n - 10) * 3
})

// 阅读时间
const readTime = computed(() => {
    return Math.ceil((wordTime.value + imageTime.value) / 60)
})

const analyze = () => {
    // 选择文档内容区域
    const docDomContainer = window.document.querySelector('#VPContent')
    // 统计图片数量
    const imgs = docDomContainer?.querySelectorAll<HTMLImageElement>('.content-container .main img')
    imageCount.value = imgs?.length || 0

    // 统计文字数量textContent提取纯文本
    const words = docDomContainer?.querySelector('.content-container .main')?.textContent || ''
    wordCount.value = countWord(words)
}

watch(() => page.value.title, () => {
    // 路径变化时执行
    analyze()
})

onMounted(() => {
    // 初始化时执行一次
    analyze()
})
</script>

<style scoped>
.blog-stats {
    margin-bottom: 10px;

    display: flex;
    flex-wrap: wrap;
    gap: 5px;
}

.title {
    margin-bottom: 5px;
    padding: 10px 0;
    text-align: center;
    font-size: 32px;
    color: #fff;
    font-weight: bold;
    background: -webkit-linear-gradient(10deg, #3DC8F0 5%, #8036FA 15%);
    background-clip: text;
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
}

.icon {
    display: inline-block;
    transform: translate(0px , 2px);
}
</style>

  这里统计了正文部分的文字和图片数量,并通过相应规则计算阅读时间。
  通过page.frontmatter获取自定义的元数据信息并在博客最上面显示。

3.2.3 注册组件

  在.vitepress/theme/index.ts注册组件参考3.1节内容)。

3.2.4 自定义布局组件

  需要将博客信息组件放在自定义布局组件中。
  在.vitepress/theme/components文件夹下新建自定义布局组件MyLayout.vue

<template>
  <DefaultTheme.Layout v-bind="$attrs">
    <template #doc-before>
      <ArticleMetadat />
    </template>
  </DefaultTheme.Layout>
</template>

<script setup lang="ts">
import DefaultTheme from 'vitepress/theme'
import ArticleMetadat from './ArticleMetadata.vue';
</script>

  更多插槽可以参考Github源码
  同样需要注册组件。

3.2.5 配置布局组件

  在.vitepress/config.mts中配置布局:

import DefaultTheme from 'vitepress/theme'
import MyLayout from './components/MyLayout.vue';

export default {
  extends: DefaultTheme,
  Layout: MyLayout,
};

3.3 热力图

  使用vue3-calendar-heatmap组件在首页绘制博客热力图。

3.3.1 安装

pnpm install vue3-calendar-heatmap

3.3.2 获取数据

  使用VitePress提供的createContentLoader来获取所有的md博客数据。
  在.vitepress/data新建posts.data.js

import { createContentLoader } from 'vitepress'

export default createContentLoader('./**/*.md', {
  includeSrc: true, // 包含原始 markdown 源
  render: true,     // 包含渲染的整页 HTML
  transform(rawData) {
    return rawData.filter(page => {
      return page.url.includes('.html') && page.frontmatter.date
    }).map((page) => {
      // 对每个页面进行处理
      return {
        // 返回你需要的页面数据
        title: page.frontmatter.title,
        date: page.frontmatter.date,
        url: page.url
      }
    })
  }
})

  通过判断路径是否包含html来确认是博客文档,并且筛选出元数据中包含日期的博客,然后返回相应内容。

3.3.3 博客统计组件

  在.vitepress/theme/components文件夹下新建博客信息组件StatsChart.vue

<template>
  <div v-if="!isMobile()">
    <calendar-heatmap
      :values="heatmapData"
      :end-date="new Date()"
      no-data-text="暂无记录"
      tooltip-unit=""
      :round="2"
      :locale="{
        less: '少于',
        more: '多于',
        months: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'],
        days: ['日', '一', '二', '三', '四', '五', '六'],
      }"
      class="blog-heatmap"
    />
    <div class="heatmap-footer">
      ⏱️ 已坚持<span class="highlight">{{ getDaysDifference(new Date(), startBlogDate) }}</span>  
      | 📝 已创作<span class="highlight">{{  data.length }}</span> 
    </div>
  </div>
</template>

<script setup lang="ts">
import { CalendarHeatmap } from 'vue3-calendar-heatmap';
import 'vue3-calendar-heatmap/dist/style.css';
import { data } from '../../data/posts.data.js'
import { isMobile, getDaysDifference, startBlogDate } from "../utils";

const dateCountMap = new Map<string, number>();

// 按日期分组统计
data.forEach(item => {
  const date = item.date

  if (dateCountMap.has(date)) {
    dateCountMap.set(date, dateCountMap.get(date)! + 1);
  } else {
    dateCountMap.set(date, 1);
  }
});
  
// 转换为目标格式
const heatmapData = Array.from(dateCountMap.entries()).map(([date, count]) => ({
  date,
  count
}))
</script>

<style scoped>
.blog-heatmap {
  margin-top: 10px;
}

.blog-heatmap :deep(.vch__wrapper) {
  font-family: 'CustomFont', sans-serif !important;
}

.blog-heatmap :deep(.vch__month__label) {
  font-size: 8px !important;
}

.blog-heatmap :deep(.vch__day__label) {
  font-size: 8px !important;
}

.heatmap-footer {
  font-size: 15px;
  color: #666;
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 6px 12px;
}

.highlight {
  font-weight: 700;
  color: #6366f1;
  margin: 0 2px;
  font-size: 16px;
}
</style>

  根据posts.data.js提供的博客数据进行日期分组,并统计已坚持天数和已创作博客数。

3.3.4 注册组件

  在.vitepress/theme/index.ts注册组件参考3.1节内容)。

3.3.5 使用组件

  在docs/index.md最后添加组件。

::: tip 如果在打包时遇到Named export 'CalendarHeatmap' not found. The requested module 'vue3-calendar-heatmap' is a CommonJS module, which may not support all module.exports as named exports.错误提示,说明vue3-calendar-heatmapCommonJS模块,而你在代码中使用了ES模块的命名导入方式,需要在config.mts中添加以下配置:

export default defineConfig({
  vite: {
    ssr: {
      noExternal: ['vue3-calendar-heatmap']
    }
  }
})

:::

3.4 Mermaid 图表

3.4.1 安装

pnpm i vitepress-plugin-mermaid mermaid -D

3.4.2 配置

  在.vitepress/config.mts中配置:

import { withMermaid } from 'vitepress-plugin-mermaid'

// https://vitepress.dev/reference/site-config
export default withMermaid({
  title: "拾光记"
})

3.4.3 使用

  在Markdown中使用:

'''mermaid
graph TD
    A[Vue 3] --> B[Compiler]
    A --> C[Runtime]
    A --> D[Reactivity]

    B --> B1[Parser]
    B --> B2[Transformer]
    B --> B3[Codegen]

    C --> C1[Virtual DOM]
    C --> C2[Renderer]
    C --> C3[Component]

    D --> D1[Proxy]
    D --> D2[Effect]
    D --> D3[Dependency Graph]

    B3 -->|生成| C1
    C1 -->|Diff/Patch| C2
    D3 -->|驱动更新| C1
'''