项目背景
重构一个大型电子商务平台的前端系统。原有系统使用jQuery和传统的MVC架构构建,随着业务的快速发展,系统变得越来越难以维护和扩展。新的技术栈基于React、TypeScript、GraphQL和Tailwind CSS,本文将分享我们在这个项目中的技术选型、架构设计、性能优化以及遇到的挑战与解决方案。
技术栈选择
前端核心技术
- React 18:利用并发渲染和自动批处理等新特性
- TypeScript:提供类型安全和更好的开发体验
- Apollo Client:管理GraphQL数据和本地状态
- React Router v6:处理路由
- Tailwind CSS:构建响应式UI
- React Query:处理REST API请求和缓存
构建工具
- Vite:替代Create React App,提供更快的开发和构建体验
- ESLint & Prettier:代码质量和格式化
- Husky & lint-staged:提交前代码检查
- Vitest & React Testing Library:单元测试和组件测试
项目架构设计
目录结构
我们采用了基于特性的目录结构,而不是传统的按类型分组:
src/ ├── assets/ # 静态资源 ├── components/ # 共享 UI 组件 │ ├── common/ # 基础组件(按钮、输入框等) │ └── layout/ # 布局组件 ├── features/ # 按业务功能组织的模块 │ ├── product/ # 产品相关功能 │ │ ├── api/ # GraphQL 查询和变更 │ │ ├── components/ # 产品特定组件 │ │ ├── hooks/ # 产品相关自定义钩子 │ │ ├── types/ # TypeScript 类型定义 │ │ └── utils/ # 工具函数 │ ├── cart/ # 购物车功能 │ ├── checkout/ # 结账流程 │ └── user/ # 用户账户管理 ├── hooks/ # 全局共享钩子 ├── lib/ # 第三方库配置 ├── routes/ # 路由定义 ├── services/ # API 服务 ├── store/ # 全局状态管理 ├── types/ # 全局类型定义 └── utils/ # 全局工具函数
|
GraphQL架构
我们使用Apollo Client管理GraphQL数据流,并实现了以下架构:
- Fragment驱动开发:为每个组件定义明确的数据需求
- 类型生成:使用GraphQL Code Generator自动生成TypeScript类型
- 本地状态管理:利用Apollo Client的缓存作为单一数据源
import { gql } from '@apollo/client'; import { PRODUCT_CARD_FRAGMENT } from './fragments';
export const GET_PRODUCT_DETAIL = gql` query GetProductDetail($id: ID!) { product(id: $id) { ...ProductCard description specifications { name value } relatedProducts { ...ProductCard } } } ${PRODUCT_CARD_FRAGMENT} `;
const ProductDetail = ({ productId }) => { const { data, loading, error } = useQuery(GET_PRODUCT_DETAIL, { variables: { id: productId }, });
if (loading) return <ProductSkeleton />; if (error) return <ErrorMessage error={error} />;
const { product } = data; return ( <div className="product-detail"> {/* 产品详情UI */} </div> ); };
|
组件设计模式
复合组件模式
为了构建灵活且可维护的UI组件,我们大量使用了复合组件模式:
import React, { createContext, useContext, useState } from 'react';
const FilterContext = createContext<FilterContextType | undefined>(undefined);
export const ProductFilter: React.FC<ProductFilterProps> & { Category: typeof FilterCategory; Price: typeof FilterPrice; Rating: typeof FilterRating; ApplyButton: typeof ApplyButton; } = ({ children, onApply }) => { const [filters, setFilters] = useState({ categories: [], priceRange: { min: 0, max: 1000 }, rating: 0, });
const updateFilter = (key, value) => { setFilters(prev => ({ ...prev, [key]: value })); };
const handleApply = () => { onApply(filters); };
return ( <FilterContext.Provider value={{ filters, updateFilter, handleApply }}> <div className="bg-white rounded-lg shadow p-4"> <h3 className="text-lg font-medium mb-4">筛选商品</h3> {children} </div> </FilterContext.Provider> ); };
const FilterCategory = ({ categories }) => { const { filters, updateFilter } = useFilterContext(); return ( <div className="mb-4"> <h4 className="font-medium mb-2">分类</h4> {categories.map(category => ( <label key={category.id} className="flex items-center mb-1"> <input type="checkbox" checked={filters.categories.includes(category.id)} onChange={() => { // 切换类别选择 const newCategories = filters.categories.includes(category.id) ? filters.categories.filter(id => id !== category.id) : [...filters.categories, category.id]; updateFilter('categories', newCategories); }} className="mr-2" /> {category.name} </label> ))} </div> ); };
ProductFilter.Category = FilterCategory; ProductFilter.Price = FilterPrice; ProductFilter.Rating = FilterRating; ProductFilter.ApplyButton = ApplyButton;
<ProductFilter onApply={handleApplyFilters}> <ProductFilter.Category categories={categories} /> <ProductFilter.Price /> <ProductFilter.Rating /> <ProductFilter.ApplyButton /> </ProductFilter>
|
自定义钩子
我们将复杂的业务逻辑封装在自定义钩子中,提高代码的可测试性和可重用性:
export const useCart = () => { const { data, loading, error } = useQuery(GET_CART); const [addToCart] = useMutation(ADD_TO_CART); const [removeFromCart] = useMutation(REMOVE_FROM_CART); const [updateQuantity] = useMutation(UPDATE_QUANTITY); const cart = data?.cart || { items: [], totalItems: 0, totalPrice: 0 }; const addItem = async (productId, quantity = 1, options = {}) => { try { await addToCart({ variables: { productId, quantity, options }, update: (cache, { data: { addToCart } }) => { cache.writeQuery({ query: GET_CART, data: { cart: addToCart }, }); }, }); return { success: true }; } catch (err) { return { success: false, error: err }; } }; return { cart, loading, error, addItem, removeItem, updateItemQuantity, clearCart, }; };
const ProductCard = ({ product }) => { const { addItem, cart } = useCart(); const isInCart = cart.items.some(item => item.product.id === product.id); return ( <div className="product-card"> {/* 产品信息 */} <button onClick={() => addItem(product.id)} disabled={isInCart} className="btn btn-primary" > {isInCart ? '已加入购物车' : '加入购物车'} </button> </div> ); };
|
性能优化策略
代码分割
我们使用React的React.lazy和Suspense实现基于路由的代码分割:
import { lazy, Suspense } from 'react'; import { createBrowserRouter } from 'react-router-dom'; import { PageLoader } from './components/common/PageLoader';
const HomePage = lazy(() => import('./pages/HomePage')); const ProductListPage = lazy(() => import('./pages/ProductListPage')); const ProductDetailPage = lazy(() => import('./pages/ProductDetailPage')); const CartPage = lazy(() => import('./pages/CartPage')); const CheckoutPage = lazy(() => import('./pages/CheckoutPage'));
export const router = createBrowserRouter([ { path: '/', element: ( <Suspense fallback={<PageLoader />}> <HomePage /> </Suspense> ), }, { path: '/products', element: ( <Suspense fallback={<PageLoader />}> <ProductListPage /> </Suspense> ), }, ]);
|
组件优化
- React.memo:对纯展示组件使用memo减少不必要的重渲染
- useMemo和useCallback:优化计算密集型操作和事件处理函数
const ProductGrid = React.memo(({ products }) => { return ( <div className="grid grid-cols-1 md:grid-cols-3 gap-4"> {products.map(product => ( <ProductCard key={product.id} product={product} /> ))} </div> ); });
const ProductListPage = () => { const { data, loading } = useQuery(GET_PRODUCTS, { variables: { filter }, }); const sortedProducts = useMemo(() => { if (!data?.products) return []; return [...data.products].sort((a, b) => { if (sortOrder === 'price-asc') return a.price - b.price; if (sortOrder === 'price-desc') return b.price - a.price; return 0; }); }, [data?.products, sortOrder]); const handleSortChange = useCallback((newSortOrder) => { setSortOrder(newSortOrder); }, []); return ( <div> <SortControls onSortChange={handleSortChange} /> {loading ? <ProductGridSkeleton /> : <ProductGrid products={sortedProducts} />} </div> ); };
|
图片优化
我们实现了一个响应式图片组件,结合现代图像格式和懒加载:
const ResponsiveImage = ({ src, alt, sizes, className }) => { const generateSrcSet = (imageSrc) => { const widths = [320, 640, 960, 1280]; return widths .map(width => `${getOptimizedImageUrl(imageSrc, width)} ${width}w`) .join(', '); }; return ( <img src={src} srcSet={generateSrcSet(src)} sizes={sizes || '(max-width: 768px) 100vw, 50vw'} alt={alt} loading="lazy" className={className} /> ); };
const getOptimizedImageUrl = (url, width) => { return url.replace('/upload/', `/upload/w_${width},f_auto,q_auto/`); };
|
状态管理策略
我们采用了混合状态管理策略:
- Apollo Client缓存:作为远程数据的主要存储
- React Context:管理UI状态和主题
- React Query:处理非GraphQL API的数据获取和缓存
const ThemeContext = createContext();
export const ThemeProvider = ({ children }) => { const [theme, setTheme] = useState(() => { return localStorage.getItem('theme') || 'light'; }); useEffect(() => { document.documentElement.setAttribute('data-theme', theme); localStorage.setItem('theme', theme); }, [theme]); const toggleTheme = () => { setTheme(prev => (prev === 'light' ? 'dark' : 'light')); }; return ( <ThemeContext.Provider value={{ theme, toggleTheme }}> {children} </ThemeContext.Provider> ); };
const CartContext = createContext();
export const CartProvider = ({ children }) => { const { cart, loading, addItem, removeItem, updateItemQuantity } = useCart(); return ( <CartContext.Provider value={{ cart, loading, addItem, removeItem, updateItemQuantity }} > {children} </CartContext.Provider> ); };
const App = () => { return ( <ApolloProvider client={apolloClient}> <QueryClientProvider client={queryClient}> <ThemeProvider> <CartProvider> <RouterProvider router={router} /> </CartProvider> </ThemeProvider> </QueryClientProvider> </ApolloProvider> ); };
|
测试策略
我们采用了多层次的测试策略:
- 单元测试:使用Vitest测试工具函数和钩子
- 组件测试:使用React Testing Library测试组件行为
- 集成测试:测试组件之间的交互
- E2E测试:使用Cypress测试关键用户流程
import { renderHook, act } from '@testing-library/react-hooks'; import { MockedProvider } from '@apollo/client/testing'; import { useCart } from './useCart'; import { GET_CART, ADD_TO_CART } from '../api/cartQueries';
const mocks = [ { request: { query: GET_CART, }, result: { data: { cart: { items: [], totalItems: 0, totalPrice: 0, }, }, }, }, { request: { query: ADD_TO_CART, variables: { productId: '1', quantity: 1, options: {} }, }, result: { data: { addToCart: { items: [ { id: 'cart-item-1', product: { id: '1', name: 'Test Product', price: 100 }, quantity: 1, totalPrice: 100, }, ], totalItems: 1, totalPrice: 100, }, }, }, }, ];
test('should add item to cart', async () => { const { result, waitForNextUpdate } = renderHook(() => useCart(), { wrapper: ({ children }) => ( <MockedProvider mocks={mocks} addTypename={false}> {children} </MockedProvider> ), }); expect(result.current.cart.items).toHaveLength(0); act(() => { result.current.addItem('1'); }); await waitForNextUpdate(); expect(result.current.cart.items).toHaveLength(1); expect(result.current.cart.items[0].product.id).toBe('1'); expect(result.current.cart.totalItems).toBe(1); expect(result.current.cart.totalPrice).toBe(100); });
|
遇到的挑战与解决方案
挑战1:大型列表性能优化
在产品列表页面,当筛选和排序大量产品时,页面性能下降明显。
解决方案:实现虚拟滚动
import { useVirtualizer } from '@tanstack/react-virtual'; import { useRef } from 'react';
const VirtualizedProductList = ({ products }) => { const parentRef = useRef(null); const rowVirtualizer = useVirtualizer({ count: products.length, getScrollElement: () => parentRef.current, estimateSize: () => 300, overscan: 5, }); return ( <div ref={parentRef} className="h-[800px] overflow-auto" > <div style={{ height: `${rowVirtualizer.getTotalSize()}px`, width: '100%', position: 'relative', }} > {rowVirtualizer.getVirtualItems().map(virtualRow => ( <div key={virtualRow.index} style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: `${virtualRow.size}px`, transform: `translateY(${virtualRow.start}px)`, }} > <ProductCard product={products[virtualRow.index]} /> </div> ))} </div> </div> ); };
|
挑战2:表单状态管理
结账流程中的复杂表单状态管理变得难以维护。
解决方案:使用React Hook Form和Zod进行表单验证
import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod';
const shippingSchema = z.object({ fullName: z.string().min(2, '姓名至少需要2个字符'), address: z.string().min(5, '请输入完整地址'), city: z.string().min(2, '请输入城市名称'), zipCode: z.string().regex(/^\d{6}$/, '邮政编码必须是6位数字'), phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入有效的手机号码'), });
const ShippingForm = ({ onSubmit }) => { const { register, handleSubmit, formState: { errors }, } = useForm({ resolver: zodResolver(shippingSchema), }); return ( <form onSubmit={handleSubmit(onSubmit)} className="space-y-4"> <div> <label htmlFor="fullName" className="block text-sm font-medium"> 收货人姓名 </label> <input id="fullName" type="text" {...register('fullName')} className={`mt-1 block w-full rounded-md border ${ errors.fullName ? 'border-red-500' : 'border-gray-300' }`} /> {errors.fullName && ( <p className="mt-1 text-sm text-red-500">{errors.fullName.message}</p> )} </div> {/* 其他表单字段 */} <button type="submit" className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700" > 保存配送信息 </button> </form> ); };
|
挑战3:国际化支持
需要支持多语言,同时保持良好的性能。
解决方案:使用i18next和代码分割按语言加载翻译文件
import i18n from 'i18next'; import { initReactI18next, useTranslation } from 'react-i18next'; import LanguageDetector from 'i18next-browser-languagedetector'; import Backend from 'i18next-http-backend';
i18n .use(Backend) .use(LanguageDetector) .use(initReactI18next) .init({ fallbackLng: 'zh', ns: ['common', 'product', 'checkout'], defaultNS: 'common', backend: { loadPath: '/locales/{{lng}}/{{ns}}.json', }, });
const LanguageSwitcher = () => { const { i18n } = useTranslation(); const changeLanguage = async (lng) => { await i18n.changeLanguage(lng); document.documentElement.lang = lng; }; return ( <div className="flex space-x-2"> <button onClick={() => changeLanguage('zh')} className={`px-2 py-1 rounded ${ i18n.language === 'zh' ? 'bg-blue-600 text-white' : 'bg-gray-200' }`} > 中文 </button> <button onClick={() => changeLanguage('en')} className={`px-2 py-1 rounded ${ i18n.language === 'en' ? 'bg-blue-600 text-white' : 'bg-gray-200' }`} > English </button> </div> ); };
const ProductDetail = ({ product }) => { const { t } = useTranslation('product'); return ( <div> <h1>{product.name}</h1> <p>{t('price')}: ¥{product.price}</p> <button className="btn btn-primary"> {t('addToCart')} </button> </div> ); };
|
部署与监控
我们使用GitHub Actions设置了CI/CD流程,并部署到AWS上:
- 构建流程:使用Vite构建生产版本
- 部署目标:AWS S3 + CloudFront作为CDN
- 监控:使用Sentry进行错误跟踪和性能监控
name: Deploy
on: push: branches: [main]
jobs: build-and-deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup Node.js uses: actions/setup-node@v3 with: node-version: '18' cache: 'npm' - name: Install dependencies run: npm ci - name: Run tests run: npm test - name: Build run: npm run build - name: Deploy to S3 uses: jakejarvis/s3-sync-action@master with: args: --delete env: AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} SOURCE_DIR: 'dist' - name: Invalidate CloudFront uses: chetan/invalidate-cloudfront-action@v2 env: DISTRIBUTION: ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} PATHS: '/*' AWS_REGION: 'us-east-1' AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
前端性能监控集成:
import * as Sentry from '@sentry/react'; import { BrowserTracing } from '@sentry/tracing';
export const initSentry = () => { if (import.meta.env.PROD) { Sentry.init({ dsn: import.meta.env.VITE_SENTRY_DSN, integrations: [new BrowserTracing()], tracesSampleRate: 0.2, environment: import.meta.env.MODE, }); } };
import { initSentry } from './lib/sentry';
initSentry();
|
项目成果与经验总结
性能指标改进
重构后,我们的关键性能指标获得了显著改善:
- 首次内容绘制 (FCP): 从2.8s降低到0.9s
- 最大内容绘制 (LCP): 从4.2s降低到2.1s
- 首次输入延迟 (FID): 从120ms降低到25ms
- 累积布局偏移 (CLS): 从0.25降低到0.05
业务成果
- 转化率提高了15%
- 平均会话时长增加了20%
- 移动端用户增长了25%
经验教训
渐进式重构:我们采用了页面级别的渐进式重构,而不是一次性重写整个应用,这减少了风险并允许我们更早地交付价值。
性能预算:为每个页面设置明确的性能预算,并在CI/CD流程中自动检查,确保性能不会随着功能迭代而下降。
组件设计系统:在项目初期投入时间建立完善的组件设计系统,大大提高了后期的开发效率和UI一致性。
数据获取策略:GraphQL为我们提供了精确获取所需数据的能力,减少了过度获取和数据转换的工作。
自动化测试:高测试覆盖率帮助我们在重构过程中保持信心,减少了回归问题。
结语
通过这个项目,我们不仅成功地将一个传统的电子商务平台转变为现代化的React应用,还积累了宝贵的技术经验和最佳实践。React生态系统的强大和灵活性使我们能够构建高性能、可维护的大型前端应用,同时GraphQL的引入显著改善了前后端协作效率。
未来,我们计划进一步优化应用性能,探索React Server Components和Streaming SSR等新技术,为用户提供更好的体验。