大规模代码重构
上周我重构了一个18000行代码的老项目,技术债很重,架构混乱。用Claude Code处理后,效率提升了20倍。这个案例会展示整个重构过程。
项目背景
项目什么情况
这是个跑了3年的电商管理系统,问题一堆:
- 18,000+行React代码
- 技术栈老掉牙: React 15.6, Redux 3.7
- 组件耦合严重
- 首屏要12秒,慢得要命
- 改一个bug出三个新bug
- 没有测试
重构目标
- 升级到React 18, Redux Toolkit
- 组件解耦,重新组织目录
- 首屏降到2秒以内
- 加TypeScript和测试
- 工具链现代化
需要多久
- 传统方式: 4-6周
- 用Claude Code: 1-2周
- 效率提升: 4-6倍
准备工作
项目分析
开始前得先搞清楚项目什么情况。启动Claude Code:
cd ecommerce-admin-system
claude
第一轮对话
我需要重构这个大项目,帮我分析一下:
项目路径: /path/to/ecommerce-admin-system
重点看:
1. 项目结构怎么组织的
2. 依赖版本老不老
3. 哪里有代码重复
4. 性能瓶颈在哪
Claude Code会深入分析:
📊 分析中...
✓ 读取package.json
✓ 扫描src/目录(127个文件)
✓ 分析组件层级
✓ 识别依赖关系
✓ 计算代码指标
## 分析结果
### 项目结构
src/ ├── components/ # 73个组件,职责混乱 ├── containers/ # 15个容器组件 ├── redux/ # Redux配置 ├── utils/ # 工具函数 ├── api/ # API调用 └── styles/ # CSS文件
主要问题
🔴 严重问题:
- 依赖过时(React 15.6, Redux 3.7)
- 没有类型检查
- 没有测试
- 没有代码分割(包大小2.8MB)
- 23处直接操作DOM
🟡 中等优先级:
- 状态管理混乱(Redux和本地state混用)
- 18个组件有重复代码
- 没有错误边界
- API URL硬编码
重构策略
第一周: 依赖和基础设施 第二周: 架构和组件 第三周: 性能和测试
开始第一周?
## 第一阶段:依赖升级
### 制定计划
创建详细的重构计划,包括:
- 每个阶段的具体任务
- 怎么回滚
- 测试策略
- 风险评估
**REFACTORING_PLAN.md**:
```markdown
# 电商管理系统重构计划
## 项目信息
- 当前代码行数: 18,247行
- 组件数量: 88个
- 重构开始日期: 2024-01-15
- 预计完成日期: 2024-02-15
## 阶段一:依赖升级 (Day 1-3)
### 任务清单
- [ ] 升级React到18.x
- [ ] 升级Redux到Redux Toolkit
- [ ] 升级React Router到v6
- [ ] 升级Webpack到5.x
- [ ] 配置TypeScript
- [ ] 设置ESLint和Prettier
### 回滚策略
- 使用Git分支: `refactor/phase1-dependencies`
- 保留原package.json为package.json.backup
- 每个升级点创建commit
## 阶段二:架构重构 (Day 4-10)
### 任务清单
- [ ] 重新设计目录结构
- [ ] 提取公共组件
- [ ] 重构状态管理
- [ ] API层抽象
- [ ] 路由重构
## 阶段三:性能优化 (Day 11-15)
### 任务清单
- [ ] 代码分割和懒加载
- [ ] 组件memoization
- [ ] 图片优 化
- [ ] 缓存策略
- [ ] 添加测试
### 风险评估
🔴 高风险:
- 依赖升级可能导致的不兼容
- 状态管理迁移可能引入数据流问题
🟡 中风险:
- 组件重构可能影响业务逻辑
- 性能优化可能改变用户行为
🟢 低风险:
- 代码规范调整
- 文档更新
### 回滚预案
1. Git版本控制 - 每个阶段独立分支
2. 数据库迁移脚本 - 保留rollback脚本
3. 环境变量备份 - .env.backup
4. 部署配置保留 - k8s-config.backup/
升级依赖
开始第一阶段:
1. 创建新分支
2. 升级React和核心依赖
3. 更新构建配置
4. 确保项目能跑起来
Claude Code会执行升级流程:
# Git操作
git checkout -b refactor/phase1-dependencies
git checkout -b backup/before-refactor
# 依赖升级流程
package.json (升级后):
{
"name": "ecommerce-admin-system",
"version": "2.0.0",
"description": "E-commerce Admin System - Refactored",
"private": true,
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"test:coverage": "react-scripts test --coverage --watchAll=false",
"eject": "react-scripts eject",
"lint": "eslint src/**/*.{js,jsx,ts,tsx}",
"lint:fix": "eslint src/**/*.{js,jsx,ts,tsx} --fix",
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,md}\"",
"type-check": "tsc --noEmit"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.20.0",
"@reduxjs/toolkit": "^2.0.1",
"react-redux": "^9.0.4",
"axios": "^1.6.2",
"@mui/material": "^5.15.0",
"@mui/icons-material": "^5.15.0",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"date-fns": "^3.0.0",
"react-hook-form": "^7.49.0",
"react-query": "^3.39.3"
},
"devDependencies": {
"@types/react": "^18.2.45",
"@types/react-dom": "^18.2.18",
"@typescript-eslint/eslint-plugin": "^6.15.0",
"@typescript-eslint/parser": "^6.15.0",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"prettier": "^3.1.1",
"typescript": "^5.3.3",
"react-scripts": "5.0.1",
"@testing-library/react": "^14.1.2",
"@testing-library/jest-dom": "^6.1.5",
"@testing-library/user-event": "^14.5.1"
}
}
TypeScript迁移
配置TypeScript:
1. 创建tsconfig.json,开启严格模式
2. 设置路径别名(@components, @utils)
3. Jest支持TypeScript
4. 渐进式迁移,别一次性全改
tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"jsx": "react-jsx",
"module": "ESNext",
"moduleResolution": "node",
"resolveJsonModule": true,
"allowJs": true,
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": "src",
"paths": {
"@components/*": ["components/*"],
"@containers/*": ["containers/*"],
"@utils/*": ["utils/*"],
"@api/*": ["api/*"],
"@hooks/*": ["hooks/*"],
"@types/*": ["types/*"],
"@constants/*": ["constants/*"],
"@services/*": ["services/*"],
"@store/*": ["store/*"],
"@assets/*": ["assets/*"]
}
},
"include": [
"src/**/*",
"src/**/*.tsx"
],
"exclude": [
"node_modules",
"build",
"dist"
]
}
重构工具配置 - .eslintrc.js:
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
project: './tsconfig.json',
},
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'prettier',
],
plugins: ['@typescript-eslint', 'react', 'react-hooks'],
rules: {
'react/react-in-jsx-scope': 'off', // React 18不需要
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'warn',
'react/prop-types': 'off', // 使用TypeScript代替
},
settings: {
react: {
version: 'detect',
},
},
};
.prettierrc:
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"arrowParens": "always",
"endOfLine": "lf"
}
第二阶段:架构重构
重新组织目录
重新设计项目结构:
1. 按功能模块组织
2. 业务逻辑和UI组件分开
3. 层次清晰
4. 方便团队协作
新目录结构:
src/
├── api/ # API接口层
│ ├── client.ts # Axios配置
│ ├── endpoints.ts # API端点定义
│ ├── products.ts # 产品相关API
│ ├── orders.ts # 订单相关API
│ └── customers.ts # 客户相关API
├── assets/ # 静态资源
│ ├── images/
│ ├── fonts/
│ └── icons/
├── components/ # 通用UI组件
│ ├── ui/ # 基础UI组件
│ │ ├── Button/
│ │ ├── Input/
│ │ ├── Modal/
│ │ ├── Table/
│ │ └── Form/
│ └── layout/ # 布局组件
│ ├── Header/
│ ├── Sidebar/
│ └── Footer/
├── features/ # 功能模块
│ ├── products/
│ │ ├── components/ # 产品相关组件
│ │ ├── hooks/ # 自定义hooks
│ │ ├── services/ # 业务逻辑
│ │ ├── types.ts # 类型定义
│ │ └── index.tsx # 入口
│ ├── orders/
│ ├── customers/
│ ├── dashboard/
│ └── auth/
├── hooks/ # 全局自定义hooks
│ ├── useAuth.ts
│ ├── usePagination.ts
│ └── useDebounce.ts
├── store/ # Redux store
│ ├── index.ts
│ ├── slices/
│ │ ├── authSlice.ts
│ │ ├── productSlice.ts
│ │ └── orderSlice.ts
│ └── middleware/
├── types/ # 全局类型定义
│ ├── api.ts
│ ├── models.ts
│ └── index.ts
├── utils/ # 工具函数
│ ├── formatters.ts
│ ├── validators.ts
│ └── constants.ts
├── App.tsx
├── main.tsx
└── vite-env.d.ts
API层重构
src/api/client.ts:
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import { store } from '@/store';
import { logout } from '@/store/slices/authSlice';
const apiClient: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
});
// 请求拦截器
apiClient.interceptors.request.use(
(config) => {
const token = store.getState().auth.token;
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`;
}
config.headers['X-Request-ID'] = generateRequestId();
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 响应拦截器
apiClient.interceptors.response.use(
(response: AxiosResponse) => {
return response.data;
},
(error) => {
const { response } = error;
// 处理认证错误
if (response?.status === 401) {
store.dispatch(logout());
window.location.href = '/login';
return Promise.reject(new Error('Session expired'));
}
// 处理其他错误
const errorMessage = response?.data?.message || error.message || 'An error occurred';
// 记录错误(可以集成错误监控服务)
console.error('API Error:', {
url: error.config?.url,
status: response?.status,
message: errorMessage,
});
return Promise.reject(new Error(errorMessage));
}
);
// 生成请求ID
function generateRequestId(): string {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
// 通用API方法
export const api = {
get: <T>(url: string, config?: AxiosRequestConfig): Promise<T> => {
return apiClient.get(url, config);
},
post: <T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> => {
return apiClient.post(url, data, config);
},
put: <T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> => {
return apiClient.put(url, data, config);
},
patch: <T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> => {
return apiClient.patch(url, data, config);
},
delete: <T>(url: string, config?: AxiosRequestConfig): Promise<T> => {
return apiClient.delete(url, config);
},
};
export default apiClient;
src/api/endpoints.ts:
// API端点集中管理
export const ENDPOINTS = {
// 认证
AUTH: {
LOGIN: '/auth/login',
LOGOUT: '/auth/logout',
REFRESH: '/auth/refresh',
REGISTER: '/auth/register',
},
// 产品
PRODUCTS: {
LIST: '/products',
DETAIL: (id: string) => `/products/${id}`,
CREATE: '/products',
UPDATE: (id: string) => `/products/${id}`,
DELETE: (id: string) => `/products/${id}`,
SEARCH: '/products/search',
CATEGORIES: '/products/categories',
},
// 订单
ORDERS: {
LIST: '/orders',
DETAIL: (id: string) => `/orders/${id}`,
CREATE: '/orders',
UPDATE: (id: string) => `/orders/${id}`,
DELETE: (id: string) => `/orders/${id}`,
STATS: '/orders/stats',
},
// 客户
CUSTOMERS: {
LIST: '/customers',
DETAIL: (id: string) => `/customers/${id}`,
CREATE: '/customers',
UPDATE: (id: string) => `/customers/${id}`,
DELETE: (id: string) => `/customers/${id}`,
},
} as const;
src/api/products.ts:
import { api } from './client';
import { ENDPOINTS } from './endpoints';
import type { Product, ProductFilters, PaginatedResponse, ProductFormData } from '@/types';
export const productsApi = {
// 获取产品列表
getProducts: async (params: ProductFilters): Promise<PaginatedResponse<Product>> => {
return api.get(ENDPOINTS.PRODUCTS.LIST, { params });
},
// 获取产品详情
getProduct: async (id: string): Promise<Product> => {
return api.get(ENDPOINTS.PRODUCTS.DETAIL(id));
},
// 创建产品
createProduct: async (data: ProductFormData): Promise<Product> => {
return api.post(ENDPOINTS.PRODUCTS.CREATE, data);
},
// 更新产品
updateProduct: async (id: string, data: Partial<ProductFormData>): Promise<Product> => {
return api.put(ENDPOINTS.PRODUCTS.UPDATE(id), data);
},
// 删除产品
deleteProduct: async (id: string): Promise<void> => {
return api.delete(ENDPOINTS.PRODUCTS.DELETE(id));
},
// 搜索产品
searchProducts: async (query: string): Promise<Product[]> => {
return api.get(ENDPOINTS.PRODUCTS.SEARCH, { params: { q: query } });
},
// 获取产品分类
getCategories: async (): Promise<string[]> => {
return api.get(ENDPOINTS.PRODUCTS.CATEGORIES);
},
};
Redux迁移到Redux Toolkit
重构Redux状态管理:
1. 迁移到Redux Toolkit
2. 使用RTK Query进行数据获取
3. 创建类型安全的slices
4. 实现持久化存储
src/store/index.ts:
import { configureStore } from '@reduxjs/toolkit';
import { setupListeners } from '@reduxjs/toolkit/query';
import { reducer as authReducer } from './slices/authSlice';
import { productsApi } from './services/productsApi';
import { ordersApi } from './services/ordersApi';
import storage from 'redux-persist/lib/storage';
import { persistStore, persistReducer } from 'redux-persist';
// Redux Persist配置
const persistConfig = {
key: 'root',
storage,
whitelist: ['auth'], // 只持久化auth reducer
};
// 配置store
export const store = configureStore({
reducer: {
auth: persistReducer(persistConfig, authReducer),
[productsApi.reducerPath]: productsApi.reducer,
[ordersApi.reducerPath]: ordersApi.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: ['persist/PERSIST', 'persist/REHYDRATE'],
},
}).concat(productsApi.middleware, ordersApi.middleware),
devTools: process.env.NODE_ENV !== 'production',
});
// 启用查询自动-refetch
setupListeners(store.dispatch);
export const persistor = persistStore(store);
// 类型导出
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
src/store/slices/authSlice.ts:
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import type { RootState } from '../index';
import { authApi } from '../services/authApi';
interface User {
id: string;
username: string;
email: string;
role: string;
}
interface AuthState {
user: User | null;
token: string | null;
isAuthenticated: boolean;
loading: boolean;
error: string | null;
}
const initialState: AuthState = {
user: null,
token: localStorage.getItem('token'),
isAuthenticated: false,
loading: false,
error: null,
};
// 异步thunks
export const login = createAsyncThunk(
'auth/login',
async (credentials: { email: string; password: string }) => {
const response = await authApi.login(credentials);
// 存储token到localStorage
localStorage.setItem('token', response.token);
return response;
}
);
export const logout = createAsyncThunk('auth/logout', async () => {
await authApi.logout();
localStorage.removeItem('token');
});
export const register = createAsyncThunk(
'auth/register',
async (userData: { username: string; email: string; password: string }) => {
const response = await authApi.register(userData);
localStorage.setItem('token', response.token);
return response;
}
);
const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
clearError: (state) => {
state.error = null;
},
setUser: (state, action: PayloadAction<User>) => {
state.user = action.payload;
state.isAuthenticated = true;
},
},
extraReducers: (builder) => {
// 登录
builder
.addCase(login.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(login.fulfilled, (state, action) => {
state.loading = false;
state.user = action.payload.user;
state.token = action.payload.token;
state.isAuthenticated = true;
})
.addCase(login.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message || 'Login failed';
});
// 登出
builder.addCase(logout.fulfilled, (state) => {
state.user = null;
state.token = null;
state.isAuthenticated = false;
});
// 注册
builder
.addCase(register.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(register.fulfilled, (state, action) => {
state.loading = false;
state.user = action.payload.user;
state.token = action.payload.token;
state.isAuthenticated = true;
})
.addCase(register.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message || 'Registration failed';
});
},
});
export const { clearError, setUser } = authSlice.actions;
// Selectors
export const selectAuth = (state: RootState) => state.auth;
export const selectUser = (state: RootState) => state.auth.user;
export const selectIsAuthenticated = (state: RootState) => state.auth.isAuthenticated;
export const selectAuthLoading = (state: RootState) => state.auth.loading;
export default authSlice.reducer;
src/store/services/productsApi.ts (使用RTK Query):
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import type { RootState } from '../index';
import type { Product, ProductFilters, PaginatedResponse, ProductFormData } from '@/types';
import { ENDPOINTS } from '@/api/endpoints';
export const productsApi = createApi({
reducerPath: 'productsApi',
baseQuery: fetchBaseQuery({
baseUrl: import.meta.env.VITE_API_BASE_URL || '/api',
prepareHeaders: (headers, { getState }) => {
// 从Redux store获取token
const token = (getState() as RootState).auth.token;
if (token) {
headers.set('authorization', `Bearer ${token}`);
}
return headers;
},
}),
tagTypes: ['Product', 'ProductsList'],
endpoints: (builder) => ({
// 获取产品列表
getProducts: builder.query<PaginatedResponse<Product>, ProductFilters>({
query: (params) => ({
url: ENDPOINTS.PRODUCTS.LIST,
params,
}),
providesTags: ['ProductsList'],
}),
// 获取产品详情
getProduct: builder.query<Product, string>({
query: (id) => ENDPOINTS.PRODUCTS.DETAIL(id),
providesTags: (result, error, id) => [{ type: 'Product', id }],
}),
// 创建产品
createProduct: builder.mutation<Product, ProductFormData>({
query: (data) => ({
url: ENDPOINTS.PRODUCTS.CREATE,
method: 'POST',
body: data,
}),
invalidatesTags: ['ProductsList'],
}),
// 更新产品
updateProduct: builder.mutation<Product, { id: string; data: Partial<ProductFormData> }>({
query: ({ id, data }) => ({
url: ENDPOINTS.PRODUCTS.UPDATE(id),
method: 'PUT',
body: data,
}),
invalidatesTags: (result, error, { id }) => [
{ type: 'Product', id },
'ProductsList',
],
}),
// 删除产品
deleteProduct: builder.mutation<void, string>({
query: (id) => ({
url: ENDPOINTS.PRODUCTS.DELETE(id),
method: 'DELETE',
}),
invalidatesTags: ['ProductsList'],
}),
// 搜索产品
searchProducts: builder.query<Product[], string>({
query: (query) => ({
url: ENDPOINTS.PRODUCTS.SEARCH,
params: { q: query },
}),
}),
// 获取分类
getCategories: builder.query<string[], void>({
query: () => ENDPOINTS.PRODUCTS.CATEGORIES,
}),
}),
});
// 导出hooks
export const {
useGetProductsQuery,
useGetProductQuery,
useCreateProductMutation,
useUpdateProductMutation,
useDeleteProductMutation,
useSearchProductsQuery,
useGetCategoriesQuery,
} = productsApi;
第三阶段:组件重构
创建通用UI组件库
创建可复用UI组件库:
1. Button - 支持多种样式和尺寸
2. Input - 带验证的输入框
3. Modal - 对话框组件
4. Table - 数据表格
5. Form - 表单组件
所有组件使用TypeScript,支持主题定制
src/components/ui/Button/Button.tsx:
import React from 'react';
import { StyledButton, ButtonProps } from './Button.styles';
export const Button: React.FC<ButtonProps> = ({
variant = 'primary',
size = 'medium',
children,
loading = false,
disabled = false,
startIcon,
endIcon,
fullWidth = false,
...props
}) => {
return (
<StyledButton
variant={variant}
size={size}
disabled={disabled || loading}
fullWidth={fullWidth}
startIcon={startIcon}
endIcon={endIcon}
{...props}
>
{loading && <span className="spinner" />}
{children}
</StyledButton>
);
};
export default Button;
src/components/ui/Button/Button.styles.ts:
import styled, { css } from 'styled-components';
export interface ButtonProps {
variant?: 'primary' | 'secondary' | 'success' | 'danger' | 'ghost';
size?: 'small' | 'medium' | 'large';
loading?: boolean;
disabled?: boolean;
fullWidth?: boolean;
startIcon?: React.ReactNode;
endIcon?: React.ReactNode;
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
children: React.ReactNode;
}
const variantStyles = {
primary: css`
background-color: #3b82f6;
color: white;
&:hover:not(:disabled) {
background-color: #2563eb;
}
`,
secondary: css`
background-color: #6b7280;
color: white;
&:hover:not(:disabled) {
background-color: #4b5563;
}
`,
success: css`
background-color: #10b981;
color: white;
&:hover:not(:disabled) {
background-color: #059669;
}
`,
danger: css`
background-color: #ef4444;
color: white;
&:hover:not(:disabled) {
background-color: #dc2626;
}
`,
ghost: css`
background-color: transparent;
color: #3b82f6;
border: 1px solid #3b82f6;
&:hover:not(:disabled) {
background-color: rgba(59, 130, 246, 0.1);
}
`,
};
const sizeStyles = {
small: css`
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
gap: 0.375rem;
`,
medium: css`
padding: 0.625rem 1.25rem;
font-size: 1rem;
gap: 0.5rem;
`,
large: css`
padding: 0.875rem 1.75rem;
font-size: 1.125rem;
gap: 0.625rem;
`,
};
export const StyledButton = styled.button<ButtonProps>`
display: inline-flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 0.5rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
overflow: hidden;
${({ fullWidth }) =>
fullWidth &&
css`
`}
${({ variant }) => variantStyles[variant || 'primary']}
${({ size }) => sizeStyles[size || 'medium']}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.spinner {
display: inline-block;
width: 1em;
height: 1em;
border: 2px solid currentColor;
border-right-color: transparent;
border-radius: 50%;
animation: spin 0.6s linear infinite;
margin-right: 0.5em;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
`;
src/components/ui/Input/Input.tsx:
import React, { forwardRef } from 'react';
import { StyledInput, InputWrapper, Label, ErrorText, HelperText, InputProps } from './Input.styles';
export const Input = forwardRef<HTMLInputElement, InputProps>(
(
{
label,
error,
helperText,
leftIcon,
rightIcon,
fullWidth = false,
...props
},
ref
) => {
return (
<InputWrapper fullWidth={fullWidth}>
{label && <Label>{label}</Label>}
<StyledInput
ref={ref}
hasError={!!error}
hasLeftIcon={!!leftIcon}
hasRightIcon={!!rightIcon}
fullWidth={fullWidth}
{...props}
/>
{error && <ErrorText>{error}</ErrorText>}
{helperText && !error && <HelperText>{helperText}</HelperText>}
</InputWrapper>
);
}
);
Input.displayName = 'Input';
export default Input;
重构业务组件
src/features/products/components/ProductList/ProductList.tsx:
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
TablePagination,
Checkbox,
IconButton,
Typography,
Box,
Chip,
Menu,
MenuItem,
} from '@mui/material';
import {
MoreVert as MoreVertIcon,
Edit as EditIcon,
Delete as DeleteIcon,
Visibility as ViewIcon,
} from '@mui/icons-material';
import { useGetProductsQuery } from '@/store/services/productsApi';
import { useAppDispatch } from '@/store/hooks';
import { deleteProduct } from '@/store/slices/productSlice';
import { formatPrice, formatDate } from '@/utils/formatters';
import type { Product, ProductFilters } from '@/types';
import { Button } from '@/components/ui/Button';
import { Loader } from '@/components/ui/Loader';
import { EmptyState } from '@/components/ui/EmptyState';
export const ProductList: React.FC = () => {
const navigate = useNavigate();
const dispatch = useAppDispatch();
// 状态管理
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10);
const [selected, setSelected] = useState<readonly string[]>([]);
const [filters, setFilters] = useState<ProductFilters>({
page: page + 1,
limit: rowsPerPage,
});
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
// RTK Query hook
const { data, isLoading, isError, refetch } = useGetProductsQuery(filters, {
refetchOnMountOrArgChange: true,
});
const products = data?.data || [];
const total = data?.total || 0;
// 处理分页变化
const handleChangePage = (event: unknown, newPage: number) => {
setPage(newPage);
setFilters((prev) => ({ ...prev, page: newPage + 1 }));
};
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
const newRowsPerPage = parseInt(event.target.value, 10);
setRowsPerPage(newRowsPerPage);
setPage(0);
setFilters((prev) => ({ ...prev, page: 1, limit: newRowsPerPage }));
};
// 处理选择
const handleSelectAllClick = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.checked) {
const newSelected = products.map((product) => product.id);
setSelected(newSelected);
return;
}
setSelected([]);
};
const handleClick = (event: React.MouseEvent<unknown>, id: string) => {
const selectedIndex = selected.indexOf(id);
let newSelected: readonly string[] = [];
if (selectedIndex === -1) {
newSelected = newSelected.concat(selected, id);
} else if (selectedIndex === 0) {
newSelected = newSelected.concat(selected.slice(1));
} else if (selectedIndex === selected.length - 1) {
newSelected = newSelected.concat(selected.slice(0, -1));
} else if (selectedIndex > 0) {
newSelected = newSelected.concat(
selected.slice(0, selectedIndex),
selected.slice(selectedIndex + 1)
);
}
setSelected(newSelected);
};
const isSelected = (id: string) => selected.indexOf(id) !== -1;
// 菜单操作
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, product: Product) => {
setAnchorEl(event.currentTarget);
setSelectedProduct(product);
};
const handleMenuClose = () => {
setAnchorEl(null);
setSelectedProduct(null);
};
const handleView = () => {
navigate(`/products/${selectedProduct?.id}`);
handleMenuClose();
};
const handleEdit = () => {
navigate(`/products/${selectedProduct?.id}/edit`);
handleMenuClose();
};
const handleDelete = async () => {
if (selectedProduct && window.confirm(`确定要删除产品 "${selectedProduct.name}" 吗?`)) {
try {
await dispatch(deleteProduct(selectedProduct.id)).unwrap();
refetch();
} catch (error) {
console.error('删除失败:', error);
}
}
handleMenuClose();
};
const handleBulkDelete = async () => {
if (window.confirm(`确定要删除选中的 ${selected.length} 个产品吗?`)) {
// 批量删除逻辑
console.log('批量删除:', selected);
}
};
// 加载状态
if (isLoading) {
return <Loader />;
}
// 错误状态
if (isError) {
return (
<EmptyState
title="加载失败"
description="无法加载产品列表,请稍后重试"
action={
<Button variant="primary" onClick={() => refetch()}>
重新加载
</Button>
}
/>
);
}
// 空状态
if (products.length === 0) {
return (
<EmptyState
title="暂无产品"
description="开始添加您的第一个产品"
action={
<Button variant="primary" onClick={() => navigate('/products/new')}>
添加产品
</Button>
}
/>
);
}
return (
<Box>
{/* 工具栏 */}
<Box sx={{ mb: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="h6">产品列表</Typography>
{selected.length > 0 && (
<Box sx={{ display: 'flex', gap: 1 }}>
<Button variant="danger" size="small" onClick={handleBulkDelete}>
删除选中 ({selected.length})
</Button>
</Box>
)}
</Box>
{/* 表格 */}
<Paper>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell padding="checkbox">
<Checkbox
indeterminate={selected.length > 0 && selected.length < products.length}
checked={products.length > 0 && selected.length === products.length}
onChange={handleSelectAllClick}
/>
</TableCell>
<TableCell>产品名称</TableCell>
<TableCell>分类</TableCell>
<TableCell align="right">价格</TableCell>
<TableCell align="center">库存</TableCell>
<TableCell>状态</TableCell>
<TableCell>创建时间</TableCell>
<TableCell align="right">操作</TableCell>
</TableRow>
</TableHead>
<TableBody>
{products.map((product) => {
const isItemSelected = isSelected(product.id);
return (
<TableRow
key={product.id}
hover
onClick={(event) => handleClick(event, product.id)}
role="checkbox"
aria-checked={isItemSelected}
selected={isItemSelected}
>
<TableCell padding="checkbox">
<Checkbox checked={isItemSelected} />
</TableCell>
<TableCell>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{product.image && (
<Box
component="img"
src={product.image}
alt={product.name}
sx={{ width: 40, height: 40, borderRadius: 1, objectFit: 'cover' }}
/>
)}
<Typography variant="body2" fontWeight={500}>
{product.name}
</Typography>
</Box>
</TableCell>
<TableCell>
<Chip label={product.category} size="small" variant="outlined" />
</TableCell>
<TableCell align="right">
<Typography variant="body2" fontWeight={600}>
{formatPrice(product.price)}
</Typography>
</TableCell>
<TableCell align="center">
<Chip
label={product.stock}
size="small"
color={product.stock < 10 ? 'error' : 'default'}
/>
</TableCell>
<TableCell>
<Chip
label={product.status === 'active' ? '上架' : '下架'}
size="small"
color={product.status === 'active' ? 'success' : 'default'}
/>
</TableCell>
<TableCell>
<Typography variant="body2" color="text.secondary">
{formatDate(product.createdAt)}
</Typography>
</TableCell>
<TableCell align="right" onClick={(e) => e.stopPropagation()}>
<IconButton
onClick={(e) => handleMenuOpen(e, product)}
size="small"
>
<MoreVertIcon />
</IconButton>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={[5, 10, 25, 50]}
component="div"
count={total}
rowsPerPage={rowsPerPage}
page={page}
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
labelRowsPerPage="每页行数:"
labelDisplayedRows={({ from, to, count }) =>
`${from}-${to} 共 ${count !== -1 ? count : `超过 ${to}`} 条`
}
/>
</Paper>
{/* 操作菜单 */}
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleMenuClose}
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
>
<MenuItem onClick={handleView}>
<ViewIcon fontSize="small" sx={{ mr: 1 }} />
查看
</MenuItem>
<MenuItem onClick={handleEdit}>
<EditIcon fontSize="small" sx={{ mr: 1 }} />
编辑
</MenuItem>
<MenuItem onClick={handleDelete} sx={{ color: 'error.main' }}>
<DeleteIcon fontSize="small" sx={{ mr: 1 }} />
删除
</MenuItem>
</Menu>
</Box>
);
};
export default ProductList;
第四阶段:性能优化
代码分割和懒加载
实现代码分割和路由懒加载:
1. 使用React.lazy进行组件懒加载
2. 配置Suspense fallback
3. 分析bundle大小
4. 优化首屏加载时间
src/App.tsx:
import React, { Suspense, lazy } from 'react';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import { ThemeProvider } from '@mui/material/styles';
import { CssBaseline } from '@mui/material';
import { store, persistor } from '@/store';
import { theme } from '@/theme';
import { Layout } from '@/components/layout/Layout';
import { Loader } from '@/components/ui/Loader';
import { AuthGuard } from '@/components/auth/AuthGuard';
import { GuestGuard } from '@/components/auth/GuestGuard';
// 懒加载页面组件
const LoginPage = lazy(() => import('@/features/auth/LoginPage'));
const RegisterPage = lazy(() => import('@/features/auth/RegisterPage'));
const Dashboard = lazy(() => import('@/features/dashboard/Dashboard'));
const ProductList = lazy(() => import('@/features/products/ProductList'));
const ProductDetail = lazy(() => import('@/features/products/ProductDetail'));
const ProductForm = lazy(() => import('@/features/products/ProductForm'));
const OrderList = lazy(() => import('@/features/orders/OrderList'));
const OrderDetail = lazy(() => import('@/features/orders/OrderDetail'));
const CustomerList = lazy(() => import('@/features/customers/CustomerList'));
const Settings = lazy(() => import('@/features/settings/Settings'));
const NotFound = lazy(() => import('@/components/error/NotFound'));
// 加载组件
const PageLoader: React.FC = () => (
<Box
sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
minHeight: '100vh',
}}
>
<Loader size="large" />
</Box>
);
function App() {
return (
<Provider store={store}>
<PersistGate loading={<PageLoader />} persistor={persistor}>
<ThemeProvider theme={theme}>
<CssBaseline />
<BrowserRouter>
<Suspense fallback={<PageLoader />}>
<Routes>
{/* 公开路由 */}
<Route
path="/login"
element={
<GuestGuard>
<LoginPage />
</GuestGuard>
}
/>
<Route
path="/register"
element={
<GuestGuard>
<RegisterPage />
</GuestGuard>
}
/>
{/* 受保护的路由 */}
<Route
path="/"
element={
<AuthGuard>
<Layout />
</AuthGuard>
}
>
<Route index element={<Navigate to="/dashboard" replace />} />
<Route path="dashboard" element={<Dashboard />} />
{/* 产品路由 */}
<Route path="products">
<Route index element={<ProductList />} />
<Route path="new" element={<ProductForm />} />
<Route path=":id" element={<ProductDetail />} />
<Route path=":id/edit" element={<ProductForm />} />
</Route>
{/* 订单路由 */}
<Route path="orders">
<Route index element={<OrderList />} />
<Route path=":id" element={<OrderDetail />} />
</Route>
{/* 客户路由 */}
<Route path="customers">
<Route index element={<CustomerList />} />
</Route>
{/* 设置 */}
<Route path="settings" element={<Settings />} />
</Route>
{/* 404 */}
<Route path="*" element={<NotFound />} />
</Routes>
</Suspense>
</BrowserRouter>
</ThemeProvider>
</PersistGate>
</Provider>
);
}
export default App;
性能监控和优化
src/utils/performance.ts:
// 性能监控工具
export class PerformanceMonitor {
private marks: Map<string, number> = new Map();
// 开始标记
start(label: string): void {
this.marks.set(label, performance.now());
}
// 结束标记并返回耗时
end(label: string): number {
const startTime = this.marks.get(label);
if (!startTime) {
console.warn(`No start mark found for: ${label}`);
return 0;
}
const endTime = performance.now();
const duration = endTime - startTime;
console.log(`⏱️ ${label}: ${duration.toFixed(2)}ms`);
this.marks.delete(label);
return duration;
}
// 测量异步操作
async measure<T>(label: string, fn: () => Promise<T>): Promise<T> {
this.start(label);
try {
const result = await fn();
this.end(label);
return result;
} catch (error) {
this.end(label);
throw error;
}
}
// 测量同步操 作
measureSync<T>(label: string, fn: () => T): T {
this.start(label);
try {
const result = fn();
this.end(label);
return result;
} catch (error) {
this.end(label);
throw error;
}
}
// 获取Web Vitals
getWebVitals() {
if (!('PerformanceObserver' in window)) {
return null;
}
return new Promise((resolve) => {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const vitals: any = {};
entries.forEach((entry: any) => {
if (entry.entryType === 'navigation') {
vitals.domContentLoaded = entry.domContentLoadedEventEnd - entry.domContentLoadedEventStart;
vitals.loadComplete = entry.loadEventEnd - entry.loadEventStart;
vitals.domReady = entry.domComplete - entry.domInteractive;
}
});
resolve(vitals);
observer.disconnect();
});
observer.observe({ entryTypes: ['navigation'] });
});
}
}
// 全局性能监控实例
export const perfMonitor = new PerformanceMonitor();
// React性能优化hook
import { useEffect, useRef } from 'react';
export function useRenderCount(componentName: string) {
const renderCount = useRef(0);
useEffect(() => {
renderCount.current += 1;
console.log(`🔄 ${componentName} rendered ${renderCount.current} times`);
});
}
export function usePerformanceLog(componentName: string) {
const renderStartTime = useRef<number>();
useEffect(() => {
renderStartTime.current = performance.now();
return () => {
if (renderStartTime.current) {
const renderTime = performance.now() - renderStartTime.current;
if (renderTime > 16) { // 超过一帧(16ms)
console.warn(`⚠️ ${componentName} slow render: ${renderTime.toFixed(2)}ms`);
}
}
};
});
}
Bundle分析和优化
vite.config.ts:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
import { visualizer } from 'rollup-plugin-visualizer';
import compression from 'vite-plugin-compression';
export default defineConfig({
plugins: [
react(),
// Gzip压缩
compression({
algorithm: 'gzip',
ext: '.gz',
threshold: 10240, // 只压缩大于10KB的 文件
}),
// Brotli压缩
compression({
algorithm: 'brotliCompress',
ext: '.br',
threshold: 10240,
}),
// Bundle分析
visualizer({
open: false,
gzipSize: true,
brotliSize: true,
filename: 'dist/stats.html',
}),
],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@components': path.resolve(__dirname, './src/components'),
'@features': path.resolve(__dirname, './src/features'),
'@utils': path.resolve(__dirname, './src/utils'),
'@hooks': path.resolve(__dirname, './src/hooks'),
'@store': path.resolve(__dirname, './src/store'),
'@types': path.resolve(__dirname, './src/types'),
'@api': path.resolve(__dirname, './src/api'),
},
},
build: {
// 代码分割
rollupOptions: {
output: {
manualChunks: {
// React核心库
'react-vendor': ['react', 'react-dom', 'react-router-dom'],
// UI库
'mui-vendor': ['@mui/material', '@mui/icons-material', '@emotion/react', '@emotion/styled'],
// Redux
'redux-vendor': ['@reduxjs/toolkit', 'react-redux', 'redux-persist'],
// 其他第三方库
'vendor': ['axios', 'date-fns'],
},
},
},
// 压缩配置
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
},
},
// Chunk大小警告阈值
chunkSizeWarningLimit: 1000,
// Source map
sourcemap: false,
},
server: {
port: 3000,
open: true,
},
});
图片优化
src/components/OptimizedImage/OptimizedImage.tsx:
import React, { useState, useRef, useEffect } from 'react';
import styled from 'styled-components';
interface OptimizedImageProps {
src: string;
alt: string;
width?: number;
height?: number;
loading?: 'lazy' | 'eager';
className?: string;
}
const ImageWrapper = styled.div<{ width?: number; height?: number }>`
position: relative;
overflow: hidden;
width: ${(props) => props.width || 'auto'}px;
height: ${(props) => props.height || 'auto'}px;
background-color: #f3f4f6;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(90deg, #f3f4f6 25%, #e5e7eb 50%, #f3f4f6 75%);
background-size: 200% 100%;
animation: loading 1.5s infinite;
z-index: 1;
}
@keyframes loading {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
`;
const StyledImage = styled.img<{ loaded: boolean }>`
width: 100%;
height: 100%;
object-fit: cover;
opacity: ${(props) => (props.loaded ? 1 : 0)};
transition: opacity 0.3s ease;
position: relative;
z-index: 2;
`;
export const OptimizedImage: React.FC<OptimizedImageProps> = ({
src,
alt,
width,
height,
loading = 'lazy',
className,
}) => {
const [isLoaded, setIsLoaded] = useState(false);
const [isInView, setIsInView] = useState(false);
const imgRef = useRef<HTMLImageElement>(null);
// Intersection Observer for lazy loading
useEffect(() => {
if (loading === 'eager') {
setIsInView(true);
return;
}
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsInView(true);
observer.disconnect();
}
},
{ rootMargin: '50px' }
);
if (imgRef.current) {
observer.observe(imgRef.current);
}
return () => observer.disconnect();
}, [loading]);
const handleLoad = () => {
setIsLoaded(true);
};
return (
<ImageWrapper width={width} height={height} className={className}>
{isInView && (
<StyledImage
ref={imgRef}
src={src}
alt={alt}
loading={loading}
loaded={isLoaded}
onLoad={handleLoad}
/>
)}
</ImageWrapper>
);
};
第五阶段:测试和质量保障
单元测试
为关键组件和工具函数添加单元测试:
1. 使用Jest和React Testing Library
2. 测试覆盖率 > 80%
3. 包含边界情况和错误处理
4. 集成到CI/CD流程
src/components/ui/Button/tests/Button.test.tsx:
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import { Button } from '../Button';
describe('Button Component', () => {
it('renders correctly with default props', () => {
render(<Button>Click me</Button>);
const button = screen.getByRole('button', { name: /click me/i });
expect(button).toBeInTheDocument();
expect(button).toHaveClass('primary');
});
it('handles click events', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click me</Button>);
const button = screen.getByRole('button');
fireEvent.click(button);
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('renders different variants', () => {
const { rerender } = render(<Button variant="primary">Primary</Button>);
expect(screen.getByRole('button')).toHaveClass('primary');
rerender(<Button variant="secondary">Secondary</Button>);
expect(screen.getByRole('button')).toHaveClass('secondary');
rerender(<Button variant="danger">Danger</Button>);
expect(screen.getByRole('button')).toHaveClass('danger');
});
it('renders different sizes', () => {
const { rerender } = render(<Button size="small">Small</Button>);
expect(screen.getByRole('button')).toHaveClass('small');
rerender(<Button size="medium">Medium</Button>);
expect(screen.getByRole('button')).toHaveClass('medium');
rerender(<Button size="large">Large</Button>);
expect(screen.getByRole('button')).toHaveClass('large');
});
it('is disabled when loading', () => {
render(<Button loading>Loading</Button>);
const button = screen.getByRole('button');
expect(button).toBeDisabled();
expect(screen.getByText(/loading/i)).toBeInTheDocument();
});
it('is disabled when disabled prop is true', () => {
render(<Button disabled>Disabled</Button>);
const button = screen.getByRole('button');
expect(button).toBeDisabled();
});
it('does not call onClick when disabled', () => {
const handleClick = jest.fn();
render(
<Button onClick={handleClick} disabled>
Click me
</Button>
);
const button = screen.getByRole('button');
fireEvent.click(button);
expect(handleClick).not.toHaveBeenCalled();
});
it('renders with icons', () => {
const StartIcon = () => <span data-testid="start-icon">Start</span>;
const EndIcon = () => <span data-testid="end-icon">End</span>;
render(
<Button startIcon={<StartIcon />} endIcon={<EndIcon />}>
With Icons
</Button>
);
expect(screen.getByTestId('start-icon')).toBeInTheDocument();
expect(screen.getByTestId('end-icon')).toBeInTheDocument();
});
it('renders as full width when fullWidth is true', () => {
render(<Button fullWidth>Full Width</Button>);
const button = screen.getByRole('button');
expect(button).toHaveStyle({ width: '100%' });
});
});
src/utils/tests/formatters.test.ts:
import { formatPrice, formatDate, formatPhoneNumber, formatCurrency } from '../formatters';
describe('Format Utilities', () => {
describe('formatPrice', () => {
it('formats positive numbers correctly', () => {
expect(formatPrice(100)).toBe('¥100.00');
expect(formatPrice(1000)).toBe('¥1,000.00');
expect(formatPrice(1000000)).toBe('¥1,000,000.00');
});
it('handles decimal values', () => {
expect(formatPrice(99.9)).toBe('¥99.90');
expect(formatPrice(99.999)).toBe('¥100.00');
});
it('handles zero', () => {
expect(formatPrice(0)).toBe('¥0.00');
});
it('handles negative numbers', () => {
expect(formatPrice(-100)).toBe('-¥100.00');
});
});
describe('formatDate', () => {
it('formats date strings correctly', () => {
const date = '2024-01-15T10:30:00Z';
const formatted = formatDate(date);
expect(formatted).toMatch(/2024/);
expect(formatted).toMatch(/01/);
expect(formatted).toMatch(/15/);
});
it('handles invalid dates', () => {
expect(formatDate('invalid-date')).toBe('Invalid Date');
});
it('uses custom format', () => {
const date = '2024-01-15T10:30:00Z';
const formatted = formatDate(date, 'yyyy-MM-dd');
expect(formatted).toBe('2024-01-15');
});
});
describe('formatPhoneNumber', () => {
it('formats Chinese phone numbers', () => {
expect(formatPhoneNumber('13800138000')).toBe('138-0013-8000');
expect(formatPhoneNumber('18912345678')).toBe('189-1234-5678');
});
it('handles invalid phone numbers', () => {
expect(formatPhoneNumber('123')).toBe('123');
expect(formatPhoneNumber('')).toBe('');
});
});
describe('formatCurrency', () => {
it('formats currency for different locales', () => {
expect(formatCurrency(1000, 'USD')).toBe('$1,000.00');
expect(formatCurrency(1000, 'EUR')).toBe('€1,000.00');
expect(formatCurrency(1000, 'CNY')).toBe('¥1,000.00');
});
it('handles different currencies', () => {
expect(formatCurrency(100, 'JPY')).toBe('¥100');
});
});
});