现代前端开发全栈指南
现代前端开发已经从简单的页面制作演进为复杂的工程化体系,涵盖了框架选型、架构设计、性能优化、工程化工具链等多个维度。本指南将深入解析现代前端开发的核心技术和最佳实践,帮助开发者构建高质量的前端应用。
核心价值
现代前端 = 组件化架构 + 工程化工具链 + 性能优化 + 用户体验
- 🎯 组件化架构:可复用、可维护的组件体系
- 🛠️ 工程化工具链:自动化构建、测试、部署流程
- ⚡ 性能优化:首屏加载、运行时性能、用户体验优化
- 🎨 现代化UI:响应式设计、交互动效、无障碍访问
- 🔧 开发体验:热更新、类型检查、调试工具
- 🌐 跨平台能力:Web、移动端、桌面端统一开发
1. 前端技术栈全景图
1.1 技术栈架构层次
现代前端技术栈可以分为多个层次,每个层次都有其特定的职责和技术选型。
技术选型对比矩阵
| 技术类别 | 主流方案 | 优势 | 适用场景 | 学习成本 |
|---|---|---|---|---|
| 前端框架 | React | 生态丰富、灵活性高 | 大型应用、复杂交互 | ⭐⭐⭐ |
| Vue | 学习曲线平缓、文档完善 | 中小型项目、快速开发 | ⭐⭐ | |
| Angular | 企业级、完整解决方案 | 大型企业应用 | ⭐⭐⭐⭐ | |
| 状态管理 | Redux Toolkit | 可预测、时间旅行调试 | 复杂状态逻辑 | ⭐⭐⭐ |
| Zustand | 轻量级、简单易用 | 中小型应用 | ⭐⭐ | |
| Pinia | Vue生态、组合式API | Vue项目 | ⭐⭐ | |
| 构建工具 | Vite | 快速冷启动、HMR | 现代项目首选 | ⭐⭐ |
| Webpack | 成熟稳定、插件丰富 | 复杂配置需求 | ⭐⭐⭐⭐ | |
| CSS方案 | Tailwind CSS | 原子化、高度可定制 | 快速原型、设计系统 | ⭐⭐⭐ |
| CSS Modules | 作用域隔离、零运行时 | 组件化开发 | ⭐⭐ | |
| Styled Components | CSS-in-JS、动态样式 | React生态 | ⭐⭐⭐ | |
| 发生命周期 |
现代前端开发遵循完整的软件开发生命周期,每个阶段都有相应的工具和最佳实践。
- 规划设计
- 开发阶段
- 测试阶段
- 部署上线
需求分析与技术选型
技术选型决策因素:
- 团队技能:团队对技术栈的熟悉程度
- 项目规模:小型、中型、大型项目的不同需求
- 性能要求:首屏加载、运行时性能指标
- 维护成本:长期维护和扩展的便利性
- 生态系统:第三方库、工具链的完善程度
开发工作流程
现代前端开发工作流
typescript
1// 1. 项目初始化2const projectSetup = {3 packageManager: 'pnpm', // 快速、节省磁盘空间4 buildTool: 'vite', // 快速构建和热更新5 framework: 'react', // 或 vue、angular6 language: 'typescript', // 类型安全7 linting: 'eslint', // 代码质量8 formatting: 'prettier', // 代码格式化9 testing: 'vitest', // 单元测试10 e2e: 'playwright' // 端到端测试11};1213// 2. 开发环境配置14const devEnvironment = {15 hotReload: true, // 热更新16 sourceMap: true, // 源码映射17 typeChecking: true, // 类型检查18 linting: 'onSave', // 保存时检查19 autoFormat: true // 自动格式化20};2122// 3. 代码组织结构23const projectStructure = `24src/25├── components/ # 可复用组件26│ ├── ui/ # 基础UI组件27│ ├── business/ # 业务组件28│ └── layout/ # 布局组件29├── pages/ # 页面组件30├── hooks/ # 自定义Hooks31├── utils/ # 工具函数32├── services/ # API服务33├── stores/ # 状态管理34├── styles/ # 样式文件35├── types/ # TypeScript类型定义36└── tests/ # 测试文件37`;开发最佳实践:
- 组件化开发:单一职责、可复用、可测试
- 类型安全:使用TypeScript提供类型检查
- 代码规范:统一的代码风格和命名规范
- 版本控制:合理的Git工作流和提交规范
- 文档维护:组件文档、API文档、README
测试策略金字塔
测试类型与工具:
| 测试类型 | 测试范围 | 推荐工具 | 测试比例 |
|---|---|---|---|
| 单元测试 | 函数、组件、Hook | Vitest + Testing Library | 60% |
| 集成测试 | 组件交互、API集成 | MSW + Testing Library | 30% |
| E2E测试 | 用户完整流程 | Playwright + Cypress | 10% |
测试示例
typescript
1// 单元测试示例2import { render, screen } from '@testing-library/react';3import { Button } from './Button';45describe('Button Component', () => {6 it('renders with correct text', () => {7 render(<Button>Click me</Button>);8 expect(screen.getByText('Click me')).toBeInTheDocument();9 });1011 it('handles click events', () => {12 const handleClick = vi.fn();13 render(<Button onClick={handleClick}>Click me</Button>);14 15 screen.getByText('Click me').click();16 expect(handleClick).toHaveBeenCalledTimes(1);17 });18});1920// E2E测试示例21import { test, expect } from '@playwright/test';2223test('user can complete checkout flow', async ({ page }) => {24 await page.goto('/products');25 await page.click('[data-testid="add-to-cart"]');26 await page.click('[data-testid="checkout"]');27 28 await expect(page.locator('[data-testid="success-message"]'))29 .toBeVisible();30});2. React生态系统深度解析
2.1 React 18+ 新特性与最佳实践
React 18引入了并发特性、自动批处理、Suspense改进等重要更新,为构建高性能应用提供了更强大的能力。
- 并发特性
- Hooks最佳实践
- 性能优化
并发渲染与Suspense
React 18并发特性示例
typescript
1import { Suspense, lazy, startTransition, useDeferredValue } from 'react';23// 1. 代码分割与懒加载4const LazyComponent = lazy(() => import('./LazyComponent'));56// 2. 并发渲染组件7function SearchResults({ query }: { query: string }) {8 // 延迟值,降低搜索输入的优先级9 const deferredQuery = useDeferredValue(query);10 11 return (12 <div>13 <h2>搜索结果</h2>14 <Suspense fallback={<SearchSkeleton />}>15 <SearchList query={deferredQuery} />16 </Suspense>17 </div>18 );19}2021// 3. 使用startTransition标记非紧急更新22function SearchInput() {23 const [query, setQuery] = useState('');24 const [isPending, startTransition] = useTransition();25 26 const handleSearch = (value: string) => {27 setQuery(value); // 紧急更新:立即更新输入框28 29 startTransition(() => {30 // 非紧急更新:搜索结果可以延迟31 setSearchResults(value);32 });33 };34 35 return (36 <div>37 <input 38 value={query}39 onChange={(e) => handleSearch(e.target.value)}40 placeholder="搜索..."41 />42 {isPending && <Spinner />}43 </div>44 );45}并发特性优势:
- 可中断渲染:React可以暂停渲染工作,优先处理用户交互
- 时间切片:将长时间的渲染工作分解为小块
- 优先级调度:根据更新的重要性分配不同优先级
- 更好的用户体验:减少页面卡顿,提升响应性
自定义Hooks设计模式
高质量自定义Hooks
typescript
1// 1. 数据获取Hook2function useApi<T>(url: string, options?: RequestInit) {3 const [data, setData] = useState<T | null>(null);4 const [loading, setLoading] = useState(true);5 const [error, setError] = useState<Error | null>(null);6 7 useEffect(() => {8 const abortController = new AbortController();9 10 const fetchData = async () => {11 try {12 setLoading(true);13 setError(null);14 15 const response = await fetch(url, {16 ...options,17 signal: abortController.signal18 });19 20 if (!response.ok) {21 throw new Error(`HTTP error! status: ${response.status}`);22 }23 24 const result = await response.json();25 setData(result);26 } catch (err) {27 if (err.name !== 'AbortError') {28 setError(err as Error);29 }30 } finally {31 setLoading(false);32 }33 };34 35 fetchData();36 37 return () => abortController.abort();38 }, [url, JSON.stringify(options)]);39 40 return { data, loading, error };41}4243// 2. 本地存储Hook44function useLocalStorage<T>(key: string, initialValue: T) {45 const [storedValue, setStoredValue] = useState<T>(() => {46 try {47 const item = window.localStorage.getItem(key);48 return item ? JSON.parse(item) : initialValue;49 } catch (error) {50 console.error(`Error reading localStorage key "${key}":`, error);51 return initialValue;52 }53 });54 55 const setValue = useCallback((value: T | ((val: T) => T)) => {56 try {57 const valueToStore = value instanceof Function ? value(storedValue) : value;58 setStoredValue(valueToStore);59 window.localStorage.setItem(key, JSON.stringify(valueToStore));60 } catch (error) {61 console.error(`Error setting localStorage key "${key}":`, error);62 }63 }, [key, storedValue]);64 65 return [storedValue, setValue] as const;66}6768// 3. 防抖Hook69function useDebounce<T>(value: T, delay: number): T {70 const [debouncedValue, setDebouncedValue] = useState<T>(value);71 72 useEffect(() => {73 const handler = setTimeout(() => {74 setDebouncedValue(value);75 }, delay);76 77 return () => clearTimeout(handler);78 }, [value, delay]);79 80 return debouncedValue;81}8283// 4. 使用示例84function UserProfile({ userId }: { userId: string }) {85 const { data: user, loading, error } = useApi<User>(`/api/users/${userId}`);86 const [preferences, setPreferences] = useLocalStorage('userPreferences', {});87 88 if (loading) return <ProfileSkeleton />;89 if (error) return <ErrorMessage error={error} />;90 if (!user) return <NotFound />;91 92 return (93 <div>94 <h1>{user.name}</h1>95 <UserSettings 96 preferences={preferences}97 onPreferencesChange={setPreferences}98 />99 </div>100 );101}React性能优化策略
React性能优化实践
typescript
1// 1. 组件记忆化2const ExpensiveComponent = memo(({ data, onUpdate }: Props) => {3 const processedData = useMemo(() => {4 return data.map(item => ({5 ...item,6 processed: expensiveCalculation(item)7 }));8 }, [data]);9 10 const handleUpdate = useCallback((id: string, updates: Partial<Item>) => {11 onUpdate(id, updates);12 }, [onUpdate]);13 14 return (15 <div>16 {processedData.map(item => (17 <ItemCard 18 key={item.id}19 item={item}20 onUpdate={handleUpdate}21 />22 ))}23 </div>24 );25});2627// 2. 虚拟滚动28function VirtualList<T>({ 29 items, 30 itemHeight, 31 containerHeight,32 renderItem 33}: VirtualListProps<T>) {34 const [scrollTop, setScrollTop] = useState(0);35 36 const visibleStart = Math.floor(scrollTop / itemHeight);37 const visibleEnd = Math.min(38 visibleStart + Math.ceil(containerHeight / itemHeight) + 1,39 items.length40 );41 42 const visibleItems = items.slice(visibleStart, visibleEnd);43 44 return (45 <div 46 style={{ height: containerHeight, overflow: 'auto' }}47 onScroll={(e) => setScrollTop(e.currentTarget.scrollTop)}48 >49 <div style={{ height: items.length * itemHeight, position: 'relative' }}>50 {visibleItems.map((item, index) => (51 <div52 key={visibleStart + index}53 style={{54 position: 'absolute',55 top: (visibleStart + index) * itemHeight,56 height: itemHeight,57 width: '100%'58 }}59 >60 {renderItem(item, visibleStart + index)}61 </div>62 ))}63 </div>64 </div>65 );66}6768// 3. 错误边界69class ErrorBoundary extends Component<70 { children: ReactNode; fallback: ComponentType<{ error: Error }> },71 { hasError: boolean; error: Error | null }72> {73 constructor(props: any) {74 super(props);75 this.state = { hasError: false, error: null };76 }77 78 static getDerivedStateFromError(error: Error) {79 return { hasError: true, error };80 }81 82 componentDidCatch(error: Error, errorInfo: ErrorInfo) {83 console.error('Error caught by boundary:', error, errorInfo);84 // 发送错误报告到监控服务85 reportError(error, errorInfo);86 }87 88 render() {89 if (this.state.hasError) {90 const FallbackComponent = this.props.fallback;91 return <FallbackComponent error={this.state.error!} />;92 }93 94 return this.props.children;95 }96}性能优化检查清单:
- ✅ 使用React.memo()避免不必要的重渲染
- ✅ 使用useMemo()缓存昂贵的计算结果
- ✅ 使用useCallback()稳定函数引用
- ✅ 实现虚拟滚动处理大列表
- ✅ 使用代码分割和懒加载
- ✅ 优化Bundle大小和加载性能
- ✅ 使用错误边界处理异常
2.2 状态管理架构设计
现代React应用的状态管理需要考虑本地状态、全局状态、服务器状态等多个维度,选择合适的状态管理方案至关重要。
- Redux Toolkit
- Zustand轻量方案
- 服务器状态管理
现代Redux最佳实践
Redux Toolkit完整实现
typescript
1// 1. Store配置2import { configureStore } from '@reduxjs/toolkit';3import { setupListeners } from '@reduxjs/toolkit/query';4import { userSlice } from './slices/userSlice';5import { apiSlice } from './api/apiSlice';67export const store = configureStore({8 reducer: {9 user: userSlice.reducer,10 api: apiSlice.reducer,11 },12 middleware: (getDefaultMiddleware) =>13 getDefaultMiddleware({14 serializableCheck: {15 ignoredActions: ['persist/PERSIST', 'persist/REHYDRATE'],16 },17 }).concat(apiSlice.middleware),18 devTools: process.env.NODE_ENV !== 'production',19});2021setupListeners(store.dispatch);2223export type RootState = ReturnType<typeof store.getState>;24export type AppDispatch = typeof store.dispatch;2526// 2. Slice定义27import { createSlice, PayloadAction } from '@reduxjs/toolkit';2829interface UserState {30 currentUser: User | null;31 preferences: UserPreferences;32 loading: boolean;33 error: string | null;34}3536const initialState: UserState = {37 currentUser: null,38 preferences: {39 theme: 'light',40 language: 'zh-CN',41 notifications: true,42 },43 loading: false,44 error: null,45};4647export const userSlice = createSlice({48 name: 'user',49 initialState,50 reducers: {51 setUser: (state, action: PayloadAction<User>) => {52 state.currentUser = action.payload;53 state.error = null;54 },55 updatePreferences: (state, action: PayloadAction<Partial<UserPreferences>>) => {56 state.preferences = { ...state.preferences, ...action.payload };57 },58 setLoading: (state, action: PayloadAction<boolean>) => {59 state.loading = action.payload;60 },61 setError: (state, action: PayloadAction<string>) => {62 state.error = action.payload;63 state.loading = false;64 },65 clearError: (state) => {66 state.error = null;67 },68 },69 extraReducers: (builder) => {70 builder71 .addCase(loginUser.pending, (state) => {72 state.loading = true;73 state.error = null;74 })75 .addCase(loginUser.fulfilled, (state, action) => {76 state.loading = false;77 state.currentUser = action.payload;78 })79 .addCase(loginUser.rejected, (state, action) => {80 state.loading = false;81 state.error = action.error.message || '登录失败';82 });83 },84});8586// 3. 异步Thunk87import { createAsyncThunk } from '@reduxjs/toolkit';8889export const loginUser = createAsyncThunk(90 'user/login',91 async (credentials: LoginCredentials, { rejectWithValue }) => {92 try {93 const response = await authAPI.login(credentials);94 localStorage.setItem('token', response.token);95 return response.user;96 } catch (error) {97 return rejectWithValue(error.message);98 }99 }100);101102// 4. RTK Query API定义103import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';104105export const apiSlice = createApi({106 reducerPath: 'api',107 baseQuery: fetchBaseQuery({108 baseUrl: '/api',109 prepareHeaders: (headers, { getState }) => {110 const token = (getState() as RootState).user.token;111 if (token) {112 headers.set('authorization', `Bearer ${token}`);113 }114 return headers;115 },116 }),117 tagTypes: ['User', 'Post', 'Comment'],118 endpoints: (builder) => ({119 getUsers: builder.query<User[], void>({120 query: () => '/users',121 providesTags: ['User'],122 }),123 getUserById: builder.query<User, string>({124 query: (id) => `/users/${id}`,125 providesTags: (result, error, id) => [{ type: 'User', id }],126 }),127 updateUser: builder.mutation<User, { id: string; updates: Partial<User> }>({128 query: ({ id, updates }) => ({129 url: `/users/${id}`,130 method: 'PATCH',131 body: updates,132 }),133 invalidatesTags: (result, error, { id }) => [{ type: 'User', id }],134 }),135 }),136});137138export const { useGetUsersQuery, useGetUserByIdQuery, useUpdateUserMutation } = apiSlice;139140// 5. 类型安全的Hooks141import { useSelector, useDispatch } from 'react-redux';142import type { TypedUseSelectorHook } from 'react-redux';143144export const useAppDispatch = () => useDispatch<AppDispatch>();145export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;146147// 6. 组件中使用148function UserProfile({ userId }: { userId: string }) {149 const dispatch = useAppDispatch();150 const { currentUser, preferences, loading } = useAppSelector(state => state.user);151 const { data: user, error, isLoading } = useGetUserByIdQuery(userId);152 const [updateUser] = useUpdateUserMutation();153 154 const handleUpdatePreferences = (newPreferences: Partial<UserPreferences>) => {155 dispatch(userSlice.actions.updatePreferences(newPreferences));156 };157 158 const handleUpdateProfile = async (updates: Partial<User>) => {159 try {160 await updateUser({ id: userId, updates }).unwrap();161 toast.success('Profile updated successfully');162 } catch (error) {163 toast.error('Failed to update profile');164 }165 };166 167 if (isLoading) return <ProfileSkeleton />;168 if (error) return <ErrorMessage error={error} />;169 170 return (171 <div>172 <UserInfo user={user} onUpdate={handleUpdateProfile} />173 <UserSettings 174 preferences={preferences}175 onUpdate={handleUpdatePreferences}176 />177 </div>178 );179}Zustand状态管理
Zustand实现方案
typescript
1import { create } from 'zustand';2import { devtools, persist, subscribeWithSelector } from 'zustand/middleware';3import { immer } from 'zustand/middleware/immer';45// 1. 基础Store定义6interface UserStore {7 // State8 user: User | null;9 preferences: UserPreferences;10 loading: boolean;11 error: string | null;12 13 // Actions14 setUser: (user: User) => void;15 updatePreferences: (preferences: Partial<UserPreferences>) => void;16 login: (credentials: LoginCredentials) => Promise<void>;17 logout: () => void;18 clearError: () => void;19}2021export const useUserStore = create<UserStore>()(22 devtools(23 persist(24 subscribeWithSelector(25 immer((set, get) => ({26 // Initial state27 user: null,28 preferences: {29 theme: 'light',30 language: 'zh-CN',31 notifications: true,32 },33 loading: false,34 error: null,35 36 // Actions37 setUser: (user) => set((state) => {38 state.user = user;39 state.error = null;40 }),41 42 updatePreferences: (newPreferences) => set((state) => {43 state.preferences = { ...state.preferences, ...newPreferences };44 }),45 46 login: async (credentials) => {47 set((state) => {48 state.loading = true;49 state.error = null;50 });51 52 try {53 const response = await authAPI.login(credentials);54 set((state) => {55 state.user = response.user;56 state.loading = false;57 });58 localStorage.setItem('token', response.token);59 } catch (error) {60 set((state) => {61 state.error = error.message;62 state.loading = false;63 });64 }65 },66 67 logout: () => set((state) => {68 state.user = null;69 localStorage.removeItem('token');70 }),71 72 clearError: () => set((state) => {73 state.error = null;74 }),75 }))76 ),77 {78 name: 'user-store',79 partialize: (state) => ({ 80 user: state.user, 81 preferences: state.preferences 82 }),83 }84 ),85 { name: 'user-store' }86 )87);8889// 2. 计算属性和选择器90export const useUserSelectors = () => {91 const isLoggedIn = useUserStore(state => !!state.user);92 const userName = useUserStore(state => state.user?.name || '');93 const isLoading = useUserStore(state => state.loading);94 const theme = useUserStore(state => state.preferences.theme);95 96 return { isLoggedIn, userName, isLoading, theme };97};9899// 3. 分离的Actions100export const userActions = {101 login: (credentials: LoginCredentials) => useUserStore.getState().login(credentials),102 logout: () => useUserStore.getState().logout(),103 updatePreferences: (preferences: Partial<UserPreferences>) => 104 useUserStore.getState().updatePreferences(preferences),105};106107// 4. 组件中使用108function LoginForm() {109 const { loading, error } = useUserStore();110 const { isLoggedIn } = useUserSelectors();111 const [credentials, setCredentials] = useState({ email: '', password: '' });112 113 const handleSubmit = async (e: FormEvent) => {114 e.preventDefault();115 await userActions.login(credentials);116 };117 118 if (isLoggedIn) {119 return <Navigate to="/dashboard" replace />;120 }121 122 return (123 <form onSubmit={handleSubmit}>124 <input125 type="email"126 value={credentials.email}127 onChange={(e) => setCredentials(prev => ({ ...prev, email: e.target.value }))}128 placeholder="Email"129 required130 />131 <input132 type="password"133 value={credentials.password}134 onChange={(e) => setCredentials(prev => ({ ...prev, password: e.target.value }))}135 placeholder="Password"136 required137 />138 <button type="submit" disabled={loading}>139 {loading ? 'Logging in...' : 'Login'}140 </button>141 {error && <ErrorMessage message={error} />}142 </form>143 );144}145146// 5. 中间件和插件147const loggerMiddleware = (config) => (set, get, api) =>148 config(149 (...args) => {150 console.log('Previous state:', get());151 set(...args);152 console.log('New state:', get());153 },154 get,155 api156 );157158// 6. 多Store组合159interface AppStore {160 userStore: UserStore;161 cartStore: CartStore;162 uiStore: UIStore;163}164165export const useAppStore = create<AppStore>()((set, get) => ({166 userStore: useUserStore.getState(),167 cartStore: useCartStore.getState(),168 uiStore: useUIStore.getState(),169}));React Query/TanStack Query
服务器状态管理最佳实践
typescript
1import { useQuery, useMutation, useQueryClient, useInfiniteQuery } from '@tanstack/react-query';23// 1. Query Keys工厂4export const queryKeys = {5 all: ['todos'] as const,6 lists: () => [...queryKeys.all, 'list'] as const,7 list: (filters: string) => [...queryKeys.lists(), { filters }] as const,8 details: () => [...queryKeys.all, 'detail'] as const,9 detail: (id: number) => [...queryKeys.details(), id] as const,10};1112// 2. API函数13const todoAPI = {14 getAll: async (filters?: TodoFilters): Promise<Todo[]> => {15 const params = new URLSearchParams(filters);16 const response = await fetch(`/api/todos?${params}`);17 if (!response.ok) throw new Error('Failed to fetch todos');18 return response.json();19 },20 21 getById: async (id: number): Promise<Todo> => {22 const response = await fetch(`/api/todos/${id}`);23 if (!response.ok) throw new Error('Failed to fetch todo');24 return response.json();25 },26 27 create: async (todo: CreateTodoRequest): Promise<Todo> => {28 const response = await fetch('/api/todos', {29 method: 'POST',30 headers: { 'Content-Type': 'application/json' },31 body: JSON.stringify(todo),32 });33 if (!response.ok) throw new Error('Failed to create todo');34 return response.json();35 },36 37 update: async ({ id, ...updates }: UpdateTodoRequest): Promise<Todo> => {38 const response = await fetch(`/api/todos/${id}`, {39 method: 'PATCH',40 headers: { 'Content-Type': 'application/json' },41 body: JSON.stringify(updates),42 });43 if (!response.ok) throw new Error('Failed to update todo');44 return response.json();45 },46 47 delete: async (id: number): Promise<void> => {48 const response = await fetch(`/api/todos/${id}`, { method: 'DELETE' });49 if (!response.ok) throw new Error('Failed to delete todo');50 },51};5253// 3. 自定义Hooks54export function useTodos(filters?: TodoFilters) {55 return useQuery({56 queryKey: queryKeys.list(JSON.stringify(filters)),57 queryFn: () => todoAPI.getAll(filters),58 staleTime: 5 * 60 * 1000, // 5分钟59 cacheTime: 10 * 60 * 1000, // 10分钟60 refetchOnWindowFocus: false,61 retry: (failureCount, error) => {62 if (error.status === 404) return false;63 return failureCount < 3;64 },65 });66}6768export function useTodo(id: number) {69 return useQuery({70 queryKey: queryKeys.detail(id),71 queryFn: () => todoAPI.getById(id),72 enabled: !!id,73 staleTime: 5 * 60 * 1000,74 });75}7677export function useCreateTodo() {78 const queryClient = useQueryClient();79 80 return useMutation({81 mutationFn: todoAPI.create,82 onSuccess: (newTodo) => {83 // 乐观更新84 queryClient.setQueryData<Todo[]>(queryKeys.lists(), (old) => 85 old ? [...old, newTodo] : [newTodo]86 );87 88 // 重新获取列表数据89 queryClient.invalidateQueries({ queryKey: queryKeys.lists() });90 91 toast.success('Todo created successfully');92 },93 onError: (error) => {94 toast.error(`Failed to create todo: ${error.message}`);95 },96 });97}9899export function useUpdateTodo() {100 const queryClient = useQueryClient();101 102 return useMutation({103 mutationFn: todoAPI.update,104 onMutate: async (updatedTodo) => {105 // 取消相关查询106 await queryClient.cancelQueries({ queryKey: queryKeys.detail(updatedTodo.id) });107 108 // 保存之前的数据109 const previousTodo = queryClient.getQueryData(queryKeys.detail(updatedTodo.id));110 111 // 乐观更新112 queryClient.setQueryData(queryKeys.detail(updatedTodo.id), updatedTodo);113 114 return { previousTodo };115 },116 onError: (err, updatedTodo, context) => {117 // 回滚118 if (context?.previousTodo) {119 queryClient.setQueryData(queryKeys.detail(updatedTodo.id), context.previousTodo);120 }121 toast.error(`Failed to update todo: ${err.message}`);122 },123 onSettled: (data, error, updatedTodo) => {124 // 重新获取数据125 queryClient.invalidateQueries({ queryKey: queryKeys.detail(updatedTodo.id) });126 },127 });128}129130// 4. 无限滚动131export function useInfiniteTodos(filters?: TodoFilters) {132 return useInfiniteQuery({133 queryKey: ['todos', 'infinite', filters],134 queryFn: ({ pageParam = 0 }) => 135 todoAPI.getAll({ ...filters, page: pageParam, limit: 20 }),136 getNextPageParam: (lastPage, pages) => {137 return lastPage.length === 20 ? pages.length : undefined;138 },139 staleTime: 5 * 60 * 1000,140 });141}142143// 5. 组件使用示例144function TodoList({ filters }: { filters?: TodoFilters }) {145 const { data: todos, isLoading, error, refetch } = useTodos(filters);146 const createTodo = useCreateTodo();147 const updateTodo = useUpdateTodo();148 149 const handleCreate = (todoData: CreateTodoRequest) => {150 createTodo.mutate(todoData);151 };152 153 const handleToggle = (todo: Todo) => {154 updateTodo.mutate({155 id: todo.id,156 completed: !todo.completed,157 });158 };159 160 if (isLoading) return <TodoSkeleton />;161 if (error) return <ErrorMessage error={error} onRetry={refetch} />;162 163 return (164 <div>165 <CreateTodoForm onSubmit={handleCreate} isLoading={createTodo.isLoading} />166 <div className="todo-list">167 {todos?.map(todo => (168 <TodoItem169 key={todo.id}170 todo={todo}171 onToggle={handleToggle}172 isUpdating={updateTodo.isLoading}173 />174 ))}175 </div>176 </div>177 );178}3. Vue.js生态系统与组合式API
3.1 Vue 3组合式API深度实践
Vue 3的组合式API为开发者提供了更灵活的逻辑复用和更好的TypeScript支持,是现代Vue开发的核心。
- 组合式API
- 组合式函数
- Pinia状态管理
组合式API最佳实践
Vue 3组合式API完整示例
vue
1<template>2 <div class="user-dashboard">3 <!-- 用户信息卡片 -->4 <UserCard 5 :user="user" 6 :loading="userLoading"7 @update="handleUserUpdate"8 />9 10 <!-- 搜索和过滤 -->11 <div class="search-section">12 <input13 v-model="searchQuery"14 placeholder="搜索用户..."15 class="search-input"16 />17 <select v-model="selectedRole" class="role-filter">18 <option value="">所有角色</option>19 <option value="admin">管理员</option>20 <option value="user">普通用户</option>21 </select>22 </div>23 24 <!-- 用户列表 -->25 <div class="user-list">26 <UserListItem27 v-for="user in filteredUsers"28 :key="user.id"29 :user="user"30 @edit="handleEditUser"31 @delete="handleDeleteUser"32 />33 </div>34 35 <!-- 分页 -->36 <Pagination37 :current="currentPage"38 :total="totalUsers"39 :page-size="pageSize"40 @change="handlePageChange"41 />42 </div>43</template>4445<script setup lang="ts">46import { ref, computed, watch, onMounted, nextTick } from 'vue';47import { useRouter, useRoute } from 'vue-router';48import { storeToRefs } from 'pinia';49import { useUserStore } from '@/stores/user';50import { useNotification } from '@/composables/useNotification';51import { useDebounce } from '@/composables/useDebounce';52import type { User, UserRole } from '@/types/user';5354// Props和Emits定义55interface Props {56 initialUserId?: string;57}5859const props = withDefaults(defineProps<Props>(), {60 initialUserId: '',61});6263const emit = defineEmits<{64 userSelected: [user: User];65 usersLoaded: [count: number];66}>();6768// 路由和状态管理69const router = useRouter();70const route = useRoute();71const userStore = useUserStore();72const { user, users, loading: userLoading } = storeToRefs(userStore);73const { showSuccess, showError } = useNotification();7475// 响应式数据76const searchQuery = ref('');77const selectedRole = ref<UserRole | ''>('');78const currentPage = ref(1);79const pageSize = ref(10);80const totalUsers = ref(0);8182// 防抖搜索83const debouncedSearchQuery = useDebounce(searchQuery, 300);8485// 计算属性86const filteredUsers = computed(() => {87 let result = users.value;88 89 // 搜索过滤90 if (debouncedSearchQuery.value) {91 const query = debouncedSearchQuery.value.toLowerCase();92 result = result.filter(user => 93 user.name.toLowerCase().includes(query) ||94 user.email.toLowerCase().includes(query)95 );96 }97 98 // 角色过滤99 if (selectedRole.value) {100 result = result.filter(user => user.role === selectedRole.value);101 }102 103 return result;104});105106const hasUsers = computed(() => filteredUsers.value.length > 0);107const isFirstPage = computed(() => currentPage.value === 1);108const isLastPage = computed(() => {109 return currentPage.value >= Math.ceil(totalUsers.value / pageSize.value);110});111112// 方法定义113const fetchUsers = async (page = 1) => {114 try {115 const result = await userStore.fetchUsers({116 page,117 pageSize: pageSize.value,118 search: debouncedSearchQuery.value,119 role: selectedRole.value || undefined,120 });121 122 totalUsers.value = result.total;123 emit('usersLoaded', result.total);124 } catch (error) {125 showError('获取用户列表失败');126 console.error('Failed to fetch users:', error);127 }128};129130const handleUserUpdate = async (updates: Partial<User>) => {131 try {132 await userStore.updateUser(user.value!.id, updates);133 showSuccess('用户信息更新成功');134 } catch (error) {135 showError('更新用户信息失败');136 }137};138139const handleEditUser = (user: User) => {140 emit('userSelected', user);141 router.push(`/users/${user.id}/edit`);142};143144const handleDeleteUser = async (user: User) => {145 if (!confirm(`确定要删除用户 ${user.name} 吗?`)) return;146 147 try {148 await userStore.deleteUser(user.id);149 showSuccess('用户删除成功');150 await fetchUsers(currentPage.value);151 } catch (error) {152 showError('删除用户失败');153 }154};155156const handlePageChange = (page: number) => {157 currentPage.value = page;158 fetchUsers(page);159};160161// 监听器162watch([debouncedSearchQuery, selectedRole], () => {163 currentPage.value = 1;164 fetchUsers(1);165});166167watch(() => route.query, (newQuery) => {168 if (newQuery.search) {169 searchQuery.value = newQuery.search as string;170 }171 if (newQuery.role) {172 selectedRole.value = newQuery.role as UserRole;173 }174}, { immediate: true });175176// 生命周期177onMounted(async () => {178 await fetchUsers();179 180 // 如果有初始用户ID,选中该用户181 if (props.initialUserId) {182 const initialUser = users.value.find(u => u.id === props.initialUserId);183 if (initialUser) {184 emit('userSelected', initialUser);185 }186 }187});188189// 暴露给模板的方法和数据190defineExpose({191 refreshUsers: () => fetchUsers(currentPage.value),192 resetFilters: () => {193 searchQuery.value = '';194 selectedRole.value = '';195 currentPage.value = 1;196 },197});198</script>199200<style scoped>201.user-dashboard {202 padding: 20px;203 max-width: 1200px;204 margin: 0 auto;205}206207.search-section {208 display: flex;209 gap: 16px;210 margin-bottom: 24px;211 align-items: center;212}213214.search-input {215 flex: 1;216 padding: 8px 12px;217 border: 1px solid #d1d5db;218 border-radius: 6px;219 font-size: 14px;220}221222.role-filter {223 padding: 8px 12px;224 border: 1px solid #d1d5db;225 border-radius: 6px;226 font-size: 14px;227 min-width: 120px;228}229230.user-list {231 display: grid;232 gap: 16px;233 margin-bottom: 24px;234}235236@media (max-width: 768px) {237 .search-section {238 flex-direction: column;239 align-items: stretch;240 }241}242</style>高质量Composables设计
Vue 3 Composables最佳实践
typescript
1// 1. useApi - 通用API请求Hook2import { ref, unref, type Ref } from 'vue';3import type { MaybeRef } from '@vueuse/core';45interface UseApiOptions<T> {6 immediate?: boolean;7 onSuccess?: (data: T) => void;8 onError?: (error: Error) => void;9 transform?: (data: any) => T;10}1112export function useApi<T = any>(13 url: MaybeRef<string>,14 options: UseApiOptions<T> = {}15) {16 const data = ref<T | null>(null);17 const loading = ref(false);18 const error = ref<Error | null>(null);19 20 const execute = async () => {21 try {22 loading.value = true;23 error.value = null;24 25 const response = await fetch(unref(url));26 if (!response.ok) {27 throw new Error(`HTTP error! status: ${response.status}`);28 }29 30 let result = await response.json();31 if (options.transform) {32 result = options.transform(result);33 }34 35 data.value = result;36 options.onSuccess?.(result);37 38 return result;39 } catch (err) {40 const errorObj = err instanceof Error ? err : new Error(String(err));41 error.value = errorObj;42 options.onError?.(errorObj);43 throw errorObj;44 } finally {45 loading.value = false;46 }47 };48 49 if (options.immediate !== false) {50 execute();51 }52 53 return {54 data: readonly(data),55 loading: readonly(loading),56 error: readonly(error),57 execute,58 refresh: execute,59 };60}6162// 2. useLocalStorage - 本地存储Hook63import { ref, watch, type Ref } from 'vue';6465export function useLocalStorage<T>(66 key: string,67 defaultValue: T,68 options: {69 serializer?: {70 read: (value: string) => T;71 write: (value: T) => string;72 };73 } = {}74): [Ref<T>, (value: T) => void] {75 const serializer = options.serializer || {76 read: (v: string) => {77 try {78 return JSON.parse(v);79 } catch {80 return v as T;81 }82 },83 write: (v: T) => JSON.stringify(v),84 };85 86 const storedValue = ref<T>(defaultValue);87 88 // 初始化89 try {90 const item = localStorage.getItem(key);91 if (item !== null) {92 storedValue.value = serializer.read(item);93 }94 } catch (error) {95 console.error(`Error reading localStorage key "${key}":`, error);96 }97 98 // 监听变化并同步到localStorage99 watch(100 storedValue,101 (newValue) => {102 try {103 localStorage.setItem(key, serializer.write(newValue));104 } catch (error) {105 console.error(`Error setting localStorage key "${key}":`, error);106 }107 },108 { deep: true }109 );110 111 const setValue = (value: T) => {112 storedValue.value = value;113 };114 115 return [storedValue, setValue];116}117118// 3. useForm - 表单处理Hook119import { reactive, computed } from 'vue';120121interface ValidationRule<T = any> {122 required?: boolean;123 min?: number;124 max?: number;125 pattern?: RegExp;126 validator?: (value: T) => boolean | string;127}128129interface FormField<T = any> {130 value: T;131 rules?: ValidationRule<T>[];132 error?: string;133 touched?: boolean;134}135136export function useForm<T extends Record<string, any>>(137 initialValues: T,138 validationRules: Partial<Record<keyof T, ValidationRule[]>> = {}139) {140 const form = reactive<Record<keyof T, FormField>>(141 Object.keys(initialValues).reduce((acc, key) => {142 acc[key as keyof T] = {143 value: initialValues[key],144 rules: validationRules[key as keyof T] || [],145 error: '',146 touched: false,147 };148 return acc;149 }, {} as Record<keyof T, FormField>)150 );151 152 const validateField = (fieldName: keyof T): boolean => {153 const field = form[fieldName];154 const rules = field.rules || [];155 156 for (const rule of rules) {157 if (rule.required && (!field.value || field.value === '')) {158 field.error = '此字段为必填项';159 return false;160 }161 162 if (rule.min && field.value.length < rule.min) {163 field.error = `最少需要${rule.min}个字符`;164 return false;165 }166 167 if (rule.max && field.value.length > rule.max) {168 field.error = `最多允许${rule.max}个字符`;169 return false;170 }171 172 if (rule.pattern && !rule.pattern.test(field.value)) {173 field.error = '格式不正确';174 return false;175 }176 177 if (rule.validator) {178 const result = rule.validator(field.value);179 if (result !== true) {180 field.error = typeof result === 'string' ? result : '验证失败';181 return false;182 }183 }184 }185 186 field.error = '';187 return true;188 };189 190 const validateForm = (): boolean => {191 let isValid = true;192 Object.keys(form).forEach(key => {193 const fieldValid = validateField(key as keyof T);194 if (!fieldValid) isValid = false;195 });196 return isValid;197 };198 199 const resetForm = () => {200 Object.keys(form).forEach(key => {201 const field = form[key as keyof T];202 field.value = initialValues[key as keyof T];203 field.error = '';204 field.touched = false;205 });206 };207 208 const setFieldValue = (fieldName: keyof T, value: any) => {209 form[fieldName].value = value;210 form[fieldName].touched = true;211 validateField(fieldName);212 };213 214 const values = computed(() => {215 return Object.keys(form).reduce((acc, key) => {216 acc[key as keyof T] = form[key as keyof T].value;217 return acc;218 }, {} as T);219 });220 221 const errors = computed(() => {222 return Object.keys(form).reduce((acc, key) => {223 const error = form[key as keyof T].error;224 if (error) acc[key as keyof T] = error;225 return acc;226 }, {} as Partial<Record<keyof T, string>>);227 });228 229 const isValid = computed(() => {230 return Object.values(form).every(field => !field.error);231 });232 233 const isDirty = computed(() => {234 return Object.values(form).some(field => field.touched);235 });236 237 return {238 form,239 values,240 errors,241 isValid,242 isDirty,243 validateField,244 validateForm,245 resetForm,246 setFieldValue,247 };248}249250// 4. useInfiniteScroll - 无限滚动Hook251import { ref, onMounted, onUnmounted } from 'vue';252253export function useInfiniteScroll(254 callback: () => void | Promise<void>,255 options: {256 threshold?: number;257 immediate?: boolean;258 } = {}259) {260 const { threshold = 100, immediate = true } = options;261 const loading = ref(false);262 const isEnd = ref(false);263 264 const handleScroll = async () => {265 if (loading.value || isEnd.value) return;266 267 const { scrollTop, scrollHeight, clientHeight } = document.documentElement;268 269 if (scrollTop + clientHeight >= scrollHeight - threshold) {270 loading.value = true;271 try {272 await callback();273 } catch (error) {274 console.error('Infinite scroll callback error:', error);275 } finally {276 loading.value = false;277 }278 }279 };280 281 onMounted(() => {282 if (immediate) {283 window.addEventListener('scroll', handleScroll);284 }285 });286 287 onUnmounted(() => {288 window.removeEventListener('scroll', handleScroll);289 });290 291 const start = () => {292 window.addEventListener('scroll', handleScroll);293 };294 295 const stop = () => {296 window.removeEventListener('scroll', handleScroll);297 };298 299 const setEnd = (end: boolean) => {300 isEnd.value = end;301 };302 303 return {304 loading: readonly(loading),305 isEnd: readonly(isEnd),306 start,307 stop,308 setEnd,309 };310}311312// 5. 使用示例313export default defineComponent({314 setup() {315 // API请求316 const { data: users, loading, error, refresh } = useApi<User[]>('/api/users');317 318 // 本地存储319 const [theme, setTheme] = useLocalStorage('theme', 'light');320 321 // 表单处理322 const { form, values, errors, isValid, setFieldValue, validateForm } = useForm(323 { name: '', email: '', age: 0 },324 {325 name: [{ required: true, min: 2 }],326 email: [{ required: true, pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ }],327 age: [{ required: true, validator: (value) => value >= 18 || '年龄必须大于18岁' }],328 }329 );330 331 // 无限滚动332 const { loading: scrollLoading } = useInfiniteScroll(async () => {333 // 加载更多数据334 await loadMoreUsers();335 });336 337 return {338 users,339 loading,340 error,341 refresh,342 theme,343 setTheme,344 form,345 values,346 errors,347 isValid,348 setFieldValue,349 validateForm,350 scrollLoading,351 };352 },353});Pinia现代状态管理
Pinia Store最佳实践
typescript
1// 1. 用户Store定义2import { defineStore } from 'pinia';3import { computed, ref } from 'vue';4import type { User, LoginCredentials, UserPreferences } from '@/types/user';5import { authAPI } from '@/services/auth';67export const useUserStore = defineStore('user', () => {8 // State9 const currentUser = ref<User | null>(null);10 const preferences = ref<UserPreferences>({11 theme: 'light',12 language: 'zh-CN',13 notifications: true,14 });15 const loading = ref(false);16 const error = ref<string | null>(null);17 18 // Getters19 const isLoggedIn = computed(() => !!currentUser.value);20 const userName = computed(() => currentUser.value?.name || '');21 const userRole = computed(() => currentUser.value?.role || 'guest');22 const isAdmin = computed(() => userRole.value === 'admin');23 24 // Actions25 const setUser = (user: User) => {26 currentUser.value = user;27 error.value = null;28 };29 30 const updatePreferences = (newPreferences: Partial<UserPreferences>) => {31 preferences.value = { ...preferences.value, ...newPreferences };32 };33 34 const login = async (credentials: LoginCredentials) => {35 loading.value = true;36 error.value = null;37 38 try {39 const response = await authAPI.login(credentials);40 currentUser.value = response.user;41 localStorage.setItem('token', response.token);42 return response;43 } catch (err) {44 error.value = err instanceof Error ? err.message : '登录失败';45 throw err;46 } finally {47 loading.value = false;48 }49 };50 51 const logout = async () => {52 try {53 await authAPI.logout();54 } catch (error) {55 console.error('Logout error:', error);56 } finally {57 currentUser.value = null;58 localStorage.removeItem('token');59 }60 };61 62 const updateProfile = async (updates: Partial<User>) => {63 if (!currentUser.value) throw new Error('No user logged in');64 65 loading.value = true;66 try {67 const updatedUser = await authAPI.updateProfile(currentUser.value.id, updates);68 currentUser.value = { ...currentUser.value, ...updatedUser };69 return updatedUser;70 } catch (err) {71 error.value = err instanceof Error ? err.message : '更新失败';72 throw err;73 } finally {74 loading.value = false;75 }76 };77 78 const clearError = () => {79 error.value = null;80 };81 82 // 持久化83 const $persist = {84 storage: localStorage,85 paths: ['currentUser', 'preferences'],86 };87 88 return {89 // State90 currentUser,91 preferences,92 loading,93 error,94 95 // Getters96 isLoggedIn,97 userName,98 userRole,99 isAdmin,100 101 // Actions102 setUser,103 updatePreferences,104 login,105 logout,106 updateProfile,107 clearError,108 109 // Persist config110 $persist,111 };112});113114// 2. 产品Store115export const useProductStore = defineStore('product', () => {116 const products = ref<Product[]>([]);117 const currentProduct = ref<Product | null>(null);118 const loading = ref(false);119 const error = ref<string | null>(null);120 121 // 分页状态122 const pagination = ref({123 page: 1,124 pageSize: 20,125 total: 0,126 });127 128 // 过滤和搜索129 const filters = ref({130 category: '',131 priceRange: [0, 1000],132 inStock: true,133 });134 135 const searchQuery = ref('');136 137 // Getters138 const filteredProducts = computed(() => {139 let result = products.value;140 141 if (searchQuery.value) {142 const query = searchQuery.value.toLowerCase();143 result = result.filter(product =>144 product.name.toLowerCase().includes(query) ||145 product.description.toLowerCase().includes(query)146 );147 }148 149 if (filters.value.category) {150 result = result.filter(product => product.category === filters.value.category);151 }152 153 if (filters.value.inStock) {154 result = result.filter(product => product.stock > 0);155 }156 157 const [minPrice, maxPrice] = filters.value.priceRange;158 result = result.filter(product => 159 product.price >= minPrice && product.price <= maxPrice160 );161 162 return result;163 });164 165 const productsByCategory = computed(() => {166 return products.value.reduce((acc, product) => {167 if (!acc[product.category]) {168 acc[product.category] = [];169 }170 acc[product.category].push(product);171 return acc;172 }, {} as Record<string, Product[]>);173 });174 175 // Actions176 const fetchProducts = async (params?: {177 page?: number;178 pageSize?: number;179 category?: string;180 search?: string;181 }) => {182 loading.value = true;183 error.value = null;184 185 try {186 const response = await productAPI.getProducts(params);187 products.value = response.data;188 pagination.value = {189 page: response.page,190 pageSize: response.pageSize,191 total: response.total,192 };193 } catch (err) {194 error.value = err instanceof Error ? err.message : '获取产品失败';195 throw err;196 } finally {197 loading.value = false;198 }199 };200 201 const fetchProductById = async (id: string) => {202 loading.value = true;203 try {204 const product = await productAPI.getProductById(id);205 currentProduct.value = product;206 return product;207 } catch (err) {208 error.value = err instanceof Error ? err.message : '获取产品详情失败';209 throw err;210 } finally {211 loading.value = false;212 }213 };214 215 const createProduct = async (productData: CreateProductRequest) => {216 loading.value = true;217 try {218 const newProduct = await productAPI.createProduct(productData);219 products.value.unshift(newProduct);220 return newProduct;221 } catch (err) {222 error.value = err instanceof Error ? err.message : '创建产品失败';223 throw err;224 } finally {225 loading.value = false;226 }227 };228 229 const updateProduct = async (id: string, updates: Partial<Product>) => {230 loading.value = true;231 try {232 const updatedProduct = await productAPI.updateProduct(id, updates);233 const index = products.value.findIndex(p => p.id === id);234 if (index !== -1) {235 products.value[index] = updatedProduct;236 }237 if (currentProduct.value?.id === id) {238 currentProduct.value = updatedProduct;239 }240 return updatedProduct;241 } catch (err) {242 error.value = err instanceof Error ? err.message : '更新产品失败';243 throw err;244 } finally {245 loading.value = false;246 }247 };248 249 const deleteProduct = async (id: string) => {250 loading.value = true;251 try {252 await productAPI.deleteProduct(id);253 products.value = products.value.filter(p => p.id !== id);254 if (currentProduct.value?.id === id) {255 currentProduct.value = null;256 }257 } catch (err) {258 error.value = err instanceof Error ? err.message : '删除产品失败';259 throw err;260 } finally {261 loading.value = false;262 }263 };264 265 const setFilters = (newFilters: Partial<typeof filters.value>) => {266 filters.value = { ...filters.value, ...newFilters };267 };268 269 const setSearchQuery = (query: string) => {270 searchQuery.value = query;271 };272 273 const clearFilters = () => {274 filters.value = {275 category: '',276 priceRange: [0, 1000],277 inStock: true,278 };279 searchQuery.value = '';280 };281 282 return {283 // State284 products,285 currentProduct,286 loading,287 error,288 pagination,289 filters,290 searchQuery,291 292 // Getters293 filteredProducts,294 productsByCategory,295 296 // Actions297 fetchProducts,298 fetchProductById,299 createProduct,300 updateProduct,301 deleteProduct,302 setFilters,303 setSearchQuery,304 clearFilters,305 };306});307308// 3. Store组合使用309export function useStores() {310 const userStore = useUserStore();311 const productStore = useProductStore();312 313 return {314 userStore,315 productStore,316 };317}318319// 4. 组件中使用320export default defineComponent({321 setup() {322 const { userStore, productStore } = useStores();323 const { isLoggedIn, userName } = storeToRefs(userStore);324 const { filteredProducts, loading } = storeToRefs(productStore);325 326 onMounted(() => {327 productStore.fetchProducts();328 });329 330 return {331 isLoggedIn,332 userName,333 filteredProducts,334 loading,335 login: userStore.login,336 logout: userStore.logout,337 setFilters: productStore.setFilters,338 };339 },340});4. TypeScript在前端开发中的应用
4.1 TypeScript高级类型系统
TypeScript为前端开发提供了强大的类型安全保障,通过高级类型系统可以构建更健壮的应用。
- 高级类型
- React + TypeScript
- 类型安全实践
高级类型定义与应用
TypeScript高级类型实践
typescript
1// 1. 工具类型和条件类型2type NonNullable<T> = T extends null | undefined ? never : T;3type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;4type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;56// 2. 映射类型7type Partial<T> = {8 [P in keyof T]?: T[P];9};1011type Required<T> = {12 [P in keyof T]-?: T[P];13};1415type Readonly<T> = {16 readonly [P in keyof T]: T[P];17};1819// 3. 自定义工具类型20type DeepPartial<T> = {21 [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];22};2324type DeepReadonly<T> = {25 readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];26};2728type PickByType<T, U> = {29 [P in keyof T as T[P] extends U ? P : never]: T[P];30};3132type OmitByType<T, U> = {33 [P in keyof T as T[P] extends U ? never : P]: T[P];34};3536// 4. 字符串模板类型37type EventName<T extends string> = `on${Capitalize<T>}`;38type CSSProperty = `--${string}`;39type APIEndpoint<T extends string> = `/api/${T}`;4041// 使用示例42type UserEvents = EventName<'click' | 'hover' | 'focus'>; // 'onClick' | 'onHover' | 'onFocus'43type UserAPI = APIEndpoint<'users' | 'posts'>; // '/api/users' | '/api/posts'4445// 5. 递归类型46type JSONValue = 47 | string 48 | number 49 | boolean 50 | null 51 | JSONValue[] 52 | { [key: string]: JSONValue };5354type TreeNode<T> = {55 value: T;56 children?: TreeNode<T>[];57};5859// 6. 品牌类型(Branded Types)60type Brand<T, B> = T & { __brand: B };61type UserId = Brand<string, 'UserId'>;62type Email = Brand<string, 'Email'>;63type URL = Brand<string, 'URL'>;6465const createUserId = (id: string): UserId => id as UserId;66const createEmail = (email: string): Email => {67 if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {68 throw new Error('Invalid email format');69 }70 return email as Email;71};7273// 7. 函数重载类型74interface APIClient {75 get<T>(url: string): Promise<T>;76 get<T>(url: string, config: RequestConfig): Promise<T>;77 post<T, D>(url: string, data: D): Promise<T>;78 post<T, D>(url: string, data: D, config: RequestConfig): Promise<T>;79}8081// 8. 泛型约束82interface Lengthwise {83 length: number;84}8586function loggingIdentity<T extends Lengthwise>(arg: T): T {87 console.log(arg.length);88 return arg;89}9091// 9. 索引访问类型92type Person = {93 name: string;94 age: number;95 address: {96 street: string;97 city: string;98 };99};100101type PersonName = Person['name']; // string102type PersonAddress = Person['address']; // { street: string; city: string; }103type PersonAddressCity = Person['address']['city']; // string104105// 10. 键值映射类型106type APIResponse<T> = {107 data: T;108 status: number;109 message: string;110};111112type UserResponse = APIResponse<User>;113type ProductResponse = APIResponse<Product[]>;114115// 11. 条件类型的实际应用116type ApiFunction<T> = T extends (...args: any[]) => Promise<infer R>117 ? (...args: Parameters<T>) => Promise<ApiResponse<R>>118 : never;119120// 原始函数121declare function fetchUser(id: string): Promise<User>;122declare function fetchProducts(): Promise<Product[]>;123124// 包装后的API函数类型125type WrappedFetchUser = ApiFunction<typeof fetchUser>;126type WrappedFetchProducts = ApiFunction<typeof fetchProducts>;React组件类型定义最佳实践
React TypeScript最佳实践
typescript
1// 1. 组件Props类型定义2interface ButtonProps {3 variant?: 'primary' | 'secondary' | 'danger';4 size?: 'small' | 'medium' | 'large';5 disabled?: boolean;6 loading?: boolean;7 children: React.ReactNode;8 onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;9 className?: string;10 'data-testid'?: string;11}1213// 使用React.FC的替代方案(推荐)14const Button = ({ 15 variant = 'primary', 16 size = 'medium', 17 disabled = false,18 loading = false,19 children,20 onClick,21 className,22 ...rest 23}: ButtonProps) => {24 const baseClasses = 'btn';25 const variantClasses = {26 primary: 'btn-primary',27 secondary: 'btn-secondary',28 danger: 'btn-danger',29 };30 const sizeClasses = {31 small: 'btn-sm',32 medium: 'btn-md',33 large: 'btn-lg',34 };35 36 const classes = [37 baseClasses,38 variantClasses[variant],39 sizeClasses[size],40 className,41 ].filter(Boolean).join(' ');42 43 return (44 <button45 className={classes}46 disabled={disabled || loading}47 onClick={onClick}48 {...rest}49 >50 {loading ? <Spinner size="small" /> : children}51 </button>52 );53};5455// 2. 泛型组件56interface ListProps<T> {57 items: T[];58 renderItem: (item: T, index: number) => React.ReactNode;59 keyExtractor: (item: T) => string | number;60 loading?: boolean;61 emptyMessage?: string;62 className?: string;63}6465function List<T>({66 items,67 renderItem,68 keyExtractor,69 loading = false,70 emptyMessage = 'No items found',71 className,72}: ListProps<T>) {73 if (loading) {74 return <div className="loading">Loading...</div>;75 }76 77 if (items.length === 0) {78 return <div className="empty-state">{emptyMessage}</div>;79 }80 81 return (82 <div className={className}>83 {items.map((item, index) => (84 <div key={keyExtractor(item)}>85 {renderItem(item, index)}86 </div>87 ))}88 </div>89 );90}9192// 使用泛型组件93const UserList = () => {94 const users: User[] = [/* ... */];95 96 return (97 <List98 items={users}99 keyExtractor={(user) => user.id}100 renderItem={(user) => (101 <div>102 <h3>{user.name}</h3>103 <p>{user.email}</p>104 </div>105 )}106 emptyMessage="No users found"107 />108 );109};110111// 3. 高阶组件类型112type WithLoadingProps = {113 loading: boolean;114};115116function withLoading<P extends object>(117 Component: React.ComponentType<P>118): React.ComponentType<P & WithLoadingProps> {119 return ({ loading, ...props }: P & WithLoadingProps) => {120 if (loading) {121 return <div>Loading...</div>;122 }123 return <Component {...(props as P)} />;124 };125}126127// 使用HOC128const UserProfileWithLoading = withLoading(UserProfile);129130// 4. Render Props类型131interface DataFetcherProps<T> {132 url: string;133 children: (data: {134 data: T | null;135 loading: boolean;136 error: Error | null;137 refetch: () => void;138 }) => React.ReactNode;139}140141function DataFetcher<T>({ url, children }: DataFetcherProps<T>) {142 const [data, setData] = useState<T | null>(null);143 const [loading, setLoading] = useState(true);144 const [error, setError] = useState<Error | null>(null);145 146 const fetchData = useCallback(async () => {147 try {148 setLoading(true);149 setError(null);150 const response = await fetch(url);151 const result = await response.json();152 setData(result);153 } catch (err) {154 setError(err as Error);155 } finally {156 setLoading(false);157 }158 }, [url]);159 160 useEffect(() => {161 fetchData();162 }, [fetchData]);163 164 return <>{children({ data, loading, error, refetch: fetchData })}</>;165}166167// 使用Render Props168const UserProfile = ({ userId }: { userId: string }) => (169 <DataFetcher<User> url={`/api/users/${userId}`}>170 {({ data: user, loading, error, refetch }) => {171 if (loading) return <div>Loading...</div>;172 if (error) return <div>Error: {error.message}</div>;173 if (!user) return <div>User not found</div>;174 175 return (176 <div>177 <h1>{user.name}</h1>178 <p>{user.email}</p>179 <button onClick={refetch}>Refresh</button>180 </div>181 );182 }}183 </DataFetcher>184);185186// 5. Context类型定义187interface AuthContextType {188 user: User | null;189 login: (credentials: LoginCredentials) => Promise<void>;190 logout: () => void;191 loading: boolean;192}193194const AuthContext = createContext<AuthContextType | undefined>(undefined);195196export const useAuth = (): AuthContextType => {197 const context = useContext(AuthContext);198 if (context === undefined) {199 throw new Error('useAuth must be used within an AuthProvider');200 }201 return context;202};203204export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {205 const [user, setUser] = useState<User | null>(null);206 const [loading, setLoading] = useState(false);207 208 const login = async (credentials: LoginCredentials) => {209 setLoading(true);210 try {211 const response = await authAPI.login(credentials);212 setUser(response.user);213 } finally {214 setLoading(false);215 }216 };217 218 const logout = () => {219 setUser(null);220 localStorage.removeItem('token');221 };222 223 const value: AuthContextType = {224 user,225 login,226 logout,227 loading,228 };229 230 return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;231};232233// 6. 自定义Hook类型234interface UseApiResult<T> {235 data: T | null;236 loading: boolean;237 error: Error | null;238 refetch: () => Promise<void>;239}240241function useApi<T>(url: string): UseApiResult<T> {242 const [data, setData] = useState<T | null>(null);243 const [loading, setLoading] = useState(true);244 const [error, setError] = useState<Error | null>(null);245 246 const fetchData = useCallback(async () => {247 try {248 setLoading(true);249 setError(null);250 const response = await fetch(url);251 if (!response.ok) {252 throw new Error(`HTTP error! status: ${response.status}`);253 }254 const result = await response.json();255 setData(result);256 } catch (err) {257 setError(err as Error);258 } finally {259 setLoading(false);260 }261 }, [url]);262 263 useEffect(() => {264 fetchData();265 }, [fetchData]);266 267 return { data, loading, error, refetch: fetchData };268}269270// 7. 事件处理类型271interface FormProps {272 onSubmit: (data: FormData) => void;273 onChange: (field: string, value: any) => void;274}275276const Form: React.FC<FormProps> = ({ onSubmit, onChange }) => {277 const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {278 e.preventDefault();279 const formData = new FormData(e.currentTarget);280 onSubmit(formData);281 };282 283 const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {284 onChange(e.target.name, e.target.value);285 };286 287 return (288 <form onSubmit={handleSubmit}>289 <input name="username" onChange={handleInputChange} />290 <button type="submit">Submit</button>291 </form>292 );293};294295// 8. Ref类型296interface ModalRef {297 open: () => void;298 close: () => void;299}300301const Modal = forwardRef<ModalRef, { children: React.ReactNode }>(302 ({ children }, ref) => {303 const [isOpen, setIsOpen] = useState(false);304 305 useImperativeHandle(ref, () => ({306 open: () => setIsOpen(true),307 close: () => setIsOpen(false),308 }));309 310 if (!isOpen) return null;311 312 return (313 <div className="modal">314 <div className="modal-content">315 {children}316 <button onClick={() => setIsOpen(false)}>Close</button>317 </div>318 </div>319 );320 }321);322323// 使用Ref324const App = () => {325 const modalRef = useRef<ModalRef>(null);326 327 return (328 <div>329 <button onClick={() => modalRef.current?.open()}>Open Modal</button>330 <Modal ref={modalRef}>331 <p>Modal content</p>332 </Modal>333 </div>334 );335};类型安全最佳实践
类型安全实践指南
typescript
1// 1. 严格的tsconfig.json配置2{3 "compilerOptions": {4 "strict": true,5 "noImplicitAny": true,6 "noImplicitReturns": true,7 "noImplicitThis": true,8 "noUnusedLocals": true,9 "noUnusedParameters": true,10 "exactOptionalPropertyTypes": true,11 "noUncheckedIndexedAccess": true,12 "noImplicitOverride": true13 }14}1516// 2. 类型守卫(Type Guards)17function isString(value: unknown): value is string {18 return typeof value === 'string';19}2021function isUser(obj: unknown): obj is User {22 return (23 typeof obj === 'object' &&24 obj !== null &&25 'id' in obj &&26 'name' in obj &&27 'email' in obj28 );29}3031function isApiError(error: unknown): error is ApiError {32 return (33 error instanceof Error &&34 'status' in error &&35 'code' in error36 );37}3839// 使用类型守卫40function handleApiResponse(response: unknown) {41 if (isUser(response)) {42 // TypeScript知道这里response是User类型43 console.log(response.name);44 } else if (isApiError(response)) {45 // TypeScript知道这里response是ApiError类型46 console.error(`API Error: ${response.status} - ${response.message}`);47 }48}4950// 3. 断言函数(Assertion Functions)51function assertIsNumber(value: unknown): asserts value is number {52 if (typeof value !== 'number') {53 throw new Error('Expected number');54 }55}5657function assertIsUser(obj: unknown): asserts obj is User {58 if (!isUser(obj)) {59 throw new Error('Expected User object');60 }61}6263// 使用断言函数64function processUserData(data: unknown) {65 assertIsUser(data);66 // 这里TypeScript知道data是User类型67 console.log(data.name);68}6970// 4. 判别联合类型(Discriminated Unions)71type LoadingState = {72 status: 'loading';73};7475type SuccessState = {76 status: 'success';77 data: any;78};7980type ErrorState = {81 status: 'error';82 error: string;83};8485type AsyncState = LoadingState | SuccessState | ErrorState;8687function handleAsyncState(state: AsyncState) {88 switch (state.status) {89 case 'loading':90 return <div>Loading...</div>;91 case 'success':92 // TypeScript知道这里state有data属性93 return <div>Data: {JSON.stringify(state.data)}</div>;94 case 'error':95 // TypeScript知道这里state有error属性96 return <div>Error: {state.error}</div>;97 default:98 // 确保所有情况都被处理99 const _exhaustiveCheck: never = state;100 return _exhaustiveCheck;101 }102}103104// 5. 模板字面量类型的实际应用105type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';106type ApiEndpoint = `/api/${string}`;107type ApiCall<M extends HttpMethod, E extends ApiEndpoint> = {108 method: M;109 endpoint: E;110 data?: M extends 'GET' | 'DELETE' ? never : any;111};112113// 类型安全的API调用114const getUserCall: ApiCall<'GET', '/api/users'> = {115 method: 'GET',116 endpoint: '/api/users',117 // data: {} // 这里会报错,因为GET请求不应该有data118};119120const createUserCall: ApiCall<'POST', '/api/users'> = {121 method: 'POST',122 endpoint: '/api/users',123 data: { name: 'John', email: 'john@example.com' }124};125126// 6. 递归类型的实际应用127type DeepReadonly<T> = {128 readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];129};130131type Config = {132 database: {133 host: string;134 port: number;135 credentials: {136 username: string;137 password: string;138 };139 };140 api: {141 baseUrl: string;142 timeout: number;143 };144};145146type ReadonlyConfig = DeepReadonly<Config>;147// 所有嵌套属性都变为readonly148149// 7. 条件类型的实际应用150type NonNullable<T> = T extends null | undefined ? never : T;151type FunctionPropertyNames<T> = {152 [K in keyof T]: T[K] extends Function ? K : never;153}[keyof T];154155type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>;156157class UserService {158 name = 'UserService';159 version = '1.0.0';160 161 getUser(id: string): Promise<User> {162 return Promise.resolve({} as User);163 }164 165 createUser(data: CreateUserRequest): Promise<User> {166 return Promise.resolve({} as User);167 }168 169 updateUser(id: string, data: Partial<User>): Promise<User> {170 return Promise.resolve({} as User);171 }172}173174type UserServiceMethods = FunctionProperties<UserService>;175// 只包含方法的类型:{ getUser: ..., createUser: ..., updateUser: ... }176177// 8. 类型安全的环境变量178interface EnvironmentVariables {179 NODE_ENV: 'development' | 'production' | 'test';180 API_BASE_URL: string;181 DATABASE_URL: string;182 JWT_SECRET: string;183}184185function getEnvVar<K extends keyof EnvironmentVariables>(186 key: K187): EnvironmentVariables[K] {188 const value = process.env[key];189 if (!value) {190 throw new Error(`Environment variable ${key} is not defined`);191 }192 return value as EnvironmentVariables[K];193}194195// 类型安全的使用196const nodeEnv = getEnvVar('NODE_ENV'); // 类型为 'development' | 'production' | 'test'197const apiUrl = getEnvVar('API_BASE_URL'); // 类型为 string198199// 9. 类型安全的事件系统200type EventMap = {201 'user:login': { userId: string; timestamp: number };202 'user:logout': { userId: string };203 'order:created': { orderId: string; userId: string; amount: number };204 'order:cancelled': { orderId: string; reason: string };205};206207class TypedEventEmitter {208 private listeners: {209 [K in keyof EventMap]?: Array<(data: EventMap[K]) => void>;210 } = {};211 212 on<K extends keyof EventMap>(213 event: K,214 listener: (data: EventMap[K]) => void215 ): void {216 if (!this.listeners[event]) {217 this.listeners[event] = [];218 }219 this.listeners[event]!.push(listener);220 }221 222 emit<K extends keyof EventMap>(event: K, data: EventMap[K]): void {223 const eventListeners = this.listeners[event];224 if (eventListeners) {225 eventListeners.forEach(listener => listener(data));226 }227 }228}229230// 类型安全的使用231const emitter = new TypedEventEmitter();232233emitter.on('user:login', (data) => {234 // data的类型自动推断为 { userId: string; timestamp: number }235 console.log(`User ${data.userId} logged in at ${data.timestamp}`);236});237238emitter.emit('user:login', {239 userId: '123',240 timestamp: Date.now()241});242243// 10. 类型安全的路由系统244type RouteParams = {245 '/': {};246 '/users': {};247 '/users/:id': { id: string };248 '/users/:id/posts': { id: string };249 '/users/:id/posts/:postId': { id: string; postId: string };250};251252function navigate<T extends keyof RouteParams>(253 path: T,254 ...args: RouteParams[T] extends {} 255 ? [RouteParams[T]] extends [{}] 256 ? [] 257 : [RouteParams[T]]258 : [RouteParams[T]]259): void {260 // 路由导航逻辑261 console.log('Navigating to:', path, args);262}263264// 类型安全的使用265navigate('/'); // OK266navigate('/users'); // OK267navigate('/users/:id', { id: '123' }); // OK268navigate('/users/:id/posts/:postId', { id: '123', postId: '456' }); // OK269// navigate('/users/:id'); // 错误:缺少参数270// navigate('/users/:id', { id: 123 }); // 错误:id应该是string类型5. 前端性能优化全方位指南
5.1 加载性能优化策略
前端性能优化是提升用户体验的关键,需要从多个维度进行系统性优化。
- 打包优化
- 资源优化
- 运行时优化
代码分割与懒加载
代码分割最佳实践
typescript
1// 1. 路由级别的代码分割2import { lazy, Suspense } from 'react';3import { Routes, Route } from 'react-router-dom';45// 懒加载组件6const Home = lazy(() => import('./pages/Home'));7const About = lazy(() => import('./pages/About'));8const Dashboard = lazy(() => import('./pages/Dashboard'));9const UserProfile = lazy(() => import('./pages/UserProfile'));1011// 预加载关键路由12const AdminPanel = lazy(() => 13 import(/* webpackChunkName: "admin" */ './pages/AdminPanel')14);1516// 条件加载17const AdvancedFeatures = lazy(() => 18 import(/* webpackChunkName: "advanced" */ './components/AdvancedFeatures')19);2021function App() {22 return (23 <Suspense fallback={<PageSkeleton />}>24 <Routes>25 <Route path="/" element={<Home />} />26 <Route path="/about" element={<About />} />27 <Route path="/dashboard" element={<Dashboard />} />28 <Route path="/profile" element={<UserProfile />} />29 <Route path="/admin" element={<AdminPanel />} />30 </Routes>31 </Suspense>32 );33}3435// 2. 组件级别的懒加载36const LazyModal = lazy(() => import('./components/Modal'));37const LazyChart = lazy(() => import('./components/Chart'));3839function Dashboard() {40 const [showModal, setShowModal] = useState(false);41 const [showChart, setShowChart] = useState(false);42 43 return (44 <div>45 <h1>Dashboard</h1>46 47 {/* 条件渲染懒加载组件 */}48 {showModal && (49 <Suspense fallback={<div>Loading modal...</div>}>50 <LazyModal onClose={() => setShowModal(false)} />51 </Suspense>52 )}53 54 {showChart && (55 <Suspense fallback={<ChartSkeleton />}>56 <LazyChart data={chartData} />57 </Suspense>58 )}59 60 <button onClick={() => setShowModal(true)}>Open Modal</button>61 <button onClick={() => setShowChart(true)}>Show Chart</button>62 </div>63 );64}6566// 3. 动态导入工具函数67async function loadUtility() {68 const { heavyUtilityFunction } = await import('./utils/heavyUtils');69 return heavyUtilityFunction;70}7172// 4. 预加载策略73function usePreloadRoute(routePath: string) {74 useEffect(() => {75 const timer = setTimeout(() => {76 // 在空闲时间预加载路由77 if ('requestIdleCallback' in window) {78 requestIdleCallback(() => {79 import(/* webpackChunkName: "[request]" */ `./pages/${routePath}`);80 });81 }82 }, 2000);83 84 return () => clearTimeout(timer);85 }, [routePath]);86}8788// 5. Webpack配置优化89// webpack.config.js90module.exports = {91 optimization: {92 splitChunks: {93 chunks: 'all',94 cacheGroups: {95 // 第三方库单独打包96 vendor: {97 test: /[\\/]node_modules[\\/]/,98 name: 'vendors',99 chunks: 'all',100 priority: 10,101 },102 // 公共组件103 common: {104 name: 'common',105 minChunks: 2,106 chunks: 'all',107 priority: 5,108 reuseExistingChunk: true,109 },110 // React相关库111 react: {112 test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,113 name: 'react',114 chunks: 'all',115 priority: 20,116 },117 },118 },119 // 运行时代码单独提取120 runtimeChunk: {121 name: 'runtime',122 },123 },124};125126// 6. Vite配置优化127// vite.config.ts128export default defineConfig({129 build: {130 rollupOptions: {131 output: {132 manualChunks: {133 // 第三方库分包134 vendor: ['react', 'react-dom'],135 router: ['react-router-dom'],136 ui: ['antd', '@ant-design/icons'],137 utils: ['lodash', 'dayjs'],138 },139 },140 },141 // 启用压缩142 minify: 'terser',143 terserOptions: {144 compress: {145 drop_console: true,146 drop_debugger: true,147 },148 },149 },150});静态资源优化策略
资源优化实践
typescript
1// 1. 图片优化组件2import { useState, useRef, useEffect } from 'react';34interface OptimizedImageProps {5 src: string;6 alt: string;7 width?: number;8 height?: number;9 loading?: 'lazy' | 'eager';10 placeholder?: string;11 className?: string;12}1314const OptimizedImage: React.FC<OptimizedImageProps> = ({15 src,16 alt,17 width,18 height,19 loading = 'lazy',20 placeholder,21 className,22}) => {23 const [isLoaded, setIsLoaded] = useState(false);24 const [error, setError] = useState(false);25 const imgRef = useRef<HTMLImageElement>(null);26 27 // 生成不同尺寸的图片URL28 const generateSrcSet = (baseSrc: string) => {29 const sizes = [320, 640, 960, 1280, 1920];30 return sizes31 .map(size => `${baseSrc}?w=${size} ${size}w`)32 .join(', ');33 };34 35 // WebP支持检测36 const supportsWebP = () => {37 const canvas = document.createElement('canvas');38 canvas.width = 1;39 canvas.height = 1;40 return canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0;41 };42 43 // 获取优化后的图片URL44 const getOptimizedSrc = (originalSrc: string) => {45 if (supportsWebP()) {46 return originalSrc.replace(/\.(jpg|jpeg|png)$/i, '.webp');47 }48 return originalSrc;49 };50 51 useEffect(() => {52 if (!imgRef.current) return;53 54 const observer = new IntersectionObserver(55 (entries) => {56 entries.forEach((entry) => {57 if (entry.isIntersecting) {58 const img = entry.target as HTMLImageElement;59 img.src = getOptimizedSrc(src);60 observer.unobserve(img);61 }62 });63 },64 { threshold: 0.1 }65 );66 67 if (loading === 'lazy') {68 observer.observe(imgRef.current);69 }70 71 return () => observer.disconnect();72 }, [src, loading]);73 74 return (75 <div className={`image-container ${className || ''}`}>76 {!isLoaded && placeholder && (77 <div className="image-placeholder">78 <img src={placeholder} alt="" />79 </div>80 )}81 82 <img83 ref={imgRef}84 alt={alt}85 width={width}86 height={height}87 loading={loading}88 srcSet={loading === 'eager' ? generateSrcSet(src) : undefined}89 sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"90 onLoad={() => setIsLoaded(true)}91 onError={() => setError(true)}92 style={{93 opacity: isLoaded ? 1 : 0,94 transition: 'opacity 0.3s ease',95 }}96 />97 98 {error && (99 <div className="image-error">100 Failed to load image101 </div>102 )}103 </div>104 );105};106107// 2. 字体优化108// CSS中的字体优化109const fontOptimizationCSS = `110/* 字体预加载 */111@font-face {112 font-family: 'CustomFont';113 src: url('/fonts/custom-font.woff2') format('woff2'),114 url('/fonts/custom-font.woff') format('woff');115 font-display: swap; /* 重要:避免字体加载阻塞 */116 font-weight: 400;117 font-style: normal;118}119120/* 字体子集化 */121@font-face {122 font-family: 'CustomFont';123 src: url('/fonts/custom-font-latin.woff2') format('woff2');124 font-display: swap;125 unicode-range: U+0000-00FF, U+0131, U+0152-0153;126}127128/* 系统字体栈 */129body {130 font-family: 131 -apple-system,132 BlinkMacSystemFont,133 'Segoe UI',134 'Roboto',135 'Oxygen',136 'Ubuntu',137 'Cantarell',138 'Fira Sans',139 'Droid Sans',140 'Helvetica Neue',141 sans-serif;142}143`;144145// 3. CSS优化146const cssOptimization = `147/* 关键CSS内联 */148<style>149 /* 首屏关键样式 */150 .header { /* ... */ }151 .hero { /* ... */ }152 .navigation { /* ... */ }153</style>154155/* 非关键CSS延迟加载 */156<link rel="preload" href="/css/non-critical.css" as="style" onload="this.onload=null;this.rel='stylesheet'">157<noscript><link rel="stylesheet" href="/css/non-critical.css"></noscript>158`;159160// 4. JavaScript优化161// 使用Web Workers处理重计算162class PerformanceWorker {163 private worker: Worker | null = null;164 165 constructor() {166 if (typeof Worker !== 'undefined') {167 this.worker = new Worker('/workers/performance-worker.js');168 }169 }170 171 async processLargeDataset(data: any[]): Promise<any> {172 if (!this.worker) {173 // Fallback to main thread174 return this.processInMainThread(data);175 }176 177 return new Promise((resolve, reject) => {178 this.worker!.postMessage({ type: 'PROCESS_DATA', data });179 180 this.worker!.onmessage = (event) => {181 if (event.data.type === 'PROCESS_COMPLETE') {182 resolve(event.data.result);183 } else if (event.data.type === 'PROCESS_ERROR') {184 reject(new Error(event.data.error));185 }186 };187 });188 }189 190 private processInMainThread(data: any[]): any {191 // 主线程处理逻辑192 return data.map(item => ({ ...item, processed: true }));193 }194 195 destroy() {196 if (this.worker) {197 this.worker.terminate();198 this.worker = null;199 }200 }201}202203// 5. Service Worker缓存策略204// service-worker.js205const CACHE_NAME = 'app-cache-v1';206const STATIC_ASSETS = [207 '/',208 '/static/css/main.css',209 '/static/js/main.js',210 '/manifest.json',211];212213// 安装事件 - 缓存静态资源214self.addEventListener('install', (event) => {215 event.waitUntil(216 caches.open(CACHE_NAME)217 .then((cache) => cache.addAll(STATIC_ASSETS))218 );219});220221// 网络请求拦截 - 缓存策略222self.addEventListener('fetch', (event) => {223 const { request } = event;224 const url = new URL(request.url);225 226 // API请求 - 网络优先策略227 if (url.pathname.startsWith('/api/')) {228 event.respondWith(229 fetch(request)230 .then((response) => {231 const responseClone = response.clone();232 caches.open(CACHE_NAME)233 .then((cache) => cache.put(request, responseClone));234 return response;235 })236 .catch(() => caches.match(request))237 );238 return;239 }240 241 // 静态资源 - 缓存优先策略242 event.respondWith(243 caches.match(request)244 .then((response) => {245 if (response) {246 return response;247 }248 return fetch(request);249 })250 );251});运行时性能优化
运行时性能优化策略
typescript
1// 1. 虚拟滚动实现2import { useState, useEffect, useMemo, useCallback } from 'react';34interface VirtualScrollProps<T> {5 items: T[];6 itemHeight: number;7 containerHeight: number;8 renderItem: (item: T, index: number) => React.ReactNode;9 overscan?: number;10}1112function VirtualScroll<T>({13 items,14 itemHeight,15 containerHeight,16 renderItem,17 overscan = 5,18}: VirtualScrollProps<T>) {19 const [scrollTop, setScrollTop] = useState(0);20 21 // 计算可见范围22 const visibleRange = useMemo(() => {23 const start = Math.floor(scrollTop / itemHeight);24 const end = Math.min(25 start + Math.ceil(containerHeight / itemHeight) + overscan,26 items.length27 );28 29 return {30 start: Math.max(0, start - overscan),31 end,32 };33 }, [scrollTop, itemHeight, containerHeight, items.length, overscan]);34 35 // 可见项目36 const visibleItems = useMemo(() => {37 return items.slice(visibleRange.start, visibleRange.end);38 }, [items, visibleRange]);39 40 // 滚动处理41 const handleScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {42 setScrollTop(e.currentTarget.scrollTop);43 }, []);44 45 // 总高度46 const totalHeight = items.length * itemHeight;47 48 // 偏移量49 const offsetY = visibleRange.start * itemHeight;50 51 return (52 <div53 style={{54 height: containerHeight,55 overflow: 'auto',56 }}57 onScroll={handleScroll}58 >59 <div style={{ height: totalHeight, position: 'relative' }}>60 <div61 style={{62 transform: `translateY(${offsetY}px)`,63 position: 'absolute',64 top: 0,65 left: 0,66 right: 0,67 }}68 >69 {visibleItems.map((item, index) => (70 <div71 key={visibleRange.start + index}72 style={{ height: itemHeight }}73 >74 {renderItem(item, visibleRange.start + index)}75 </div>76 ))}77 </div>78 </div>79 </div>80 );81}8283// 2. 防抖和节流Hook84function useDebounce<T>(value: T, delay: number): T {85 const [debouncedValue, setDebouncedValue] = useState<T>(value);86 87 useEffect(() => {88 const handler = setTimeout(() => {89 setDebouncedValue(value);90 }, delay);91 92 return () => clearTimeout(handler);93 }, [value, delay]);94 95 return debouncedValue;96}9798function useThrottle<T>(value: T, limit: number): T {99 const [throttledValue, setThrottledValue] = useState<T>(value);100 const lastRan = useRef(Date.now());101 102 useEffect(() => {103 const handler = setTimeout(() => {104 if (Date.now() - lastRan.current >= limit) {105 setThrottledValue(value);106 lastRan.current = Date.now();107 }108 }, limit - (Date.now() - lastRan.current));109 110 return () => clearTimeout(handler);111 }, [value, limit]);112 113 return throttledValue;114}115116// 3. 内存泄漏防护117function useEventListener<T extends keyof WindowEventMap>(118 eventName: T,119 handler: (event: WindowEventMap[T]) => void,120 element: Window | HTMLElement = window121) {122 const savedHandler = useRef(handler);123 124 useEffect(() => {125 savedHandler.current = handler;126 }, [handler]);127 128 useEffect(() => {129 const eventListener = (event: Event) => {130 savedHandler.current(event as WindowEventMap[T]);131 };132 133 element.addEventListener(eventName, eventListener);134 135 return () => {136 element.removeEventListener(eventName, eventListener);137 };138 }, [eventName, element]);139}140141// 4. 性能监控Hook142function usePerformanceMonitor() {143 const [metrics, setMetrics] = useState({144 renderTime: 0,145 memoryUsage: 0,146 fps: 0,147 });148 149 useEffect(() => {150 let frameCount = 0;151 let lastTime = performance.now();152 let animationId: number;153 154 const measureFPS = () => {155 frameCount++;156 const currentTime = performance.now();157 158 if (currentTime >= lastTime + 1000) {159 const fps = Math.round((frameCount * 1000) / (currentTime - lastTime));160 161 setMetrics(prev => ({162 ...prev,163 fps,164 memoryUsage: (performance as any).memory?.usedJSHeapSize || 0,165 }));166 167 frameCount = 0;168 lastTime = currentTime;169 }170 171 animationId = requestAnimationFrame(measureFPS);172 };173 174 animationId = requestAnimationFrame(measureFPS);175 176 return () => cancelAnimationFrame(animationId);177 }, []);178 179 const measureRenderTime = useCallback((componentName: string) => {180 const startTime = performance.now();181 182 return () => {183 const endTime = performance.now();184 const renderTime = endTime - startTime;185 186 console.log(`${componentName} render time: ${renderTime.toFixed(2)}ms`);187 188 setMetrics(prev => ({189 ...prev,190 renderTime,191 }));192 };193 }, []);194 195 return { metrics, measureRenderTime };196}197198// 5. 组件性能分析199const withPerformanceTracking = <P extends object>(200 WrappedComponent: React.ComponentType<P>,201 componentName: string202) => {203 return React.memo((props: P) => {204 const { measureRenderTime } = usePerformanceMonitor();205 206 useEffect(() => {207 const endMeasure = measureRenderTime(componentName);208 return endMeasure;209 });210 211 return <WrappedComponent {...props} />;212 });213};214215// 6. 长列表优化216interface OptimizedListProps<T> {217 items: T[];218 renderItem: (item: T, index: number) => React.ReactNode;219 getItemKey: (item: T, index: number) => string | number;220 estimatedItemHeight?: number;221 threshold?: number;222}223224function OptimizedList<T>({225 items,226 renderItem,227 getItemKey,228 estimatedItemHeight = 50,229 threshold = 100,230}: OptimizedListProps<T>) {231 const [visibleItems, setVisibleItems] = useState<T[]>([]);232 const [hasMore, setHasMore] = useState(true);233 const containerRef = useRef<HTMLDivElement>(null);234 235 // 无限滚动236 const { loading } = useInfiniteScroll(237 async () => {238 const startIndex = visibleItems.length;239 const endIndex = Math.min(startIndex + threshold, items.length);240 241 if (startIndex >= items.length) {242 setHasMore(false);243 return;244 }245 246 const newItems = items.slice(startIndex, endIndex);247 setVisibleItems(prev => [...prev, ...newItems]);248 },249 { threshold: 200 }250 );251 252 // 初始化253 useEffect(() => {254 const initialItems = items.slice(0, threshold);255 setVisibleItems(initialItems);256 setHasMore(items.length > threshold);257 }, [items, threshold]);258 259 return (260 <div ref={containerRef} className="optimized-list">261 {visibleItems.map((item, index) => (262 <div key={getItemKey(item, index)} className="list-item">263 {renderItem(item, index)}264 </div>265 ))}266 267 {loading && <div className="loading">Loading more...</div>}268 {!hasMore && <div className="end">No more items</div>}269 </div>270 );271}272273// 7. 使用示例274function App() {275 const [searchQuery, setSearchQuery] = useState('');276 const debouncedQuery = useDebounce(searchQuery, 300);277 const { metrics } = usePerformanceMonitor();278 279 const largeDataset = useMemo(() => 280 Array.from({ length: 10000 }, (_, i) => ({281 id: i,282 name: `Item ${i}`,283 value: Math.random() * 100,284 }))285 , []);286 287 const filteredData = useMemo(() => 288 largeDataset.filter(item => 289 item.name.toLowerCase().includes(debouncedQuery.toLowerCase())290 )291 , [largeDataset, debouncedQuery]);292 293 return (294 <div>295 <div className="performance-metrics">296 <span>FPS: {metrics.fps}</span>297 <span>Memory: {(metrics.memoryUsage / 1024 / 1024).toFixed(2)}MB</span>298 </div>299 300 <input301 type="text"302 value={searchQuery}303 onChange={(e) => setSearchQuery(e.target.value)}304 placeholder="Search items..."305 />306 307 <VirtualScroll308 items={filteredData}309 itemHeight={60}310 containerHeight={400}311 renderItem={(item) => (312 <div className="item">313 <h3>{item.name}</h3>314 <p>Value: {item.value.toFixed(2)}</p>315 </div>316 )}317 />318 </div>319 );320}6. 现代CSS与样式解决方案
6.1 CSS-in-JS与原子化CSS
现代前端开发中,CSS的组织和管理方式发生了重大变革,从传统的CSS文件到CSS-in-JS,再到原子化CSS。
- CSS-in-JS
- Tailwind CSS
- CSS Modules
Styled Components与Emotion
CSS-in-JS最佳实践
typescript
1// 1. Styled Components基础用法2import styled, { css, ThemeProvider, createGlobalStyle } from 'styled-components';34// 主题定义5const theme = {6 colors: {7 primary: '#007bff',8 secondary: '#6c757d',9 success: '#28a745',10 danger: '#dc3545',11 warning: '#ffc107',12 info: '#17a2b8',13 light: '#f8f9fa',14 dark: '#343a40',15 },16 spacing: {17 xs: '0.25rem',18 sm: '0.5rem',19 md: '1rem',20 lg: '1.5rem',21 xl: '3rem',22 },23 breakpoints: {24 mobile: '576px',25 tablet: '768px',26 desktop: '992px',27 wide: '1200px',28 },29 shadows: {30 sm: '0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24)',31 md: '0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23)',32 lg: '0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23)',33 },34};3536// 全局样式37const GlobalStyle = createGlobalStyle`38 * {39 box-sizing: border-box;40 margin: 0;41 padding: 0;42 }43 44 body {45 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;46 line-height: 1.6;47 color: ${props => props.theme.colors.dark};48 background-color: ${props => props.theme.colors.light};49 }50 51 a {52 color: ${props => props.theme.colors.primary};53 text-decoration: none;54 55 &:hover {56 text-decoration: underline;57 }58 }59`;6061// 2. 基础组件样式62interface ButtonProps {63 variant?: 'primary' | 'secondary' | 'danger';64 size?: 'small' | 'medium' | 'large';65 fullWidth?: boolean;66 disabled?: boolean;67}6869const Button = styled.button<ButtonProps>`70 display: inline-flex;71 align-items: center;72 justify-content: center;73 border: none;74 border-radius: 4px;75 font-weight: 500;76 cursor: pointer;77 transition: all 0.2s ease-in-out;78 79 /* 尺寸变体 */80 ${props => {81 switch (props.size) {82 case 'small':83 return css`848586`;87 case 'large':88 return css`899091`;92 default:93 return css`949596`;97 }98 }}99 100 /* 颜色变体 */101 ${props => {102 const color = props.theme.colors[props.variant || 'primary'];103 return css`104105106107108109110111112113114115116117`;118 }}119 120 /* 全宽度 */121 ${props => props.fullWidth && css`122123`}124 125 /* 禁用状态 */126 ${props => props.disabled && css`127128129130131132133134`}135 136 /* 响应式设计 */137 @media (max-width: ${props => props.theme.breakpoints.mobile}) {138 padding: ${props => props.theme.spacing.sm};139 font-size: 0.875rem;140 }141`;142143// 3. 复杂组件样式144const Card = styled.div`145 background: white;146 border-radius: 8px;147 box-shadow: ${props => props.theme.shadows.sm};148 overflow: hidden;149 transition: box-shadow 0.2s ease-in-out;150 151 &:hover {152 box-shadow: ${props => props.theme.shadows.md};153 }154`;155156const CardHeader = styled.div`157 padding: ${props => props.theme.spacing.lg};158 border-bottom: 1px solid ${props => props.theme.colors.light};159 160 h3 {161 margin: 0;162 color: ${props => props.theme.colors.dark};163 }164`;165166const CardBody = styled.div`167 padding: ${props => props.theme.spacing.lg};168`;169170const CardFooter = styled.div`171 padding: ${props => props.theme.spacing.md} ${props => props.theme.spacing.lg};172 background-color: ${props => props.theme.colors.light};173 border-top: 1px solid #dee2e6;174 175 display: flex;176 justify-content: flex-end;177 gap: ${props => props.theme.spacing.sm};178`;179180// 4. 动画和过渡181const fadeIn = css`182 @keyframes fadeIn {183 from {184 opacity: 0;185 transform: translateY(20px);186 }187 to {188 opacity: 1;189 transform: translateY(0);190 }191 }192`;193194const AnimatedContainer = styled.div`195 ${fadeIn}196 animation: fadeIn 0.3s ease-out;197`;198199// 5. 响应式工具200const media = {201 mobile: (styles: TemplateStringsArray | string) => css`202 @media (max-width: ${props => props.theme.breakpoints.mobile}) {203 ${styles}204 }205 `,206 tablet: (styles: TemplateStringsArray | string) => css`207 @media (max-width: ${props => props.theme.breakpoints.tablet}) {208 ${styles}209 }210 `,211 desktop: (styles: TemplateStringsArray | string) => css`212 @media (min-width: ${props => props.theme.breakpoints.desktop}) {213 ${styles}214 }215 `,216};217218const ResponsiveGrid = styled.div`219 display: grid;220 gap: ${props => props.theme.spacing.md};221 grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));222 223 ${media.mobile`224 grid-template-columns: 1fr;225 gap: ${props => props.theme.spacing.sm};226 `}227`;228229// 6. 使用示例230function App() {231 return (232 <ThemeProvider theme={theme}>233 <GlobalStyle />234 <div>235 <ResponsiveGrid>236 <Card>237 <CardHeader>238 <h3>Card Title</h3>239 </CardHeader>240 <CardBody>241 <p>Card content goes here...</p>242 </CardBody>243 <CardFooter>244 <Button variant="secondary" size="small">245 Cancel246 </Button>247 <Button variant="primary" size="small">248 Save249 </Button>250 </CardFooter>251 </Card>252 </ResponsiveGrid>253 </div>254 </ThemeProvider>255 );256}原子化CSS最佳实践
Tailwind CSS高级应用
typescript
1// 1. Tailwind配置文件2// tailwind.config.js3module.exports = {4 content: [5 './src/**/*.{js,jsx,ts,tsx}',6 './public/index.html',7 ],8 theme: {9 extend: {10 colors: {11 primary: {12 50: '#eff6ff',13 100: '#dbeafe',14 500: '#3b82f6',15 600: '#2563eb',16 700: '#1d4ed8',17 900: '#1e3a8a',18 },19 gray: {20 50: '#f9fafb',21 100: '#f3f4f6',22 200: '#e5e7eb',23 300: '#d1d5db',24 400: '#9ca3af',25 500: '#6b7280',26 600: '#4b5563',27 700: '#374151',28 800: '#1f2937',29 900: '#111827',30 },31 },32 spacing: {33 '18': '4.5rem',34 '88': '22rem',35 },36 animation: {37 'fade-in': 'fadeIn 0.5s ease-in-out',38 'slide-up': 'slideUp 0.3s ease-out',39 'bounce-in': 'bounceIn 0.6s ease-out',40 },41 keyframes: {42 fadeIn: {43 '0%': { opacity: '0' },44 '100%': { opacity: '1' },45 },46 slideUp: {47 '0%': { transform: 'translateY(100%)', opacity: '0' },48 '100%': { transform: 'translateY(0)', opacity: '1' },49 },50 bounceIn: {51 '0%': { transform: 'scale(0.3)', opacity: '0' },52 '50%': { transform: 'scale(1.05)' },53 '70%': { transform: 'scale(0.9)' },54 '100%': { transform: 'scale(1)', opacity: '1' },55 },56 },57 },58 },59 plugins: [60 require('@tailwindcss/forms'),61 require('@tailwindcss/typography'),62 require('@tailwindcss/aspect-ratio'),63 ],64};6566// 2. 组件类名组合工具67import { clsx, type ClassValue } from 'clsx';68import { twMerge } from 'tailwind-merge';6970export function cn(...inputs: ClassValue[]) {71 return twMerge(clsx(inputs));72}7374// 3. 可复用的样式变体75const buttonVariants = {76 variant: {77 default: 'bg-primary-600 text-white hover:bg-primary-700',78 destructive: 'bg-red-600 text-white hover:bg-red-700',79 outline: 'border border-gray-300 bg-white text-gray-700 hover:bg-gray-50',80 secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',81 ghost: 'text-gray-700 hover:bg-gray-100',82 link: 'text-primary-600 underline-offset-4 hover:underline',83 },84 size: {85 default: 'h-10 px-4 py-2',86 sm: 'h-9 rounded-md px-3',87 lg: 'h-11 rounded-md px-8',88 icon: 'h-10 w-10',89 },90};9192interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {93 variant?: keyof typeof buttonVariants.variant;94 size?: keyof typeof buttonVariants.size;95 asChild?: boolean;96}9798const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(99 ({ className, variant = 'default', size = 'default', ...props }, ref) => {100 return (101 <button102 className={cn(103 'inline-flex items-center justify-center rounded-md text-sm font-medium',104 'ring-offset-white transition-colors focus-visible:outline-none',105 'focus-visible:ring-2 focus-visible:ring-primary-600 focus-visible:ring-offset-2',106 'disabled:pointer-events-none disabled:opacity-50',107 buttonVariants.variant[variant],108 buttonVariants.size[size],109 className110 )}111 ref={ref}112 {...props}113 />114 );115 }116);117118// 4. 复杂布局组件119const Card = React.forwardRef<120 HTMLDivElement,121 React.HTMLAttributes<HTMLDivElement>122>(({ className, ...props }, ref) => (123 <div124 ref={ref}125 className={cn(126 'rounded-lg border border-gray-200 bg-white text-gray-950 shadow-sm',127 className128 )}129 {...props}130 />131));132133const CardHeader = React.forwardRef<134 HTMLDivElement,135 React.HTMLAttributes<HTMLDivElement>136>(({ className, ...props }, ref) => (137 <div138 ref={ref}139 className={cn('flex flex-col space-y-1.5 p-6', className)}140 {...props}141 />142));143144const CardTitle = React.forwardRef<145 HTMLParagraphElement,146 React.HTMLAttributes<HTMLHeadingElement>147>(({ className, ...props }, ref) => (148 <h3149 ref={ref}150 className={cn(151 'text-2xl font-semibold leading-none tracking-tight',152 className153 )}154 {...props}155 />156));157158const CardContent = React.forwardRef<159 HTMLDivElement,160 React.HTMLAttributes<HTMLDivElement>161>(({ className, ...props }, ref) => (162 <div ref={ref} className={cn('p-6 pt-0', className)} {...props} />163));164165// 5. 响应式设计模式166const ResponsiveGrid = ({ children }: { children: React.ReactNode }) => (167 <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">168 {children}169 </div>170);171172const ResponsiveContainer = ({ children }: { children: React.ReactNode }) => (173 <div className="container mx-auto px-4 sm:px-6 lg:px-8">174 {children}175 </div>176);177178// 6. 动画和交互179const AnimatedCard = ({ children }: { children: React.ReactNode }) => (180 <div className="group relative overflow-hidden rounded-lg bg-white shadow-md transition-all duration-300 hover:shadow-xl hover:-translate-y-1">181 <div className="absolute inset-0 bg-gradient-to-r from-primary-600 to-primary-700 opacity-0 transition-opacity duration-300 group-hover:opacity-10" />182 <div className="relative z-10">183 {children}184 </div>185 </div>186);187188const LoadingSpinner = () => (189 <div className="inline-block h-4 w-4 animate-spin rounded-full border-2 border-solid border-current border-r-transparent motion-reduce:animate-[spin_1.5s_linear_infinite]" />190);191192// 7. 表单组件193const Input = React.forwardRef<194 HTMLInputElement,195 React.InputHTMLAttributes<HTMLInputElement>196>(({ className, type, ...props }, ref) => {197 return (198 <input199 type={type}200 className={cn(201 'flex h-10 w-full rounded-md border border-gray-300 bg-white px-3 py-2',202 'text-sm ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium',203 'placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2',204 'focus-visible:ring-primary-600 focus-visible:ring-offset-2',205 'disabled:cursor-not-allowed disabled:opacity-50',206 className207 )}208 ref={ref}209 {...props}210 />211 );212});213214const Label = React.forwardRef<215 HTMLLabelElement,216 React.LabelHTMLAttributes<HTMLLabelElement>217>(({ className, ...props }, ref) => (218 <label219 ref={ref}220 className={cn(221 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',222 className223 )}224 {...props}225 />226));227228// 8. 使用示例229function Dashboard() {230 return (231 <ResponsiveContainer>232 <div className="space-y-8">233 {/* 页面标题 */}234 <div className="border-b border-gray-200 pb-4">235 <h1 className="text-3xl font-bold tracking-tight text-gray-900">236 Dashboard237 </h1>238 <p className="mt-2 text-gray-600">239 Welcome back! Here's what's happening with your projects.240 </p>241 </div>242 243 {/* 统计卡片 */}244 <ResponsiveGrid>245 {[246 { title: 'Total Users', value: '12,345', change: '+12%' },247 { title: 'Revenue', value: '$45,678', change: '+8%' },248 { title: 'Orders', value: '1,234', change: '+23%' },249 { title: 'Conversion', value: '3.2%', change: '-2%' },250 ].map((stat, index) => (251 <AnimatedCard key={index}>252 <Card>253 <CardHeader className="pb-2">254 <CardTitle className="text-sm font-medium text-gray-600">255 {stat.title}256 </CardTitle>257 </CardHeader>258 <CardContent>259 <div className="text-2xl font-bold">{stat.value}</div>260 <p className={cn(261 'text-xs',262 stat.change.startsWith('+') 263 ? 'text-green-600' 264 : 'text-red-600'265 )}>266 {stat.change} from last month267 </p>268 </CardContent>269 </Card>270 </AnimatedCard>271 ))}272 </ResponsiveGrid>273 274 {/* 操作按钮 */}275 <div className="flex flex-wrap gap-4">276 <Button>Create New Project</Button>277 <Button variant="outline">Import Data</Button>278 <Button variant="ghost">View Reports</Button>279 </div>280 </div>281 </ResponsiveContainer>282 );283}CSS Modules模块化方案
CSS Modules最佳实践
typescript
1// 1. Button.module.css2.button {3 display: inline-flex;4 align-items: center;5 justify-content: center;6 padding: 0.5rem 1rem;7 border: none;8 border-radius: 0.375rem;9 font-weight: 500;10 cursor: pointer;11 transition: all 0.2s ease-in-out;12}1314.button:disabled {15 opacity: 0.6;16 cursor: not-allowed;17}1819.primary {20 background-color: #3b82f6;21 color: white;22}2324.primary:hover:not(:disabled) {25 background-color: #2563eb;26 transform: translateY(-1px);27 box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);28}2930.secondary {31 background-color: #6b7280;32 color: white;33}3435.outline {36 background-color: transparent;37 color: #374151;38 border: 1px solid #d1d5db;39}4041.outline:hover:not(:disabled) {42 background-color: #f9fafb;43}4445.small {46 padding: 0.25rem 0.5rem;47 font-size: 0.875rem;48}4950.large {51 padding: 0.75rem 1.5rem;52 font-size: 1.125rem;53}5455.fullWidth {56 width: 100%;57}5859.loading {60 position: relative;61 color: transparent;62}6364.loading::after {65 content: '';66 position: absolute;67 top: 50%;68 left: 50%;69 transform: translate(-50%, -50%);70 width: 1rem;71 height: 1rem;72 border: 2px solid currentColor;73 border-radius: 50%;74 border-top-color: transparent;75 animation: spin 1s linear infinite;76}7778@keyframes spin {79 to {80 transform: translate(-50%, -50%) rotate(360deg);81 }82}8384// 2. Button.tsx85import React from 'react';86import styles from './Button.module.css';87import { cn } from '@/utils/cn';8889interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {90 variant?: 'primary' | 'secondary' | 'outline';91 size?: 'small' | 'medium' | 'large';92 fullWidth?: boolean;93 loading?: boolean;94 children: React.ReactNode;95}9697export const Button: React.FC<ButtonProps> = ({98 variant = 'primary',99 size = 'medium',100 fullWidth = false,101 loading = false,102 className,103 disabled,104 children,105 ...props106}) => {107 const buttonClasses = cn(108 styles.button,109 styles[variant],110 styles[size],111 {112 [styles.fullWidth]: fullWidth,113 [styles.loading]: loading,114 },115 className116 );117118 return (119 <button120 className={buttonClasses}121 disabled={disabled || loading}122 {...props}123 >124 {children}125 </button>126 );127};128129// 3. Card.module.css130.card {131 background: white;132 border-radius: 0.5rem;133 box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);134 overflow: hidden;135 transition: box-shadow 0.2s ease-in-out;136}137138.card:hover {139 box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);140}141142.header {143 padding: 1.5rem;144 border-bottom: 1px solid #e5e7eb;145}146147.title {148 margin: 0;149 font-size: 1.25rem;150 font-weight: 600;151 color: #111827;152}153154.subtitle {155 margin: 0.5rem 0 0 0;156 font-size: 0.875rem;157 color: #6b7280;158}159160.body {161 padding: 1.5rem;162}163164.footer {165 padding: 1rem 1.5rem;166 background-color: #f9fafb;167 border-top: 1px solid #e5e7eb;168 display: flex;169 justify-content: flex-end;170 gap: 0.5rem;171}172173.interactive {174 cursor: pointer;175}176177.interactive:hover {178 transform: translateY(-2px);179}180181// 4. Card.tsx182import React from 'react';183import styles from './Card.module.css';184import { cn } from '@/utils/cn';185186interface CardProps {187 children: React.ReactNode;188 interactive?: boolean;189 className?: string;190 onClick?: () => void;191}192193interface CardHeaderProps {194 title: string;195 subtitle?: string;196 className?: string;197}198199interface CardBodyProps {200 children: React.ReactNode;201 className?: string;202}203204interface CardFooterProps {205 children: React.ReactNode;206 className?: string;207}208209export const Card: React.FC<CardProps> = ({210 children,211 interactive = false,212 className,213 onClick,214}) => {215 return (216 <div217 className={cn(218 styles.card,219 {220 [styles.interactive]: interactive,221 },222 className223 )}224 onClick={onClick}225 >226 {children}227 </div>228 );229};230231export const CardHeader: React.FC<CardHeaderProps> = ({232 title,233 subtitle,234 className,235}) => {236 return (237 <div className={cn(styles.header, className)}>238 <h3 className={styles.title}>{title}</h3>239 {subtitle && <p className={styles.subtitle}>{subtitle}</p>}240 </div>241 );242};243244export const CardBody: React.FC<CardBodyProps> = ({245 children,246 className,247}) => {248 return (249 <div className={cn(styles.body, className)}>250 {children}251 </div>252 );253};254255export const CardFooter: React.FC<CardFooterProps> = ({256 children,257 className,258}) => {259 return (260 <div className={cn(styles.footer, className)}>261 {children}262 </div>263 );264};265266// 5. Layout.module.css267.container {268 max-width: 1200px;269 margin: 0 auto;270 padding: 0 1rem;271}272273.grid {274 display: grid;275 gap: 1rem;276}277278.gridCols1 {279 grid-template-columns: 1fr;280}281282.gridCols2 {283 grid-template-columns: repeat(2, 1fr);284}285286.gridCols3 {287 grid-template-columns: repeat(3, 1fr);288}289290.gridCols4 {291 grid-template-columns: repeat(4, 1fr);292}293294.gridAutoFit {295 grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));296}297298@media (max-width: 768px) {299 .container {300 padding: 0 0.5rem;301 }302 303 .gridCols2,304 .gridCols3,305 .gridCols4 {306 grid-template-columns: 1fr;307 }308 309 .gridAutoFit {310 grid-template-columns: 1fr;311 }312}313314// 6. Layout.tsx315import React from 'react';316import styles from './Layout.module.css';317import { cn } from '@/utils/cn';318319interface ContainerProps {320 children: React.ReactNode;321 className?: string;322}323324interface GridProps {325 children: React.ReactNode;326 cols?: 1 | 2 | 3 | 4 | 'auto-fit';327 className?: string;328}329330export const Container: React.FC<ContainerProps> = ({331 children,332 className,333}) => {334 return (335 <div className={cn(styles.container, className)}>336 {children}337 </div>338 );339};340341export const Grid: React.FC<GridProps> = ({342 children,343 cols = 'auto-fit',344 className,345}) => {346 const gridClass = cols === 'auto-fit' 347 ? styles.gridAutoFit 348 : styles[`gridCols${cols}` as keyof typeof styles];349350 return (351 <div className={cn(styles.grid, gridClass, className)}>352 {children}353 </div>354 );355};356357// 7. 使用示例358import React from 'react';359import { Container, Grid } from './components/Layout';360import { Card, CardHeader, CardBody, CardFooter } from './components/Card';361import { Button } from './components/Button';362363function App() {364 const products = [365 { id: 1, name: 'Product 1', price: '$99' },366 { id: 2, name: 'Product 2', price: '$149' },367 { id: 3, name: 'Product 3', price: '$199' },368 ];369370 return (371 <Container>372 <h1>Product Catalog</h1>373 374 <Grid cols={3}>375 {products.map((product) => (376 <Card key={product.id} interactive>377 <CardHeader 378 title={product.name}379 subtitle={`Starting at ${product.price}`}380 />381 <CardBody>382 <p>Product description goes here...</p>383 </CardBody>384 <CardFooter>385 <Button variant="outline" size="small">386 Learn More387 </Button>388 <Button size="small">389 Add to Cart390 </Button>391 </CardFooter>392 </Card>393 ))}394 </Grid>395 </Container>396 );397}398399export default App;
参与讨论