mixvideo-v2/.promptx/frontend-coding-standards.md

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;

组件设计原则

  1. 单一职责: 每个组件只负责一个功能
  2. 可复用性: 通过props实现组件的灵活配置
  3. 可测试性: 组件逻辑清晰,易于测试
  4. 性能优化: 合理使用memo、useMemo、useCallback
  5. 无障碍性: 支持键盘导航和屏幕阅读器

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 });
    }
  },
}));

状态管理原则

  1. 单一数据源: 每个状态只有一个来源
  2. 不可变更新: 使用不可变的方式更新状态
  3. 异步处理: 统一的异步操作模式
  4. 错误处理: 完善的错误状态管理

服务层规范

服务类设计

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 使用规范

// ✅ 推荐的样式写法
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();
  });
});

测试原则

  1. 测试行为: 测试组件的行为而不是实现细节
  2. 用户视角: 从用户的角度编写测试
  3. 覆盖率: 关键功能100%覆盖
  4. 可维护性: 测试代码也要易于维护

性能优化规范

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类型是否完整准确
  • 是否有适当的错误处理
  • 是否有性能优化考虑
  • 是否有相应的测试覆盖
  • 代码是否符合团队规范