645 lines
16 KiB
Markdown
645 lines
16 KiB
Markdown
# MixVideo 前端开发规范
|
|
|
|
## 技术栈规范
|
|
|
|
### 核心技术
|
|
- **React 18**: 函数组件 + Hooks
|
|
- **TypeScript 5.8**: 严格类型检查
|
|
- **Vite 6.0**: 构建工具
|
|
- **TailwindCSS 3.4**: 样式框架
|
|
- **Zustand 4.4**: 状态管理
|
|
- **React Router 6.20**: 路由管理
|
|
|
|
### 开发工具
|
|
- **ESLint**: 代码检查
|
|
- **Prettier**: 代码格式化
|
|
- **Vitest**: 单元测试
|
|
- **Testing Library**: 组件测试
|
|
|
|
## 项目结构规范
|
|
|
|
### 目录结构
|
|
```
|
|
src/
|
|
├── components/ # 可复用组件
|
|
│ ├── ui/ # 基础UI组件
|
|
│ ├── business/ # 业务组件
|
|
│ └── __tests__/ # 组件测试
|
|
├── pages/ # 页面组件
|
|
├── hooks/ # 自定义Hooks
|
|
├── services/ # 业务服务层
|
|
├── store/ # Zustand状态管理
|
|
├── types/ # TypeScript类型定义
|
|
├── utils/ # 工具函数
|
|
├── styles/ # 样式文件
|
|
│ ├── design-system.css
|
|
│ └── animations.css
|
|
└── tests/ # 测试文件
|
|
├── setup.ts
|
|
├── components/
|
|
├── integration/
|
|
└── e2e/
|
|
```
|
|
|
|
### 文件命名规范
|
|
- **组件文件**: PascalCase (如 `MaterialCard.tsx`)
|
|
- **Hook文件**: camelCase (如 `useMaterialSearch.ts`)
|
|
- **服务文件**: camelCase (如 `materialService.ts`)
|
|
- **类型文件**: camelCase (如 `material.ts`)
|
|
- **工具文件**: camelCase (如 `imagePathUtils.ts`)
|
|
|
|
## 组件开发规范
|
|
|
|
### 组件结构模板
|
|
```typescript
|
|
import React from 'react';
|
|
import { clsx } from 'clsx';
|
|
|
|
// 类型定义
|
|
interface ComponentProps {
|
|
// 必需属性
|
|
id: string;
|
|
title: string;
|
|
|
|
// 可选属性
|
|
description?: string;
|
|
className?: string;
|
|
|
|
// 事件处理
|
|
onClick?: () => void;
|
|
onSubmit?: (data: FormData) => void;
|
|
}
|
|
|
|
/**
|
|
* 组件描述
|
|
* 遵循 Tauri 开发规范的组件设计原则
|
|
*/
|
|
export const Component: React.FC<ComponentProps> = ({
|
|
id,
|
|
title,
|
|
description,
|
|
className,
|
|
onClick,
|
|
onSubmit,
|
|
}) => {
|
|
// Hooks (按顺序)
|
|
const [state, setState] = useState<StateType>(initialState);
|
|
const { data, loading, error } = useCustomHook();
|
|
|
|
// 事件处理函数
|
|
const handleClick = useCallback(() => {
|
|
onClick?.();
|
|
}, [onClick]);
|
|
|
|
// 副作用
|
|
useEffect(() => {
|
|
// 副作用逻辑
|
|
}, [dependencies]);
|
|
|
|
// 条件渲染
|
|
if (loading) {
|
|
return <LoadingSpinner />;
|
|
}
|
|
|
|
if (error) {
|
|
return <ErrorMessage error={error} />;
|
|
}
|
|
|
|
// 主渲染
|
|
return (
|
|
<div className={clsx('base-classes', className)}>
|
|
<h2 className="text-lg font-semibold">{title}</h2>
|
|
{description && (
|
|
<p className="text-gray-600">{description}</p>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// 默认导出
|
|
export default Component;
|
|
```
|
|
|
|
### 组件设计原则
|
|
1. **单一职责**: 每个组件只负责一个功能
|
|
2. **可复用性**: 通过props实现组件的灵活配置
|
|
3. **可测试性**: 组件逻辑清晰,易于测试
|
|
4. **性能优化**: 合理使用memo、useMemo、useCallback
|
|
5. **无障碍性**: 支持键盘导航和屏幕阅读器
|
|
|
|
### Props设计规范
|
|
```typescript
|
|
// ✅ 好的Props设计
|
|
interface GoodProps {
|
|
// 必需属性在前
|
|
id: string;
|
|
title: string;
|
|
|
|
// 可选属性在后
|
|
description?: string;
|
|
variant?: 'primary' | 'secondary' | 'danger';
|
|
size?: 'sm' | 'md' | 'lg';
|
|
disabled?: boolean;
|
|
|
|
// 事件处理器
|
|
onClick?: (event: MouseEvent) => void;
|
|
onSubmit?: (data: FormData) => Promise<void>;
|
|
|
|
// 样式相关
|
|
className?: string;
|
|
style?: CSSProperties;
|
|
}
|
|
|
|
// ❌ 避免的Props设计
|
|
interface BadProps {
|
|
data: any; // 避免使用any
|
|
config: object; // 避免使用object
|
|
callback: Function; // 避免使用Function
|
|
}
|
|
```
|
|
|
|
## TypeScript 规范
|
|
|
|
### 类型定义规范
|
|
```typescript
|
|
// 基础类型定义
|
|
export interface Material {
|
|
id: string;
|
|
name: string;
|
|
path: string;
|
|
type: MaterialType;
|
|
size: number;
|
|
duration?: number;
|
|
createdAt: Date;
|
|
updatedAt: Date;
|
|
}
|
|
|
|
// 枚举定义
|
|
export enum MaterialType {
|
|
VIDEO = 'video',
|
|
AUDIO = 'audio',
|
|
IMAGE = 'image',
|
|
}
|
|
|
|
// 联合类型
|
|
export type Status = 'idle' | 'loading' | 'success' | 'error';
|
|
|
|
// 泛型接口
|
|
export interface ApiResponse<T> {
|
|
data: T;
|
|
message: string;
|
|
success: boolean;
|
|
}
|
|
|
|
// 工具类型
|
|
export type CreateMaterialRequest = Omit<Material, 'id' | 'createdAt' | 'updatedAt'>;
|
|
export type UpdateMaterialRequest = Partial<Pick<Material, 'name' | 'type'>>;
|
|
```
|
|
|
|
### 类型导入导出规范
|
|
```typescript
|
|
// 类型导入
|
|
import type { Material, MaterialType } from '../types/material';
|
|
import type { ComponentProps } from 'react';
|
|
|
|
// 值导入
|
|
import { MaterialService } from '../services/materialService';
|
|
import { useMaterialStore } from '../store/materialStore';
|
|
|
|
// 混合导入
|
|
import React, { type FC, type ReactNode } from 'react';
|
|
```
|
|
|
|
## 状态管理规范
|
|
|
|
### Zustand Store 设计
|
|
```typescript
|
|
import { create } from 'zustand';
|
|
import type { Material } from '../types/material';
|
|
|
|
interface MaterialState {
|
|
// 状态数据
|
|
materials: Material[];
|
|
selectedMaterial: Material | null;
|
|
loading: boolean;
|
|
error: string | null;
|
|
|
|
// 同步操作
|
|
setMaterials: (materials: Material[]) => void;
|
|
setSelectedMaterial: (material: Material | null) => void;
|
|
setLoading: (loading: boolean) => void;
|
|
setError: (error: string | null) => void;
|
|
|
|
// 异步操作
|
|
fetchMaterials: () => Promise<void>;
|
|
createMaterial: (request: CreateMaterialRequest) => Promise<void>;
|
|
updateMaterial: (id: string, request: UpdateMaterialRequest) => Promise<void>;
|
|
deleteMaterial: (id: string) => Promise<void>;
|
|
}
|
|
|
|
export const useMaterialStore = create<MaterialState>((set, get) => ({
|
|
// 初始状态
|
|
materials: [],
|
|
selectedMaterial: null,
|
|
loading: false,
|
|
error: null,
|
|
|
|
// 同步操作
|
|
setMaterials: (materials) => set({ materials }),
|
|
setSelectedMaterial: (selectedMaterial) => set({ selectedMaterial }),
|
|
setLoading: (loading) => set({ loading }),
|
|
setError: (error) => set({ error }),
|
|
|
|
// 异步操作
|
|
fetchMaterials: async () => {
|
|
try {
|
|
set({ loading: true, error: null });
|
|
const materials = await MaterialService.getAllMaterials();
|
|
set({ materials, loading: false });
|
|
} catch (error) {
|
|
set({ error: error.message, loading: false });
|
|
}
|
|
},
|
|
|
|
createMaterial: async (request) => {
|
|
try {
|
|
set({ loading: true, error: null });
|
|
const material = await MaterialService.createMaterial(request);
|
|
set((state) => ({
|
|
materials: [...state.materials, material],
|
|
loading: false,
|
|
}));
|
|
} catch (error) {
|
|
set({ error: error.message, loading: false });
|
|
}
|
|
},
|
|
}));
|
|
```
|
|
|
|
### 状态管理原则
|
|
1. **单一数据源**: 每个状态只有一个来源
|
|
2. **不可变更新**: 使用不可变的方式更新状态
|
|
3. **异步处理**: 统一的异步操作模式
|
|
4. **错误处理**: 完善的错误状态管理
|
|
|
|
## 服务层规范
|
|
|
|
### 服务类设计
|
|
```typescript
|
|
import { invoke } from '@tauri-apps/api/core';
|
|
import type { Material, CreateMaterialRequest, UpdateMaterialRequest } from '../types/material';
|
|
|
|
/**
|
|
* 素材管理服务
|
|
* 遵循 Tauri 开发规范的前端服务层设计
|
|
*/
|
|
export class MaterialService {
|
|
/**
|
|
* 获取所有素材
|
|
*/
|
|
static async getAllMaterials(): Promise<Material[]> {
|
|
try {
|
|
const materials = await invoke<Material[]>('get_all_materials');
|
|
return materials;
|
|
} catch (error) {
|
|
console.error('获取素材列表失败:', error);
|
|
throw new Error(`获取素材列表失败: ${error}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 根据ID获取素材
|
|
*/
|
|
static async getMaterialById(id: string): Promise<Material | null> {
|
|
try {
|
|
const material = await invoke<Material | null>('get_material_by_id', { id });
|
|
return material;
|
|
} catch (error) {
|
|
console.error('获取素材详情失败:', error);
|
|
throw new Error(`获取素材详情失败: ${error}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 创建素材
|
|
*/
|
|
static async createMaterial(request: CreateMaterialRequest): Promise<Material> {
|
|
try {
|
|
const material = await invoke<Material>('create_material', { request });
|
|
return material;
|
|
} catch (error) {
|
|
console.error('创建素材失败:', error);
|
|
throw new Error(`创建素材失败: ${error}`);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### 服务层原则
|
|
1. **静态方法**: 使用静态方法避免实例化
|
|
2. **错误处理**: 统一的错误处理和日志记录
|
|
3. **类型安全**: 严格的类型定义和检查
|
|
4. **文档注释**: 详细的JSDoc注释
|
|
|
|
## 样式规范
|
|
|
|
### TailwindCSS 使用规范
|
|
```typescript
|
|
// ✅ 推荐的样式写法
|
|
const buttonStyles = {
|
|
base: 'inline-flex items-center justify-center rounded-md font-medium transition-colors',
|
|
variants: {
|
|
primary: 'bg-primary-600 text-white hover:bg-primary-700',
|
|
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
|
|
danger: 'bg-red-600 text-white hover:bg-red-700',
|
|
},
|
|
sizes: {
|
|
sm: 'px-3 py-1.5 text-sm',
|
|
md: 'px-4 py-2 text-base',
|
|
lg: 'px-6 py-3 text-lg',
|
|
},
|
|
};
|
|
|
|
// 使用clsx进行条件样式
|
|
const className = clsx(
|
|
buttonStyles.base,
|
|
buttonStyles.variants[variant],
|
|
buttonStyles.sizes[size],
|
|
disabled && 'opacity-50 cursor-not-allowed',
|
|
className
|
|
);
|
|
```
|
|
|
|
### CSS变量使用
|
|
```css
|
|
/* 使用设计系统中定义的CSS变量 */
|
|
.custom-component {
|
|
background-color: var(--primary-500);
|
|
color: var(--gray-50);
|
|
border-radius: var(--radius-md);
|
|
box-shadow: var(--shadow-md);
|
|
}
|
|
```
|
|
|
|
## 自定义Hooks规范
|
|
|
|
### Hook设计模板
|
|
```typescript
|
|
import { useState, useEffect, useCallback } from 'react';
|
|
import type { Material } from '../types/material';
|
|
import { MaterialService } from '../services/materialService';
|
|
|
|
interface UseMaterialSearchOptions {
|
|
initialQuery?: string;
|
|
autoSearch?: boolean;
|
|
}
|
|
|
|
interface UseMaterialSearchReturn {
|
|
materials: Material[];
|
|
query: string;
|
|
loading: boolean;
|
|
error: string | null;
|
|
search: (query: string) => Promise<void>;
|
|
clearResults: () => void;
|
|
}
|
|
|
|
/**
|
|
* 素材搜索Hook
|
|
* 提供素材搜索功能的封装
|
|
*/
|
|
export const useMaterialSearch = (
|
|
options: UseMaterialSearchOptions = {}
|
|
): UseMaterialSearchReturn => {
|
|
const { initialQuery = '', autoSearch = false } = options;
|
|
|
|
const [materials, setMaterials] = useState<Material[]>([]);
|
|
const [query, setQuery] = useState(initialQuery);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const search = useCallback(async (searchQuery: string) => {
|
|
try {
|
|
setLoading(true);
|
|
setError(null);
|
|
setQuery(searchQuery);
|
|
|
|
const results = await MaterialService.searchMaterials(searchQuery);
|
|
setMaterials(results);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : '搜索失败');
|
|
setMaterials([]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
const clearResults = useCallback(() => {
|
|
setMaterials([]);
|
|
setQuery('');
|
|
setError(null);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (autoSearch && initialQuery) {
|
|
search(initialQuery);
|
|
}
|
|
}, [autoSearch, initialQuery, search]);
|
|
|
|
return {
|
|
materials,
|
|
query,
|
|
loading,
|
|
error,
|
|
search,
|
|
clearResults,
|
|
};
|
|
};
|
|
```
|
|
|
|
## 测试规范
|
|
|
|
### 组件测试模板
|
|
```typescript
|
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
import { describe, it, expect, vi } from 'vitest';
|
|
import { MaterialCard } from '../MaterialCard';
|
|
import type { Material } from '../../types/material';
|
|
|
|
const mockMaterial: Material = {
|
|
id: '1',
|
|
name: 'Test Material',
|
|
path: '/test/path',
|
|
type: 'video',
|
|
size: 1024,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
|
|
describe('MaterialCard', () => {
|
|
it('应该正确渲染素材信息', () => {
|
|
render(<MaterialCard material={mockMaterial} />);
|
|
|
|
expect(screen.getByText('Test Material')).toBeInTheDocument();
|
|
expect(screen.getByText('video')).toBeInTheDocument();
|
|
});
|
|
|
|
it('应该处理点击事件', async () => {
|
|
const onClickMock = vi.fn();
|
|
render(<MaterialCard material={mockMaterial} onClick={onClickMock} />);
|
|
|
|
fireEvent.click(screen.getByRole('button'));
|
|
|
|
await waitFor(() => {
|
|
expect(onClickMock).toHaveBeenCalledWith(mockMaterial);
|
|
});
|
|
});
|
|
|
|
it('应该显示加载状态', () => {
|
|
render(<MaterialCard material={mockMaterial} loading />);
|
|
|
|
expect(screen.getByTestId('loading-spinner')).toBeInTheDocument();
|
|
});
|
|
});
|
|
```
|
|
|
|
### 测试原则
|
|
1. **测试行为**: 测试组件的行为而不是实现细节
|
|
2. **用户视角**: 从用户的角度编写测试
|
|
3. **覆盖率**: 关键功能100%覆盖
|
|
4. **可维护性**: 测试代码也要易于维护
|
|
|
|
## 性能优化规范
|
|
|
|
### React性能优化
|
|
```typescript
|
|
// 使用memo优化组件重渲染
|
|
export const MaterialCard = memo<MaterialCardProps>(({ material, onClick }) => {
|
|
// 组件实现
|
|
});
|
|
|
|
// 使用useMemo优化计算
|
|
const expensiveValue = useMemo(() => {
|
|
return computeExpensiveValue(data);
|
|
}, [data]);
|
|
|
|
// 使用useCallback优化函数引用
|
|
const handleClick = useCallback((id: string) => {
|
|
onClick?.(id);
|
|
}, [onClick]);
|
|
|
|
// 使用lazy加载组件
|
|
const LazyComponent = lazy(() => import('./LazyComponent'));
|
|
```
|
|
|
|
### 列表优化
|
|
```typescript
|
|
// 使用react-window进行虚拟化
|
|
import { FixedSizeList as List } from 'react-window';
|
|
|
|
const VirtualizedList: FC<{ items: Material[] }> = ({ items }) => (
|
|
<List
|
|
height={600}
|
|
itemCount={items.length}
|
|
itemSize={80}
|
|
itemData={items}
|
|
>
|
|
{({ index, style, data }) => (
|
|
<div style={style}>
|
|
<MaterialCard material={data[index]} />
|
|
</div>
|
|
)}
|
|
</List>
|
|
);
|
|
```
|
|
|
|
## 错误处理规范
|
|
|
|
### 错误边界
|
|
```typescript
|
|
import React, { Component, type ReactNode } from 'react';
|
|
|
|
interface ErrorBoundaryState {
|
|
hasError: boolean;
|
|
error?: Error;
|
|
}
|
|
|
|
export class ErrorBoundary extends Component<
|
|
{ children: ReactNode },
|
|
ErrorBoundaryState
|
|
> {
|
|
constructor(props: { children: ReactNode }) {
|
|
super(props);
|
|
this.state = { hasError: false };
|
|
}
|
|
|
|
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
|
return { hasError: true, error };
|
|
}
|
|
|
|
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
|
console.error('ErrorBoundary caught an error:', error, errorInfo);
|
|
}
|
|
|
|
render() {
|
|
if (this.state.hasError) {
|
|
return (
|
|
<div className="error-fallback">
|
|
<h2>出现了错误</h2>
|
|
<p>{this.state.error?.message}</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return this.props.children;
|
|
}
|
|
}
|
|
```
|
|
|
|
### 异步错误处理
|
|
```typescript
|
|
// 在组件中处理异步错误
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const handleAsyncOperation = async () => {
|
|
try {
|
|
setError(null);
|
|
await someAsyncOperation();
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : '操作失败');
|
|
}
|
|
};
|
|
|
|
// 在服务层统一错误处理
|
|
export const handleApiError = (error: unknown): never => {
|
|
if (error instanceof Error) {
|
|
throw new Error(`API错误: ${error.message}`);
|
|
}
|
|
throw new Error('未知错误');
|
|
};
|
|
```
|
|
|
|
## 代码质量规范
|
|
|
|
### ESLint配置
|
|
```json
|
|
{
|
|
"extends": [
|
|
"@vitejs/eslint-config-react",
|
|
"@typescript-eslint/recommended"
|
|
],
|
|
"rules": {
|
|
"react-hooks/exhaustive-deps": "error",
|
|
"@typescript-eslint/no-unused-vars": "error",
|
|
"@typescript-eslint/explicit-function-return-type": "warn"
|
|
}
|
|
}
|
|
```
|
|
|
|
### 代码审查清单
|
|
- [ ] 组件是否遵循单一职责原则
|
|
- [ ] TypeScript类型是否完整准确
|
|
- [ ] 是否有适当的错误处理
|
|
- [ ] 是否有性能优化考虑
|
|
- [ ] 是否有相应的测试覆盖
|
|
- [ ] 代码是否符合团队规范
|