引言:为什么需要一份实战指南?
在当今快速发展的Web开发领域,前端技术栈日新月异。从传统的jQuery到现代的React、Vue、Angular,再到新兴的Svelte、SolidJS,开发者面临着前所未有的选择。然而,技术选型只是第一步,如何从零开始构建一个高效、可维护的现代Web应用,才是真正的挑战。
本指南将带你从项目初始化开始,逐步构建一个完整的前端项目,涵盖技术选型、架构设计、开发流程、性能优化、测试部署等关键环节。我们将以React + TypeScript + Vite为核心技术栈,结合现代前端工程化实践,为你提供一套完整的解决方案。
第一章:项目初始化与技术选型
1.1 技术栈选择
在开始项目之前,我们需要明确技术选型。对于现代Web应用,我们推荐以下技术栈:
- 框架: React 18 + TypeScript
- 构建工具: Vite (替代Webpack)
- 状态管理: Zustand (轻量级替代Redux)
- 路由: React Router v6
- UI组件库: Ant Design 或 Material-UI (根据项目需求选择)
- 样式方案: CSS Modules + Tailwind CSS
- 测试: Jest + React Testing Library
- 代码规范: ESLint + Prettier + Husky
- 构建部署: Vercel/Netlify (CI/CD)
1.2 项目初始化
使用Vite创建React + TypeScript项目:
# 使用npm
npm create vite@latest my-app -- --template react-ts
# 或使用yarn
yarn create vite my-app --template react-ts
# 进入项目目录
cd my-app
# 安装依赖
npm install
1.3 项目结构设计
一个良好的项目结构是可维护性的基础。推荐以下结构:
src/
├── assets/ # 静态资源
├── components/ # 通用组件
│ ├── common/ # 基础组件
│ └── business/ # 业务组件
├── hooks/ # 自定义Hook
├── pages/ # 页面组件
├── services/ # API服务
├── store/ # 状态管理
├── types/ # TypeScript类型定义
├── utils/ # 工具函数
├── App.tsx # 应用入口
└── main.tsx # 主入口文件
1.4 配置开发环境
1.4.1 配置ESLint和Prettier
安装依赖:
npm install --save-dev eslint prettier eslint-config-prettier eslint-plugin-react eslint-plugin-react-hooks @typescript-eslint/parser @typescript-eslint/eslint-plugin
创建.eslintrc.js:
module.exports = {
env: {
browser: true,
es2021: true,
},
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
'prettier',
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 'latest',
sourceType: 'module',
},
plugins: ['react', '@typescript-eslint', 'react-hooks'],
rules: {
'react/react-in-jsx-scope': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'react/prop-types': 'off',
},
settings: {
react: {
version: 'detect',
},
},
};
创建.prettierrc:
{
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 100,
"tabWidth": 2,
"endOfLine": "lf"
}
1.4.2 配置Husky和lint-staged
安装依赖:
npm install --save-dev husky lint-staged
初始化Husky:
npx husky install
添加pre-commit钩子:
npx husky add .husky/pre-commit "npx lint-staged"
在package.json中添加:
{
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint --fix",
"prettier --write"
]
}
}
第二章:核心架构设计
2.1 状态管理方案
对于中小型项目,Zustand是比Redux更轻量的选择。安装Zustand:
npm install zustand
创建一个简单的计数器Store:
// src/store/counterStore.ts
import { create } from 'zustand';
interface CounterState {
count: number;
increment: () => void;
decrement: () => void;
reset: () => void;
}
export const useCounterStore = create<CounterState>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}));
在组件中使用:
// src/pages/CounterPage.tsx
import React from 'react';
import { useCounterStore } from '../store/counterStore';
const CounterPage: React.FC = () => {
const { count, increment, decrement, reset } = useCounterStore();
return (
<div className="counter-container">
<h2>Counter: {count}</h2>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={reset}>Reset</button>
</div>
);
};
export default CounterPage;
2.2 路由管理
使用React Router v6进行路由管理:
npm install react-router-dom
配置路由:
// src/App.tsx
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import HomePage from './pages/HomePage';
import CounterPage from './pages/CounterPage';
import Layout from './components/Layout';
function App() {
return (
<Router>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<HomePage />} />
<Route path="counter" element={<CounterPage />} />
</Route>
</Routes>
</Router>
);
}
export default App;
2.3 API服务封装
创建统一的API服务层,便于管理和维护:
// src/services/api.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
// 创建axios实例
const api: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
// 请求拦截器
api.interceptors.request.use(
(config) => {
// 添加认证token
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 响应拦截器
api.interceptors.response.use(
(response) => {
// 处理响应数据
return response;
},
(error) => {
// 处理错误
if (error.response) {
const { status, data } = error.response;
switch (status) {
case 401:
// 未授权,跳转登录页
window.location.href = '/login';
break;
case 403:
// 无权限
console.error('无权限访问');
break;
case 404:
// 资源不存在
console.error('资源不存在');
break;
case 500:
// 服务器错误
console.error('服务器错误');
break;
default:
console.error('请求失败');
}
}
return Promise.reject(error);
}
);
// 封装通用请求方法
export const request = async <T>(
config: AxiosRequestConfig
): Promise<AxiosResponse<T>> => {
try {
const response = await api.request<T>(config);
return response;
} catch (error) {
throw error;
}
};
// 具体API接口
export const userAPI = {
login: (data: { username: string; password: string }) =>
request<{ token: string }>({
method: 'POST',
url: '/auth/login',
data,
}),
getUserInfo: () =>
request<{ id: string; name: string; email: string }>({
method: 'GET',
url: '/user/info',
}),
updateUser: (data: Partial<{ name: string; email: string }>) =>
request({
method: 'PUT',
url: '/user/update',
data,
}),
};
export const productAPI = {
getProducts: (params?: { page?: number; size?: number; category?: string }) =>
request<{ data: any[]; total: number }>({
method: 'GET',
url: '/products',
params,
}),
getProductDetail: (id: string) =>
request<{ id: string; name: string; price: number }>({
method: 'GET',
url: `/products/${id}`,
}),
};
2.4 自定义Hook封装
创建可复用的自定义Hook:
// src/hooks/useFetch.ts
import { useState, useEffect } from 'react';
interface FetchState<T> {
data: T | null;
loading: boolean;
error: Error | null;
}
export function useFetch<T>(url: string, options?: RequestInit): FetchState<T> {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err as Error);
} finally {
setLoading(false);
}
};
fetchData();
}, [url, JSON.stringify(options)]);
return { data, loading, error };
}
// 使用示例
// const { data, loading, error } = useFetch<User[]>('/api/users');
第三章:组件开发最佳实践
3.1 组件设计原则
3.1.1 单一职责原则
每个组件应该只做一件事。例如,一个按钮组件应该只负责渲染按钮,而不应该包含业务逻辑。
// src/components/common/Button.tsx
import React from 'react';
import './Button.css';
interface ButtonProps {
children: React.ReactNode;
onClick?: () => void;
variant?: 'primary' | 'secondary' | 'danger';
size?: 'small' | 'medium' | 'large';
disabled?: boolean;
type?: 'button' | 'submit' | 'reset';
}
const Button: React.FC<ButtonProps> = ({
children,
onClick,
variant = 'primary',
size = 'medium',
disabled = false,
type = 'button',
}) => {
const baseClasses = 'btn';
const variantClasses = {
primary: 'btn-primary',
secondary: 'btn-secondary',
danger: 'btn-danger',
};
const sizeClasses = {
small: 'btn-sm',
medium: 'btn-md',
large: 'btn-lg',
};
return (
<button
className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]}`}
onClick={onClick}
disabled={disabled}
type={type}
>
{children}
</button>
);
};
export default Button;
3.1.2 受控组件与非受控组件
在表单处理中,正确使用受控和非受控组件:
// 受控组件示例
import React, { useState } from 'react';
const ControlledForm: React.FC = () => {
const [formData, setFormData] = useState({
name: '',
email: '',
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log('提交数据:', formData);
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
placeholder="姓名"
/>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
placeholder="邮箱"
/>
<button type="submit">提交</button>
</form>
);
};
// 非受控组件示例
import React, { useRef } from 'react';
const UncontrolledForm: React.FC = () => {
const nameRef = useRef<HTMLInputElement>(null);
const emailRef = useRef<HTMLInputElement>(null);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const name = nameRef.current?.value || '';
const email = emailRef.current?.value || '';
console.log('提交数据:', { name, email });
};
return (
<form onSubmit={handleSubmit}>
<input ref={nameRef} type="text" placeholder="姓名" />
<input ref={emailRef} type="email" placeholder="邮箱" />
<button type="submit">提交</button>
</form>
);
};
3.2 组件通信
3.2.1 Props传递
// 父组件
import React from 'react';
import UserProfile from './UserProfile';
const ParentComponent: React.FC = () => {
const user = {
id: 1,
name: '张三',
email: 'zhangsan@example.com',
};
return (
<div>
<UserProfile user={user} />
</div>
);
};
// 子组件
import React from 'react';
interface User {
id: number;
name: string;
email: string;
}
interface UserProfileProps {
user: User;
}
const UserProfile: React.FC<UserProfileProps> = ({ user }) => {
return (
<div>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
};
3.2.2 Context API
对于跨层级的组件通信,使用Context API:
// src/contexts/ThemeContext.tsx
import React, { createContext, useContext, useState, ReactNode } from 'react';
type Theme = 'light' | 'dark';
interface ThemeContextType {
theme: Theme;
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export const ThemeProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [theme, setTheme] = useState<Theme>('light');
const toggleTheme = () => {
setTheme((prev) => (prev === 'light' ? 'dark' : 'light'));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
export const useTheme = () => {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
};
// 使用示例
// 在App.tsx中包裹组件
// <ThemeProvider>
// <App />
// </ThemeProvider>
// 在任意组件中使用
// const { theme, toggleTheme } = useTheme();
3.3 组件性能优化
3.3.1 React.memo和useMemo
import React, { memo, useMemo, useState } from 'react';
// 使用React.memo避免不必要的重新渲染
const ExpensiveComponent: React.FC<{ data: string[] }> = memo(({ data }) => {
console.log('ExpensiveComponent渲染');
return (
<div>
{data.map((item, index) => (
<div key={index}>{item}</div>
))}
</div>
);
});
// 使用useMemo缓存计算结果
const ParentComponent: React.FC = () => {
const [count, setCount] = useState(0);
const [items, setItems] = useState(['item1', 'item2', 'item3']);
// 缓存计算结果,只有items变化时才重新计算
const expensiveCalculation = useMemo(() => {
console.log('执行昂贵计算');
return items.map(item => item.toUpperCase());
}, [items]);
return (
<div>
<button onClick={() => setCount(count + 1)}>
重新渲染父组件: {count}
</button>
<ExpensiveComponent data={expensiveCalculation} />
</div>
);
};
3.3.2 useCallback
import React, { useCallback, useState } from 'react';
const ChildComponent: React.FC<{ onClick: () => void }> = ({ onClick }) => {
console.log('ChildComponent渲染');
return <button onClick={onClick}>点击我</button>;
};
const ParentComponent: React.FC = () => {
const [count, setCount] = useState(0);
// 使用useCallback缓存函数引用
const handleClick = useCallback(() => {
console.log('按钮被点击');
setCount(prev => prev + 1);
}, []); // 空依赖数组,函数不会重新创建
return (
<div>
<p>计数: {count}</p>
<ChildComponent onClick={handleClick} />
</div>
);
};
第四章:状态管理进阶
4.1 复杂状态管理
当应用状态变得复杂时,需要更结构化的状态管理:
// src/store/userStore.ts
import { create } from 'zustand';
import { userAPI } from '../services/api';
interface User {
id: string;
name: string;
email: string;
avatar?: string;
}
interface UserState {
user: User | null;
loading: boolean;
error: string | null;
fetchUser: () => Promise<void>;
updateUser: (data: Partial<User>) => Promise<void>;
logout: () => void;
}
export const useUserStore = create<UserState>((set, get) => ({
user: null,
loading: false,
error: null,
fetchUser: async () => {
set({ loading: true, error: null });
try {
const response = await userAPI.getUserInfo();
set({ user: response.data, loading: false });
} catch (error) {
set({ error: '获取用户信息失败', loading: false });
}
},
updateUser: async (data) => {
set({ loading: true, error: null });
try {
await userAPI.updateUser(data);
const response = await userAPI.getUserInfo();
set({ user: response.data, loading: false });
} catch (error) {
set({ error: '更新用户信息失败', loading: false });
}
},
logout: () => {
localStorage.removeItem('token');
set({ user: null });
window.location.href = '/login';
},
}));
4.2 状态持久化
使用zustand/middleware实现状态持久化:
npm install zustand/middleware
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
interface CounterState {
count: number;
increment: () => void;
decrement: () => void;
}
export const useCounterStore = create(
persist<CounterState>(
(set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}),
{
name: 'counter-storage', // 存储在localStorage中的key
storage: createJSONStorage(() => localStorage), // 使用localStorage
partialize: (state) => ({ count: state.count }), // 只持久化count
}
)
);
第五章:样式与主题管理
5.1 CSS Modules
CSS Modules提供局部作用域,避免样式冲突:
// src/components/Button/Button.module.css
.button {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
.primary {
background-color: #1890ff;
color: white;
}
.primary:hover {
background-color: #40a9ff;
}
.secondary {
background-color: #f0f0f0;
color: #333;
}
.secondary:hover {
background-color: #e0e0e0;
}
.disabled {
opacity: 0.6;
cursor: not-allowed;
}
// src/components/Button/Button.tsx
import React from 'react';
import styles from './Button.module.css';
interface ButtonProps {
children: React.ReactNode;
onClick?: () => void;
variant?: 'primary' | 'secondary';
disabled?: boolean;
}
const Button: React.FC<ButtonProps> = ({
children,
onClick,
variant = 'primary',
disabled = false,
}) => {
const buttonClasses = [
styles.button,
styles[variant],
disabled ? styles.disabled : '',
].join(' ');
return (
<button className={buttonClasses} onClick={onClick} disabled={disabled}>
{children}
</button>
);
};
export default Button;
5.2 主题管理
使用CSS变量实现主题切换:
// src/styles/theme.css
:root {
--primary-color: #1890ff;
--secondary-color: #f0f0f0;
--text-color: #333;
--background-color: #fff;
--border-color: #d9d9d9;
--shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
[data-theme="dark"] {
--primary-color: #177ddc;
--secondary-color: #1f1f1f;
--text-color: #f0f0f0;
--background-color: #141414;
--border-color: #434343;
--shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
// src/components/ThemeToggle/ThemeToggle.tsx
import React, { useEffect } from 'react';
import { useTheme } from '../contexts/ThemeContext';
const ThemeToggle: React.FC = () => {
const { theme, toggleTheme } = useTheme();
useEffect(() => {
document.documentElement.setAttribute('data-theme', theme);
}, [theme]);
return (
<button onClick={toggleTheme}>
{theme === 'light' ? '🌙' : '☀️'}
</button>
);
};
export default ThemeToggle;
第六章:测试策略
6.1 单元测试
使用Jest和React Testing Library进行单元测试:
npm install --save-dev jest @testing-library/react @testing-library/jest-dom @testing-library/user-event ts-jest @types/jest
配置Jest:
// jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
moduleNameMapper: {
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
},
transform: {
'^.+\\.(ts|tsx)$': 'ts-jest',
},
};
创建测试文件:
// src/components/Button/Button.test.tsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import Button from './Button';
describe('Button Component', () => {
test('renders children correctly', () => {
render(<Button>Click me</Button>);
expect(screen.getByText('Click me')).toBeInTheDocument();
});
test('calls onClick when clicked', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click me</Button>);
fireEvent.click(screen.getByText('Click me'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
test('is disabled when disabled prop is true', () => {
render(<Button disabled>Click me</Button>);
const button = screen.getByText('Click me');
expect(button).toBeDisabled();
});
});
6.2 集成测试
// src/pages/CounterPage.test.tsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import CounterPage from './CounterPage';
import { useCounterStore } from '../store/counterStore';
// Mock the store
jest.mock('../store/counterStore', () => ({
useCounterStore: jest.fn(),
}));
describe('CounterPage', () => {
const mockIncrement = jest.fn();
const mockDecrement = jest.fn();
const mockReset = jest.fn();
beforeEach(() => {
(useCounterStore as jest.Mock).mockReturnValue({
count: 0,
increment: mockIncrement,
decrement: mockDecrement,
reset: mockReset,
});
});
test('renders counter value', () => {
render(<CounterPage />);
expect(screen.getByText('Counter: 0')).toBeInTheDocument();
});
test('calls increment when + button is clicked', () => {
render(<CounterPage />);
fireEvent.click(screen.getByText('+'));
expect(mockIncrement).toHaveBeenCalled();
});
test('calls decrement when - button is clicked', () => {
render(<CounterPage />);
fireEvent.click(screen.getByText('-'));
expect(mockDecrement).toHaveBeenCalled();
});
test('calls reset when Reset button is clicked', () => {
render(<CounterPage />);
fireEvent.click(screen.getByText('Reset'));
expect(mockReset).toHaveBeenCalled();
});
});
第七章:性能优化
7.1 代码分割
使用React.lazy和Suspense实现代码分割:
// src/App.tsx
import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
// 懒加载页面组件
const HomePage = lazy(() => import('./pages/HomePage'));
const DashboardPage = lazy(() => import('./pages/DashboardPage'));
const SettingsPage = lazy(() => import('./pages/SettingsPage'));
// 加载中组件
const Loading: React.FC = () => (
<div className="loading">加载中...</div>
);
function App() {
return (
<Router>
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/dashboard" element={<DashboardPage />} />
<Route path="/settings" element={<SettingsPage />} />
</Routes>
</Suspense>
</Router>
);
}
export default App;
7.2 图片优化
// src/components/Image/Image.tsx
import React, { useState, useEffect } from 'react';
interface ImageProps {
src: string;
alt: string;
width?: number;
height?: number;
lazy?: boolean;
placeholder?: string;
}
const Image: React.FC<ImageProps> = ({
src,
alt,
width,
height,
lazy = true,
placeholder = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiB2aWV3Qm94PSIwIDAgMTAwIDEwMCI+PHJlY3Qgd2lkdGg9IjEwMCIgaGVpZ2h0PSIxMDAiIGZpbGw9IiNmMGYwZjAiLz48L3N2Zz4=',
}) => {
const [imageSrc, setImageSrc] = useState<string>(placeholder);
const [isLoaded, setIsLoaded] = useState<boolean>(false);
useEffect(() => {
if (!lazy) {
setImageSrc(src);
return;
}
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = new Image();
img.src = src;
img.onload = () => {
setImageSrc(src);
setIsLoaded(true);
};
observer.disconnect();
}
});
},
{ rootMargin: '50px' }
);
const imgElement = document.createElement('img');
imgElement.style.display = 'none';
document.body.appendChild(imgElement);
observer.observe(imgElement);
return () => {
observer.disconnect();
document.body.removeChild(imgElement);
};
}, [src, lazy]);
return (
<img
src={imageSrc}
alt={alt}
width={width}
height={height}
style={{ opacity: isLoaded ? 1 : 0.5, transition: 'opacity 0.3s' }}
/>
);
};
export default Image;
7.3 虚拟列表
对于长列表渲染,使用虚拟滚动:
npm install react-window
// src/components/VirtualList/VirtualList.tsx
import React from 'react';
import { FixedSizeList as List } from 'react-window';
interface VirtualListProps {
items: any[];
itemHeight: number;
width?: number;
height?: number;
}
const VirtualList: React.FC<VirtualListProps> = ({
items,
itemHeight,
width = '100%',
height = 400,
}) => {
const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (
<div style={style}>
<div style={{ padding: '10px', borderBottom: '1px solid #eee' }}>
{items[index].name}
</div>
</div>
);
return (
<List
height={height}
itemCount={items.length}
itemSize={itemHeight}
width={width}
>
{Row}
</List>
);
};
export default VirtualList;
第八章:构建与部署
8.1 构建配置
Vite的构建配置:
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': resolve(__dirname, './src'),
},
},
build: {
outDir: 'dist',
sourcemap: true,
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom', 'react-router-dom'],
ui: ['antd', '@ant-design/icons'],
charts: ['echarts', 'recharts'],
},
},
},
},
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
});
8.2 环境变量管理
创建环境变量文件:
# .env.development
VITE_API_BASE_URL=http://localhost:8080/api
VITE_APP_NAME=MyApp-Dev
# .env.production
VITE_API_BASE_URL=https://api.myapp.com/api
VITE_APP_NAME=MyApp
在代码中使用:
// src/services/api.ts
const api: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10000,
});
8.3 CI/CD配置
8.3.1 GitHub Actions配置
# .github/workflows/deploy.yml
name: Deploy to Vercel
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Build
run: npm run build
- name: Deploy to Vercel
uses: amondnet/vercel-action@v20
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
vercel-args: '--prod'
8.3.2 Docker配置
# Dockerfile
FROM node:18-alpine as builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# 生产阶段
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
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 application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
# 单页应用路由支持
location / {
try_files $uri $uri/ /index.html;
}
# API代理
location /api/ {
proxy_pass http://backend:8080/;
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;
}
}
第九章:错误处理与监控
9.1 全局错误处理
// src/utils/errorHandler.ts
import React from 'react';
// 全局错误边界组件
class ErrorBoundary extends React.Component<
{ children: React.ReactNode },
{ hasError: boolean; error: Error | null }
> {
constructor(props: { children: React.ReactNode }) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
// 发送到错误监控服务
this.reportError(error, errorInfo);
}
reportError(error: Error, errorInfo: React.ErrorInfo) {
// 这里可以集成Sentry、LogRocket等错误监控服务
const errorData = {
message: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
};
// 发送到后端
fetch('/api/error-report', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(errorData),
}).catch(() => {
// 如果发送失败,可以存储到本地
const errors = JSON.parse(localStorage.getItem('errorLogs') || '[]');
errors.push(errorData);
localStorage.setItem('errorLogs', JSON.stringify(errors.slice(-50)));
});
}
render() {
if (this.state.hasError) {
return (
<div className="error-fallback">
<h2>抱歉,发生了错误</h2>
<p>{this.state.error?.message}</p>
<button onClick={() => this.setState({ hasError: false, error: null })}>
重试
</button>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;
9.2 API错误处理
// src/services/api.ts (扩展)
import axios, { AxiosError } from 'axios';
// 错误类型定义
interface ApiError {
code: number;
message: string;
details?: any;
}
// 错误处理函数
export const handleApiError = (error: AxiosError<ApiError>) => {
if (error.response) {
const { status, data } = error.response;
switch (status) {
case 400:
return { message: data.message || '请求参数错误' };
case 401:
// 未授权,清除token并跳转登录页
localStorage.removeItem('token');
window.location.href = '/login';
return { message: '请重新登录' };
case 403:
return { message: '无权限访问' };
case 404:
return { message: '资源不存在' };
case 429:
return { message: '请求过于频繁,请稍后重试' };
case 500:
return { message: '服务器内部错误' };
default:
return { message: '请求失败' };
}
} else if (error.request) {
return { message: '网络连接失败,请检查网络' };
} else {
return { message: '请求配置错误' };
}
};
// 在API请求中使用
export const request = async <T>(
config: AxiosRequestConfig
): Promise<AxiosResponse<T>> => {
try {
const response = await api.request<T>(config);
return response;
} catch (error) {
const apiError = handleApiError(error as AxiosError<ApiError>);
// 可以在这里显示错误提示
showNotification(apiError.message, 'error');
throw error;
}
};
第十章:性能监控与优化
10.1 性能指标监控
// src/utils/performanceMonitor.ts
interface PerformanceMetrics {
fcp: number; // First Contentful Paint
lcp: number; // Largest Contentful Paint
cls: number; // Cumulative Layout Shift
fid: number; // First Input Delay
ttfb: number; // Time to First Byte
loadTime: number;
}
class PerformanceMonitor {
private metrics: Partial<PerformanceMetrics> = {};
private observer: PerformanceObserver | null = null;
constructor() {
this.initPerformanceObserver();
this.initNavigationTiming();
}
private initPerformanceObserver() {
if ('PerformanceObserver' in window) {
// 监控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'] });
// 监控CLS
const clsObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach((entry) => {
if (!entry.hadRecentInput) {
this.metrics.cls = (this.metrics.cls || 0) + entry.value;
}
});
});
clsObserver.observe({ entryTypes: ['layout-shift'] });
// 监控FID
const fidObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach((entry) => {
this.metrics.fid = entry.processingStart - entry.startTime;
});
});
fidObserver.observe({ entryTypes: ['first-input'] });
}
}
private initNavigationTiming() {
if ('performance' in window) {
const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
if (navigation) {
this.metrics.ttfb = navigation.responseStart - navigation.requestStart;
this.metrics.loadTime = navigation.loadEventEnd - navigation.startTime;
// 监控FCP
const paintEntries = performance.getEntriesByType('paint');
const fcpEntry = paintEntries.find(entry => entry.name === 'first-contentful-paint');
if (fcpEntry) {
this.metrics.fcp = fcpEntry.startTime;
}
}
}
}
public getMetrics(): PerformanceMetrics {
return {
fcp: this.metrics.fcp || 0,
lcp: this.metrics.lcp || 0,
cls: this.metrics.cls || 0,
fid: this.metrics.fid || 0,
ttfb: this.metrics.ttfb || 0,
loadTime: this.metrics.loadTime || 0,
};
}
public reportMetrics() {
const metrics = this.getMetrics();
// 发送到监控服务
fetch('/api/performance', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...metrics,
url: window.location.href,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
}),
}).catch(() => {
// 失败时存储到本地
const reports = JSON.parse(localStorage.getItem('perfReports') || '[]');
reports.push({ ...metrics, timestamp: new Date().toISOString() });
localStorage.setItem('perfReports', JSON.stringify(reports.slice(-10)));
});
}
}
// 使用示例
const monitor = new PerformanceMonitor();
// 页面加载完成后报告性能指标
window.addEventListener('load', () => {
setTimeout(() => {
monitor.reportMetrics();
}, 1000);
});
10.2 优化建议
根据性能指标,我们可以采取以下优化措施:
LCP优化:
- 优化图片和视频资源
- 使用CDN加速静态资源
- 实现懒加载
- 减少阻塞渲染的JavaScript
CLS优化:
- 为图片和视频设置明确的尺寸
- 避免在现有内容上方插入内容
- 使用transform动画而不是top/left动画
FID优化:
- 减少JavaScript执行时间
- 使用Web Workers处理复杂计算
- 代码分割和懒加载
第十一章:安全最佳实践
11.1 XSS防护
// src/utils/security.ts
import DOMPurify from 'dompurify';
// HTML净化函数
export const sanitizeHTML = (html: string): string => {
return DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'span'],
ALLOWED_ATTR: ['href', 'title', 'class'],
ALLOW_DATA_ATTR: false,
FORBID_TAGS: ['script', 'style', 'iframe', 'object', 'embed'],
FORBID_ATTR: ['onerror', 'onclick', 'onload', 'onmouseover'],
});
};
// 安全的URL验证
export const isValidUrl = (url: string): boolean => {
try {
const parsed = new URL(url);
const allowedProtocols = ['http:', 'https:'];
return allowedProtocols.includes(parsed.protocol);
} catch {
return false;
}
};
// 安全的JSON解析
export const safeJSONParse = <T>(str: string): T | null => {
try {
return JSON.parse(str);
} catch {
return null;
}
};
11.2 CSRF防护
// src/utils/csrf.ts
// 生成CSRF Token
export const generateCSRFToken = (): string => {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
};
// 存储CSRF Token
export const storeCSRFToken = (token: string): void => {
localStorage.setItem('csrf_token', token);
};
// 获取CSRF Token
export const getCSRFToken = (): string | null => {
return localStorage.getItem('csrf_token');
};
// 在API请求中添加CSRF Token
export const addCSRFToken = (config: RequestInit): RequestInit => {
const token = getCSRFToken();
if (token) {
return {
...config,
headers: {
...config.headers,
'X-CSRF-Token': token,
},
};
}
return config;
};
11.3 敏感信息保护
// src/utils/sensitiveData.ts
// 敏感数据脱敏
export const maskSensitiveData = (data: string, type: 'phone' | 'id' | 'email'): string => {
switch (type) {
case 'phone':
// 手机号脱敏:138****1234
return data.replace(/^(\d{3})\d{4}(\d{4})$/, '$1****$2');
case 'id':
// 身份证脱敏:110101****1234
return data.replace(/^(\d{6})\d{8}(\d{4})$/, '$1****$2');
case 'email':
// 邮箱脱敏:a***@example.com
const [local, domain] = data.split('@');
if (local.length > 3) {
return `${local[0]}***@${domain}`;
}
return data;
default:
return data;
}
};
// 安全的本地存储
export const secureLocalStorage = {
setItem: (key: string, value: any): void => {
const encrypted = btoa(JSON.stringify(value));
localStorage.setItem(key, encrypted);
},
getItem: <T>(key: string): T | null => {
const encrypted = localStorage.getItem(key);
if (!encrypted) return null;
try {
return JSON.parse(atob(encrypted));
} catch {
return null;
}
},
removeItem: (key: string): void => {
localStorage.removeItem(key);
},
};
第十二章:移动端适配
12.1 响应式设计
/* src/styles/responsive.css */
/* 移动端优先的响应式设计 */
/* 基础样式(移动端) */
.container {
width: 100%;
padding: 16px;
box-sizing: border-box;
}
/* 平板设备(≥768px) */
@media (min-width: 768px) {
.container {
max-width: 720px;
margin: 0 auto;
padding: 24px;
}
}
/* 桌面设备(≥992px) */
@media (min-width: 992px) {
.container {
max-width: 960px;
padding: 32px;
}
}
/* 大桌面设备(≥1200px) */
@media (min-width: 1200px) {
.container {
max-width: 1140px;
}
}
/* 触摸设备优化 */
@media (hover: none) and (pointer: coarse) {
/* 增大点击区域 */
button, .btn {
min-height: 44px;
min-width: 44px;
}
/* 增加触摸反馈 */
button:active {
opacity: 0.7;
}
}
12.2 移动端优化
// src/utils/mobileUtils.ts
// 检测移动设备
export const isMobile = (): boolean => {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent
);
};
// 检测iOS设备
export const isIOS = (): boolean => {
return /iPad|iPhone|iPod/.test(navigator.userAgent);
};
// 检测Android设备
export const isAndroid = (): boolean => {
return /Android/.test(navigator.userAgent);
};
// 检测是否支持触摸
export const isTouchDevice = (): boolean => {
return 'ontouchstart' in window || navigator.maxTouchPoints > 0;
};
// 移动端视口设置
export const setViewport = (): void => {
if (isMobile()) {
const viewport = document.querySelector('meta[name="viewport"]');
if (viewport) {
viewport.setAttribute(
'content',
'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'
);
}
}
};
// 防止iOS双击缩放
export const preventDoubleTapZoom = (): void => {
if (isIOS()) {
document.addEventListener('touchstart', (event) => {
if (event.touches.length > 1) {
event.preventDefault();
}
});
let lastTouchEnd = 0;
document.addEventListener('touchend', (event) => {
const now = Date.now();
if (now - lastTouchEnd <= 300) {
event.preventDefault();
}
lastTouchEnd = now;
}, false);
}
};
第十三章:国际化与本地化
13.1 多语言支持
npm install i18next react-i18next
// src/i18n/config.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import HttpApi from 'i18next-http-backend';
// 语言资源
const resources = {
en: {
translation: {
welcome: 'Welcome',
login: 'Login',
logout: 'Logout',
home: 'Home',
dashboard: 'Dashboard',
settings: 'Settings',
error: {
notFound: 'Page not found',
serverError: 'Server error',
},
},
},
zh: {
translation: {
welcome: '欢迎',
login: '登录',
logout: '退出',
home: '首页',
dashboard: '仪表盘',
settings: '设置',
error: {
notFound: '页面未找到',
serverError: '服务器错误',
},
},
},
ja: {
translation: {
welcome: 'ようこそ',
login: 'ログイン',
logout: 'ログアウト',
home: 'ホーム',
dashboard: 'ダッシュボード',
settings: '設定',
error: {
notFound: 'ページが見つかりません',
serverError: 'サーバーエラー',
},
},
},
};
i18n
.use(HttpApi)
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources,
fallbackLng: 'en',
debug: import.meta.env.DEV,
interpolation: {
escapeValue: false,
},
detection: {
order: ['cookie', 'localStorage', 'navigator', 'htmlTag'],
caches: ['cookie', 'localStorage'],
},
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
},
});
export default i18n;
13.2 在组件中使用
// src/components/Header/Header.tsx
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
const Header: React.FC = () => {
const { t, i18n } = useTranslation();
const navigate = useNavigate();
const changeLanguage = (lng: string) => {
i18n.changeLanguage(lng);
localStorage.setItem('language', lng);
};
return (
<header className="header">
<div className="header-left">
<h1 onClick={() => navigate('/')}>{t('welcome')}</h1>
</div>
<div className="header-right">
<button onClick={() => changeLanguage('en')}>EN</button>
<button onClick={() => changeLanguage('zh')}>中文</button>
<button onClick={() => changeLanguage('ja')}>日本語</button>
<nav>
<button onClick={() => navigate('/')}>{t('home')}</button>
<button onClick={() => navigate('/dashboard')}>{t('dashboard')}</button>
<button onClick={() => navigate('/settings')}>{t('settings')}</button>
</nav>
</div>
</header>
);
};
export default Header;
第十四章:可访问性(A11y)
14.1 语义化HTML
// src/components/AccessibleButton/AccessibleButton.tsx
import React from 'react';
interface AccessibleButtonProps {
children: React.ReactNode;
onClick: () => void;
ariaLabel?: string;
ariaDescribedBy?: string;
ariaPressed?: boolean;
disabled?: boolean;
}
const AccessibleButton: React.FC<AccessibleButtonProps> = ({
children,
onClick,
ariaLabel,
ariaDescribedBy,
ariaPressed,
disabled = false,
}) => {
return (
<button
type="button"
onClick={onClick}
aria-label={ariaLabel}
aria-describedby={ariaDescribedBy}
aria-pressed={ariaPressed}
disabled={disabled}
className="accessible-button"
>
{children}
</button>
);
};
export default AccessibleButton;
14.2 键盘导航支持
// src/hooks/useKeyboardNavigation.ts
import { useEffect, useRef } from 'react';
interface KeyboardNavigationOptions {
onEnter?: () => void;
onEscape?: () => void;
onArrowUp?: () => void;
onArrowDown?: () => void;
onArrowLeft?: () => void;
onArrowRight?: () => void;
onTab?: () => void;
}
export const useKeyboardNavigation = (
ref: React.RefObject<HTMLElement>,
options: KeyboardNavigationOptions
) => {
useEffect(() => {
const element = ref.current;
if (!element) return;
const handleKeyDown = (event: KeyboardEvent) => {
switch (event.key) {
case 'Enter':
options.onEnter?.();
break;
case 'Escape':
options.onEscape?.();
break;
case 'ArrowUp':
event.preventDefault();
options.onArrowUp?.();
break;
case 'ArrowDown':
event.preventDefault();
options.onArrowDown?.();
break;
case 'ArrowLeft':
event.preventDefault();
options.onArrowLeft?.();
break;
case 'ArrowRight':
event.preventDefault();
options.onArrowRight?.();
break;
case 'Tab':
options.onTab?.();
break;
}
};
element.addEventListener('keydown', handleKeyDown);
return () => element.removeEventListener('keydown', handleKeyDown);
}, [ref, options]);
};
// 使用示例
const Dropdown: React.FC = () => {
const dropdownRef = useRef<HTMLDivElement>(null);
const [isOpen, setIsOpen] = useState(false);
useKeyboardNavigation(dropdownRef, {
onEnter: () => setIsOpen(!isOpen),
onEscape: () => setIsOpen(false),
onArrowDown: () => {
if (!isOpen) setIsOpen(true);
},
});
return (
<div ref={dropdownRef} tabIndex={0} className="dropdown">
<button onClick={() => setIsOpen(!isOpen)}>选项</button>
{isOpen && (
<ul role="menu">
<li role="menuitem">选项1</li>
<li role="menuitem">选项2</li>
<li role="menuitem">选项3</li>
</ul>
)}
</div>
);
};
第十五章:项目维护与文档
15.1 代码注释规范
/**
* 用户管理模块
* @description 提供用户相关的API接口和状态管理
* @version 1.0.0
* @author 张三
* @created 2024-01-01
*/
// 文件头部注释
// src/services/userService.ts
// 函数注释
/**
* 获取用户信息
* @param userId - 用户ID
* @returns Promise<User> - 用户信息
* @throws Error - 当请求失败时抛出错误
* @example
* ```typescript
* const user = await getUserInfo('123');
* console.log(user.name);
* ```
*/
export async function getUserInfo(userId: string): Promise<User> {
// 实现代码
}
// 类注释
/**
* 用户管理类
* @description 管理用户相关的业务逻辑
*/
class UserManager {
/**
* 构造函数
* @param api - API客户端实例
*/
constructor(private api: ApiClient) {}
/**
* 创建用户
* @param userData - 用户数据
* @returns Promise<User> - 创建的用户
*/
async createUser(userData: CreateUserDto): Promise<User> {
// 实现代码
}
}
15.2 项目文档
创建README.md:
# EB前端项目
## 项目简介
这是一个基于React + TypeScript + Vite构建的现代Web应用。
## 技术栈
- React 18
- TypeScript
- Vite
- Zustand
- React Router v6
- ESLint + Prettier
- Jest + React Testing Library
## 快速开始
### 安装依赖
```bash
npm install
开发环境
npm run dev
构建
npm run build
测试
npm run test
代码规范
npm run lint
项目结构
src/
├── assets/ # 静态资源
├── components/ # 组件
├── hooks/ # 自定义Hook
├── pages/ # 页面
├── services/ # API服务
├── store/ # 状态管理
├── types/ # 类型定义
├── utils/ # 工具函数
└── App.tsx # 应用入口
开发规范
组件开发
- 使用函数组件和Hooks
- 组件文件使用
.tsx扩展名 - 样式使用CSS Modules
- 每个组件应该有对应的测试文件
提交规范
使用Conventional Commits:
feat: 新功能
fix: 修复bug
docs: 文档更新
style: 代码格式
refactor: 重构
test: 测试相关
chore: 构建/工具相关
部署
Vercel部署
- 安装Vercel CLI:
npm i -g vercel - 登录:
vercel login - 部署:
vercel --prod
Docker部署
docker build -t my-app .
docker run -p 80:80 my-app
环境变量
创建.env文件:
VITE_API_BASE_URL=http://localhost:8080/api
贡献指南
- Fork项目
- 创建特性分支:
git checkout -b feature/amazing-feature - 提交更改:
git commit -m 'feat: add amazing feature' - 推送分支:
git push origin feature/amazing-feature - 创建Pull Request
许可证
MIT License
## 第十六章:总结与展望
### 16.1 项目回顾
通过本指南,我们从零开始构建了一个完整的现代Web应用,涵盖了:
1. **项目初始化**:技术选型、环境配置、代码规范
2. **架构设计**:状态管理、路由、API服务、组件设计
3. **开发实践**:组件开发、性能优化、测试策略
4. **构建部署**:构建配置、CI/CD、Docker化
5. **高级主题**:错误处理、性能监控、安全、移动端适配、国际化、可访问性
### 16.2 未来优化方向
1. **微前端架构**:对于大型应用,可以考虑微前端架构
2. **服务端渲染**:使用Next.js或Remix提升SEO和首屏性能
3. **PWA支持**:添加Service Worker,实现离线访问
4. **WebAssembly**:在性能敏感场景使用WebAssembly
5. **AI集成**:集成机器学习模型,提供智能功能
### 16.3 持续学习
前端技术日新月异,建议持续关注:
- React官方文档和新特性
- TypeScript类型系统进阶
- Web性能优化最佳实践
- 新兴框架和工具(如Svelte、SolidJS、Bun)
- Web标准和API更新
## 附录:常用命令速查
```bash
# 项目初始化
npm create vite@latest my-app -- --template react-ts
# 开发
npm run dev
# 构建
npm run build
# 预览构建结果
npm run preview
# 测试
npm run test
# 代码检查
npm run lint
# 格式化代码
npm run format
# 生成类型定义
npm run types
# 清理缓存
npm run clean
# 更新依赖
npm update
# 安全审计
npm audit
# 依赖分析
npx depcheck
结语
构建一个高效、可维护的现代Web应用是一个系统工程,需要良好的架构设计、规范的开发流程和持续的优化。希望本指南能为你提供清晰的路线图和实用的工具,帮助你在前端开发的道路上走得更远。
记住,优秀的代码不仅仅是能运行的代码,更是易于理解、易于维护、易于扩展的代码。保持学习,持续改进,享受编码的乐趣!
最后更新: 2024年1月
版本: 1.0.0
作者: EB前端团队
