Skip to content

组件库文档

本教程将详细介绍如何为 Vue 组件库搭建一个专业、美观的文档站点。

项目规划

目录结构

一个完整的组件库文档项目结构:

text
my-ui/
├── src/                     # 组件源码
│   ├── components/
│   │   ├── Button/
│   │   │   ├── index.ts
│   │   │   ├── Button.vue
│   │   │   └── Button.test.ts
│   │   ├── Input/
│   │   └── index.ts
│   └── index.ts
├── docs/                    # 文档目录
│   ├── .vitepress/
│   │   ├── config.mts
│   │   └── theme/
│   │       ├── index.ts
│   │       └── components/
│   │           ├── DemoPreview.vue
│   │           ├── ApiTable.vue
│   │           └── ComponentCard.vue
│   ├── components/          # 组件文档
│   │   ├── button.md
│   │   ├── input.md
│   │   └── index.md
│   ├── guide/               # 指南文档
│   │   ├── getting-started.md
│   │   ├── installation.md
│   │   └── theming.md
│   ├── public/
│   │   └── logo.svg
│   └── index.md
├── package.json
└── vite.config.ts

Monorepo 工作区结构

对于大型组件库,推荐使用 Monorepo:

text
my-ui/
├── packages/
│   ├── components/          # 组件源码
│   │   ├── src/
│   │   │   ├── button/
│   │   │   └── index.ts
│   │   ├── package.json
│   │   └── tsconfig.json
│   ├── utils/               # 工具函数
│   ├── theme/               # 主题包
│   └── docs/                # 文档站点
│       ├── .vitepress/
│       ├── components/
│       ├── package.json
│       └── tsconfig.json
├── pnpm-workspace.yaml
├── package.json
└── tsconfig.json

pnpm-workspace.yaml:

yaml
packages:
  - 'packages/*'

packages/docs/package.json:

json
{
  "name": "@my-ui/docs",
  "private": true,
  "scripts": {
    "dev": "vitepress dev",
    "build": "vitepress build",
    "preview": "vitepress preview"
  },
  "devDependencies": {
    "@my-ui/components": "workspace:*",
    "vitepress": "^2.0.0",
    "vue": "^3.4.0"
  }
}

VitePress 配置别名:

ts
// packages/docs/.vitepress/config.mts
import { defineConfig } from 'vitepress'
import { resolve } from 'path'

export default defineConfig({
  vite: {
    resolve: {
      alias: {
        // 直接引用组件源码,无需先构建
        '@my-ui/components': resolve(__dirname, '../../components/src')
      }
    }
  }
})

基础配置

VitePress 配置

ts
// docs/.vitepress/config.mts
import { defineConfig } from 'vitepress'
import { resolve } from 'path'

export default defineConfig({
  title: 'My UI',
  description: '一个现代化的 Vue 3 组件库',
  lang: 'zh-CN',

  // 配置别名,方便在文档中引入组件
  vite: {
    resolve: {
      alias: {
        '@': resolve(__dirname, '../../src')
      }
    }
  },

  themeConfig: {
    logo: '/logo.svg',

    nav: [
      { text: '首页', link: '/' },
      { text: '指南', link: '/guide/getting-started' },
      { text: '组件', link: '/components/' },
      {
        text: '相关链接',
        items: [
          { text: 'GitHub', link: 'https://github.com/user/my-ui' },
          { text: 'npm', link: 'https://npmjs.com/package/my-ui' }
        ]
      }
    ],

    sidebar: {
      '/guide/': [
        {
          text: '开始',
          items: [
            { text: '快速开始', link: '/guide/getting-started' },
            { text: '安装', link: '/guide/installation' },
            { text: '主题定制', link: '/guide/theming' }
          ]
        }
      ],
      '/components/': [
        {
          text: '基础组件',
          collapsed: false,
          items: [
            { text: 'Button 按钮', link: '/components/button' },
            { text: 'Input 输入框', link: '/components/input' },
            { text: 'Icon 图标', link: '/components/icon' }
          ]
        },
        {
          text: '表单组件',
          collapsed: false,
          items: [
            { text: 'Form 表单', link: '/components/form' },
            { text: 'Select 选择器', link: '/components/select' }
          ]
        }
      ]
    },

    search: {
      provider: 'local'
    },

    outline: {
      level: [2, 3],
      label: '目录'
    }
  }
})

组件演示系统

DemoPreview 组件

创建一个可交互的组件演示容器:

vue
<!-- docs/.vitepress/theme/components/DemoPreview.vue -->
<script setup lang="ts">
import { ref } from 'vue'

interface Props {
  title?: string
  description?: string
}

const props = withDefaults(defineProps<Props>(), {
  title: '',
  description: ''
})

const showCode = ref(false)
const copied = ref(false)

const copyCode = async () => {
  const codeEl = document.querySelector('.demo-code pre code')
  if (codeEl) {
    await navigator.clipboard.writeText(codeEl.textContent || '')
    copied.value = true
    setTimeout(() => {
      copied.value = false
    }, 2000)
  }
}
</script>

<template>
  <div class="demo-preview">
    <div v-if="title || description" class="demo-header">
      <h4 v-if="title" class="demo-title">{{ title }}</h4>
      <p v-if="description" class="demo-desc">{{ description }}</p>
    </div>

    <div class="demo-container">
      <slot name="demo" />
    </div>

    <div class="demo-actions">
      <button
        class="action-btn"
        :class="{ active: showCode }"
        @click="showCode = !showCode"
      >
        {{ showCode ? '隐藏代码' : '显示代码' }}
      </button>
      <button class="action-btn" @click="copyCode">
        {{ copied ? '已复制!' : '复制代码' }}
      </button>
    </div>

    <div v-show="showCode" class="demo-code">
      <slot name="code" />
    </div>
  </div>
</template>

<style scoped>
.demo-preview {
  margin: 24px 0;
  border: 1px solid var(--vp-c-divider);
  border-radius: 12px;
  overflow: hidden;
}

.demo-header {
  padding: 16px 20px;
  border-bottom: 1px solid var(--vp-c-divider);
  background: var(--vp-c-bg-soft);
}

.demo-title {
  margin: 0 0 4px;
  font-size: 16px;
  font-weight: 600;
  color: var(--vp-c-text-1);
}

.demo-desc {
  margin: 0;
  font-size: 14px;
  color: var(--vp-c-text-2);
}

.demo-container {
  padding: 32px;
  background: var(--vp-c-bg);
}

.demo-actions {
  display: flex;
  gap: 8px;
  padding: 12px 16px;
  border-top: 1px solid var(--vp-c-divider);
  background: var(--vp-c-bg-soft);
}

.action-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px;
  border: 1px solid var(--vp-c-divider);
  border-radius: 6px;
  background: var(--vp-c-bg);
  color: var(--vp-c-text-2);
  font-size: 13px;
  cursor: pointer;
  transition: all 0.25s ease;
}

.action-btn:hover {
  border-color: var(--vp-c-brand-1);
  color: var(--vp-c-brand-1);
}

.demo-code {
  border-top: 1px solid var(--vp-c-divider);
  background: var(--vp-code-block-bg);
}
</style>

在文档中使用

在组件文档中使用 DemoPreview:

markdown
# Button 按钮

常用的操作按钮,提供多种样式和尺寸。

## 基础用法

<DemoPreview title="基础按钮" description="使用 type 属性定义按钮样式">
  <template #demo>
    <div class="button-demo">
      <Button>默认按钮</Button>
      <Button type="primary">主要按钮</Button>
    </div>
  </template>
  <template #code>

```vue
<template>
  <Button>默认按钮</Button>
  <Button type="primary">主要按钮</Button>
</template>
```

  </template>
</DemoPreview>

注意

在 Markdown 中使用组件时,确保代码块中的 Vue 模板正确缩进,并且闭合标签正确。

API 文档组件

ApiTable 组件

用于展示组件 API 文档的表格组件:

vue
<!-- docs/.vitepress/theme/components/ApiTable.vue -->
<script setup lang="ts">
interface PropItem {
  name: string
  description: string
  type: string
  default?: string
  required?: boolean
}

interface Props {
  title?: string
  data: PropItem[]
}

defineProps<Props>()
</script>

<template>
  <div class="api-table">
    <h4 v-if="title" class="api-title">{{ title }}</h4>
    <table>
      <thead>
        <tr>
          <th>属性名</th>
          <th>说明</th>
          <th>类型</th>
          <th>默认值</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="item in data" :key="item.name">
          <td>
            <code class="prop-name">{{ item.name }}</code>
            <span v-if="item.required" class="required-tag">必填</span>
          </td>
          <td>{{ item.description }}</td>
          <td><code class="type-code">{{ item.type }}</code></td>
          <td>
            <code v-if="item.default" class="default-code">{{ item.default }}</code>
            <span v-else class="empty">-</span>
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<style scoped>
.api-table {
  margin: 24px 0;
}

.api-title {
  font-size: 18px;
  font-weight: 600;
  color: var(--vp-c-text-1);
  margin: 0 0 16px;
}

.api-table table {
  width: 100%;
  border-collapse: collapse;
  border-radius: 8px;
  overflow: hidden;
}

.api-table th,
.api-table td {
  padding: 12px 16px;
  text-align: left;
  border-bottom: 1px solid var(--vp-c-divider);
}

.api-table th {
  background: var(--vp-c-bg-soft);
  font-weight: 600;
}

.prop-name {
  padding: 2px 8px;
  background: var(--vp-c-brand-soft);
  color: var(--vp-c-brand-1);
  border-radius: 4px;
  font-size: 13px;
}

.required-tag {
  display: inline-block;
  margin-left: 6px;
  padding: 1px 6px;
  background: #fef3c7;
  color: #b45309;
  border-radius: 4px;
  font-size: 11px;
}
</style>

使用 API 表格

markdown
## API

### Props

<ApiTable :data="[
  { name: 'type', description: '按钮类型', type: 'string', default: 'default' },
  { name: 'size', description: '按钮尺寸', type: 'string', default: 'medium' },
  { name: 'disabled', description: '是否禁用', type: 'boolean', default: 'false' }
]" />

Props 自动生成

手动维护 API 表格容易遗漏和出错,推荐使用工具从 TypeScript 类型自动生成。

方案一:vue-component-meta

vue-component-meta 可以从 Vue 组件的 TypeScript 定义中提取 Props 信息:

bash
npm install -D vue-component-meta

创建提取脚本:

ts
// scripts/gen-api-table.ts
import { createComponentMetaChecker } from 'vue-component-meta'
import fs from 'fs'
import path from 'path'

const checker = createComponentMetaChecker(
  path.resolve(__dirname, '../tsconfig.json')
)

interface PropMeta {
  name: string
  type: string
  default?: string
  required: boolean
  description?: string
  tags?: string[]
}

function extractProps(componentPath: string): PropMeta[] {
  const meta = checker.getComponentMeta(componentPath)

  return meta.props
    .filter(prop => !prop.name.startsWith('_') && !prop.name.startsWith('on'))
    .map(prop => ({
      name: prop.name,
      type: prop.type || 'any',
      default: prop.default,
      required: prop.required || false,
      description: prop.description,
      tags: prop.tags
    }))
}

function extractEvents(componentPath: string) {
  const meta = checker.getComponentMeta(componentPath)

  return meta.events
    .filter(event => !event.name.startsWith('_'))
    .map(event => ({
      name: event.name,
      type: event.type || '(...args: any[]) => void',
      description: event.description
    }))
}

function extractSlots(componentPath: string) {
  const meta = checker.getComponentMeta(componentPath)

  return meta.slots
    .filter(slot => !slot.name.startsWith('_'))
    .map(slot => ({
      name: slot.name,
      description: slot.description
    }))
}

// 生成 API 文档 Markdown 片段
function generateApiSection(
  componentName: string,
  componentPath: string
): string {
  const props = extractProps(componentPath)
  const events = extractEvents(componentPath)
  const slots = extractSlots(componentPath)

  let md = '## API\n\n'

  // Props
  if (props.length) {
    md += '### Props\n\n'
    md += '| 属性名 | 说明 | 类型 | 默认值 |\n'
    md += '|-------|------|------|-------|\n'
    for (const prop of props) {
      const required = prop.required ? ' *(必填)*' : ''
      md += `| \`${prop.name}\`${required} | ${prop.description || '-'} | \`${prop.type}\` | ${prop.default ? `\`${prop.default}\`` : '-'} |\n`
    }
    md += '\n'
  }

  // Events
  if (events.length) {
    md += '### Events\n\n'
    md += '| 事件名 | 说明 | 回调参数 |\n'
    md += '|-------|------|---------|\n'
    for (const event of events) {
      md += `| \`${event.name}\` | ${event.description || '-'} | \`${event.type}\` |\n`
    }
    md += '\n'
  }

  // Slots
  if (slots.length) {
    md += '### Slots\n\n'
    md += '| 插槽名 | 说明 |\n'
    md += '|-------|------|\n'
    for (const slot of slots) {
      md += `| \`${slot.name}\` | ${slot.description || '-'} |\n`
    }
  }

  return md
}

// 批量生成
const components = [
  { name: 'Button', path: 'src/components/Button/Button.vue' },
  { name: 'Input', path: 'src/components/Input/Input.vue' },
  { name: 'Select', path: 'src/components/Select/Select.vue' }
]

for (const comp of components) {
  const md = generateApiSection(comp.name, comp.path)
  const outPath = path.join('docs', 'api-snippets', `${comp.name.toLowerCase()}-api.md`)
  fs.mkdirSync(path.dirname(outPath), { recursive: true })
  fs.writeFileSync(outPath, md, 'utf-8')
  console.log(`✅ Generated API docs for ${comp.name}`)
}

在组件文档中引入生成的片段:

markdown
# Button 按钮

常用的操作按钮,提供多种样式、尺寸和状态。

## 代码演示

### 基础用法

...

<<< @/api-snippets/button-api.md

方案二:vue-docgen-api

vue-docgen-api 通过解析 JSDoc 注释提取组件信息:

bash
npm install -D vue-docgen-api
ts
// scripts/gen-api-docgen.ts
import { parse } from 'vue-docgen-api'
import fs from 'fs'
import path from 'path'

async function generateApiDocs(componentPath: string) {
  const doc = await parse(componentPath)

  let md = '## API\n\n'

  // Props
  if (doc.props?.length) {
    md += '### Props\n\n'
    md += '| 属性名 | 说明 | 类型 | 默认值 |\n'
    md += '|-------|------|------|-------|\n'
    for (const prop of doc.props) {
      const type = prop.type?.name || 'any'
      const desc = prop.description || '-'
      const defaultVal = prop.defaultValue?.value || '-'
      const required = prop.required ? ' *(必填)*' : ''
      md += `| \`${prop.name}\`${required} | ${desc} | \`${type}\` | \`${defaultVal}\` |\n`
    }
    md += '\n'
  }

  // Events
  if (doc.events?.length) {
    md += '### Events\n\n'
    md += '| 事件名 | 说明 | 回调参数 |\n'
    md += '|-------|------|---------|\n'
    for (const event of doc.events) {
      md += `| \`${event.name}\` | ${event.description || '-'} | - |\n`
    }
    md += '\n'
  }

  // Slots
  if (doc.slots?.length) {
    md += '### Slots\n\n'
    md += '| 插槽名 | 说明 |\n'
    md += '|-------|------|\n'
    for (const slot of doc.slots) {
      md += `| \`${slot.name}\` | ${slot.description || '-'} |\n`
    }
  }

  return md
}

方案三:构建时自动注入

将 API 提取集成到 VitePress 构建流程中:

ts
// docs/.vitepress/plugins/api-table-plugin.ts
import type { Plugin } from 'vite'
import { createComponentMetaChecker } from 'vue-component-meta'
import path from 'path'

export function apiTablePlugin(): Plugin {
  return {
    name: 'vitepress-plugin-api-table',

    // 在开发模式下动态提供 API 数据
    resolveId(id) {
      if (id.startsWith('virtual:api-table:')) {
        return id
      }
      return null
    },

    async load(id) {
      if (!id.startsWith('virtual:api-table:')) return null

      const componentName = id.replace('virtual:api-table:', '')
      const checker = createComponentMetaChecker(
        path.resolve(process.cwd(), 'tsconfig.json')
      )

      const componentPath = `src/components/${componentName}/${componentName}.vue`
      const meta = checker.getComponentMeta(componentPath)

      const props = meta.props
        .filter(p => !p.name.startsWith('_') && !p.name.startsWith('on'))
        .map(p => ({
          name: p.name,
          type: p.type,
          default: p.default,
          required: p.required,
          description: p.description
        }))

      return `export default ${JSON.stringify(props)}`
    }
  }
}

在 VitePress 配置中使用:

ts
// docs/.vitepress/config.mts
import { apiTablePlugin } from './plugins/api-table-plugin'

export default defineConfig({
  vite: {
    plugins: [apiTablePlugin()]
  }
})

在组件中使用:

vue
<script setup lang="ts">
import buttonProps from 'virtual:api-table:Button'
</script>

<template>
  <ApiTable title="Props" :data="buttonProps" />
</template>

组件沙箱集成

StackBlitz 集成

在文档中嵌入 StackBlitz 在线编辑器,让用户直接体验组件:

vue
<!-- docs/.vitepress/theme/components/StackBlitzEmbed.vue -->
<script setup lang="ts">
interface Props {
  projectId?: string
  githubRepo?: string
  file?: string
  title?: string
}

const props = withDefaults(defineProps<Props>(), {
  projectId: '',
  githubRepo: '',
  file: 'src/App.vue',
  title: '在线编辑'
})

const src = props.projectId
  ? `https://stackblitz.com/edit/${props.projectId}?embed=1&file=${props.file}`
  : `https://stackblitz.com/github/${props.githubRepo}?embed=1&file=${props.file}`
</script>

<template>
  <div class="stackblitz-embed">
    <p class="embed-title">{{ title }}</p>
    <iframe
      :src="src"
      frameborder="0"
      loading="lazy"
      allow="clipboard-read; clipboard-write"
      style="width: 100%; height: 500px; border-radius: 8px; border: 1px solid var(--vp-c-divider);"
    />
  </div>
</template>

<style scoped>
.embed-title {
  font-weight: 600;
  margin-bottom: 8px;
  color: var(--vp-c-text-1);
}
</style>

CodeSandbox 集成

vue
<!-- docs/.vitepress/theme/components/CodeSandboxEmbed.vue -->
<script setup lang="ts">
interface Props {
  sandboxId: string
  title?: string
  module?: string
}

const props = withDefaults(defineProps<Props>(), {
  title: '在线编辑',
  module: '/src/App.vue'
})

const src = `https://codesandbox.io/embed/${props.sandboxId}?module=${props.module}&fontsize=14&hidenavigation=1&theme=dark`
</script>

<template>
  <div class="codesandbox-embed">
    <p class="embed-title">{{ title }}</p>
    <iframe
      :src="src"
      frameborder="0"
      loading="lazy"
      style="width: 100%; height: 500px; border-radius: 8px; border: 1px solid var(--vp-c-divider);"
      allow="accelerometer; clipboard-write"
    />
  </div>
</template>

一键打开编辑器按钮

不嵌入 iframe,改为按钮跳转:

vue
<!-- docs/.vitepress/theme/components/EditOnline.vue -->
<script setup lang="ts">
interface Props {
  type?: 'stackblitz' | 'codesandbox'
  url: string
}

const props = withDefaults(defineProps<Props>(), {
  type: 'stackblitz'
})

const label = props.type === 'stackblitz' ? 'StackBlitz' : 'CodeSandbox'
const icon = props.type === 'stackblitz' ? '⚡' : '📦'
</script>

<template>
  <a
    :href="url"
    target="_blank"
    rel="noopener noreferrer"
    class="edit-online-btn"
  >
    <span class="btn-icon">{{ icon }}</span>
    <span>在 {{ label }} 中打开</span>
  </a>
</template>

<style scoped>
.edit-online-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 8px 16px;
  border: 1px solid var(--vp-c-brand-1);
  border-radius: 8px;
  color: var(--vp-c-brand-1);
  font-size: 14px;
  text-decoration: none;
  transition: all 0.2s ease;
}

.edit-online-btn:hover {
  background: var(--vp-c-brand-soft);
}
</style>

主题定制

CSS 变量

组件库使用 CSS 变量实现主题定制:

css
/* docs/.vitepress/theme/style.css */

:root {
  /* 品牌色 */
  --my-primary-color: #6366f1;
  --my-success-color: #10b981;
  --my-warning-color: #f59e0b;
  --my-danger-color: #ef4444;

  /* 边框圆角 */
  --my-border-radius-sm: 4px;
  --my-border-radius-md: 8px;
  --my-border-radius-lg: 12px;

  /* 阴影 */
  --my-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
  --my-shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
  --my-shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
}

.dark {
  --my-primary-color: #818cf8;
  --my-success-color: #34d399;
  --my-warning-color: #fbbf24;
  --my-danger-color: #f87171;
}

主题配置页面

创建主题配置演示页面,让用户实时预览主题变化:

vue
<!-- docs/.vitepress/theme/components/ThemeCustomizer.vue -->
<script setup lang="ts">
import { ref, watch } from 'vue'

const primaryColor = ref('#6366f1')
const borderRadius = ref(8)

watch([primaryColor, borderRadius], ([color, radius]) => {
  document.documentElement.style.setProperty('--my-primary-color', color)
  document.documentElement.style.setProperty('--my-border-radius-md', radius + 'px')
})
</script>

<template>
  <div class="theme-customizer">
    <div class="customizer-item">
      <label>主题色</label>
      <input type="color" v-model="primaryColor" />
    </div>
    <div class="customizer-item">
      <label>圆角大小: {{ borderRadius }}px</label>
      <input type="range" v-model="borderRadius" min="0" max="20" />
    </div>
  </div>
</template>

使用 JSDoc 注释

在组件源码中添加 JSDoc 注释,为自动生成 API 文档提供信息:

vue
<script setup lang="ts">
/**
 * Button 组件
 * @component Button
 * @description 常用的操作按钮组件
 */

interface ButtonProps {
  /**
   * 按钮类型
   * @values default, primary, success, warning, danger
   */
  type?: 'default' | 'primary' | 'success' | 'warning' | 'danger'

  /**
   * 按钮尺寸
   * @values small, medium, large
   */
  size?: 'small' | 'medium' | 'large'

  /**
   * 是否禁用
   */
  disabled?: boolean
}

const props = withDefaults(defineProps<ButtonProps>(), {
  type: 'default',
  size: 'medium',
  disabled: false
})

/**
 * 点击按钮时触发
 * @event click
 * @type {(event: MouseEvent) => void}
 */
const emit = defineEmits<{
  (e: 'click', event: MouseEvent): void
}>()
</script>

完整组件文档模板

markdown
---
title: Button 按钮
description: 常用的操作按钮组件
---

# Button 按钮

常用的操作按钮,提供多种样式、尺寸和状态。

## 何时使用

- 需要用户执行某个操作时
- 表单提交、确认/取消等场景
- 需要强调某个操作时

## 代码演示

### 基础用法

最基本的按钮用法。

```vue
<template>
  <Button>默认按钮</Button>
  <Button type="primary">主要按钮</Button>
  <Button type="success">成功按钮</Button>
</template>

按钮尺寸

提供三种尺寸:小、中、大。

vue
<template>
  <Button size="small">小按钮</Button>
  <Button size="medium">中按钮</Button>
  <Button size="large">大按钮</Button>
</template>

禁用状态

按钮不可用状态。

vue
<template>
  <Button disabled>禁用按钮</Button>
</template>

加载状态

显示加载中的按钮。

vue
<template>
  <Button loading>加载中</Button>
</template>

API

Props

属性名说明类型默认值
type按钮类型'default' | 'primary' | 'success' | 'warning' | 'danger''default'
size按钮尺寸'small' | 'medium' | 'large''medium'
disabled是否禁用booleanfalse
loading是否加载中booleanfalse

Events

事件名说明回调参数
click点击按钮时触发(event: MouseEvent)

Slots

插槽名说明
default按钮内容
icon按钮图标

设计指南

按钮层级

  1. 主要按钮:用于主要操作,一个页面最多一个
  2. 次要按钮:用于次要操作
  3. 文字按钮:用于最低优先级的操作

最佳实践

  • ✅ 保持按钮文案简洁明了
  • ✅ 危险操作使用红色警示
  • ❌ 不要在一个区域使用过多按钮
  • ❌ 不要使用过长的按钮文案

## 部署与发布

### 自动化部署

```yaml
# .github/workflows/docs.yml
name: Deploy Docs

on:
  push:
    branches: [main]
    paths:
      - 'docs/**'
      - 'src/**'

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - run: npm ci
      - run: npm run docs:build

      - uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: docs/.vitepress/dist

Monorepo 的 CI 配置

yaml
# .github/workflows/docs.yml (Monorepo 版本)
name: Deploy Docs

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v2
        with:
          version: 8

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'

      - run: pnpm install --frozen-lockfile

      # 先构建组件包
      - run: pnpm --filter @my-ui/components build

      # 再构建文档
      - run: pnpm --filter @my-ui/docs build

      - uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: packages/docs/.vitepress/dist

最佳实践总结

实践说明
一致的文档结构每个组件文档遵循相同格式
丰富的示例提供多种使用场景
清晰的 API完整记录 props、events、slots
自动生成 API从 TypeScript 类型自动提取,避免遗漏
可交互演示让用户直接体验组件
在线编辑嵌入 StackBlitz/CodeSandbox 一键体验
主题定制器实时预览主题变化
自动化部署每次提交自动更新文档
Monorepo大型组件库使用工作区管理

相关主题

下一步

🎯 进阶挑战

完成基础教程后,尝试以下挑战来提升你的技能:

挑战 1:组件 Props 编辑器 ⭐⭐⭐

目标:创建一个可视化的 Props 编辑器,实时预览组件变化。

提示

  • 使用 Vue 的 defineProps 类型推断
  • 动态渲染表单控件
  • 双向绑定实现实时预览

参考思路

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

const propsConfig = [
  { name: 'type', type: 'select', options: ['default', 'primary', 'danger'] },
  { name: 'size', type: 'select', options: ['small', 'medium', 'large'] },
  { name: 'disabled', type: 'boolean' }
]

const currentProps = reactive<Record<string, any>>({})
</script>

挑战 2:组件代码在线编辑 ⭐⭐⭐⭐

目标:集成 Monaco Editor,实现在线编辑组件代码。

提示

  • 使用 @monaco-editor/loader
  • 实现代码热更新
  • 添加错误提示

挑战 3:组件主题切换器 ⭐⭐⭐

目标:创建实时主题切换器,让用户自定义组件库主题。

提示

  • 使用 CSS 变量
  • 实时预览主题变化
  • 导出主题配置

挑战 4:组件使用统计 ⭐⭐⭐

目标:统计每个组件文档的访问量,展示热门组件。

提示

  • 使用数据加载器读取访问数据
  • 创建热度排行榜
  • 可视化展示统计数据

挑战 5:组件版本差异对比 ⭐⭐⭐⭐

目标:展示不同版本组件的 API 差异。

提示

  • 使用 diff 算法对比
  • 高亮变化部分
  • 添加版本选择器

挑战 6:组件可访问性检测 ⭐⭐⭐⭐

目标:为组件演示添加 a11y 检测功能。

提示

  • 使用 axe-core 库
  • 在演示容器中运行检测
  • 展示可访问性问题列表

贡献者

加载中...

想要成为贡献者?

在 CNB 上参与贡献