跳至主要内容

大規模程式碼重構

上週我重構了一個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硬編碼

重構策略

第一週: 依賴和基礎設施 第二週: 架構和元件 第三週: 效能和測試

開始第一週?


## 第一阶段:依赖升级

### 制定计划

建立詳細的重構計劃,包括:

  1. 每個階段的具體任務
  2. 怎麼回滾
  3. 測試策略
  4. 風險評估

**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;
__CODE_BLOCK_15__typescript
// API端点集中管理
export const ENDPOINTS = {
// 认证
AUTH: {
LOGIN: '/auth/login',
LOGOUT: '/auth/logout',
REFRESH: '/auth/refresh',
REGISTER: '/auth/register',
},

// 产品
PRODUCTS: {
LIST: '/products',
DETAIL: (id: string) => __INLINE_CODE_55__,
CREATE: '/products',
UPDATE: (id: string) => __INLINE_CODE_56__,
DELETE: (id: string) => __INLINE_CODE_57__,
SEARCH: '/products/search',
CATEGORIES: '/products/categories',
},

// 订单
ORDERS: {
LIST: '/orders',
DETAIL: (id: string) => __INLINE_CODE_58__,
CREATE: '/orders',
UPDATE: (id: string) => __INLINE_CODE_59__,
DELETE: (id: string) => __INLINE_CODE_60__,
STATS: '/orders/stats',
},

// 客户
CUSTOMERS: {
LIST: '/customers',
DETAIL: (id: string) => __INLINE_CODE_61__,
CREATE: '/customers',
UPDATE: (id: string) => __INLINE_CODE_62__,
DELETE: (id: string) => __INLINE_CODE_63__,
},
} as const;
__CODE_BLOCK_16__typescript
__IMPORT_91__
__IMPORT_92__
__IMPORT_93__

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);
},
};
__CODE_BLOCK_17__
重构Redux状态管理:
1. 迁移到Redux Toolkit
2. 使用RTK Query进行数据获取
3. 创建类型安全的slices
4. 实现持久化存储
__CODE_BLOCK_18__typescript
__IMPORT_94__
__IMPORT_95__
__IMPORT_96__
__IMPORT_97__
__IMPORT_98__
__IMPORT_99__
__IMPORT_100__

// 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;
__CODE_BLOCK_19__typescript
__IMPORT_101__
__IMPORT_102__
__IMPORT_103__

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;
__CODE_BLOCK_20__typescript
__IMPORT_104__
__IMPORT_105__
__IMPORT_106__
__IMPORT_107__

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', __INLINE_CODE_64__);
}
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;
__CODE_BLOCK_21__
创建可复用UI组件库:
1. Button - 支持多种样式和尺寸
2. Input - 带验证的输入框
3. Modal - 对话框组件
4. Table - 数据表格
5. Form - 表单组件

所有组件使用TypeScript,支持主题定制
__CODE_BLOCK_22__typescript
__IMPORT_108__
__IMPORT_109__

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__INLINE_CODE_65__,

secondary: css__INLINE_CODE_66__,

success: css__INLINE_CODE_67__,

danger: css__INLINE_CODE_68__,

ghost: css__INLINE_CODE_69__,
};

const sizeStyles = {
small: css__INLINE_CODE_70__,

medium: css__INLINE_CODE_71__,

large: css__INLINE_CODE_72__,
};

export const StyledButton = styled.button<ButtonProps>__INLINE_CODE_73__
width: 100%;
__INLINE_CODE_74__;
__CODE_BLOCK_24__typescript
__IMPORT_111__
__IMPORT_112__

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;
__CODE_BLOCK_25__typescript
__IMPORT_113__
__IMPORT_114__
__IMPORT_115__
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
TablePagination,
Checkbox,
IconButton,
Typography,
Box,
Chip,
Menu,
MenuItem,
} from '@mui/material';
__IMPORT_116__
MoreVert as MoreVertIcon,
Edit as EditIcon,
Delete as DeleteIcon,
Visibility as ViewIcon,
} from '@mui/icons-material';
__IMPORT_117__
__IMPORT_118__
__IMPORT_119__
__IMPORT_120__
__IMPORT_121__
__IMPORT_122__
__IMPORT_123__
__IMPORT_124__

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(__INLINE_CODE_75__);
handleMenuClose();
};

const handleEdit = () => {
navigate(__INLINE_CODE_76__);
handleMenuClose();
};

const handleDelete = async () => {
if (selectedProduct && window.confirm(__INLINE_CODE_77__)) {
try {
await dispatch(deleteProduct(selectedProduct.id)).unwrap();
refetch();
} catch (error) {
console.error('删除失败:', error);
}
}
handleMenuClose();
};

const handleBulkDelete = async () => {
if (window.confirm(__INLINE_CODE_78__)) {
// 批量删除逻辑
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 }) =>
__INLINE_CODE_79__超过 ${to}__INLINE_CODE_80__
}
/>
</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_137__

export function useRenderCount(componentName: string) {
const renderCount = useRef(0);

useEffect(() => {
renderCount.current += 1;
console.log(__INLINE_CODE_83__);
});
}

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(__INLINE_CODE_84__);
}
}
};
});
}
__CODE_BLOCK_29__typescript
__IMPORT_138__
__IMPORT_139__
__IMPORT_140__
__IMPORT_141__
__IMPORT_142__

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

interface OptimizedImageProps {
src: string;
alt: string;
width?: number;
height?: number;
loading?: 'lazy' | 'eager';
className?: string;
}

const ImageWrapper = styled.div<{ width?: number; height?: number }>__INLINE_CODE_85__;

const StyledImage = styled.img<{ loaded: boolean }>__INLINE_CODE_86__;

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%' });
});
});
__CODE_BLOCK_33__typescript
__IMPORT_149__

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');
});
});
});
__CODE_BLOCK_34__typescript
__IMPORT_150__
__IMPORT_151__
__IMPORT_152__
__IMPORT_153__
__IMPORT_154__
__IMPORT_155__
__IMPORT_156__
__IMPORT_157__
__IMPORT_158__

// Mock Service Worker setup
const server = setupServer(
rest.get('/api/products', (req, res, ctx) => {
return res(
ctx.json({
data: [
{
id: '1',
name: 'Product 1',
price: 100,
category: 'Electronics',
stock: 50,
status: 'active',
createdAt: '2024-01-15T10:00:00Z',
},
{
id: '2',
name: 'Product 2',
price: 200,
category: 'Books',
stock: 5,
status: 'active',
createdAt: '2024-01-14T10:00:00Z',
},
],
total: 2,
page: 1,
limit: 10,
})
);
}),

rest.delete('/api/products/:id', (req, res, ctx) => {
return res(ctx.status(204));
})
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

const renderWithProviders = (component: React.ReactElement) => {
return render(
<Provider store={store}>
<BrowserRouter>{component}</BrowserRouter>
</Provider>
);
};

describe('ProductList Integration Tests', () => {
it('fetches and displays products', async () => {
renderWithProviders(<ProductList />);

// 等待產品載入
await waitFor(() => {
expect(screen.getByText('Product 1')).toBeInTheDocument();
expect(screen.getByText('Product 2')).toBeInTheDocument();
});
});

it('displays product details correctly', async () => {
renderWithProviders(<ProductList />);

await waitFor(() => {
const product1 = screen.getByText('Product 1').closest('tr');
expect(product1).toBeInTheDocument();

// 檢查價格
expect(within(product1!).getByText('¥100.00')).toBeInTheDocument();

// 檢查分類
expect(within(product1!).getByText('Electronics')).toBeInTheDocument();

// 檢查庫存
expect(within(product1!).getByText('50')).toBeInTheDocument();
});
});

it('handles product deletion', async () => {
const confirmSpy = jest.spyOn(window, 'confirm').mockReturnValue(true);

renderWithProviders(<ProductList />);

await waitFor(() => {
expect(screen.getByText('Product 1')).toBeInTheDocument();
});

// 點選刪除按鈕
const deleteButton = screen.getAllByLabelText(/delete/i)[0];
fireEvent.click(deleteButton);

await waitFor(() => {
expect(screen.queryByText('Product 1')).not.toBeInTheDocument();
});

confirmSpy.mockRestore();
});

it('displays empty state when no products', async () => {
server.use(
rest.get('/api/products', (req, res, ctx) => {
return res(
ctx.json({
data: [],
total: 0,
})
);
})
);

renderWithProviders(<ProductList />);

await waitFor(() => {
expect(screen.getByText(/暫無產品/i)).toBeInTheDocument();
});
});

it('handles API errors gracefully', async () => {
server.use(
rest.get('/api/products', (req, res, ctx) => {
return res(ctx.status(500));
})
);

renderWithProviders(<ProductList />);

await waitFor(() => {
expect(screen.getByText(/載入失敗/i)).toBeInTheDocument();
});
});
});

第六阶段:部署和监控

Docker配置

Dockerfile:

# 多階段構建

# 構建階段
FROM node:18-alpine AS builder

WORKDIR /app

# 複製package檔案
COPY package*.json ./
COPY yarn.lock ./

# 安裝依賴
RUN yarn install --frozen-lockfile

# 複製原始碼
COPY . .

# 構建應用
RUN yarn build

# 生產階段
FROM nginx:alpine

# 複製構建產物到nginx
COPY --from=builder /app/dist /usr/share/nginx/html

# 複製nginx配置
COPY nginx.conf /etc/nginx/conf.d/default.conf

# 暴露埠
EXPOSE 80

# 啟動nginx
CMD ["nginx", "-g", "daemon off;"]

nginx.conf:

server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;

# Gzip壓縮
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/javascript application/json;

# SPA路由支援
location / {
try_files $uri $uri/ /index.html;
}

# API代理
location /api {
proxy_pass http://backend:5000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}

# 靜態資源快取
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}

CI/CD配置

.github/workflows/deploy.yml:

name: Deploy to Production

on:
push:
branches:
- main

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'yarn'

- name: Install dependencies
run: yarn install --frozen-lockfile

- name: Run linter
run: yarn lint

- name: Run type check
run: yarn type-check

- name: Run tests
run: yarn test:coverage

- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info

build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2

- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}

- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: yourusername/ecommerce-admin:latest
cache-from: type=registry,ref=yourusername/ecommerce-admin:buildcache
cache-to: type=registry,ref=yourusername/ecommerce-admin:buildcache,mode=max

deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Deploy to production
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.PRODUCTION_HOST }}
username: ${{ secrets.PRODUCTION_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /app/ecommerce-admin
docker-compose pull
docker-compose up -d
docker image prune -f

性能监控

src/utils/monitoring.ts:

// 錯誤監控和效能追蹤

export class MonitoringService {
private static instance: MonitoringService;
private apiKey: string;

private constructor() {
this.apiKey = import.meta.env.VITE_MONITORING_API_KEY || '';
}

static getInstance(): MonitoringService {
if (!MonitoringService.instance) {
MonitoringService.instance = new MonitoringService();
}
return MonitoringService.instance;
}

// 記錄錯誤
logError(error: Error, context?: Record<string, any>): void {
const errorData = {
message: error.message,
stack: error.stack,
context,
timestamp: new Date().toISOString(),
url: window.location.href,
userAgent: navigator.userAgent,
};

console.error('Error logged:', errorData);

// 傳送到監控服務
if (this.apiKey) {
fetch('/api/monitoring/error', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Monitoring-Key': this.apiKey,
},
body: JSON.stringify(errorData),
}).catch((err) => console.error('Failed to send error:', err));
}
}

// 記錄效能指標
logPerformance(metricName: string, value: number, unit: 'ms' | 'bytes' = 'ms'): void {
const metricData = {
name: metricName,
value,
unit,
timestamp: new Date().toISOString(),
url: window.location.href,
};

console.log(`Performance: ${metricName} = ${value}${unit}`);

if (this.apiKey) {
fetch('/api/monitoring/performance', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Monitoring-Key': this.apiKey,
},
body: JSON.stringify(metricData),
}).catch((err) => console.error('Failed to send metric:', err));
}
}

// 記錄使用者行為
logEvent(eventName: string, properties?: Record<string, any>): void {
const eventData = {
event: eventName,
properties,
timestamp: new Date().toISOString(),
url: window.location.href,
};

console.log('Event:', eventData);

if (this.apiKey) {
fetch('/api/monitoring/event', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Monitoring-Key': this.apiKey,
},
body: JSON.stringify(eventData),
}).catch((err) => console.error('Failed to send event:', err));
}
}
}

// 全域性錯誤處理
export function setupGlobalErrorHandling(): void {
const monitoring = MonitoringService.getInstance();

// 捕獲未處理的Promise rejection
window.addEventListener('unhandledrejection', (event) => {
monitoring.logError(new Error(event.reason), {
type: 'unhandledRejection',
});
});

// 捕獲全域性錯誤
window.addEventListener('error', (event) => {
monitoring.logError(event.error, {
type: 'globalError',
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
});
});
}

重构结果对比

性能指标对比

指标重构前重构后改进
首屏加载时间12.5s1.8s85.6% ↓
Bundle大小2.8MB580KB79.3% ↓
Time to Interactive18.2s2.3s87.4% ↓
Lighthouse评分4592104% ↑
页面切换响应3-5s100ms以下98% ↓

代码质量提升

指标重构前重构后改进
代码行数18,24712,85629.5% ↓
组件数量886328.4% ↓
代码重复率23%3%87% ↓
测试覆盖率0%87%-
TypeScript覆盖0%100%-
ESLint警告3420100% ↓

开发效率提升

任务重构前重构后效率提升
新增页面8-12小时1-2小时6-12倍
修复Bug2-4小时10-30分钟4-8倍
添加API4-6小时30分钟8-12倍
代码审查2-3小时20-30分钟4-6倍

实战技巧总结

1. 分阶段重构策略

不要一次性重写所有代码,采用渐进式重构:

# 重構的三個階段
階段1: 基礎設施(依賴、構建、型別系統)
階段2: 架構重構(目錄結構、狀態管理、API層)
階段3: 效能最佳化(程式碼分割、快取、監控)

2. 安全的回滚机制

# 每個階段獨立分支
git checkout -b refactor/phase1-dependencies
git checkout -b refactor/phase2-architecture
git checkout -b refactor/phase3-performance

# 關鍵節點打tag
git tag -a v1.0.0-before-refactor -m "Before refactoring"
git tag -a v2.0.0-after-refactor -m "After refactoring"

3. TypeScript渐进式迁移

// 策略1: 先配置,後遷移
{
"compilerOptions": {
"allowJs": true, // 允許JS檔案
"checkJs": false // 暫不檢查JS檔案
}
}

// 策略2: 按模組遷移
// 優先遷移: 工具函式、型別定義、API層
// 次要遷移: 業務元件、頁面

// 策略3: 使用型別斷言過渡
const data: any = response.data;
const typedData = data as Product[];

4. 性能优化优先级

P0 (必須做):
- 程式碼分割和懶載入
- 圖片最佳化
- Bundle大小最佳化

P1 (應該做):
- 元件memoization
- API響應快取
- 虛擬滾動(長列表)

P2 (可以做):
- Service Worker快取
- 預載入關鍵資源
- CDN配置

5. 测试策略

// 測試金字塔
- 70% 單元測試 (工具函式、hooks、純元件)
- 20% 整合測試 (API呼叫、使用者流程)
- 10% E2E測試 (關鍵業務流程)

// 測試覆蓋率目標
- 工具函式: > 95%
- 業務元件: > 80%
- 頁面元件: > 70%

常见问题解决

Q1: 如何处理大型组件的重构?

策略:

  1. 先分析组件职责
  2. 提取可复用逻辑到自定义hooks
  3. 拆分为更小的子组件
  4. 使用Composition替代继承
分析ProductList元件,它有500行程式碼:
請:
1. 識別可以提取的自定義hooks
2. 識別可以拆分的子元件
3. 重構為更小的模組

Q2: 如何迁移Redux到Redux Toolkit?

步骤:

  1. 安装Redux Toolkit
  2. 创建新的slices
  3. 使用RTK Query替换thunks
  4. 逐步迁移组件
// 從Redux遷移
const oldReducer = (state = initialState, action) => {
switch (action.type) {
case 'FETCH_SUCCESS':
return { ...state, data: action.payload };
// ...
}
};

// 到Redux Toolkit
const newSlice = createSlice({
name: 'data',
initialState,
reducers: {
fetchSuccess: (state, action) => {
state.data = action.payload;
},
},
});

Q3: 如何优化首屏加载速度?

方法:

  1. 代码分割
  2. 预加载关键资源
  3. 优化bundle大小
  4. 使用SSR/SSG(可选)
// 1. 路由級別程式碼分割
const Dashboard = lazy(() => import('./Dashboard'));

// 2. 預載入
import('./Dashboard'); // 在hover時預載入

// 3. 最佳化依賴樹
// 使用bundle analyzer分析
yarn build --profile

Q4: 如何保证重构过程中功能不被破坏?

措施:

  1. 完善的测试覆盖
  2. Git分支管理
  3. Code Review
  4. 灰度发布
# 測試策略
1. 重構前: 新增快照測試
2. 重構中: 執行測試套件
3. 重構後: 對比效能指標

# 釋出策略
1. Beta環境測試
2. 灰度釋出(10% -> 50% -> 100%)
3. 監控錯誤率

进阶挑战

完成基础重构后,可以尝试:

1. 微前端架构

使用qiankun或Module Federation將應用拆分為:
- 產品管理子系統
- 訂單管理子系統
- 客戶管理子系統
- 資料分析子系統

2. Server-Side Rendering

使用Next.js遷移到SSR:
- 更好的SEO
- 更快的首屏渲染
- 社交媒體最佳化

3. 实时功能

新增WebSocket支援:
- 實時訂單更新
- 庫存同步
- 通知推送

4. PWA功能

新增Service Worker:
- 離線支援
- 訊息推送
- 後臺同步

總結

這個重構案例展示了:

完整的重構流程 - 從分析到部署 ✅ 實用的技術方案 - TypeScript、Redux Toolkit、程式碼分割 ✅ 詳細的程式碼示例 - 超過2000行實際程式碼 ✅ 效能最佳化技巧 - 從12秒到1.8秒的最佳化實踐 ✅ 測試和質量保障 - 87%測試覆蓋率

關鍵成功因素

  1. 漸進式重構 - 不要一次性重寫
  2. 充分的測試 - 測試是安全重構的保障
  3. 版本控制 - 每個階段獨立分支
  4. 效能監控 - 持續追蹤關鍵指標
  5. 團隊協作 - Code Review和知識分享

Claude Code在大規模重構中的價值:

  • 程式碼分析: 快速識別問題模式
  • 批次重構: 安全地重新命名和移動程式碼
  • 測試生成: 自動生成測試用例
  • 文件生成: 自動更新技術文件
  • 程式碼審查: 發現潛在問題

使用Claude Code進行大規模重構,可以將重構時間從4-6周縮短到1-2周,同時保證程式碼質量和系統穩定性。

現在,開始你自己的大規模重構之旅吧!

下一步