436 lines
8.7 KiB
Markdown
436 lines
8.7 KiB
Markdown
# 最佳实践
|
|
|
|
> **文档版本**: v1.0.0
|
|
> **创建日期**: 2026-05-25
|
|
> **适用范围**: FischerX 项目开发人员
|
|
|
|
## 目录
|
|
|
|
- [前端最佳实践](#前端最佳实践)
|
|
- [后端最佳实践](#后端最佳实践)
|
|
- [共享包开发](#共享包开发)
|
|
- [测试最佳实践](#测试最佳实践)
|
|
|
|
---
|
|
|
|
## 前端最佳实践
|
|
|
|
### 组件设计
|
|
|
|
#### 单一职责原则
|
|
|
|
每个组件应该只做一件事,保持组件保持组件应该组件应该只做一件事
|
|
|
|
```tsx
|
|
// 好的做法
|
|
function UserList({ users }: { users: User[] }) {
|
|
return (
|
|
<div className="user-list">
|
|
{users.map(user => (
|
|
<UserCard key={user.id} user={user} />
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function UserCard({ user }: { user: User }) {
|
|
return (
|
|
<div className="user-card">
|
|
<UserAvatar user={user} />
|
|
<UserInfo user={user} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 避免 - 组件太大太组件太组件太大了
|
|
function UserPage({ users }: { users: User[] }) {
|
|
return (
|
|
<div className="user-page">
|
|
{users.map(user => (
|
|
<div key={user.id} className="user-card">
|
|
<img src={user.avatar} alt={user.name} />
|
|
<div>
|
|
<h3>{user.name}</h3>
|
|
<p>{user.email}</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
#### Props 解构与组合
|
|
|
|
使用组合而不是继承,使用组合而不是继承
|
|
|
|
```tsx
|
|
// 好的做法
|
|
function Button({
|
|
children, variant = 'primary', className, ...props }: ButtonProps) {
|
|
return (
|
|
<button
|
|
className={cn(buttonVariants({ variant }), className)}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</button>
|
|
);
|
|
}
|
|
|
|
// 使用
|
|
<Button variant="secondary" onClick={handleClick} disabled={isLoading}>
|
|
<Icon />
|
|
Submit
|
|
</Button>
|
|
```
|
|
|
|
### 状态管理
|
|
|
|
#### 使用 React Query 进行服务器状态
|
|
|
|
```tsx
|
|
// hooks/useUsers.ts
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { userApi } from '@/lib/api';
|
|
|
|
export function useUsers() {
|
|
return useQuery({
|
|
queryKey: ['users'],
|
|
queryFn: userApi.getAll,
|
|
});
|
|
}
|
|
|
|
export function useCreateUser() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: userApi.create,
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['users'] });
|
|
},
|
|
});
|
|
}
|
|
```
|
|
|
|
#### 使用 Zustand 进行客户端状态
|
|
|
|
```tsx
|
|
// stores/useAuthStore.ts
|
|
import { create } from 'zustand';
|
|
import { persist } from 'zustand/middleware';
|
|
|
|
interface AuthState {
|
|
user: User | null;
|
|
token: string | null;
|
|
setUser: (user: User | null) => void;
|
|
setToken: (token: string | null) => void;
|
|
logout: () => void;
|
|
}
|
|
|
|
export const useAuthStore = create<AuthState>()(
|
|
persist(
|
|
(set) => ({
|
|
user: null,
|
|
token: null,
|
|
setUser: (user) => set({ user }),
|
|
setToken: (token) => set({ token }),
|
|
logout: () => set({ user: null, token: null }),
|
|
}),
|
|
{
|
|
name: 'auth-storage',
|
|
}
|
|
)
|
|
);
|
|
```
|
|
|
|
### 性能优化
|
|
|
|
#### 组件懒加载
|
|
|
|
```tsx
|
|
// 使用动态导入
|
|
const HeavyComponent = dynamic(() => import('./HeavyComponent'), {
|
|
loading: () => <div>Loading...</div>,
|
|
ssr: false,
|
|
});
|
|
|
|
// 在需要时再加载
|
|
function SomePage() {
|
|
const [showHeavy, setShowHeavy] = useState(false);
|
|
|
|
return (
|
|
<div>
|
|
<button onClick={() => setShowHeavy(true)}>Show</button>
|
|
{showHeavy && <HeavyComponent />}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
#### 使用 useMemo 和 useCallback
|
|
|
|
```tsx
|
|
// 只在 dependencies 变化时重新计算
|
|
const filteredUsers = useMemo(() => {
|
|
return users.filter(user => user.active);
|
|
}, [users]);
|
|
|
|
// 只在 dependencies 变化时重新创建函数
|
|
const handleUserClick = useCallback((user: User) => {
|
|
navigate(`/users/${user.id}`);
|
|
}, [navigate]);
|
|
```
|
|
|
|
---
|
|
|
|
## 后端最佳实践
|
|
|
|
### API 设计
|
|
|
|
#### RESTful 规范
|
|
|
|
```typescript
|
|
// 好的做法
|
|
GET /users # 获取用户列表
|
|
GET /users/:id # 获取单个用户
|
|
POST /users # 创建用户
|
|
PUT /users/:id # 更新用户
|
|
DELETE /users/:id # 删除用户
|
|
|
|
GET /users/:id/orders # 获取用户订单
|
|
```
|
|
|
|
#### 统一响应格式
|
|
|
|
```typescript
|
|
// 成功响应
|
|
{
|
|
"success": true,
|
|
"data": { ... },
|
|
"message": "Operation successful"
|
|
}
|
|
|
|
// 错误响应
|
|
{
|
|
"success": false,
|
|
"error": {
|
|
"code": "NOT_FOUND",
|
|
"message": "User not found",
|
|
"details": []
|
|
}
|
|
}
|
|
```
|
|
|
|
### 数据库设计
|
|
|
|
#### 使用 Prisma 迁移
|
|
|
|
```prisma
|
|
// schema.prisma
|
|
model User {
|
|
id String @id @default(cuid())
|
|
name String
|
|
email String @unique
|
|
password String
|
|
role Role @default(USER)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([email])
|
|
}
|
|
|
|
enum Role {
|
|
USER
|
|
ADMIN
|
|
}
|
|
```
|
|
|
|
#### 数据库查询优化
|
|
|
|
```typescript
|
|
// 使用 select 只查询需要的字段
|
|
async function getUsers() {
|
|
return this.prisma.user.findMany({
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
email: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
// 使用 include 加载关联数据
|
|
async function getUserWithOrders(id: string) {
|
|
return this.prisma.user.findUnique({
|
|
where: { id },
|
|
include: { orders: true },
|
|
});
|
|
}
|
|
```
|
|
|
|
### 缓存策略
|
|
|
|
```typescript
|
|
// 使用 Redis 缓存
|
|
@Injectable()
|
|
export class UserService {
|
|
constructor(
|
|
private prisma: PrismaService,
|
|
@Inject(CACHE_MANAGER) private cacheManager: Cache,
|
|
) {}
|
|
|
|
async findOne(id: string) {
|
|
const cacheKey = `user:${id}`;
|
|
const cached = await this.cacheManager.get(cacheKey);
|
|
|
|
if (cached) {
|
|
return cached;
|
|
}
|
|
|
|
const user = await this.prisma.user.findUnique({ where: { id } });
|
|
await this.cacheManager.set(cacheKey, user, 300); // 5 分钟
|
|
|
|
return user;
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 共享包开发
|
|
|
|
### 包的设计原则
|
|
|
|
#### 高内聚低耦合
|
|
|
|
```typescript
|
|
// packages/utils/src/string.ts
|
|
export function formatDate(date: Date): string {
|
|
return new Intl.DateTimeFormat('zh-CN').format(date);
|
|
}
|
|
|
|
export function truncate(str: string, length: number): string {
|
|
return str.length > length ? `${str.slice(0, length)}...` : str;
|
|
}
|
|
|
|
export function capitalize(str: string): string {
|
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
}
|
|
```
|
|
|
|
#### 明确的导出
|
|
|
|
```typescript
|
|
// packages/utils/src/index.ts
|
|
export * from './string';
|
|
export * from './date';
|
|
export * from './validation';
|
|
```
|
|
|
|
### 类型安全
|
|
|
|
```typescript
|
|
// packages/types/src/user.ts
|
|
export interface User {
|
|
id: string;
|
|
name: string;
|
|
email: string;
|
|
role: 'admin' | 'user';
|
|
}
|
|
|
|
export type CreateUserInput = Omit<User, 'id'>;
|
|
export type UpdateUserInput = Partial<CreateUserInput>;
|
|
```
|
|
|
|
---
|
|
|
|
## 测试最佳实践
|
|
|
|
### 单元测试
|
|
|
|
#### 使用 Vitest 进行前端测试
|
|
|
|
```typescript
|
|
// components/__tests__/UserCard.test.tsx
|
|
import { describe, it, expect } from 'vitest';
|
|
import { render, screen } from '@testing-library/react';
|
|
import { UserCard } from '../UserCard';
|
|
|
|
describe('UserCard', () => {
|
|
const user = {
|
|
id: '1',
|
|
name: 'John Doe',
|
|
email: 'john@example.com',
|
|
};
|
|
|
|
it('renders user information', () => {
|
|
render(<UserCard user={user} />);
|
|
|
|
expect(screen.getByText('John Doe')).toBeInTheDocument();
|
|
expect(screen.getByText('john@example.com')).toBeInTheDocument();
|
|
});
|
|
});
|
|
```
|
|
|
|
#### 使用 Jest 进行后端测试
|
|
|
|
```typescript
|
|
// services/api/src/user/user.service.spec.ts
|
|
import { Test, TestingModule } from '@nestjs/testing';
|
|
import { UserService } from './user.service';
|
|
|
|
describe('UserService', () => {
|
|
let service: UserService;
|
|
|
|
beforeEach(async () => {
|
|
const module: TestingModule = await Test.createTestingModule({
|
|
providers: [UserService],
|
|
}).compile();
|
|
|
|
service = module.get<UserService>(UserService);
|
|
});
|
|
|
|
it('should be defined', () => {
|
|
expect(service).toBeDefined();
|
|
});
|
|
});
|
|
```
|
|
|
|
### 测试原则
|
|
|
|
- **AAA 模式**: Arrange, Act, Assert
|
|
- **描述性的测试名称
|
|
- **测试应该独立
|
|
- **测试行为而不是实现
|
|
|
|
```typescript
|
|
describe('UserService', () => {
|
|
it('should create a new user', async () => {
|
|
// Arrange
|
|
const createUserDto = {
|
|
name: 'Test',
|
|
email: 'test@example.com',
|
|
password: 'password123',
|
|
};
|
|
|
|
// Act
|
|
const result = await service.create(createUserDto);
|
|
|
|
// Assert
|
|
expect(result.name).toEqual('Test');
|
|
});
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## 下一步
|
|
|
|
- [常见问题](./troubleshooting.md) - 查看常见问题
|
|
- [贡献指南](./contributing.md) - 了解如何贡献
|
|
|
|
---
|
|
|
|
> **文档维护**: 本文档由开发团队维护,如有问题或建议请提交 Issue。
|
|
> **最后更新**: 2026-05-25
|