深夜重构前端工程化流程的一些思考
凌晨一点半,客厅的灯早关了,书房里就剩我显示器这点亮光。隔壁卧室传来二宝翻身的动静,我下意识屏住呼吸听了两秒——没哭,继续。大宝倒是省心,雷打不动的睡相,随他妈。
我灌了口已经凉透的咖啡,揉了揉发酸的眼睛,把视线从屏幕上那坨祖传的 webpack 配置移开。在公司待了三年多,从最初一个人撸前端,到现在带着三个小弟,项目越做越大,但工程化这块儿一直是个"能用就行"的状态。最近面试了几家,被问到工程化体系的时候,说实话有点虚。不是不会,是太碎了,没系统地梳理过。
索性今晚不写代码了,把这三年的坑和心得整理出来,也算给自己交个底。毕竟三十好几的人了,不能总靠"深夜肝代码"拼体力,得有点体系化的东西傍身。
先说说我接手时的那坨屎山
三年前刚入职的时候,公司前端项目是这样的:
- 构建工具是 webpack 3,对,你没看错,webpack 3
- 没有统一的代码规范,每个人写出来的风格都不一样
- 部署靠手动 ftp 上传,运维大哥每次发版都在群里骂娘
- 测试环境、预发环境、生产环境的配置全靠人肉记忆切
- 没有 CI/CD,每次上线都像在拆炸弹
当时我心想,这特么也能跑?但没办法,业务催得紧,产品天天追着要需求,哪有时间搞这些"基础设施"。直到去年双11前夕,线上出了一个 P0 级事故——某个环境的 API 地址配错了,导致用户下单后支付回调找不到接口,整整影响了四十分钟。
那天晚上我在公司待到凌晨四点,一边排查一边想:这日子没法过了。
工具链这块,我选了 Vite 而不是继续Webpack
说干就干。第一步就是换构建工具。webpack 5 虽然也能用,但配置实在太繁琐了。我花了两个周末(没错,就是大宝上早教课我在外面等的那两个周末)研究了一圈,最后选了 Vite。
原因很简单:
| 维度 | Webpack 5 | Vite |
|---|---|---|
| 冷启动速度 | 慢,项目大了要几十秒 | 快,基于 ESM,秒级启动 |
| HMR 速度 | 随项目增大变慢 | 基本不受项目规模影响 |
| 配置复杂度 | 高,loader/plugin 一堆 | 低,开箱即用 |
| 生态成熟度 | 非常成熟 | 快速追赶中 |
| 生产构建 | Rollup 也行但得自己配 | 内置 Rollup,配置简单 |
有个细节特别爽:以前改一行代码等 HMR 要三四秒,现在基本是瞬间。对于我们这种经常调试前端动画和交互的项目来说,这个体验提升太明显了。我最近在搞一个页面转场动画,来回调贝塞尔曲线参数,Vite 的 HMR 速度让我感觉像是在用 Figma 做原型。
迁移过程也不是一帆风顺。最大的坑是有些老插件不兼容 Vite,特别是几个公司内部封装的 webpack plugin。没办法,只能自己重写或者找替代方案。这里分享一个我重写的路径别名解析的配置:
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
'@components': path.resolve(__dirname, 'src/components'),
'@utils': path.resolve(__dirname, 'src/utils'),
'@assets': path.resolve(__dirname, 'src/assets'),
},
},
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "@/styles/variables.scss";`,
},
},
},
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
build: {
rollupOptions: {
output: {
manualChunks: {
'vendor': ['vue', 'vue-router', 'pinia'],
'echarts': ['echarts'],
},
},
},
},
})
有个小技巧:manualChunks 这个配置很关键。我们把第三方库单独拆包,利用浏览器缓存,用户第二次访问的时候 vendor 包直接从缓存读,体验好很多。
代码规范这事儿,得靠机器不靠人
以前团队里有个兄弟,代码写得那叫一个"自由"。变量命名全靠心情,缩进全凭手感,每次 code review 我都能被他气到血压升高。后来我痛定思痛,搞了一套自动化的代码规范体系。
核心就是这几个东西的组合拳:
// package.json 里的相关配置
{
"scripts": {
"lint": "eslint src --ext .ts,.tsx,.vue",
"lint:fix": "eslint src --ext .ts,.tsx,.vue --fix",
"format": "prettier --write \"src/**/*.{ts,tsx,vue,scss,css,json}\"",
"prepare": "husky install"
},
"lint-staged": {
"src/**/*.{ts,tsx,vue}": [
"eslint --fix",
"prettier --write"
],
"src/**/*.{scss,css}": [
"stylelint --fix",
"prettier --write"
]
}
}
配合 husky 在 git commit 前自动跑 lint-staged,不通过就不让提交。刚开始团队怨声载道,特别是那个"自由派"兄弟,天天吐槽我搞这些形式主义。但两个月后他自己都说:"嗯,现在看别人的代码舒服多了。"
这里有个坑要提醒大家:ESLint 和 Prettier 的规则可能会冲突。我的解决方案是用 eslint-config-prettier 关掉 ESLint 中跟格式化相关的规则,让 Prettier 全权负责格式化。不然你会发现,ESLint 刚格式化完,Prettier 又给改回去了,无限循环,能把人逼疯。
// .eslintrc.js
module.exports = {
extends: [
'eslint:recommended',
'plugin:vue/vue3-recommended',
'@vue/typescript/recommended',
'prettier', // 这个一定要放最后,用来关闭冲突规则
],
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'@typescript-eslint/no-explicit-any': 'warn', // 先warn,后面慢慢收紧
'@typescript-eslint/explicit-module-boundary-types': 'off',
},
}
CI/CD 这块我用了 GitHub Actions
部署流程的改造是我花了最多心思的地方。以前手动 ftp 上传的日子,真的是提心吊胆。有一次我手滑把测试环境的包传到了生产,幸好运维大哥发现得早,不然又是一次线上事故。
现在公司用 GitHub 做代码托管,我就顺水推舟上了 GitHub Actions。说实话,GitHub Actions 的体验确实不错,yaml 配置写起来也很直观。
# .github/workflows/deploy.yml
name: Deploy Frontend
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
NODE_VERSION: '18'
jobs:
lint-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run ESLint
run: npm run lint
- name: Run unit tests
run: npm run test:unit
- name: Run build
run: npm run build
deploy-staging:
needs: lint-and-test
if: github.ref == 'refs/heads/develop'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Build for staging
run: npm run build:staging
- name: Deploy to staging
uses: appleboy/scp-action@master
with:
host: ${{ secrets.STAGING_HOST }}
username: ${{ secrets.STAGING_USER }}
key: ${{ secrets.STAGING_SSH_KEY }}
source: "dist/*"
target: "/var/www/staging"
deploy-production:
needs: lint-and-test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production # 需要手动审批
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Build for production
run: npm run build:prod
- name: Deploy to production
uses: appleboy/scp-action@master
with:
host: ${{ secrets.PROD_HOST }}
username: ${{ secrets.PROD_USER }}
key: ${{ secrets.PROD_SSH_KEY }}
source: "dist/*"
target: "/var/www/production"
- name: Notify team
if: success()
run: |
curl -X POST ${{ secrets.WEBHOOK_URL }} \
-H 'Content-Type: application/json' \
-d '{"msgtype":"text","text":{"content":"前端生产环境部署完成 ✅"}}'
这里有个设计上的考量:生产环境的部署我加了 environment: production,这样每次上线都需要在 GitHub 上手动点一下确认。不是为了卡流程,是为了防止有人直接 push 到 main 分支就自动部署了。上次有个新来的小弟,合并了一个没测全的分支到 main,差点直接上了生产。有了这个审批环节,至少多了一道保险。
另外,部署完成后自动发通知到企业微信群,这个功能虽然小,但很实用。运维大哥再也不用盯着发布系统看了,群里一有消息就知道前端发版了。
多环境配置,别再用人脑记了
说到环境配置,这也是一大痛点。以前我们靠 .env 文件区分环境,但管理起来很混乱,经常有人把测试环境的配置带到生产。
我的方案是用环境变量 + 构建时注入的方式:
# .env.development
VITE_APP_TITLE=我的应用(开发)
VITE_APP_API_BASE=http://localhost:8080/api
VITE_APP_CDN_URL=
# .env.staging
VITE_APP_TITLE=我的应用(测试)
VITE_APP_API_BASE=https://staging-api.example.com
VITE_APP_CDN_URL=https://staging-cdn.example.com
# .env.production
VITE_APP_TITLE=我的应用
VITE_APP_API_BASE=https://api.example.com
VITE_APP_CDN_URL=https://cdn.example.com
然后在 package.json 里配好对应的构建命令:
{
"scripts": {
"dev": "vite",
"build:staging": "vite build --mode staging",
"build:prod": "vite build --mode production"
}
}
这样每个环境有独立的配置文件,构建时自动注入,再也不会出现"我明明改了啊怎么没生效"的灵异事件。
前端监控,上线不是结束是开始
以前项目上线后就是"听天由命",出了问题全靠用户反馈。有一次一个页面白屏了整整两天,直到客户打电话投诉我们才知道。
后来我接入了前端监控系统,主要监控这几个维度:
- JS 错误捕获和上报
- 接口请求异常监控
- 页面性能指标(FCP、LCP、CLS 等)
- 用户行为追踪
这块我用了 Sentry,开源免费,自建也不复杂。关键代码其实很简单:
// src/utils/monitor.ts
import * as Sentry from '@sentry/vue'
import { BrowserTracing } from '@sentry/tracing'
import type { App } from 'vue'
import type { Router } from 'vue-router'
export function setupMonitor(app: App, router: Router) {
if (import.meta.env.PROD) {
Sentry.init({
app,
dsn: import.meta.env.VITE_SENTRY_DSN,
integrations: [
new BrowserTracing({
routingInstrumentation: Sentry.vueRouterInstrumentation(router),
tracePropagationTargets: ['api.example.com', /^\//],
}),
],
tracesSampleRate: 0.2, // 只采样20%,不然数据量太大
release: `frontend@${import.meta.env.VITE_APP_VERSION}`,
beforeSend(event) {
// 过滤掉一些已知的无害错误
if (event.message?.includes('ResizeObserver loop')) {
return null
}
return event
},
})
}
}
有个细节:tracesSampleRate 不能设太高。我们日活大概五万左右,如果 100% 采样,Sentry 那边的数据量能把免费额度撑爆。0.2 的采样率基本够用了,真出问题的时候错误上报是 100% 的,不受这个限制。
性能优化,不只是首屏快
说到性能优化,这可能是我兴趣最浓的一块了。前端动画和交互体验,归根结底还是得靠性能撑着。
我们项目之前有个问题:首页加载要 4 秒多。4 秒!这年头用户等 3 秒就想关页面了。我花了一周时间做优化,把首屏时间压到了 1.5 秒以内。
主要做了这几件事:
- 路由懒加载:这个基本是标配了,不细说
- 图片优化:全面换成 WebP 格式,配合
loading="lazy"懒加载 - 关键 CSS 内联:首屏用到的 CSS 直接内联到 HTML 里,减少一次请求
- 预加载关键资源:用
<link rel="preload">提前加载首屏需要的字体和关键 JS - Tree Shaking:确保用到的 UI 库支持按需引入
// router/index.ts - 路由懒加载
const routes = [
{
path: '/',
component: () => import('@/layouts/DefaultLayout.vue'),
children: [
{
path: '',
name: 'Home',
component: () => import('@/views/Home.vue'),
},
{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue'),
// 对动画要求高的页面,可以预加载
meta: { preload: true },
},
],
},
]
这里分享一个调试小技巧:Chrome DevTools 的 Performance 面板是神器,但很多人不会用。我的习惯是:先录一段页面加载的过程,然后看 Main 线程的火焰图,找到耗时最长的任务。通常瓶颈要么是 JS 执行时间太长,要么是渲染阻塞。如果是 JS 的问题,就去看 Call Tree,找到具体是哪个函数在拖后腿。
文档,虽然烦但真的有用
最后说一个很多人不爱干的事:写文档。
我以前也觉得,代码就是最好的文档。但团队大了之后发现,新来的人根本看不懂你那些"巧妙"的设计。特别是工程化相关的配置,不写文档的话,半年后连自己都看不懂。
我在项目根目录建了个 docs 文件夹,主要包含这些内容:
- 项目架构说明
- 开发环境搭建指南
- 工程化配置说明(就是本文这种)
- 部署流程文档
- 常见问题 FAQ
文档用 Markdown 写,直接放在代码仓库里。好处是:代码改了文档也得跟着改,不然 PR review 的时候就给你打回来。
写在最后
搞完这一套,最大的感受是:前端工程化不是选几个工具拼在一起就完事了,它需要结合团队实际情况来设计。工具是为人服务的,如果一套流程让团队觉得麻烦、不愿意遵守,那再好的方案也是白搭。
另外就是,这些事儿真的得抽时间系统地搞。平时业务需求压着,根本没时间思考这些。我这次能梳理出来,也是借着想换环境的由头,逼自己把这几年的东西沉淀一下。
好了,快两点了。明天还得送大宝上学,得睡了。希望这篇文章对同样在深夜肝代码的同行们有点帮助。如果有什么问题,欢迎评论区交流。
不说了,我去看看二宝踢被子了没。


评论 0