前端工程化最佳实践:从工具链到部署流程

萌萌熊
2026-07-10 07:07
阅读 240

一个杭州在职程序员,一边还房贷一边备考公,一边把踩过的坑写成这篇文章。


凌晨两点半,我在改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没配好吧?"

"我配了啊,但你的配置和我的不一样啊。"

我:……

第三个痛点:部署像玄学。

之前的项目,部署流程是这样的:

  1. 本地npm run build
  2. 把dist文件夹打成zip
  3. 通过FTP上传到服务器
  4. 解压,覆盖
  5. 祈祷

对,你没看错,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

这里有几个关键点:

  1. cache:缓存node_modules,避免每次pipeline都重新install。这一招能省下两三分钟。
  2. npm ci:用npm ci代替npm install,更严格、更快,适合CI环境。
  3. artifacts:构建产物传递给部署阶段,避免重复构建。
  4. 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分。不算高,但对我来说,已经是每天挤时间、一点一点磨出来的成绩了。

我不知道能不能考上。

但我知道,这一年的准备,让我学会了很多东西。不只是行测和申论的知识,还有如何管理时间、如何优化流程、如何在有限的资源下做到最好。

这些能力,不管考不考得上,都是我的。

就像工程化一样。

很多人觉得工程化是"锦上添花"的东西,业务都忙不过来了,搞什么工程化?

但我的经验告诉我,工程化是雪中送炭。它不是让你多做事情,而是让你少做重复的、低价值的事情,把时间留给真正重要的东西。

对我来说,真正重要的东西,是家人的笑容,是生活的稳定,是未来的确定性。

如果考公上岸了,我会感谢这一年的自己。

如果没考上,我也不会后悔。因为这一年里,我把工作理顺了,把技术提升了,把时间管理做好了。这些东西,到哪里都用得上。


最后,给同行的你一些建议

如果你也是一个在职备考的程序员,或者你只是单纯想把工程化做好,这里有几条我的建议:

  1. 从小处着手。不要一上来就搞什么"全套DevOps",先把最痛的点解决了。环境不一致?先统一Node版本。代码风格乱?先上ESLint + Prettier。一步一步来。

  2. 善用AI工具。Kimi、Gemini、Bolt.new,这些工具不是来替代你的,是来帮你的。把重复性的、模板化的工作交给AI,把思考和决策留给自己。

  3. 算清楚时间账。工程化的投入是有成本的,但收益也是实实在在的。算清楚你每个月在哪些事情上浪费了时间,优先解决那些高频的、耗时的痛点。

  4. 别忘了生活。技术很重要,但生活更重要。该陪家人的时候陪家人,该休息的时候休息。别把自己逼太紧。

  5. 保持学习,但别焦虑。技术更新太快了,你不可能什么都学。抓住核心的、底层的东西,其他的,用的时候再学。


好了,真的该睡了。

明天还要去图书馆做最后一套模拟卷。

不管结果如何,这一路走来,我不后悔。

如果你也在类似的处境里,希望这篇文章能给你一些启发。工程化不是为了炫技,是为了让我们的生活更好一点。

共勉。

—— 2024年11月的某个深夜,于杭州临平


P.S. 如果这篇文章对你有帮助,欢迎点赞收藏。等我考完研……不对,考完公,再来更新后续。希望下次写文章的时候,身份已经从"在职程序员"变成"上岸的体制内程序员"了。哈哈。

P.P.S. 评论区可以聊聊你们的工程化实践,互相学习。也欢迎同样在备考的朋友一起加油。

评论 0

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