全文约 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.js 设 base: '/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 是否自动执行并收到群通知