组件库 SSG 文档站构建:从 Markdown 到交互式示例
组件库 SSG 文档站构建从 Markdown 到交互式示例一、引言文档是组件库的门面组件库的质量一半由代码决定一半由文档体现。优秀的组件文档不仅记录 API更能提供直观的交互式体验开发者可以在文档页面上直接调整参数、预览效果、复制代码。传统的文档方案面临几个痛点手写文档与代码不同步、示例代码无法实时预览、文档站构建速度慢、多版本管理复杂。随着组件库规模增长这些问题会急剧放大。SSGStatic Site Generation结合 MDX 技术为组件库文档站提供了一种高效的构建方案。文档以 Markdown 编写内嵌可交互的组件示例构建时生成纯静态 HTML兼顾了开发体验和访问性能。VitePress 和 Docusaurus 是两个主流选择本文以 VitePress 为实践主体。二、核心方案SSG 文档站的架构设计2.1 技术架构graph TD A[Markdown 文档] -- B[MDX 处理器] C[组件源码] -- D[TypeScript 类型提取] B -- E[VitePress 构建引擎] D -- E E -- F[静态 HTML 页面] E -- G[交互式示例 iframe] E -- H[API 文档自动生成] F -- I[CDN 部署] G -- I H -- I J[GitHub Actions] -- K{变更检测} K -- L[组件变更 - 触发构建] K -- M[文档变更 - 触发构建] L -- E M -- E2.2 核心能力MDX 支持在 Markdown 中直接使用 Vue/React 组件实现交互式示例API 文档自动生成从 TypeScript 类型定义自动生成 Props/Events/Slots 文档多版本管理支持组件库多个版本的文档并存方便用户查阅历史版本SSG 构建构建时预渲染所有页面访问速度极快三、实战实现构建完整的文档站3.1 VitePress 文档站搭建// docs/.vitepress/config.ts import { defineConfig } from vitepress; export default defineConfig({ title: Aurora UI, description: 企业级 Vue 3 组件库, lang: zh-CN, lastUpdated: true, themeConfig: { logo: /logo.svg, nav: [ { text: 指南, link: /guide/introduction }, { text: 组件, link: /components/button }, { text: API, link: /api/ }, { text: v2.0.0, items: [ { text: v2.0.0 (latest), link: / }, { text: v1.x, link: https://v1.aurora-ui.dev } ] } ], sidebar: { /guide/: [ { text: 快速开始, link: /guide/introduction }, { text: 安装, link: /guide/installation }, { text: 主题定制, link: /guide/theming }, { text: 国际化, link: /guide/i18n } ], /components/: [ { text: 基础组件, items: [ { text: Button 按钮, link: /components/button }, { text: Input 输入框, link: /components/input }, { text: Select 选择器, link: /components/select } ] }, { text: 数据展示, items: [ { text: Table 表格, link: /components/table }, { text: Tree 树形控件, link: /components/tree } ] } ] }, socialLinks: [ { icon: github, link: https://github.com/aurora-ui/aurora } ], footer: { message: Released under the MIT License., copyright: Copyright 2024 Aurora UI } }, vite: { ssr: { noExternal: [aurora-ui] }, resolve: { alias: { aurora-ui: /packages/components/src/index.ts } } } });3.2 MDX 交互式示例实现在 Markdown 中嵌入可运行的组件示例# Button 按钮 常用的操作按钮。 ## 基础用法 使用 type、size 和 disabled 属性来定义按钮的样式。 :::demo 通过组合 type 和 size 属性可以快速创建不同样式的按钮。 vue template div classdemo-button au-button默认按钮/au-button au-button typeprimary主要按钮/au-button au-button typesuccess成功按钮/au-button au-button typewarning警告按钮/au-button au-button typedanger危险按钮/au-button /div /template:::加载状态:::demo 设置loading属性为true按钮进入加载状态。template au-button typeprimary :loadingisLoading clickhandleClick {{ isLoading ? 加载中... : 点击加载 }} /au-button /template script setup import { ref } from vue; const isLoading ref(false); const handleClick () { isLoading.value true; setTimeout(() { isLoading.value false; }, 2000); }; /script:::VitePress 插件实现 :::demo 自定义容器 typescript // docs/.vitepress/theme/demo-block.ts import { createApp, h, defineComponent, ref, type Component } from vue; export function useDemoBlock() { return defineComponent({ name: DemoBlock, props: { code: { type: String, required: true }, component: { type: Object as () Component, required: true } }, setup(props) { const collapsed ref(false); const sourceCodeVisible ref(false); const copyCode async () { try { await navigator.clipboard.writeText(props.code); } catch { const textarea document.createElement(textarea); textarea.value props.code; document.body.appendChild(textarea); textarea.select(); document.execCommand(copy); document.body.removeChild(textarea); } }; return () h(div, { class: demo-block }, [ // 示例预览区 h(div, { class: demo-block__preview }, [ collapsed.value ? h(div, { class: demo-block__collapsed }, 点击展开示例) : h(props.component) ]), // 控制栏 h(div, { class: demo-block__controls }, [ h(button, { class: demo-block__control-btn, onClick: () collapsed.value !collapsed.value }, collapsed.value ? 展开 : 收起), h(button, { class: demo-block__control-btn, onClick: () sourceCodeVisible.value !sourceCodeVisible.value }, 查看代码), h(button, { class: demo-block__control-btn, onClick: copyCode }, 复制代码) ]), // 源代码展示 sourceCodeVisible.value h(div, { class: demo-block__source }, [ h(pre, h(code, { class: language-vue }, props.code)) ]) ]); } }); }3.3 API 文档自动生成从 TypeScript 类型自动生成 Props 文档// scripts/generate-api-docs.ts import ts from typescript; import fs from fs; import path from path; interface PropDoc { name: string; type: string; defaultValue?: string; required: boolean; description: string; } interface ComponentDoc { name: string; props: PropDoc[]; events: PropDoc[]; slots: PropDoc[]; } function extractComponentDocs(filePath: string): ComponentDoc { const program ts.createProgram([filePath], { target: ts.ScriptTarget.ESNext, module: ts.ModuleKind.ESNext, jsx: ts.JsxEmit.Preserve, }); const sourceFile program.getSourceFile(filePath); if (!sourceFile) throw new Error(无法读取文件: ${filePath}); const checker program.getTypeChecker(); const docs: ComponentDoc { name: path.basename(filePath, .vue), props: [], events: [], slots: [] }; function visit(node: ts.Node) { // 识别 defineProps 调用 if (ts.isCallExpression(node) node.expression.getText() defineProps) { const typeArg node.typeArguments?.[0]; if (typeArg) { const type checker.getTypeFromTypeNode(typeArg); extractPropsFromType(type, checker, docs); } } ts.forEachChild(node, visit); } visit(sourceFile); return docs; } function extractPropsFromType( type: ts.Type, checker: ts.TypeChecker, docs: ComponentDoc ) { for (const prop of type.getProperties()) { const declarations prop.getDeclarations(); if (!declarations?.length) continue; const declaration declarations[0]; const propType checker.getTypeOfSymbolAtLocation(prop, declaration); const jsDocTags ts.getJSDocTags(declaration); docs.props.push({ name: prop.getName(), type: checker.typeToString(propType), defaultValue: extractDefaultValue(declaration), required: !(prop.flags ts.SymbolFlags.Optional), description: getJsDocDescription(jsDocTags) }); } } // 生成 Markdown 格式的 API 文档 function generateMarkdownDoc(doc: ComponentDoc): string { let md # ${doc.name} API\n\n; md ## Props\n\n; md | 参数 | 类型 | 必填 | 默认值 | 说明 |\n; md |------|------|------|--------|------|\n; for (const prop of doc.props) { md | ${prop.name} | \${prop.type}\ | ${prop.required ? 是 : 否} | ${prop.defaultValue || -} | ${prop.description || -} |\n; } return md; }3.4 构建优化与自动化配置 CI 自动构建和部署# .github/workflows/docs-deploy.yml name: Deploy Documentation on: push: branches: [main] paths: - packages/components/** - docs/** - .github/workflows/docs-deploy.yml jobs: build-and-deploy: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - uses: pnpm/action-setupv2 with: version: 8 - uses: actions/setup-nodev4 with: node-version: 20 cache: pnpm - name: Install dependencies run: pnpm install - name: Generate API docs run: pnpm run docs:api - name: Build documentation run: pnpm run docs:build - name: Check for broken links run: pnpm run docs:check-links - name: Deploy to Cloudflare Pages uses: cloudflare/pages-actionv1 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} projectName: aurora-ui-docs directory: docs/.vitepress/dist branch: main四、最佳实践与注意事项4.1 文档与代码同步策略文档与组件代码不同步是组件库文档最突出的问题。推荐以下策略类型驱动文档从 TypeScript 类型自动生成 API 表格类型变更自动反映到文档示例即测试使用 VitePress 的交互式示例作为可视化测试组件变更后示例自动验证文档 CI 检查在 PR 中检查组件变更是否同步更新了文档文档版本化每个组件库版本发布时冻结对应的文档版本实际踩坑有一次在 Button 组件中新增了variant: ghost属性但只更新了源码忘记同步文档。两周后用户在 Issue 里提问Button 有没有 ghost 样式我们才发现文档遗漏。此后在 CI 中添加了检查脚本对比 PR 中组件 Props 类型的变化和文档中 API 表格的变化如果不一致则 CI 报错强制在合并前同步文档。4.2 交互式示例的性能大量交互式示例可能导致文档站加载缓慢// 使用动态导入和 Suspense 优化示例加载 import { defineAsyncComponent } from vue; const DemoComponent defineAsyncComponent({ loader: () import(./demos/ComplexTableDemo.vue), loadingComponent: { template: div classdemo-loading加载示例中.../div }, delay: 200, timeout: 10000 });4.3 多版本管理方案URL 路径策略/v2/components/button、/v1/components/button分支部署每个大版本维护独立分支对应独立的部署域名版本选择器在文档顶部提供版本切换下拉菜单红框提示过时版本五、总结与展望SSG 文档站结合 MDX 交互式示例为组件库提供了高效、可维护的文档方案。核心优势开发体验Markdown 编写文档内嵌可交互的示例代码所见即所得构建性能SSG 预渲染首次加载极快增量构建文档变更秒级更新可维护性API 文档自动生成消除手写与代码不同步的问题SEO 友好纯静态 HTML 页面搜索引擎可直接抓取未来方向AI 辅助文档生成从组件代码和注释自动生成初版文档人工审核后发布设计稿联动文档示例与 Figma 设计稿双向同步视觉一致性自动检查用户行为分析记录文档页面的搜索和浏览行为优化信息架构好的文档让组件库的价值被真正看到和使用。投入文档建设本质上是对开发者体验的投资。组件库文档不是一个附属产物而是组件库产品体验的核心组成部分。希望本文的实践能帮你构建出专业级的组件文档站。