大家好,我是写文档和教程出身的前端工程师一步梅。
做组件库的同学,大概率都经历过被「手写 API 表格」支配的恐惧:组件改了个属性名,文档忘了同步;新增了一个可选值,文档漏写了。最后,文档和代码彻底脱节,变成两张皮。
把话讲明白的第一原则是:同一份信息,绝对不要维护两次。
组件的代码和类型定义本身,就是最权威的单一真实数据源(Single Source of Truth)。今天这篇文章,我将通过具体的步骤和代码,把如何基于 react-docgen-typescript(下文简称 RDT)与 TypeScript AST 自动提取 Props、生成 API 表格的落地过程梳理清楚,并把踩过的几个大坑直接摊开讲透。
一、 核心流程与基础接入
RDT 的底层其实就是调用了 TypeScript 的 Compiler API,通过类型检查器(TypeChecker)去解析组件的接口定义,同时抓取 JSDoc 注释。
第 1 步:规范组件的类型与注释
工具不是读心术,提取的前提是代码写得足够规范。以一个简单的 Button 组件为例:
// src/components/Button/Button.tsx
import React from 'react';
export interface ButtonProps {
/** 按钮的样式变体 */
variant?: 'primary' | 'secondary' | 'danger';
/** 是否处于加载中状态 */
loading?: boolean;
/** 点击事件回调 */
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
/** 子节点内容 */
children: React.ReactNode;
}
/**
* 通用按钮组件,支持多种业务状态。
*/
export const Button: React.FC<ButtonProps> = ({
variant = 'primary',
loading = false,
children,
onClick,
}) => {
return (
<button className={`btn btn-${variant}`} onClick={onClick} disabled={loading}>
{children}
</button>
);
};
第 2 步:调用 RDT 提取元数据
编写一个脚本,调用 react-docgen-typescript 解析目标文件:
// scripts/extract-api.ts
import * as path from 'path';
import * as docgen from 'react-docgen-typescript';
const tsconfigPath = path.resolve(__dirname, '../tsconfig.json');
// 创建解析器
const parser = docgen.withCustomConfig(tsconfigPath, {
savePropValueAsString: true,
shouldExtractLiteralValuesFromEnum: true, // 提取联合类型的具体字面量
});
const componentPath = path.resolve(__dirname, '../src/components/Button/Button.tsx');
const docs = parser.parse(componentPath);
console.log(JSON.stringify(docs, null, 2));
运行后,你会得到一个包含 displayName、description 和 props 字典的 JSON 对象。拿着这个 JSON,无论是生成 Markdown 还是喂给 VitePress / Dumi,都变得易如反掌。
二、 工程落地中的三大“深坑”与解决方案
原理很简单,但在真实的业务组件库落地时,通常会遇到以下三个典型阻碍。
坑 1:继承原生 HTML 属性导致 Props 表格爆满
在写组件库时,为了让组件更灵活,我们通常会继承原生属性:
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary';
}
问题表现:
RDT 会把 onCopy、onCut、aria-* 等上百个原生 DOM 属性全部扫描出来,文档瞬间多出几十页废话。
解决方案:
配置 propFilter,把来源是 node_modules 的类型过滤掉,只保留业务自定义的属性:
const parser = docgen.withCustomConfig(tsconfigPath, {
propFilter: (prop, component) => {
if (prop.declarations !== undefined && prop.declarations.length > 0) {
// 检查属性声明是否来自 node_modules
const isFromNodeModules = prop.declarations.some((declaration) => {
return declaration.fileName.includes('node_modules');
});
return !isFromNodeModules;
}
return true;
},
});
如果你希望保留某些特定的原生属性(如 disabled、id),可以在过滤函数里增加一个白名单放行逻辑。
坑 2:联合类型被退化展示成了 string
看一个典型的场景:
export type Size = 'small' | 'medium' | 'large';
export interface CardProps {
size?: Size;
}
问题表现:
导出的 JSON 里面,type.name 赫然写着 "string",或者干脆显示成别名 "Size",使用者根本不知道具体有哪些取值可选。
解决方案:
在解析器选项中开启 shouldExtractLiteralValuesFromEnum: true。开启后,RDT 会通过 TypeScript 的类型展开能力,输出具体的字面量结构:
"size": {
"type": {
"name": "enum",
"value": [
{ "value": "\"small\"" },
{ "value": "\"medium\"" },
{ "value": "\"large\"" }
]
}
}
渲染模板时,判断 type.name === 'enum',直接取 value 列表用代码块包裹拼接即可(如 'small' | 'medium' | 'large')。
坑 3:forwardRef 与高阶组件导致组件识别失败
许多组件为了透传 DOM 引用,会这样写:
export const Input = React.forwardRef<HTMLInputElement, InputProps>((props, ref) => {
return <input ref={ref} {...props} />;
});
问题表现:
在某些 TS 版本或复杂的复合导出场景下,RDT 可能无法提取出 displayName,导致生成的文档组件名为 "Anonymous" 甚至直接漏掉该组件。
解决方案: 处理这个问题通常有两步:
- 规范组件写法:务必给
forwardRef包裹的组件挂载displayName:tsxInput.displayName = 'Input'; - 结合原生 TS AST(使用 ts-morph 辅助)兜底: 如果 RDT 解析不到组件名字,写一层 AST 遍历作为兜底逻辑,直接根据导出的变量名进行纠正。
import { Project, SyntaxKind } from 'ts-morph';
function fixComponentDisplayName(filePath: string, defaultName: string): string {
const project = new Project();
const sourceFile = project.addSourceFileAtPath(filePath);
// 查找 export const xxx = ...
const exportDeclarations = sourceFile.getVariableStatements()
.filter(statement => statement.isExported());
for (const statement of exportDeclarations) {
for (const decl of statement.getDeclarationList().getDeclarations()) {
const initializer = decl.getInitializer();
// 如果包含 forwardRef 调用,直接拿变量名作为组件名
if (initializer && initializer.getText().includes('forwardRef')) {
return decl.getName();
}
}
}
return defaultName;
}
三、 总结:如何建立长效自动化机制
搞定 Props 自动化提取后,你的工程流水线应当长成这样:
- 开发者规范:在
interface上写好清晰的 JSDoc,遇到默认值在解构时指定或使用@default标记。 - Pre-build 阶段提取:在执行
npm run build:docs时,先跑脚本提取出所有组件的api.json。 - 渲染器消费:写一个通用的 Markdown 生成器或 React 展示组件,把 JSON 数据渲染成标准的 HTML 表格。
文档不是写出来的,而是从规范的代码里长出来的。把体力活交给 react-docgen-typescript 和 AST,开发者才能把更多精力聚焦在把组件本身的 API 设计得清晰、好用上。
License: CC BY-NC 4.0
Updated 13 hours ago
Was this article helpful? Give it a like.
0 comments


