从零搭建一个现代化前端项目,我踩过的坑和攒下的经验

赵艳
2025-12-24 23:18
阅读 1393

两周前的周五晚上十点,办公室只剩我一个人。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-reviewuser-management)有自己的页面、组件、hooks、API 层,彼此隔离。这样,新人接手时只需关注一个文件夹,而不是全局搜索 Button 到底有几个版本。


类型安全:TS 不是可选项,是保命符

作为 Android 开发,我早就习惯了 Kotlin 的强类型。刚接触 JS 时,看到 anyundefined 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

最热最新
暂无评论
赵艳Lv.1
0
影响力
0
文章
0
粉丝