如何安全地将 Claude Code 生成的界面代码集成到现有项目中
在终端中打开 Claude Code 并输入 /design,几秒钟内就能生成一个界面草案。对于独立开发者来说,这省去了不少麻烦,用起来很方便。然而,问题往往从你打开生成的代码那一刻开始。
它完全不理会项目中已经配置好的 shadcn/ui 组件,而是新写了一堆原始的 <button> 标签。它不在每个文件的调色板中注册语义化颜色,而是到处散落着像 bg-[#1e293b] 这样的自定义十六进制代码。每做一个页面,都要花 40 分钟去修正混乱的导入路径并清除内联样式。
这并非错觉。根据 GitClear 对 2.11 亿行提交代码的研究显示,引入 AI 工具后短短两周内,被完全丢弃或重写的代码比例从 3.1% 几乎翻倍至 5.7%。重构比例则从 25% 暴跌至 10% 以下。代码增长的速度有多快,技术债累积的速度就有多快。别指望模型能自觉遵守原有的设计系统,你必须在系统层面上束缚住它的手脚。
用项目规则文件固定组件路径
Claude Code 之所以会忽视现有代码,原因很简单:为了节省上下文窗口,它只会狭隘地浏览必要的文件。如果不加任何限制,模型就会组合使用最原始的 HTML 标签来绘制界面。
由于提示词缓存的作用,会话根目录的配置文件仅占用基础输入成本的 10% 左右。即使重置或压缩对话,它们也不会消失。如果在其中固化组件重用规则,就能防止模型随心所欲地编写原始标签。
在项目根目录下的 CLAUDE.md 文件中写入公共组件路径和样式规则。
`markdown
Design System Guidelines
- Component Reuse (STRICT)
- DO NOT use raw DOM tags (, , ).
- MUST import from @/components/ui:
- Button: import { Button } from "@/components/ui/button"
- Input: import { Input } from "@/components/ui/input"
- Card: import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"
- Dialog: import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"
- If a component does not exist in @/components/ui, ask to run: "npx shadcn@latest add ".
- Token Boundaries
- NEVER use arbitrary hex codes or pixel widths: NO bg-[#...], NO w-[...px].
- Use Semantic CSS variables:
- Surfaces: bg-background, bg-card, bg-muted
- Text: text-foreground, text-muted-foreground, text-primary
- Borders: border-border, border-input
`
没必要每次都在提示词中完整传入长达数千行的全局 CSS 文件。只需在 Tailwind 配置中挑选出所需的 Token 名称并导出为 JSON 即可。
`javascript
// scripts/extract-tokens.mjs
import fs from 'fs';
import resolveConfig from 'tailwindcss/resolveConfig.js';
import tailwindConfig from '../tailwind.config.js';
const fullConfig = resolveConfig(tailwindConfig);
const semanticTokens = {
colors: Object.keys(fullConfig.theme.colors || {}).filter(
(name) => !['inherit', 'current', 'transparent'].includes(name)
),
spacing: Object.keys(fullConfig.theme.spacing || {}),
borderRadius: Object.keys(fullConfig.theme.borderRadius || {}),
};
if (!fs.existsSync('.claude')) {
fs.mkdirSync('.claude');
}
fs.writeFileSync(
'.claude/design-tokens.json',
JSON.stringify(semanticTokens, null, 2)
);
`
将这个脚本挂载到 package.json 的 postinstall 和 predev 中。
`json
{
"scripts": {
"postinstall": "node scripts/extract-tokens.mjs",
"predev": "node scripts/extract-tokens.mjs"
}
}
`
每次构建时都会自动更新可用类名列表。过去用来排查拼写错误的样式类名所耗费的手动调整时间也将不复存在。
用 Hook 脚本自动替换任意样式
无论在提示词中写了多少注意事项,模型偶尔还是会输出奇怪的值。如果丢给它一张截图要求生成界面,它就会根据图片比例硬编码出像 w-[380px] 这样的固定宽度,这正是导致移动端页面出现横向滚动条的罪魁祸首。
为了符合 WCAG 2.1 AA 标准,必须明确各分辨率的基准,并在文件生成的瞬间通过 Linter 进行强制检查。
| 验证区域 |
目标标准 |
必需的 Tailwind 类 |
拦截条件 |
| 移动端 |
390px (base) |
flex-col, w-full, grid-cols-1 |
因使用 w-[...px] 固定宽度而导致水平滚动 |
| 平板端 |
768px (md:) |
md:flex-row, md:grid-cols-2, md:p-6 |
移动端的单列结构在宽屏幕上仍然保持不变 |
| 桌面端 |
1440px (xl:) |
xl:max-w-7xl, mx-auto, xl:grid-cols-4 |
在高分辨率下布局容器无限拉伸 |
| 暗黑模式 |
.dark 选择器 |
bg-background, text-foreground |
单独残留 bg-white、text-black 等默认类 |
| 无障碍 |
WCAG 2.1 AA |
aria-label, <main>, focus-visible:ring-2 |
图标按钮缺少屏幕阅读器替代文本 |
当遇到不听话的模型时,使用生命周期 Hook 是最稳妥的办法。利用 Claude Code 的 PostToolUse 훅(Hook),一旦将文件写入磁盘就会立即执行脚本。
在 .claude/settings.json 中注册 Hook 命令。
`json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "node .claude/hooks/ast-lint-guard.mjs",
"timeout": 30
}
]
}
]
}
}
`
接下来编写检查脚本。通过正则表达式捕获常见的十六进制代码并将其转换为项目 Token,同时借助 Tailwind ESLint 插件强制执行规则。
`javascript
// .claude/hooks/ast-lint-guard.mjs
import fs from 'fs';
import readline from 'readline';
import { execSync } from 'child_process';
const rl = readline.createInterface({ input: process.stdin });
let inputBuffer = '';
rl.on('line', (line) => { inputBuffer += line; });
rl.on('close', () => {
try {
const payload = JSON.parse(inputBuffer);
const filePath = payload.tool_input?.file_path || payload.tool_input?.path;
if (!filePath || !/\.(tsx|jsx)$/.test(filePath) || !fs.existsSync(filePath)) {
process.exit(0);
}
let code = fs.readFileSync(filePath, 'utf-8');
let changed = false;
const replacementMap = {
'#ffffff': 'bg-background',
'#000000': 'text-foreground',
'#020817': 'bg-background',
'#0f172a': 'bg-card',
'#1e293b': 'bg-muted',
'#64748b': 'text-muted-foreground',
'#2563eb': 'bg-primary',
};
for (const [hex, token] of Object.entries(replacementMap)) {
const regex = new RegExp(`(bg|text|border)-\[${hex}\]`, 'gi');
if (regex.test(code)) {
code = code.replace(regex, token);
changed = true;
}
}
if (changed) {
fs.writeFileSync(filePath, code, 'utf-8');
}
execSync(`npx eslint "${filePath}" --rule "tailwindcss/no-arbitrary-value: error"`, {
stdio: 'pipe',
});
process.exit(0);
} catch (error) {
const failureLog = error.stdout?.toString() || error.stderr?.toString() || error.message;
console.error([Lint Pipeline Block] 违反样式规则:\n${failureLog});
process.exit(1);
}
});
`
在项目中安装 Linter 插件。
`bash
npm install -D eslint-plugin-tailwindcss
`
如果脚本返回退出码 1,Claude Code 就会读取错误日志并在下一轮对话中使用语义化类名重写代码。在 QA 阶段为崩溃的界面焦头烂额的情况将会大大减少。
将视图与业务逻辑物理隔离
让 Claude Code 编写界面时,它往往会将 fetch 函数、庞大的模拟数据对象和 JSX 混杂在同一个文件中。将来如果想接入真实的 API,就必须把渲染代码全部重构一遍。
视图代码不需要关心状态变更的方式。将一个功能目录拆分为 4 个文件,并预先确定好数据架构会更安全。
首先定义数据规范。
`typescript
// src/components/features/dashboard-card/schema.ts
import { z } from "zod";
export const MetricItemSchema = z.object({
id: z.string(),
label: z.string(),
value: z.string(),
changePercentage: z.number(),
trend: z.enum(["up", "down", "neutral"]),
});
export const DashboardCardSchema = z.object({
title: z.string().min(1),
metrics: z.array(MetricItemSchema),
});
export type DashboardCardData = z.infer;
export interface DashboardCardViewProps {
data: DashboardCardData;
isLoading?: boolean;
onActionClick?: (metricId: string) => void;
}
`
接着,在终端中调用 Claude Code 时,明确要求绝对不要使用内部状态。
`bash
claude "请创建纯 UI 组件 src/components/features/dashboard-card/dashboard-card-view.tsx,以实现 src/components/features/dashboard-card/schema.ts 中的 DashboardCardViewProps。绝对不要在内部使用 useState、useEffect 和 fetch,仅使用传入的 Props 和 @/components/ui 元素以响应式方式编写。"
`
接入数据时使用 Hook 进行封装。让模拟数据和真实 API 调用函数保持相同的结构。
`typescript
// src/components/features/dashboard-card/use-dashboard-card.ts
import { useQuery } from "@tanstack/react-query";
import { DashboardCardData } from "./schema";
const MOCK_DATA: DashboardCardData = {
title: "月度活跃指标",
metrics: [
{ id: "m-1", label: "新增引入", value: "1,240人", changePercentage: 12.5, trend: "up" },
{ id: "m-2", label: "流失率", value: "2.1%", changePercentage: -0.4, trend: "down" },
],
};
export const useDashboardCard = (cardId: string, useMock = false) => {
return useQuery({
queryKey: ["dashboard-card", cardId],
queryFn: async () => {
if (useMock) {
return MOCK_DATA;
}
const res = await fetch(/api/dashboard/${cardId});
if (!res.ok) throw new Error("数据查询失败");
return res.json();
},
});
};
`
在容器组件中将两者组装起来。
`typescript
// src/components/features/dashboard-card/index.tsx
"use client";
import React from "react";
import { DashboardCardView } from "./dashboard-card-view";
import { useDashboardCard } from "./use-dashboard-card";
export function DashboardCardContainer({ cardId, useMock = false }: { cardId: string; useMock?: boolean }) {
const { data, isLoading } = useDashboardCard(cardId, useMock);
if (!data) return null;
return (
<DashboardCardView
data={data}
isLoading={isLoading}
onActionCardId={(id) => console.log(id)}
/>
);
}
`
在后端准备好之前,通过 useMock={true} 调整界面。当 API 完成后,只需删除该标志位,视图代码无需改动哪怕一行。
隔离工作树并挑选合并变更
在终端中与模型进行长对话并摆弄 UI 时,原本正常的全局配置文件常常会被修改,或者目录各处会产生无用的临时文件。最好在保留自己工作空间的同时,开辟一个专用于实验的目录来进行开发。
利用 Git worktree 可以在完全隔离的文件夹中运行 Claude Code。
`bash
git worktree add ../saas-ui-sandbox -b experiment/ai-dashboard-ui
cd ../saas-ui-sandbox
claude
`
如果实验搞砸了,无需纠结,直接删掉整个文件夹即可。
`bash
cd ../saas-platform
git worktree remove ../saas-ui-sandbox --force
git branch -D experiment/ai-dashboard-ui
`
如果得到了想要的效果,切记不要直接合并整个分支,而是通过交互模式只挑取视图文件的代码片段。
`bash
git checkout main
git checkout -p experiment/ai-dashboard-ui -- src/components/features/dashboard-card/dashboard-card-view.tsx
`
一边查看终端中显示的代码块,一边对满意的部分按 y,对异常的修改按 n 剔除。
当会话超过 5 轮时,上下文就会变得模糊,模型开始胡言乱语。因此必须适时清理会话:
- 检查类型:完成单个组件编写后,立即运行
npx tsc --noEmit。只有当类型错误为 0 时,才能进入下一个提示词。
- 压缩上下文:对话变长时,毫不犹豫地输入
/compact 以减少 Token 浪费。
- 清空会话:界面工作结束后,提交代码并用
/clear 完全清空内存。如果代码错乱了,可以通过 /rewind 回到之前的检查点。
模型的生成能力强,与这些代码能否进入生产环境完全是两码事。通过 CLAUDE.md 收窄输入通道,利用生命周期 Hook 验证输出代码,并借助 worktree 隔离工作空间,就能避免整夜通宵收拾 AI 吐出代码的惨剧。