AI 驱动的组件文档生成:从 JSDoc 注释到完整 Storybook 示例的自动化
AI 驱动的组件文档生成:从 JSDoc 注释到完整 Storybook 示例的自动化
一、组件文档的工程痛点
组件文档的维护是前端工程中公认的高投入低产出工作。一个典型场景:组件代码更新后,对应的文档没有同步更新,随后新的使用者根据过时文档写出了错误的调用方式,以 bug 的形式返回到组件的维护者手中。
理想情况下,组件文档和代码应该是一体的——代码变更自动触发文档更新。人工编写的文档总是滞后于代码,而代码本身是最准确的文档来源。从这个角度出发,组件文档的自动生成有两个关键步骤:从代码中提取信息(Props、Events、Slots),以及将提取的信息组织为可读的格式(Markdown、Storybook、MDX)。
JSDoc 注释是第一个步骤的理想入口,而 Storybook 是第二个步骤的理想输出格式。
二、文档生成的流水线架构
graph TD A[源文件] --> B[JSDoc 解析器] B --> C[AST 提取 Props/Events] C --> D[类型推导与合并] D --> E[文档模板引擎] E --> F1[Markdown 输出] E --> F2[Storybook MDX] E --> F3[API Reference JSON] A --> G[LLM 增强分析] G --> H[语义补充] H --> D流水线分为三个层次:
- 静态分析层:从 TypeScript AST 和 JSDoc 注释中提取结构信息。
- 语义增强层:利用 LLM 理解注释语义,生成更丰富的文档内容。
- 模板渲染层:将元数据结合模板生成最终文档格式。
三、静态分析层的实现
组件的元信息提取需要同时处理 TypeScript 的类型定义和 JSDoc 注释。使用ts-morph或typescript编译器 API 可以直接解析类型信息:
// component-analyzer.ts — 组件元信息提取器 import ts from 'typescript'; import * as fs from 'fs'; interface PropMeta { name: string; type: string; required: boolean; default?: string; description: string; } interface ComponentMeta { name: string; description: string; props: PropMeta[]; events: { name: string; payload: string; description: string }[]; slots: { name: string; description: string }[]; examples: string[]; } export function extractComponentMeta(filePath: string): ComponentMeta | null { if (!fs.existsSync(filePath)) { console.error(`文件不存在: ${filePath}`); return null; } const sourceCode = fs.readFileSync(filePath, 'utf-8'); const sourceFile = ts.createSourceFile( filePath, sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX ); const meta: ComponentMeta = { name: '', description: '', props: [], events: [], slots: [], examples: [], }; // 遍历 AST 节点 function visit(node: ts.Node): void { // 提取组件注释 if (ts.isFunctionDeclaration(node) || ts.isVariableStatement(node)) { const jsDoc = getJSDoc(node); if (jsDoc?.description) { meta.description = jsDoc.description; } // 提取 @example 标签 if (jsDoc?.tags) { meta.examples = jsDoc.tags .filter((t) => t.tagName.text === 'example') .map((t) => t.comment ?? ''); } } // 提取 Props 类型定义 if (ts.isInterfaceDeclaration(node) && node.name.text.endsWith('Props')) { meta.name = node.name.text.replace('Props', ''); node.members.forEach((member) => { if (ts.isPropertySignature(member) && member.name) { const propName = member.name.getText(); const propType = member.type?.getText() ?? 'any'; const required = !member.questionToken; const jsDoc = getJSDoc(member); // 提取默认值(从 JSDoc @default 标签或类型注释) const defaultTag = jsDoc?.tags?.find((t) => t.tagName.text === 'default'); const propDefault = defaultTag?.comment; meta.props.push({ name: propName, type: propType, required, default: propDefault, description: jsDoc?.description ?? '', }); } }); } ts.forEachChild(node, visit); } visit(sourceFile); return meta.name ? meta : null; } interface JSDocInfo { description: string; tags?: { tagName: { text: string }; comment?: string }[]; } function getJSDoc(node: ts.Node): JSDocInfo | null { const jsDocs = (node as any).jsDoc as ts.JSDoc[] | undefined; if (!jsDocs || jsDocs.length === 0) return null; const doc = jsDocs[0]; const description = doc.comment && typeof doc.comment === 'string' ? doc.comment : doc.getText().replace(/\/\*\*|\*\/|\*/g, '').trim(); const tags = (doc.tags as any) ?.map((t: any) => ({ tagName: { text: t.tagName?.text ?? t.tagName?.escapedText ?? '' }, comment: t.comment ?? '', })) .filter((t: any) => t.tagName.text); return { description, tags }; }LLM 语义增强
静态分析只能提取结构信息,无法生成"什么时候用这个组件"、"和另一个组件有什么区别"等语义内容。LLM 在这个环节发挥关键作用:它读取组件的 Props 定义和现有注释,生成更详细的使用说明和场景描述。
语义增强的调用时机是在静态分析完成后,将ComponentMeta对象作为上下文输入到 LLM,由 LLM 补充以下内容:
- 组件的使用场景和典型用例
- 各 Props 之间的约束关系(如
size和variant的组合限制) - 与同类组件的选用建议
- 常见错误和避免方式
四、Storybook 文档的自动生成
将提取的元信息转换为 Storybook 的 MDX 格式,可以使用模板引擎:
// story-generator.ts — Storybook MDX 生成器 import { ComponentMeta } from './component-analyzer'; export function generateStorybookMDX(meta: ComponentMeta): string { const propsTable = meta.props .map( (prop) => `| \`${prop.name}\` | \`${prop.type}\` | ${prop.required ? '是' : '否'} | ${prop.default ?? '—'} | ${prop.description} |` ) .join('\n'); // 生成基本示例的 Story const basicProps = meta.props .filter((p) => p.required) .map((p) => `${p.name}={/* ${p.type} */}`) .join(' '); return `import { Meta, Story, Canvas, ArgsTable } from '@storybook/addon-docs'; import { ${meta.name} } from './${meta.name}'; <Meta title="Components/${meta.name}" component={${meta.name}} /> # ${meta.name} ${meta.description} ## Props <ArgsTable of={${meta.name}} /> | 属性 | 类型 | 必填 | 默认值 | 说明 | |------|------|------|--------|------| ${propsTable} ## 基本示例 <Canvas> <Story name="基本用法"> <${meta.name} ${basicProps} /> </Story> </Canvas> ${meta.examples.length > 0 ? `## 使用示例 ${meta.examples .map( (example, index) => ` ### 示例 ${index + 1} \`\`\`tsx ${example} \`\`\`` ) .join('\n')}` : ''} ## 注意事项 - 组件会自动处理边界情况,但调用方仍需关注必填参数的传入 - 请勿在组件内部绕过 Props 验证直接操作 DOM `; }五、总结
AI 驱动的组件文档生成,核心思路是将文档维护从人工行为转变为自动化流水线。静态分析提取结构,LLM 补充语义,模板引擎输出格式。三个环节各自独立、可以单独迭代升级。
在实际落地时,一个重要的工程决策是 LLM 增强的触发时机。实时生成(每次 CI 运行都调用 LLM)可以获得最新的文档,但会增加构建耗时和 API 成本。延迟生成(合并到 nightly build 或手动触发)可以降低成本,但文档更新会有延迟。建议在 CI 中执行静态分析生成骨架文档(成本极低),LLM 增强作为非阻塞的后置步骤执行。