从零搭建一个现代化前端项目,我踩过的坑和攒下的经验
两周前的周五晚上十点,办公室只剩我一个人。MacBook风扇呼呼作响,VSCode 里满屏红色报错,终端还在疯狂刷 npm install 的进度条。产品经理在群里@我:“明天演示要用,前端能跑起来吗?”
那一刻我真的想把键盘扔进垃圾桶——但转念一想,这不就是我两个月前入职这家新公司时,立下的 Flag 吗?“从 Android 转 Flutter,再搞懂现代 Web 前端,我要打通跨平台任督二脉!”
是的,我是那个曾经只写 Kotlin、XML 和 Jetpack Compose 的 Android 开发,现在天天和 React、Vite、TypeScript 打交道。Windows 对我来说只是用来测 IE(哦不,现在连 IE 都没了,主要是测 Edge 兼容性)。而 Mac?那是我的信仰。
最近团队要启动一个新项目:一个面向内部运营的 AI 辅助内容审核平台。技术栈由我们前端组自选——这是个机会,也是个坑。领导说:“搞个‘现代化’的,别再用三年前的老架构了。” 我心里嘀咕:现代化?那得先定义什么叫现代化。
什么是“现代化”前端项目?
在我理解里,一个现代化前端项目至少得满足:
- 开发体验丝滑:热更新快、类型安全、调试方便
- 构建高效:打包快、产物小、按需加载
- 可维护性强:代码结构清晰、测试覆盖、文档齐全
- 用户体验好:首屏快、交互流畅、无障碍支持
- 部署简单:CI/CD 自动化,一行命令上线
说白了,就是别让新人第一天就骂娘,也别让我半夜被 PagerDuty 叫醒修线上 bug。
技术选型:不是越新越好,而是“刚刚好”
我拉着两个同事开了个技术评审会(其实就是点了杯瑞幸在会议室瞎聊)。我们列了几套方案:
| 方案 | 核心工具链 | 优点 | 缺点 |
|---|---|---|---|
| A | Create React App + Webpack | 熟悉、稳定 | 构建慢、配置封闭 |
| B | Next.js | SSR、开箱即用 | 服务端耦合重,我们纯前端 |
| C | Vite + React + TypeScript | 快、轻、灵活 | 需要自己搭架子 |
我们最终选了 C。理由很简单:我们不需要 SSR,但需要极致的本地开发速度。Vite 的 ESBuild + Rollup 组合,冷启动只要 300ms,改代码热更新几乎无感——这对频繁调试 UI 的场景太友好了。
而且,作为一个刚从原生 Android 跳过来的人,Vite 的配置逻辑比 Webpack 直观太多。Webpack 那套 loader/plugin 嵌套,简直像在写 Gradle 脚本时没开 ProGuard 混淆——看得懂,但头大。
项目骨架:别一上来就 npm init
很多人以为 npm create vite@latest 就完事了,其实真正的功夫在后面。我花了一整天时间,搭出这个目录结构:
my-audit-app/
├── src/
│ ├── app/ # 应用入口
│ ├── features/ # 按功能划分的模块(不是 components!)
│ ├── shared/ # 跨模块复用的 hooks、utils、UI
│ ├── lib/ # 第三方库封装(比如 axios 实例)
│ └── assets/ # 静态资源
├── public/ # 静态文件(favicon、robots.txt)
├── tests/ # 单元/集成测试
├── .env # 环境变量
├── vite.config.ts # 构建配置
├── tsconfig.json # TS 配置
└── package.json
重点来了:我把 components 文件夹干掉了。为什么?因为在大型项目中,按“组件”组织代码,最后会变成一锅粥。你根本不知道哪个组件属于哪个业务域。
我们改用 Feature-Sliced Design(特性切片设计):每个功能模块(比如 content-review、user-management)有自己的页面、组件、hooks、API 层,彼此隔离。这样,新人接手时只需关注一个文件夹,而不是全局搜索 Button 到底有几个版本。
类型安全:TS 不是可选项,是保命符
作为 Android 开发,我早就习惯了 Kotlin 的强类型。刚接触 JS 时,看到 any 和 undefined is not a function 就头皮发麻。所以这次,TypeScript 是底线。
我们在 tsconfig.json 里开启了几乎所有严格选项:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"exactOptionalPropertyTypes": true,
"useUnknownInCatchVariables": true,
"paths": {
"@/*": ["./src/*"]
}
}
}
还加了个 eslint 规则:禁止使用 any。谁用了,CI 直接挂掉。一开始后端同事吐槽:“前端怎么比我们还卷?” 但一周后他主动来问:“你们那个 Zod schema 校验怎么做的?我们 API 参数老出问题……”
说到 Zod,我们用它做运行时类型校验。比如从 API 拿到的数据:
import { z } from 'zod';
const ContentItemSchema = z.object({
id: z.string(),
title: z.string().min(1),
status: z.enum(['pending', 'approved', 'rejected']),
aiConfidence: z.number().gte(0).lte(1)
});
type ContentItem = z.infer<typeof ContentItemSchema>;
// 使用
const data = await fetch('/api/items').then(res => res.json());
const parsed = ContentItemSchema.parse(data); // 自动校验 + 类型推导
再也不用担心后端突然把 status 改成 0/1/2 了!
性能优化:别等上线才想起 Lighthouse
项目初期我就把性能指标写进了 PR 模板:
- 首屏加载 ≤ 1.5s(3G 网络模拟)
- Bundle size ≤ 300KB(gzip 后)
- Lighthouse Performance ≥ 90
怎么做到?几个关键操作:
1. 动态导入 + 路由懒加载
// routes.tsx
const ReviewPage = lazy(() => import('@/features/content-review/pages/ReviewPage'));
function App() {
return (
<Suspense fallback={<Spinner />}>
<Routes>
<Route path="/review" element={<ReviewPage />} />
</Routes>
</Suspense>
);
}
2. 图标用 SVG Sprite,别用 iconfont
我们用 vite-plugin-svg-icons 自动生成 sprite,按需引入:
import { SvgIcon } from '@/shared/ui/SvgIcon';
function MyButton() {
return <SvgIcon name="check-circle" className="w-5 h-5 text-green-500" />;
}
体积小、可着色、无障碍友好——比字体图标香太多。
3. 防止 Bundle 膨胀
用 source-map-explorer 分析打包结果:
npm run build -- --sourcemap
npx source-map-explorer dist/assets/*.js
发现 moment.js 悄悄占了 70KB?立刻换成 date-fns。发现某个 UI 库全量引入?改成按需:
// vite.config.ts
export default defineConfig({
plugins: [
// ...
vitePluginImport({
libraryName: 'antd',
libraryDirectory: 'es',
style: true,
}),
],
});
测试与 CI:别信“我本地能跑”
我们要求每个 feature 至少包含:
- 单元测试(Jest + React Testing Library)
- E2E 测试(Playwright,跑关键路径)
CI 流程如下:
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run lint
- run: npm run type-check
- run: npm test -- --coverage
- run: npm run build
- run: npx playwright test
有一次,我提交了一个“完美”的 PR,本地测试全过。结果 CI 挂了——因为 Playwright 在 Linux 下渲染字体和 macOS 不同,导致截图对比失败。那一刻我深刻体会到:“Works on my Mac” 不等于 “Works”。
部署:一行命令上云
我们用 Vercel。为啥?因为真的只要一行:
vercel --prod
环境变量自动注入,预览部署自动生成,回滚一键完成。比起以前在 Jenkins 里写 shell 脚本、求运维开权限的日子,简直是天堂。
而且 Vercel 的边缘函数(Edge Functions)还能让我们未来轻松接入 AI 推理——比如把敏感词检测放到边缘节点,减少主服务压力。这和我最近学的 AI 技术也算呼应上了(笑)。
最后的碎碎念
从 Android 到 Flutter 再到 React,我越来越觉得:前端的本质不是框架,而是解决问题的思路。无论是 Jetpack Compose 的声明式 UI,还是 React 的状态驱动,核心都是“如何高效、可靠地把数据变成用户看得见的东西”。
这个项目上线后,Lighthouse 分数 96,首屏 1.2s(模拟 3G),团队新人三天就能独立开发 feature。上周五,产品经理又@我:“下周要加个 AI 自动生成摘要的功能,前端能接吗?”
我回了个 😎,然后默默打开了 Hugging Face 的文档。
技术分享的意义,不就是让更多人少踩一点我踩过的坑吗?
如果你也在从原生转向跨平台,或者正在搭新项目——别怕折腾,现代化的代价就是前期多花点时间。但一旦架子搭稳,后面全是爽文剧情。
对了,代码已脱敏开源(内部 GitLab),欢迎来提 PR —— 前提是你能通过我们的 ESLint 校验 😏

评论 0