从零搭建现代前端项目:一个Cursor重度用户的实战手记
入职新公司快两个月了,坦白说,前一个月我基本在“装”——装熟悉代码、装理解业务、装跟得上节奏。作为一个刚跳槽的前端工程师,简历上写着“精通React、熟悉工程化”,实际上每天下班回家第一件事就是打开Cursor,把白天没搞懂的逻辑喂给AI,让它给我讲人话。
上周五晚上,产品突然甩过来一个需求:“我们要做一个全新的内部工具平台,技术栈你们自己定,但必须现代化、可维护、能跑动画。” 我心里一咯噔:完了,这不就是冲着我来的吗?毕竟组里都知道我对前端动画和交互有点执念(曾经为了一个微动效调了三天贝塞尔曲线)。
更刺激的是,后端已经用Spring Boot搭好了基础服务,接口文档都扔过来了。而我们前端,除了一个Figma设计稿,啥都没有。连个package.json都是空的。
于是,这个周末我没去约会,也没打游戏,坐在我的MacBook Pro前(Windows?那只是我测试兼容性时才碰的“备用机”),从零开始,用一套我认为“现代化”的方式,搭起了整个前端项目。这篇文章,就是我边喝冰美式边敲出来的血泪总结。
为什么不能直接 npx create-react-app?
很多新人(包括半年前的我)会想:不就是个React项目吗?一行命令搞定!
但现实很骨感。CRA(Create React App)确实香,但它像个被过度保护的孩子——你没法轻易换Babel配置,改Webpack要eject,一eject就再也回不去了。而且它默认不支持TypeScript的最佳实践、ESLint规则老旧、Prettier集成别扭,更别说现在流行的Vite、TurboRepo、pnpm这些了。
我们这次要做的,是一个长期维护的企业级内部平台。它需要:
- 快速启动和热更新(开发体验)
- 严格的代码规范(团队协作)
- 自动化的类型安全(减少低级Bug)
- 动画性能优化(我的执念)
- 与Spring Boot后端无缝对接(联调效率)
所以,我决定抛弃CRA,从底层一点点拼起来。听起来很硬核?其实有Cursor帮忙,90%的配置文件它都能帮我生成,我只需要告诉它:“我要一个基于Vite + React 18 + TypeScript + Tailwind CSS + ESLint + Prettier + Husky + lint-staged 的项目,要求支持absolute import,alias设为@,并且集成axios和react-router-dom v6。”
几秒钟后,.eslintrc.cjs、tsconfig.json、vite.config.ts 全齐了。我甚至不用记住那些拗口的插件名。
项目骨架:不是炫技,是生存必需
1. 包管理器:pnpm > yarn > npm
别杠,我就用pnpm。速度快、磁盘省、依赖扁平清晰。而且我们公司内网慢得像2G网络,pnpm的hard link机制能让我在咖啡还没凉的时候就装完所有依赖。
pnpm create vite my-app --template react-ts
cd my-app
pnpm install
然后立刻替换掉默认的App.tsx,删掉那些丑陋的旋转Logo。
2. 路径别名:告别 ../../../components
没人喜欢写三层以上的相对路径。我在tsconfig.json里加了:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
再在vite.config.ts里同步配置:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
}
})
从此,import Button from '@/components/Button' 成了我的快乐源泉。
3. 代码规范:不让队友骂你
我们组有个共识:代码是写给人看的,机器只是顺便执行一下。所以我配了ESLint + Prettier + Husky + lint-staged 组合拳。
关键点:
- 使用
eslint-config-react-app但升级到最新规则 - Prettier 强制单引号、尾逗号、2空格缩进(别跟我争,这是团队约定)
- pre-commit 钩子自动 fix 可修复问题,否则拒绝提交
// package.json scripts
{
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"format": "prettier --write ."
}
Husky的配置很简单,Cursor三行命令就搞定了:
pnpm dlx husky-init && pnpm install
pnpm dlx lint-staged
然后在.husky/pre-commit里写:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
pnpm lint-staged
这样,每次git commit,脏代码根本出不了门。上周有个实习生想提交一堆console.log,直接被拦住,他当场学会了什么叫“敬畏代码”。
和Spring Boot谈恋爱:API联调的艺术
后端用的是Spring Boot,RESTful API,返回JSON。看起来很标准,对吧?但坑永远在细节里。
比如,他们返回的时间戳是Long类型(Java的System.currentTimeMillis()),而前端需要Date对象。又比如,有些字段可能为null,但TS接口里没标可选,导致运行时报错。
我的解决方案是:在axios拦截器里做统一数据清洗。
// src/utils/request.ts
import axios from 'axios'
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10000
})
// 响应拦截器
request.interceptors.response.use(
(response) => {
const { data } = response
// 这里可以遍历data,把时间戳转成Date,null转成undefined等
return transformResponseData(data)
},
(error) => {
// 统一错误处理:401跳登录,500弹Toast
if (error.response?.status === 401) {
window.location.href = '/login'
}
return Promise.reject(error)
}
)
function transformResponseData(obj: any): any {
if (obj === null || obj === undefined) return obj
if (typeof obj !== 'object') return obj
if (Array.isArray(obj)) {
return obj.map(transformResponseData)
}
const result: Record<string, any> = {}
for (const key in obj) {
const value = obj[key]
// 假设所有以Time结尾的字段都是时间戳
if (key.endsWith('Time') && typeof value === 'number') {
result[key] = new Date(value)
} else if (value === null) {
result[key] = undefined // TS更喜欢undefined
} else {
result[key] = transformResponseData(value)
}
}
return result
}
export default request
这招让前端完全不用关心后端的数据格式差异。产品经理看到页面正常显示“2024-06-15 14:30”,根本不知道背后我写了这么多胶水代码。
动画与交互:不只是“好看”
我说过我对动画感兴趣,但这不是为了炫技。好的动效能降低用户认知负荷,提升操作反馈感。
比如,一个按钮点击后要有“按下-恢复”的微反馈;列表加载要有渐显;页面切换要有方向感。
我选择了 framer-motion —— 它和React Hooks结合得天衣无缝。
举个例子:一个卡片悬停放大效果。
// src/components/Card.tsx
import { motion } from 'framer-motion'
const Card = ({ children }) => {
return (
<motion.div
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.98 }}
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
className="bg-white rounded-lg shadow p-4 cursor-pointer"
>
{children}
</motion.div>
)
}
就这么几行,用户体验立马不一样。关键是,性能开销极小,因为framer-motion底层用了transform和opacity,不会触发重排。
另外,对于复杂交互动画(比如数据可视化),我会用 GSAP。它虽然重一点,但控制力无敌。不过这类场景我们内部工具暂时用不上,先不展开。
性能与兼容性:别让用户等得想砸电脑
我们虽然是内部系统,但用户量不小——全公司500+人天天用。如果页面加载慢、卡顿,IT工单分分钟爆掉。
关键优化点:
| 优化项 | 工具/方法 | 效果 |
|---|---|---|
| 代码分割 | React.lazy + Suspense | 首屏JS减少40% |
| 图片懒加载 | loading="lazy" + IntersectionObserver |
首屏渲染快1.2s |
| 缓存策略 | Vite的build.rollupOptions.output.manualChunks |
公共库单独打包,长效缓存 |
| 动画性能 | 只用transform/opacity |
60fps稳如老狗 |
| 浏览器兼容 | browserslist + @vitejs/plugin-legacy |
支持Chrome 80+ |
特别提一下兼容性。虽然我们强制用Chrome,但总有那么几个“顽固分子”用着IE模式的Edge。于是我加了@vitejs/plugin-legacy,自动生成两套bundle:现代浏览器走ESM,老浏览器走polyfilled版本。
// vite.config.ts
import legacy from '@vitejs/plugin-legacy'
export default defineConfig({
plugins: [
react(),
legacy({
targets: ['defaults', 'not IE 11']
})
]
})
上线后,连那个总抱怨“页面打不开”的财务大姐都说:“今天怎么这么快?”
书籍、简历与成长
写到这里,我突然想到——当初面试时,我说自己“热爱技术,持续学习”。HR问我最近看了什么书,我脱口而出《深入React技术栈》。其实那本书我只翻了前三章……但Cursor帮我整理了读书笔记,面试时聊得头头是道。
现在回头看,真正推动我成长的,不是书,而是真实项目里的痛点。比如这次从零搭项目,逼我重新思考:
- 为什么需要TypeScript?因为后端字段变更时,我能立刻知道哪里要改。
- 为什么自动化规范重要?因为我不想半夜被叫起来修一个因为少了个分号导致的线上bug。
- 为什么动画要有节制?因为过度动效反而分散注意力。
这些经验,比任何简历上的“精通”都值钱。
最后:AI不是拐杖,是加速器
很多人担心AI会让程序员失业。但我的体验恰恰相反——Cursor让我从重复劳动中解放出来,去思考更有价值的问题。
比如,它帮我生成了90%的配置文件,但我依然要理解每一行的作用;它能写出基础组件,但我得决定交互逻辑是否合理;它甚至能建议性能优化方案,但最终要不要用,还得我拍板。
这就像开车:导航告诉你怎么走,但方向盘还在你手里。
所以,如果你也在从零搭建项目,别怕麻烦。把基础打牢,把规范立住,把体验做好。至于那些繁琐的配置?放心交给AI吧,它不会抢你饭碗,只会让你的简历更硬。
哦对了,下周产品又要加个“暗黑模式”……我已经准备好让Cursor帮我写CSS变量了。

评论 0