前端多环境自动化部署实战:GitHub Actions + Azure Blob + Cloudflare

前言

在团队协作开发中,经常会遇到这样的场景:

  • 多个功能分支并行开发,每个分支都需要独立的测试环境
  • 测试通过后需要部署到预发布环境验证
  • 最终手动控制发布到生产环境

如果每次都手动打包、上传、配置,不仅效率低下,还容易出错。本文将介绍一套完整的自动化部署方案,实现:

  • feature/* 分支 push 后自动部署到 test.example.com/feature/xxx
  • PR 合并到 master 后自动部署到预发布环境 example.com/preview
  • 生产环境 example.com 需要手动触发部署
  • 部署完成后自动发送飞书/钉钉通知

架构概览

用户访问

Cloudflare CDN

Azure Blob Storage

GitHub Actions 工作流

GitHub 仓库

push

trigger

trigger

feature/xxx 分支

GitHub Actions

PR 合并到 master

手动触发 / release tag

安装依赖

构建打包

上传到 Azure Blob

清理 CDN 缓存

发送通知

test/project-name/

test/project-name/feature/xxx/

prod/project-name/

prod/project-name/preview/

test.example.com

example.com

test.example.com/feature/xxx

example.com/preview

example.com

核心原理

为什么能实现分支隔离部署?

关键在于 动态 base 路径 的设计。

当构建 feature/test1 分支时:

  1. GitHub Actions 将分支名 feature/test1 作为环境变量 BASE_URL 传入
  2. Vite 读取 BASE_URL,设置 base: '/feature/test1/'
  3. 构建产物输出到 dist/feature/test1/ 目录
  4. 上传到 Azure Blob 的 test/project-name/ 路径下
  5. 最终文件位置:test/project-name/feature/test1/index.html

这样,不同分支的文件互不干扰,通过 URL 路径区分。

Vite base 配置的作用

base 配置决定了打包后资源的引用路径:

<!-- base: '/' 时 -->
<script src="/assets/main.js"></script>

<!-- base: '/feature/test1/' 时 -->
<script src="/feature/test1/assets/main.js"></script>

如果不设置正确的 base,部署到子路径后所有资源都会 404。

目录结构

project/
├── .github/
│   └── workflows/
│       ├── ci.yml                        # 代码检查
│       ├── deploy-test.yml               # 测试环境部署
│       ├── deploy-preview.yml            # 预发布环境部署
│       └── deploy-production.yml         # 生产环境部署
├── src/
├── vite.config.ts
├── package.json
└── ...

完整代码实现

1. Vite 配置

// vite.config.ts
import { fileURLToPath, URL } from 'node:url';
import { defineConfig, loadEnv, type ConfigEnv } from 'vite';
import vue from '@vitejs/plugin-vue';

// 从环境变量获取 BASE_URL,默认为根路径
// 这个值由 GitHub Actions 在构建时注入
const BASE_URL = process.env.BASE_URL || '/';

// 判断是否为预发布环境,用于代码中的条件判断
const IS_PREVIEW_ENV = BASE_URL.startsWith('/preview');

export default defineConfig((env: ConfigEnv) => {
  // 加载 .env 文件中的环境变量
  const viteEnv = loadEnv(env.mode, process.cwd());

  return {
    // 设置资源基础路径,这是实现分支隔离部署的关键
    // 例如:feature/test1 分支会设置为 '/feature/test1/'
    base: BASE_URL,

    build: {
      // 输出目录也要包含 BASE_URL,确保目录结构正确
      // 例如:dist/feature/test1/
      outDir: `dist${BASE_URL}`,

      rollupOptions: {
        output: {
          // 入口文件命名,添加 hash 用于缓存控制
          entryFileNames: 'assets/main-[hash].js',
          // 代码分割后的 chunk 命名
          chunkFileNames: 'assets/chunks/[name]-[hash].js',
          // 静态资源分类存放
          assetFileNames: (assetInfo) => {
            const name = assetInfo?.name || '';
            if (name.endsWith('.css')) {
              return 'assets/styles/[name]-[hash].css';
            }
            if (/\.(png|jpe?g|gif|svg|webp)$/.test(name)) {
              return 'assets/images/[name]-[hash].[ext]';
            }
            if (/\.(woff2?|ttf|eot)$/.test(name)) {
              return 'assets/fonts/[name]-[hash].[ext]';
            }
            return 'assets/[name]-[hash].[ext]';
          }
        }
      }
    },

    // 在代码中可以通过 import.meta.env.IS_PREVIEW_ENV 判断环境
    define: {
      'import.meta.env.IS_PREVIEW_ENV': IS_PREVIEW_ENV
    },

    plugins: [vue()],

    resolve: {
      alias: {
        '@': fileURLToPath(new URL('./src', import.meta.url))
      }
    }
  };
});

2. Vue Router 配置

// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router';
import type { RouteRecordRaw } from 'vue-router';

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'Home',
    component: () => import('@/views/Home.vue')
  },
  {
    path: '/about',
    name: 'About',
    component: () => import('@/views/About.vue')
  }
  // ... 其他路由
];

// 从 Vite 注入的环境变量获取 base 路径
// import.meta.env.BASE_URL 会自动读取 vite.config.ts 中的 base 配置
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes
});

export default router;

3. package.json 脚本配置

{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "build-testing": "vite build --mode testing",
    "build-preview": "vite build --mode preview",
    "build-production": "vite build --mode production",
    "clean": "rimraf dist",
    "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx --fix"
  }
}

4. 环境变量文件

# .env.testing
VITE_API_BASE_URL=https://test-api.example.com
VITE_ENV=testing

# .env.preview  
VITE_API_BASE_URL=https://api.example.com
VITE_ENV=preview

# .env.production
VITE_API_BASE_URL=https://api.example.com
VITE_ENV=production

5. GitHub Actions 工作流

5.1 代码检查 (ci.yml)
# .github/workflows/ci.yml
# 代码质量检查工作流
# 触发时机:向 master 分支提交代码或发起 PR 时执行
name: CI

on:
  push:
    branches: [master]
  pull_request:
    branches: [master]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      # 检出代码
      - uses: actions/checkout@v4

      # 设置 Node.js 环境
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20.x
          # 如果使用私有 npm 包,需要配置 registry
          # registry-url: https://npm.pkg.github.com
          # scope: '@your-org'

      # 缓存依赖,加速后续构建
      - name: Cache dependencies
        uses: actions/cache@v4
        with:
          path: ~/.npm
          key: npm-${{ hashFiles('package-lock.json') }}
          restore-keys: npm-

      # 安装依赖
      - name: Install dependencies
        run: npm ci
        # 如果使用私有包,需要设置 token
        # env:
        #   NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

      # 执行 lint 检查
      - name: Run lint
        run: npm run lint
5.2 测试环境部署 (deploy-test.yml)
# .github/workflows/deploy-test.yml
# 测试环境自动部署工作流
# 触发时机:feature/* 分支 push 时自动触发,或手动触发
# 部署结果:https://test.example.com/feature/xxx/
name: 测试环境部署

on:
  # 手动触发入口
  workflow_dispatch:
  # feature 分支 push 自动触发
  push:
    branches:
      - feature/**

env:
  # 动态计算 BASE_URL
  # 如果是 feature 分支 push,则 BASE_URL 为 /feature/xxx
  # 否则为空(手动触发时部署到根路径)
  BASE_URL: ${{ github.event_name == 'push' && startsWith(github.ref_name, 'feature') && format('/{0}', github.ref_name) || '' }}
  
  # Azure Blob 配置
  AZURE_BLOB_CONNECTION_STRING: ${{ secrets.AZURE_BLOB_CONNECTION_STRING }}
  AZURE_BLOB_CONTAINER_NAME: ${{ vars.AZURE_BLOB_CONTAINER_NAME }}
  # 测试环境部署路径
  AZURE_BLOB_DESTINATION_PATH: "test/my-project"
  BUILD_OUT_DIR: "dist"

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    # 使用 GitHub Environment 管理敏感配置
    environment: testing
    outputs:
      # 输出 commit message,用于通知
      firstCommitMessage: ${{ steps.getCommitMessage.outputs.firstCommitMessage }}

    steps:
      - uses: actions/checkout@v4

      # 获取最新的 commit message
      - id: getCommitMessage
        name: Get commit message
        run: |
          firstCommitMessage=$(git log -1 --format=%s)
          echo "firstCommitMessage=$firstCommitMessage" >> "$GITHUB_OUTPUT"

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20.x

      - name: Cache dependencies
        uses: actions/cache@v4
        with:
          path: ~/.npm
          key: npm-${{ hashFiles('package-lock.json') }}
          restore-keys: npm-

      - name: Install dependencies
        run: npm ci

      # 构建时注入 BASE_URL 环境变量
      # Vite 会读取这个变量设置 base 路径
      - name: Build
        run: npm run clean && BASE_URL=$BASE_URL npm run build-testing

      # 上传到 Azure Blob Storage
      - name: Deploy to Azure Blob
        uses: Azure/cli@v2.1.0
        with:
          inlineScript: |
            az storage blob upload-batch \
              --destination '${{ env.AZURE_BLOB_CONTAINER_NAME }}' \
              --source ${{ env.BUILD_OUT_DIR }} \
              --destination-path ${{ env.AZURE_BLOB_DESTINATION_PATH }} \
              --overwrite \
              --connection-string '${{ env.AZURE_BLOB_CONNECTION_STRING }}'

      # 单独设置 HTML 文件的缓存策略
      # HTML 文件不缓存,确保用户总是获取最新版本
      - name: Set HTML cache control
        uses: Azure/cli@v2.1.0
        with:
          inlineScript: |
            az storage blob upload-batch \
              --destination '${{ env.AZURE_BLOB_CONTAINER_NAME }}' \
              --source ${{ env.BUILD_OUT_DIR }} \
              --destination-path ${{ env.AZURE_BLOB_DESTINATION_PATH }} \
              --overwrite \
              --connection-string '${{ env.AZURE_BLOB_CONNECTION_STRING }}' \
              --content-cache-control 'public, max-age=0' \
              --pattern '*.html'

  # 部署完成后发送通知
  notify:
    needs: deploy
    runs-on: ubuntu-latest
    name: Send notification
    # 无论部署成功还是失败都发送通知
    if: ${{ always() }}
    steps:
      - name: Send Feishu notification
        env:
          WEBHOOK_URL: ${{ vars.FEISHU_WEBHOOK }}
          DEPLOY_RESULT: ${{ needs.deploy.result == 'success' && '成功' || '失败' }}
          DEPLOY_ENV: '测试环境'
          # 动态生成访问地址
          DEPLOY_URL: ${{ format('https://test.example.com{0}/', env.BASE_URL) }}
          BRANCH_NAME: ${{ github.ref_name }}
          ACTOR: ${{ github.triggering_actor }}
          ACTION_URL: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
          COMMIT_MESSAGE: ${{ needs.deploy.outputs.firstCommitMessage }}
        run: |
          # 获取中国时区时间
          eventTime=$(TZ='Asia/Shanghai' date +"%Y-%m-%d %H:%M:%S")
          
          # 构造飞书消息体
          reqData="{
            \"msg_type\": \"text\",
            \"content\": {
              \"text\": \"项目部署通知\n状态:$DEPLOY_RESULT\n环境:$DEPLOY_ENV\n分支:$BRANCH_NAME\n提交:$COMMIT_MESSAGE\n操作人:$ACTOR\n时间:$eventTime\n访问地址:$DEPLOY_URL\n构建详情:$ACTION_URL\"
            }
          }"
          
          curl -X POST -H "Content-Type: application/json" -d "$reqData" $WEBHOOK_URL
5.3 预发布环境部署 (deploy-preview.yml)
# .github/workflows/deploy-preview.yml
# 预发布环境部署工作流
# 触发时机:PR 合并到 master 时自动触发,或手动触发
# 部署结果:https://example.com/preview/
name: 预发布环境部署

on:
  workflow_dispatch:
  pull_request:
    types:
      - closed
    branches:
      - master

env:
  AZURE_BLOB_CONNECTION_STRING: ${{ secrets.AZURE_BLOB_CONNECTION_STRING }}
  AZURE_BLOB_CONTAINER_NAME: ${{ vars.AZURE_BLOB_CONTAINER_NAME }}
  # 预发布环境部署到生产容器的 preview 子路径
  AZURE_BLOB_DESTINATION_PATH: "prod/my-project"
  BUILD_OUT_DIR: "dist"

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    # 只在手动触发或 PR 被合并时执行(不是关闭未合并的 PR)
    if: github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.merged == true)
    runs-on: ubuntu-latest
    environment: preview

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20.x

      - name: Cache dependencies
        uses: actions/cache@v4
        with:
          path: ~/.npm
          key: npm-${{ hashFiles('package-lock.json') }}
          restore-keys: npm-

      - name: Install dependencies
        run: npm ci

      # 预发布环境使用 /preview 作为 base 路径
      - name: Build
        run: npm run clean && BASE_URL=/preview npm run build-preview

      - name: Deploy to Azure Blob
        uses: Azure/cli@v2.1.0
        with:
          inlineScript: |
            az storage blob upload-batch \
              --destination '${{ env.AZURE_BLOB_CONTAINER_NAME }}' \
              --source ${{ env.BUILD_OUT_DIR }} \
              --destination-path ${{ env.AZURE_BLOB_DESTINATION_PATH }} \
              --overwrite \
              --connection-string '${{ env.AZURE_BLOB_CONNECTION_STRING }}'

      - name: Set HTML cache control
        uses: Azure/cli@v2.1.0
        with:
          inlineScript: |
            az storage blob upload-batch \
              --destination '${{ env.AZURE_BLOB_CONTAINER_NAME }}' \
              --source ${{ env.BUILD_OUT_DIR }} \
              --destination-path ${{ env.AZURE_BLOB_DESTINATION_PATH }} \
              --overwrite \
              --connection-string '${{ env.AZURE_BLOB_CONNECTION_STRING }}' \
              --content-cache-control 'public, max-age=0' \
              --pattern '*.html'

  notify:
    needs: deploy
    runs-on: ubuntu-latest
    if: ${{ always() }}
    steps:
      - name: Send notification
        env:
          WEBHOOK_URL: ${{ vars.FEISHU_WEBHOOK }}
          DEPLOY_RESULT: ${{ needs.deploy.result == 'success' && '成功' || '失败' }}
          DEPLOY_ENV: '预发布环境'
          DEPLOY_URL: 'https://example.com/preview/'
          PR_BRANCH: ${{ github.event.pull_request.head.ref }}
          PR_URL: ${{ github.event.pull_request.html_url }}
          PR_TITLE: ${{ github.event.pull_request.title }}
          ACTOR: ${{ github.triggering_actor }}
          ACTION_URL: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
        run: |
          eventTime=$(TZ='Asia/Shanghai' date +"%Y-%m-%d %H:%M:%S")
          reqData="{
            \"msg_type\": \"text\",
            \"content\": {
              \"text\": \"项目部署通知\n状态:$DEPLOY_RESULT\n环境:$DEPLOY_ENV\nPR标题:$PR_TITLE\nPR分支:$PR_BRANCH\n操作人:$ACTOR\n时间:$eventTime\n访问地址:$DEPLOY_URL\nPR地址:$PR_URL\n构建详情:$ACTION_URL\n\n提示:预发布环境验证完成后,请手动触发正式发布\"
            }
          }"
          curl -X POST -H "Content-Type: application/json" -d "$reqData" $WEBHOOK_URL
5.4 生产环境部署 (deploy-production.yml)
# .github/workflows/deploy-production.yml
# 生产环境部署工作流
# 触发时机:手动触发 或 推送 release/* tag
# 部署结果:https://example.com/
# 注意:生产环境部署需要谨慎,建议只允许手动触发
name: 生产环境部署

on:
  # 手动触发,这是推荐的生产部署方式
  workflow_dispatch:
  # 也可以通过 release tag 触发
  push:
    tags:
      - release/**

env:
  AZURE_BLOB_CONNECTION_STRING: ${{ secrets.AZURE_BLOB_CONNECTION_STRING }}
  AZURE_BLOB_CONTAINER_NAME: ${{ vars.AZURE_BLOB_CONTAINER_NAME }}
  AZURE_BLOB_DESTINATION_PATH: "prod/my-project"
  BUILD_OUT_DIR: "dist"

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    # 生产环境需要单独的 environment 配置
    # 可以在 GitHub 仓库设置中配置审批流程
    environment: production
    outputs:
      firstCommitMessage: ${{ steps.getCommitMessage.outputs.firstCommitMessage }}

    steps:
      - uses: actions/checkout@v4

      - id: getCommitMessage
        name: Get commit message
        run: |
          firstCommitMessage=$(git log -1 --format=%s)
          echo "firstCommitMessage=$firstCommitMessage" >> "$GITHUB_OUTPUT"

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20.x

      - name: Cache dependencies
        uses: actions/cache@v4
        with:
          path: ~/.npm
          key: npm-${{ hashFiles('package-lock.json') }}
          restore-keys: npm-

      - name: Install dependencies
        run: npm ci

      # 生产环境 base 为根路径
      - name: Build
        run: npm run build-production

      - name: Deploy to Azure Blob
        uses: Azure/cli@v2.1.0
        with:
          inlineScript: |
            az storage blob upload-batch \
              --destination '${{ env.AZURE_BLOB_CONTAINER_NAME }}' \
              --source ${{ env.BUILD_OUT_DIR }} \
              --destination-path ${{ env.AZURE_BLOB_DESTINATION_PATH }} \
              --overwrite \
              --connection-string '${{ env.AZURE_BLOB_CONNECTION_STRING }}'

      - name: Set HTML cache control
        uses: Azure/cli@v2.1.0
        with:
          inlineScript: |
            az storage blob upload-batch \
              --destination '${{ env.AZURE_BLOB_CONTAINER_NAME }}' \
              --source ${{ env.BUILD_OUT_DIR }} \
              --destination-path ${{ env.AZURE_BLOB_DESTINATION_PATH }} \
              --overwrite \
              --connection-string '${{ env.AZURE_BLOB_CONNECTION_STRING }}' \
              --content-cache-control 'public, max-age=0' \
              --pattern '*.html'

      # 生产部署后清理 CDN 缓存
      # 确保用户能立即访问到最新版本
      - name: Clear Cloudflare cache
        id: clearCache
        uses: actions/github-script@v6
        env:
          # Cloudflare Zone ID,在 Cloudflare 控制台获取
          CF_ZONE_ID: ${{ secrets.CF_ZONE_ID }}
          CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
        with:
          result-encoding: string
          script: |
            const { CF_ZONE_ID, CF_API_TOKEN } = process.env;
            const apiUrl = `https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/purge_cache`;
            
            // 清理指定页面的缓存
            // 也可以使用 purge_everything: true 清理所有缓存
            const response = await fetch(apiUrl, {
              method: 'POST',
              headers: {
                'Authorization': `Bearer ${CF_API_TOKEN}`,
                'Content-Type': 'application/json'
              },
              body: JSON.stringify({
                files: [
                  'https://example.com/',
                  'https://example.com/about',
                  'https://example.com/products'
                  // 添加其他需要清理缓存的页面
                ]
              })
            });
            
            const result = await response.json();
            console.log('Cloudflare cache purge result:', result);
            return result?.success ? 'success' : 'failure';

      # 检查缓存清理结果
      - name: Check cache clear result
        run: |
          if [ "${{ steps.clearCache.outputs.result }}" != "success" ]; then
            echo "Warning: Failed to clear Cloudflare cache"
            # 缓存清理失败不阻断部署,只是警告
          fi

  notify:
    needs: deploy
    runs-on: ubuntu-latest
    if: ${{ always() }}
    steps:
      - name: Send notification
        env:
          WEBHOOK_URL: ${{ vars.FEISHU_WEBHOOK }}
          DEPLOY_RESULT: ${{ needs.deploy.result == 'success' && '成功' || '失败' }}
          DEPLOY_ENV: '生产环境'
          DEPLOY_URL: 'https://example.com'
          BRANCH_NAME: ${{ github.ref_name }}
          ACTOR: ${{ github.triggering_actor }}
          ACTION_URL: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
          COMMIT_MESSAGE: ${{ needs.deploy.outputs.firstCommitMessage }}
        run: |
          eventTime=$(TZ='Asia/Shanghai' date +"%Y-%m-%d %H:%M:%S")
          reqData="{
            \"msg_type\": \"text\",
            \"content\": {
              \"text\": \"生产环境部署通知\n状态:$DEPLOY_RESULT\n提交信息:$COMMIT_MESSAGE\n分支/标签:$BRANCH_NAME\n操作人:$ACTOR\n时间:$eventTime\n访问地址:$DEPLOY_URL\n构建详情:$ACTION_URL\"
            }
          }"
          curl -X POST -H "Content-Type: application/json" -d "$reqData" $WEBHOOK_URL

配置说明

GitHub 仓库配置

需要在 GitHub 仓库的 Settings -> Secrets and variables -> Actions 中配置以下内容:

Secrets(敏感信息)
名称说明获取方式
AZURE_BLOB_CONNECTION_STRINGAzure Storage 连接字符串Azure Portal -> Storage Account -> Access keys
CF_ZONE_IDCloudflare Zone IDCloudflare Dashboard -> 域名概览页右侧
CF_API_TOKENCloudflare API TokenCloudflare Dashboard -> My Profile -> API Tokens
Variables(非敏感配置)
名称说明示例值
AZURE_BLOB_CONTAINER_NAMEAzure Blob 容器名$web
FEISHU_WEBHOOK飞书机器人 Webhook 地址https://open.feishu.cn/open-apis/bot/v2/hook/xxx
Environments(环境配置)

建议创建三个 Environment:

  1. testing - 测试环境,无需审批
  2. preview - 预发布环境,可选审批
  3. production - 生产环境,建议配置审批流程

在 Settings -> Environments 中可以为每个环境配置:

  • 审批人员(Required reviewers)
  • 等待时间(Wait timer)
  • 部署分支限制(Deployment branches)

Azure Blob Storage 配置

1. 创建 Storage Account
# 使用 Azure CLI 创建
az storage account create \
  --name mystorageaccount \
  --resource-group myResourceGroup \
  --location eastasia \
  --sku Standard_LRS
2. 启用静态网站托管
az storage blob service-properties update \
  --account-name mystorageaccount \
  --static-website \
  --index-document index.html \
  --404-document index.html

启用后会自动创建 $web 容器,静态网站端点格式为:
https://mystorageaccount.z6.web.core.windows.net

3. 配置 CORS(如需要)
az storage cors add \
  --account-name mystorageaccount \
  --services b \
  --methods GET HEAD \
  --origins '*' \
  --allowed-headers '*'

Cloudflare 配置

1. 添加域名

在 Cloudflare Dashboard 添加域名,并将域名的 NS 记录指向 Cloudflare。

2. 配置 DNS 记录
类型名称内容代理状态
CNAME@mystorageaccount.z6.web.core.windows.net已代理
CNAMEtestmystorageaccount.z6.web.core.windows.net已代理
3. 配置 Page Rules(可选)

为了优化缓存策略,可以添加 Page Rules:

URL: example.com/*.html
设置: Cache Level = Bypass

URL: example.com/assets/*
设置: Cache Level = Cache Everything, Edge Cache TTL = 1 month
4. 创建 API Token

在 My Profile -> API Tokens -> Create Token:

  • 权限:Zone -> Cache Purge -> Purge
  • 区域资源:选择对应的域名

工作流程图

完整的开发部署流程

开发者创建 feature/xxx 分支

本地开发

Push 到远程

GitHub Actions

自动部署到测试环境

test.example.com/feature/xxx

测试验证

测试通过?

创建 PR 到 master

Code Review

Review 通过?

合并 PR

GitHub Actions

自动部署到预发布环境

example.com/preview

预发布验证

验证通过?

修复问题,重新提 PR

手动触发生产部署

GitHub Actions

部署到生产环境

example.com

清理 CDN 缓存

发送部署通知

分支与环境对应关系

环境

分支

push 自动触发

PR 合并自动触发

手动触发

手动触发

feature/xxx

master

release/v1.0.0

测试环境
test.example.com/feature/xxx

预发布环境
example.com/preview

生产环境
example.com

常见问题

Q1: 为什么资源加载 404?

最常见的原因是 base 路径配置不正确。检查以下几点:

  1. Vite 配置中的 base 是否正确读取了 BASE_URL 环境变量
  2. 构建命令是否正确传入了 BASE_URL
  3. Vue Router 的 createWebHistory 是否使用了 import.meta.env.BASE_URL

Q2: 如何支持 SPA 路由?

Azure Blob 静态网站需要配置 404 页面指向 index.html

az storage blob service-properties update \
  --account-name mystorageaccount \
  --static-website \
  --index-document index.html \
  --404-document index.html  # 关键配置

Q3: 如何清理旧的分支部署?

可以添加一个定时清理的 workflow:

name: Cleanup old deployments

on:
  schedule:
    # 每周日凌晨 2 点执行
    - cron: '0 2 * * 0'
  workflow_dispatch:

jobs:
  cleanup:
    runs-on: ubuntu-latest
    steps:
      - name: Delete old feature deployments
        uses: Azure/cli@v2.1.0
        env:
          AZURE_BLOB_CONNECTION_STRING: ${{ secrets.AZURE_BLOB_CONNECTION_STRING }}
        with:
          inlineScript: |
            # 列出所有 feature 目录
            # 删除超过 30 天未修改的目录
            # 具体实现根据需求调整
            echo "Cleanup completed"

Q4: 如何回滚到上一个版本?

方案一:重新触发上一次成功的 workflow

方案二:使用 Git tag 管理版本,回滚时部署指定 tag

# 创建版本 tag
git tag release/v1.0.0
git push origin release/v1.0.0

# 回滚时,手动触发 workflow 并选择对应的 tag

Q5: 如何在代码中判断当前环境?

// 判断是否为预发布环境
if (import.meta.env.IS_PREVIEW_ENV) {
  console.log('当前是预发布环境');
}

// 判断构建模式
if (import.meta.env.MODE === 'production') {
  console.log('生产模式');
}

// 使用自定义环境变量
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL;

扩展:钉钉通知

如果团队使用钉钉而不是飞书,可以替换通知步骤:

- name: Send DingTalk notification
  env:
    DINGTALK_WEBHOOK: ${{ vars.DINGTALK_WEBHOOK }}
    DEPLOY_RESULT: ${{ needs.deploy.result == 'success' && '成功' || '失败' }}
  run: |
    eventTime=$(TZ='Asia/Shanghai' date +"%Y-%m-%d %H:%M:%S")
    reqData="{
      \"msgtype\": \"markdown\",
      \"markdown\": {
        \"title\": \"部署通知\",
        \"text\": \"### 项目部署$DEPLOY_RESULT\n- 环境:测试环境\n- 时间:$eventTime\n- 操作人:${{ github.triggering_actor }}\"
      }
    }"
    curl -X POST -H "Content-Type: application/json" -d "$reqData" $DINGTALK_WEBHOOK

扩展:使用 Cloudflare Pages 替代 Azure Blob

如果不想使用 Azure,可以直接使用 Cloudflare Pages:

- name: Deploy to Cloudflare Pages
  uses: cloudflare/wrangler-action@v3
  with:
    apiToken: ${{ secrets.CF_API_TOKEN }}
    accountId: ${{ secrets.CF_ACCOUNT_ID }}
    command: pages deploy dist --project-name=my-project --branch=${{ github.ref_name }}

Cloudflare Pages 的优势:

  • 自动处理分支预览部署
  • 内置 CDN,无需额外配置
  • 免费额度较高

总结

本文介绍的多环境自动化部署方案,核心要点:

  1. 动态 base 路径:通过环境变量控制 Vite 的 base 配置,实现分支隔离部署
  2. GitHub Actions 工作流:根据不同触发条件执行不同的部署流程
  3. Azure Blob + Cloudflare:静态资源存储 + CDN 加速的经典组合
  4. 缓存策略:HTML 不缓存,静态资源长期缓存
  5. 通知机制:部署完成后自动通知团队

这套方案已在多个生产项目中验证,可以直接复用到新项目中。根据实际需求,可以灵活调整触发条件、部署目标和通知方式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

发现你走远了

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值