前端部署实战:从人工发布到全自动化流程

时间:2024-12-17 18:11:59

"又发错环境了!"周四下午,测试同学小李急匆匆地找到我。原来是开发人员手动部署时,不小心把测试代码发布到了生产环境。这已经是本月第二次类似的事故了。

回想起每次发布时的场景:手动打包、手动上传、手动替换文件...每一步都战战兢兢,生怕出错。作为技术负责人,我决定要彻底改变这种状况,建立一套可靠的自动化部署流程。

现状问题

首先梳理了当前部署流程中的痛点:

  • 人工操作步骤多,容易出错
  • 环境配置混乱,经常配错
  • 发布耗时长,经常加班
  • 回滚困难,出问题很被动
  • 缺乏发布记录,无法追溯

就像是一场没有彩排的演出,每次都提心吊胆。我们需要把这个过程变得像工厂的流水线一样可靠。

自动化方案

1. 构建脚本

首先是统一的构建脚本:

// scripts/build.js
const webpack = require('webpack')
const { merge } = require('webpack-merge')
const baseConfig = require('./webpack.base')
const envConfigs = {
  dev: require('./webpack.dev'),
  test: require('./webpack.test'),
  prod: require('./webpack.prod')
}

async function build(env) {
  // 合并配置
  const config = merge(baseConfig, envConfigs[env], {
    mode: env === 'dev' ? 'development' : 'production'
  })

  // 执行构建
  const compiler = webpack(config)
  const stats = await new Promise((resolve, reject) => {
    compiler.run((err, stats) => {
      if (err) reject(err)
      else resolve(stats)
    })
  })

  // 输出构建信息
  console.log(
    stats.toString({
      colors: true,
      modules: false,
      children: false
    })
  )

  // 验证构建产物
  await validateBuild()
}

// 构建产物验证
async function validateBuild() {
  const files = await fs.readdir('./dist')

  // 检查必要文件
  const required = ['index.html', 'main.js', 'vendor.js']
  const missing = required.filter(file => !files.includes(file))
  if (missing.length) {
    throw new Error(`构建产物缺失: ${missing.join(', ')}`)
  }

  // 检查文件大小
  const stats = await Promise.all(
    files.map(async file => ({
      file,
      size: (await fs.stat(`./dist/${file}`)).size
    }))
  )

  // 超出限制告警
  const limit = 1024 * 1024 // 1MB
  stats.forEach(({ file, size }) => {
    if (size > limit) {
      console.warn(`文件过大: ${file} (${(size / 1024 / 1024).toFixed(2)}MB)`)
    }
  })
}

2. 环境配置

然后是规范的环境配置管理:

// config/index.js
const configs = {
  dev: {
    api: 'http://dev-api.example.com',
    cdn: 'http://dev-cdn.example.com',
    features: {
      logger: true,
      mock: true
    }
  },
  test: {
    api: 'http://test-api.example.com',
    cdn: 'http://test-cdn.example.com',
    features: {
      logger: true,
      mock: false
    }
  },
  prod: {
    api: 'https://api.example.com',
    cdn: 'https://cdn.example.com',
    features: {
      logger: false,
      mock: false
    }
  }
}

// 环境变量注入
function injectEnv(env) {
  const config = configs[env]

  // 写入环境变量文件
  const content = Object.entries(config)
    .map(([key, value]) => `VITE_${key.toUpperCase()}=${JSON.stringify(value)}`)
    .join('\n')

  fs.writeFileSync('.env.local', content)
}

// 环境变量验证
function validateEnv() {
  const required = ['VITE_API', 'VITE_CDN']
  const missing = required.filter(key => !process.env[key])
  if (missing.length) {
    throw new Error(`环境变量缺失: ${missing.join(', ')}`)
  }
}

3. 自动化部署

最关键的是自动化部署流程:

# .github/workflows/deploy.yml
name: Deploy
on:
  push:
    branches: [main, test]
  pull_request:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - name: Setup Node.js
        uses: actions/setup-node@v2
        with:
          node-version: '16'

      - name: Install Dependencies
        run: npm ci

      - name: Type Check
        run: npm run type-check

      - name: Test
        run: npm test

      - name: Build
        run: |
          if [[ $GITHUB_REF == refs/heads/main ]]; then
            npm run build:prod
          elif [[ $GITHUB_REF == refs/heads/test ]]; then
            npm run build:test
          fi

      - name: Deploy
        uses: azure/webapps-deploy@v2
        with:
          app-name: ${{ secrets.AZURE_WEBAPP_NAME }}
          publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }}
          package: ./dist

      - name: Notify
        if: always()
        uses: actions/github-script@v6
        with:
          script: |
            const { repo, owner } = context.repo
            const run_id = context.runId

            const message = `
              部署 ${context.sha.slice(0, 7)} 完成
              分支: ${context.ref}
              状态: ${context.job.status}
              详情: https://github.com/${owner}/${repo}/actions/runs/${run_id}
            `

            await github.rest.issues.createComment({
              owner,
              repo,
              issue_number: context.issue.number,
              body: message
            })

4. 灰度发布

为了更安全的发布,我们实现了灰度发布机制:

// server/canary.js
class CanaryRelease {
  constructor(options = {}) {
    this.ratio = options.ratio || 0.1 // 灰度比例
    this.rules = options.rules || [] // 灰度规则
    this.versions = new Map() // 版本映射
  }

  // 添加新版本
  addVersion(version, url) {
    this.versions.set(version, url)
  }

  // 获取用户版本
  getVersion(req) {
    // 优先匹配规则
    for (const rule of this.rules) {
      if (rule.test(req)) {
        return rule.version
      }
    }

    // 按比例灰度
    const random = Math.random()
    if (random < this.ratio) {
      return Array.from(this.versions.keys()).pop() // 最新版本
    }

    return Array.from(this.versions.keys())[0] // 稳定版本
  }

  // 中间件
  middleware() {
    return (req, res, next) => {
      const version = this.getVersion(req)
      const url = this.versions.get(version)

      if (url) {
        res.redirect(url)
      } else {
        next()
      }
    }
  }
}

// 使用示例
const canary = new CanaryRelease({
  ratio: 0.2,
  rules: [
    {
      test: req => req.query.version === 'new',
      version: 'v2'
    },
    {
      test: req => req.cookies.beta === 'true',
      version: 'v2'
    }
  ]
})

canary.addVersion('v1', 'https://stable.example.com')
canary.addVersion('v2', 'https://canary.example.com')

app.use(canary.middleware())

实践效果

通过这套自动化部署流程:

  • 部署时间从 1 小时减少到 5 分钟
  • 人工操作失误完全消除
  • 发布过程可追溯、可回滚
  • 新版本平滑过渡,用户无感知

最让我印象深刻的是测试同学的反馈:"现在再也不用担心环境被搞混了!"

经验总结

前端部署就像是一条生产线,需要严格的流程和质量控制。关键是要:

环境隔离 - 不同环境配置要严格分开流程自动化 - 减少人工操作降低风险质量把控 - 层层把关确保代码质量监控反馈 - 及时发现和解决问题

写在最后

前端部署不仅仅是把代码放到服务器上,而是一个需要精心设计的完整流程。就像那句话说的:"磨刀不误砍柴工",投入时间建设自动化流程,最终会让团队受益良多。

有什么问题欢迎在评论区讨论,让我们一起探讨前端部署的最佳实践!

如果觉得有帮助,别忘了点赞关注,我会继续分享更多实战经验~