全栈实战:分支管理到CI/CD全流程

现代全栈项目实战:分支管理、Vercel 部署、Docker 容器化与 CI/CD 完整指南

基于真实 Monorepo 项目的最佳实践总结


📋 目录

  1. [Git 分支管理策略](#Git 分支管理策略)
  2. [Vercel 前端部署详解](#Vercel 前端部署详解)
  3. [Docker 容器化部署](#Docker 容器化部署)
  4. [CI/CD 流水线配置](#CI/CD 流水线配置)
  5. 常见问题与解决方案

1. Git 分支管理策略

1.1 主流分支管理方案对比

Git Flow(经典方案)
复制代码
main (生产环境)
├── develop (开发主干)
│   ├── feature/user-auth (功能分支)
│   ├── feature/blog-editor (功能分支)
│   └── hotfix/security-patch (紧急修复)
└── release/v1.0.0 (发布分支)

特点:

  • ✅ 结构清晰,适合大型团队
  • ❌ 流程复杂,学习成本高
  • 🎯 适用场景:传统企业级应用、多版本并行开发
GitHub Flow(简化方案)
复制代码
main (唯一长期分支)
├── feature/add-search (功能分支)
├── fix/cors-issue (修复分支)
└── docs/update-readme (文档分支)

特点:

  • ✅ 简单直观,快速迭代
  • ❌ 缺少明确的发布流程
  • 🎯 适用场景:SaaS 应用、持续部署项目
Trunk Based Development(主干开发)
复制代码
main (主干)
├── short-lived-feature-1 (短期分支 < 1天)
├── short-lived-feature-2 (短期分支 < 1天)
└── feature-flags (通过特性开关控制)

特点:

  • ✅ 减少合并冲突,快速集成
  • ❌ 需要完善的测试和特性开关机制
  • 🎯 适用场景:高成熟度团队、超大型项目

1.2 推荐方案:混合分支策略(本项目采用)

基于实际项目经验,我们采用改进的 GitHub Flow

分支命名规范
分支类型 命名格式 示例 说明
主分支 main main 生产环境代码
开发分支 develop develop 集成测试环境
功能分支 feature/* feature/user-auth 新功能开发
修复分支 fix/* fix/cors-issue Bug 修复
文档分支 docs/* docs/update-readme 文档更新
热修复 hotfix/* hotfix/security-patch 线上紧急修复
发布分支 release/* release/v1.0.0 版本发布准备

1.3 分支操作流程详解

场景一:新功能开发流程
bash 复制代码
# 1. 从 main 创建功能分支
git checkout main
git pull origin main
git checkout -b feature/new-feature

# 2. 开发并提交代码
git add .
git commit -m "feat: 添加新功能模块"

# 3. 推送到远程
git push -u origin feature/new-feature

# 4. 创建 Pull Request (GitHub/GitLab)
# 在 Web 界面创建 PR,选择 base: main, compare: feature/new-feature

# 5. Code Review 通过后合并
# 使用 Squash Merge 保持提交历史整洁

# 6. 删除远程分支
git push origin --delete feature/new-feature

# 7. 删除本地分支
git branch -d feature/new-feature
场景二:线上 Bug 修复流程(Hotfix)
bash 复制代码
# 1. 从 main 创建热修复分支
git checkout main
git pull origin main
git checkout -b hotfix/critical-bug

# 2. 修复 Bug
git add .
git commit -m "fix: 修复线上关键 Bug #123"

# 3. 推送到远程并创建 PR
git push -u origin hotfix/critical-bug

# 4. 紧急情况下可以直接合并到 main
git checkout main
git merge hotfix/critical-bug
git push origin main

# 5. 同时合并到 develop 保持一致性
git checkout develop
git merge hotfix/critical-bug
git push origin develop

# 6. 打上版本标签
git tag -a v1.0.1 -m "修复关键 Bug"
git push origin v1.0.1
场景三:处理分支冲突
bash 复制代码
# 1. 切换到目标分支并更新
git checkout feature/my-feature
git fetch origin
git rebase origin/main

# 2. 如果有冲突,手动解决后继续
# 编辑冲突文件...
git add .
git rebase --continue

# 3. 如果冲突太复杂,可以中止 rebase
git rebase --abort

# 4. 强制推送(谨慎使用)
git push --force-with-lease

1.4 分支保护规则(GitHub Settings)

推荐配置
yaml 复制代码
Branch Protection Rules for 'main':
  ✓ Require pull request reviews before merging
    - Required approving reviews: 1
    - Dismiss stale pull request approvals: ✓
    - Require review from Code Owners: ✓
  
  ✓ Require status checks to pass before merging
    - Required checks:
      - "Test Suite"
      - "Build and Deploy"
    - Require branches to be up to date: ✓
  
  ✓ Include administrators: ✓
  ✓ Allow force pushes: ✗
  ✓ Allow deletions: ✗
  ✓ Require linear history: ✓ (禁止 Merge Commit)

1.5 线上问题应急处理流程

Critical
Normal


发现线上问题
严重程度?
创建 hotfix 分支
创建 fix 分支
快速修复
本地测试
测试通过?
直接合并到 main
立即部署
验证修复效果
同步到 develop
打版本标签
正常开发流程
Code Review
合并到 develop
等待下次发布

关键原则:

  1. Hotfix 优先:线上 Critical Bug 必须走 hotfix 流程
  2. 双向合并:hotfix 既要合并到 main,也要合并到 develop
  3. 版本标签:每次 hotfix 都要打标签,方便回滚
  4. 事后复盘:记录问题原因,避免重复发生

2. Vercel 前端部署详解

2.1 Vercel 部署步骤

步骤一:项目准备
bash 复制代码
# 1. 安装 Vercel CLI
npm install -g vercel@latest

# 2. 登录 Vercel
vercel login

# 3. 初始化项目(在项目根目录执行)
cd apps/frontend
vercel init
步骤二:配置 vercel.json
json 复制代码
{
  "framework": "vite",
  "buildCommand": "pnpm build",
  "outputDirectory": "dist",
  "installCommand": "pnpm install",
  "rewrites": [
    {
      "source": "/api/(.*)",
      "destination": "http://localhost:3000/api/$1"
    },
    {
      "source": "/(.*)",
      "destination": "/index.html"
    }
  ],
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        {
          "key": "X-Content-Type-Options",
          "value": "nosniff"
        },
        {
          "key": "X-Frame-Options",
          "value": "DENY"
        },
        {
          "key": "X-XSS-Protection",
          "value": "1; mode=block"
        }
      ]
    }
  ]
}

配置解析:

字段 作用 说明
framework 指定框架 Vercel 自动优化构建流程
buildCommand 构建命令 执行 pnpm build 生成静态文件
outputDirectory 输出目录 Vite 默认输出到 dist
installCommand 安装依赖 使用 pnpm 安装
rewrites 路由重写 处理 API 代理和 SPA 路由
headers 安全头 增强安全性

2.2 单页面应用(SPA)路由配置

问题背景

React/Vue 等 SPA 应用在刷新页面时会出现 404 错误,因为服务器找不到对应的物理文件。

解决方案
json 复制代码
{
  "rewrites": [
    {
      "source": "/(.*)",
      "destination": "/index.html"
    }
  ]
}

工作原理:

复制代码
用户访问 /blog/article-1
    ↓
Vercel 查找 /blog/article-1.html(不存在)
    ↓
触发 rewrite 规则
    ↓
返回 /index.html
    ↓
React Router 接管路由,渲染对应组件

注意事项:

  • ⚠️ 此规则必须放在最后,避免覆盖其他 rewrite 规则
  • ⚠️ 静态资源(CSS/JS/图片)不受影响,因为它们有具体文件路径
  • ⚠️ SEO 友好性需要考虑 SSR 或预渲染方案

2.3 跨域配置(CORS)

前端 Vercel 配置

Vercel 本身不需要特殊 CORS 配置,关键在于后端配置

后端 Nest.js CORS 配置
typescript 复制代码
// apps/backend/src/main.ts
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  // CORS 配置
  app.enableCors({
    origin: process.env.CORS_ORIGIN?.split(',') || ['http://localhost:5173'],
    credentials: true,
    methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
    allowedHeaders: ['Content-Type', 'Authorization'],
    maxAge: 3600,
  });
  
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}
bootstrap();
环境变量配置
bash 复制代码
# .env.production
CORS_ORIGIN=https://your-app.vercel.app,https://www.your-domain.com

关键点:

  • ✅ 生产环境必须配置具体的域名,不要使用 *
  • ✅ 多个域名用逗号分隔
  • ✅ 包含 credentials: true 时,origin 不能为 *
  • ✅ 定期审查允许的域名列表,移除废弃域名

2.4 环境变量管理

方式一:Vercel Dashboard 配置
  1. 进入项目 → Settings → Environment Variables
  2. 添加变量(支持 Development/Preview/Production 环境)
  3. 重新部署生效
方式二:CLI 配置
bash 复制代码
# 添加环境变量
vercel env add VITE_API_URL production
# 输入值:https://your-api-domain.com/api

# 拉取环境变量到本地
vercel env pull .env.production.local

# 列出所有环境变量
vercel env ls
方式三:CI/CD 中自动配置
yaml 复制代码
# .github/workflows/ci-cd.yml
- name: Pull Vercel Environment Information
  run: vercel pull --yes --environment=production --token="${{ secrets.VERCEL_TOKEN }}"
  working-directory: ./apps/frontend
  env:
    VITE_API_URL: ${{ secrets.VITE_API_URL }}

⚠️ 常见问题:VITE_ 前缀变量

Vite 只暴露以 VITE_ 开头的环境变量给客户端代码。如果在 Vercel 中配置了引用 Secret 的变量,可能导致构建失败。

解决方案:

bash 复制代码
# 在 CI/CD 中移除 problematic 变量
vercel env rm VITE_API_URL production --token="${{ secrets.VERCEL_TOKEN }}" --yes

然后在代码中使用 fallback:

typescript 复制代码
// apps/frontend/src/config/api.ts
export const API_URL = import.meta.env.VITE_API_URL || 'https://default-api-domain.com/api';

2.5 自定义域名配置

bash 复制代码
# 添加自定义域名
vercel domains add your-domain.com

# 配置 DNS 记录
# Type: CNAME
# Name: www
# Value: cname.vercel-dns.com

# Type: A
# Name: @
# Value: 76.76.21.21

3. Docker 容器化部署

3.1 Docker 架构设计

本项目采用 Monorepo + 微服务 架构:
Docker Network: ai-blog-network
Database Layer
Backend Container
Frontend Container
Load Balancer / Nginx
React App :80
Nest.js API :3000
MongoDB :27017
Redis :6379
用户浏览器


3.2 MongoDB 容器配置

docker-compose.yml 配置
yaml 复制代码
mongodb:
  image: mongo:7.0
  container_name: ai-blog-mongodb
  restart: unless-stopped
  ports:
    - "27017:27017"
  environment:
    MONGO_INITDB_ROOT_USERNAME: admin
    MONGO_INITDB_ROOT_PASSWORD: password123
    MONGO_INITDB_DATABASE: ai_blog
  volumes:
    - mongodb_data:/data/db
    - ./docker/mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro
  networks:
    - ai-blog-network
  healthcheck:
    test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017/test --quiet
    interval: 10s
    timeout: 5s
    retries: 5

关键配置说明:

配置项 作用 生产建议
restart: unless-stopped 容器异常退出时自动重启 ✅ 必须配置
volumes 数据持久化 ✅ 必须挂载到宿主机
healthcheck 健康检查 ✅ 确保服务可用
mongo-init.js 初始化脚本 可选,用于创建索引等
初始化脚本示例
javascript 复制代码
// docker/mongo-init.js
db = db.getSiblingDB('ai_blog');

// 创建集合
db.createCollection('users');
db.createCollection('articles');
db.createCollection('comments');

// 创建索引
db.users.createIndex({ email: 1 }, { unique: true });
db.articles.createIndex({ slug: 1 }, { unique: true });
db.articles.createIndex({ createdAt: -1 });

// 创建初始管理员用户
db.users.insertOne({
  username: 'admin',
  email: 'admin@example.com',
  password: '$2b$10$...', // bcrypt hash
  role: 'admin',
  createdAt: new Date(),
});
数据备份与恢复
bash 复制代码
# 备份数据库
docker exec ai-blog-mongodb mongodump \
  --username admin \
  --password password123 \
  --authenticationDatabase admin \
  --db ai_blog \
  --out /tmp/backup

# 复制备份文件到宿主机
docker cp ai-blog-mongodb:/tmp/backup ./backup-$(date +%Y%m%d)

# 恢复数据库
docker cp ./backup-20240101 ai-blog-mongodb:/tmp/backup
docker exec ai-blog-mongodb mongorestore \
  --username admin \
  --password password123 \
  --authenticationDatabase admin \
  --db ai_blog \
  /tmp/backup/ai_blog

3.3 Redis 容器配置

docker-compose.yml 配置
yaml 复制代码
redis:
  image: redis:7-alpine
  container_name: ai-blog-redis
  restart: unless-stopped
  ports:
    - "6379:6379"
  command: redis-server --requirepass redis_password123
  volumes:
    - redis_data:/data
  networks:
    - ai-blog-network
  healthcheck:
    test: ["CMD", "redis-cli", "-a", "redis_password123", "ping"]
    interval: 10s
    timeout: 5s
    retries: 5

高级配置(持久化):

yaml 复制代码
command: >
  redis-server
  --requirepass redis_password123
  --appendonly yes
  --appendfsync everysec
  --maxmemory 256mb
  --maxmemory-policy allkeys-lru

配置说明:

参数 作用 推荐值
--appendonly yes 开启 AOF 持久化 生产必开
--appendfsync everysec AOF 同步策略 everysec(平衡性能与安全)
--maxmemory 最大内存限制 根据服务器内存设置
--maxmemory-policy 内存淘汰策略 allkeys-lru(缓存场景)
常用 Redis 操作
bash 复制代码
# 进入 Redis CLI
docker exec -it ai-blog-redis redis-cli -a redis_password123

# 查看内存使用情况
INFO memory

# 查看键数量
DBSIZE

# 清空数据库(谨慎使用)
FLUSHDB

# 监控实时命令
MONITOR

3.4 后端服务构建与部署

多阶段 Dockerfile
dockerfile 复制代码
# docker/Dockerfile.backend
FROM node:22-alpine AS builder

WORKDIR /app

# 安装 pnpm
RUN npm install -g pnpm@10.10.0

# 复制 monorepo 文件
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml turbo.json ./
COPY packages/types/package.json ./packages/types/
COPY apps/backend/package.json ./apps/backend/

# 安装依赖
RUN pnpm install --frozen-lockfile || pnpm install

# 复制源代码
COPY packages/types ./packages/types
COPY apps/backend ./apps/backend

# 构建 types 包
RUN cd packages/types && pnpm build

# 构建 backend
RUN cd apps/backend && pnpm build

# 生产阶段
FROM node:22-alpine

WORKDIR /app

# 安装 pnpm
RUN npm install -g pnpm@10.10.0

# 复制 package 文件
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./
COPY apps/backend/package.json ./apps/backend/

# 仅安装生产依赖
RUN pnpm install --prod --frozen-lockfile || pnpm install --prod

# 复制构建产物
COPY --from=builder /app/apps/backend/dist ./apps/backend/dist
COPY --from=builder /app/packages/types/dist ./packages/types/dist

EXPOSE 3000

WORKDIR /app/apps/backend

CMD ["node", "dist/main.js"]

多阶段构建优势:

  • ✅ 最终镜像不包含 devDependencies
  • ✅ 不包含源代码和 TypeScript 编译器
  • ✅ 镜像体积减小 60-70%
构建与运行
bash 复制代码
# 构建镜像
docker build -f docker/Dockerfile.backend -t ai-blog-backend:latest .

# 运行容器
docker run -d \
  --name ai-blog-backend \
  --network ai-blog-network \
  -p 3000:3000 \
  -e NODE_ENV=production \
  -e MONGODB_URI=mongodb://admin:password@mongodb:27017/ai_blog?authSource=admin \
  -e REDIS_URL=redis://:password@redis:6379 \
  -e JWT_SECRET=your-secret-key \
  -e OPENAI_API_KEY=sk-xxx \
  ai-blog-backend:latest

# 查看日志
docker logs -f ai-blog-backend

# 进入容器调试
docker exec -it ai-blog-backend sh

3.5 前端服务构建与部署

Nginx + React Dockerfile
dockerfile 复制代码
# docker/Dockerfile.frontend
FROM node:18-alpine AS builder

WORKDIR /app

# 安装 pnpm
RUN npm install -g pnpm@8.9.0

# 复制 monorepo 文件
COPY package.json pnpm-workspace.yaml turbo.json ./
COPY packages/types/package.json ./packages/types/
COPY apps/frontend/package.json ./apps/frontend/

# 安装依赖
RUN pnpm install --frozen-lockfile

# 复制源代码
COPY packages/types ./packages/types
COPY apps/frontend ./apps/frontend

# 构建 types 包
RUN cd packages/types && pnpm build

# 构建前端
RUN cd apps/frontend && pnpm build

# 生产阶段:Nginx
FROM nginx:alpine

# 复制构建产物
COPY --from=builder /app/apps/frontend/dist /usr/share/nginx/html

# 复制 Nginx 配置
COPY apps/frontend/nginx.conf /etc/nginx/conf.d/default.conf

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]
Nginx 配置(SPA 路由支持)
nginx 复制代码
# apps/frontend/nginx.conf
server {
    listen 80;
    server_name localhost;
    root /usr/share/nginx/html;
    index index.html;

    # Gzip 压缩
    gzip on;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
    gzip_min_length 1000;

    # 静态资源缓存
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # API 代理到后端
    location /api/ {
        proxy_pass http://backend:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    # SPA 路由回退
    location / {
        try_files $uri $uri/ /index.html;
    }

    # 健康检查
    location /health {
        access_log off;
        return 200 "healthy\n";
        add_header Content-Type text/plain;
    }
}

3.6 Docker Compose 完整配置

开发环境
yaml 复制代码
# docker/docker-compose.yml
version: '3.8'

services:
  mongodb:
    image: mongo:7.0
    container_name: ai-blog-mongodb
    restart: unless-stopped
    ports:
      - "27017:27017"
    environment:
      MONGO_INITDB_ROOT_USERNAME: admin
      MONGO_INITDB_ROOT_PASSWORD: password123
      MONGO_INITDB_DATABASE: ai_blog
    volumes:
      - mongodb_data:/data/db
    networks:
      - ai-blog-network
    healthcheck:
      test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017/test --quiet
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    container_name: ai-blog-redis
    restart: unless-stopped
    ports:
      - "6379:6379"
    command: redis-server --requirepass redis_password123
    volumes:
      - redis_data:/data
    networks:
      - ai-blog-network
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "redis_password123", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

  backend:
    build:
      context: ..
      dockerfile: docker/Dockerfile.backend
    container_name: ai-blog-backend
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      NODE_ENV: development
      PORT: 3000
      MONGODB_URI: mongodb://admin:password123@mongodb:27017/ai_blog?authSource=admin
      REDIS_URL: redis://:redis_password123@redis:6379
      OPENAI_API_KEY: ${OPENAI_API_KEY}
      JWT_SECRET: ${JWT_SECRET:-your-secret-key-change-in-production}
      CORS_ORIGIN: http://localhost:5173
    volumes:
      - ../apps/backend:/app/apps/backend
      - ../packages/types:/app/packages/types
      - /app/node_modules
    depends_on:
      mongodb:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - ai-blog-network
    command: pnpm start:dev

  frontend:
    build:
      context: ..
      dockerfile: docker/Dockerfile.frontend
    container_name: ai-blog-frontend
    restart: unless-stopped
    ports:
      - "5173:5173"
    environment:
      VITE_API_URL: http://localhost:3000/api
    volumes:
      - ../apps/frontend:/app/apps/frontend
      - ../packages/types:/app/packages/types
      - /app/node_modules
    depends_on:
      - backend
    networks:
      - ai-blog-network
    command: pnpm dev -- --host 0.0.0.0

networks:
  ai-blog-network:
    driver: bridge

volumes:
  mongodb_data:
  redis_data:
生产环境
yaml 复制代码
# docker-compose.prod.yml
version: '3.8'

services:
  mongodb:
    image: mongo:7
    container_name: ai-blog-mongodb
    restart: unless-stopped
    ports:
      - "27017:27017"
    environment:
      MONGO_INITDB_ROOT_USERNAME: admin
      MONGO_INITDB_ROOT_PASSWORD: ${MONGO_PASSWORD}
    volumes:
      - mongodb_data:/data/db
    networks:
      - ai-blog-network
    healthcheck:
      test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017/test --quiet
      interval: 10s
      timeout: 10s
      retries: 5

  redis:
    image: redis:7-alpine
    container_name: ai-blog-redis
    restart: unless-stopped
    ports:
      - "6379:6379"
    command: redis-server --requirepass ${REDIS_PASSWORD}
    volumes:
      - redis_data:/data
    networks:
      - ai-blog-network
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile.prod
    container_name: ai-blog-backend
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      NODE_ENV: production
      PORT: 3000
      MONGODB_URI: mongodb://admin:${MONGO_PASSWORD}@mongodb:27017/aiblog?authSource=admin
      REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379
      JWT_SECRET: ${JWT_SECRET}
      JWT_EXPIRES_IN: 7d
      LLM_PROVIDER: openai
      LLM_MODEL: gpt-4
      OPENAI_API_KEY: ${OPENAI_API_KEY}
      RATE_LIMIT_WINDOW_MS: 900000
      RATE_LIMIT_MAX_REQUESTS: 100
    depends_on:
      mongodb:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - ai-blog-network
    volumes:
      - backend_logs:/app/logs
      - backend_uploads:/app/uploads

  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile.prod
    container_name: ai-blog-frontend
    restart: unless-stopped
    ports:
      - "80:80"
    depends_on:
      - backend
    networks:
      - ai-blog-network

volumes:
  mongodb_data:
  redis_data:
  backend_logs:
  backend_uploads:

networks:
  ai-blog-network:
    driver: bridge

生产环境 .env 文件:

bash 复制代码
# .env.production
MONGO_PASSWORD=strong-password-here
REDIS_PASSWORD=another-strong-password
JWT_SECRET=ultra-secure-random-string-at-least-32-chars
OPENAI_API_KEY=sk-your-openai-key

启动生产环境:

bash 复制代码
docker-compose -f docker-compose.prod.yml --env-file .env.production up -d

3.7 Docker 网络与存储管理

网络模式对比

Overlay Network(Swarm)
Node 1
Node 2
Container 4
Container 5
Host Network
Container 3
HostOS
Bridge Network(默认)
Container 1
Bridge
Container 2
Internet

网络模式 适用场景 优点 缺点
bridge 单机多容器通信 隔离性好,易于管理 需要端口映射
host 高性能需求 无 NAT 开销 端口冲突风险
overlay Docker Swarm 集群 跨主机通信 配置复杂
数据卷管理
bash 复制代码
# 查看所有数据卷
docker volume ls

# 查看数据卷详情
docker volume inspect mongodb_data

# 备份数据卷
docker run --rm \
  -v mongodb_data:/data \
  -v $(pwd):/backup \
  alpine tar czf /backup/mongodb-backup.tar.gz /data

# 恢复数据卷
docker run --rm \
  -v mongodb_data:/data \
  -v $(pwd):/backup \
  alpine tar xzf /backup/mongodb-backup.tar.gz -C /

# 清理未使用的数据卷
docker volume prune

4. CI/CD 流水线配置

4.1 GitHub Actions 完整配置

yaml 复制代码
# .github/workflows/ci-cd.yml
name: CI/CD Pipeline

on:
  push:
    branches: [ main, develop, 'feature/*', 'fix/*' ]
  pull_request:
    branches: [ main ]

env:
  NODE_VERSION: '22'
  PNPM_VERSION: '10.10.0'
  VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
  VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
  TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
  TURBO_TEAM: ${{ secrets.TURBO_TEAM }}

jobs:
  test:
    name: Test Suite
    runs-on: ubuntu-latest

    services:
      mongodb:
        image: mongo:7
        ports:
          - 27017:27017
        options: >-
          --health-cmd "mongosh --eval 'db.runCommand(\"ping\")'"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
      redis:
        image: redis:7
        ports:
          - 6379:6379
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - uses: actions/checkout@v4

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

      - name: Install pnpm
        uses: pnpm/action-setup@v4
        with:
          version: '10.10.0'
          run_install: false

      - name: Get pnpm store directory
        shell: bash
        run: |
          echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV

      - name: Setup pnpm cache
        uses: actions/cache@v4
        with:
          path: ${{ env.STORE_PATH }}
          key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
          restore-keys: |
            ${{ runner.os }}-pnpm-store-

      - name: Install dependencies
        run: pnpm install --frozen-lockfile || pnpm install

      - name: Build shared types
        run: pnpm --filter @ai-blog/types build

      - name: Run backend linting
        run: pnpm --filter @ai-blog/backend lint || echo "Backend lint completed with warnings"

      - name: Run frontend linting
        run: pnpm --filter @ai-blog/frontend lint || echo "Frontend lint completed with warnings"

      - name: Check backend TypeScript
        run: pnpm --filter @ai-blog/backend exec tsc --noEmit || echo "Backend type check completed with warnings"

      - name: Check frontend TypeScript
        run: pnpm --filter @ai-blog/frontend exec tsc --noEmit || echo "Frontend type check completed with warnings"

      - name: Run backend tests
        run: pnpm --filter @ai-blog/backend test
        env:
          MONGODB_URI: mongodb://localhost:27017/test
          REDIS_URL: redis://localhost:6379
          JWT_SECRET: test-secret-for-ci
          OPENAI_API_KEY: test-key-placeholder

      - name: Run frontend tests
        run: pnpm --filter @ai-blog/frontend test || echo "No frontend tests configured"
        continue-on-error: true

      - name: Upload coverage reports
        uses: codecov/codecov-action@v4
        if: always() && hashFiles('./apps/backend/coverage') != ''
        continue-on-error: true
        with:
          directory: ./apps/backend/coverage
          flags: backend
          name: backend-coverage
          token: ${{ secrets.CODECOV_TOKEN }}

  build-and-deploy:
    name: Build and Deploy
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'

    steps:
      - uses: actions/checkout@v4

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

      - name: Install pnpm
        uses: pnpm/action-setup@v4
        with:
          version: '10.10.0'
          run_install: false

      - name: Get pnpm store directory
        shell: bash
        run: |
          echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV

      - name: Setup pnpm cache
        uses: actions/cache@v4
        with:
          path: ${{ env.STORE_PATH }}
          key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
          restore-keys: |
            ${{ runner.os }}-pnpm-store-

      - name: Install dependencies
        run: pnpm install --frozen-lockfile || pnpm install

      # Frontend: Deploy to Vercel
      - name: Check Vercel secrets
        id: vercel-check
        run: |
          if [ -z "${{ secrets.VERCEL_TOKEN }}" ]; then
            echo "has_vercel=false" >> $GITHUB_OUTPUT
            echo "::warning::VERCEL_TOKEN secret is not configured. Skipping Vercel deployment."
          else
            echo "has_vercel=true" >> $GITHUB_OUTPUT
          fi

      - name: Install Vercel CLI
        if: steps.vercel-check.outputs.has_vercel == 'true'
        run: pnpm add -g vercel@latest

      - name: Remove problematic env vars from Vercel
        if: steps.vercel-check.outputs.has_vercel == 'true'
        run: |
          echo "Removing VITE_API_URL from Vercel project config to avoid secret reference error"
          vercel env rm VITE_API_URL production --token="${{ secrets.VERCEL_TOKEN }}" --yes || echo "Variable not found or already removed"
          vercel env rm VITE_API_URL preview --token="${{ secrets.VERCEL_TOKEN }}" --yes || echo "Variable not found or already removed"
          vercel env rm VITE_API_URL development --token="${{ secrets.VERCEL_TOKEN }}" --yes || echo "Variable not found or already removed"
        working-directory: ./apps/frontend
        continue-on-error: true

      - name: Pull Vercel Environment Information
        if: steps.vercel-check.outputs.has_vercel == 'true'
        run: vercel pull --yes --environment=production --token="${{ secrets.VERCEL_TOKEN }}"
        working-directory: ./apps/frontend
        env:
          VITE_API_URL: ${{ secrets.VITE_API_URL || 'https://your-api-domain.com/api' }}

      - name: Build Frontend
        if: steps.vercel-check.outputs.has_vercel == 'true'
        run: vercel build --prod --token="${{ secrets.VERCEL_TOKEN }}"
        working-directory: ./apps/frontend
        env:
          VITE_API_URL: ${{ secrets.VITE_API_URL || 'https://your-api-domain.com/api' }}

      - name: Deploy Frontend to Vercel
        if: steps.vercel-check.outputs.has_vercel == 'true'
        run: vercel deploy --prebuilt --prod --token="${{ secrets.VERCEL_TOKEN }}"
        working-directory: ./apps/frontend
        env:
          VITE_API_URL: ${{ secrets.VITE_API_URL || 'https://your-api-domain.com/api' }}

      # Backend: Build and push Docker image
      - name: Check Docker secrets
        id: docker-check
        run: |
          if [ -z "${{ secrets.DOCKER_USERNAME }}" ] || [ -z "${{ secrets.DOCKER_PASSWORD }}" ]; then
            echo "has_docker=false" >> $GITHUB_OUTPUT
            echo "::warning::DOCKER_USERNAME or DOCKER_PASSWORD secret is not configured. Skipping Docker deployment."
          else
            echo "has_docker=true" >> $GITHUB_OUTPUT
          fi

      - name: Set up Docker Buildx
        if: steps.docker-check.outputs.has_docker == 'true'
        uses: docker/setup-buildx-action@v3

      - name: Login to Docker Hub
        if: steps.docker-check.outputs.has_docker == 'true'
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Extract metadata for Docker
        if: steps.docker-check.outputs.has_docker == 'true'
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ secrets.DOCKER_USERNAME }}/ai-blog-backend
          tags: |
            type=sha,prefix=
            type=ref,event=branch
            latest

      - name: Build and push backend
        if: steps.docker-check.outputs.has_docker == 'true'
        uses: docker/build-push-action@v6
        with:
          context: .
          file: ./docker/Dockerfile.backend
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  deploy-backend:
    needs: build-and-deploy
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'

    steps:
      - uses: actions/checkout@v4

      - name: Deploy Backend
        run: |
          echo "✅ Backend Docker image deployed"
          echo "✅ Frontend deployed to Vercel"
          echo "🚀 Full deployment complete!"

4.2 CI/CD 流程图

PR 到 main
Push 到 main


开发者推送代码
触发条件?
仅运行测试
完整流水线
代码检查
类型检查
单元测试
覆盖率报告
全部通过?
允许合并
阻止合并
测试套件
构建前端
部署到 Vercel
构建 Docker 镜像
推送到 Docker Hub
部署后端
发送通知
部署完成


4.3 配置项详细解释

触发器配置
yaml 复制代码
on:
  push:
    branches: [ main, develop, 'feature/*', 'fix/*' ]
  pull_request:
    branches: [ main ]

说明:

  • push:推送到指定分支时触发
  • pull_request:创建/更新 PR 时触发
  • 使用通配符 'feature/*' 匹配所有功能分支

环境变量
yaml 复制代码
env:
  NODE_VERSION: '22'
  PNPM_VERSION: '10.10.0'
  VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
  VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
  TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
  TURBO_TEAM: ${{ secrets.TURBO_TEAM }}

配置位置: GitHub → Settings → Secrets and variables → Actions

必需的秘密变量:

变量名 用途 获取方式
VERCEL_TOKEN Vercel API Token Vercel Dashboard → Account → Tokens
VERCEL_ORG_ID Vercel 组织 ID Vercel Dashboard → Settings → General
VERCEL_PROJECT_ID Vercel 项目 ID Vercel Dashboard → Project → Settings
DOCKER_USERNAME Docker Hub 用户名 Docker Hub 账号
DOCKER_PASSWORD Docker Hub 密码 Docker Hub → Account Settings → Security
TURBO_TOKEN Turborepo Remote Cache Turborepo Dashboard
TURBO_TEAM Turborepo Team ID Turborepo Dashboard

测试服务配置
yaml 复制代码
services:
  mongodb:
    image: mongo:7
    ports:
      - 27017:27017
    options: >-
      --health-cmd "mongosh --eval 'db.runCommand(\"ping\")'"
      --health-interval 10s
      --health-timeout 5s
      --health-retries 5

说明:

  • GitHub Actions 自动启动这些服务容器
  • health-cmd:健康检查命令,确保服务就绪
  • 测试完成后自动销毁,无需手动清理

依赖缓存优化
yaml 复制代码
- name: Get pnpm store directory
  shell: bash
  run: |
    echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV

- name: Setup pnpm cache
  uses: actions/cache@v4
  with:
    path: ${{ env.STORE_PATH }}
    key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
    restore-keys: |
      ${{ runner.os }}-pnpm-store-

优势:

  • ⚡ 缓存 pnpm 全局存储目录
  • ⚡ 基于 pnpm-lock.yaml 哈希作为缓存键
  • ⚡ 依赖未变化时直接使用缓存,节省 2-3 分钟

条件部署
yaml 复制代码
- name: Check Vercel secrets
  id: vercel-check
  run: |
    if [ -z "${{ secrets.VERCEL_TOKEN }}" ]; then
      echo "has_vercel=false" >> $GITHUB_OUTPUT
      echo "::warning::VERCEL_TOKEN secret is not configured. Skipping Vercel deployment."
    else
      echo "has_vercel=true" >> $GITHUB_OUTPUT
    fi

- name: Deploy Frontend to Vercel
  if: steps.vercel-check.outputs.has_vercel == 'true'
  run: vercel deploy --prebuilt --prod --token="${{ secrets.VERCEL_TOKEN }}"

好处:

  • ✅ secrets 未配置时不会失败,而是跳过部署
  • ✅ 适合开源项目,贡献者无需配置所有 secrets
  • ✅ 使用 ::warning:: 输出警告信息

Docker 构建优化
yaml 复制代码
- name: Build and push backend
  uses: docker/build-push-action@v6
  with:
    context: .
    file: ./docker/Dockerfile.backend
    push: true
    tags: ${{ steps.meta.outputs.tags }}
    labels: ${{ steps.meta.outputs.labels }}
    cache-from: type=gha
    cache-to: type=gha,mode=max

优化点:

  • cache-from: type=gha:使用 GitHub Actions Cache 作为构建缓存
  • cache-to: type=gha,mode=max:保存所有层的缓存
  • 后续构建速度提升 50-70%

4.4 部署策略

蓝绿部署(Blue-Green Deployment)

新部署环境
当前生产环境
切换流量
Blue: v1.0.0
Green: v1.1.0
Load Balancer
用户

实现步骤:

  1. 部署新版本到 Green 环境
  2. 运行健康检查和集成测试
  3. 切换负载均衡器指向 Green
  4. 观察监控指标
  5. 如有问题,快速切回 Blue

滚动更新(Rolling Update)
yaml 复制代码
# Kubernetes 示例
spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
      maxSurge: 1

特点:

  • ✅ 零停机时间
  • ✅ 逐步替换旧版本
  • ❌ 存在版本共存期,需保证向后兼容

5. 常见问题与解决方案

5.1 Git 分支管理问题

Q1: 如何撤销已推送的提交?
bash 复制代码
# 方法一:创建新的反向提交(推荐)
git revert <commit-hash>
git push origin main

# 方法二:强制回退(危险,仅限个人分支)
git reset --hard HEAD~1
git push --force-with-lease

⚠️ 警告: 永远不要在共享分支(main/develop)上使用 git push --force


Q2: 如何 cherry-pick 特定提交?
bash 复制代码
# 从 feature 分支挑选某个提交到 main
git checkout main
git cherry-pick <commit-hash>
git push origin main

应用场景: hotfix 已合并到 develop,但需要紧急应用到生产环境


Q3: 如何处理大型二进制文件?
bash 复制代码
# 使用 Git LFS(Large File Storage)
git lfs install
git lfs track "*.psd"
git add .gitattributes
git add file.psd
git commit -m "Add large file with LFS"

5.2 Vercel 部署问题

Q1: 构建失败 - "Module not found"

原因: 依赖未正确安装或 monorepo 链接问题

解决方案:

json 复制代码
{
  "installCommand": "pnpm install",
  "buildCommand": "pnpm --filter @ai-blog/types build && pnpm --filter @ai-blog/frontend build"
}

Q2: 环境变量未生效

检查清单:

  1. 确认变量名以 VITE_ 开头(Vite 项目)
  2. 在 Vercel Dashboard 中检查是否配置了正确的环境(Production/Preview)
  3. 重新部署使变量生效
  4. 检查代码中是否正确引用:import.meta.env.VITE_API_URL

Q3: API 请求 404

原因: Vercel 函数超时或后端服务不可达

解决方案:

json 复制代码
{
  "rewrites": [
    {
      "source": "/api/(.*)",
      "destination": "https://your-backend-domain.com/api/$1"
    }
  ]
}

确保后端 URL 是公网可访问的地址,不是 localhost


5.3 Docker 部署问题

Q1: 容器启动后立即退出
bash 复制代码
# 查看日志
docker logs ai-blog-backend

# 常见原因:
# 1. 环境变量缺失
# 2. 数据库连接失败
# 3. 端口被占用
# 4. 应用程序崩溃

调试步骤:

bash 复制代码
# 交互式启动容器
docker run -it --entrypoint sh ai-blog-backend:latest

# 在容器内手动启动应用
node dist/main.js

Q2: MongoDB 连接超时

检查清单:

  1. 确认 MongoDB 容器已启动:docker ps | grep mongodb
  2. 检查网络连接:docker network inspect ai-blog-network
  3. 验证连接字符串中的主机名(应使用容器名 mongodb,不是 localhost
  4. 检查防火墙规则

正确连接字符串:

bash 复制代码
# Docker 内部网络
MONGODB_URI=mongodb://admin:password@mongodb:27017/ai_blog?authSource=admin

# 本地开发
MONGODB_URI=mongodb://admin:password@localhost:27017/ai_blog?authSource=admin

Q3: Docker 镜像体积过大

优化前: 1.2GB
优化后: 350MB

优化技巧:

dockerfile 复制代码
# 1. 使用 Alpine 基础镜像
FROM node:22-alpine

# 2. 多阶段构建
FROM node:22-alpine AS builder
# ... 构建步骤 ...

FROM node:22-alpine
COPY --from=builder /app/dist ./dist

# 3. 合并 RUN 指令
RUN apt-get update && \
    apt-get install -y --no-install-recommends package && \
    rm -rf /var/lib/apt/lists/*

# 4. 使用 .dockerignore
echo "node_modules\n.git\ndist" > .dockerignore

# 5. 清理缓存
RUN npm ci --only=production && npm cache clean --force

5.4 CI/CD 流水线问题

Q1: 构建时间过长

优化策略:

yaml 复制代码
# 1. 启用依赖缓存
- name: Setup pnpm cache
  uses: actions/cache@v4
  with:
    path: ${{ env.STORE_PATH }}
    key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}

# 2. 并行执行独立任务
jobs:
  lint:
    runs-on: ubuntu-latest
    steps: [...]
  
  test:
    runs-on: ubuntu-latest
    steps: [...]
  
  build:
    needs: [lint, test]  # 并行完成后才构建
    runs-on: ubuntu-latest
    steps: [...]

# 3. 使用 TurboRepo Remote Cache
env:
  TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
  TURBO_TEAM: ${{ secrets.TURBO_TEAM }}

效果对比:

  • 优化前:15 分钟
  • 优化后:5 分钟(缓存命中时 2 分钟)

Q2: 部署失败 - 权限不足

错误信息:

复制代码
Error: The caller does not have permission

解决方案:

  1. 检查 GitHub Secrets 是否正确配置
  2. 验证 Vercel Token 是否有部署权限
  3. 确认 Docker Hub 账号密码正确
  4. 检查 Token 是否过期

重新生成 Vercel Token:

复制代码
Vercel Dashboard → Account → Tokens → Create Token
Scope: Read & Write

Q3: 测试通过但部署后出错

原因: 测试环境与生产环境差异

解决方案:

yaml 复制代码
# 1. 在 CI 中使用与生产相同的环境变量
- name: Run integration tests
  env:
    NODE_ENV: production
    DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}

# 2. 添加健康检查
- name: Health check after deployment
  run: |
    sleep 10  # 等待服务启动
    curl -f https://your-domain.com/health || exit 1

# 3. 使用 E2E 测试
- name: Run E2E tests
  run: pnpm test:e2e

5.5 性能优化建议

前端优化
nginx 复制代码
# Nginx 配置优化
gzip on;
gzip_types text/plain text/css application/json application/javascript;
gzip_min_length 1000;

location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

Vite 构建优化:

typescript 复制代码
// vite.config.ts
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
          antd: ['antd'],
        },
      },
    },
  },
});

后端优化
typescript 复制代码
// Nest.js 启用压缩
import compression from 'compression';

app.use(compression());

// 启用缓存
import { CacheInterceptor, CacheModule } from '@nestjs/cache-manager';

@Module({
  imports: [
    CacheModule.register({
      ttl: 300, // 5 minutes
      max: 100, // maximum number of items in cache
    }),
  ],
})
export class AppModule {}

数据库优化
javascript 复制代码
// MongoDB 索引优化
db.articles.createIndex({ slug: 1 }, { unique: true });
db.articles.createIndex({ createdAt: -1 });
db.articles.createIndex({ author: 1, createdAt: -1 });

// Redis 缓存策略
const cacheKey = `article:${slug}`;
const cached = await redis.get(cacheKey);
if (cached) {
  return JSON.parse(cached);
}

const article = await Article.findOne({ slug });
await redis.setex(cacheKey, 300, JSON.stringify(article)); // 5 min TTL
return article;

6. 总结与最佳实践

6.1 核心原则

开发效率
高质量交付
代码质量
系统稳定性
自动化流程
快速反馈
Code Review
自动化测试
静态分析
健康检查
监控告警
快速回滚


6.2 检查清单

分支管理
  • 遵循分支命名规范
  • 功能分支从 main/develop 创建
  • 提交信息清晰描述变更
  • PR 包含完整的测试和文档
  • Code Review 至少 1 人批准
  • 合并后删除远程分支
Vercel 部署
  • vercel.json 配置正确
  • SPA 路由回退规则已添加
  • 环境变量已配置(区分环境)
  • CORS 后端已正确配置
  • 自定义域名 DNS 已设置
  • 构建缓存已启用
Docker 部署
  • 使用多阶段构建减小镜像体积
  • 敏感信息通过环境变量传递
  • 数据卷已持久化
  • 健康检查已配置
  • 日志已收集和管理
  • 资源限制已设置(CPU/内存)
CI/CD
  • 测试覆盖率 > 80%
  • 依赖缓存已启用
  • Secrets 已正确配置
  • 部署失败有告警通知
  • 支持快速回滚
  • 构建时间 < 10 分钟

6.3 推荐工具链

类别 工具 用途
版本控制 Git + GitHub 代码管理
包管理 pnpm 快速、节省磁盘空间
Monorepo Turborepo 智能缓存、并行构建
CI/CD GitHub Actions 自动化流水线
前端部署 Vercel 零配置、全球 CDN
容器化 Docker 环境一致性
编排 Docker Compose 多容器管理
监控 Sentry + Prometheus 错误追踪、性能监控
日志 ELK Stack 日志聚合分析

6.4 学习资源


结语

本文基于真实的 Monorepo 全栈项目,系统地介绍了:

  1. Git 分支管理:从理论到实践,包括多种策略对比和应急处理流程
  2. Vercel 部署:SPA 路由、CORS 配置、环境变量管理等细节
  3. Docker 容器化:MongoDB、Redis、前后端服务的完整配置
  4. CI/CD 流水线:GitHub Actions 的详细解析和优化技巧

希望这份指南能帮助你构建高效、可靠的现代化开发工作流。记住,工具是为目标服务的,选择最适合你团队的方案,持续迭代优化,才能最大化开发效率。


📝 关于作者

本文基于 AI Blog Platform 项目实战经验总结而成,该项目是一个基于 Nest.js + React + LangChain 的全栈 AI 内容创作平台,采用 Turborepo Monorepo 架构,支持智能写作、RAG 知识库、可视化工作流编排等功能。

🔗 相关链接


License: MIT © 2024

相关推荐
剑神一笑1 小时前
Linux find 命令深度解析:从递归遍历到性能优化的完整实现
linux·运维·性能优化
隔窗听雨眠1 小时前
Chrome 安全机制深度解析
前端·chrome·安全
GISer_Jing2 小时前
GitHub Actions 完整 Token/Secret 配置详解(Vercel + Docker 2026最新版)
docker·容器·github
火车叼位2 小时前
像管理 Linux 一样 SSH 到 Windows:OpenSSH Server 与 Git Bash 实战
运维·windows·ssh
我是Superman丶2 小时前
Docker 镜像加速
运维·docker·容器
白緢2 小时前
二、Linux 开发工具
linux·运维·服务器
爱喝水的鱼丶2 小时前
SAP-ABAP:SAP 系统变量 SY-INDEX 学习笔记:从 1 开始的循环计数器
运维·开发语言·数据库·sap·abap
史迪仔01122 小时前
[QML] Qt6/Qt5四大渐变效果实战指南
开发语言·前端·c++·qt
果壳~2 小时前
【Uniapp】【rich-text】富文本展示以及图片预览功能解决方案
前端·javascript·uni-app