前端智能组件生成先把边界收紧

问题从生成边界开始
一句描述生成一个表单,很适合展示能力,却不足以说明代码可以直接进入仓库。组件还要遵守项目现有的类型、依赖、设计令牌和可访问性约定;只要其中一项缺少上下文,语法正确的 TSX 也可能无法构建或难以维护。
更稳妥的做法,是先决定模型可以产出什么。它可以把自然语言整理成候选组件结构,也可以补充待确认的字段;最终代码仍由受控转换器生成,并经过与人工代码相同的类型检查、测试和评审。模型输出始终是输入材料,不是已经通过验证的编译产物。
直接生成组件容易留下哪些问题
1. 业务逻辑与 UI 层的无界缠绕
当提示只描述页面效果时,模型可能把数据获取、状态管理和 JSX 渲染放进同一个文件,因为它不知道仓库如何划分容器组件、服务和表单状态。问题不在单文件本身,而在生成结果绕过了现有职责边界。提交前需要把副作用、状态来源与展示逻辑分别核对,不能只看页面能否打开。
2. 幻觉代码造成的构建穿透
模型可能使用仓库里不存在的包,也可能混用不同版本的组件属性。生成阶段应只允许项目依赖清单中的模块,并根据本地类型定义检查属性。遇到无法确认的 API,先返回待人工选择的状态,不要自动新增依赖来掩盖错误。
3. 代码重复与样式雪崩
提示中没有提供设计系统时,生成结果容易出现新的颜色、间距和组件变体。与其要求模型记住全部规范,不如让结构描述只能引用已经登记的组件和设计令牌。未识别的样式值直接拒绝或标为待处理,避免一个临时页面悄悄形成第二套规范。
把模型输出收敛为结构数据
一种可控的链路是让模型只返回有限 Schema,再由普通代码将其转换成 TSX。Schema 负责限制组件名、属性类型和可用节点;转换器只生成项目允许的结构;最后使用 TypeScript 与项目现有检查验证结果。每一层都应返回具体错误,避免模型在失败后无限改写。
Schema 不必试图表达所有 React 能力。它只覆盖当前生成器明确支持的场景,复杂交互则退出自动生成流程。协议版本也要随产物保存,否则 Schema 或组件库升级后,同一份输入可能无法重现原代码。
一个受限的 TypeScript AST 转换示例
下面的代码演示 Schema 校验与 AST 打印的基本过程。它只生成简单属性和 JSX 节点,没有处理事件、状态逻辑、可访问性属性或完整编译,因此适合作为结构说明,不能直接视为完整生成引擎。实际接入时还要限制 tag、className 与属性名称的允许集合,并把打印结果送进项目的类型检查和测试。
import * as ts from 'typescript';
import { z } from 'zod';
// 1. 定义严密的组件 Schema 约束(防止 AI 输出任意非法属性)
export const ComponentSchema = z.object({
name: z.string().regex(/^[A-Z][a-zA-Z0-9]+$/, "组件名必须为大驼峰格式"),
props: z.array(z.object({
name: z.string(),
type: z.enum(['string', 'number', 'boolean', 'function']),
required: z.boolean(),
})),
stateVars: z.array(z.object({
name: z.string(),
initialValue: z.union([z.string(), z.number(), z.boolean()]),
})),
elements: z.array(z.object({
tag: z.string(),
className: z.string(),
childrenText: z.string().optional(),
})),
});
export type ComponentSpec = z.infer<typeof ComponentSchema>;
/**
* 基于 TS AST 的确定性代码生成器
* 拒绝拼接字符串,通过 AST 节点生成纯正、符合规范的代码
*/
export class SafeComponentGenerator {
/**
* 校验大模型返回的 JSON Schema,校验失败抛出具体定位
*/
public static validateSchema(input: unknown): ComponentSpec {
const parseResult = ComponentSchema.safeParse(input);
if (!parseResult.success) {
throw new Error(`Schema 校验失败: ${JSON.stringify(parseResult.error.format())}`);
}
return parseResult.data;
}
/**
* 将经过校验的 Schema 转化为 TypeScript AST 代码串
*/
public static generateTSX(spec: ComponentSpec): string {
const factory = ts.factory;
// 构建组件 Props 接口: interface Props { … }
const interfaceMembers = spec.props.map(p =>
factory.createPropertySignature(
undefined,
factory.createIdentifier(p.name),
p.required ? undefined : factory.createToken(ts.SyntaxKind.QuestionToken),
this.mapTypeToAST(p.type)
)
);
const propsInterface = factory.createInterfaceDeclaration(
[factory.createToken(ts.SyntaxKind.ExportKeyword)],
factory.createIdentifier(`${spec.name}Props`),
undefined,
undefined,
interfaceMembers
);
// 构建 JSX 返回结构
const jsxElements = spec.elements.map(elem =>
factory.createJsxElement(
factory.createJsxOpeningElement(
factory.createIdentifier(elem.tag),
undefined,
factory.createJsxAttributes([
factory.createJsxAttribute(
factory.createIdentifier('className'),
factory.createStringLiteral(elem.className)
)
])
),
elem.childrenText ? [factory.createJsxText(elem.childrenText, false)] : [],
factory.createJsxClosingElement(factory.createIdentifier(elem.tag))
)
);
// 组合组件主体表达式
const returnStatement = factory.createReturnStatement(
factory.createJsxFragment(
factory.createJsxOpeningFragment(),
jsxElements,
factory.createJsxClosingFragment()
)
);
const componentFunction = factory.createFunctionDeclaration(
[factory.createToken(ts.SyntaxKind.ExportKeyword)],
undefined,
factory.createIdentifier(spec.name),
undefined,
[
factory.createParameterDeclaration(
undefined,
undefined,
factory.createIdentifier('props'),
undefined,
factory.createTypeReferenceNode(factory.createIdentifier(`${spec.name}Props`)),
undefined
)
],
undefined,
factory.createBlock([returnStatement], true)
);
// 格式化输出为标准字符串
const sourceFile = ts.createSourceFile(
`${spec.name}.tsx`,
'',
ts.ScriptTarget.Latest,
false,
ts.ScriptKind.TSX
);
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
const interfaceCode = printer.printNode(ts.EmitHint.Unspecified, propsInterface, sourceFile);
const functionCode = printer.printNode(ts.EmitHint.Unspecified, componentFunction, sourceFile);
return `${interfaceCode}\\n\\n${functionCode}`;
}
private static mapTypeToAST(typeStr: string): ts.TypeNode {
switch (typeStr) {
case 'string': return ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
case 'number': return ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword);
case 'boolean': return ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword);
default: return ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword);
}
}
}
哪些场景可以尝试生成
使用范围取决于状态复杂度、失败后果和仓库约束是否能被 Schema 表达。下表给的是评审起点,不是脱离项目条件的固定结论。
| 标准 Form / Table 布局 | 可小范围试用 | 字段、布局与已有组件能被有限 Schema 表达时较容易核对 |
| 运营活动 H5 组件 | 先确认约束 | 页面独立不等于可以绕开性能、埋点和可访问性检查 |
| 核心业务复杂状态机 | 不直接生成执行逻辑 | 状态迁移与副作用应由明确代码和测试控制,模型可整理候选需求 |
| 基础设计系统核心组件 | 只生成提案 | 底层组件要维持公共 API、键盘操作与跨页面兼容,需单独评审 |
AST 转换和运行时校验会增加生成步骤,但这部分成本换来的是较早、较具体的失败信息。另一方面,Schema 越窄,能够自动表达的组件越少。团队应根据真实任务记录命中率、人工修订原因和失败类别,再决定是否扩展协议,而不是预先承诺节省比例。
生成结果还要走现有设计系统检查、lint、类型检查和受影响测试。写入仓库前先展示差异;新增依赖、修改公共组件或触发发布仍需人工批准。生成器失败时返回 Schema 字段和规则名即可,不把仓库代码、令牌或完整提示写进日志。
交付前的检查
智能组件生成是否值得保留,要看生成内容能否被现有工具验证,人工修订是否可控,以及失败时能否停在写入仓库之前。模型负责整理意图,Schema 缩小表达范围,AST 负责生成语法结构,类型检查与测试验证仓库契约。任何一层无法确认时就退出自动流程。
把这条边界写进工具,而不是只写进提示词,生成能力才不会随着模型或组件库版本变化而失控。最终提交仍是一份普通代码变更,应当能读、能测,也能说明为什么符合当前项目的约定。



