前端工程化最佳实践:从工具链到部署流程
一个杭州在职程序员,一边还房贷一边备考公,一边把踩过的坑写成这篇文章。
凌晨两点半,我在改CI/CD配置文件
上周五晚上,准确说是上周六凌晨两点半。
杭州滨江的夜已经凉透了,我裹着那件穿了三年、袖口都磨起毛的优衣库摇粒绒,盯着屏幕上GitLab CI的红色报错发呆。
Error: Cannot find module 'vite'
我第三遍检查了Dockerfile,第三遍检查了package.json,第三遍怀疑人生。
手机屏幕亮了一下,是老婆发来的微信:"还没睡?明天不是还要去图书馆看书吗?"
我回了个"快了快了",然后把手机扣在桌上。
房贷6800,车贷没有因为压根没买车,物业水电加起来一个月小一千。老婆在一家小公司做行政,月薪5500,勉强够日常开销。我的工资,杭州一家中型互联网公司的前端开发,税后到手18k出头。
听起来还行是吧?
但你知道杭州的房价吗?你知道我买的那个临平的小房子,89平,总价210万,首付掏空了两家六个口袋,每个月房贷雷打不动6800吗?
我今年28了。
去年十月,我做了一个决定——考公。
不是冲动,是深思熟虑。前端这行,35岁危机不是传说,是悬在每个程序员头上的达摩克利斯之剑。我见过太多30出头的大哥被"优化",见过太多人投了几百份简历石沉大海。与其等着被裁,不如自己先找条退路。
但是考公要时间啊兄弟。白天上班写代码,晚上刷题看申论,周末去图书馆占座。我恨不得把自己劈成三瓣用。
所以,工程化这件事,对我来说不是"技术追求",是生存刚需。
我得把能自动化的全自动化了,把能省的时间全抠出来。
这篇文章,就是我这一路摸爬滚打总结出来的前端工程化实践。不是什么高大上的架构设计,就是一个普通程序员,在生活的重压下,为了多挤出一点复习时间,硬生生把项目的工具链和部署流程优化了一遍的真实记录。
一、先说痛点:我到底在烦什么?
去年九月的时候,我们组接手了一个新项目。B端管理系统,技术栈是React + TypeScript + Ant Design,很常规的组合。
刚开始一切顺利,但做着做着,问题就来了。
第一个痛点:环境不一致。
我们组四个人,老张用的是Mac,小李是Windows,我是Linux(对,我就是那个异类),实习生小周用的也是Windows。同一个项目,npm install跑出来的结果居然不一样。
"哥,我这边跑不起来啊,报了一堆peer dependency的错。"小周第三次找我。
我过去一看,好家伙,他用的Node 14,我用的Node 18,老张用的Node 16。package.json里的engines字段?谁写那玩意儿啊。
第二个痛点:代码风格不统一。
老张喜欢用分号,小李不喜欢。我习惯用单引号,小周用双引号。每次Code Review,一半的时间在吵这种无聊的问题。
"这个分号到底要不要加?"
"你ESLint没配好吧?"
"我配了啊,但你的配置和我的不一样啊。"
我:……
第三个痛点:部署像玄学。
之前的项目,部署流程是这样的:
- 本地
npm run build - 把dist文件夹打成zip
- 通过FTP上传到服务器
- 解压,覆盖
- 祈祷
对,你没看错,FTP。2024年了,我们还在用FTP部署。
有一次上线,我忘了把某个环境变量改成生产环境的值,结果线上直接白屏。老板在群里@我:"这个页面怎么打不开?"
那一刻,我的手是抖的。
第四个痛点:太慢了。
项目越来越大,npm install从最开始的两分钟变成了五分钟,npm run build从三十秒变成了两分半。每次改一行代码,热更新要等四五秒。
这些时间,零零碎碎加起来,一天能有个把小时。
一个多小时啊兄弟。够我做两套行测资料分析题了。
所以我下定决心:必须搞工程化,必须把这套流程理顺。
二、工具链搭建:从混沌到秩序
2.1 统一Node版本:Volta
第一个解决的问题是Node版本。
我试过nvm,说实话,nvm挺好的,但它需要每个开发者手动安装、手动切换。总有新人进来忘了切版本,然后跑过来问你"为什么跑不起来"。
后来我发现了Volta。
这玩意儿的好处是,你可以在package.json里直接指定Node和包管理器的版本:
{
"volta": {
"node": "18.17.0",
"npm": "9.6.7"
}
}
任何人clone了项目,只要装了Volta,运行任何npm命令时,它会自动切到指定版本。不需要手动操作,不需要看文档,不需要问老员工。
我把这个改动提交上去的时候,在commit message里写了一行:"再也不用回答'你用的什么Node版本'这个问题了。"
老张给我点了个赞。
2.2 代码规范:ESLint + Prettier + Husky + lint-staged
代码风格的问题,我用了一套组合拳。
ESLint负责代码质量检查,Prettier负责格式化,Husky负责在git commit的时候触发检查,lint-staged负责只检查你改了的文件(不然每次commit都要等半天)。
配置文件我贴一下核心部分:
// .eslintrc.js
module.exports = {
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'prettier' // 这个很重要,让ESLint和Prettier不冲突
],
rules: {
'react/react-in-jsx-scope': 'off', // React 17+不需要了
'@typescript-eslint/no-explicit-any': 'warn', // 先用warn,别一上来就error,会被打
'@typescript-eslint/explicit-function-return-type': 'off'
}
}
// .prettierrc.js
module.exports = {
semi: false, // 不分号,我的选择
singleQuote: true, // 单引号
tabWidth: 2,
trailingComma: 'es5',
printWidth: 100
}
// package.json里的husky和lint-staged配置
{
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{json,md}": [
"prettier --write"
]
}
}
配好之后,效果立竿见影。
小周第一次commit的时候,被lint-staged拦住了。他跑过来问我:"哥,我代码提交不上去。"
我说:"你看终端输出。"
他看了半天:"哦……原来我这里有未使用的变量,还有格式不对。"
"对,它帮你自动修了一部分,剩下的你自己改。改完再commit。"
"这也太方便了吧。"
从那以后,Code Review的时候,再也没人纠结分号和引号的问题了。我们的Review时间从平均40分钟缩短到了15分钟。
省下来的时间,我又多刷了几十道判断推理题。
2.3 构建工具:从Webpack到Vite
说到Vite,这里有个小插曲。
今年三月的一个周末,我在临平区图书馆看书。行测做得头昏脑涨,刷会儿手机换换脑子。然后我看到了Bolt.new的介绍。
Bolt.new是一个AI驱动的全栈开发工具,可以在浏览器里直接生成和运行项目。我当时就想,这玩意儿底层用的是什么构建工具?
点进去一看,Vite。
其实我早就知道Vite了,但一直没在正式项目里用过。之前那个项目用的是Webpack 5,配置复杂得像天书。光是搞懂那些loader和plugin的配置关系,就花了我将近一周。
那天从图书馆回来,我就在项目里试了一下Vite的迁移。
结果?
本地开发启动时间从45秒变成了不到2秒。热更新从4-5秒变成了毫秒级。
我当时坐在电脑前,愣了十秒钟。
然后我打开微信,给老张发了条消息:"张哥,我想把构建工具换成Vite,你覺得呢?"
老张秒回:"能快多少?"
"启动从45秒到2秒。"
"换。"
就这么简单。程序员对性能的追求,有时候就是这么朴素。
迁移过程不算特别顺利,主要是一些Webpack特有的配置需要找Vite的替代方案。比如require.context在Vite里要用import.meta.glob来替代,一些Webpack plugin要找Vite plugin的平替。
但总体来说,半天就搞定了。
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
},
server: {
port: 3000,
open: true
},
build: {
outDir: 'dist',
sourcemap: false,
minify: 'terser',
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom', 'react-router-dom'],
antd: ['antd', '@ant-design/icons']
}
}
}
}
})
manualChunks这个配置很关键,把第三方库单独打包,利用浏览器缓存,首屏加载速度又能快一截。
三、CI/CD:告别FTP,拥抱自动化
3.1 GitLab CI/CD 配置
工具链理顺了,接下来是重头戏——部署流程。
我把FTP扔进了历史的垃圾桶,全面转向GitLab CI/CD。
说实话,刚开始写.gitlab-ci.yml的时候,我是懵的。这玩意儿的语法和逻辑跟写代码完全不同,更像是在描述一个流水线。
我花了两个晚上,查文档、看博客、问ChatGPT(对,那时候还没用Kimi),终于搞出了一个能用的版本。
# .gitlab-ci.yml
stages:
- lint
- test
- build
- deploy
variables:
NODE_IMAGE: node:18.17.0-alpine
npm_config_cache: "$CI_PROJECT_DIR/.npm"
cache:
key:
files:
- package-lock.json
paths:
- .npm/
- node_modules/
# 代码检查
lint:
stage: lint
image: $NODE_IMAGE
script:
- npm ci
- npm run lint
- npm run type-check
only:
- merge_requests
- main
# 单元测试
test:
stage: test
image: $NODE_IMAGE
script:
- npm ci
- npm run test:coverage
coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/'
artifacts:
reports:
coverageReport: coverage/lcov-report/index.html
only:
- merge_requests
- main
# 构建
build:
stage: build
image: $NODE_IMAGE
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 week
only:
- main
- develop
# 部署到测试环境
deploy:staging:
stage: deploy
image: alpine:latest
script:
- apk add --no-cache openssh-client
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- ssh-keyscan $STAGING_HOST >> ~/.ssh/known_hosts
- rsync -avz --delete dist/ $DEPLOY_USER@$STAGING_HOST:/var/www/staging/
environment:
name: staging
url: https://staging.example.com
only:
- develop
# 部署到生产环境
deploy:production:
stage: deploy
image: alpine:latest
script:
- apk add --no-cache openssh-client
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- ssh-keyscan $PROD_HOST >> ~/.ssh/known_hosts
- rsync -avz --delete dist/ $DEPLOY_USER@$PROD_HOST:/var/www/production/
environment:
name: production
url: https://www.example.com
when: manual # 生产环境手动触发,防止误操作
only:
- main
这里有几个关键点:
- cache:缓存node_modules,避免每次pipeline都重新install。这一招能省下两三分钟。
- npm ci:用
npm ci代替npm install,更严格、更快,适合CI环境。 - artifacts:构建产物传递给部署阶段,避免重复构建。
- when: manual:生产环境部署必须手动触发。这一点太重要了,我可不想某天手一抖,把没测过的代码直接推到线上。
这套配置上线的第一天,小周合并了一个MR到develop分支。然后他惊讶地发现,代码自动部署到了测试环境。
"哥,这就上了?"
"对,自动的。"
"那我以后不用手动上传了?"
"不用了。你只管写代码、提MR,剩下的交给CI/CD。"
他笑了,我也笑了。
3.2 环境变量管理
说到部署,就不得不提环境变量。
之前那个"线上白屏"的事故,就是因为环境变量没管好。开发环境的API地址和生产环境的API地址混了。
现在的做法是:
# .env.development
VITE_API_BASE_URL=http://localhost:8080/api
VITE_APP_TITLE=管理系统(开发)
# .env.staging
VITE_API_BASE_URL=https://staging-api.example.com
VITE_APP_TITLE=管理系统(测试)
# .env.production
VITE_API_BASE_URL=https://api.example.com
VITE_APP_TITLE=管理系统
在GitLab CI/CD的Settings里,配置了对应的环境变量,不同分支用不同的.env文件。
从此,再也没出过环境变量搞混的问题。
四、AI工具的加持:Kimi和Gemini帮我提效
说到提效,不得不提今年AI工具的爆发。
作为一个时间被压榨到极致的在职备考人,我对任何能提高效率的工具都抱有极大的热情。
4.1 Kimi:我的CI/CD导师
前面说了,刚开始写.gitlab-ci.yml的时候,我是很懵的。
那时候我发现了Kimi。说实话,之前我用的是ChatGPT,但Kimi对中文的理解确实更好,而且支持超长上下文,可以把整个项目的配置文件都丢给它。
我记得很清楚,有一天晚上,我在图书馆看完书回家已经十点半了。老婆已经睡了,我坐在书房里,对着那个报错的CI配置发愁。
我打开Kimi,把整个.gitlab-ci.yml和报错日志贴了进去,问它:"帮我看看哪里有问题,顺便解释一下每个stage的作用。"
Kimi不仅帮我找到了问题(是SSH key的格式问题,Windows的换行符\r在Linux里会报错),还详细解释了每个stage、每个关键字的含义。
它甚至帮我优化了缓存策略,建议我把node_modules的缓存key设为基于package-lock.json的hash,这样依赖更新的时候会自动刷新缓存。
那天晚上,我改完配置,跑通了pipeline,已经是凌晨一点了。
但我不觉得累。因为我知道,这套东西配好之后,以后每次部署,我都能省下至少半小时的手动操作时间。
半小时,够我做一道申论大题了。
4.2 Gemini:帮我写测试用例
单元测试这块,之前我们项目的覆盖率不到20%。不是不想写,是真的没时间。
后来我试了Gemini。它生成测试用例的能力让我眼前一亮。
我把一个工具函数的代码丢给Gemini,说:"帮我写单元测试,覆盖边界情况和异常场景。"
它生成的测试用例,质量出乎意料地好。不仅覆盖了正常情况,还想到了我没想到的边界条件。
// 原始函数
function formatDate(date: Date | string | number, format?: string): string {
// ...
}
// Gemini生成的测试用例
describe('formatDate', () => {
it('should format Date object correctly', () => {
const date = new Date('2024-01-15T10:30:00')
expect(formatDate(date, 'YYYY-MM-DD')).toBe('2024-01-15')
})
it('should format string date correctly', () => {
expect(formatDate('2024-01-15', 'YYYY/MM/DD')).toBe('2024/01/15')
})
it('should format timestamp correctly', () => {
const timestamp = new Date('2024-01-15').getTime()
expect(formatDate(timestamp)).toBe('2024-01-15')
})
it('should throw error for invalid date', () => {
expect(() => formatDate('invalid-date')).toThrow()
})
it('should use default format when not specified', () => {
const date = new Date('2024-01-15')
expect(formatDate(date)).toBe('2024-01-15')
})
it('should handle null and undefined gracefully', () => {
expect(() => formatDate(null as any)).toThrow()
expect(() => formatDate(undefined as any)).toThrow()
})
})
当然,AI生成的测试用例不能直接用,还是需要人工review。但比起从零开始写,效率提升了至少三倍。
现在我们的测试覆盖率已经到了65%,目标是年底达到80%。
4.3 Bolt.new:快速原型验证
前面提到过Bolt.new。这个工具我主要用在两个场景:
一是快速验证技术方案。比如我想试一下某个新的状态管理方案,但又不想在正式项目里折腾。Bolt.new可以在几分钟内帮我搭一个demo出来,跑通之后再把思路迁移到正式项目。
二是给老婆做小工具。是的,我老婆有时候需要一些简单的数据处理工具,比如把Excel里的数据按某个规则分类汇总。以前我可能懒得做,现在用Bolt.new,十分钟就能搞出来一个能用的网页版小工具。
"老公你也太厉害了吧!"
嘿嘿,家庭地位+1。
五、工程化不是目的,是手段
写了这么多技术细节,我想说说我的感悟。
工程化这件事,很多人觉得是"高大上"的东西,是大厂才需要关心的。但对我来说,它就是一件很朴素的事情——省时间。
我来算一笔账:
| 优化项 | 之前耗时 | 之后耗时 | 每次节省 | 频率 | 每月节省 |
|---|---|---|---|---|---|
| 环境不一致排查 | 30min | 0 | 30min | 2次 | 1h |
| 代码风格争论 | 20min/次 | 0 | 20min | 5次 | 1.7h |
| 本地启动等待 | 45s | 2s | 43s | 20次 | 0.5h |
| 手动部署 | 30min | 0 | 30min | 4次 | 2h |
| 写测试用例 | 2h/模块 | 40min/模块 | 1.3h | 2次 | 2.6h |
| 合计 | 约8h |
每个月省出8个小时。
8个小时,够我做4套行测模拟卷了。够我背200个常识知识点了。够我把申论的真题多写两遍了。
这些时间,就是工程化带给我的最大价值。
六、一些掏心窝子的话
写到这里,已经凌晨了。
老婆在卧室里翻了个身,迷迷糊糊问了句:"还没睡啊?"
"快了,文章快写完了。"
"别太晚了,明天还要早起。"
"嗯,马上。"
我看了看时间,已经快一点了。
下周六就是国考了。
说实话,我很紧张。准备了一年,行测从最初的55分提到了现在的72分,申论从50分提到了65分。不算高,但对我来说,已经是每天挤时间、一点一点磨出来的成绩了。
我不知道能不能考上。
但我知道,这一年的准备,让我学会了很多东西。不只是行测和申论的知识,还有如何管理时间、如何优化流程、如何在有限的资源下做到最好。
这些能力,不管考不考得上,都是我的。
就像工程化一样。
很多人觉得工程化是"锦上添花"的东西,业务都忙不过来了,搞什么工程化?
但我的经验告诉我,工程化是雪中送炭。它不是让你多做事情,而是让你少做重复的、低价值的事情,把时间留给真正重要的东西。
对我来说,真正重要的东西,是家人的笑容,是生活的稳定,是未来的确定性。
如果考公上岸了,我会感谢这一年的自己。
如果没考上,我也不会后悔。因为这一年里,我把工作理顺了,把技术提升了,把时间管理做好了。这些东西,到哪里都用得上。
最后,给同行的你一些建议
如果你也是一个在职备考的程序员,或者你只是单纯想把工程化做好,这里有几条我的建议:
从小处着手。不要一上来就搞什么"全套DevOps",先把最痛的点解决了。环境不一致?先统一Node版本。代码风格乱?先上ESLint + Prettier。一步一步来。
善用AI工具。Kimi、Gemini、Bolt.new,这些工具不是来替代你的,是来帮你的。把重复性的、模板化的工作交给AI,把思考和决策留给自己。
算清楚时间账。工程化的投入是有成本的,但收益也是实实在在的。算清楚你每个月在哪些事情上浪费了时间,优先解决那些高频的、耗时的痛点。
别忘了生活。技术很重要,但生活更重要。该陪家人的时候陪家人,该休息的时候休息。别把自己逼太紧。
保持学习,但别焦虑。技术更新太快了,你不可能什么都学。抓住核心的、底层的东西,其他的,用的时候再学。
好了,真的该睡了。
明天还要去图书馆做最后一套模拟卷。
不管结果如何,这一路走来,我不后悔。
如果你也在类似的处境里,希望这篇文章能给你一些启发。工程化不是为了炫技,是为了让我们的生活更好一点。
共勉。
—— 2024年11月的某个深夜,于杭州临平
P.S. 如果这篇文章对你有帮助,欢迎点赞收藏。等我考完研……不对,考完公,再来更新后续。希望下次写文章的时候,身份已经从"在职程序员"变成"上岸的体制内程序员"了。哈哈。
P.P.S. 评论区可以聊聊你们的工程化实践,互相学习。也欢迎同样在备考的朋友一起加油。


评论 0