Next.js 组件开发最佳实践文档
(基于模块化、TypeScript、UI/状态/操作分离设计)
目录结构规范
bash
components/
├─ search/ # 功能模块目录(如搜索相关组件)
│ ├─ hooks/ # 模块专属自定义 Hook
│ │ └─ use-search.ts
│ ├─ agent-card/ # 单个组件独立目录
│ │ ├─ agent-card.tsx # 主组件(UI + 接口定义)
│ │ ├─ agent-card.stories.tsx # Storybook 交互文档
│ │ ├─ agent-card.test.tsx # 单元测试
│ │ └─ index.ts # 统一导出
│ └─ ...
├─ ui/ # 基础 UI 组件库
│ ├─ prompt-input/ # 带封装逻辑的 UI 组件
│ │ ├─ prompt-input.tsx
│ │ ├─ prompt-input.stories.tsx
│ │ └─ index.ts
│ └─ ...
└─ ...
组件设计原则
1. 类型定义先行(TypeScript First)
typescript
// components/search/agent-card/agent-card.tsx
interface AgentData {
id: string;
name: string;
status: 'online' | 'offline';
}
type AgentCardProps = {
agent: AgentData;
onSelect?: (id: string) => void;
className?: string;
};
2. UI 与逻辑分离
tsx
// components/search/agent-card/agent-card.tsx
export const AgentCardUI = ({
name,
status,
onSelect
}: Pick<AgentCardProps, 'name' | 'status' | 'onSelect'>) => {
return (
<div className={styles.card} onClick={() => onSelect?.()}>
<Avatar name={name} />
<StatusBadge status={status} />
</div>
);
};
export const AgentCard = ({ agent, ...props }: AgentCardProps) => {
// 逻辑处理层(数据转换/状态管理)
const { isHighlighted } = useAgentHighlights(agent.id);
return (
<AgentCardUI
{...agent}
{...props}
className={clsx(isHighlighted && styles.highlight)}
/>
);
};
3. 状态管理策略
• 组件状态 :使用 useState
或 useReducer
管理局部状态
• 跨组件状态:通过 Context + Custom Hook 封装
typescript
// components/search/hooks/use-search.ts
export const useSearch = () => {
const [keywords, setKeywords] = useState('');
const { data, isLoading } = useSWR(`/api/search?q=${keywords}`, fetcher);
return {
keywords,
setKeywords,
results: data,
isLoading
};
};
4. 操作行为封装
• 事件处理函数独立为纯函数
• 异步操作使用 Service 层抽象
typescript
// components/search/agent-card/actions.ts
export const fetchAgentDetails = async (id: string) => {
const response = await fetch(`/api/agents/${id}`);
return response.json();
};
// 在组件中使用
const handleSelect = async (id: string) => {
const details = await fetchAgentDetails(id);
// ...更新状态
};
文档与测试规范
1. Storybook 交互文档
typescript
// components/search/agent-card/agent-card.stories.tsx
const meta = {
title: 'Search/AgentCard',
component: AgentCard,
tags: ['autodocs'],
} satisfies Meta<typeof AgentCard>;
export default meta;
export const OnlineAgent = {
args: {
agent: {
id: '1',
name: 'John Doe',
status: 'online'
}
}
};
export const OfflineAgent = {
args: {
...OnlineAgent.args,
agent: { ...OnlineAgent.args.agent, status: 'offline' }
}
};
2. 单元测试规范
typescript
// components/search/agent-card/agent-card.test.tsx
describe('AgentCard', () => {
it('显示在线状态徽章', () => {
const { getByText } = render(
<AgentCard agent={{ id: '1', name: 'Test', status: 'online' }} />
);
expect(getByText('online')).toBeInTheDocument();
});
it('点击触发选择事件', async () => {
const mockSelect = vi.fn();
const { container } = render(
<AgentCard agent={mockAgent} onSelect={mockSelect} />
);
await user.click(container.firstChild!);
expect(mockSelect).toHaveBeenCalledWith('1');
});
});
代码组织最佳实践
1. 模块化入口文件
typescript
// components/search/agent-card/index.ts
export { AgentCard } from './agent-card';
export type { AgentData, AgentCardProps } from './agent-card';
2. 样式隔离方案
• 使用 CSS Modules 或 Styled Components
• 避免全局样式污染
scss
// components/search/agent-card/agent-card.module.scss
.card {
border: 1px solid var(--neutral-200);
&.highlight {
border-color: var(--blue-500);
}
}
3. 组件 Props 设计规范
• 必填属性不加 ?
修饰符
• 事件处理器统一以 on
前缀命名
• 样式扩展使用 className
+ clsx
组合
总结:核心优势
维度 | 实现方案 |
---|---|
可维护性 | 模块化目录结构 + 单一职责组件 |
可测试性 | Storybook 可视化 + Jest 单元测试 |
扩展性 | 接口驱动设计 + 组合式 Props |
类型安全 | 严格 TypeScript 类型定义 |
协作效率 | 自描述组件 + 交互式文档 |
实施参考 :根据图中 agent-card.tsx
结构,将 UI 渲染与业务逻辑解耦,通过 Storybook 实现可视化调试,结合 TypeScript 接口确保类型安全。