从零开始构建一个现代化前端项目:一个Cursor重度用户的血泪经验
说实话,写这篇文章的时候我正瘫在沙发上,左手咖啡右手键盘,背景音是楼下快递小哥的电动车声——典型的远程办公日常。作为一个已经离不开 AI 写代码的开发者(没错,就是那种连 console.log 都要问 Cursor 有没有更优雅写法的人),我最近被拉进了一个“从零搭建新项目”的坑里。
事情是这样的:我们团队去年双11搞了个大促活动页,临时拼凑的 React + Webpack 脚手架在线上崩了三次,老板脸都绿了。年后复盘会上,CTO 拍桌子说:“这次必须搞一套现代化、可维护、高性能、还能扛住流量洪峰的前端架构!”然后,眼神精准地落在我身上——毕竟我是组里唯一一个天天和 Claude 打交道、还敢在 PR 里写 “This was mostly written by AI, but I reviewed it” 的人。
于是,我被迫(其实是有点兴奋)接下了这个任务:从零开始构建一个现代化前端项目。今天就来和大家唠唠这一个月踩过的坑、熬过的夜、以及那些让产品经理闭嘴的性能数据。
别再用 create-react-app 了,真的
我知道很多同学(包括一年前的我)一建 React 项目就 npx create-react-app my-app,图个快。但兄弟,2024 年了,CRA 已经像你家那台老 ThinkPad 一样,该退休了。
CRA 的问题在哪?打包慢、配置黑盒、无法 Tree Shaking 优化、SSR 支持差……最致命的是,它不支持 Vite。而 Vite,是我这次项目的基石。
为什么选 Vite?简单说:快到离谱。本地开发启动时间从 CRA 的 15s+ 降到 300ms 以内,HMR(热更新)几乎是瞬时的。上周五晚上我在改一个 Modal 组件的动画,保存即生效,根本不用等,那种丝滑感让我一度以为自己在用 Figma 做设计。
而且,Vite 对 TypeScript、CSS Modules、Sass、PostCSS 的原生支持非常友好,几乎开箱即用。再加上它基于 ES Modules 的按需加载机制,开发体验直接拉满。
所以第一步,我直接:
npm create vite@latest my-modern-app -- --template react-ts
cd my-modern-app
npm install
搞定。整个过程比泡面还快。
架构设计:别只顾着写组件,先想清楚怎么组织代码
很多人一上来就 src/components/Button.tsx,但现代化项目的核心不是组件,而是可维护性和可扩展性。我参考了 Feature-Sliced Design 的理念,把代码分层如下:
src/
├── app/ # 应用入口、全局状态、路由
├── features/ # 业务功能模块(如登录、购物车)
├── entities/ # 业务实体(如 User、Product)
├── widgets/ # 跨页面复用的 UI 块(如 Header、Footer)
├── shared/ # 真正的原子级共享代码(hooks、utils、ui)
└── pages/ # 页面级组件,组合 features/widgets
这种结构的好处是:高内聚、低耦合。比如产品经理突然说“把购物车功能抽成独立微前端”,我只需要把 features/cart 整个目录拎出来就行,几乎零成本。
当然,这套架构一开始被后端老哥吐槽:“你们前端现在也搞 DDD 了?” 我回他:“这不是 DDD,这是防 PM 骚操作的防御性编程。”
性能优化:别等上线才想起来
说到性能,我就想起去年双11那个事故——首页白屏 8 秒,用户全跑了。这次我直接把性能指标写进 CI 流程。
1. 代码分割(Code Splitting)
React.lazy + Suspense 是基础操作,但很多人只用在路由层面。其实,任何非首屏内容都可以懒加载。比如一个复杂的图表组件:
// shared/ui/Chart.tsx
const Chart = lazy(() => import('./Chart.lazy'));
export const ChartWrapper = () => (
<Suspense fallback={<Skeleton height={200} />}>
<Chart />
</Suspense>
);
注意 fallback 用 Skeleton(骨架屏),而不是“Loading...”,用户体验直接提升一个档次。
2. 图片优化
别再直接 <img src="/logo.png" /> 了!现代方案是:
- 使用
<picture>+ WebP 格式 - 结合
loading="lazy"和fetchpriority="high"控制加载优先级 - 用
next/image?不,我们没用 Next.js。所以我封装了一个Image组件,内部用 Intersection Observer 实现懒加载
// shared/ui/Image.tsx
export const Image = ({ src, alt, priority = false }) => {
const [loaded, setLoaded] = useState(false);
return (
<img
src={src}
alt={alt}
loading={priority ? 'eager' : 'lazy'}
fetchpriority={priority ? 'high' : undefined}
onLoad={() => set_loaded(true)}
style={{ opacity: loaded ? 1 : 0, transition: 'opacity 0.3s' }}
/>
);
};
3. Bundle 分析
我加了一个 npm script:
{
"scripts": {
"analyze": "vite build --mode analyze && open dist/stats.html"
}
}
配合 rollup-plugin-visualizer,每次 PR 都要附上 bundle 报告。有一次发现 moment.js 被不小心引入了,整整 70KB!立马换成 date-fns,砍掉 90% 体积。
状态管理:别一上来就上 Redux
现在都 2024 年了,90% 的项目根本不需要 Redux。我这次只用了两种状态方案:
- 全局状态:Zustand(轻量、简洁、支持 middleware)
- 局部状态:React 自带的
useState/useReducer
Zustand 的写法有多爽?
// app/store/userStore.ts
import { create } from 'zustand';
type UserState = {
user: User | null;
login: (token: string) => Promise<void>;
logout: () => void;
};
export const useUserStore = create<UserState>((set, get) => ({
user: null,
login: async (token) => {
const user = await api.getUser(token);
set({ user });
},
logout: () => set({ user: null }),
}));
没有 action、没有 reducer、没有 provider 包裹,直接在任何组件里 useUserStore() 就行。而且它天然支持 partial subscribe,避免不必要的重渲染。
面试题来了:“React 状态管理有哪些方案?如何选择?”
我的答案是:先问业务复杂度。如果是 Todo App,用 useState 足够;如果跨组件通信多、有持久化需求,上 Zustand;只有当你需要时间旅行调试、严格的状态快照,才考虑 Redux Toolkit。
TypeScript:不是可选项,是必选项
作为 Cursor 用户,我深刻体会到:TypeScript 是 AI 编程的最佳搭档。因为类型信息越完整,AI 生成的代码越准确。
我启用了几乎所有 strict 选项:
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"exactOptionalPropertyTypes": true,
"noUncheckedIndexedAccess": true,
// ...
}
}
虽然刚开始各种报错,但一旦跑通,后续开发简直飞起。特别是和后端联调时,直接导入他们的 OpenAPI spec 生成 types,再也不用猜字段名了。
测试:别等 QA 来骂你
我知道很多前端(包括我以前)觉得测试是浪费时间。直到有一次,我改了个 Button 的 padding,结果导致表单提交按钮错位,线上用户点不到——被测试小姐姐追着骂了一周。
这次我强制要求:
- 单元测试:Vitest(比 Jest 快 20 倍)
- E2E 测试:Playwright(支持多浏览器、移动端模拟)
- 可视化回归测试:Storybook + Chromatic
关键代码示例:
// features/login/LoginForm.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { LoginForm } from './LoginForm';
test('submits form with valid email', async () => {
render(<LoginForm />);
fireEvent.change(screen.getByLabelText(/email/i), {
target: { value: 'test@example.com' }
});
fireEvent.click(screen.getByRole('button', { name: /login/i }));
expect(await screen.findByText(/welcome/i)).toBeInTheDocument();
});
CI 里加上:
# .github/workflows/test.yml
- name: Run tests
run: npm test -- --coverage
- name: Fail if coverage < 80%
run: npx check-coverage --threshold=80
从此,PR 不带测试?直接拒!
构建与部署:让运维少找你麻烦
前端常背锅:“你们代码太大了,CDN 带宽爆了!” 这次我直接优化到极致。
关键配置(vite.config.ts)
export default defineConfig({
build: {
sourcemap: true,
rollupOptions: {
output: {
manualChunks: {
// 拆分 vendor
vendor: ['react', 'react-dom'],
ui: ['@mui/material', '@mui/icons-material'],
utils: ['lodash-es', 'date-fns'],
},
},
},
// 启用 brotli 压缩
assetsInlineLimit: 4096, // 小于 4kb 转 base64
},
// 开发服务器代理
server: {
proxy: {
'/api': 'http://localhost:3001',
},
},
});
性能对比
| 指标 | 旧项目 (CRA) | 新项目 (Vite + 优化) |
|---|---|---|
| 首屏加载 (3G) | 5.2s | 1.8s |
| Bundle 体积 | 1.2MB | 480KB |
| Lighthouse 性能分 | 58 | 92 |
| 构建时间 | 45s | 12s |
上线后,运维大哥居然主动请我喝奶茶:“终于不用半夜被 CDN 告警吵醒了。”
那些让我想砸电脑的坑
当然,过程不是一帆风顺。
坑 1:Vite + Tailwind CSS 的 JIT 模式在 Docker 里失效
解决方案:在 tailwind.config.js 里显式指定 content 路径,并禁用 mode: 'jit'(Vite 2.8+ 已默认启用 JIT,无需配置)。
坑 2:Zustand 在 SSR 下报 window is not defined
解决方案:用 createWithEqualityFn + shallow,并在服务端返回空 store。
坑 3:Playwright 在 GitHub Actions 上跑 E2E 测试超时
解决方案:加 --headed 调试模式录屏,发现是某个 API mock 没生效。最终用 page.route() 拦截请求。
每一个坑都让我深夜发朋友圈:“程序员到底做错了什么,要经历这些?”
总结:现代化 ≠ 复杂化
折腾一个月,项目终于上线。首周 DAU 10w+,Lighthouse 评分稳定在 90+,老板在群里发红包,产品经理说“这次交互真流畅”。
回头看,所谓“现代化前端项目”,核心不是堆砌最新技术,而是:
- 开发体验优先(Vite 让编码快乐)
- 性能内建(从第一天就考虑加载、渲染、交互)
- 可维护为王(清晰的架构 > 花哨的语法糖)
- 自动化兜底(测试、lint、bundle 分析)
最后说句掏心窝的话:作为 AI 辅助编程的重度用户,我越来越觉得,工具只是放大器。你对工程的理解越深,AI 帮你放大的价值就越大。反之,只会 Cmd+K 的话,迟早被自己的技术债埋了。
如果你也在准备面试,不妨想想:当面试官问“你怎么搭建一个前端项目”,你能说出的不只是 npx create-react-app,而是一整套思考体系——那你就赢了。
好了,我去喂猫了(远程办公的福利之一:随时撸猫)。代码写累了,记得站起来走走,别学我上周坐出腰椎间盘突出……
Peace ✌️

评论 0