16 KiB
16 KiB
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)
组件开发规范
组件结构模板
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;
组件设计原则
- 单一职责: 每个组件只负责一个功能
- 可复用性: 通过props实现组件的灵活配置
- 可测试性: 组件逻辑清晰,易于测试
- 性能优化: 合理使用memo、useMemo、useCallback
- 无障碍性: 支持键盘导航和屏幕阅读器
Props设计规范
// ✅ 好的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 规范
类型定义规范
// 基础类型定义
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'>>;
类型导入导出规范
// 类型导入
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 设计
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 });
}
},
}));
状态管理原则
- 单一数据源: 每个状态只有一个来源
- 不可变更新: 使用不可变的方式更新状态
- 异步处理: 统一的异步操作模式
- 错误处理: 完善的错误状态管理
服务层规范
服务类设计
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}`);
}
}
}
服务层原则
- 静态方法: 使用静态方法避免实例化
- 错误处理: 统一的错误处理和日志记录
- 类型安全: 严格的类型定义和检查
- 文档注释: 详细的JSDoc注释
样式规范
TailwindCSS 使用规范
// ✅ 推荐的样式写法
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变量 */
.custom-component {
background-color: var(--primary-500);
color: var(--gray-50);
border-radius: var(--radius-md);
box-shadow: var(--shadow-md);
}
自定义Hooks规范
Hook设计模板
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,
};
};
测试规范
组件测试模板
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();
});
});
测试原则
- 测试行为: 测试组件的行为而不是实现细节
- 用户视角: 从用户的角度编写测试
- 覆盖率: 关键功能100%覆盖
- 可维护性: 测试代码也要易于维护
性能优化规范
React性能优化
// 使用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'));
列表优化
// 使用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>
);
错误处理规范
错误边界
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;
}
}
异步错误处理
// 在组件中处理异步错误
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配置
{
"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类型是否完整准确
- 是否有适当的错误处理
- 是否有性能优化考虑
- 是否有相应的测试覆盖
- 代码是否符合团队规范