React + TypeScript 最佳实践:构建高可维护前端项目

简介: 本文系统梳理了 React + TypeScript 高可维护项目的最佳实践,涵盖项目结构、类型设计、组件模式、自定义 Hook、状态管理、API 服务、性能优化及测试部署等全链路方案,助力构建高质量企业级前端应用。

React + TypeScript 最佳实践:构建高可维护前端项目


引言

在现代前端开发中,React 和 TypeScript 的组合已经成为构建大型、复杂应用的首选技术栈。React 提供了组件化的开发模式和高效的虚拟 DOM 机制,而 TypeScript 则为 JavaScript 带来了静态类型检查,显著提升了代码的可靠性和可维护性。

本文将深入探讨 React + TypeScript 项目中的最佳实践,涵盖从项目架构设计到具体编码实现的各个方面。我们将通过实际的代码示例,展示如何构建高可维护、高性能的前端项目。
image.png

项目结构与组织

良好的项目结构是构建可维护应用的基础。一个合理的项目结构应该清晰地分离关注点,便于团队协作和代码维护。

src/
├── components/           # 可复用的 UI 组件
│   ├── common/          # 通用组件
│   ├── layout/          # 布局组件
│   └── ui/             # 基础 UI 组件
├── pages/              # 页面级组件
├── hooks/              # 自定义 hooks
├── services/           # API 服务和业务逻辑
├── types/              # TypeScript 类型定义
├── utils/              # 工具函数
├── store/              # 状态管理 (Redux/Zustand)
├── constants/          # 常量定义
├── assets/             # 静态资源
├── styles/             # 全局样式
└── config/             # 配置文件

这种结构遵循了单一职责原则,每个目录都有明确的职责范围,便于维护和扩展。

TypeScript 类型系统最佳实践

基础类型定义

export interface User {
   
  id: string;
  name: string;
  email: string;
  avatar?: string;
  createdAt: Date;
  updatedAt: Date;
  isActive: boolean;
}

export interface UserPreferences {
   
  theme: 'light' | 'dark';
  language: 'en' | 'zh' | 'es';
  notifications: {
   
    email: boolean;
    push: boolean;
    sms: boolean;
  };
}

// types/api.ts
export interface ApiResponse<T> {
   
  data: T;
  message: string;
  success: boolean;
  timestamp: number;
}

export interface Pagination {
   
  page: number;
  size: number;
  total: number;
  totalPages: number;
}

export interface QueryParams {
   
  page?: number;
  size?: number;
  sort?: string;
  search?: string;
  filters?: Record<string, string>;
}

高级类型技巧

// utils/types.ts
export type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

export type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;

export type UnionToIntersection<U> = 
  (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;

export type DeepPartial<T> = {
   
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

export type NonNullableProperties<T> = {
   
  [K in keyof T]: NonNullable<T[K]>;
};

// 用于创建受控组件的类型
export interface ControlledComponent<T> {
   
  value: T;
  onChange: (value: T) => void;
}

// 用于表单验证的类型
export interface ValidationRule<T> {
   
  validate: (value: T) => boolean;
  message: string;
}

export interface FormField<T> {
   
  name: string;
  value: T;
  error?: string;
  touched: boolean;
  rules?: ValidationRule<T>[];
}

组件设计模式

基础组件模式

// components/common/Button.tsx
import React, {
    ButtonHTMLAttributes, forwardRef } from 'react';

export type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'success' | 'outline';
export type ButtonSize = 'small' | 'medium' | 'large';

export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
   
  variant?: ButtonVariant;
  size?: ButtonSize;
  loading?: boolean;
  icon?: React.ReactNode;
  iconPosition?: 'left' | 'right';
  fullWidth?: boolean;
}

const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  (
    {
   
      children,
      variant = 'primary',
      size = 'medium',
      loading = false,
      icon,
      iconPosition = 'left',
      fullWidth = false,
      className = '',
      disabled,
      ...props
    },
    ref
  ) => {
   
    const baseClasses = 'inline-flex items-center justify-center font-medium rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed';

    const variantClasses = {
   
      primary: 'bg-blue-600 hover:bg-blue-700 focus:ring-blue-500 text-white',
      secondary: 'bg-gray-600 hover:bg-gray-700 focus:ring-gray-500 text-white',
      danger: 'bg-red-600 hover:bg-red-700 focus:ring-red-500 text-white',
      success: 'bg-green-600 hover:bg-green-700 focus:ring-green-500 text-white',
      outline: 'border border-gray-300 hover:bg-gray-50 focus:ring-blue-500 text-gray-700',
    };

    const sizeClasses = {
   
      small: 'px-2 py-1 text-sm',
      medium: 'px-4 py-2 text-base',
      large: 'px-6 py-3 text-lg',
    };

    const widthClass = fullWidth ? 'w-full' : '';

    const classes = [
      baseClasses,
      variantClasses[variant],
      sizeClasses[size],
      widthClass,
      className,
    ].join(' ');

    return (
      <button
        ref={
   ref}
        className={
   classes}
        disabled={
   disabled || loading}
        {
   ...props}
      >
        {
   loading && (
          <span className="animate-spin mr-2"></span>
        )}
        {
   icon && iconPosition === 'left' && <span className="mr-2">{
   icon}</span>}
        {
   children}
        {
   icon && iconPosition === 'right' && <span className="ml-2">{
   icon}</span>}
      </button>
    );
  }
);

Button.displayName = 'Button';

export default Button;

容器组件模式

// components/containers/UserProfileContainer.tsx
import React, {
    useState, useEffect } from 'react';
import {
    User, UserPreferences } from '../../types/user';
import {
    Button } from '../common/Button';
import {
    UserProfile } from '../ui/UserProfile';
import {
    UserPreferencesForm } from '../forms/UserPreferencesForm';
import {
    userService } from '../../services/userService';

interface UserProfileContainerProps {
   
  userId: string;
}

export const UserProfileContainer: React.FC<UserProfileContainerProps> = ({
    userId }) => {
   
  const [user, setUser] = useState<User | null>(null);
  const [preferences, setPreferences] = useState<UserPreferences | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
   
    const fetchUserData = async () => {
   
      try {
   
        setLoading(true);
        const [userData, preferencesData] = await Promise.all([
          userService.getUserById(userId),
          userService.getUserPreferences(userId)
        ]);

        setUser(userData);
        setPreferences(preferencesData);
      } catch (err) {
   
        setError(err instanceof Error ? err.message : 'Failed to fetch user data');
      } finally {
   
        setLoading(false);
      }
    };

    fetchUserData();
  }, [userId]);

  const handlePreferencesUpdate = async (newPreferences: UserPreferences) => {
   
    try {
   
      await userService.updateUserPreferences(userId, newPreferences);
      setPreferences(newPreferences);
    } catch (err) {
   
      setError(err instanceof Error ? err.message : 'Failed to update preferences');
    }
  };

  if (loading) {
   
    return <div className="flex justify-center items-center h-64">
      <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
    </div>;
  }

  if (error) {
   
    return (
      <div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative">
        <strong>Error: </strong> {
   error}
      </div>
    );
  }

  if (!user) {
   
    return <div>User not found</div>;
  }

  return (
    <div className="max-w-4xl mx-auto p-6">
      <div className="bg-white shadow rounded-lg p-6">
        <UserProfile user={
   user} />

        <div className="mt-8">
          <h2 className="text-xl font-semibold mb-4">Preferences</h2>
          <UserPreferencesForm
            preferences={
   preferences}
            onSubmit={
   handlePreferencesUpdate}
          />
        </div>

        <div className="mt-6 flex space-x-4">
          <Button variant="primary">Edit Profile</Button>
          <Button variant="secondary">Change Password</Button>
        </div>
      </div>
    </div>
  );
};

自定义 Hooks 模式

数据获取 Hook

// hooks/useApi.ts
import {
    useState, useEffect, useCallback } from 'react';

export interface ApiState<T> {
   
  data: T | null;
  loading: boolean;
  error: string | null;
  refetch: () => void;
}

export function useApi<T>(apiCall: () => Promise<T>, dependencies: any[] = []): ApiState<T> {
   
  const [state, setState] = useState<ApiState<T>>({
   
    data: null,
    loading: true,
    error: null,
    refetch: () => {
   }
  });

  const fetchData = useCallback(async () => {
   
    try {
   
      setState(prev => ({
    ...prev, loading: true, error: null }));
      const data = await apiCall();
      setState(prev => ({
    ...prev, data, loading: false }));
    } catch (error) {
   
      setState(prev => ({
    
        ...prev, 
        loading: false, 
        error: error instanceof Error ? error.message : 'An error occurred' 
      }));
    }
  }, [apiCall]);

  useEffect(() => {
   
    fetchData();
  }, dependencies);

  return {
   
    ...state,
    refetch: fetchData
  };
}

// hooks/usePagination.ts
export interface PaginationParams {
   
  page: number;
  size: number;
  total: number;
  totalPages: number;
}

export interface UsePaginationResult {
   
  pagination: PaginationParams;
  goToPage: (page: number) => void;
  nextPage: () => void;
  prevPage: () => void;
  setPageSize: (size: number) => void;
  canGoNext: boolean;
  canGoPrev: boolean;
}

export function usePagination(total: number, initialPageSize: number = 10): UsePaginationResult {
   
  const [page, setPage] = useState(1);
  const [pageSize, setPageSize] = useState(initialPageSize);

  const totalPages = Math.ceil(total / pageSize);
  const canGoNext = page < totalPages;
  const canGoPrev = page > 1;

  const goToPage = (newPage: number) => {
   
    if (newPage >= 1 && newPage <= totalPages) {
   
      setPage(newPage);
    }
  };

  const nextPage = () => {
   
    if (canGoNext) {
   
      setPage(prev => prev + 1);
    }
  };

  const prevPage = () => {
   
    if (canGoPrev) {
   
      setPage(prev => prev - 1);
    }
  };

  return {
   
    pagination: {
   
      page,
      size: pageSize,
      total,
      totalPages
    },
    goToPage,
    nextPage,
    prevPage,
    setPageSize,
    canGoNext,
    canGoPrev
  };
}

状态管理 Hook

// hooks/useForm.ts
import {
    useState, useCallback } from 'react';

export interface FormErrors {
   
  [key: string]: string;
}

export interface UseFormResult<T> {
   
  values: T;
  errors: FormErrors;
  touched: {
    [key: string]: boolean };
  handleChange: (name: keyof T, value: any) => void;
  handleBlur: (name: keyof T) => void;
  handleSubmit: (onSubmit: (values: T) => void) => void;
  setFieldValue: (name: keyof T, value: any) => void;
  setFieldError: (name: keyof T, error: string) => void;
  resetForm: () => void;
}

export function useForm<T extends Record<string, any>>(
  initialValues: T,
  validationSchema?: (values: T) => FormErrors
): UseFormResult<T> {
   
  const [values, setValues] = useState<T>(initialValues);
  const [errors, setErrors] = useState<FormErrors>({
   });
  const [touched, setTouched] = useState<{
    [key: string]: boolean }>({
   });

  const validate = useCallback((values: T): FormErrors => {
   
    if (validationSchema) {
   
      return validationSchema(values);
    }
    return {
   };
  }, [validationSchema]);

  const handleChange = useCallback((name: keyof T, value: any) => {
   
    setValues(prev => ({
    ...prev, [name]: value }));

    // 如果字段已经被触摸过,则重新验证
    if (touched[name as string]) {
   
      const newErrors = validate({
    ...values, [name]: value });
      setErrors(prev => ({
    ...prev, [name]: newErrors[name as string] }));
    }
  }, [touched, values, validate]);

  const handleBlur = useCallback((name: keyof T) => {
   
    setTouched(prev => ({
    ...prev, [name]: true }));
    const newErrors = validate(values);
    setErrors(newErrors);
  }, [values, validate]);

  const handleSubmit = useCallback((onSubmit: (values: T) => void) => {
   
    const newErrors = validate(values);
    setErrors(newErrors);
    setTouched(Object.keys(values).reduce((acc, key) => ({
    ...acc, [key]: true }), {
   } as any));

    if (Object.keys(newErrors).length === 0) {
   
      onSubmit(values);
    }
  }, [values, validate]);

  const setFieldValue = useCallback((name: keyof T, value: any) => {
   
    setValues(prev => ({
    ...prev, [name]: value }));
  }, []);

  const setFieldError = useCallback((name: keyof T, error: string) => {
   
    setErrors(prev => ({
    ...prev, [name]: error }));
  }, []);

  const resetForm = useCallback(() => {
   
    setValues(initialValues);
    setErrors({
   });
    setTouched({
   });
  }, [initialValues]);

  return {
   
    values,
    errors,
    touched,
    handleChange,
    handleBlur,
    handleSubmit,
    setFieldValue,
    setFieldError,
    resetForm
  };
}

状态管理策略

Context API 模式

// contexts/UserContext.tsx
import React, {
    createContext, useContext, useReducer, ReactNode } from 'react';
import {
    User } from '../types/user';

interface UserState {
   
  currentUser: User | null;
  isAuthenticated: boolean;
  loading: boolean;
  error: string | null;
}

type UserAction =
  | {
    type: 'LOGIN_START' }
  | {
    type: 'LOGIN_SUCCESS'; payload: User }
  | {
    type: 'LOGIN_FAILURE'; payload: string }
  | {
    type: 'LOGOUT' }
  | {
    type: 'UPDATE_USER'; payload: Partial<User> };

const initialState: UserState = {
   
  currentUser: null,
  isAuthenticated: false,
  loading: false,
  error: null,
};

const UserContext = createContext<{
   
  state: UserState;
  dispatch: React.Dispatch<UserAction>;
} | undefined>(undefined);

const userReducer = (state: UserState, action: UserAction): UserState => {
   
  switch (action.type) {
   
    case 'LOGIN_START':
      return {
   
        ...state,
        loading: true,
        error: null,
      };
    case 'LOGIN_SUCCESS':
      return {
   
        ...state,
        loading: false,
        isAuthenticated: true,
        currentUser: action.payload,
        error: null,
      };
    case 'LOGIN_FAILURE':
      return {
   
        ...state,
        loading: false,
        isAuthenticated: false,
        currentUser: null,
        error: action.payload,
      };
    case 'LOGOUT':
      return {
   
        ...state,
        loading: false,
        isAuthenticated: false,
        currentUser: null,
        error: null,
      };
    case 'UPDATE_USER':
      if (!state.currentUser) return state;
      return {
   
        ...state,
        currentUser: {
    ...state.currentUser, ...action.payload },
      };
    default:
      return state;
  }
};

interface UserProviderProps {
   
  children: ReactNode;
}

export const UserProvider: React.FC<UserProviderProps> = ({
    children }) => {
   
  const [state, dispatch] = useReducer(userReducer, initialState);

  return (
    <UserContext.Provider value={
   {
    state, dispatch }}>
      {
   children}
    </UserContext.Provider>
  );
};

export const useUser = () => {
   
  const context = useContext(UserContext);
  if (!context) {
   
    throw new Error('useUser must be used within a UserProvider');
  }
  return context;
};

状态持久化

// hooks/useStorageState.ts
import {
    useState, useEffect } from 'react';

export function useStorageState<T>(
  key: string,
  initialValue: T,
  storageType: 'localStorage' | 'sessionStorage' = 'localStorage'
): [T, (value: T) => void] {
   
  const storage = storageType === 'localStorage' ? localStorage : sessionStorage;

  const [state, setState] = useState<T>(() => {
   
    try {
   
      const item = storage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
   
      console.error(`Error reading ${
     storageType} key "${
     key}":`, error);
      return initialValue;
    }
  });

  useEffect(() => {
   
    try {
   
      storage.setItem(key, JSON.stringify(state));
    } catch (error) {
   
      console.error(`Error setting ${
     storageType} key "${
     key}":`, error);
    }
  }, [key, state, storageType]);

  return [state, setState];
}

// 使用示例
const [theme, setTheme] = useStorageState<'light' | 'dark'>('theme', 'light');
const [userPreferences, setUserPreferences] = useStorageState<UserPreferences>('userPreferences', {
   
  theme: 'light',
  language: 'en',
  notifications: {
   
    email: true,
    push: true,
    sms: false,
  },
});

API 服务层设计

服务层抽象

// services/apiClient.ts
import axios, {
    AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';

export interface ApiResponse<T> {
   
  data: T;
  message: string;
  success: boolean;
}

export interface ApiError {
   
  message: string;
  code: string;
  details?: any;
}

class ApiClient {
   
  private client: AxiosInstance;

  constructor(baseURL: string) {
   
    this.client = axios.create({
   
      baseURL,
      timeout: 10000,
      headers: {
   
        'Content-Type': 'application/json',
      },
    });

    this.setupInterceptors();
  }

  private setupInterceptors() {
   
    // 请求拦截器
    this.client.interceptors.request.use(
      (config) => {
   
        const token = localStorage.getItem('access_token');
        if (token) {
   
          config.headers.Authorization = `Bearer ${
     token}`;
        }
        return config;
      },
      (error) => {
   
        return Promise.reject(error);
      }
    );

    // 响应拦截器
    this.client.interceptors.response.use(
      (response) => response,
      (error) => {
   
        if (error.response?.status === 401) {
   
          // 处理未授权错误
          localStorage.removeItem('access_token');
          window.location.href = '/login';
        }
        return Promise.reject(error);
      }
    );
  }

  public async get<T>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<ApiResponse<T>>> {
   
    return this.client.get<ApiResponse<T>>(url, config);
  }

  public async post<T, R = T>(url: string, data?: T, config?: AxiosRequestConfig): Promise<AxiosResponse<ApiResponse<R>>> {
   
    return this.client.post<ApiResponse<R>>(url, data, config);
  }

  public async put<T, R = T>(url: string, data?: T, config?: AxiosRequestConfig): Promise<AxiosResponse<ApiResponse<R>>> {
   
    return this.client.put<ApiResponse<R>>(url, data, config);
  }

  public async delete<T>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<ApiResponse<T>>> {
   
    return this.client.delete<ApiResponse<T>>(url, config);
  }

  public async patch<T, R = T>(url: string, data?: T, config?: AxiosRequestConfig): Promise<AxiosResponse<ApiResponse<R>>> {
   
    return this.client.patch<ApiResponse<R>>(url, data, config);
  }
}

export const apiClient = new ApiClient(process.env.REACT_APP_API_BASE_URL || 'http://localhost:3001/api');

具体服务实现

// services/userService.ts
import {
    User, UserPreferences } from '../types/user';
import {
    apiClient } from './apiClient';

export interface UserService {
   
  getUserById: (id: string) => Promise<User>;
  getUserPreferences: (userId: string) => Promise<UserPreferences>;
  updateUserPreferences: (userId: string, preferences: UserPreferences) => Promise<UserPreferences>;
  getUsers: (page: number, size: number) => Promise<{
    data: User[]; total: number }>;
  createUser: (userData: Omit<User, 'id' | 'createdAt' | 'updatedAt'>) => Promise<User>;
  updateUser: (id: string, userData: Partial<User>) => Promise<User>;
  deleteUser: (id: string) => Promise<void>;
}

class UserServiceImpl implements UserService {
   
  async getUserById(id: string): Promise<User> {
   
    const response = await apiClient.get<User>(`/users/${
     id}`);
    return response.data.data;
  }

  async getUserPreferences(userId: string): Promise<UserPreferences> {
   
    const response = await apiClient.get<UserPreferences>(`/users/${
     userId}/preferences`);
    return response.data.data;
  }

  async updateUserPreferences(userId: string, preferences: UserPreferences): Promise<UserPreferences> {
   
    const response = await apiClient.put<UserPreferences>(`/users/${
     userId}/preferences`, preferences);
    return response.data.data;
  }

  async getUsers(page: number, size: number): Promise<{
    data: User[]; total: number }> {
   
    const response = await apiClient.get<{
    data: User[]; total: number }>(`/users?page=${
     page}&size=${
     size}`);
    return response.data.data;
  }

  async createUser(userData: Omit<User, 'id' | 'createdAt' | 'updatedAt'>): Promise<User> {
   
    const response = await apiClient.post<Omit<User, 'id' | 'createdAt' | 'updatedAt'>, User>('/users', userData);
    return response.data.data;
  }

  async updateUser(id: string, userData: Partial<User>): Promise<User> {
   
    const response = await apiClient.put<Partial<User>, User>(`/users/${
     id}`, userData);
    return response.data.data;
  }

  async deleteUser(id: string): Promise<void> {
   
    await apiClient.delete(`/users/${
     id}`);
  }
}

export const userService = new UserServiceImpl();

性能优化策略

组件优化

// hooks/useMemoCompare.ts
import {
    useRef, useEffect } from 'react';

export function useMemoCompare<T>(current: T, compare: (prev: T, next: T) => boolean) {
   
  const previousRef = useRef<T>();
  const previous = previousRef.current;

  const isEqual = compare(previous, current);

  useEffect(() => {
   
    if (!isEqual) {
   
      previousRef.current = current;
    }
  });

  return isEqual ? previous : current;
}
// hooks/useDebounce.ts
import {
    useState, useEffect } from 'react';

export function useDebounce<T>(value: T, delay: number): T {
   
  const [debouncedValue, setDebouncedValue] = useState<T>(value);

  useEffect(() => {
   
    const handler = setTimeout(() => {
   
      setDebouncedValue(value);
    }, delay);

    return () => {
   
      clearTimeout(handler);
    };
  }, [value, delay]);

  return debouncedValue;
}
// components/optimized/UserList.tsx
import React, {
    memo, useMemo } from 'react';
import {
    User } from '../../types/user';

interface UserListProps {
   
  users: User[];
  onUserClick: (user: User) => void;
  searchTerm?: string;
}

const UserList: React.FC<UserListProps> = memo(({
    users, onUserClick, searchTerm = '' }) => {
   
  const filteredUsers = useMemo(() => {
   
    if (!searchTerm) return users;
    return users.filter(user => 
      user.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
      user.email.toLowerCase().includes(searchTerm.toLowerCase())
    );
  }, [users, searchTerm]);

  return (
    <div className="space-y-2">
      {
   filteredUsers.map(user => (
        <div 
          key={
   user.id}
          className="p-4 border rounded cursor-pointer hover:bg-gray-50 transition-colors"
          onClick={
   () => onUserClick(user)}
        >
          <div className="flex items-center space-x-3">
            {
   user.avatar && <img src={
   user.avatar} alt={
   user.name} className="w-10 h-10 rounded-full" />}
            <div>
              <h3 className="font-medium">{
   user.name}</h3>
              <p className="text-sm text-gray-600">{
   user.email}</p>
            </div>
          </div>
        </div>
      ))}
    </div>
  );
});

UserList.displayName = 'UserList';

export default UserList;

虚拟滚动实现

// components/common/VirtualList.tsx
import React, {
    useState, useEffect, useRef, useCallback } from 'react';

interface VirtualListProps<T> {
   
  items: T[];
  itemHeight: number;
  renderItem: (item: T, index: number) => React.ReactNode;
  containerHeight?: number;
}

export const VirtualList = <T extends {
   }>({
   
  items,
  itemHeight,
  renderItem,
  containerHeight = 400,
}: VirtualListProps<T>) => {
   
  const [scrollTop, setScrollTop] = useState(0);
  const [visibleStart, setVisibleStart] = useState(0);
  const [visibleEnd, setVisibleEnd] = useState(0);
  const containerRef = useRef<HTMLDivElement>(null);

  const totalHeight = items.length * itemHeight;
  const visibleCount = Math.ceil(containerHeight / itemHeight);
  const bufferCount = 5;

  useEffect(() => {
   
    const calculateVisibleRange = () => {
   
      const start = Math.max(0, Math.floor(scrollTop / itemHeight) - bufferCount);
      const end = Math.min(items.length, start + visibleCount + bufferCount * 2);

      setVisibleStart(start);
      setVisibleEnd(end);
    };

    calculateVisibleRange();
  }, [scrollTop, itemHeight, containerHeight, items.length]);

  const handleScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
   
    setScrollTop(e.currentTarget.scrollTop);
  }, []);

  const visibleItems = items.slice(visibleStart, visibleEnd);

  return (
    <div
      ref={
   containerRef}
      className="overflow-y-auto relative"
      style={
   {
    height: containerHeight }}
      onScroll={
   handleScroll}
    >
      <div style={
   {
    height: totalHeight, position: 'relative' }}>
        <div
          style={
   {
   
            position: 'absolute',
            top: visibleStart * itemHeight,
            height: visibleItems.length * itemHeight,
          }}
        >
          {
   visibleItems.map((item, index) => (
            <div key={
   visibleStart + index} style={
   {
    height: itemHeight }}>
              {
   renderItem(item, visibleStart + index)}
            </div>
          ))}
        </div>
      </div>
    </div>
  );
};

测试策略

单元测试

// __tests__/hooks/useForm.test.ts
import {
    renderHook, act } from '@testing-library/react';
import {
    useForm } from '../../hooks/useForm';

describe('useForm', () => {
   
  const initialValues = {
    name: '', email: '' };

  it('should initialize with correct values', () => {
   
    const {
    result } = renderHook(() => useForm(initialValues));

    expect(result.current.values).toEqual(initialValues);
    expect(result.current.errors).toEqual({
   });
    expect(result.current.touched).toEqual({
   });
  });

  it('should handle field changes', () => {
   
    const {
    result } = renderHook(() => useForm(initialValues));

    act(() => {
   
      result.current.handleChange('name', 'John Doe');
    });

    expect(result.current.values.name).toBe('John Doe');
  });

  it('should validate form on submit', () => {
   
    const validationSchema = (values: typeof initialValues) => {
   
      const errors: {
    [key: string]: string } = {
   };
      if (!values.name) errors.name = 'Name is required';
      if (!values.email) errors.email = 'Email is required';
      return errors;
    };

    const {
    result } = renderHook(() => useForm(initialValues, validationSchema));

    let submittedValues: typeof initialValues | null = null;
    const onSubmit = (values: typeof initialValues) => {
   
      submittedValues = values;
    };

    act(() => {
   
      result.current.handleSubmit(onSubmit);
    });

    expect(result.current.errors).toEqual({
   
      name: 'Name is required',
      email: 'Email is required',
    });
    expect(submittedValues).toBeNull();
  });
});

集成测试

// __tests__/components/UserProfileContainer.test.tsx
import React from 'react';
import {
    render, screen, waitFor } from '@testing-library/react';
import {
    UserProfileContainer } from '../../components/containers/UserProfileContainer';
import {
    UserProvider } from '../../contexts/UserContext';

// Mock the service
jest.mock('../../services/userService', () => ({
   
  userService: {
   
    getUserById: jest.fn().mockResolvedValue({
   
      id: '1',
      name: 'John Doe',
      email: 'john@example.com',
      createdAt: new Date(),
      updatedAt: new Date(),
      isActive: true,
    }),
    getUserPreferences: jest.fn().mockResolvedValue({
   
      theme: 'light',
      language: 'en',
      notifications: {
   
        email: true,
        push: true,
        sms: false,
      },
    }),
  },
}));

describe('UserProfileContainer', () => {
   
  const renderWithProvider = (ui: React.ReactElement) => {
   
    return render(
      <UserProvider>
        {
   ui}
      </UserProvider>
    );
  };

  it('should render user profile', async () => {
   
    renderWithProvider(<UserProfileContainer userId="1" />);

    await waitFor(() => {
   
      expect(screen.getByText('John Doe')).toBeInTheDocument();
      expect(screen.getByText('john@example.com')).toBeInTheDocument();
    });
  });

  it('should handle loading state', () => {
   
    renderWithProvider(<UserProfileContainer userId="1" />);

    expect(screen.getByRole('status')).toBeInTheDocument();
  });
});

代码质量保证

ESLint 配置

// .eslintrc.js
module.exports = {
   
  parser: '@typescript-eslint/parser',
  parserOptions: {
   
    ecmaVersion: 2020,
    sourceType: 'module',
    ecmaFeatures: {
   
      jsx: true,
    },
  },
  settings: {
   
    react: {
   
      version: 'detect',
    },
  },
  extends: [
    'react-app',
    'react-app/jest',
    '@typescript-eslint/recommended',
    'prettier',
    'prettier/react',
  ],
  rules: {
   
    '@typescript-eslint/explicit-function-return-type': 'off',
    '@typescript-eslint/explicit-module-boundary-types': 'off',
    '@typescript-eslint/no-explicit-any': 'warn',
    '@typescript-eslint/no-unused-vars': 'error',
    'react/jsx-props-no-spreading': 'off',
    'react/prop-types': 'off',
    'react/react-in-jsx-scope': 'off',
    'react/jsx-filename-extension': [1, {
    extensions: ['.tsx', '.jsx'] }],
  },
};

Prettier 配置

// .prettierrc.js
module.exports = {
   
  semi: true,
  trailingComma: 'es5',
  singleQuote: true,
  printWidth: 100,
  tabWidth: 2,
  useTabs: false,
  bracketSpacing: true,
  arrowParens: 'avoid',
  endOfLine: 'lf',
};

部署和 CI/CD

Webpack 配置优化

// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const {
    BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = {
   
  mode: 'production',
  entry: './src/index.tsx',
  output: {
   
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash].js',
    chunkFilename: '[name].[contenthash].chunk.js',
    clean: true,
  },
  resolve: {
   
    extensions: ['.tsx', '.ts', '.js', '.jsx'],
    alias: {
   
      '@': path.resolve(__dirname, 'src'),
    },
  },
  module: {
   
    rules: [
      {
   
        test: /\.(ts|tsx)$/,
        exclude: /node_modules/,
        use: 'ts-loader',
      },
      {
   
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },
      {
   
        test: /\.(png|svg|jpg|jpeg|gif)$/i,
        type: 'asset/resource',
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
   
      template: './public/index.html',
    }),
    new BundleAnalyzerPlugin({
   
      analyzerMode: 'static',
      openAnalyzer: false,
    }),
  ],
  optimization: {
   
    splitChunks: {
   
      chunks: 'all',
      cacheGroups: {
   
        vendor: {
   
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
        },
      },
    },
  },
};

性能监控和分析

性能指标收集

// utils/performance.ts
export class PerformanceTracker {
   
  private marks: Map<string, number> = new Map();
  private measures: Map<string, number> = new Map();

  mark(name: string) {
   
    this.marks.set(name, performance.now());
  }

  measure(name: string, startMark: string, endMark: string) {
   
    const start = this.marks.get(startMark) || 0;
    const end = this.marks.get(endMark) || performance.now();
    const duration = end - start;
    this.measures.set(name, duration);
    return duration;
  }

  getMeasure(name: string): number | undefined {
   
    return this.measures.get(name);
  }

  getAllMeasures(): Map<string, number> {
   
    return new Map(this.measures);
  }

  clear() {
   
    this.marks.clear();
    this.measures.clear();
  }
}

export const performanceTracker = new PerformanceTracker();
// 组件性能监控
export const withPerformanceMonitoring = <T extends {
   }>(
  Component: React.ComponentType<T>,
  componentName: string
): React.ComponentType<T> => {
   
  return (props: T) => {
   
    performanceTracker.mark(`${
     componentName}-start`);

    const rendered = <Component {
   ...props} />;

    performanceTracker.mark(`${
     componentName}-end`);
    performanceTracker.measure(
      `${
     componentName}-render`,
      `${
     componentName}-start`,
      `${
     componentName}-end`
    );

    return rendered;
  };
};

总结

通过实施这些最佳实践,我们可以构建出高质量、高可维护的 React + TypeScript 项目。关键要点包括:

  1. 合理的项目结构:清晰的目录结构和模块划分
  2. 强类型系统:充分利用 TypeScript 的类型安全特性
  3. 组件设计模式:采用容器组件和展示组件分离的模式
  4. 自定义 Hooks:抽象通用逻辑,提高代码复用性
  5. 状态管理:选择合适的状态管理方案
  6. 性能优化:实现懒加载、虚拟滚动等优化策略
  7. 测试覆盖:编写全面的单元测试和集成测试
  8. 代码质量:配置 ESLint、Prettier 等工具保证代码质量
  9. 性能监控:持续监控应用性能指标

这些实践不仅能够提升开发效率,还能显著改善用户体验,为构建企业级前端应用奠定坚实基础。记住,最佳实践需要在实际项目中不断迭代和优化,根据具体需求选择合适的技术方案。



关于作者



🌟 我是suxiaoxiang,一位热爱技术的开发者

💡 专注于Java生态和前沿技术分享

🚀 持续输出高质量技术内容



如果这篇文章对你有帮助,请支持一下:




👍 点赞


收藏


👀 关注



您的支持是我持续创作的动力!感谢每一位读者的关注与认可!


目录
相关文章
|
4月前
|
人工智能 运维 安全
当Java遇见AI:无需Python,构建企业级RAG智能应用实战
本文深入探讨Java在RAG(检索增强生成)智能应用中的实战应用,打破“AI等于Python”的固有认知。依托Spring生态、高性能向量计算与企业级安全监控,结合文档预处理、混合检索、重排序与多LLM集成,构建高并发、可运维的生产级系统。展示如何用Java实现从文本分割、向量化到智能生成的全流程,助力企业高效落地AI能力,兼具性能、安全与可扩展性。
410 1
|
4月前
|
数据采集 机器学习/深度学习 自然语言处理
从零训练一个 ChatGPT:用 PyTorch 构建自己的 LLM 模型
本文介绍如何使用PyTorch从零构建类似ChatGPT的大型语言模型,涵盖Transformer架构、数据预处理、训练优化及文本生成全过程,助你掌握LLM核心原理与实现技术。(238字)
510 1
|
4月前
|
设计模式 缓存 监控
如何在 Spring 项目中优雅地使用设计模式
本文深入探讨在Spring项目中如何优雅应用设计模式,结合依赖注入与IoC特性,通过工厂、策略、装饰者等模式提升代码可维护性与扩展性,助力构建高效、灵活的Java应用。
310 5
|
4月前
|
缓存 监控 Java
用 Spring Boot 3 构建高性能 RESTful API 的 10 个关键技巧
本文介绍使用 Spring Boot 3 构建高性能 RESTful API 的 10 大关键技巧,涵盖启动优化、数据库连接池、缓存策略、异步处理、分页查询、限流熔断、日志监控等方面。通过合理配置与代码优化,显著提升响应速度、并发能力与系统稳定性,助力打造高效云原生应用。
544 3
|
3月前
|
Linux 开发工具
Linux VIM基本操作方式
VIM是Linux常用文本编辑器,支持多模式操作。包含普通、插入和命令三种模式,通过i/a/o等键进入插入模式,Esc返回普通模式,:进入命令模式。掌握hjkl移动、dd删除、yy复制、p粘贴及:wq保存退出等基本命令,可提升编辑效率。初学者需逐步练习,熟练运用。
237 6
|
安全 数据库 存储
数据库设计基石:一文搞懂 1NF、2NF、3NF 三大范式
数据库设计常遇数据冗余、增删改异常?根源往往是表结构不规范。本文带你轻松掌握数据库三大范式——1NF、2NF、3NF,从原子列到消除依赖,层层递进,提升数据一致性与可维护性,让数据库设计更高效、安全!#数据库 #范式设计
1567 0
|
数据采集 人工智能 JSON
大模型微调实战指南:从零开始定制你的专属 LLM
企业落地大模型常遇答非所问、风格不符等问题,因通用模型缺乏领域知识。微调(Fine-tuning)可让模型“学会说你的语言”。本文详解微调原理与PEFT技术,结合Hugging Face与LoRA实战,教你用少量数据在消费级GPU打造专属行业模型,提升垂直场景表现。
896 9
|
存储 C++ Java
C++ 指针详解:从入门到理解内存的本质
指针是C++中高效操作内存的核心工具,掌握它等于掌握程序底层运行机制。本文系统讲解指针基础、数组关联、动态内存管理及常见陷阱,助你避开“悬空”“野指针”等雷区,善用智能指针,真正实现“指”掌全局。#C++指针入门
541 156
|
4月前
|
数据可视化 JavaScript 前端开发
Three.js:开启Web 3D世界的魔法钥匙
Three.js是基于WebGL的JavaScript 3D库,简化了网页中3D图形的创建与渲染。它提供场景、相机、光照、动画等完整架构,支持丰富几何体、材质及高级特效,助力开发者轻松实现交互式3D可视化。
389 6
|
Java Spring 开发者
Spring Boot 常用注解详解:让你的开发更高效
本文详细解析Spring Boot常用注解,涵盖配置、组件、依赖注入、Web请求、数据验证、事务管理等核心场景,结合实例帮助开发者高效掌握注解使用技巧,提升开发效率与代码质量。
996 0