VitePress 主题定制完全指南
VitePress 提供了强大的主题定制能力,让你可以创建独特且专业的文档站点。本文将深入探讨 VitePress 主题定制的各个方面,从基础配置到高级自定义技巧,帮助你打造完美的文档体验。
目录
主题系统概述
VitePress 主题架构
VitePress 采用分层的主题架构:
主题层级
├── 默认主题 (Default Theme)
├── 自定义主题 (Custom Theme)
├── 主题扩展 (Theme Extensions)
└── 组件覆盖 (Component Overrides)
主题配置基础
javascript
// .vitepress/config.js
export default {
// 基础主题配置
themeConfig: {
// 导航栏
nav: [
{ text: '首页', link: '/' },
{ text: '指南', link: '/guide/' },
{ text: '配置', link: '/config/' }
],
// 侧边栏
sidebar: {
'/guide/': [
{
text: '开始',
items: [
{ text: '介绍', link: '/guide/introduction' },
{ text: '快速开始', link: '/guide/getting-started' }
]
}
]
},
// 社交链接
socialLinks: [
{ icon: 'github', link: 'https://github.com/vuejs/vitepress' }
],
// 页脚
footer: {
message: '基于 MIT 许可发布',
copyright: 'Copyright © 2025 VitePress'
}
}
}
CSS 变量定制
1. 颜色系统定制
css
/* .vitepress/theme/custom.css */
:root {
/* 品牌色彩 */
--vp-c-brand-1: #3eaf7c;
--vp-c-brand-2: #369870;
--vp-c-brand-3: #2d8063;
--vp-c-brand-soft: rgba(62, 175, 124, 0.14);
/* 背景色 */
--vp-c-bg: #ffffff;
--vp-c-bg-alt: #f6f6f7;
--vp-c-bg-elv: #ffffff;
--vp-c-bg-soft: #f6f6f7;
/* 文本色 */
--vp-c-text-1: rgba(60, 60, 67);
--vp-c-text-2: rgba(60, 60, 67, 0.78);
--vp-c-text-3: rgba(60, 60, 67, 0.56);
/* 边框色 */
--vp-c-divider: rgba(60, 60, 67, 0.29);
--vp-c-border: rgba(60, 60, 67, 0.23);
/* 控件色 */
--vp-c-gutter: rgba(60, 60, 67, 0.12);
--vp-c-neutral: rgba(60, 60, 67, 0.08);
--vp-c-neutral-inverse: rgba(255, 255, 255, 0.95);
}
/* 深色模式 */
.dark {
--vp-c-bg: #1b1b1f;
--vp-c-bg-alt: #161618;
--vp-c-bg-elv: #202127;
--vp-c-bg-soft: #202127;
--vp-c-text-1: rgba(255, 255, 245, 0.86);
--vp-c-text-2: rgba(235, 235, 245, 0.6);
--vp-c-text-3: rgba(235, 235, 245, 0.38);
--vp-c-divider: rgba(84, 84, 88, 0.65);
--vp-c-border: rgba(82, 82, 89, 0.68);
}
2. 字体系统定制
css
:root {
/* 字体族 */
--vp-font-family-base: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--vp-font-family-mono: 'Fira Code', Menlo, Monaco, Consolas, 'Courier New', monospace;
/* 字体大小 */
--vp-font-size-root: 16px;
--vp-font-size-xs: 12px;
--vp-font-size-sm: 14px;
--vp-font-size-md: 16px;
--vp-font-size-lg: 18px;
--vp-font-size-xl: 20px;
/* 行高 */
--vp-line-height-xs: 1.4;
--vp-line-height-sm: 1.5;
--vp-line-height-md: 1.6;
--vp-line-height-lg: 1.7;
}
/* 自定义字体加载 */
@font-face {
font-family: 'CustomFont';
src: url('./fonts/custom-font.woff2') format('woff2');
font-display: swap;
}
/* 应用自定义字体 */
.custom-font {
font-family: 'CustomFont', var(--vp-font-family-base);
}
3. 布局尺寸定制
css
:root {
/* 容器宽度 */
--vp-layout-max-width: 1440px;
--vp-content-max-width: 688px;
--vp-sidebar-width-small: 272px;
--vp-sidebar-width-medium: 320px;
/* 间距系统 */
--vp-space-xs: 4px;
--vp-space-sm: 8px;
--vp-space-md: 16px;
--vp-space-lg: 24px;
--vp-space-xl: 32px;
--vp-space-2xl: 48px;
/* 圆角 */
--vp-border-radius-xs: 2px;
--vp-border-radius-sm: 4px;
--vp-border-radius-md: 6px;
--vp-border-radius-lg: 8px;
--vp-border-radius-xl: 12px;
/* 阴影 */
--vp-shadow-1: 0 1px 2px rgba(0, 0, 0, 0.04), 0 1px 2px rgba(0, 0, 0, 0.06);
--vp-shadow-2: 0 3px 12px rgba(0, 0, 0, 0.07), 0 1px 4px rgba(0, 0, 0, 0.07);
--vp-shadow-3: 0 12px 32px rgba(0, 0, 0, 0.1), 0 2px 6px rgba(0, 0, 0, 0.08);
}
组件定制
1. 导航栏定制
vue
<!-- .vitepress/theme/components/CustomNavBar.vue -->
<template>
<div class="custom-nav">
<div class="nav-container">
<!-- Logo -->
<div class="nav-logo">
<img src="/logo.svg" alt="Logo" />
<span class="nav-title">{{ site.title }}</span>
</div>
<!-- 导航菜单 -->
<nav class="nav-menu">
<div
v-for="item in nav"
:key="item.text"
class="nav-item"
:class="{ active: isActive(item.link) }"
>
<a :href="item.link" class="nav-link">
{{ item.text }}
</a>
<!-- 下拉菜单 -->
<div v-if="item.items" class="nav-dropdown">
<div
v-for="subItem in item.items"
:key="subItem.text"
class="nav-dropdown-item"
>
<a :href="subItem.link">{{ subItem.text }}</a>
</div>
</div>
</div>
</nav>
<!-- 搜索和主题切换 -->
<div class="nav-actions">
<SearchBox />
<ThemeToggle />
<SocialLinks />
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useData, useRouter } from 'vitepress'
import SearchBox from './SearchBox.vue'
import ThemeToggle from './ThemeToggle.vue'
import SocialLinks from './SocialLinks.vue'
const { site, theme } = useData()
const router = useRouter()
const nav = computed(() => theme.value.nav || [])
const isActive = (link) => {
return router.route.path.startsWith(link)
}
</script>
<style scoped>
.custom-nav {
position: sticky;
top: 0;
z-index: 10;
background: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
backdrop-filter: blur(8px);
}
.nav-container {
display: flex;
align-items: center;
justify-content: space-between;
max-width: var(--vp-layout-max-width);
margin: 0 auto;
padding: 0 var(--vp-space-lg);
height: 64px;
}
.nav-logo {
display: flex;
align-items: center;
gap: var(--vp-space-sm);
font-weight: 600;
font-size: var(--vp-font-size-lg);
}
.nav-logo img {
width: 32px;
height: 32px;
}
.nav-menu {
display: flex;
align-items: center;
gap: var(--vp-space-lg);
}
.nav-item {
position: relative;
}
.nav-link {
color: var(--vp-c-text-1);
text-decoration: none;
font-weight: 500;
transition: color 0.2s ease;
}
.nav-link:hover,
.nav-item.active .nav-link {
color: var(--vp-c-brand-1);
}
.nav-dropdown {
position: absolute;
top: 100%;
left: 0;
background: var(--vp-c-bg-elv);
border: 1px solid var(--vp-c-border);
border-radius: var(--vp-border-radius-md);
box-shadow: var(--vp-shadow-2);
opacity: 0;
visibility: hidden;
transform: translateY(-8px);
transition: all 0.2s ease;
min-width: 200px;
}
.nav-item:hover .nav-dropdown {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.nav-dropdown-item {
padding: var(--vp-space-sm) var(--vp-space-md);
}
.nav-dropdown-item a {
color: var(--vp-c-text-2);
text-decoration: none;
display: block;
transition: color 0.2s ease;
}
.nav-dropdown-item a:hover {
color: var(--vp-c-brand-1);
}
.nav-actions {
display: flex;
align-items: center;
gap: var(--vp-space-md);
}
@media (max-width: 768px) {
.nav-menu {
display: none;
}
.nav-container {
padding: 0 var(--vp-space-md);
}
}
</style>
2. 侧边栏定制
vue
<!-- .vitepress/theme/components/CustomSidebar.vue -->
<template>
<aside class="custom-sidebar">
<div class="sidebar-content">
<!-- 搜索框 -->
<div class="sidebar-search">
<SearchBox />
</div>
<!-- 导航树 -->
<nav class="sidebar-nav">
<div
v-for="group in sidebarGroups"
:key="group.text"
class="sidebar-group"
>
<h3 class="sidebar-group-title">
{{ group.text }}
</h3>
<ul class="sidebar-group-items">
<li
v-for="item in group.items"
:key="item.link"
class="sidebar-item"
:class="{
active: isActive(item.link),
collapsed: item.collapsed
}"
>
<a
:href="item.link"
class="sidebar-link"
@click="handleItemClick(item)"
>
<span class="sidebar-link-text">{{ item.text }}</span>
<span
v-if="item.items"
class="sidebar-link-arrow"
:class="{ expanded: !item.collapsed }"
>
<ChevronRightIcon />
</span>
</a>
<!-- 子菜单 -->
<ul
v-if="item.items && !item.collapsed"
class="sidebar-sub-items"
>
<li
v-for="subItem in item.items"
:key="subItem.link"
class="sidebar-sub-item"
:class="{ active: isActive(subItem.link) }"
>
<a :href="subItem.link" class="sidebar-sub-link">
{{ subItem.text }}
</a>
</li>
</ul>
</li>
</ul>
</div>
</nav>
<!-- 页面导航 */
<div class="sidebar-toc">
<h4 class="toc-title">本页内容</h4>
<TableOfContents />
</div>
</div>
</aside>
</template>
<script setup>
import { computed, ref } from 'vue'
import { useData, useRouter } from 'vitepress'
import SearchBox from './SearchBox.vue'
import TableOfContents from './TableOfContents.vue'
import ChevronRightIcon from './icons/ChevronRightIcon.vue'
const { theme } = useData()
const router = useRouter()
const collapsedItems = ref(new Set())
const sidebarGroups = computed(() => {
const sidebar = theme.value.sidebar
const currentPath = router.route.path
// 根据当前路径获取对应的侧边栏配置
for (const path in sidebar) {
if (currentPath.startsWith(path)) {
return sidebar[path].map(group => ({
...group,
items: group.items?.map(item => ({
...item,
collapsed: collapsedItems.value.has(item.link)
}))
}))
}
}
return []
})
const isActive = (link) => {
return router.route.path === link
}
const handleItemClick = (item) => {
if (item.items) {
if (collapsedItems.value.has(item.link)) {
collapsedItems.value.delete(item.link)
} else {
collapsedItems.value.add(item.link)
}
}
}
</script>
<style scoped>
.custom-sidebar {
width: var(--vp-sidebar-width-medium);
height: 100vh;
position: fixed;
top: 0;
left: 0;
background: var(--vp-c-bg-alt);
border-right: 1px solid var(--vp-c-divider);
overflow-y: auto;
z-index: 5;
}
.sidebar-content {
padding: var(--vp-space-lg);
}
.sidebar-search {
margin-bottom: var(--vp-space-lg);
}
.sidebar-group {
margin-bottom: var(--vp-space-xl);
}
.sidebar-group-title {
font-size: var(--vp-font-size-sm);
font-weight: 600;
color: var(--vp-c-text-1);
margin: 0 0 var(--vp-space-md) 0;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.sidebar-group-items {
list-style: none;
padding: 0;
margin: 0;
}
.sidebar-item {
margin-bottom: var(--vp-space-xs);
}
.sidebar-link {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--vp-space-sm) var(--vp-space-md);
color: var(--vp-c-text-2);
text-decoration: none;
border-radius: var(--vp-border-radius-sm);
transition: all 0.2s ease;
}
.sidebar-link:hover {
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
}
.sidebar-item.active .sidebar-link {
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-1);
font-weight: 500;
}
.sidebar-link-arrow {
transition: transform 0.2s ease;
}
.sidebar-link-arrow.expanded {
transform: rotate(90deg);
}
.sidebar-sub-items {
list-style: none;
padding: 0;
margin: var(--vp-space-sm) 0 0 var(--vp-space-lg);
}
.sidebar-sub-item {
margin-bottom: var(--vp-space-xs);
}
.sidebar-sub-link {
display: block;
padding: var(--vp-space-xs) var(--vp-space-md);
color: var(--vp-c-text-3);
text-decoration: none;
border-radius: var(--vp-border-radius-sm);
transition: all 0.2s ease;
font-size: var(--vp-font-size-sm);
}
.sidebar-sub-link:hover {
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-2);
}
.sidebar-sub-item.active .sidebar-sub-link {
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-1);
font-weight: 500;
}
.sidebar-toc {
margin-top: var(--vp-space-2xl);
padding-top: var(--vp-space-lg);
border-top: 1px solid var(--vp-c-divider);
}
.toc-title {
font-size: var(--vp-font-size-sm);
font-weight: 600;
color: var(--vp-c-text-1);
margin: 0 0 var(--vp-space-md) 0;
}
@media (max-width: 1024px) {
.custom-sidebar {
transform: translateX(-100%);
transition: transform 0.3s ease;
}
.custom-sidebar.open {
transform: translateX(0);
}
}
</style>
3. 内容区域定制
vue
<!-- .vitepress/theme/components/CustomContent.vue -->
<template>
<main class="custom-content">
<div class="content-container">
<!-- 面包屑导航 -->
<Breadcrumb v-if="showBreadcrumb" />
<!-- 文章头部 -->
<header v-if="frontmatter.title" class="content-header">
<h1 class="content-title">{{ frontmatter.title }}</h1>
<div v-if="frontmatter.description" class="content-description">
{{ frontmatter.description }}
</div>
<div v-if="showMeta" class="content-meta">
<span v-if="frontmatter.author" class="meta-author">
作者: {{ frontmatter.author }}
</span>
<span v-if="frontmatter.date" class="meta-date">
发布于: {{ formatDate(frontmatter.date) }}
</span>
<span v-if="readingTime" class="meta-reading-time">
阅读时间: {{ readingTime }}
</span>
</div>
<div v-if="frontmatter.tags" class="content-tags">
<span
v-for="tag in frontmatter.tags"
:key="tag"
class="content-tag"
>
{{ tag }}
</span>
</div>
</header>
<!-- 文章内容 -->
<article class="content-body">
<Content />
</article>
<!-- 文章底部 -->
<footer class="content-footer">
<!-- 上一篇/下一篇导航 -->
<PrevNext />
<!-- 编辑链接 -->
<EditLink v-if="showEditLink" />
<!-- 最后更新时间 -->
<LastUpdated v-if="showLastUpdated" />
<!-- 分享按钮 -->
<ShareButtons v-if="showShare" />
</footer>
</div>
<!-- 右侧目录 -->
<aside v-if="showToc" class="content-toc">
<TableOfContents />
</aside>
</main>
</template>
<script setup>
import { computed } from 'vue'
import { useData } from 'vitepress'
import Breadcrumb from './Breadcrumb.vue'
import PrevNext from './PrevNext.vue'
import EditLink from './EditLink.vue'
import LastUpdated from './LastUpdated.vue'
import ShareButtons from './ShareButtons.vue'
import TableOfContents from './TableOfContents.vue'
const { page, frontmatter, theme } = useData()
const showBreadcrumb = computed(() =>
theme.value.breadcrumb !== false && frontmatter.value.breadcrumb !== false
)
const showMeta = computed(() =>
frontmatter.value.author || frontmatter.value.date
)
const showEditLink = computed(() =>
theme.value.editLink && frontmatter.value.editLink !== false
)
const showLastUpdated = computed(() =>
theme.value.lastUpdated && frontmatter.value.lastUpdated !== false
)
const showShare = computed(() =>
theme.value.share !== false && frontmatter.value.share !== false
)
const showToc = computed(() =>
theme.value.outline !== false && frontmatter.value.outline !== false
)
const readingTime = computed(() => {
const content = page.value.content
if (!content) return null
const wordsPerMinute = 200
const words = content.split(/\s+/).length
const minutes = Math.ceil(words / wordsPerMinute)
return `${minutes} 分钟`
})
const formatDate = (date) => {
return new Date(date).toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric'
})
}
</script>
<style scoped>
.custom-content {
display: flex;
max-width: var(--vp-layout-max-width);
margin: 0 auto;
padding: var(--vp-space-lg);
gap: var(--vp-space-2xl);
}
.content-container {
flex: 1;
max-width: var(--vp-content-max-width);
}
.content-header {
margin-bottom: var(--vp-space-2xl);
padding-bottom: var(--vp-space-lg);
border-bottom: 1px solid var(--vp-c-divider);
}
.content-title {
font-size: 2.5rem;
font-weight: 700;
line-height: 1.2;
color: var(--vp-c-text-1);
margin: 0 0 var(--vp-space-md) 0;
}
.content-description {
font-size: var(--vp-font-size-lg);
color: var(--vp-c-text-2);
line-height: var(--vp-line-height-md);
margin-bottom: var(--vp-space-lg);
}
.content-meta {
display: flex;
flex-wrap: wrap;
gap: var(--vp-space-md);
margin-bottom: var(--vp-space-md);
font-size: var(--vp-font-size-sm);
color: var(--vp-c-text-3);
}
.content-tags {
display: flex;
flex-wrap: wrap;
gap: var(--vp-space-sm);
}
.content-tag {
padding: var(--vp-space-xs) var(--vp-space-sm);
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-1);
border-radius: var(--vp-border-radius-sm);
font-size: var(--vp-font-size-xs);
font-weight: 500;
}
.content-body {
margin-bottom: var(--vp-space-2xl);
}
.content-footer {
padding-top: var(--vp-space-lg);
border-top: 1px solid var(--vp-c-divider);
}
.content-toc {
width: 240px;
position: sticky;
top: var(--vp-space-lg);
height: fit-content;
}
@media (max-width: 1024px) {
.custom-content {
flex-direction: column;
}
.content-toc {
width: 100%;
position: static;
}
}
@media (max-width: 768px) {
.custom-content {
padding: var(--vp-space-md);
}
.content-title {
font-size: 2rem;
}
.content-meta {
flex-direction: column;
gap: var(--vp-space-xs);
}
}
</style>
高级定制技巧
1. 自定义布局
vue
<!-- .vitepress/theme/layouts/CustomLayout.vue -->
<template>
<div class="custom-layout" :class="layoutClass">
<!-- 顶部横幅 -->
<Banner v-if="showBanner" />
<!-- 导航栏 -->
<CustomNavBar />
<!-- 主要内容区域 -->
<div class="layout-main">
<!-- 侧边栏 -->
<CustomSidebar v-if="showSidebar" />
<!-- 内容区域 -->
<div class="layout-content" :class="contentClass">
<CustomContent />
</div>
<!-- 右侧栏 -->
<aside v-if="showAside" class="layout-aside">
<slot name="aside" />
</aside>
</div>
<!-- 页脚 -->
<CustomFooter />
<!-- 回到顶部按钮 -->
<BackToTop />
<!-- 移动端菜单遮罩 -->
<div
v-if="showMobileOverlay"
class="mobile-overlay"
@click="closeMobileMenu"
/>
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
import { useData } from 'vitepress'
import Banner from '../components/Banner.vue'
import CustomNavBar from '../components/CustomNavBar.vue'
import CustomSidebar from '../components/CustomSidebar.vue'
import CustomContent from '../components/CustomContent.vue'
import CustomFooter from '../components/CustomFooter.vue'
import BackToTop from '../components/BackToTop.vue'
const { page, frontmatter, theme } = useData()
const showMobileMenu = ref(false)
const layoutClass = computed(() => ({
'has-sidebar': showSidebar.value,
'has-aside': showAside.value,
'mobile-menu-open': showMobileMenu.value
}))
const contentClass = computed(() => ({
'full-width': !showSidebar.value && !showAside.value,
'with-sidebar': showSidebar.value,
'with-aside': showAside.value
}))
const showBanner = computed(() =>
theme.value.banner && frontmatter.value.banner !== false
)
const showSidebar = computed(() =>
theme.value.sidebar && frontmatter.value.sidebar !== false
)
const showAside = computed(() =>
theme.value.aside && frontmatter.value.aside !== false
)
const showMobileOverlay = computed(() =>
showMobileMenu.value
)
const closeMobileMenu = () => {
showMobileMenu.value = false
}
</script>
<style scoped>
.custom-layout {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.layout-main {
flex: 1;
display: flex;
position: relative;
}
.layout-content {
flex: 1;
min-width: 0;
}
.layout-content.with-sidebar {
margin-left: var(--vp-sidebar-width-medium);
}
.layout-content.with-aside {
margin-right: 280px;
}
.layout-aside {
width: 280px;
position: fixed;
top: 64px;
right: 0;
height: calc(100vh - 64px);
overflow-y: auto;
background: var(--vp-c-bg-alt);
border-left: 1px solid var(--vp-c-divider);
padding: var(--vp-space-lg);
}
.mobile-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 9;
display: none;
}
@media (max-width: 1024px) {
.layout-content.with-sidebar {
margin-left: 0;
}
.layout-content.with-aside {
margin-right: 0;
}
.layout-aside {
display: none;
}
.mobile-menu-open .mobile-overlay {
display: block;
}
}
</style>
2. 插件系统
javascript
// .vitepress/theme/plugins/index.js
export class ThemePlugin {
constructor(name, options = {}) {
this.name = name
this.options = options
this.hooks = {}
}
// 注册钩子
hook(name, callback) {
if (!this.hooks[name]) {
this.hooks[name] = []
}
this.hooks[name].push(callback)
}
// 执行钩子
async executeHook(name, context = {}) {
const hooks = this.hooks[name] || []
for (const hook of hooks) {
await hook(context)
}
}
}
// 代码高亮插件
export class CodeHighlightPlugin extends ThemePlugin {
constructor(options = {}) {
super('code-highlight', options)
this.setupHighlight()
}
setupHighlight() {
this.hook('content:updated', async (context) => {
const codeBlocks = context.el.querySelectorAll('pre code')
codeBlocks.forEach(block => {
this.enhanceCodeBlock(block)
})
})
}
enhanceCodeBlock(block) {
const pre = block.parentElement
// 添加复制按钮
const copyButton = document.createElement('button')
copyButton.className = 'copy-code-button'
copyButton.textContent = '复制'
copyButton.onclick = () => this.copyCode(block)
pre.appendChild(copyButton)
// 添加行号
if (this.options.lineNumbers) {
this.addLineNumbers(block)
}
// 添加语言标签
const language = this.getLanguage(block)
if (language) {
const langLabel = document.createElement('span')
langLabel.className = 'code-language'
langLabel.textContent = language
pre.appendChild(langLabel)
}
}
copyCode(block) {
const code = block.textContent
navigator.clipboard.writeText(code).then(() => {
// 显示复制成功提示
this.showCopySuccess()
})
}
addLineNumbers(block) {
const lines = block.textContent.split('\n')
const lineNumbers = document.createElement('div')
lineNumbers.className = 'line-numbers'
lines.forEach((_, index) => {
const lineNumber = document.createElement('span')
lineNumber.textContent = index + 1
lineNumbers.appendChild(lineNumber)
})
block.parentElement.appendChild(lineNumbers)
}
getLanguage(block) {
const className = block.className
const match = className.match(/language-(\w+)/)
return match ? match[1] : null
}
showCopySuccess() {
// 实现复制成功提示
const toast = document.createElement('div')
toast.className = 'copy-toast'
toast.textContent = '代码已复制到剪贴板'
document.body.appendChild(toast)
setTimeout(() => {
document.body.removeChild(toast)
}, 2000)
}
}
// 图片懒加载插件
export class LazyLoadPlugin extends ThemePlugin {
constructor(options = {}) {
super('lazy-load', options)
this.observer = null
this.setupLazyLoad()
}
setupLazyLoad() {
this.hook('content:updated', (context) => {
this.observeImages(context.el)
})
this.createObserver()
}
createObserver() {
this.observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.loadImage(entry.target)
this.observer.unobserve(entry.target)
}
})
}, {
rootMargin: '50px'
})
}
observeImages(container) {
const images = container.querySelectorAll('img[data-src]')
images.forEach(img => {
this.observer.observe(img)
})
}
loadImage(img) {
const src = img.dataset.src
if (src) {
img.src = src
img.removeAttribute('data-src')
img.classList.add('loaded')
}
}
}
3. 主题配置系统
javascript
// .vitepress/theme/config/theme-config.js
export class ThemeConfig {
constructor() {
this.config = {
// 布局配置
layout: {
navbar: true,
sidebar: true,
aside: true,
footer: true
},
// 样式配置
style: {
colorScheme: 'auto', // light, dark, auto
primaryColor: '#3eaf7c',
fontFamily: 'Inter',
borderRadius: '6px'
},
// 功能配置
features: {
search: true,
darkMode: true,
editLink: true,
lastUpdated: true,
prevNext: true,
outline: true
},
// 插件配置
plugins: {
codeHighlight: {
enabled: true,
lineNumbers: true,
copyButton: true
},
lazyLoad: {
enabled: true,
placeholder: '/images/placeholder.svg'
}
}
}
}
// 合并配置
merge(userConfig) {
this.config = this.deepMerge(this.config, userConfig)
return this
}
// 深度合并对象
deepMerge(target, source) {
const result = { ...target }
for (const key in source) {
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
result[key] = this.deepMerge(target[key] || {}, source[key])
} else {
result[key] = source[key]
}
}
return result
}
// 获取配置值
get(path, defaultValue = null) {
const keys = path.split('.')
let current = this.config
for (const key of keys) {
if (current && typeof current === 'object' && key in current) {
current = current[key]
} else {
return defaultValue
}
}
return current
}
// 设置配置值
set(path, value) {
const keys = path.split('.')
const lastKey = keys.pop()
let current = this.config
for (const key of keys) {
if (!(key in current) || typeof current[key] !== 'object') {
current[key] = {}
}
current = current[key]
}
current[lastKey] = value
}
// 应用样式配置
applyStyles() {
const root = document.documentElement
const style = this.get('style')
if (style.primaryColor) {
root.style.setProperty('--vp-c-brand-1', style.primaryColor)
}
if (style.fontFamily) {
root.style.setProperty('--vp-font-family-base', style.fontFamily)
}
if (style.borderRadius) {
root.style.setProperty('--vp-border-radius-md', style.borderRadius)
}
}
}
// 使用示例
const themeConfig = new ThemeConfig()
.merge({
style: {
primaryColor: '#007bff',
fontFamily: 'Roboto'
},
features: {
search: false
}
})
export default themeConfig
实战案例
1. 企业文档站点主题
javascript
// .vitepress/config.js - 企业主题配置
export default {
title: '企业产品文档',
description: '专业的企业级产品文档站点',
themeConfig: {
// 企业 Logo 和品牌
logo: '/logo-enterprise.svg',
siteTitle: '企业文档中心',
// 企业色彩方案
primaryColor: '#1890ff',
// 导航配置
nav: [
{ text: '产品介绍', link: '/product/' },
{ text: 'API 文档', link: '/api/' },
{ text: '最佳实践', link: '/best-practices/' },
{ text: '支持中心', link: '/support/' }
],
// 企业功能
features: {
feedback: true,
analytics: true,
search: {
provider: 'algolia',
options: {
appId: 'YOUR_APP_ID',
apiKey: 'YOUR_API_KEY',
indexName: 'docs'
}
}
},
// 页脚信息
footer: {
message: '© 2025 企业名称. 保留所有权利.',
links: [
{ text: '隐私政策', link: '/privacy' },
{ text: '服务条款', link: '/terms' },
{ text: '联系我们', link: '/contact' }
]
}
}
}
2. 开源项目文档主题
javascript
// .vitepress/config.js - 开源项目主题配置
export default {
title: 'OpenSource Project',
description: '开源项目文档',
themeConfig: {
// GitHub 集成
repo: 'username/project',
repoLabel: 'GitHub',
editLinks: true,
editLinkText: '在 GitHub 上编辑此页',
// 社交链接
socialLinks: [
{ icon: 'github', link: 'https://github.com/username/project' },
{ icon: 'twitter', link: 'https://twitter.com/username' },
{ icon: 'discord', link: 'https://discord.gg/invite' }
],
// 贡献者展示
contributors: true,
// 版本管理
versions: [
{ text: 'v2.0', link: '/v2/' },
{ text: 'v1.0', link: '/v1/' }
],
// 开源特色功能
features: {
stargazers: true,
issues: true,
discussions: true
}
}
}
性能优化
1. 样式优化
css
/* 关键 CSS 内联 */
:root {
/* 只包含首屏必需的 CSS 变量 */
--vp-c-brand-1: #3eaf7c;
--vp-c-bg: #ffffff;
--vp-c-text-1: rgba(60, 60, 67);
}
/* 非关键 CSS 异步加载 */
@media print {
/* 打印样式 */
}
/* 预加载字体 */
@font-face {
font-family: 'Inter';
src: url('./fonts/inter.woff2') format('woff2');
font-display: swap;
}
/* 优化动画性能 */
.smooth-transition {
transition: transform 0.2s ease;
will-change: transform;
}
.smooth-transition:hover {
transform: translateY(-2px);
}
2. JavaScript 优化
javascript
// 组件懒加载
const LazyComponent = defineAsyncComponent({
loader: () => import('./HeavyComponent.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorComponent,
delay: 200,
timeout: 3000
})
// 防抖搜索
import { debounce } from 'lodash-es'
const debouncedSearch = debounce((query) => {
// 执行搜索
}, 300)
// 虚拟滚动(大列表优化)
import { RecycleScroller } from 'vue-virtual-scroller'
export default {
components: {
RecycleScroller
}
}
总结
VitePress 主题定制提供了强大的灵活性,让你可以创建独特的文档体验。关键要点:
- 渐进式定制:从 CSS 变量开始,逐步深入到组件定制
- 组件化思维:将复杂功能拆分为可复用的组件
- 性能优先:始终考虑加载性能和用户体验
- 可维护性:保持代码结构清晰,便于后续维护
通过合理运用这些技巧,你可以打造出既美观又实用的文档站点。