从零搭建现代前端项目:一个Cursor重度用户的实战手记

奇妙之云端
2025-12-23 07:24
阅读 466

入职新公司快两个月了,坦白说,前一个月我基本在“装”——装熟悉代码、装理解业务、装跟得上节奏。作为一个刚跳槽的前端工程师,简历上写着“精通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.cjstsconfig.jsonvite.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底层用了transformopacity,不会触发重排。

另外,对于复杂交互动画(比如数据可视化),我会用 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

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝