React + TypeScript 最佳实践:构建高可维护前端项目
引言
在现代前端开发中,React 和 TypeScript 的组合已经成为构建大型、复杂应用的首选技术栈。React 提供了组件化的开发模式和高效的虚拟 DOM 机制,而 TypeScript 则为 JavaScript 带来了静态类型检查,显著提升了代码的可靠性和可维护性。
本文将深入探讨 React + TypeScript 项目中的最佳实践,涵盖从项目架构设计到具体编码实现的各个方面。我们将通过实际的代码示例,展示如何构建高可维护、高性能的前端项目。
项目结构与组织
良好的项目结构是构建可维护应用的基础。一个合理的项目结构应该清晰地分离关注点,便于团队协作和代码维护。
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 项目。关键要点包括:
- 合理的项目结构:清晰的目录结构和模块划分
- 强类型系统:充分利用 TypeScript 的类型安全特性
- 组件设计模式:采用容器组件和展示组件分离的模式
- 自定义 Hooks:抽象通用逻辑,提高代码复用性
- 状态管理:选择合适的状态管理方案
- 性能优化:实现懒加载、虚拟滚动等优化策略
- 测试覆盖:编写全面的单元测试和集成测试
- 代码质量:配置 ESLint、Prettier 等工具保证代码质量
- 性能监控:持续监控应用性能指标
这些实践不仅能够提升开发效率,还能显著改善用户体验,为构建企业级前端应用奠定坚实基础。记住,最佳实践需要在实际项目中不断迭代和优化,根据具体需求选择合适的技术方案。
关于作者
🌟 我是suxiaoxiang,一位热爱技术的开发者
💡 专注于Java生态和前沿技术分享
🚀 持续输出高质量技术内容
如果这篇文章对你有帮助,请支持一下:
👍 点赞
⭐ 收藏
👀 关注
您的支持是我持续创作的动力!感谢每一位读者的关注与认可!