请写一篇关于【前端工程化最佳实践:从工具链到部署流程】的技术文章。写作风格要求:踩坑经验分享。文章需要涵盖以下关键词:Sora,文心一言,Ollama,React。
凌晨三点,我在浦东的出租屋里重构了整个前端工程
上周五晚上十一点半,女朋友已经睡了。我蹑手蹑脚从床上爬起来,轻手轻脚地走到客厅那张1米2的小书桌前,打开MacBook。屏幕的蓝光映在脸上,我灌了半罐已经没气的可乐,盯着屏幕上那坨跑了8分钟的CI/CD流水线,脑子里只有一个念头:
"这破工程化,老子今天非得给它整明白不可。"
先自我介绍一下。我,坐标上海浦东,某中型互联网公司前端开发,工龄四年半。去年十月,在经历了大概二十多次相亲——对,二十多次,我妈给我安排的,有护士、有老师、有银行柜员,甚至还有一个在静安寺卖水晶的——终于,在一个朋友组织的剧本杀局上,遇到了我现在的媳妇儿,小雅。
小雅是做UI设计的,我们在浦东张江附近合租了一个两室一厅,房租3500,押一付三。说实话,刚搬进来的时候,我月薪15k,交完房租再扣掉五险一金,每个月到手也就一万出头。当时真的很焦虑,看着浦东的房价,再看看自己的银行余额,差点想放弃前端这行回老家考公。
但小雅跟我说了一句话,我记到现在:"你先把眼前的事做好,别总想那些有的没的。"
就是这句话,让我决定死磕前端工程化。因为当时我们组的项目,工程化简直是一坨屎。
那坨屎一样的旧工程
我们组的项目是一个ToB的后台管理系统,技术栈是React 16 + Webpack 4 + 一堆乱七八糟的loader和plugin。说实话,这配置还是三年前一个已经离职的大哥留下来的,没人敢动,也没人会动。
具体有多烂呢?我给你说几个数字:
- 本地冷启动:4分37秒。对,你没看错,改一行代码,等四分多钟才能看到效果。
- 生产环境构建:8分12秒。每次发版,我都得去楼下便利店买杯咖啡等着。
- 打包产物体积:首屏JS 2.3MB。用户打开页面,得转圈转个五六秒。
- CI/CD流水线:从代码提交到部署上线,22分钟。
我们组一共四个前端,每天光是等构建、等部署,就能浪费掉差不多一个小时。组长老王每次周会都骂:"你们能不能快点?业务方催得紧!"但没人敢接话,因为大家都知道,这锅不是前端的,是工程化的。
转折发生在今年三月份。公司接了个大项目,要给某金融机构做一套数据可视化平台。老板亲自开会,说这个项目要用最新的技术栈,要"体现公司的技术实力"。
老王把任务分下来,让我负责前端工程化的搭建。
当时我心里是崩溃的。我之前的经验,说白了就是"能跑就行"。什么Monorepo、什么微前端、什么模块化构建,我都是面试的时候背过概念,实操基本为零。
但我没退路。小雅那会儿刚换了工作,月薪从8k涨到了12k,我们正准备攒钱下半年结婚。我如果在这个项目上拉胯,别说涨薪了,能不能保住现在的位置都两说。
踩坑之路:从Vite到Turborepo
第一个坑:构建工具的选择
最开始我想的是继续用Webpack 5,毕竟团队都熟悉。但实测下来,冷启动还是要两分多钟,根本达不到老板要求的"秒级启动"。
后来我试了Vite。说实话,第一次跑起来的时候我惊了——冷启动1.2秒。我当时在工位上差点喊出来,隔壁工位的小李以为我疯了。
但Vite也不是万能的。我们项目里有一些老模块依赖CommonJS,Vite对CJS的支持一直是个坑。我花了整整三天时间,写了一个自定义插件去转换这些模块,头发都掉了一把。
// vite.config.js 中处理 CJS 兼容的踩坑代码
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import commonjs from 'vite-plugin-commonjs'
export default defineConfig({
plugins: [
react(),
commonjs({
// 这里踩了个大坑:有些包用了动态 require
// 必须手动指定 include
include: [/node_modules\/(lodash|moment)/]
})
],
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
antd: ['antd', '@ant-design/icons'],
echarts: ['echarts']
}
}
}
}
})
第二个坑:Monorepo的诱惑与代价
项目越做越大,组件库、工具函数库、业务代码全堆在一个仓库里,代码review的时候简直是灾难。我提议上Monorepo,用Turborepo来管理。
理想很丰满,现实很骨感。
第一个问题:Turborepo的缓存机制。理论上它应该能缓存没改动的包的构建结果,但实际上,我们有一个共享的utils包,每次改一行代码,所有依赖它的业务包都要重新构建。我研究了好久,才发现是tsconfig的paths配置没对齐,导致Turborepo认为依赖关系变了。
第二个问题:CI/CD流水线的适配。我们之前的Jenkins配置是针对单仓库的,改成Monorepo之后,得重新写pipeline。我花了两个周末,才把增量构建的逻辑跑通。
# turbo.json 踩坑记录
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
// 坑:这里如果不配 outputs,
// Turborepo 每次都会重新构建
},
"lint": {
"outputs": []
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"]
}
}
}
第三个坑:部署流程的噩梦
构建搞定了,部署又是一坨屎。
我们之前的部署流程是这样的:前端打包 → 手动把dist目录scp到服务器 → 手动重启Nginx。对,手动。每次发版,我都得SSH到服务器上敲命令,有一次手抖把生产环境的配置删了,整个系统挂了四十分钟,我被老王骂得狗血淋头。
我花了两周时间,搭了一套完整的CI/CD流程:
- 代码提交 → 触发GitLab CI
- 自动化检查 → ESLint + Prettier + Stylelint + 单元测试
- 自动化构建 → Turborepo增量构建
- Docker镜像打包 → 推到内部镜像仓库
- K8s滚动部署 → 自动灰度发布
这套流程搭好之后,发版时间从22分钟降到了6分钟,而且全程自动化,再也不用手动SSH了。
# 多阶段构建 Dockerfile,这个也踩了坑
# 坑:Node 镜像太大,生产环境用 alpine 版本
FROM node:18-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm turbo build --filter=@app/web
FROM nginx:alpine
COPY --from=builder /app/apps/web/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
AI工具的加持:那些帮我续命的夜晚
说实话,整个工程化重构的过程中,如果不是有AI工具帮忙,我可能早就放弃了。
那段时间,我几乎每天都在用文心一言。不是那种"帮我写个函数"的用法,而是把它当成一个技术顾问。比如我在研究Turborepo缓存机制的时候,直接把报错日志和配置文件贴给它,让它帮我分析原因。有一次凌晨两点,我死活搞不定一个Webpack到Vite迁移的兼容问题,文心一言给了我一个很冷门的思路——用@originjs/vite-plugin-commonjs替代我手写的那个插件,问题迎刃而解。
后来Sora火了之后,我也试着用它来做一些项目演示视频的生成。你别说,给业务方汇报的时候,放一段Sora生成的产品概念视频,比干巴巴地讲PPT有用多了。老板看完直接说:"这个效果不错,下个月预算给你加20%。"
至于Ollama,这是我最近才开始玩的。我在本地跑了一个CodeLlama模型,主要是用来做代码review的辅助。虽然效果比不上云端的大模型,但胜在免费、隐私安全,而且不用联网。有时候半夜改bug,不想把公司代码传到外部API,Ollama就派上用场了。
当然,最核心的业务代码,还是得靠React本身的基本功。工程化再好,代码写得烂也是白搭。我花了大量时间重构组件,把之前那些两千行的巨型组件拆成粒度更小的hooks和组件,配合React 18的Concurrent Features,首屏渲染性能直接提升了一倍。
// React 18 性能优化的一个实际例子
import { Suspense, useTransition, useState } from 'react'
function Dashboard() {
const [isPending, startTransition] = useTransition()
const [data, setData] = useState(null)
const handleFilterChange = (filter) => {
// 用 startTransition 包裹非紧急更新
// 避免阻塞用户输入
startTransition(() => {
setData(fetchFilteredData(filter))
})
}
return (
<div>
<FilterBar onChange={handleFilterChange} />
{isPending ? <Skeleton /> : (
<Suspense fallback={<Loading />}>
<DataGrid data={data} />
</Suspense>
)}
</div>
)
}
成果与收获
折腾了大概四个月,到今年七月份,整个前端工程化体系终于稳定下来了。给大伙看看数据对比:
| 指标 | 改造前 | 改造后 |
|---|---|---|
| 本地冷启动 | 4分37秒 | 1.2秒 |
| 生产构建 | 8分12秒 | 2分40秒 |
| 首屏JS体积 | 2.3MB | 487KB |
| CI/CD全流程 | 22分钟 | 6分钟 |
| 发版事故率 | 每月1-2次 | 连续3个月零事故 |
老王在季度总结会上专门提了一嘴,说我们组的工程化水平"达到了公司前列"。年底调薪,我从15k涨到了22k。虽然在上海这点钱也就那样,但至少,我和小雅的婚礼有着落了。
一些真心话
写了这么多,其实最想说的是:前端工程化不是什么高大上的东西,它本质上就是让你和你的团队少加班、少背锅、少掉头发的工具。
不要为了用新工具而用新工具。Vite很好,但如果你的项目大量依赖CJS,迁移成本可能比你想象的高得多。Monorepo很好,但如果你的团队只有两三个人,可能就是过度设计了。
还有,一定要善用AI工具。文心一言、Ollama这些,不是要替代你,而是帮你加速学习曲线。我当年要是有人告诉我Turborepo的缓存坑在哪,我能少熬多少个夜?
最后,也是最重要的——别把生活搞丢了。
重构工程化那四个月,我几乎每天都是最后一个走公司的。小雅虽然没说什么,但我知道她有意见。有一次周末,她拉我去世纪公园散步,我脑子里还在想Docker配置的事,她直接生气了:"你到底是在跟我谈恋爱,还是在跟电脑谈恋爱?"
那句话把我骂醒了。技术固然重要,但生活才是目的。后来我给自己定了规矩:晚上十一点之后不碰代码,周末至少抽一天完全不工作。
现在回想起来,那段日子虽然苦,但真的很充实。从一个"能跑就行"的切图仔,到现在能独立搭建一整套前端工程化体系,这个过程让我成长了很多。
如果你也在经历类似的困境,别怕,慢慢来。把问题拆小,一个一个解决。实在不行,就出去走走,吹吹浦东的风,看看陆家嘴的灯。
日子总会好起来的。就像我和小雅,从合租的两室一厅,到上个月刚签了一套小两居的购房合同——虽然还在外环外,但好歹是在上海扎根了。
好了,不说了,小雅喊我吃饭了。今天是周五,她做了红烧肉。
以上,共勉。
写于2024年某个周五的傍晚,浦东


评论 0