深夜重构前端工程化流程的一些思考

王华_大数据
2026-07-25 09:54
阅读 1046

凌晨一点半,客厅的灯早关了,书房里就剩我显示器这点亮光。隔壁卧室传来二宝翻身的动静,我下意识屏住呼吸听了两秒——没哭,继续。大宝倒是省心,雷打不动的睡相,随他妈。

我灌了口已经凉透的咖啡,揉了揉发酸的眼睛,把视线从屏幕上那坨祖传的 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 秒以内。

主要做了这几件事:

  1. 路由懒加载:这个基本是标配了,不细说
  2. 图片优化:全面换成 WebP 格式,配合 loading="lazy" 懒加载
  3. 关键 CSS 内联:首屏用到的 CSS 直接内联到 HTML 里,减少一次请求
  4. 预加载关键资源:用 <link rel="preload"> 提前加载首屏需要的字体和关键 JS
  5. 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

最热最新
暂无评论
王华_大数据Lv.1
0
影响力
0
文章
0
粉丝