🌟 引言

现代前端开发已经远远超越了简单的HTML、CSS、JavaScript三件套。随着应用复杂度的增加,前端工程化成为了提高开发效率、保证代码质量、优化用户体验的关键。本文将带你深入了解前端工程化的完整体系,从项目搭建到生产部署,构建现代化的前端开发工作流。

🚀 现代前端工程化架构

1. 项目初始化与脚手架

自定义脚手架工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
// cli/index.js - 自定义脚手架入口
#!/usr/bin/env node

const { program } = require('commander');
const inquirer = require('inquirer');
const chalk = require('chalk');
const fs = require('fs-extra');
const path = require('path');
const { execSync } = require('child_process');

class ProjectGenerator {
constructor() {
this.templatePath = path.join(__dirname, 'templates');
this.questions = [
{
type: 'input',
name: 'projectName',
message: '请输入项目名称:',
validate: (input) => {
if (!input) return '项目名称不能为空';
if (!/^[a-zA-Z0-9-_]+$/.test(input)) {
return '项目名称只能包含字母、数字、横线和下划线';
}
return true;
}
},
{
type: 'list',
name: 'framework',
message: '选择前端框架:',
choices: [
{ name: 'React + TypeScript', value: 'react-ts' },
{ name: 'Vue 3 + TypeScript', value: 'vue3-ts' },
{ name: 'Vanilla TypeScript', value: 'vanilla-ts' }
]
},
{
type: 'list',
name: 'buildTool',
message: '选择构建工具:',
choices: [
{ name: 'Vite (推荐)', value: 'vite' },
{ name: 'Webpack 5', value: 'webpack' }
]
},
{
type: 'checkbox',
name: 'features',
message: '选择需要的功能:',
choices: [
{ name: 'ESLint + Prettier', value: 'linting', checked: true },
{ name: 'Husky + lint-staged', value: 'git-hooks', checked: true },
{ name: 'Jest + Testing Library', value: 'testing', checked: true },
{ name: 'Storybook', value: 'storybook' },
{ name: 'PWA', value: 'pwa' },
{ name: 'Docker', value: 'docker' },
{ name: 'GitHub Actions', value: 'ci-cd', checked: true }
]
},
{
type: 'list',
name: 'cssPreprocessor',
message: '选择CSS预处理器:',
choices: [
{ name: 'Sass/SCSS', value: 'sass' },
{ name: 'Less', value: 'less' },
{ name: 'Stylus', value: 'stylus' },
{ name: '原生CSS', value: 'css' }
]
},
{
type: 'confirm',
name: 'installDeps',
message: '是否立即安装依赖?',
default: true
}
];
}

async create() {
console.log(chalk.blue.bold('\n🚀 欢迎使用前端项目脚手架\n'));

try {
const answers = await inquirer.prompt(this.questions);
await this.generateProject(answers);
} catch (error) {
console.error(chalk.red('创建项目失败:'), error.message);
process.exit(1);
}
}

async generateProject(config) {
const { projectName, framework, buildTool, features, cssPreprocessor, installDeps } = config;
const targetPath = path.join(process.cwd(), projectName);

// 导航到产品管理页面
await page.goto('/admin/products');
});

test('should create new product', async ({ page }) => {
await page.click('[data-testid="add-product-button"]');

await page.fill('[data-testid="product-name"]', 'Test Product');
await page.fill('[data-testid="product-price"]', '99.99');
await page.fill('[data-testid="product-description"]', 'This is a test product');

await page.click('[data-testid="save-product-button"]');

await expect(page.locator('[data-testid="success-message"]')).toBeVisible();
await expect(page.locator('text=Test Product')).toBeVisible();
});

test('should edit existing product', async ({ page }) => {
// 点击第一个产品的编辑按钮
await page.click('[data-testid="product-item"]:first-child [data-testid="edit-button"]');

await page.fill('[data-testid="product-name"]', 'Updated Product');
await page.click('[data-testid="save-product-button"]');

await expect(page.locator('text=Updated Product')).toBeVisible();
});

test('should delete product', async ({ page }) => {
const productCount = await page.locator('[data-testid="product-item"]').count();

await page.click('[data-testid="product-item"]:first-child [data-testid="delete-button"]');
await page.click('[data-testid="confirm-delete"]');

await expect(page.locator('[data-testid="product-item"]')).toHaveCount(productCount - 1);
});
});

// 跨浏览器测试配置
test.describe('Cross Browser Tests', () => {
['chromium', 'firefox', 'webkit'].forEach(browserName => {
test(`should work in ${browserName}`, async ({ page }) => {
await page.goto('/');
await expect(page.locator('h1')).toBeVisible();
});
});
});

// 移动端测试
test.describe('Mobile Tests', () => {
test.use({
viewport: { width: 375, height: 667 },
isMobile: true,
hasTouch: true
});

test('should be responsive on mobile', async ({ page }) => {
await page.goto('/');

// 检查移动端导航
await expect(page.locator('[data-testid="mobile-menu-button"]')).toBeVisible();

// 点击移动端菜单
await page.click('[data-testid="mobile-menu-button"]');
await expect(page.locator('[data-testid="mobile-menu"]')).toBeVisible();
});
});

🚀 性能优化策略

1. 构建性能优化

Webpack优化配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
// webpack.optimization.js
const path = require('path');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const CompressionPlugin = require('compression-webpack-plugin');

class WebpackOptimizer {
static getOptimizationConfig(isDev = false) {
return {
// 代码分割
splitChunks: {
chunks: 'all',
minSize: 20000,
maxSize: 244000,
cacheGroups: {
// 第三方库
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
priority: 10,
reuseExistingChunk: true
},
// React相关
react: {
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
name: 'react',
chunks: 'all',
priority: 20
},
// UI库
ui: {
test: /[\\/]node_modules[\\/](antd|@ant-design|element-ui)[\\/]/,
name: 'ui',
chunks: 'all',
priority: 15
},
// 工具库
utils: {
test: /[\\/]node_modules[\\/](lodash|moment|dayjs|axios)[\\/]/,
name: 'utils',
chunks: 'all',
priority: 12
},
// 公共代码
common: {
name: 'common',
minChunks: 2,
chunks: 'all',
priority: 5,
reuseExistingChunk: true
}
}
},
// 运行时chunk
runtimeChunk: {
name: 'runtime'
},
// 压缩配置
minimize: !isDev,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: !isDev,
drop_debugger: !isDev,
pure_funcs: isDev ? [] : ['console.log', 'console.info']
},
mangle: {
safari10: true
},
format: {
comments: false
}
},
extractComments: false,
parallel: true
}),
new CssMinimizerPlugin({
minimizerOptions: {
preset: [
'default',
{
discardComments: { removeAll: true },
normalizeWhitespace: true
}
]
}
})
]
};
}

static getPerformanceConfig() {
return {
hints: 'warning',
maxEntrypointSize: 512000,
maxAssetSize: 512000,
assetFilter: (assetFilename) => {
return !assetFilename.endsWith('.map');
}
};
}

static getPlugins(isDev = false, analyze = false) {
const plugins = [];

if (!isDev) {
// Gzip压缩
plugins.push(
new CompressionPlugin({
algorithm: 'gzip',
test: /\.(js|css|html|svg)$/,
threshold: 8192,
minRatio: 0.8
})
);

// Brotli压缩
plugins.push(
new CompressionPlugin({
filename: '[path][base].br',
algorithm: 'brotliCompress',
test: /\.(js|css|html|svg)$/,
compressionOptions: {
level: 11
},
threshold: 8192,
minRatio: 0.8
})
);
}

// 包分析
if (analyze) {
plugins.push(
new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false,
reportFilename: 'bundle-report.html'
})
);
}

return plugins;
}
}

module.exports = WebpackOptimizer;

Vite优化配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
// vite.optimization.js
import { defineConfig } from 'vite';
import { visualizer } from 'rollup-plugin-visualizer';
import { createHtmlPlugin } from 'vite-plugin-html';

export class ViteOptimizer {
static getBuildConfig(isDev = false) {
return {
target: 'es2015',
outDir: 'dist',
assetsDir: 'assets',
sourcemap: isDev,
minify: isDev ? false : 'terser',
terserOptions: {
compress: {
drop_console: !isDev,
drop_debugger: !isDev
}
},
rollupOptions: {
output: {
// 手动分包
manualChunks: {
// React相关
react: ['react', 'react-dom'],
// 路由
router: ['react-router-dom'],
// UI库
ui: ['antd', '@ant-design/icons'],
// 工具库
utils: ['lodash-es', 'dayjs', 'axios'],
// 图表库
charts: ['echarts', 'recharts']
},
// 资源文件命名
chunkFileNames: (chunkInfo) => {
const facadeModuleId = chunkInfo.facadeModuleId
? chunkInfo.facadeModuleId.split('/').pop()
: 'chunk';
return `js/${facadeModuleId}-[hash].js`;
},
assetFileNames: (assetInfo) => {
const info = assetInfo.name.split('.');
const ext = info[info.length - 1];
if (/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/i.test(assetInfo.name)) {
return `media/[name]-[hash].${ext}`;
}
if (/\.(png|jpe?g|gif|svg)(\?.*)?$/.test(assetInfo.name)) {
return `images/[name]-[hash].${ext}`;
}
if (/\.(woff2?|eot|ttf|otf)(\?.*)?$/i.test(assetInfo.name)) {
return `fonts/[name]-[hash].${ext}`;
}
return `assets/[name]-[hash].${ext}`;
}
}
},
// 预构建优化
optimizeDeps: {
include: [
'react',
'react-dom',
'react-router-dom',
'antd',
'lodash-es',
'dayjs'
],
exclude: ['@vite/client', '@vite/env']
}
};
}

static getPlugins(isDev = false, analyze = false) {
const plugins = [];

// HTML优化
plugins.push(
createHtmlPlugin({
minify: !isDev,
inject: {
data: {
title: 'Frontend Engineering App',
injectScript: isDev ? '' : `
<script>
// 预加载关键资源
const preloadLinks = [
'/assets/main.css',
'/assets/vendor.js'
];
preloadLinks.forEach(href => {
const link = document.createElement('link');
link.rel = 'preload';
link.href = href;
link.as = href.endsWith('.css') ? 'style' : 'script';
document.head.appendChild(link);
});
</script>
`
}
}
})
);

// 包分析
if (analyze) {
plugins.push(
visualizer({
filename: 'dist/stats.html',
open: true,
gzipSize: true,
brotliSize: true
})
);
}

return plugins;
}
}

2. 运行时性能优化

React性能优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
// performance/ReactOptimizer.tsx
import React, { memo, useMemo, useCallback, lazy, Suspense } from 'react';
import { debounce, throttle } from 'lodash-es';

// 组件懒加载
const LazyComponent = lazy(() => import('./HeavyComponent'));

// 虚拟滚动组件
interface VirtualListProps {
items: any[];
itemHeight: number;
containerHeight: number;
renderItem: (item: any, index: number) => React.ReactNode;
}

export const VirtualList: React.FC<VirtualListProps> = memo(({
items,
itemHeight,
containerHeight,
renderItem
}) => {
const [scrollTop, setScrollTop] = React.useState(0);

const visibleItems = useMemo(() => {
const startIndex = Math.floor(scrollTop / itemHeight);
const endIndex = Math.min(
startIndex + Math.ceil(containerHeight / itemHeight) + 1,
items.length
);

return items.slice(startIndex, endIndex).map((item, index) => ({
item,
index: startIndex + index
}));
}, [items, itemHeight, containerHeight, scrollTop]);

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

const totalHeight = items.length * itemHeight;
const offsetY = Math.floor(scrollTop / itemHeight) * itemHeight;

return (
<div
style={{ height: containerHeight, overflow: 'auto' }}
onScroll={handleScroll}
>
<div style={{ height: totalHeight, position: 'relative' }}>
<div style={{ transform: `translateY(${offsetY}px)` }}>
{visibleItems.map(({ item, index }) => (
<div key={index} style={{ height: itemHeight }}>
{renderItem(item, index)}
</div>
))}
</div>
</div>
</div>
);
});

// 防抖搜索组件
interface SearchInputProps {
onSearch: (query: string) => void;
delay?: number;
placeholder?: string;
}

export const SearchInput: React.FC<SearchInputProps> = memo(({
onSearch,
delay = 300,
placeholder = '请输入搜索关键词'
}) => {
const [value, setValue] = React.useState('');

const debouncedSearch = useMemo(
() => debounce((query: string) => {
onSearch(query);
}, delay),
[onSearch, delay]
);

const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;
setValue(newValue);
debouncedSearch(newValue);
}, [debouncedSearch]);

React.useEffect(() => {
return () => {
debouncedSearch.cancel();
};
}, [debouncedSearch]);

return (
<input
type="text"
value={value}
onChange={handleChange}
placeholder={placeholder}
className="search-input"
/>
);
});

// 图片懒加载组件
interface LazyImageProps {
src: string;
alt: string;
placeholder?: string;
className?: string;
}

export const LazyImage: React.FC<LazyImageProps> = memo(({
src,
alt,
placeholder = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIwIiBoZWlnaHQ9IjE4MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZGRkIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtc2l6ZT0iMTgiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIiBmaWxsPSIjOTk5Ij5Mb2FkaW5nLi4uPC90ZXh0Pjwvc3ZnPg==',
className
}) => {
const [isLoaded, setIsLoaded] = React.useState(false);
const [isInView, setIsInView] = React.useState(false);
const imgRef = React.useRef<HTMLImageElement>(null);

React.useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsInView(true);
observer.disconnect();
}
},
{ threshold: 0.1 }
);

if (imgRef.current) {
observer.observe(imgRef.current);
}

return () => observer.disconnect();
}, []);

const handleLoad = useCallback(() => {
setIsLoaded(true);
}, []);

return (
<img
ref={imgRef}
src={isInView ? src : placeholder}
alt={alt}
className={`${className} ${isLoaded ? 'loaded' : 'loading'}`}
onLoad={handleLoad}
loading="lazy"
/>
);
});

// 性能监控Hook
export const usePerformanceMonitor = () => {
const [metrics, setMetrics] = React.useState({
renderTime: 0,
componentCount: 0,
memoryUsage: 0
});

React.useEffect(() => {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach((entry) => {
if (entry.entryType === 'measure') {
setMetrics(prev => ({
...prev,
renderTime: entry.duration
}));
}
});
});

observer.observe({ entryTypes: ['measure'] });

// 内存使用监控
const memoryInterval = setInterval(() => {
if ('memory' in performance) {
const memory = (performance as any).memory;
setMetrics(prev => ({
...prev,
memoryUsage: memory.usedJSHeapSize / 1024 / 1024 // MB
}));
}
}, 5000);

return () => {
observer.disconnect();
clearInterval(memoryInterval);
};
}, []);

const measureRender = useCallback((name: string, fn: () => void) => {
performance.mark(`${name}-start`);
fn();
performance.mark(`${name}-end`);
performance.measure(name, `${name}-start`, `${name}-end`);
}, []);

return { metrics, measureRender };
};

🚀 部署策略与CI/CD

1. 多环境部署配置

Docker部署

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# Dockerfile.multi-stage
# 构建阶段
FROM node:18-alpine AS builder

WORKDIR /app

# 复制package文件
COPY package*.json ./
COPY yarn.lock ./

# 安装依赖
RUN yarn install --frozen-lockfile --production=false

# 复制源代码
COPY . .

# 构建应用
RUN yarn build

# 生产阶段
FROM nginx:alpine AS production

# 安装必要工具
RUN apk add --no-cache curl

# 复制nginx配置
COPY nginx.conf /etc/nginx/nginx.conf
COPY default.conf /etc/nginx/conf.d/default.conf

# 复制构建产物
COPY --from=builder /app/dist /usr/share/nginx/html

# 添加健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost/ || exit 1

# 暴露端口
EXPOSE 80

# 启动nginx
CMD ["nginx", "-g", "daemon off;"]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# nginx.conf
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
worker_connections 1024;
use epoll;
multi_accept on;
}

http {
include /etc/nginx/mime.types;
default_type application/octet-stream;

# 日志格式
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

access_log /var/log/nginx/access.log main;

# 基础配置
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
client_max_body_size 16M;

# Gzip压缩
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied any;
gzip_comp_level 6;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml+rss
application/atom+xml
image/svg+xml;

# 包含站点配置
include /etc/nginx/conf.d/*.conf;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# default.conf
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html index.htm;

# 安全头
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;

# 静态资源缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}

# 字体文件
location ~* \.(woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public";
add_header Access-Control-Allow-Origin "*";
try_files $uri =404;
}

# HTML文件不缓存
location ~* \.html$ {
expires -1;
add_header Cache-Control "no-cache, no-store, must-revalidate";
try_files $uri $uri/ /index.html;
}

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

# API代理
location /api/ {
proxy_pass http://backend:3000/;
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 /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}

GitHub Actions CI/CD

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# .github/workflows/deploy.yml
name: Deploy Frontend Application

on:
push:
branches: [main, develop]
pull_request:
branches: [main]

env:
NODE_VERSION: '18'
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'yarn'

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

- name: Run linting
run: yarn lint

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

- name: Run unit tests
run: yarn test:coverage

- name: Upload coverage reports
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}

e2e-test:
runs-on: ubuntu-latest
needs: test

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'yarn'

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

- name: Install Playwright
run: npx playwright install --with-deps

- name: Build application
run: yarn build

- name: Run E2E tests
run: yarn test:e2e

- name: Upload test results
uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/

build-and-push:
runs-on: ubuntu-latest
needs: [test, e2e-test]
if: github.event_name == 'push'

permissions:
contents: read
packages: write

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Login to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=sha,prefix={{branch}}-
type=raw,value=latest,enable={{is_default_branch}}

- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

deploy-staging:
runs-on: ubuntu-latest
needs: build-and-push
if: github.ref == 'refs/heads/develop'
environment: staging

steps:
- name: Deploy to staging
run: |
echo "Deploying to staging environment..."
# 这里添加部署到测试环境的脚本

deploy-production:
runs-on: ubuntu-latest
needs: build-and-push
if: github.ref == 'refs/heads/main'
environment: production

steps:
- name: Deploy to production
run: |
echo "Deploying to production environment..."
# 这里添加部署到生产环境的脚本

lighthouse-audit:
runs-on: ubuntu-latest
needs: deploy-staging
if: github.ref == 'refs/heads/develop'

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Run Lighthouse CI
uses: treosh/lighthouse-ci-action@v10
with:
configPath: './lighthouserc.json'
uploadArtifacts: true
temporaryPublicStorage: true

2. 监控与告警

性能监控

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
// monitoring/PerformanceMonitor.ts
interface PerformanceMetrics {
fcp: number; // First Contentful Paint
lcp: number; // Largest Contentful Paint
fid: number; // First Input Delay
cls: number; // Cumulative Layout Shift
ttfb: number; // Time to First Byte
}

class PerformanceMonitor {
private metrics: Partial<PerformanceMetrics> = {};
private observers: PerformanceObserver[] = [];

constructor(private reportUrl: string) {
this.initObservers();
this.setupErrorTracking();
this.trackUserInteractions();
}

private initObservers() {
// FCP & LCP
const paintObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.name === 'first-contentful-paint') {
this.metrics.fcp = entry.startTime;
}
}
});
paintObserver.observe({ entryTypes: ['paint'] });
this.observers.push(paintObserver);

// LCP
const lcpObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
this.metrics.lcp = lastEntry.startTime;
});
lcpObserver.observe({ entryTypes: ['largest-contentful-paint'] });
this.observers.push(lcpObserver);

// FID
const fidObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
this.metrics.fid = entry.processingStart - entry.startTime;
}
});
fidObserver.observe({ entryTypes: ['first-input'] });
this.observers.push(fidObserver);

// CLS
let clsValue = 0;
const clsObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!(entry as any).hadRecentInput) {
clsValue += (entry as any).value;
}
}
this.metrics.cls = clsValue;
});
clsObserver.observe({ entryTypes: ['layout-shift'] });
this.observers.push(clsObserver);

// Navigation Timing
const navigationObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
const navEntry = entry as PerformanceNavigationTiming;
this.metrics.ttfb = navEntry.responseStart - navEntry.requestStart;
}
});
navigationObserver.observe({ entryTypes: ['navigation'] });
this.observers.push(navigationObserver);
}

private setupErrorTracking() {
// JavaScript错误
window.addEventListener('error', (event) => {
this.reportError({
type: 'javascript',
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
stack: event.error?.stack,
timestamp: Date.now()
});
});

// Promise rejection
window.addEventListener('unhandledrejection', (event) => {
this.reportError({
type: 'promise',
message: event.reason?.message || 'Unhandled Promise Rejection',
stack: event.reason?.stack,
timestamp: Date.now()
});
});

// 资源加载错误
window.addEventListener('error', (event) => {
if (event.target !== window) {
this.reportError({
type: 'resource',
message: `Failed to load resource: ${(event.target as any)?.src || (event.target as any)?.href}`,
element: (event.target as any)?.tagName,
timestamp: Date.now()
});
}
}, true);
}

private trackUserInteractions() {
// 页面可见性
document.addEventListener('visibilitychange', () => {
this.reportMetric('visibility', {
hidden: document.hidden,
timestamp: Date.now()
});
});

// 用户会话时长
let sessionStart = Date.now();
window.addEventListener('beforeunload', () => {
this.reportMetric('session', {
duration: Date.now() - sessionStart,
timestamp: Date.now()
});
});
}

private async reportError(error: any) {
try {
await fetch(`${this.reportUrl}/errors`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
...error,
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: Date.now()
})
});
} catch (e) {
console.error('Failed to report error:', e);
}
}

private async reportMetric(type: string, data: any) {
try {
await fetch(`${this.reportUrl}/metrics`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
type,
data,
url: window.location.href,
timestamp: Date.now()
})
});
} catch (e) {
console.error('Failed to report metric:', e);
}
}

public getMetrics(): Partial<PerformanceMetrics> {
return { ...this.metrics };
}

public async reportAllMetrics() {
await this.reportMetric('performance', this.metrics);
}

public destroy() {
this.observers.forEach(observer => observer.disconnect());
this.observers = [];
}
}

// 使用示例
const monitor = new PerformanceMonitor('https://api.example.com/monitoring');

// 页面加载完成后报告指标
window.addEventListener('load', () => {
setTimeout(() => {
monitor.reportAllMetrics();
}, 0);
});

📊 总结与最佳实践

核心要点

  1. 工程化体系

    • 统一的项目结构和代码规范
    • 自动化的构建和部署流程
    • 完善的测试和质量保证体系
  2. 性能优化

    • 构建时优化:代码分割、压缩、缓存
    • 运行时优化:懒加载、虚拟滚动、防抖节流
    • 监控告警:性能指标、错误追踪、用户体验
  3. 开发效率

    • 热更新和快速构建
    • 智能的代码提示和错误检查
    • 自动化的代码格式化和修复
  4. 团队协作

    • 统一的开发环境和工具链
    • 规范的Git工作流和代码审查
    • 完善的文档和知识分享

未来趋势

  • 微前端架构:模块化、独立部署、技术栈无关
  • 边缘计算:CDN、边缘渲染、就近服务
  • AI辅助开发:智能代码生成、自动化测试、性能优化建议
  • Web Assembly:高性能计算、跨语言开发
  • Serverless:函数即服务、按需计费、自动扩缩容

前端工程化是一个持续演进的过程,需要根据项目需求和团队情况选择合适的工具和方案。关键是要建立完善的开发工作流,提高开发效率和代码质量,为用户提供更好的产品体验。


本文涵盖了前端工程化的核心内容,从项目搭建到生产部署的完整流程。希望能帮助你构建高效、可维护的前端开发体系。 检查目录是否存在
if (await fs.pathExists(targetPath)) {
const { overwrite } = await inquirer.prompt([
{
type: ‘confirm’,
name: ‘overwrite’,
message: 目录 ${projectName} 已存在,是否覆盖?,
default: false
}
]);

        if (!overwrite) {
            console.log(chalk.yellow('已取消创建'));
            return;
        }

        await fs.remove(targetPath);
    }

    console.log(chalk.green(`\n📁 创建项目目录: ${projectName}`));
    await fs.ensureDir(targetPath);

    // 生成基础文件结构
    await this.generateBaseStructure(targetPath, config);

    // 生成配置文件
    await this.generateConfigFiles(targetPath, config);

    // 生成源代码
    await this.generateSourceCode(targetPath, config);

    // 生成package.json
    await this.generatePackageJson(targetPath, config);

    // 安装依赖
    if (installDeps) {
        await this.installDependencies(targetPath);
    }

    // 初始化Git
    await this.initGit(targetPath, features.includes('git-hooks'));

    console.log(chalk.green.bold('\n✅ 项目创建成功!'));
    console.log(chalk.cyan('\n📖 接下来的步骤:'));
    console.log(chalk.white(`  cd ${projectName}`));
    if (!installDeps) {
        console.log(chalk.white('  npm install'));
    }
    console.log(chalk.white('  npm run dev'));
    console.log(chalk.white('\n🎉 开始你的前端之旅吧!\n'));
}

async generateBaseStructure(targetPath, config) {
    const structure = {
        'src': {
            'components': {},
            'utils': {},
            'styles': {},
            'assets': {
                'images': {},
                'icons': {}
            },
            'types': {},
            'hooks': {},
            'services': {},
            'store': {},
            '__tests__': {}
        },
        'public': {},
        'docs': {},
        '.vscode': {}
    };

    await this.createDirectoryStructure(targetPath, structure);
}

async createDirectoryStructure(basePath, structure) {
    for (const [name, children] of Object.entries(structure)) {
        const dirPath = path.join(basePath, name);
        await fs.ensureDir(dirPath);

        if (typeof children === 'object' && Object.keys(children).length > 0) {
            await this.createDirectoryStructure(dirPath, children);
        }
    }
}

async generateConfigFiles(targetPath, config) {
    const { buildTool, features, cssPreprocessor } = config;

    // TypeScript配置
    await this.generateTsConfig(targetPath);

    // 构建工具配置
    if (buildTool === 'vite') {
        await this.generateViteConfig(targetPath, config);
    } else {
        await this.generateWebpackConfig(targetPath, config);
    }

    // ESLint配置
    if (features.includes('linting')) {
        await this.generateESLintConfig(targetPath, config);
        await this.generatePrettierConfig(targetPath);
    }

    // Git hooks配置
    if (features.includes('git-hooks')) {
        await this.generateHuskyConfig(targetPath);
    }

    // 测试配置
    if (features.includes('testing')) {
        await this.generateJestConfig(targetPath, config);
    }

    // VS Code配置
    await this.generateVSCodeConfig(targetPath);

    // 环境变量配置
    await this.generateEnvFiles(targetPath);

    // Docker配置
    if (features.includes('docker')) {
        await this.generateDockerConfig(targetPath, config);
    }

    // CI/CD配置
    if (features.includes('ci-cd')) {
        await this.generateGitHubActions(targetPath, config);
    }
}

async generateTsConfig(targetPath) {
    const tsConfig = {
        compilerOptions: {
            target: "ES2020",
            lib: ["DOM", "DOM.Iterable", "ES6"],
            allowJs: true,
            skipLibCheck: true,
            esModuleInterop: true,
            allowSyntheticDefaultImports: true,
            strict: true,
            forceConsistentCasingInFileNames: true,
            noFallthroughCasesInSwitch: true,
            module: "ESNext",
            moduleResolution: "node",
            resolveJsonModule: true,
            isolatedModules: true,
            noEmit: true,
            jsx: "react-jsx",
            baseUrl: ".",
            paths: {
                "@/*": ["src/*"],
                "@/components/*": ["src/components/*"],
                "@/utils/*": ["src/utils/*"],
                "@/types/*": ["src/types/*"],
                "@/hooks/*": ["src/hooks/*"],
                "@/services/*": ["src/services/*"],
                "@/store/*": ["src/store/*"]
            }
        },
        include: [
            "src/**/*",
            "src/**/*.tsx",
            "src/**/*.ts"
        ],
        exclude: [
            "node_modules",
            "dist",
            "build"
        ]
    };

    await fs.writeJSON(path.join(targetPath, 'tsconfig.json'), tsConfig, { spaces: 2 });
}

async generateViteConfig(targetPath, config) {
    const { framework, features } = config;
    
    let viteConfig = `import { defineConfig } from 'vite'

import { resolve } from ‘path’
`;

    if (framework === 'react-ts') {
        viteConfig += `import react from '@vitejs/plugin-react'

; } else if (framework === 'vue3-ts') { viteConfig += import vue from ‘@vitejs/plugin-vue’
`;
}

    if (features.includes('pwa')) {
        viteConfig += `import { VitePWA } from 'vite-plugin-pwa'

`;
}

    viteConfig += `

export default defineConfig({
plugins: [`;

    if (framework === 'react-ts') {
        viteConfig += `
react(),`;
    } else if (framework === 'vue3-ts') {
        viteConfig += `
vue(),`;
    }

    if (features.includes('pwa')) {
        viteConfig += `
VitePWA({
  registerType: 'autoUpdate',
  workbox: {
    globPatterns: ['**/*.{js,css,html,ico,png,svg}']
  }
}),`;
    }

    viteConfig += `

],
resolve: {
alias: {
‘@’: resolve(__dirname, ‘src’),
},
},
server: {
port: 3000,
open: true,
cors: true,
},
build: {
target: ‘es2015’,
outDir: ‘dist’,
assetsDir: ‘assets’,
sourcemap: false,
rollupOptions: {
output: {
chunkFileNames: ‘js/[name]-[hash].js’,
entryFileNames: ‘js/[name]-[hash].js’,
assetFileNames: ‘[ext]/[name]-[hash].[ext]’,
manualChunks: {
vendor: [‘react’, ‘react-dom’],
utils: [‘lodash’, ‘axios’],
},
},
},
chunkSizeWarningLimit: 1000,
},
optimizeDeps: {
include: [‘react’, ‘react-dom’],
},
})
`;

    await fs.writeFile(path.join(targetPath, 'vite.config.ts'), viteConfig);
}

async generateESLintConfig(targetPath, config) {
    const { framework } = config;
    
    const eslintConfig = {
        env: {
            browser: true,
            es2021: true,
            node: true
        },
        extends: [
            'eslint:recommended',
            '@typescript-eslint/recommended'
        ],
        parser: '@typescript-eslint/parser',
        parserOptions: {
            ecmaVersion: 'latest',
            sourceType: 'module'
        },
        plugins: ['@typescript-eslint'],
        rules: {
            'indent': ['error', 2],
            'linebreak-style': ['error', 'unix'],
            'quotes': ['error', 'single'],
            'semi': ['error', 'always'],
            '@typescript-eslint/no-unused-vars': 'error',
            '@typescript-eslint/no-explicit-any': 'warn',
            'no-console': 'warn',
            'no-debugger': 'error'
        }
    };

    if (framework === 'react-ts') {
        eslintConfig.extends.push('plugin:react/recommended', 'plugin:react-hooks/recommended');
        eslintConfig.plugins.push('react', 'react-hooks');
        eslintConfig.parserOptions.ecmaFeatures = { jsx: true };
        eslintConfig.settings = { react: { version: 'detect' } };
    } else if (framework === 'vue3-ts') {
        eslintConfig.extends.push('plugin:vue/vue3-recommended');
        eslintConfig.plugins.push('vue');
    }

    await fs.writeJSON(path.join(targetPath, '.eslintrc.json'), eslintConfig, { spaces: 2 });

    // .eslintignore
    const eslintIgnore = `node_modules/

dist/
build/
*.min.js
coverage/
.nyc_output/
`;
await fs.writeFile(path.join(targetPath, ‘.eslintignore’), eslintIgnore);
}

async generatePrettierConfig(targetPath) {
    const prettierConfig = {
        semi: true,
        trailingComma: 'es5',
        singleQuote: true,
        printWidth: 100,
        tabWidth: 2,
        useTabs: false,
        bracketSpacing: true,
        arrowParens: 'avoid'
    };

    await fs.writeJSON(path.join(targetPath, '.prettierrc'), prettierConfig, { spaces: 2 });

    // .prettierignore
    const prettierIgnore = `node_modules/

dist/
build/
coverage/
*.min.js
*.min.css
`;
await fs.writeFile(path.join(targetPath, ‘.prettierignore’), prettierIgnore);
}

async generatePackageJson(targetPath, config) {
    const { projectName, framework, buildTool, features, cssPreprocessor } = config;

    const packageJson = {
        name: projectName,
        version: '1.0.0',
        description: `A modern ${framework} project with ${buildTool}`,
        main: 'index.js',
        scripts: {
            dev: buildTool === 'vite' ? 'vite' : 'webpack serve --mode development',
            build: buildTool === 'vite' ? 'vite build' : 'webpack --mode production',
            preview: buildTool === 'vite' ? 'vite preview' : 'serve -s dist',
            type: 'tsc --noEmit'
        },
        keywords: [framework, buildTool, 'typescript', 'frontend'],
        author: '',
        license: 'MIT',
        dependencies: {},
        devDependencies: {
            typescript: '^5.0.0',
            '@types/node': '^20.0.0'
        }
    };

    // 添加框架相关依赖
    if (framework === 'react-ts') {
        packageJson.dependencies = {
            ...packageJson.dependencies,
            react: '^18.2.0',
            'react-dom': '^18.2.0'
        };
        packageJson.devDependencies = {
            ...packageJson.devDependencies,
            '@types/react': '^18.2.0',
            '@types/react-dom': '^18.2.0'
        };
    } else if (framework === 'vue3-ts') {
        packageJson.dependencies = {
            ...packageJson.dependencies,
            vue: '^3.3.0'
        };
    }

    // 添加构建工具依赖
    if (buildTool === 'vite') {
        packageJson.devDependencies.vite = '^4.4.0';
        if (framework === 'react-ts') {
            packageJson.devDependencies['@vitejs/plugin-react'] = '^4.0.0';
        } else if (framework === 'vue3-ts') {
            packageJson.devDependencies['@vitejs/plugin-vue'] = '^4.0.0';
        }
    }

    // 添加代码质量工具
    if (features.includes('linting')) {
        packageJson.devDependencies = {
            ...packageJson.devDependencies,
            eslint: '^8.45.0',
            '@typescript-eslint/eslint-plugin': '^6.0.0',
            '@typescript-eslint/parser': '^6.0.0',
            prettier: '^3.0.0'
        };

        packageJson.scripts.lint = 'eslint src --ext .ts,.tsx --fix';
        packageJson.scripts.format = 'prettier --write "src/**/*.{ts,tsx,css,scss,json}"';
    }

    // 添加Git hooks
    if (features.includes('git-hooks')) {
        packageJson.devDependencies = {
            ...packageJson.devDependencies,
            husky: '^8.0.0',
            'lint-staged': '^13.0.0'
        };

        packageJson.scripts.prepare = 'husky install';
        packageJson['lint-staged'] = {
            '*.{ts,tsx}': ['eslint --fix', 'prettier --write'],
            '*.{css,scss,json,md}': ['prettier --write']
        };
    }

    // 添加测试工具
    if (features.includes('testing')) {
        packageJson.devDependencies = {
            ...packageJson.devDependencies,
            jest: '^29.0.0',
            '@testing-library/jest-dom': '^5.16.0',
            '@types/jest': '^29.0.0'
        };

        if (framework === 'react-ts') {
            packageJson.devDependencies['@testing-library/react'] = '^13.0.0';
            packageJson.devDependencies['@testing-library/user-event'] = '^14.0.0';
        }

        packageJson.scripts.test = 'jest';
        packageJson.scripts['test:watch'] = 'jest --watch';
        packageJson.scripts['test:coverage'] = 'jest --coverage';
    }

    await fs.writeJSON(path.join(targetPath, 'package.json'), packageJson, { spaces: 2 });
}

async installDependencies(targetPath) {
    console.log(chalk.yellow('\n📦 安装依赖中...'));
    
    try {
        execSync('npm install', { 
            cwd: targetPath, 
            stdio: 'inherit' 
        });
        console.log(chalk.green('✅ 依赖安装完成'));
    } catch (error) {
        console.error(chalk.red('❌ 依赖安装失败:'), error.message);
    }
}

async initGit(targetPath, useHooks) {
    try {
        execSync('git init', { cwd: targetPath, stdio: 'pipe' });
        
        // 创建.gitignore
        const gitignore = `# Dependencies

node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*

Production builds

dist/
build/

Environment variables

.env
.env.local
.env.development.local
.env.test.local
.env.production.local

IDE

.vscode/
.idea/
*.swp
*.swo

OS

.DS_Store
Thumbs.db

Logs

logs/
*.log

Coverage

coverage/
.nyc_output/

Cache

.cache/
.parcel-cache/
`;
await fs.writeFile(path.join(targetPath, ‘.gitignore’), gitignore);

        if (useHooks) {
            execSync('npx husky install', { cwd: targetPath, stdio: 'pipe' });
            execSync('npx husky add .husky/pre-commit "npx lint-staged"', { 
                cwd: targetPath, 
                stdio: 'pipe' 
            });
        }

        console.log(chalk.green('✅ Git仓库初始化完成'));
    } catch (error) {
        console.error(chalk.red('❌ Git初始化失败:'), error.message);
    }
}

}

// CLI程序入口
program
.version(‘1.0.0’)
.description(‘现代前端项目脚手架’)
.action(async () => {
const generator = new ProjectGenerator();
await generator.create();
});

program.parse(process.argv);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291

### 2. 构建工具配置与优化

#### Webpack 5 高级配置

```javascript
// webpack.config.js - 生产级Webpack配置
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const CompressionPlugin = require('compression-webpack-plugin');
const { DefinePlugin } = require('webpack');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const ESLintPlugin = require('eslint-webpack-plugin');

const isDevelopment = process.env.NODE_ENV === 'development';
const isProduction = process.env.NODE_ENV === 'production';
const isAnalyze = process.env.ANALYZE === 'true';

module.exports = {
mode: process.env.NODE_ENV || 'development',

entry: {
main: './src/index.tsx',
vendor: ['react', 'react-dom']
},

output: {
path: path.resolve(__dirname, 'dist'),
filename: isProduction
? 'js/[name].[contenthash:8].js'
: 'js/[name].js',
chunkFilename: isProduction
? 'js/[name].[contenthash:8].chunk.js'
: 'js/[name].chunk.js',
assetModuleFilename: 'assets/[name].[contenthash:8][ext]',
clean: true,
publicPath: '/'
},

resolve: {
extensions: ['.tsx', '.ts', '.jsx', '.js', '.json'],
alias: {
'@': path.resolve(__dirname, 'src'),
'@components': path.resolve(__dirname, 'src/components'),
'@utils': path.resolve(__dirname, 'src/utils'),
'@types': path.resolve(__dirname, 'src/types'),
'@hooks': path.resolve(__dirname, 'src/hooks'),
'@services': path.resolve(__dirname, 'src/services'),
'@store': path.resolve(__dirname, 'src/store')
},
fallback: {
"path": require.resolve("path-browserify"),
"os": require.resolve("os-browserify/browser"),
"crypto": require.resolve("crypto-browserify")
}
},

module: {
rules: [
// TypeScript/JavaScript
{
test: /\.(ts|tsx|js|jsx)$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', {
targets: {
browsers: ['> 1%', 'last 2 versions']
},
useBuiltIns: 'usage',
corejs: 3
}],
['@babel/preset-react', {
runtime: 'automatic'
}],
'@babel/preset-typescript'
],
plugins: [
'@babel/plugin-proposal-class-properties',
'@babel/plugin-proposal-object-rest-spread',
isDevelopment && 'react-refresh/babel'
].filter(Boolean),
cacheDirectory: true
}
}
]
},

// CSS/SCSS
{
test: /\.(css|scss|sass)$/,
use: [
isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
{
loader: 'css-loader',
options: {
modules: {
auto: true,
localIdentName: isProduction
? '[hash:base64:8]'
: '[path][name]__[local]--[hash:base64:5]'
},
sourceMap: isDevelopment
}
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
'autoprefixer',
'postcss-preset-env',
isProduction && 'cssnano'
].filter(Boolean)
}
}
},
'sass-loader'
]
},

// 图片资源
{
test: /\.(png|jpe?g|gif|svg|webp)$/i,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 8 * 1024 // 8KB
}
},
generator: {
filename: 'images/[name].[contenthash:8][ext]'
}
},

// 字体文件
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
generator: {
filename: 'fonts/[name].[contenthash:8][ext]'
}
}
]
},

plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
filename: 'index.html',
inject: true,
minify: isProduction ? {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true
} : false
}),

new DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
'process.env.REACT_APP_VERSION': JSON.stringify(process.env.npm_package_version),
'__DEV__': isDevelopment
}),

new ForkTsCheckerWebpackPlugin({
typescript: {
configFile: path.resolve(__dirname, 'tsconfig.json')
}
}),

new ESLintPlugin({
extensions: ['js', 'jsx', 'ts', 'tsx'],
exclude: 'node_modules',
emitWarning: isDevelopment,
emitError: isProduction,
failOnError: isProduction
}),

isProduction && new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css',
chunkFilename: 'css/[name].[contenthash:8].chunk.css'
}),

isProduction && new CompressionPlugin({
algorithm: 'gzip',
test: /\.(js|css|html|svg)$/,
threshold: 8192,
minRatio: 0.8
}),

isAnalyze && new BundleAnalyzerPlugin({
analyzerMode: 'server',
openAnalyzer: true
})
].filter(Boolean),

optimization: {
minimize: isProduction,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: isProduction,
drop_debugger: isProduction
},
format: {
comments: false
}
},
extractComments: false
}),
new CssMinimizerPlugin()
],

splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
priority: 10
},
react: {
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
name: 'react',
chunks: 'all',
priority: 20
},
common: {
name: 'common',
minChunks: 2,
chunks: 'all',
priority: 5,
reuseExistingChunk: true
}
}
},

runtimeChunk: {
name: 'runtime'
}
},

devServer: {
port: 3000,
hot: true,
open: true,
compress: true,
historyApiFallback: true,
static: {
directory: path.join(__dirname, 'public')
},
client: {
overlay: {
errors: true,
warnings: false
}
}
},

devtool: isDevelopment ? 'eval-source-map' : 'source-map',

performance: {
hints: isProduction ? 'warning' : false,
maxEntrypointSize: 512000,
maxAssetSize: 512000
},

stats: {
colors: true,
modules: false,
children: false,
chunks: false,
chunkModules: false
}
};

Vite 高级配置与插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
// vite.config.ts - 生产级Vite配置
import { defineConfig, loadEnv } from 'vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';
import { visualizer } from 'rollup-plugin-visualizer';
import { VitePWA } from 'vite-plugin-pwa';
import legacy from '@vitejs/plugin-legacy';
import { createHtmlPlugin } from 'vite-plugin-html';
import eslint from 'vite-plugin-eslint';
import { chunkSplitPlugin } from 'vite-plugin-chunk-split';

export default defineConfig(({ command, mode }) => {
const env = loadEnv(mode, process.cwd(), '');
const isProduction = mode === 'production';
const isDevelopment = mode === 'development';

return {
plugins: [
react({
// React Fast Refresh配置
fastRefresh: isDevelopment,
// Babel配置
babel: {
plugins: [
isDevelopment && 'react-refresh/babel'
].filter(Boolean)
}
}),

// ESLint插件
eslint({
include: ['src/**/*.ts', 'src/**/*.tsx'],
exclude: ['node_modules', 'dist'],
cache: false
}),

// HTML模板插件
createHtmlPlugin({
inject: {
data: {
title: env.VITE_APP_TITLE || 'React App',
description: env.VITE_APP_DESCRIPTION || 'A modern React application'
}
},
minify: isProduction
}),

// 代码分割插件
chunkSplitPlugin({
strategy: 'split-by-experience',
customSplitting: {
'react-vendor': ['react', 'react-dom'],
'router-vendor': ['react-router-dom'],
'ui-vendor': ['antd', '@ant-design/icons'],
'utils-vendor': ['lodash', 'dayjs', 'axios']
}
}),

// PWA插件
VitePWA({
registerType: 'autoUpdate',
includeAssets: ['favicon.ico', 'apple-touch-icon.png', 'masked-icon.svg'],
manifest: {
name: env.VITE_APP_TITLE || 'React App',
short_name: env.VITE_APP_SHORT_NAME || 'App',
description: env.VITE_APP_DESCRIPTION || 'A modern React application',
theme_color: '#ffffff',
background_color: '#ffffff',
display: 'standalone',
icons: [
{
src: 'pwa-192x192.png',
sizes: '192x192',
type: 'image/png'
},
{
src: 'pwa-512x512.png',
sizes: '512x512',
type: 'image/png'
}
]
},
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg}'],
runtimeCaching: [
{
urlPattern: /^https:\/\/api\.example\.com\/.*/i,
handler: 'CacheFirst',
options: {
cacheName: 'api-cache',
expiration: {
maxEntries: 10,
maxAgeSeconds: 60 * 60 * 24 * 365 // 1年
},
cacheableResponse: {
statuses: [0, 200]
}
}
}
]
}
}),

// 兼容性插件
legacy({
targets: ['defaults', 'not IE 11']
}),

// 打包分析插件
isProduction && visualizer({
filename: 'dist/stats.html',
open: true,
gzipSize: true,
brotliSize: true
})
].filter(Boolean),

resolve: {
alias: {
'@': resolve(__dirname, 'src'),
'@components': resolve(__dirname, 'src/components'),
'@utils': resolve(__dirname, 'src/utils'),
'@types': resolve(__dirname, 'src/types'),
'@hooks': resolve(__dirname, 'src/hooks'),
'@services': resolve(__dirname, 'src/services'),
'@store': resolve(__dirname, 'src/store'),
'@assets': resolve(__dirname, 'src/assets')
}
},

css: {
preprocessorOptions: {
scss: {
additionalData: `@import "@/styles/variables.scss";`
}
},
modules: {
localsConvention: 'camelCase',
generateScopedName: isProduction
? '[hash:base64:8]'
: '[name]__[local]___[hash:base64:5]'
},
postcss: {
plugins: [
require('autoprefixer'),
require('postcss-preset-env')({
stage: 3,
features: {
'nesting-rules': true
}
})
]
}
},

server: {
port: 3000,
open: true,
cors: true,
proxy: {
'/api': {
target: env.VITE_API_BASE_URL || 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
},

build: {
target: 'es2015',
outDir: 'dist',
assetsDir: 'assets',
sourcemap: isProduction ? 'hidden' : true,
minify: 'terser',
terserOptions: {
compress: {
drop_console: isProduction,
drop_debugger: isProduction
}
},
rollupOptions: {
output: {
chunkFileNames: 'js/[name]-[hash].js',
entryFileNames: 'js/[name]-[hash].js',
assetFileNames: (assetInfo) => {
const info = assetInfo.name.split('.');
const ext = info[info.length - 1];
if (/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/i.test(assetInfo.name)) {
return `media/[name]-[hash].${ext}`;
}
if (/\.(png|jpe?g|gif|svg)(\?.*)?$/.test(assetInfo.name)) {
return `images/[name]-[hash].${ext}`;
}
if (/\.(woff2?|eot|ttf|otf)(\?.*)?$/i.test(assetInfo.name)) {
return `fonts/[name]-[hash].${ext}`;
}
return `assets/[name]-[hash].${ext}`;
}
}
},
chunkSizeWarningLimit: 1000,
reportCompressedSize: false
},

optimizeDeps: {
include: [
'react',
'react-dom',
'react-router-dom',
'axios',
'lodash'
]
},

define: {
__DEV__: isDevelopment,
__PROD__: isProduction,
__VERSION__: JSON.stringify(process.env.npm_package_version)
}
};
});

🔧 代码质量与规范

1. 代码检查与格式化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
// .eslintrc.js - 完整的ESLint配置
module.exports = {
root: true,
env: {
browser: true,
es2021: true,
node: true,
jest: true
},
extends: [
'eslint:recommended',
'@typescript-eslint/recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:jsx-a11y/recommended',
'plugin:import/recommended',
'plugin:import/typescript',
'prettier'
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: {
jsx: true
},
ecmaVersion: 'latest',
sourceType: 'module',
project: './tsconfig.json'
},
plugins: [
'react',
'react-hooks',
'@typescript-eslint',
'jsx-a11y',
'import',
'prettier'
],
settings: {
react: {
version: 'detect'
},
'import/resolver': {
typescript: {
alwaysTryTypes: true,
project: './tsconfig.json'
}
}
},
rules: {
// TypeScript规则
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-non-null-assertion': 'warn',
'@typescript-eslint/prefer-const': 'error',
'@typescript-eslint/no-var-requires': 'error',

// React规则
'react/react-in-jsx-scope': 'off',
'react/prop-types': 'off',
'react/display-name': 'off',
'react/jsx-uses-react': 'off',
'react/jsx-uses-vars': 'error',
'react/jsx-key': 'error',
'react/jsx-no-duplicate-props': 'error',
'react/jsx-no-undef': 'error',
'react/no-children-prop': 'error',
'react/no-danger-with-children': 'error',
'react/no-deprecated': 'error',
'react/no-direct-mutation-state': 'error',
'react/no-find-dom-node': 'error',
'react/no-is-mounted': 'error',
'react/no-render-return-value': 'error',
'react/no-string-refs': 'error',
'react/no-unescaped-entities': 'error',
'react/no-unknown-property': 'error',
'react/require-render-return': 'error',

// React Hooks规则
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',

// 导入规则
'import/order': [
'error',
{
groups: [
'builtin',
'external',
'internal',
'parent',
'sibling',
'index'
],
'newlines-between': 'always',
alphabetize: {
order: 'asc',
caseInsensitive: true
}
}
],
'import/no-unresolved': 'error',
'import/no-cycle': 'error',
'import/no-self-import': 'error',
'import/no-duplicates': 'error',

// 通用规则
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
'no-alert': 'warn',
'no-var': 'error',
'prefer-const': 'error',
'no-unused-expressions': 'error',
'no-duplicate-imports': 'error',
'no-useless-return': 'error',
'eqeqeq': ['error', 'always'],
'curly': ['error', 'all'],
'brace-style': ['error', '1tbs'],
'comma-dangle': ['error', 'es5'],
'quotes': ['error', 'single', { avoidEscape: true }],
'semi': ['error', 'always'],
'indent': ['error', 2, { SwitchCase: 1 }],
'max-len': ['warn', { code: 100, ignoreUrls: true }],
'object-curly-spacing': ['error', 'always'],
'array-bracket-spacing': ['error', 'never']
},
overrides: [
{
files: ['**/*.test.ts', '**/*.test.tsx', '**/*.spec.ts', '**/*.spec.tsx'],
env: {
jest: true
},
rules: {
'@typescript-eslint/no-explicit-any': 'off'
}
}
]
};

2. 提交规范与自动化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// commitlint.config.js - 提交信息规范
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
[
'feat', // 新功能
'fix', // 修复bug
'docs', // 文档更新
'style', // 代码格式化
'refactor', // 重构
'perf', // 性能优化
'test', // 测试相关
'build', // 构建相关
'ci', // CI/CD相关
'chore', // 其他杂项
'revert' // 回滚
]
],
'type-case': [2, 'always', 'lower-case'],
'type-empty': [2, 'never'],
'scope-case': [2, 'always', 'lower-case'],
'subject-case': [2, 'never', ['sentence-case', 'start-case', 'pascal-case', 'upper-case']],
'subject-empty': [2, 'never'],
'subject-full-stop': [2, 'never', '.'],
'header-max-length': [2, 'always', 72],
'body-leading-blank': [1, 'always'],
'body-max-line-length': [2, 'always', 100],
'footer-leading-blank': [1, 'always'],
'footer-max-line-length': [2, 'always', 100]
}
};

// release.config.js - 自动发布配置
module.exports = {
branches: ['main', 'master'],
plugins: [
'@semantic-release/commit-analyzer',
'@semantic-release/release-notes-generator',
[
'@semantic-release/changelog',
{
changelogFile: 'CHANGELOG.md'
}
],
'@semantic-release/npm',
[
'@semantic-release/git',
{
assets: ['CHANGELOG.md', 'package.json'],
message: 'chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}'
}
],
'@semantic-release/github'
]
};

🧪 测试策略与实践

1. 单元测试与集成测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
// src/utils/__tests__/helpers.test.ts - 工具函数测试
import { formatDate, debounce, throttle, deepClone } from '../helpers';

describe('Helper Functions', () => {
describe('formatDate', () => {
it('should format date correctly', () => {
const date = new Date('2023-12-25T10:30:00Z');
expect(formatDate(date, 'YYYY-MM-DD')).toBe('2023-12-25');
expect(formatDate(date, 'MM/DD/YYYY')).toBe('12/25/2023');
});

it('should handle invalid date', () => {
expect(formatDate(null)).toBe('');
expect(formatDate(undefined)).toBe('');
});
});

describe('debounce', () => {
jest.useFakeTimers();

it('should debounce function calls', () => {
const mockFn = jest.fn();
const debouncedFn = debounce(mockFn, 1000);

debouncedFn();
debouncedFn();
debouncedFn();

expect(mockFn).not.toHaveBeenCalled();

jest.advanceTimersByTime(1000);

expect(mockFn).toHaveBeenCalledTimes(1);
});

it('should pass arguments correctly', () => {
const mockFn = jest.fn();
const debouncedFn = debounce(mockFn, 1000);

debouncedFn('arg1', 'arg2');

jest.advanceTimersByTime(1000);

expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2');
});
});

describe('deepClone', () => {
it('should clone primitive values', () => {
expect(deepClone(42)).toBe(42);
expect(deepClone('hello')).toBe('hello');
expect(deepClone(true)).toBe(true);
expect(deepClone(null)).toBe(null);
});

it('should clone objects deeply', () => {
const original = {
name: 'John',
age: 30,
address: {
city: 'New York',
country: 'USA'
},
hobbies: ['reading', 'swimming']
};

const cloned = deepClone(original);

expect(cloned).toEqual(original);
expect(cloned).not.toBe(original);
expect(cloned.address).not.toBe(original.address);
expect(cloned.hobbies).not.toBe(original.hobbies);
});

it('should handle circular references', () => {
const obj: any = { name: 'test' };
obj.self = obj;

const cloned = deepClone(obj);

expect(cloned.name).toBe('test');
expect(cloned.self).toBe(cloned);
});
});
});

// src/components/__tests__/Button.test.tsx - 组件测试
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Button } from '../Button';

describe('Button Component', () => {
it('should render with correct text', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument();
});

it('should handle click events', async () => {
const handleClick = jest.fn();
const user = userEvent.setup();

render(<Button onClick={handleClick}>Click me</Button>);

await user.click(screen.getByRole('button'));

expect(handleClick).toHaveBeenCalledTimes(1);
});

it('should be disabled when loading', () => {
render(<Button loading>Loading</Button>);

const button = screen.getByRole('button');
expect(button).toBeDisabled();
expect(screen.getByText(/loading/i)).toBeInTheDocument();
});

it('should apply correct variant styles', () => {
const { rerender } = render(<Button variant="primary">Primary</Button>);

let button = screen.getByRole('button');
expect(button).toHaveClass('btn-primary');

rerender(<Button variant="secondary">Secondary</Button>);

button = screen.getByRole('button');
expect(button).toHaveClass('btn-secondary');
});

it('should support different sizes', () => {
const { rerender } = render(<Button size="small">Small</Button>);

let button = screen.getByRole('button');
expect(button).toHaveClass('btn-small');

rerender(<Button size="large">Large</Button>);

button = screen.getByRole('button');
expect(button).toHaveClass('btn-large');
});

it('should show loading spinner when loading', () => {
render(<Button loading>Submit</Button>);

expect(screen.getByTestId('loading-spinner')).toBeInTheDocument();
});

it('should prevent multiple clicks when loading', async () => {
const handleClick = jest.fn();
const user = userEvent.setup();

render(<Button loading onClick={handleClick}>Submit</Button>);

const button = screen.getByRole('button');

await user.click(button);
await user.click(button);

expect(handleClick).not.toHaveBeenCalled();
});
});

// src/hooks/__tests__/useApi.test.ts - 自定义Hook测试
import { renderHook, waitFor } from '@testing-library/react';
import { useApi } from '../useApi';
import * as api from '../../services/api';

jest.mock('../../services/api');

const mockApi = api as jest.Mocked<typeof api>;

describe('useApi Hook', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('should fetch data successfully', async () => {
const mockData = { id: 1, name: 'Test User' };
mockApi.getUser.mockResolvedValue(mockData);

const { result } = renderHook(() => useApi(() => api.getUser(1)));

expect(result.current.loading).toBe(true);
expect(result.current.data).toBeNull();
expect(result.current.error).toBeNull();

await waitFor(() => {
expect(result.current.loading).toBe(false);
});

expect(result.current.data).toEqual(mockData);
expect(result.current.error).toBeNull();
expect(mockApi.getUser).toHaveBeenCalledWith(1);
});

it('should handle errors', async () => {
const mockError = new Error('API Error');
mockApi.getUser.mockRejectedValue(mockError);

const { result } = renderHook(() => useApi(() => api.getUser(1)));

await waitFor(() => {
expect(result.current.loading).toBe(false);
});

expect(result.current.data).toBeNull();
expect(result.current.error).toEqual(mockError);
});

it('should refetch data', async () => {
const mockData = { id: 1, name: 'Test User' };
mockApi.getUser.mockResolvedValue(mockData);

const { result } = renderHook(() => useApi(() => api.getUser(1)));

await waitFor(() => {
expect(result.current.loading).toBe(false);
});

expect(mockApi.getUser).toHaveBeenCalledTimes(1);

// Refetch
result.current.refetch();

await waitFor(() => {
expect(result.current.loading).toBe(false);
});

expect(mockApi.getUser).toHaveBeenCalledTimes(2);
});
});

2. E2E测试配置

// e2e/playwright.config.ts - Playwright配置
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
    testDir: './tests',
    fullyParallel: true,
    forbidOnly: !!process.env.CI,
    retries: process.env.CI ? 2 : 0,
    workers: process.env.CI ? 1 : undefined,
    reporter: [
        ['html'],
        ['json', { outputFile: 'test-results/results.json' }],
        ['junit', { outputFile: 'test-results/results.xml' }]
    ],
    use: {
        baseURL: 'http://localhost:3000',
        trace: 'on-first-retry',
        screenshot: 'only-on-failure',
        video: 'retain-on-failure'
    },

    projects: [
        {
            name: 'chromium',
            use: { ...devices['Desktop Chrome'] }
        },
        {
            name: 'firefox',
            use: { ...devices['Desktop Firefox'] }
        },
        {
            name: 'webkit',
            use: { ...devices['Desktop Safari'] }
        },
        {
            name: 'Mobile Chrome',
            use: { ...devices['Pixel 5'] }
        },
        {
            name: 'Mobile Safari',
            use: { ...devices['iPhone 12'] }
        }
    ],

    webServer: {
        command: 'npm run dev',
        port: 3000,
        reuseExistingServer: !process.env.CI
    }
});

// e2e/tests/user-flow.spec.ts - E2E测试用例
import { test, expect } from '@playwright/test';

test.describe('User Authentication Flow', () => {
    test.beforeEach(async ({ page }) => {
        await page.goto('/');
    });

    test('should login successfully', async ({ page }) => {
        // 点击登录按钮
        await page.click('[data-testid="login-button"]');

        // 填写登录表单
        await page.fill('[data-testid="email-input"]', 'test@example.com');
        await page.fill('[data-testid="password-input"]', 'password123');

        // 提交表单
        await page.click('[data-testid="submit-button"]');

        // 验证登录成功
        await expect(page.locator('[data-testid="user-menu"]')).toBeVisible();
        await expect(page.locator('[data-testid="welcome-message"]')).toContainText('Welcome');
    });

    test('should show error for invalid credentials', async ({ page }) => {
        await page.click('[data-testid="login-button"]');

        await page.fill('[data-testid="email-input"]', 'invalid@example.com');
        await page.fill('[data-testid="password-input"]', 'wrongpassword');

        await page.click('[data-testid="submit-button"]');

        await expect(page.locator('[data-testid="error-message"]')).toBeVisible();
        await expect(page.locator('[data-testid="error-message"]')).toContainText('Invalid credentials');
    });

    test('should logout successfully', async ({ page }) => {
        // 先登录
        await page.click('[data-testid="login-button"]');
        await page.fill('[data-testid="email-input"]', 'test@example.com');
        await page.fill('[data-testid="password-input"]', 'password123');
        await page.click('[data-testid="submit-button"]');

        // 等待登录完成
        await expect(page.locator('[data-testid="user-menu"]')).toBeVisible();

        // 点击用户菜单
        await page.click('[data-testid="user-menu"]');

        // 点击登出
        await page.click('[data-testid="logout-button"]');

        // 验证登出成功
        await expect(page.locator('[data-testid="login-button"]')).toBeVisible();
    });
});

test.describe('Product Management', () => {
    test.beforeEach(async ({ page }) => {
        // 登录
        await page.goto('/login');
        await page.fill('[data-testid="email-input"]', 'admin@example.com');
        await page.fill('[data-testid="password-input"]', 'admin123');
        await page.click('[data-testid="submit-button"]');
        
        await expect(page.locator('[data-testid="user-menu"]')).toBeVisible();
        
        //