GitLab + GitLab Runner + Qiankun 微前端 + Nginx + Node 中间件 前端开发机从零搭建 CI/CD 全流程

全文约 1550 行,建议阅读时间一个小时,覆盖从零到全自动 CI/CD 的完整流程:

章节 核心内容
一、目标架构 整体架构图:GitLab → Runner → Pipeline(install/lint/test/build/deploy/notify)→ Nginx 托管 Qiankun 微前端 + Node 中间件代理
二、环境准备 Docker、Docker Compose、Node.js/nvm 安装
三、GitLab Docker 部署 GitLab、初始密码获取、URL 配置、关闭公开注册
四、GitLab Runner Docker 安装 + gitlab-runner register 交互式注册(docker executor, node:20-alpine)
五、Qiankun 项目结构 主应用(registerMicroApps + start sandbox)+ Vue/React 子应用(UMD 导出 + Vite CORS 配置 + 生命周期)
六、Node 中间件 Express 服务 + 企业微信/飞书/钉钉三种 IM 机器人 Webhook 通知实现 + Winston 日志 + PM2 进程管理
七、Nginx 配置 主应用 + 各子应用 alias/try_files + 跨域头 + /api/ 代理到 Node 中间件 + Gzip + 静态资源缓存
八、.gitlab-ci.yml 6 阶段 Pipeline 完整 YAML:install(3 子应用并行 npm ci)→ lint → test → build(含 artifacts)→ deploy(rsync + SSH)→ notify(curl 到 Node 中间件,成功/失败分别通知)
九、部署准备 Nginx 目录创建、SSH 免密配置、PM2 启动中间件、GitLab Variables 配置表
十、操作流程 一次性 9 步初始化脚本 + 日常开发 push → MR → Pipeline → 合并 → 自动部署 → IM 通知全流程
十一、踩坑 Qiankun 的 CORS/publicPath/沙箱/SVG 冲突、CI 的 npm ci/pending/OOM、Nginx 的 SPA 404/alias/502 共 15 个坑和解决方案

前端开发机从零搭建 CI/CD 全流程

技术栈:GitLab + GitLab Runner + Qiankun 微前端 + Nginx + Node 中间件(IM 群通知机器人)

一、目标架构全景

1.1 最终效果

bash 复制代码
开发者 push 代码到 GitLab
        │
        ▼
GitLab Runner 自动触发 Pipeline
        │
        ├── 阶段1: install → 安装依赖
        ├── 阶段2: lint + test → 代码检查 + 单元测试
        ├── 阶段3: build → 构建子应用
        ├── 阶段4: deploy → 部署到 Nginx 静态目录
        └── 阶段5: notify → Node 中间件发 IM 群通知

1.2 整体架构图

bash 复制代码
┌─────────────────────────────────────────────────────────────────┐
│                        开发机 (CentOS/Ubuntu/macOS)               │
│                                                                 │
│  ┌──────────────┐   ┌──────────────────┐   ┌────────────────┐  │
│  │   GitLab      │   │  GitLab Runner   │   │    Nginx       │  │
│  │  (代码仓库)    │   │  (CI/CD 执行器)   │   │  (静态服务)     │  │
│  │  Port: 8080   │   │  Docker Executor │   │  Port: 80      │  │
│  └──────┬───────┘   └────────┬─────────┘   └───────┬────────┘  │
│         │                    │                      │           │
│         │  ① push 代码       │  ② 触发 Pipeline     │           │
│         │ ──────────────────→│                      │           │
│         │                    │  ③ 执行 Job          │           │
│         │                    │  (lint→test→build)   │           │
│         │                    │        │             │           │
│         │                    │  ④ 产物复制          │           │
│         │                    │ ────────────────────→│           │
│         │                    │                      │           │
│         │                    │  ⑤ 通知 Node 中间件  │           │
│         │                    │ ────────────────────→│           │
│         │                    │                      │           │
│  ┌──────┴────────────────────┴──────────────────────┴────────┐  │
│  │              Node 中间件 (Port: 3000)                       │  │
│  │  ┌──────────────────────────────────────────────────────┐  │  │
│  │  │  POST /api/notify  ← 接收 CI 通知                     │  │  │
│  │  │       │                                               │  │  │
│  │  │       └──→ 格式化消息 → 调用 IM Webhook → 群机器人推送 │  │  │
│  │  └──────────────────────────────────────────────────────┘  │  │
│  └───────────────────────────────────────────────────────────┘  │
│                                                                 │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │              Qiankun 微前端 (由 Nginx 托管)                 │  │
│  │                                                            │  │
│  │  /                → 主应用 (main-app)     → /usr/share/nginx/html/main/     │
│  │  /sub-vue/        → Vue 子应用            → /usr/share/nginx/html/sub-vue/  │
│  │  /sub-react/      → React 子应用          → /usr/share/nginx/html/sub-react/│
│  │  /api/            → Node 中间件代理        → http://127.0.0.1:3000           │
│  └───────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────┘

二、环境准备与组件安装

2.1 开发机最低配置要求

资源 最低要求 推荐
CPU 4 核 8 核
内存 8 GB 16 GB
磁盘 50 GB 100 GB SSD
操作系统 CentOS 7+ / Ubuntu 20.04+ / macOS Ubuntu 22.04

GitLab 本身比较吃内存(官方建议 4GB+),加上 GitLab Runner 的 Docker 环境和 Node 构建,8GB 是底线。

2.2 前置软件安装

bash 复制代码
# ────────── Ubuntu/Debian ──────────
sudo apt update && sudo apt install -y curl wget git vim

# ────────── CentOS/RHEL ──────────
sudo yum install -y curl wget git vim

# ────────── macOS ──────────
# 使用 Homebrew
brew install curl wget git

2.3 安装 Docker(GitLab Runner 的执行环境)

bash 复制代码
# ────────── Ubuntu ──────────
sudo apt install -y docker.io
sudo systemctl enable docker
sudo systemctl start docker
sudo usermod -aG docker $USER  # 免 sudo 执行 docker(需重新登录生效)

# ────────── CentOS ──────────
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
sudo yum install -y docker-ce docker-ce-cli containerd.io
sudo systemctl enable docker && sudo systemctl start docker
sudo usermod -aG docker $USER

# ────────── macOS ──────────
# 下载安装 Docker Desktop: https://www.docker.com/products/docker-desktop
# 或使用 Homebrew:
brew install --cask docker

# ────────── 验证 ──────────
docker --version
docker run hello-world

2.4 安装 Docker Compose(可选,推荐)

bash 复制代码
# 使用 Docker Compose 管理 GitLab 等多个容器
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" \
  -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose --version

2.5 安装 Node.js(运行中间件 + 构建前端项目)

bash 复制代码
# 推荐使用 nvm 管理 Node 版本
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
source ~/.bashrc  # 或 source ~/.zshrc

nvm install 20   # LTS 版本
nvm use 20
nvm alias default 20

node --version   # v20.x.x
npm --version

三、GitLab 安装与配置

3.1 Docker 安装 GitLab(推荐)

bash 复制代码
# 创建数据和配置目录
sudo mkdir -p /srv/gitlab/config /srv/gitlab/logs /srv/gitlab/data

# 启动 GitLab 容器
sudo docker run -d \
  --name gitlab \
  --restart always \
  --hostname gitlab.your-dev.com \
  -p 8080:80 \           # HTTP(开发机 8080 端口映射到容器 80)
  -p 8443:443 \          # HTTPS(可选)
  -p 2222:22 \           # SSH(避免和宿主机 22 端口冲突)
  -v /srv/gitlab/config:/etc/gitlab \
  -v /srv/gitlab/logs:/var/log/gitlab \
  -v /srv/gitlab/data:/var/opt/gitlab \
  gitlab/gitlab-ce:latest

等待约 3~5 分钟(GitLab 初始化较慢),查看状态:

bash 复制代码
sudo docker logs -f gitlab  # 看到 "GitLab is ready" 就启动完成

3.2 获取初始 root 密码

bash 复制代码
# 进入容器查看
sudo docker exec -it gitlab grep 'Password:' /etc/gitlab/initial_root_password
# 或者
sudo cat /srv/gitlab/config/initial_root_password

初始密码文件 24 小时后会被自动删除,建议登录后立即修改。

3.3 首次登录配置

markdown 复制代码
访问 http://你的开发机IP:8080

1. 使用 root / 初始密码登录
2. 修改 root 密码
3. Admin Area → Settings → General → Sign-up restrictions:
   - 关闭公开注册(内部开发机无需公开注册)
4. 创建第一个项目(或导入现有代码)

3.4 修改 GitLab 对外 URL(重要)

bash 复制代码
# 修改 GitLab 配置文件
sudo vi /srv/gitlab/config/gitlab.rb

# 设置外部 URL(改为你的开发机 IP)
external_url 'http://192.168.1.100:8080'

# 使配置生效
sudo docker exec -it gitlab gitlab-ctl reconfigure
sudo docker restart gitlab

3.5 创建 CI/CD 演示项目

bash 复制代码
# 在本地创建项目目录
mkdir ~/projects && cd ~/projects
mkdir qiankun-cicd-demo && cd qiankun-cicd-demo

git init
git remote add origin http://192.168.1.100:8080/root/qiankun-cicd-demo.git

# 稍后在 GitLab Web UI 中创建同名项目后 push
# 或在 Web UI 中直接创建空项目

四、GitLab Runner 安装与注册

4.1 安装 GitLab Runner

bash 复制代码
# ────────── Ubuntu / CentOS (Docker 方式) ──────────
sudo docker run -d \
  --name gitlab-runner \
  --restart always \
  -v /srv/gitlab-runner/config:/etc/gitlab-runner \
  -v /var/run/docker.sock:/var/run/docker.sock \
  gitlab/gitlab-runner:latest

# ────────── macOS (直接安装) ──────────
brew install gitlab-runner
brew services start gitlab-runner

4.2 注册 Runner 到 GitLab

第一步:获取注册 Token

arduino 复制代码
GitLab Web UI → Admin Area → CI/CD → Runners → 找到 "Registration token"
或者:项目页面 → Settings → CI/CD → Runners → 找到 token

第二步:注册 Runner

bash 复制代码
# Docker 方式安装的 Runner
sudo docker exec -it gitlab-runner gitlab-runner register

# macOS 直接安装的 Runner
gitlab-runner register

交互式注册流程:

yaml 复制代码
Enter the GitLab instance URL:
→ http://192.168.1.100:8080

Enter the registration token:
→ xxxxxxxxxxxxxxx   (上面获取的 token)

Enter a description for the runner:
→ qiankun-dev-runner

Enter tags for the runner (comma-separated):
→ qiankun,frontend,docker

Enter optional maintenance note:
→ 开发机 CI/CD Runner

Enter an executor:
→ docker  ← 选择 docker executor(推荐)

Enter the default Docker image:
→ node:20-alpine  ← 构建前端项目的默认镜像

4.3 验证 Runner 状态

bash 复制代码
# 查看 Runner 配置
sudo docker exec -it gitlab-runner cat /etc/gitlab-runner/config.toml

# GitLab Web UI → Admin Area → CI/CD → Runners
# 应该看到一个绿色圆点,表示 Runner 在线

五、Qiankun 微前端项目结构

5.1 项目目录设计

csharp 复制代码
qiankun-cicd-demo/
├── .gitlab-ci.yml              # GitLab CI/CD 配置文件(核心)
├── main-app/                   # 主应用(基座)
│   ├── package.json
│   ├── src/
│   │   ├── App.vue             # 注册子应用的路由容器
│   │   ├── main.js             # 注册微应用
│   │   └── micro-apps.js       # 子应用配置
│   ├── public/
│   │   └── index.html
│   └── vite.config.js
│
├── sub-vue/                    # Vue 子应用
│   ├── package.json
│   ├── src/
│   │   ├── App.vue
│   │   └── main.js             # 导出 mount/unmount 生命周期
│   └── vite.config.js          # 配置 qiankun 兼容
│
├── sub-react/                  # React 子应用
│   ├── package.json
│   ├── src/
│   │   ├── App.jsx
│   │   └── index.js            # 导出 mount/unmount 生命周期
│   └── vite.config.js
│
├── node-middleware/            # Node 中间件(IM 通知)
│   ├── package.json
│   ├── src/
│   │   ├── index.js            # Express 服务入口
│   │   ├── routes/
│   │   │   └── notify.js       # POST /api/notify
│   │   └── services/
│   │       ├── wecom.js        # 企业微信机器人
│   │       ├── feishu.js       # 飞书机器人
│   │       └── dingtalk.js     # 钉钉机器人
│   └── .env                    # Webhook 地址等配置
│
└── nginx/
    └── default.conf            # Nginx 配置文件

5.2 主应用核心代码(Qiankun 基座)

main-app/src/micro-apps.js:子应用注册配置

javascript 复制代码
// 子应用列表
const apps = [
  {
    name: 'sub-vue',
    entry: '//localhost:8081',           // 开发环境
    // entry: '/sub-vue/',               // 生产环境(Nginx 子目录)
    container: '#sub-app-container',
    activeRule: '/sub-vue',
    props: { globalData: {} },
  },
  {
    name: 'sub-react',
    entry: '//localhost:8082',
    // entry: '/sub-react/',
    container: '#sub-app-container',
    activeRule: '/sub-react',
    props: { globalData: {} },
  },
];

export default apps;

main-app/src/main.js:注册微应用

javascript 复制代码
import { registerMicroApps, start, setDefaultMountApp } from 'qiankun';
import apps from './micro-apps';

registerMicroApps(apps, {
  beforeLoad: [async (app) => console.log(`[qiankun] ${app.name} 加载前`)],
  beforeMount: [async (app) => console.log(`[qiankun] ${app.name} 挂载前`)],
  afterUnmount: [async (app) => console.log(`[qiankun] ${app.name} 卸载后`)],
});

setDefaultMountApp('/sub-vue'); // 默认加载第一个子应用

start({
  sandbox: { experimentalStyleIsolation: true }, // 样式隔离
});

5.3 子应用核心代码

Vue 子应用 sub-vue/src/main.js

javascript 复制代码
import { createApp } from 'vue';
import App from './App.vue';
import { createRouter, createWebHistory } from 'vue-router';

let app = null;
let router = null;
let instance = null;

function render(props = {}) {
  const { container } = props;

  router = createRouter({
    history: createWebHistory(window.__POWERED_BY_QIANKUN__ ? '/sub-vue' : '/'),
    routes: [
      { path: '/', component: () => import('./pages/Home.vue') },
      { path: '/list', component: () => import('./pages/List.vue') },
    ],
  });

  app = createApp(App);
  app.use(router);
  instance = app.mount(container ? container.querySelector('#app') : '#app');
}

// 独立运行
if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

// qiankun 生命周期(★ 必须导出)
export async function bootstrap() {
  console.log('[sub-vue] bootstrap');
}

export async function mount(props) {
  console.log('[sub-vue] mount', props);
  render(props);
}

export async function unmount() {
  console.log('[sub-vue] unmount');
  instance.unmount();
  app = null;
  router = null;
  instance = null;
}

Vite 配置 sub-vue/vite.config.js(关键:兼容 qiankun):

javascript 复制代码
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  plugins: [vue()],
  base: '/sub-vue/', // ★ 生产环境子路径
  server: {
    port: 8081,
    cors: true,      // ★ 允许跨域(qiankun 通过 fetch 加载子应用)
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
  },
  build: {
    // ★ qiankun 要求 UMD 格式,Vite 需要插件或配置
    // 方案1: 使用 vite-plugin-qiankun
    // 方案2: 配置为 library 模式(见下方)
    lib: {
      entry: './src/main.js',
      name: 'subVue',
      formats: ['umd'],
      fileName: () => 'sub-vue.js',
    },
  },
});

React 子应用 sub-react/src/index.js

jsx 复制代码
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

let root = null;

function render(props = {}) {
  const { container } = props;
  const dom = container
    ? container.querySelector('#root')
    : document.getElementById('root');

  root = ReactDOM.createRoot(dom);
  root.render(<App globalData={props.globalData} />);
}

if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

export async function bootstrap() {
  console.log('[sub-react] bootstrap');
}

export async function mount(props) {
  console.log('[sub-react] mount', props);
  render(props);
}

export async function unmount() {
  console.log('[sub-react] unmount');
  root.unmount();
  root = null;
}

六、Node 中间件(IM 群通知机器人)

6.1 中间件架构

bash 复制代码
CI Pipeline 最后阶段
       │
       │  POST /api/notify
       ▼
┌──────────────────────┐
│   Node 中间件          │
│   (Port 3000)         │
│                       │
│  1. 接收通知数据       │
│  2. 格式化消息         │
│  3. 调用 IM Webhook   │
│  4. 记录通知日志       │
└──────┬───────────────┘
       │
       ├──→ 企业微信机器人
       ├──→ 飞书机器人
       └──→ 钉钉机器人

6.2 中间件完整代码

node-middleware/package.json

json 复制代码
{
  "name": "cicd-notify-middleware",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "start": "node src/index.js",
    "dev": "nodemon src/index.js"
  },
  "dependencies": {
    "axios": "^1.7.0",
    "cors": "^2.8.5",
    "dotenv": "^16.4.0",
    "express": "^4.19.0",
    "morgan": "^1.10.0",
    "winston": "^3.13.0"
  },
  "devDependencies": {
    "nodemon": "^3.1.0"
  }
}

node-middleware/.env

bash 复制代码
PORT=3000

# 企业微信机器人 Webhook
WECOM_WEBHOOK=https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY

# 飞书机器人 Webhook
FEISHU_WEBHOOK=https://open.feishu.cn/open-apis/bot/v2/hook/YOUR_HOOK

# 钉钉机器人 Webhook
DINGTALK_WEBHOOK=https://oapi.dingtalk.com/robot/send?access_token=YOUR_TOKEN

# 日志目录
LOG_DIR=./logs

node-middleware/src/index.js

javascript 复制代码
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const morgan = require('morgan');
const notifyRoutes = require('./routes/notify');
const logger = require('./utils/logger');

const app = express();
const PORT = process.env.PORT || 3000;

// 中间件
app.use(cors());
app.use(express.json());
app.use(morgan('combined'));

// 路由
app.use('/api', notifyRoutes);

// 健康检查
app.get('/health', (req, res) => {
  res.json({ status: 'ok', uptime: process.uptime() });
});

app.listen(PORT, () => {
  logger.info(`[Notify Middleware] 服务启动在端口 ${PORT}`);
  logger.info(`[Notify Middleware] 企业微信: ${process.env.WECOM_WEBHOOK ? '已配置' : '未配置'}`);
  logger.info(`[Notify Middleware] 飞书: ${process.env.FEISHU_WEBHOOK ? '已配置' : '未配置'}`);
  logger.info(`[Notify Middleware] 钉钉: ${process.env.DINGTALK_WEBHOOK ? '已配置' : '未配置'}`);
});

node-middleware/src/routes/notify.js

javascript 复制代码
const express = require('express');
const router = express.Router();
const logger = require('../utils/logger');

// 动态加载所有通知服务
const notifiers = [];
try { notifiers.push(require('../services/wecom')); } catch (e) { logger.warn('企业微信服务加载失败'); }
try { notifiers.push(require('../services/feishu')); } catch (e) { logger.warn('飞书服务加载失败'); }
try { notifiers.push(require('../services/dingtalk')); } catch (e) { logger.warn('钉钉服务加载失败'); }

/**
 * POST /api/notify
 * Body: {
 *   project: "项目名称",
 *   branch: "分支名",
 *   commit: "提交 hash",
 *   author: "提交人",
 *   message: "提交信息",
 *   status: "success | failed",
 *   stage: "构建阶段",
 *   duration: "耗时(秒)",
 *   url: "Pipeline URL",
 *   changelog: "变更摘要"
 * }
 */
router.post('/notify', async (req, res) => {
  const { project, branch, commit, author, message, status, stage, duration, url, changelog } = req.body;

  if (!project || !status) {
    return res.status(400).json({ error: '缺少必要字段 project / status' });
  }

  const results = [];

  // 并发推送到所有 IM 平台
  const promises = notifiers.map(async (notifier) => {
    try {
      const result = await notifier.send({
        project, branch, commit, author, message,
        status, stage, duration, url, changelog,
      });
      results.push({ platform: notifier.name, success: true, ...result });
    } catch (err) {
      logger.error(`[${notifier.name}] 发送失败: ${err.message}`);
      results.push({ platform: notifier.name, success: false, error: err.message });
    }
  });

  await Promise.all(promises);

  res.json({
    received: true,
    results,
  });
});

// 查询通知历史(简易版,生产环境应接数据库)
const historyCache = [];
router.get('/notify/history', (req, res) => {
  res.json({ total: historyCache.length, list: historyCache.slice(-20) });
});

module.exports = router;

node-middleware/src/services/wecom.js(企业微信机器人):

javascript 复制代码
const axios = require('axios');

const WEBHOOK_URL = process.env.WECOM_WEBHOOK;

module.exports = {
  name: 'wecom',

  async send(data) {
    if (!WEBHOOK_URL) throw new Error('WEBHOOK_URL 未配置');

    const statusEmoji = data.status === 'success' ? '✅' : '❌';
    const statusText = data.status === 'success' ? '成功' : '失败';

    const content = [
      `## ${statusEmoji} CI/CD ${statusText}`,
      ``,
      `> **项目**: ${data.project}`,
      `> **分支**: \`${data.branch}\``,
      `> **提交**: [${(data.commit || '').slice(0, 8)}](${data.url})`,
      `> **作者**: ${data.author}`,
      `> **信息**: ${data.message}`,
      `> **阶段**: ${data.stage}`,
      `> **耗时**: ${data.duration}s`,
    ];

    if (data.changelog) {
      content.push(`> **变更**: ${data.changelog}`);
    }

    // 企业微信 Markdown 消息
    const payload = {
      msgtype: 'markdown',
      markdown: {
        content: content.join('\n'),
      },
    };

    const resp = await axios.post(WEBHOOK_URL, payload, { timeout: 5000 });
    return { code: resp.data.errcode };
  },
};

node-middleware/src/services/feishu.js(飞书机器人):

javascript 复制代码
const axios = require('axios');

const WEBHOOK_URL = process.env.FEISHU_WEBHOOK;

module.exports = {
  name: 'feishu',

  async send(data) {
    if (!WEBHOOK_URL) throw new Error('WEBHOOK_URL 未配置');

    const statusColor = data.status === 'success' ? 'green' : 'red';
    const statusText = data.status === 'success' ? '成功' : '失败';

    const payload = {
      msg_type: 'interactive',
      card: {
        header: {
          title: {
            tag: 'plain_text',
            content: `CI/CD ${statusText} - ${data.project}`,
          },
          template: statusColor,
        },
        elements: [
          { tag: 'div', text: { tag: 'lark_md', content: `**分支**: ${data.branch}` } },
          { tag: 'div', text: { tag: 'lark_md', content: `**提交人**: ${data.author}` } },
          { tag: 'div', text: { tag: 'lark_md', content: `**提交信息**: ${data.message}` } },
          { tag: 'div', text: { tag: 'lark_md', content: `**阶段**: ${data.stage} | **耗时**: ${data.duration}s` } },
          {
            tag: 'action',
            actions: [
              {
                tag: 'button',
                text: { tag: 'plain_text', content: '查看详情' },
                url: data.url,
                type: 'primary',
              },
            ],
          },
        ],
      },
    };

    const resp = await axios.post(WEBHOOK_URL, payload, { timeout: 5000 });
    return { code: resp.data.code };
  },
};

node-middleware/src/services/dingtalk.js(钉钉机器人):

javascript 复制代码
const axios = require('axios');

const WEBHOOK_URL = process.env.DINGTALK_WEBHOOK;

module.exports = {
  name: 'dingtalk',

  async send(data) {
    if (!WEBHOOK_URL) throw new Error('WEBHOOK_URL 未配置');

    const statusText = data.status === 'success' ? '✅ 成功' : '❌ 失败';

    const payload = {
      msgtype: 'markdown',
      markdown: {
        title: `CI/CD ${statusText}`,
        text: [
          `### ${statusText} - ${data.project}`,
          ``,
          `- **分支**: \`${data.branch}\``,
          `- **提交**: ${(data.commit || '').slice(0, 8)}`,
          `- **作者**: ${data.author}`,
          `- **信息**: ${data.message}`,
          `- **阶段**: ${data.stage}`,
          `- **耗时**: ${data.duration}s`,
          ``,
          `[查看 Pipeline](${data.url})`,
        ].join('\n'),
      },
    };

    const resp = await axios.post(WEBHOOK_URL, payload, { timeout: 5000 });
    return { code: resp.data.errcode };
  },
};

node-middleware/src/utils/logger.js

javascript 复制代码
const winston = require('winston');
const path = require('path');

const logDir = process.env.LOG_DIR || './logs';

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
    winston.format.printf(({ timestamp, level, message }) => {
      return `[${timestamp}] ${level.toUpperCase()}: ${message}`;
    })
  ),
  transports: [
    new winston.transports.Console({
      format: winston.format.combine(
        winston.format.colorize(),
        winston.format.printf(({ timestamp, level, message }) => {
          return `[${timestamp}] ${level}: ${message}`;
        })
      ),
    }),
    new winston.transports.File({ filename: path.join(logDir, 'error.log'), level: 'error' }),
    new winston.transports.File({ filename: path.join(logDir, 'combined.log') }),
  ],
});

module.exports = logger;

七、Nginx 配置

7.1 安装 Nginx

bash 复制代码
# Ubuntu
sudo apt install -y nginx

# CentOS
sudo yum install -y nginx

# macOS
brew install nginx

7.2 Nginx 配置文件

nginx/default.conf

nginx 复制代码
server {
    listen 80;
    server_name localhost;

    # 日志
    access_log /var/log/nginx/qiankun_access.log;
    error_log  /var/log/nginx/qiankun_error.log;

    # ────────── 主应用(基座) ──────────
    location / {
        root   /usr/share/nginx/html/main;
        index  index.html;
        try_files $uri $uri/ /index.html;  # SPA 路由
    }

    # ────────── Vue 子应用 ──────────
    location /sub-vue/ {
        alias /usr/share/nginx/html/sub-vue/;
        index index.html;
        try_files $uri $uri/ /sub-vue/index.html;

        # 子应用跨域支持(qiankun fetch 加载需要)
        add_header Access-Control-Allow-Origin *;
    }

    # ────────── React 子应用 ──────────
    location /sub-react/ {
        alias /usr/share/nginx/html/sub-react/;
        index index.html;
        try_files $uri $uri/ /sub-react/index.html;

        add_header Access-Control-Allow-Origin *;
    }

    # ────────── Node 中间件代理 ──────────
    location /api/ {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # 超时配置
        proxy_connect_timeout 10s;
        proxy_send_timeout 30s;
        proxy_read_timeout 30s;
    }

    # ────────── 静态资源缓存 ──────────
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
        root /usr/share/nginx/html;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    # ────────── Gzip 压缩 ──────────
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_types text/plain text/css text/xml text/javascript
               application/javascript application/json application/xml
               image/svg+xml;
}

7.3 部署 Nginx 配置

bash 复制代码
# 备份原配置
sudo cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak

# 将 default.conf 复制到 nginx 配置目录
sudo cp nginx/default.conf /etc/nginx/conf.d/qiankun.conf
# 或覆盖默认(macOS: /usr/local/etc/nginx/nginx.conf)

# 检查配置语法
sudo nginx -t

# 重载 Nginx
sudo nginx -s reload
# 或
sudo systemctl reload nginx

八、GitLab CI/CD 核心配置文件

8.1 .gitlab-ci.yml 完整配置

yaml 复制代码
# ──────────────────────────────────────────
# GitLab CI/CD Pipeline 配置
# 项目:Qiankun 微前端 + CI/CD
# ──────────────────────────────────────────

# 全局变量
variables:
  NODE_VERSION: "20-alpine"
  # 构建产物目录
  BUILD_DIR: /srv/builds/$CI_PROJECT_NAME
  # Node 中间件通知地址
  NOTIFY_URL: "http://192.168.1.100:3000/api/notify"

# 定义 Pipeline 阶段(按顺序执行)
stages:
  - install
  - lint
  - test
  - build
  - deploy
  - notify

# 缓存策略:缓存 node_modules 加速后续构建
cache:
  key: ${CI_COMMIT_REF_SLUG}
  paths:
    - main-app/node_modules/
    - sub-vue/node_modules/
    - sub-react/node_modules/

# ──────────────────────────────────────────
# 阶段1:安装依赖
# ──────────────────────────────────────────
install:main:
  stage: install
  image: node:${NODE_VERSION}
  script:
    - cd main-app && npm ci --cache .npm --prefer-offline
  artifacts:
    paths:
      - main-app/node_modules/
    expire_in: 1 hour
  tags:
    - qiankun
  only:
    - master
    - develop
    - merge_requests

install:sub-vue:
  stage: install
  image: node:${NODE_VERSION}
  script:
    - cd sub-vue && npm ci --cache .npm --prefer-offline
  artifacts:
    paths:
      - sub-vue/node_modules/
    expire_in: 1 hour
  tags:
    - qiankun
  only:
    - master
    - develop
    - merge_requests

install:sub-react:
  stage: install
  image: node:${NODE_VERSION}
  script:
    - cd sub-react && npm ci --cache .npm --prefer-offline
  artifacts:
    paths:
      - sub-react/node_modules/
    expire_in: 1 hour
  tags:
    - qiankun
  only:
    - master
    - develop
    - merge_requests

# ──────────────────────────────────────────
# 阶段2:代码检查
# ──────────────────────────────────────────
lint:
  stage: lint
  image: node:${NODE_VERSION}
  needs: ["install:main", "install:sub-vue", "install:sub-react"]
  before_script:
    # 安装 ESLint(如果没有全局安装)
    - npm install -g eslint
  script:
    - echo "--- ESLint: 主应用 ---"
    - cd main-app && npx eslint src/ --ext .js,.vue --max-warnings 0 || echo "ESLint 告警,但不阻断"
    - echo "--- ESLint: sub-vue ---"
    - cd ../sub-vue && npx eslint src/ --ext .js,.vue --max-warnings 0 || echo "ESLint 告警,但不阻断"
    - echo "--- ESLint: sub-react ---"
    - cd ../sub-react && npx eslint src/ --ext .js,.jsx --max-warnings 0 || echo "ESLint 告警,但不阻断"
  allow_failure: true  # lint 告警不阻断 pipeline
  tags:
    - qiankun
  only:
    - master
    - develop
    - merge_requests

# ──────────────────────────────────────────
# 阶段3:单元测试
# ──────────────────────────────────────────
test:main:
  stage: test
  image: node:${NODE_VERSION}
  needs: ["install:main"]
  script:
    - cd main-app && npm run test -- --coverage --passWithNoTests
  coverage: /All files[^|]*\|[^|]*\s+([\d\.]+)/
  artifacts:
    when: always
    reports:
      junit: main-app/junit.xml
      coverage_report:
        coverage_format: cobertura
        path: main-app/coverage/cobertura-coverage.xml
  tags:
    - qiankun
  only:
    - master
    - merge_requests

test:sub-vue:
  stage: test
  image: node:${NODE_VERSION}
  needs: ["install:sub-vue"]
  script:
    - cd sub-vue && npm run test -- --coverage --passWithNoTests
  coverage: /All files[^|]*\|[^|]*\s+([\d\.]+)/
  tags:
    - qiankun
  only:
    - master
    - merge_requests

# ──────────────────────────────────────────
# 阶段4:构建
# ──────────────────────────────────────────
build:main:
  stage: build
  image: node:${NODE_VERSION}
  needs: ["install:main", "lint", "test:main"]
  script:
    - cd main-app
    # 注入构建环境变量
    - export VITE_APP_VERSION=$(date +%Y%m%d%H%M%S)
    - export VITE_APP_ENV=production
    - npm run build
    # 确认产物
    - ls -la dist/
  artifacts:
    paths:
      - main-app/dist/
    expire_in: 7 days
  tags:
    - qiankun
  only:
    - master
    - develop

build:sub-vue:
  stage: build
  image: node:${NODE_VERSION}
  needs: ["install:sub-vue", "lint", "test:sub-vue"]
  script:
    - cd sub-vue
    - export VITE_APP_VERSION=$(date +%Y%m%d%H%M%S)
    - npm run build
    - ls -la dist/
  artifacts:
    paths:
      - sub-vue/dist/
    expire_in: 7 days
  tags:
    - qiankun
  only:
    - master
    - develop

build:sub-react:
  stage: build
  image: node:${NODE_VERSION}
  needs: ["install:sub-react", "lint"]
  script:
    - cd sub-react
    - export REACT_APP_VERSION=$(date +%Y%m%d%H%M%S)
    - npm run build
    - ls -la build/
  artifacts:
    paths:
      - sub-react/build/
    expire_in: 7 days
  tags:
    - qiankun
  only:
    - master
    - develop

# ──────────────────────────────────────────
# 阶段5:部署到 Nginx
# ──────────────────────────────────────────
deploy:
  stage: deploy
  image: alpine:latest
  needs: ["build:main", "build:sub-vue", "build:sub-react"]
  before_script:
    - apk add --no-cache rsync openssh-client
    # 配置 SSH(部署到宿主机 Nginx 目录)
    - mkdir -p ~/.ssh
    - echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
    - chmod 600 ~/.ssh/id_rsa
    - echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
  script:
    - |
      echo "========================================="
      echo "  部署 Qiankun 微前端"
      echo "========================================="

      # 部署主应用
      echo ">>> 部署主应用..."
      rsync -avz --delete main-app/dist/ ${DEPLOY_USER}@${DEPLOY_HOST}:/usr/share/nginx/html/main/

      # 部署 Vue 子应用
      echo ">>> 部署 Vue 子应用..."
      rsync -avz --delete sub-vue/dist/ ${DEPLOY_USER}@${DEPLOY_HOST}:/usr/share/nginx/html/sub-vue/

      # 部署 React 子应用
      echo ">>> 部署 React 子应用..."
      rsync -avz --delete sub-react/build/ ${DEPLOY_USER}@${DEPLOY_HOST}:/usr/share/nginx/html/sub-react/

      echo "部署完成! 🎉"
  environment:
    name: production
    url: http://192.168.1.100
  tags:
    - qiankun
  only:
    - master

# ──────────────────────────────────────────
# 阶段6:发送 IM 通知
# ──────────────────────────────────────────
notify:
  stage: notify
  image: alpine:latest
  needs: ["deploy"]
  before_script:
    - apk add --no-cache curl
  script:
    # 计算总耗时
    - |
      echo "========================================="
      echo "  发送 CI 通知到 IM 群"
      echo "========================================="

      # 获取构建耗时(秒)
      PIPELINE_DURATION=$(($(date +%s) - $(date -d "$CI_PIPELINE_CREATED_AT" +%s 2>/dev/null || echo $(date +%s))))

      # 构建通知数据
      NOTIFY_DATA=$(cat <<EOF
      {
        "project": "${CI_PROJECT_NAME}",
        "branch": "${CI_COMMIT_REF_NAME}",
        "commit": "${CI_COMMIT_SHA}",
        "author": "${GITLAB_USER_NAME:-CI}",
        "message": "${CI_COMMIT_MESSAGE}",
        "status": "success",
        "stage": "deploy",
        "duration": ${PIPELINE_DURATION},
        "url": "${CI_PROJECT_URL}/-/pipelines/${CI_PIPELINE_ID}",
        "changelog": "部署到 ${DEPLOY_HOST}"
      }
      EOF
      )

      # 发送到 Node 中间件
      curl -X POST ${NOTIFY_URL} \
        -H "Content-Type: application/json" \
        -d "$NOTIFY_DATA" \
        --connect-timeout 5 \
        --max-time 10 \
        || echo "通知发送失败,但不影响构建状态"
  when: on_success  # 仅在成功时发送
  tags:
    - qiankun
  only:
    - master

# ──────────────────────────────────────────
# 失败时也发通知
# ──────────────────────────────────────────
notify:failure:
  stage: notify
  image: alpine:latest
  needs: []  # 不需要依赖前面的 job
  before_script:
    - apk add --no-cache curl
  script:
    - |
      PIPELINE_DURATION=$(($(date +%s) - $(date -d "$CI_PIPELINE_CREATED_AT" +%s 2>/dev/null || echo $(date +%s))))
      NOTIFY_DATA=$(cat <<EOF
      {
        "project": "${CI_PROJECT_NAME}",
        "branch": "${CI_COMMIT_REF_NAME}",
        "commit": "${CI_COMMIT_SHA}",
        "author": "${GITLAB_USER_NAME:-CI}",
        "message": "${CI_COMMIT_MESSAGE}",
        "status": "failed",
        "stage": "unknown",
        "duration": ${PIPELINE_DURATION},
        "url": "${CI_PROJECT_URL}/-/pipelines/${CI_PIPELINE_ID}"
      }
      EOF
      )
      curl -X POST ${NOTIFY_URL} \
        -H "Content-Type: application/json" \
        -d "$NOTIFY_DATA" \
        --connect-timeout 5 --max-time 10 \
        || echo "通知发送失败"
  when: on_failure
  tags:
    - qiankun
  only:
    - master

8.2 GitLab CI/CD 环境变量配置

GitLab → Settings → CI/CD → Variables 中添加以下变量:

变量名 说明
SSH_PRIVATE_KEY -----BEGIN RSA PRIVATE KEY-----... 部署服务器 SSH 私钥
SSH_KNOWN_HOSTS 192.168.1.100 ssh-rsa AAA... 服务器 SSH 指纹
DEPLOY_HOST 192.168.1.100 部署目标 IP 或域名
DEPLOY_USER root 部署目标用户名
NOTIFY_URL http://192.168.1.100:3000/api/notify Node 中间件地址

九、部署服务器侧准备

9.1 创建部署目录

bash 复制代码
# 创建 Nginx 静态文件目录
sudo mkdir -p /usr/share/nginx/html/main
sudo mkdir -p /usr/share/nginx/html/sub-vue
sudo mkdir -p /usr/share/nginx/html/sub-react

# 设置权限(确保 Runner 的 deploy user 可写入)
sudo chown -R $USER:$USER /usr/share/nginx/html/

9.2 配置 SSH 免密登录(Runner → 部署目标)

bash 复制代码
# 在开发机上生成 SSH 密钥对(如果没有)
ssh-keygen -t rsa -b 4096 -C "gitlab-runner-deploy" -f ~/.ssh/gitlab_deploy

# 将公钥添加到 authorized_keys
cat ~/.ssh/gitlab_deploy.pub >> ~/.ssh/authorized_keys

# 获取私钥(用于 GitLab CI/CD 变量)
cat ~/.ssh/gitlab_deploy

# 获取 known_hosts
ssh-keyscan 192.168.1.100 >> ~/.ssh/known_hosts
cat ~/.ssh/known_hosts

9.3 启动 Node 中间件

bash 复制代码
cd ~/projects/qiankun-cicd-demo/node-middleware

# 安装依赖
npm install

# 配置环境变量
cp .env.example .env
# 编辑 .env,填入各 IM 平台的 Webhook 地址

# 开发模式启动
npm run dev

# 生产模式(推荐使用 pm2 管理进程)
npm install -g pm2
pm2 start src/index.js --name cicd-notify
pm2 save
pm2 startup  # 设置开机自启

十、完整操作流程

10.1 一次性初始化步骤

bash 复制代码
# 1. 安装 Docker
sudo apt install -y docker.io && sudo systemctl enable docker && sudo systemctl start docker

# 2. 安装并启动 GitLab
sudo docker run -d --name gitlab --restart always \
  --hostname gitlab.local -p 8080:80 \
  -v /srv/gitlab/config:/etc/gitlab \
  -v /srv/gitlab/logs:/var/log/gitlab \
  -v /srv/gitlab/data:/var/opt/gitlab \
  gitlab/gitlab-ce:latest

# 3. 安装并注册 GitLab Runner
sudo docker run -d --name gitlab-runner --restart always \
  -v /srv/gitlab-runner/config:/etc/gitlab-runner \
  -v /var/run/docker.sock:/var/run/docker.sock \
  gitlab/gitlab-runner:latest

# 注册 Runner
sudo docker exec -it gitlab-runner gitlab-runner register
# → 填入 GitLab URL 和 Token
# → 选择 docker executor
# → 默认镜像: node:20-alpine

# 4. 安装 Node.js
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
source ~/.bashrc && nvm install 20 && nvm use 20

# 5. 安装 Nginx
sudo apt install -y nginx

# 6. 创建部署目录
sudo mkdir -p /usr/share/nginx/html/{main,sub-vue,sub-react}
sudo chown -R $USER:$USER /usr/share/nginx/html/

# 7. 启动 Node 中间件
mkdir -p ~/projects && cd ~/projects
git clone http://192.168.1.100:8080/root/qiankun-cicd-demo.git
cd qiankun-cicd-demo/node-middleware
npm install && cp .env.example .env  # 编辑 .env 填入 Webhook
npm install -g pm2 && pm2 start src/index.js --name cicd-notify && pm2 save

# 8. 部署 Nginx 配置
sudo cp ~/projects/qiankun-cicd-demo/nginx/default.conf /etc/nginx/conf.d/qiankun.conf
sudo nginx -t && sudo nginx -s reload

# 9. 配置 GitLab CI/CD Variables
# 在 GitLab Web UI 中添加 SSH_PRIVATE_KEY, SSH_KNOWN_HOSTS, DEPLOY_HOST, DEPLOY_USER

10.2 日常开发流程

markdown 复制代码
1. 开发者在本地开发分支编写代码
2. git add . && git commit -m "feat: 新增xxx功能"
3. git push origin feature/xxx
4. 在 GitLab Web UI 创建 Merge Request(feature/xxx → develop)
5. MR 触发 Pipeline(install → lint → test,不执行 deploy)
6. Code Review 通过后合并到 develop
7. develop 触发完整 Pipeline(含 deploy 到测试环境)
8. 发布时创建 MR(develop → master)
9. master 合并后触发生产部署 + IM 群通知

十一、踩坑与注意事项

11.1 Qiankun 常见坑

问题 原因 解决方案
子应用加载失败 / 跨域 Vite Dev Server 未配 CORS server.headers: {'Access-Control-Allow-Origin': '*'}
子应用样式丢失 子应用打包后路径前缀不对 vite.config.jsbase: '/sub-vue/'
子应用路由 404 Nginx try_files 未配置 try_files $uri $uri/ /sub-vue/index.html
子应用 publicPath 错误 webpack/Vite 未配置公共路径 生产环境子应用 base 必须匹配 Nginx location
主应用找不到子应用入口 entry 配置的路径不对 检查主应用 entry 是否匹配 Nginx 子目录
多个子应用 JS 变量冲突 未启用沙箱 start({ sandbox: { strictStyleIsolation: true } })
子应用独立运行正常,嵌入后报错 缺少生命周期导出 检查是否导出 bootstrap/mount/unmount

11.2 GitLab CI/CD 常见坑

问题 原因 解决方案
Runner 一直 pending Runner 未注册或离线 docker exec gitlab-runner gitlab-runner verify
npm ci 报错 没有 package-lock.json npm install 生成 lock 文件再提交
SSH 部署失败 SSH 密钥未配置 检查 GitLab Variables 中 PRIVATE_KEY 格式(保留换行)
artifacts 过期 默认 30 天,太大占空间 设置 expire_in: 7 days,定期清理
构建 OOM Docker Runner 内存不足 增加 Runner 容器的内存限制
Pipeline 太慢 每次全量 npm install 使用 cache + npm ci + artifacts 传递 node_modules

11.3 Nginx 常见坑

问题 原因 解决方案
SPA 刷新 404 try_files 配置缺失 try_files $uri $uri/ /index.html
静态资源 404 alias 路径末尾少了 / location /sub-vue/alias /path/sub-vue/ 末尾斜杠必须一致
502 Bad Gateway Node 中间件未启动 pm2 list 检查进程
CORS 头未生效 Nginx add_header 被覆盖 确保子应用的 location 有独立 add_header

十二、部署后验证

12.1 验证清单

bash 复制代码
# 1. GitLab 是否正常
curl http://192.168.1.100:8080  # 应返回 GitLab 登录页

# 2. Runner 是否在线
# GitLab Web UI → Settings → CI/CD → Runners → 绿色圆点

# 3. Nginx 是否正常
curl http://192.168.1.100/          # 主应用
curl http://192.168.1.100/sub-vue/  # Vue 子应用
curl http://192.168.1.100/sub-react/ # React 子应用

# 4. Node 中间件是否正常
curl http://192.168.1.100/api/notify -X POST \
  -H "Content-Type: application/json" \
  -d '{"project":"test","status":"success","branch":"master","author":"test"}'

# 5. 全流程端到端测试
# 推送一个 commit 到 master,观察 Pipeline 是否自动执行并收到群通知

十三、参考资料

相关推荐
sheeta19981 小时前
Vue 前端基础笔记
前端·vue.js·笔记
前端那点事1 小时前
别再写垃圾组件!Vue3 如何设计「真正可复用」的高质量通用组件
前端·vue.js
卷帘依旧1 小时前
JavaScript 中的 Symbol
前端·javascript
老王以为1 小时前
Claude Code 从 GUI 到 TUI:开发者界面的范式回归
前端·人工智能·全栈
JYeontu1 小时前
正方体翻滚Loading 2.0
前端·javascript·css
llq_3501 小时前
React 组件处理 Props
前端
夫子3961 小时前
多人协同后内容丢失?一文搞懂ONLYOFFICE document.key的正确用法
前端
张元清1 小时前
React 与用户偏好:尊重用户已经在 OS 里设过的那些选项
前端·javascript·面试
RPGMZ1 小时前
RPGMZ 游戏场景全局提示框 带三秒隐藏插件
前端·javascript·游戏·rpgmz