从零开始构建一个现代化前端项目:一个微信小程序老油条的React开荒记
上周五晚上十点半,北京国贸地铁站的人流终于稀疏下来。我拖着疲惫的身体走出公司大楼,耳机里放着Lo-fi beats,脑子里还在盘算着明天上线的新活动页面——是的,又被产品经理临时加需求了。这次要做的不是小程序,而是一个独立的 React Web 应用,说是为了“多端覆盖”、“生态协同”。行吧,反正我们组现在既要维护小程序,又要搞 H5,还得支援 App 内嵌页,早就成了“全栈缝合怪”。
我是腾讯某事业群干了三年客户端的老兵,主要搞微信小程序业务。说实话,以前我对“纯前端”一直有点偏见——不就是写个页面、调个接口、加点动画?直到去年双11,我们小程序因为承载不了突发流量,临时拉了个 H5 页面做兜底,结果那个页面加载慢得像在用 2G 网络,用户跳出率直接飙到 70%。老板当场拍桌子:“这体验怎么拿得出手?” 那一刻我才意识到:前端,真不是随便糊弄就行的。
于是今年开年,我主动请缨牵头这个新项目,想从头到尾搭一套“现代化”的前端工程体系。别笑,我知道这个词已经被用烂了,但这次我真的不想再用“create-react-app 跑起来就完事”那种糙快猛的方式了。毕竟,在腾讯,代码不仅是功能实现,更是用户体验的基石——尤其当你面对的是上亿用户的时候。
为什么不能直接 npx create-react-app?
相信很多兄弟和我一样,一开始学 React 就是靠 CRA(Create React App)入门的。它确实香:一行命令,开发服务器、热更新、ESLint 全配好了。但一旦你开始认真对待性能、可维护性和 CI/CD,CRA 的黑盒就变成枷锁了。
比如我们项目需要:
- 支持微前端架构(未来可能被集成进主站)
- 按需加载 + 资源预加载
- 自动化生成 changelog 和版本管理
- 严格的 TypeScript 类型校验
- 所有静态资源走 CDN,并自动打 hash
这些需求,CRA 要么做不到,要么得 eject 出一堆 webpack 配置然后自己维护——那还不如从零开始。
所以,我决定“手搓”一个脚手架。别慌,不是真的从零写打包器,而是用社区成熟的工具链组合出最适合我们团队的方案。
工具链选型:现代前端的“瑞士军刀”
经过和组里几个前端老哥(对,虽然我是客户端出身,但现在也得自称前端了)反复 battle,我们最终敲定了这套组合:
| 工具 | 用途 | 为啥选它 |
|---|---|---|
| Vite | 构建工具 | 启动快如闪电,HMR 秒级响应,原生支持 TS/JSX |
| React 18 + TypeScript | 核心框架 | Concurrent Mode 对复杂交互更友好,TS 提升协作效率 |
| Tailwind CSS | 样式方案 | 原子化 CSS,避免样式冲突,设计系统一致性高 |
| ESLint + Prettier + Husky | 代码质量 | 提交前自动 lint/format,避免 Code Review 时互相伤害 |
| Vitest | 单元测试 | 跑得比 Jest 快十倍,和 Vite 共享插件生态 |
| Storybook | 组件文档 | UI 组件独立开发调试,设计走查不再扯皮 |
这里重点夸一下 Vite。以前用 webpack 开发时,改一行代码等十几秒刷新,简直想砸键盘。Vite 利用 ES modules 原生支持,启动速度从“泡杯咖啡的时间”缩短到“眨个眼的功夫”。上周我本地跑项目,同事路过看了一眼终端,惊呼:“你这项目刚 clone 下来?怎么已经跑起来了?”
从 npm init 到可交付产物
第一步:初始化项目骨架
npm create vite@latest my-awesome-project -- --template react-ts
cd my-awesome-project
npm install
别小看这三行,背后是尤雨溪团队对开发者体验的极致打磨。项目结构清晰:
my-awesome-project/
├── public/ # 静态资源(不会被处理)
├── src/
│ ├── assets/ # 图片、字体等
│ ├── components/ # 可复用组件
│ ├── pages/ # 页面级组件
│ ├── hooks/ # 自定义 Hooks
│ ├── utils/ # 工具函数
│ ├── App.tsx
│ └── main.tsx
├── index.html # 唯一 HTML 入口
├── vite.config.ts # 配置文件
└── package.json
第二步:配置 Vite —— 让它更“生产友好”
默认配置适合开发,但上线前得加点料。我们在 vite.config.ts 里做了这些调整:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { visualizer } from 'rollup-plugin-visualizer' // 分析包体积
export default defineConfig({
plugins: [
react(),
// 仅在分析时开启
process.env.ANALYZE && visualizer({ open: true })
],
build: {
// 代码分割策略:每个路由单独 chunk
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
// 将大型依赖单独拆包
if (id.includes('lodash') || id.includes('moment')) {
return 'vendor-heavy'
}
return 'vendor'
}
}
}
},
// 资源文件名带 hash,利于 CDN 缓存
assetsDir: 'assets',
sourcemap: true // 方便线上错误定位
},
server: {
// 本地代理解决跨域
proxy: {
'/api': {
target: 'https://test-api.example.com',
changeOrigin: true
}
}
}
})
这里有个坑:manualChunks 如果写得太粗暴(比如所有 node_modules 打成一个包),会导致首屏加载变慢。我们通过分析发现 lodash 和 moment 特别大,就单独拆出来,配合 <link rel="prefetch"> 预加载,首屏 FCP(First Contentful Paint)从 2.4s 降到 1.1s。
第三步:TypeScript + ESLint —— 团队协作的“防呆设计”
在腾讯,一个项目往往多人并行开发。没有强类型约束,光靠口头约定“这个字段是 string”,三天后肯定有人传 number。所以我们启用了严格模式:
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"esModuleInterop": true,
"skipLibCheck": true,
"jsx": "react-jsx"
}
}
配合 ESLint 规则,连 console.log 都不允许提交(除非加 // eslint-disable-next-line no-console 注释)。起初后端转前端的同事抱怨:“写个 demo 还要 type interface,烦死了!” 但两周后,当他在重构时不小心删了一个必传 prop,TS 直接报错阻止了 bug 上线,他默默给我点了杯奶茶。
第四步:Tailwind CSS —— 告别“样式战争”
以前我们用 CSS Modules,但设计师给的间距、颜色总和代码对不上。现在 Tailwind 让我们直接在 JSX 里写 className="p-4 bg-blue-500 rounded-lg",所见即所得。配合 tailwind.config.js 定制主题:
// tailwind.config.js
module.exports = {
content: ['./index.html', './src/**/*.{js,jsx,ts,tsx}'],
theme: {
extend: {
colors: {
primary: '#1890ff', // 腾讯蓝
},
spacing: {
'screen-1/2': '50vh'
}
}
}
}
UI 组件库我们没选 Ant Design(太重),而是基于 Tailwind 自建了一套轻量级组件,比如按钮:
// Button.tsx
type ButtonProps = {
variant?: 'primary' | 'secondary';
size?: 'sm' | 'md' | 'lg';
children: React.ReactNode;
onClick?: () => void;
};
export const Button = ({
variant = 'primary',
size = 'md',
children,
...props
}: ButtonProps) => {
const baseClasses = 'rounded font-medium transition-colors';
const variantClasses = {
primary: 'bg-primary text-white hover:bg-primary/90',
secondary: 'bg-gray-100 text-gray-800 hover:bg-gray-200'
};
const sizeClasses = {
sm: 'px-3 py-1 text-sm',
md: 'px-4 py-2',
lg: 'px-6 py-3 text-lg'
};
return (
<button
className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]}`}
{...props}
>
{children}
</button>
);
};
这样既保证一致性,又避免引入整个 UI 库的体积负担。
性能优化:不是炫技,是底线
上线前一周,测试同学拿着 Lighthouse 报告来找我:“首屏性能只有 60 分,老板说至少要 85。” 当时我内心 OS:你们知道为了兼容 IE11 我掉了多少头发吗?(开玩笑,现在谁还管 IE)
我们做了几件事:
- 懒加载路由:用 React.lazy + Suspense
const HomePage = lazy(() => import('./pages/Home'));
const AboutPage = lazy(() => import('./pages/About'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
</Routes>
</Suspense>
);
}
- 图片优化:所有图片转 WebP,配合
<picture>标签降级 - 关键 CSS 内联:用
critters插件自动提取首屏 CSS 内联到 HTML - 移除 console:生产构建时用
terser插件自动删除
最终 Lighthouse 分数冲到 92,测试同学看我的眼神都温柔了。
最后一点感悟
从零搭建项目的过程,其实是在回答一个问题:“我们到底想要什么样的开发体验?” 在腾讯,我们常说“用户为本,科技向善”,落实到代码层面,就是让开发者写得爽,让用户用得爽。
以前我觉得前端就是“切图仔”,现在才明白,现代化前端工程是一门系统艺术——工具链是骨架,性能是肌肉,用户体验才是灵魂。
现在这个项目已经稳定运行两个月,日活 50w+,0 P0 事故。上周产品又来找我:“下个需求能不能周三上线?” 我笑着回他:“行啊,只要你敢提,我就敢肝。” —— 毕竟,凌晨两点的北京,代码跑起来的感觉,真的很爽。
(完)
PS:如果你也在从零搭建项目,别怕折腾。工具会变,框架会换,但解决问题的思路永远通用。共勉!

评论 0