从零开始构建一个现代化前端项目:一个普通CS本科生的实战血泪史
大家好,我是小张(真名就不说了,怕被前司同事认出来 😅),普通一本 CS 专业大四狗,去年秋招侥幸拿了个二线大厂 offer,现在正卡在“已拿到 offer、等入职”的尴尬期。说白了就是——人还在学校,心已经飘去工位了。
最近这几个月闲得发慌,又不想天天躺平刷 B 站,就想着折腾点东西练练手。正好之前在公司干了三年多前端,眼看着团队技术栈慢慢老化(React 16 + Webpack 4 的组合还在苟延残喘),自己也萌生了跳槽的念头。想跳槽就得有新东西能写进简历,于是咬咬牙,决定从零搭一个“现代化”的前端项目,目标是:能跑、好看、快、还能吹。
顺便提一嘴,我习惯边听 Lo-fi Hip Hop 边写代码,BGM 一响,bug 就少一半(玄学)。今天这篇文,就是把整个过程扒开给你看——包括那些让我凌晨三点对着屏幕骂产品经理的瞬间。
为啥要重造轮子?因为产品需求太离谱!
事情的起因其实挺搞笑。上个月,我在 GitHub 上瞎逛,看到一个开源项目叫 daily-standup,是个极简站会打卡工具。界面干净,逻辑简单,但 UI 像 2012 年写的。我心想:“这玩意儿要是用现代技术重做一遍,说不定能当作品集项目。”
结果刚动手,噩梦就开始了。
我不是一个人在战斗——我虚构了一个“产品经理”角色(名字就叫 PM 老王吧),给他设定了几个“合理”需求:
- 用户能匿名打卡今日状态(✅ / 🚫 / 🤔)
- 实时看到团队其他成员的状态(要有 WebSocket!)
- 支持深色模式
- 手机端体验不能崩
- 上线前必须通过 Lighthouse 评分 ≥90
最后一句直接给我整不会了。我们公司去年双11期间搞了个活动页,Lighthouse 性能分才 45,被测试组贴墙上嘲笑了半个月。这次我发誓不能再翻车。
技术选型:别卷了,求稳!
虽然最近在狂啃 Rust(Rust 真香,内存安全+零成本抽象,可惜前端用不上😭),但做 Web 项目还是得回归现实。我的原则是:用社区成熟方案,别炫技。
最终敲定的技术栈如下:
| 类别 | 选型 | 理由 |
|---|---|---|
| 框架 | React 18 + TypeScript | 公司主力,熟;TS 能防我手滑 |
| 构建工具 | Vite 4 | 启动快到飞起,Webpack 配置我都配吐了 |
| 状态管理 | Zustand | 轻量、无模板代码,Redux 我真的累了 |
| UI 组件库 | ShadCN/ui + Tailwind CSS | 可定制性强,自带暗色模式支持 |
| 实时通信 | Socket.IO (前端) + mock server | 先本地模拟,后面可换真实后端 |
| 部署 | Vercel | 一键部署 + GitHub 集成,学生党福音 |
💡 吐槽时间:之前团队有个老哥非要上 Svelte,说“比 React 快 37%”。结果上线后发现 IE11 用户还有 2%,直接回滚。技术选型不是比谁新,而是比谁稳。
初始化项目:Vite 真是救我狗命
以前用 Create React App,改个 .env 都要重启,Webpack 编译慢得像乌龟爬。这次直接上 Vite:
npm create vite@latest daily-standup -- --template react-ts
cd daily-standup
npm install
npm run dev
不到 3 秒,localhost:5173 打开了。我当场感动得想给尤雨溪磕一个。
接着装一堆依赖:
npm install zustand socket.io-client tailwindcss postcss autoprefixer
npx tailwindcss init -p
配置 Tailwind 也很简单,tailwind.config.js 里加个暗色模式支持:
// tailwind.config.js
module.exports = {
content: ["./index.html", "./src/**/*.{js,jsx,ts,tsx}"],
darkMode: "class", // 或 'media',但我喜欢手动切换
theme: {
extend: {},
},
plugins: [],
}
然后在 main.tsx 里挂上 Tailwind:
import React from 'react'
import ReactDOM from 'react-dom/client'
import './index.css' // 这里引入 @tailwind base/utilities/components
import App from './App'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
搞定。这时候你可能会问:“CSS-in-JS 呢?”——别问,问就是 Tailwind 更适合快速原型开发。真要写复杂动画再上 Emotion 不迟。
状态管理:Zustand 让我告别 useEffect 地狱
以前用 Redux,光写 action + reducer 就能写半页纸。现在 Zustand 三行搞定全局状态:
// store/useStandupStore.ts
import { create } from 'zustand'
interface StandupState {
status: 'done' | 'blocked' | 'thinking' | null
setStatus: (status: StandupState['status']) => void
isDarkMode: boolean
toggleDarkMode: () => void
}
export const useStandupStore = create<StandupState>((set) => ({
status: null,
setStatus: (status) => set({ status }),
isDarkMode: false,
toggleDarkMode: () => set((state) => ({ isDarkMode: !state.isDarkMode })),
}))
在组件里直接用:
const { status, setStatus, isDarkMode, toggleDarkMode } = useStandupStore()
没有 Provider,没有 connect,没有 selector memoization。爽到飞起。
🚨 踩坑提醒:一开始我把
isDarkMode存 localStorage,结果每次刷新都闪白屏。后来改成 SSR-safe 写法:useEffect(() => { const saved = localStorage.getItem('darkMode') === 'true' if (saved) document.documentElement.classList.add('dark') }, [])这样首屏就不会 FOUC(Flash of Unstyled Content)了。
实时通信:Socket.IO 模拟器救我于水火
产品要求“实时看到队友状态”,但我不想真搭后端(懒)。于是写了个 mock server:
// utils/mockSocket.ts
type Status = 'done' | 'blocked' | 'thinking'
export class MockSocket {
private listeners: Record<string, Function[]> = {}
private fakeUsers = [
{ id: 'user1', name: 'Alice', status: 'done' as Status },
{ id: 'user2', name: 'Bob', status: 'blocked' as Status },
]
on(event: string, callback: Function) {
if (!this.listeners[event]) this.listeners[event] = []
this.listeners[event].push(callback)
// 模拟 2 秒后收到队友状态
if (event === 'status:update') {
setTimeout(() => {
this.fakeUsers.forEach(user => {
callback(user)
})
}, 2000)
}
}
emit(event: string, data: any) {
console.log('Emitting:', event, data)
// 这里可以触发本地状态更新
}
}
在组件里:
useEffect(() => {
const socket = new MockSocket()
socket.on('status:update', (user: any) => {
// 更新 UI
})
return () => {
// cleanup
}
}, [])
等真要对接后端,只要把 MockSocket 换成 io('http://real-server') 就行。抽象层万岁!
UI 与交互:细节决定用户体验
UI 我直接抄 ShadCN/ui 的 Button 和 Card,但加了点小心机:
- 点击状态按钮有微动效(
transition-transform hover:scale-105) - 深色模式切换按钮带月亮/太阳图标
- 手机端用
viewport单位保证字体大小适中
最关键的——无障碍访问(a11y)不能忘!比如状态按钮:
<button
aria-label={`Mark as ${status}`}
className="..."
onClick={() => setStatus(status)}
>
{icon}
</button>
不然测试同事又要拿 Axe DevTools 扫描我,然后发邮件说“你的按钮没有 label”。
性能优化:Lighthouse 90+ 不是梦
为了达到 Lighthouse ≥90,我做了这几件事:
- 代码分割:路由级拆包(用
React.lazy) - 图片优化:所有 icon 用 SVG inline,避免 HTTP 请求
- 字体加载:用
font-display: swap防止文字闪烁 - 关键 CSS 内联:Vite 插件自动处理
- 移除未使用代码:Tree-shaking 开启,
import { Button } from 'shadcn'而非全量引入
最骚的操作是——预加载队友状态数据。既然知道 mock 数据结构,我在 HTML 里直接塞了个 <script id="__INITIAL_STATE__">,首屏就能渲染,不用等 2 秒。
结果?Lighthouse 跑出来:
| 指标 | 分数 |
|---|---|
| Performance | 94 |
| Accessibility | 100 |
| Best Practices | 92 |
| SEO | 90 |
当场截图发朋友圈(虽然只有我妈点赞)。
部署上线:GitHub + Vercel,全自动流水线
代码托管在 GitHub(github.com/yourname/daily-standup),然后连 Vercel:
- 登录 Vercel,Import Project
- 选这个 repo
- 自动检测是 Vite 项目,配置都不用手动改
- 点 Deploy
30 秒后,daily-standup.vercel.app 可访问。连 CI/CD 脚本都不用写,学生党狂喜。
更绝的是,每次 push 到 main 分支,Vercel 自动 rebuild。上周五晚上我改了个 typo,commit 一推,喝口水回来就上线了。运维同事看了直呼内行(虽然我们公司运维还在用 Jenkins 跑 shell 脚本)。
求职视角:这个项目值不值得写进简历?
老实说,很多同学觉得“不就是个 TodoList 换皮吗”。但面试官真正在意的不是功能多复杂,而是:
- 你是否考虑了工程化(类型安全、构建优化、错误边界)
- 你是否关注用户体验(性能、a11y、响应式)
- 你是否具备产品思维(为什么做这个?解决了什么痛点?)
我把这个项目包装成“提升远程团队站会效率的轻量工具”,放在 GitHub README 里写了背景、技术难点、性能数据。投简历时附上链接,比空洞地写“熟悉 React”有用十倍。
上周面一家 startup,面试官直接打开我项目,问我:“WebSocket 断线怎么重连?”——还好我 mock 层留了扩展口,现场讲了指数退避重连策略,顺利过关。
最后几句真心话
从零搭一个现代化前端项目,最大的收获不是技术,而是对“完整链路”的理解。以前在公司,我只负责某个模块,打包部署都是运维的事。现在自己走一遍,才知道:
- 一个
vite.config.ts背后有多少坑 - 为什么 Lighthouse 会扣你“未压缩图片”的分
- 用户真的会在 iPhone SE 上打开你的页面
如果你也在等入职、或者准备跳槽,别光刷 LeetCode。花两周时间,认真做一个小而美的项目,放到 GitHub 上。它可能成为你简历上最亮眼的一行。
对了,项目地址我放 GitHub 了(匿了,但结构完全一致)。欢迎 star,更欢迎 issue —— 如果你发现我哪写错了,请务必骂我,毕竟程序员的进步,往往始于一次友好的 code review。
P.S. 写完这篇文章,Lo-fi 歌单刚好播到第 12 首。窗外天快亮了,而我的 Rust 学习计划还没开始……算了,先睡了,明天还得改简历。

评论 0