Skip to content

Vue 组件开发指南

VitePress 基于 Vue 3 构建,允许你在主题中使用 Vue 组件来扩展功能。本文档将详细介绍如何在主题中开发和使用 Vue 组件。

版本说明

  • 本文档基于 VitePress v1.0.0+ 和 Vue 3.3+ 编写
  • 组合式 API(Composition API)是推荐的开发方式
  • 需要具备 Vue 3 基础知识
  • 支持 Node.js 18.0.0 及以上版本

组件开发基础

VitePress 中的 Vue 环境

VitePress 使用 Vue 3 的单文件组件(SFC)系统,提供了以下特性:

特性支持情况说明
<script setup>✅ 完全支持推荐使用组合式 API
TypeScript✅ 完全支持类型推导和类型检查
CSS Scoped✅ 完全支持样式隔离
CSS Modules✅ 完全支持CSS 模块化
Pre-processors✅ 支持Sass、Less、Stylus

组件文件结构

docs/
├── .vitepress/
│   ├── theme/
│   │   ├── index.ts          # 主题入口
│   │   └── components/       # 自定义组件
│   │       ├── MyComponent.vue
│   │       └── AnotherComponent.vue
│   └── ...
└── ...

组件注册方式

方式一:全局注册

.vitepress/theme/index.ts 中全局注册组件:

typescript
// .vitepress/theme/index.ts
import DefaultTheme from 'vitepress/theme'
import MyComponent from './components/MyComponent.vue'
import AnotherComponent from './components/AnotherComponent.vue'

export default {
  extends: DefaultTheme,
  enhanceApp({ app }) {
    // 全局注册组件
    app.component('MyComponent', MyComponent)
    app.component('AnotherComponent', AnotherComponent)
  }
}

优点:

  • 在任何地方直接使用组件
  • 适合频繁使用的组件
  • 简化使用流程

缺点:

  • 可能影响打包体积
  • 全局命名空间污染

方式二:按需导入

在 Markdown 文件或 Vue 组件中按需导入:

vue
<script setup>
import MyComponent from './.vitepress/theme/components/MyComponent.vue'
</script>

<template>
  <MyComponent />
</template>

优点:

  • 更好的代码分割
  • 按需加载,优化性能
  • 适合大型组件

缺点:

  • 每次都需要导入
  • 路径可能较长

方式三:自动导入(推荐)

使用 unplugin-vue-components 自动导入组件:

bash
npm install -D unplugin-vue-components
typescript
// .vitepress/theme/index.ts
import DefaultTheme from 'vitepress/theme'
import { AntDesignContainer } from 'vitepress-theme-demoblock'

export default {
  extends: DefaultTheme,
  enhanceApp({ app }) {
    // 注册 Demo 容器
    app.use(AntDesignContainer)
  }
}
typescript
// vite.config.ts
import { defineConfig } from 'vite'
import Components from 'unplugin-vue-components/vite'

export default defineConfig({
  plugins: [
    Components({
      dirs: ['.vitepress/theme/components'],
      extensions: ['vue'],
      dts: '.vitepress/components.d.ts'
    })
  ]
})

组件开发示例

基础组件

创建一个简单的卡片组件:

vue
<!-- .vitepress/theme/components/InfoCard.vue -->
<template>
  <div class="info-card" :class="`info-card--${type}`">
    <div v-if="icon" class="info-card__icon">{{ icon }}</div>
    <div class="info-card__content">
      <h3 v-if="title" class="info-card__title">{{ title }}</h3>
      <p class="info-card__description"><slot /></p>
    </div>
  </div>
</template>

<script setup lang="ts">
interface Props {
  title?: string
  type?: 'info' | 'warning' | 'error' | 'success'
  icon?: string
}

withDefaults(defineProps<Props>(), {
  type: 'info',
  title: '',
  icon: ''
})
</script>

<style scoped>
.info-card {
  padding: 1rem;
  border-radius: 8px;
  border-left: 4px solid;
  margin: 1rem 0;
}

.info-card--info {
  background-color: var(--vp-c-brand-soft);
  border-color: var(--vp-c-brand-1);
}

.info-card--warning {
  background-color: #fff3cd;
  border-color: #ffc107;
}

.info-card--error {
  background-color: #f8d7da;
  border-color: #dc3545;
}

.info-card--success {
  background-color: #d4edda;
  border-color: #28a745;
}

.info-card__icon {
  font-size: 1.5rem;
  margin-bottom: 0.5rem;
}

.info-card__title {
  margin: 0 0 0.5rem;
  font-size: 1.1rem;
  font-weight: 600;
}

.info-card__description {
  margin: 0;
  line-height: 1.6;
}
</style>

使用方式:

vue
<InfoCard title="提示" type="info" icon="💡">
  这是一个信息提示卡片。
</InfoCard>

交互式组件

创建一个标签切换组件:

vue
<!-- .vitepress/theme/components/TabSwitch.vue -->
<template>
  <div class="tab-switch">
    <div class="tab-switch__tabs">
      <button
        v-for="(tab, index) in tabs"
        :key="index"
        class="tab-switch__tab"
        :class="{ 'tab-switch__tab--active': activeIndex === index }"
        @click="activeIndex = index"
      >
        {{ tab.label }}
      </button>
    </div>
    <div class="tab-switch__content">
      <div
        v-for="(tab, index) in tabs"
        :key="index"
        v-show="activeIndex === index"
        class="tab-switch__panel"
      >
        <component :is="tab.component" v-if="tab.component" />
        <div v-else v-html="tab.content"></div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

interface Tab {
  label: string
  content?: string
  component?: any
}

interface Props {
  tabs: Tab[]
}

const props = defineProps<Props>()
const activeIndex = ref(0)
</script>

<style scoped>
.tab-switch {
  margin: 1rem 0;
}

.tab-switch__tabs {
  display: flex;
  gap: 0.5rem;
  border-bottom: 2px solid var(--vp-c-divider);
  margin-bottom: 1rem;
}

.tab-switch__tab {
  padding: 0.5rem 1rem;
  background: none;
  border: none;
  cursor: pointer;
  font-size: 0.9rem;
  color: var(--vp-c-text-2);
  transition: all 0.2s;
}

.tab-switch__tab:hover {
  color: var(--vp-c-text-1);
}

.tab-switch__tab--active {
  color: var(--vp-c-brand-1);
  border-bottom: 2px solid var(--vp-c-brand-1);
  margin-bottom: -2px;
}

.tab-switch__panel {
  padding: 1rem 0;
}
</style>

数据驱动组件

创建一个文章列表组件:

vue
<!-- .vitepress/theme/components/ArticleList.vue -->
<template>
  <div class="article-list">
    <div
      v-for="article in articles"
      :key="article.path"
      class="article-item"
    >
      <a :href="article.path" class="article-item__link">
        <h3 class="article-item__title">{{ article.title }}</h3>
        <p v-if="article.description" class="article-item__desc">
          {{ article.description }}
        </p>
        <div class="article-item__meta">
          <span v-if="article.date">{{ formatDate(article.date) }}</span>
          <span v-if="article.tags" class="article-item__tags">
            <span
              v-for="tag in article.tags"
              :key="tag"
              class="article-item__tag"
            >
              {{ tag }}
            </span>
          </span>
        </div>
      </a>
    </div>
  </div>
</template>

<script setup lang="ts">
import { computed } from 'vue'
import { data as posts } from './posts.data'

interface Article {
  title: string
  path: string
  description?: string
  date?: string
  tags?: string[]
}

interface Props {
  limit?: number
  tags?: string[]
}

const props = withDefaults(defineProps<Props>(), {
  limit: 10,
  tags: () => []
})

const articles = computed(() => {
  let result = posts as Article[]
  
  // 按标签过滤
  if (props.tags.length > 0) {
    result = result.filter(article =>
      article.tags?.some(tag => props.tags.includes(tag))
    )
  }
  
  // 限制数量
  return result.slice(0, props.limit)
})

function formatDate(date: string) {
  return new Date(date).toLocaleDateString('zh-CN', {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  })
}
</script>

<style scoped>
.article-list {
  display: grid;
  gap: 1rem;
}

.article-item {
  border: 1px solid var(--vp-c-divider);
  border-radius: 8px;
  transition: all 0.2s;
}

.article-item:hover {
  border-color: var(--vp-c-brand-1);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.article-item__link {
  display: block;
  padding: 1.5rem;
  text-decoration: none;
  color: inherit;
}

.article-item__title {
  margin: 0 0 0.5rem;
  font-size: 1.2rem;
  color: var(--vp-c-text-1);
}

.article-item__desc {
  margin: 0 0 1rem;
  color: var(--vp-c-text-2);
  line-height: 1.6;
}

.article-item__meta {
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: 0.85rem;
  color: var(--vp-c-text-3);
}

.article-item__tags {
  display: flex;
  gap: 0.5rem;
}

.article-item__tag {
  padding: 0.2rem 0.5rem;
  background: var(--vp-c-brand-soft);
  border-radius: 4px;
  font-size: 0.8rem;
}
</style>

组件间通信

Props 和 Emits

vue
<!-- 父组件 Parent.vue -->
<template>
  <ChildComponent
    :message="parentMessage"
    @update="handleUpdate"
  />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'

const parentMessage = ref('来自父组件的消息')

function handleUpdate(newValue: string) {
  parentMessage.value = newValue
}
</script>

<!-- 子组件 ChildComponent.vue -->
<template>
  <div>
    <p>{{ message }}</p>
    <button @click="updateMessage">更新消息</button>
  </div>
</template>

<script setup lang="ts">
interface Props {
  message: string
}

const props = defineProps<Props>()
const emit = defineEmits<{
  update: [value: string]
}>()

function updateMessage() {
  emit('update', '来自子组件的新消息')
}
</script>

Provide/Inject

适合深层嵌套组件通信:

typescript
// 在父组件提供数据
import { provide, ref } from 'vue'

const theme = ref('light')
provide('theme', theme)
typescript
// 在子组件注入数据
import { inject } from 'vue'

const theme = inject('theme')

使用 Composables

创建可复用的逻辑:

typescript
// .vitepress/theme/composables/useTheme.ts
import { ref, watch } from 'vue'

export function useTheme() {
  const isDark = ref(false)

  // 监听主题变化
  watch(isDark, (val) => {
    if (val) {
      document.documentElement.classList.add('dark')
    } else {
      document.documentElement.classList.remove('dark')
    }
    localStorage.setItem('theme', val ? 'dark' : 'light')
  })

  // 初始化主题
  const savedTheme = localStorage.getItem('theme')
  if (savedTheme === 'dark') {
    isDark.value = true
  }

  const toggleTheme = () => {
    isDark.value = !isDark.value
  }

  return {
    isDark,
    toggleTheme
  }
}

在组件中使用:

vue
<template>
  <button @click="toggleTheme">
    {{ isDark ? '🌙' : '☀️' }}
  </button>
</template>

<script setup>
import { useTheme } from '../composables/useTheme'

const { isDark, toggleTheme } = useTheme()
</script>

访问 VitePress 数据

使用 useData 获取页面数据

vue
<template>
  <div>
    <h1>{{ page.title }}</h1>
    <p>最后更新: {{ lastUpdated }}</p>
  </div>
</template>

<script setup>
import { useData } from 'vitepress'

const { page, lastUpdated } = useData()
</script>

使用 useRoute 获取路由信息

vue
<template>
  <div>
    <p>当前路径: {{ route.path }}</p>
  </div>
</template>

<script setup>
import { useRoute } from 'vitepress'

const route = useRoute()
</script>

使用 useSiteConfig 获取站点配置

vue
<template>
  <div>
    <p>站点标题: {{ site.title }}</p>
    <p>站点描述: {{ site.description }}</p>
  </div>
</template>

<script setup>
import { useSiteConfig } from 'vitepress'

const site = useSiteConfig()
</script>

组件最佳实践

1. 使用 TypeScript

vue
<script setup lang="ts">
interface Props {
  title: string
  count?: number
  items: Array<{ id: number; name: string }>
}

const props = withDefaults(defineProps<Props>(), {
  count: 0
})
</script>

2. 样式隔离

使用 scoped 或 CSS Modules:

vue
<!-- 方式一:Scoped -->
<style scoped>
.my-component {
  /* 样式只作用于当前组件 */
}
</style>

<!-- 方式二:CSS Modules -->
<template>
  <div :class="$style.container">
    <p :class="$style.text">Hello</p>
  </div>
</template>

<style module>
.container {
  /* 样式会被模块化 */
}
.text {
  color: red;
}
</style>

3. 性能优化

vue
<template>
  <div>
    <!-- 使用 v-once 渲染静态内容 -->
    <header v-once>
      <h1>{{ siteTitle }}</h1>
    </header>

    <!-- 使用 v-memo 缓存复杂渲染 -->
    <div v-memo="[item.id]">
      <ExpensiveComponent :item="item" />
    </div>

    <!-- 使用 Suspense 处理异步组件 -->
    <Suspense>
      <template #default>
        <AsyncComponent />
      </template>
      <template #fallback>
        <Loading />
      </template>
    </Suspense>
  </div>
</template>

4. 响应式设计

vue
<template>
  <div class="responsive-container">
    <div class="sidebar">...</div>
    <div class="main-content">...</div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'

const isMobile = ref(false)

function checkScreenSize() {
  isMobile.value = window.innerWidth < 768
}

onMounted(() => {
  checkScreenSize()
  window.addEventListener('resize', checkScreenSize)
})

onUnmounted(() => {
  window.removeEventListener('resize', checkScreenSize)
})
</script>

<style scoped>
.responsive-container {
  display: flex;
}

.sidebar {
  width: 250px;
}

.main-content {
  flex: 1;
}

@media (max-width: 768px) {
  .responsive-container {
    flex-direction: column;
  }

  .sidebar {
    width: 100%;
  }
}
</style>

组件调试技巧

使用 Vue DevTools

  1. 安装 Vue DevTools
  2. 在开发模式下自动启用
  3. 查看组件树、状态、事件等

控制台调试

vue
<script setup>
import { onMounted, ref } from 'vue'

const data = ref([])

onMounted(() => {
  console.log('组件已挂载')
  console.log('数据:', data.value)
})
</script>

错误处理

vue
<script setup>
import { onErrorCaptured, ref } from 'vue'

const error = ref(null)

onErrorCaptured((err) => {
  error.value = err
  console.error('组件错误:', err)
  return false // 阻止错误继续向上传播
})
</script>

<template>
  <div v-if="error">
    发生错误: {{ error.message }}
  </div>
  <slot v-else />
</template>

相关资源

下一步

贡献者

加载中...

想要成为贡献者?

在 CNB 上参与贡献