日期: 2026-02-10
版本: v1.0
适用范围: 基于 Wujie + Vue 3 + Vite + pnpm Monorepo 的微前端架构
目标读者: 前端架构师、DevOps 工程师、技术负责人
目录
- 一、项目架构概览
- 二、构建产物设计
- 三、代码分层与 Hook 复用规范
- 四、环境体系规划
- 五、Nginx 网关层设计
- 六、Jenkins CI/CD 流水线
- 七、Docker 容器化方案
- 八、Kubernetes 编排方案(进阶)
- 九、容灾与高可用
- 十、灰度发布与回滚
- 十一、监控与告警
- 十二、安全加固
- 十三、性能优化
- 十四、运维 SOP 手册
- 附录 A:配置文件模板
- 附录 B:故障排查 Checklist
一、项目架构概览
1.1 Monorepo 仓库结构
cmclink-web-main/ # pnpm Monorepo 根目录
├── apps/
│ └── main/ # 主应用(@cmclink/main)— Wujie Host
├── packages/
│ ├── api/ # @cmclink/api — 统一 API 层
│ ├── biz/ # @cmclink/biz — 业务组件
│ ├── eslint-config/ # @cmclink/eslint-config — ESLint 规范
│ ├── hooks/ # @cmclink/hooks — 通用 Composables
│ ├── micro-bridge/ # @cmclink/micro-bridge — 微前端通信桥
│ ├── micro-bootstrap/ # @cmclink/micro-bootstrap — 子应用启动器
│ ├── tsconfig/ # @cmclink/tsconfig — TS 配置基线
│ ├── types/ # @cmclink/types — 类型定义
│ ├── ui/ # @cmclink/ui — UI 组件库
│ ├── utils/ # @cmclink/utils — 工具函数
│ └── vite-config/ # @cmclink/vite-config — Vite 配置工厂
├── turbo.json # Turborepo 任务编排
├── pnpm-workspace.yaml # pnpm 工作区
└── package.json
1.2 微前端应用拓扑
┌─────────────────────────────────────────┐
│ Nginx 网关 │
│ (反向代理 + 静态资源) │
└──────┬──────┬──────┬──────┬──────┬───────┘
│ │ │ │ │
┌──────▼──┐ ┌─▼────┐ ┌▼─────┐ ┌───▼───┐ ┌──▼──┐
│ /micro- │ │/doc/ │ │/mkt/ │ │/ibs- │ │/api │
│ main/ │ │ │ │ │ │manage/│ │ │
│ 主应用 │ │单证 │ │营销 │ │IBS │ │后端 │
│ (Wujie │ │子应用 │ │子应用 │ │管理 │ │服务 │
│ Host) │ │ │ │ │ │子应用 │ │ │
└─────────┘ └──────┘ └──────┘ └───────┘ └─────┘
1.3 子应用注册表
| 子应用 | name | 路由前缀 | 开发端口 | 构建产物目录 | 状态 |
|---|---|---|---|---|---|
| 主应用 | main | /micro-main/ | 3000 | micro-main/ | ✅ 已迁入 |
| 单证 | doc | /doc/ | 3003 | doc/ | ✅ 已迁入 |
| IBS 管理 | ibs-manage | /ibs-manage/ | 3007 | ibs-manage/ | ✅ 已迁入 |
| 营销 | mkt | /mkt/ | 3001 | mkt/ | 🔜 待迁入 |
| 商务财务 | commerce-finance | /commerce-finance/ | 3002 | commerce-finance/ | 🔜 待迁入 |
| 操作 | operation | /operation/ | 3004 | operation/ | 🔜 待迁入 |
| 通用 | general | /general/ | 3005 | general/ | 🔜 待迁入 |
| 公共 | common | /common/ | 3006 | common/ | 🔜 待迁入 |
1.4 技术栈
| 层面 | 技术选型 |
|---|---|
| 微前端框架 | Wujie(腾讯开源,iframe + WebComponent 沙箱) |
| 前端框架 | Vue 3.5+ (Composition API + <script setup>) |
| 构建工具 | Vite 7.0+ |
| 语言 | TypeScript 5.8+ |
| Monorepo | pnpm 10 + Turborepo 2.5 |
| UI 组件库 | Element Plus 2.9+ |
| CSS | Tailwind CSS 4.1+ |
| 状态管理 | Pinia 3.0+ |
二、构建产物设计
2.1 当前问题分析
主应用构建存在双产物目录问题:
apps/main/
├── dist/ # Visualizer 插件输出 stats.html
└── micro-main/ # 实际构建产物(VITE_BASE_NAME=micro-main)
├── index.html
├── css/
├── js/
├── img/
└── favicon.svg
根因: build.outDir 由 VITE_BASE_NAME 控制(值为 micro-main),而 Visualizer 插件 filename 默认输出到 dist/stats.html,导致同时存在两个目录。
建议修复: 将 Visualizer 输出路径改为 micro-main/stats.html,或在 CI 中统一收集时忽略 dist/。
2.2 产物目录规范
核心原则: 构建产物目录名 = Vite base 路径 = Nginx location 前缀 = 子应用 entry,四者必须一致。
VITE_BASE_PATH=/micro-main/ → Vite base: '/micro-main/'
VITE_BASE_NAME=micro-main → Vite outDir: 'micro-main'
Nginx: location /micro-main/ → alias .../micro-main/
Registry: entry: '/micro-main/' → Wujie startApp url
2.3 CI 统一收集后的部署产物
deploy-artifacts/ # CI 归档目录
├── micro-main/ # 主应用
│ ├── index.html
│ ├── css/
│ ├── js/
│ └── img/
├── doc/ # 单证子应用
├── ibs-manage/ # IBS 管理子应用
├── mkt/ # 营销子应用(待迁入)
└── build-info.json # 构建元信息
2.4 构建命令矩阵
| 环境 | 命令 | 环境文件 | NODE_ENV | sourcemap | gzip |
|---|---|---|---|---|---|
| DEV | pnpm dev | .env.dev | development | ✅ | ❌ |
| SIT | pnpm build:sit | .env.sit | development | ✅ | ✅ |
| UAT | pnpm build:uat | .env.uat | production | ❌ | ✅ |
| PROD | pnpm build:prod | .env.prod | production | ❌ | ✅ |
Turborepo 全量/增量构建:
# 全量构建
pnpm build:prod
# 仅构建受 Git 变更影响的包(增量)
pnpm build:affected
三、代码分层与 Hook 复用规范
微前端架构下,代码复用需要严格分层,避免子应用间耦合。
3.1 三层 Hook 体系
┌─────────────────────────────────────────────────────────────┐
│ L1 @vueuse/core │
│ 社区通用 Composables(优先使用,不重复造轮子) │
│ useMouse, useStorage, useDark, useClipboard ... │
├─────────────────────────────────────────────────────────────┤
│ L2 @cmclink/hooks │
│ 公司级通用 Composables(跨子应用共享的业务无关逻辑) │
│ useTable, useForm, useCache, usePermission, │
│ useValidator, useExport, useWatermark │
├─────────────────────────────────────────────────────────────┤
│ L3 apps/<app>/src/hooks/ │
│ 子应用自定义 Hook(强关联当前子应用业务的定制逻辑) │
│ 主应用: useMessage, useDesign, useBpmApproval ... │
│ 子应用: useDocForm, useShipmentTrack ... │
└─────────────────────────────────────────────────────────────┘
3.2 选型决策流程
需要一个 Composable
│
├─ @vueuse/core 有现成的? ──── 是 ──→ 直接用 @vueuse/core
│ import { useXxx } from '@vueuse/core'
│
├─ 多个子应用都需要? ────────── 是 ──→ 放入 @cmclink/hooks
│ 且与具体业务无关 import { useXxx } from '@cmclink/hooks'
│
└─ 仅当前子应用需要? ────────── 是 ──→ 放入 apps/<app>/src/hooks/
或强关联当前业务 import { useXxx } from '@/hooks/xxx'
3.3 各层职责与示例
L1 — @vueuse/core(社区层)
// ✅ 直接使用 VueUse,不要自己封装
import { useStorage, useDark, useClipboard, useEventListener } from '@vueuse/core'
判断标准: VueUse 官方文档中已有的功能,直接使用,不二次封装。
L2 — @cmclink/hooks(公司基础层)
// packages/hooks/src/ 下的 Composables
import { useTable, useForm, useCache, usePermission } from '@cmclink/hooks'
当前已有:
| Hook | 职责 |
|---|---|
useTable | 表格数据管理(分页、排序、筛选) |
useForm | 表单状态管理(校验、提交、重置) |
useCache | 缓存管理(localStorage/sessionStorage 封装) |
useValidator | 通用校验规则(手机号、邮箱、身份证等) |
usePermission | 权限判断(按钮级权限控制) |
useExport | 数据导出(Excel 下载) |
useWatermark | 页面水印 |
准入标准: 至少 2 个子应用需要使用,且不依赖特定子应用的业务逻辑。
L3 — 子应用自定义 Hook(应用层)
apps/main/src/hooks/
├── business/ # 业务 Hook
│ ├── useBpmApproval.ts # BPM 审批流程(主应用特有)
│ └── useI18nLabels.ts # 国际化标签(主应用特有)
├── validation/ # 校验 Hook
│ ├── useRules.ts # 表单校验规则
│ └── useValidation.ts # 校验逻辑
└── web/ # 通用 Web Hook
├── useCache.ts # 缓存(主应用定制版)
├── useCountdown.ts # 倒计时
├── useDesign.ts # 设计系统
├── useForm.ts # 表单(主应用定制版)
├── useI18n.ts # 国际化
├── useLocale.ts # 语言切换
├── useMessage.ts # 消息提示
├── useTitle.ts # 页面标题
└── useValidator.ts # 校验器(主应用定制版)
注意: 主应用 hooks/web/useCache.ts 和 @cmclink/hooks/useCache 存在同名,应逐步将通用部分下沉到 L2,应用层仅保留定制扩展。
3.4 packages 分层全景
┌─────────────────────────────────────────────────────────────┐
│ apps/ (应用层) │
│ main / doc / ibs-manage / mkt / ... │
│ 各子应用独立的业务代码、路由、Store、自定义 Hook │
├─────────────────────────────────────────────────────────────┤
│ packages/ (基础设施层) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ hooks │ │ api │ │ biz │ │ ui │ │
│ │ 通用组合 │ │ 统一API │ │ 业务组件 │ │ UI组件库 │ │
│ │ 式函数 │ │ 层 │ │ │ │ │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ utils │ │ types │ │ micro- │ │ micro- │ │
│ │ 工具函数 │ │ 类型定义 │ │ bridge │ │bootstrap │ │
│ │ │ │ │ │ 微前端桥 │ │ 子应用 │ │
│ │ │ │ │ │ │ │ 启动器 │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ vite- │ │ eslint- │ │ tsconfig │ │
│ │ config │ │ config │ │ 基线 │ │
│ │ 构建工厂 │ │ 规范 │ │ │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────────┘
四、环境体系规划
4.1 四环境架构
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ DEV │───▶│ SIT │───▶│ UAT │───▶│ PROD │
│ 开发环境 │ │ 测试环境 │ │ 验收环境 │ │ 生产环境 │
│ 本地联调 │ │ 集成测试 │ │ 用户验收 │ │ 正式上线 │
│ 热更新 │ │ 自动部署 │ │ 手动审批 │ │ 蓝绿/灰度 │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
4.2 环境配置对照表
| 配置项 | DEV | SIT | UAT | PROD |
|---|---|---|---|---|
| 域名 | localhost:3000 | cmclink-sit:33080 | cmclink-uat | cmclink |
| NODE_ENV | development | development | production | production |
| sourcemap | ✅ | ✅ | ❌ | ❌ |
| console.log | 保留 | 保留 | 移除 | 移除 |
| debugger | 保留 | 保留 | 移除 | 移除 |
| gzip 压缩 | ❌ | ✅ | ✅ | ✅ |
4.3 环境变量管理
.env.{mode} → 提交到 Git(非敏感配置)
.env.{mode}.local → 不提交(敏感配置、个人覆盖)
敏感信息: API 密钥、证书等通过 Jenkins Credentials 或 K8s Secrets 注入,禁止硬编码。
五、Nginx 网关层设计
5.1 架构角色
Nginx 在微前端架构中承担核心网关:
- 静态资源服务 — 托管所有应用构建产物
- 反向代理 — API 请求转发到后端
- 路由分发 — URL 前缀路由到对应子应用
- SPA 回退 — 每个子应用非静态请求回退到各自
index.html - 安全防护 — CORS、CSP、HSTS 等安全头
- 性能优化 — gzip/brotli、缓存策略、HTTP/2
5.2 核心配置
# /etc/nginx/conf.d/cmclink.conf
upstream backend_api {
least_conn;
server backend-1:8080 weight=5;
server backend-2:8080 weight=5;
}
server {
listen 80;
listen 443 ssl http2;
server_name cmclink.sinolines.com.cn;
# ─── SSL ───
ssl_certificate /etc/nginx/ssl/cmclink.crt;
ssl_certificate_key /etc/nginx/ssl/cmclink.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
if ($scheme = http) {
return 301 https://$host$request_uri;
}
# ─── 安全头 ───
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# ─── gzip ───
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 1024;
gzip_types text/plain text/css text/javascript
application/javascript application/json
application/xml image/svg+xml;
root /usr/share/nginx/html;
# ─── 根路径 → 主应用 ───
location = / {
return 302 /micro-main/;
}
# ─── 主应用 ───
location /micro-main/ {
alias /usr/share/nginx/html/micro-main/;
try_files $uri $uri/ /micro-main/index.html;
# 带 hash 的静态资源:强缓存 1 年
location ~* /micro-main/(?:js|css|img|fonts)/.*\.[a-f0-9]{8}\. {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
# index.html:不缓存
location = /micro-main/index.html {
expires -1;
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
}
# ─── 子应用:单证 ───
location /doc/ {
alias /usr/share/nginx/html/doc/;
try_files $uri $uri/ /doc/index.html;
location ~* /doc/(?:js|css|img|fonts)/.*\.[a-f0-9]{8}\. {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
location = /doc/index.html {
expires -1;
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
}
# ─── 子应用:IBS 管理 ───
location /ibs-manage/ {
alias /usr/share/nginx/html/ibs-manage/;
try_files $uri $uri/ /ibs-manage/index.html;
location ~* /ibs-manage/(?:js|css|img|fonts)/.*\.[a-f0-9]{8}\. {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
location = /ibs-manage/index.html {
expires -1;
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
}
# ─── 新增子应用模板(复制此块) ───
# location /<app-name>/ {
# alias /usr/share/nginx/html/<app-name>/;
# try_files $uri $uri/ /<app-name>/index.html;
# }
# ─── API 反向代理 ───
location /admin-api/ {
proxy_pass http://backend_api;
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_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_connect_timeout 60s;
proxy_send_timeout 120s;
proxy_read_timeout 120s;
}
# ─── 健康检查 ───
location /health {
access_log off;
return 200 '{"status":"ok"}';
add_header Content-Type application/json;
}
# ─── 禁止访问隐藏文件 ───
location ~ /\. {
deny all;
}
}
5.3 缓存策略
| 资源类型 | Cache-Control | 说明 |
|---|---|---|
index.html | no-cache, no-store, must-revalidate | 每次获取最新版本 |
js/[name]-[hash].js | public, immutable, max-age=31536000 | 强缓存 1 年 |
css/[name]-[hash].css | public, immutable, max-age=31536000 | 强缓存 1 年 |
img/[name]-[hash].ext | public, immutable, max-age=31536000 | 强缓存 1 年 |
| API 响应 | no-store | 动态数据不缓存 |
六、Jenkins CI/CD 流水线
6.1 流水线架构
Git Push / MR
│
▼
┌──────────────────────────────────────────────────────┐
│ Jenkins Pipeline │
├──────────┬──────────┬──────────┬──────────┬──────────┤
│ Prepare │ Install │ Quality │ Build │ Deploy │
│ 环境准备 │ 依赖安装 │ 质量门禁 │ 构建打包 │ 部署上线 │
│ │ │ │ │ │
│ pnpm │ frozen │ type- │ turbo │ rsync / │
│ corepack │ lockfile │ check │ build │ docker / │
│ │ │ + lint │ │ k8s │
└──────────┴──────────┴──────────┴──────────┴──────────┘
6.2 Jenkinsfile
pipeline {
agent {
docker {
image 'node:22-alpine'
args '-v pnpm-store:/root/.local/share/pnpm/store'
}
}
environment {
BUILD_ENV = "${params.BUILD_ENV ?: 'sit'}"
DOCKER_REGISTRY = 'harbor.sinolines.com.cn'
DOCKER_IMAGE = "${DOCKER_REGISTRY}/cmclink/web-frontend"
}
parameters {
choice(name: 'BUILD_ENV', choices: ['sit', 'uat', 'prod'], description: '目标环境')
booleanParam(name: 'SKIP_LINT', defaultValue: false, description: '跳过 Lint(仅紧急修复)')
booleanParam(name: 'FULL_BUILD', defaultValue: false, description: '强制全量构建')
}
options {
timeout(time: 30, unit: 'MINUTES')
disableConcurrentBuilds()
buildDiscarder(logRotator(numToKeepStr: '20'))
timestamps()
}
stages {
stage('Prepare') {
steps {
sh 'corepack enable && corepack prepare pnpm@10.15.0 --activate'
sh 'node -v && pnpm -v'
}
}
stage('Install') {
steps {
sh 'pnpm install --frozen-lockfile'
}
}
stage('Quality Gate') {
when { expression { !params.SKIP_LINT } }
parallel {
stage('Type Check') { steps { sh 'pnpm type-check' } }
stage('Lint') { steps { sh 'pnpm lint' } }
}
}
stage('Build') {
steps {
script {
def cmd = params.FULL_BUILD
? "pnpm build:${BUILD_ENV}"
: "pnpm build:affected"
sh cmd
}
}
}
stage('Archive') {
steps {
sh '''
mkdir -p deploy-artifacts
[ -d apps/main/micro-main ] && cp -r apps/main/micro-main deploy-artifacts/
# 子应用产物(按需添加)
# [ -d apps/doc/doc ] && cp -r apps/doc/doc deploy-artifacts/
# [ -d apps/ibs-manage/ibs-manage ] && cp -r apps/ibs-manage/ibs-manage deploy-artifacts/
cat > deploy-artifacts/build-info.json << EOF
{
"version": "${BUILD_NUMBER}",
"env": "${BUILD_ENV}",
"branch": "${GIT_BRANCH}",
"commit": "${GIT_COMMIT}",
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
}
EOF
'''
archiveArtifacts artifacts: 'deploy-artifacts/**', fingerprint: true
}
}
stage('Docker Build') {
when { expression { BUILD_ENV in ['uat', 'prod'] } }
steps {
script {
def tag = "${BUILD_ENV}-${BUILD_NUMBER}"
sh """
docker build \
--build-arg BUILD_ENV=${BUILD_ENV} \
-t ${DOCKER_IMAGE}:${tag} \
-t ${DOCKER_IMAGE}:${BUILD_ENV}-latest \
-f deploy/Dockerfile .
docker push ${DOCKER_IMAGE}:${tag}
docker push ${DOCKER_IMAGE}:${BUILD_ENV}-latest
"""
}
}
}
stage('Deploy') {
steps {
script {
switch (BUILD_ENV) {
case 'sit':
// SIT:rsync 直接部署
sh """
rsync -avz --delete deploy-artifacts/ \
${DEPLOY_SERVER}:/data/nginx/html/
ssh ${DEPLOY_SERVER} 'nginx -t && nginx -s reload'
"""
break
case 'uat':
input message: "确认部署到 UAT?", ok: "部署"
deployDocker('uat')
break
case 'prod':
input message: "确认部署到生产?(需技术负责人审批)", ok: "部署"
deployBlueGreen()
break
}
}
}
}
stage('Health Check') {
steps {
retry(3) {
sleep 10
sh "curl -sf ${getTargetUrl(BUILD_ENV)}/health || exit 1"
}
}
}
}
post {
success { sendNotification('✅ 部署成功') }
failure { sendNotification('❌ 部署失败') }
always { cleanWs() }
}
}
// ─── 辅助函数 ───
def deployDocker(env) {
def tag = "${env}-${BUILD_NUMBER}"
sh """
ssh ${DEPLOY_SERVER} '
docker pull ${DOCKER_IMAGE}:${tag}
docker stop cmclink-web-${env} || true
docker rm cmclink-web-${env} || true
docker run -d --name cmclink-web-${env} \
--restart unless-stopped \
-p 80:80 -p 443:443 \
-v /data/ssl:/etc/nginx/ssl:ro \
${DOCKER_IMAGE}:${tag}
'
"""
}
def deployBlueGreen() {
def tag = "prod-${BUILD_NUMBER}"
sh """
ssh ${DEPLOY_SERVER} '
ACTIVE=\$(docker ps --filter name=cmclink-web-prod \
--format "{{.Names}}" | grep -o "blue\\|green" || echo "none")
TARGET=\$([ "\$ACTIVE" = "blue" ] && echo "green" || echo "blue")
PORT=\$([ "\$TARGET" = "blue" ] && echo "8081" || echo "8082")
docker pull ${DOCKER_IMAGE}:${tag}
docker stop cmclink-web-prod-\$TARGET || true
docker rm cmclink-web-prod-\$TARGET || true
docker run -d --name cmclink-web-prod-\$TARGET \
--restart unless-stopped -p \$PORT:80 \
${DOCKER_IMAGE}:${tag}
sleep 10
curl -sf http://localhost:\$PORT/health || exit 1
# 切换 upstream
sed -i "s/server 127.0.0.1:.*/server 127.0.0.1:\$PORT;/" \
/etc/nginx/conf.d/upstream.conf
nginx -t && nginx -s reload
'
"""
}
def getTargetUrl(env) {
[sit: 'https://cmclink-sit.sinolines.com.cn:33080',
uat: 'https://cmclink-uat.sinolines.com.cn',
prod: 'https://cmclink.sinolines.com.cn'][env]
}
def sendNotification(msg) {
// 企业微信 Webhook
sh """
curl -s -X POST "\${WECHAT_WEBHOOK}" \
-H 'Content-Type: application/json' \
-d '{"msgtype":"markdown","markdown":{"content":"${msg} #${BUILD_NUMBER}\\n> 环境: ${BUILD_ENV}\\n> 分支: ${GIT_BRANCH}"}}'
"""
}
6.3 增量构建
Turborepo --affected 自动分析依赖图:
packages/utils 变更 → 所有依赖它的 apps 重新构建
packages/hooks 变更 → 所有依赖它的 apps 重新构建
apps/main/src 变更 → 仅 main 重新构建
apps/doc/src 变更 → 仅 doc 重新构建
七、Docker 容器化方案
7.1 多阶段构建 Dockerfile
# deploy/Dockerfile
# ─── 阶段 1:构建 ───
FROM node:22-alpine AS builder
RUN corepack enable && corepack prepare pnpm@10.15.0 --activate
WORKDIR /app
# 先复制依赖描述(利用 Docker 层缓存)
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml turbo.json ./
COPY apps/main/package.json apps/main/
COPY packages/api/package.json packages/api/
COPY packages/biz/package.json packages/biz/
COPY packages/eslint-config/package.json packages/eslint-config/
COPY packages/hooks/package.json packages/hooks/
COPY packages/micro-bridge/package.json packages/micro-bridge/
COPY packages/micro-bootstrap/package.json packages/micro-bootstrap/
COPY packages/tsconfig/package.json packages/tsconfig/
COPY packages/types/package.json packages/types/
COPY packages/ui/package.json packages/ui/
COPY packages/utils/package.json packages/utils/
COPY packages/vite-config/package.json packages/vite-config/
RUN pnpm install --frozen-lockfile
COPY . .
ARG BUILD_ENV=prod
RUN pnpm build:${BUILD_ENV}
# ─── 阶段 2:运行 ───
FROM nginx:1.27-alpine AS runtime
RUN apk add --no-cache curl
COPY deploy/nginx/nginx.conf /etc/nginx/nginx.conf
COPY deploy/nginx/conf.d/ /etc/nginx/conf.d/
# 复制构建产物
COPY --from=builder /app/apps/main/micro-main /usr/share/nginx/html/micro-main
# COPY --from=builder /app/apps/doc/doc /usr/share/nginx/html/doc
# COPY --from=builder /app/apps/ibs-manage/ibs-manage /usr/share/nginx/html/ibs-manage
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl -sf http://localhost/health || exit 1
EXPOSE 80 443
CMD ["nginx", "-g", "daemon off;"]
7.2 Docker Compose(SIT 环境)
# deploy/docker-compose.yml
version: '3.8'
services:
web:
build:
context: .
dockerfile: deploy/Dockerfile
args:
BUILD_ENV: ${BUILD_ENV:-sit}
container_name: cmclink-web
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./deploy/nginx/conf.d:/etc/nginx/conf.d:ro
- ./deploy/ssl:/etc/nginx/ssl:ro
- nginx-logs:/var/log/nginx
deploy:
resources:
limits:
cpus: '1.0'
memory: 256M
# 日志收集(可选)
filebeat:
image: elastic/filebeat:8.12.0
volumes:
- nginx-logs:/var/log/nginx:ro
depends_on:
- web
volumes:
nginx-logs:
7.3 镜像优化
| 优化项 | 措施 | 效果 |
|---|---|---|
| 多阶段构建 | builder → runtime | ~1.5GB → ~30MB |
| Alpine 基础镜像 | nginx:1.27-alpine | 基础镜像 ~7MB |
| 层缓存 | 先 COPY package.json | 依赖不变时跳过安装 |
| .dockerignore | 排除 node_modules/.git | 减少构建上下文 |
# deploy/.dockerignore
node_modules
.git
*.md
.env*.local
.turbo
八、Kubernetes 编排方案(进阶)
8.1 Deployment
# deploy/k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: cmclink-web
namespace: cmclink
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: cmclink-web
template:
metadata:
labels:
app: cmclink-web
spec:
containers:
- name: web
image: harbor.sinolines.com.cn/cmclink/web-frontend:prod-latest
ports:
- containerPort: 80
resources:
requests: { cpu: 100m, memory: 64Mi }
limits: { cpu: 500m, memory: 256Mi }
livenessProbe:
httpGet: { path: /health, port: 80 }
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet: { path: /health, port: 80 }
initialDelaySeconds: 5
periodSeconds: 10
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- { key: app, operator: In, values: [cmclink-web] }
topologyKey: kubernetes.io/hostname
8.2 Service + Ingress
apiVersion: v1
kind: Service
metadata:
name: cmclink-web-svc
namespace: cmclink
spec:
selector: { app: cmclink-web }
ports: [{ port: 80, targetPort: 80 }]
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: cmclink-web-ingress
namespace: cmclink
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts: [cmclink.sinolines.com.cn]
secretName: cmclink-tls
rules:
- host: cmclink.sinolines.com.cn
http:
paths:
- path: /
pathType: Prefix
backend:
service: { name: cmclink-web-svc, port: { number: 80 } }
8.3 HPA 自动扩缩
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: cmclink-web-hpa
namespace: cmclink
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: cmclink-web
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource: { name: cpu, target: { type: Utilization, averageUtilization: 70 } }
九、容灾与高可用
9.1 容灾架构
┌─────────────────────────┐
│ DNS (智能解析) │
└─────┬───────────┬────────┘
│ │
┌────────▼──┐ ┌────▼────────┐
│ 主机房 │ │ 灾备机房 │
│ (Active) │ │ (Standby) │
│ Web x 3 │ │ Web x 2 │
└────────────┘ └─────────────┘
9.2 多层容灾策略
| 层级 | 策略 | RTO |
|---|---|---|
| 应用层 | 多副本 (replicas ≥ 3) | 秒级 |
| 节点层 | Pod 反亲和(分散到不同节点) | 秒级 |
| 机房层 | 双机房热备 + DNS 切换 | 分钟级 |
| 区域层 | 异地灾备 + OSS 备份 | 小时级 |
9.3 静态资源备份
#!/bin/bash
# deploy/scripts/sync-to-oss.sh
# 构建产物同步到 OSS 作为灾备
OSS_BUCKET="cmclink-web-backup"
DEPLOY_DIR="/usr/share/nginx/html"
DATE=$(date +%Y%m%d_%H%M%S)
# 同步当前版本
ossutil sync ${DEPLOY_DIR}/ oss://${OSS_BUCKET}/current/ --delete
# 归档历史版本
ossutil cp oss://${OSS_BUCKET}/current/ oss://${OSS_BUCKET}/archive/${DATE}/ -r
十、灰度发布与回滚
10.1 发布策略对比
| 策略 | 风险 | 回滚速度 | 资源开销 | 适用场景 |
|---|---|---|---|---|
| 滚动更新 | 中 | 分钟级 | 低 | 日常发布 |
| 蓝绿部署 | 低 | 秒级 | 高(双倍) | 重大版本 |
| 灰度/金丝雀 | 最低 | 秒级 | 中 | 高风险变更 |
10.2 Nginx 灰度配置
# 基于百分比的灰度
split_clients "${remote_addr}" $upstream_group {
10% canary; # 10% 流量走灰度版
* stable;
}
upstream stable { server 127.0.0.1:8081; }
upstream canary { server 127.0.0.1:8082; }
server {
location / {
proxy_pass http://$upstream_group;
}
}
10.3 回滚方案
# Docker 回滚
docker stop cmclink-web-prod && docker rm cmclink-web-prod
docker run -d --name cmclink-web-prod -p 80:80 \
harbor.sinolines.com.cn/cmclink/web-frontend:prod-42 # 指定历史版本号
# K8s 回滚
kubectl rollout undo deployment/cmclink-web -n cmclink # 上一版本
kubectl rollout undo deployment/cmclink-web --to-revision=3 # 指定版本
# 静态文件回滚(从 OSS 恢复)
ossutil sync oss://cmclink-web-backup/archive/20260210_143000/ \
/usr/share/nginx/html/ -r
nginx -s reload
十一、监控与告警
11.1 监控体系
┌─────────────────────────────────────────────────────────┐
│ Grafana 大盘 │
├──────────┬──────────┬──────────┬──────────┬──────────────┤
│ 基础设施 │ 应用性能 │ 业务指标 │ 日志分析 │ 告警 │
│ CPU/内存 │ FCP/LCP │ PV/UV │ 错误日志 │ 企业微信 │
│ 容器状态 │ TTFB/CLS │ 子应用加载 │ 访问日志 │ 邮件/短信 │
└──────────┴──────────┴──────────┴──────────┴──────────────┘
Prometheus Sentry ELK Stack Alertmanager
11.2 关键告警规则
| 指标 | 阈值 | 级别 |
|---|---|---|
| 5xx 错误率 | > 5% 持续 5min | Critical |
| P99 响应时间 | > 3s 持续 10min | Warning |
| Pod 频繁重启 | > 0 次/15min | Critical |
| 磁盘空间 | < 10% | Warning |
十二、安全加固
12.1 安全防护矩阵
| 层面 | 措施 | 优先级 |
|---|---|---|
| 传输层 | HTTPS + HSTS + TLS 1.2+ | P0 |
| 应用层 | CSP + X-Frame-Options + X-Content-Type-Options | P0 |
| 认证层 | JWT + HttpOnly Cookie + CSRF Token | P0 |
| 网络层 | WAF + DDoS 防护 | P1 |
| 构建层 | 依赖审计 (pnpm audit) + 镜像扫描 (Trivy) | P1 |
| 运维层 | 最小权限 + 审计日志 + 密钥轮转 | P2 |
12.2 CSP 配置(微前端特殊处理)
add_header Content-Security-Policy "
default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob: https:;
font-src 'self' data:;
connect-src 'self' https://cmclink*.sinolines.com.cn;
frame-src 'self';
" always;
⚠️ Wujie 使用 iframe 沙箱,
frame-src 'self'必须;子应用 JS 在闭包中执行需要'unsafe-eval'。
十三、性能优化
13.1 构建产物优化
| 优化项 | 措施 | 效果 |
|---|---|---|
| 代码分割 | manualChunks 拆分 vendor | 首屏 JS -60% |
| Tree Shaking | ESM + sideEffects | 移除未使用代码 |
| gzip 压缩 | vite-plugin-compression (≥20KB) | 体积 -70% |
| CSS 优化 | Tailwind CSS purge | CSS -90% |
13.2 运行时优化
| 优化项 | 措施 | 效果 |
|---|---|---|
| 子应用预加载 | 分级 preloadApp(高频立即、低频延迟 3s) | 首次打开 < 1s |
| 预执行 + 保活 | exec: true + alive: true | 瞬间打开 |
| fiber 模式 | fiber: true(默认) | 不阻塞主应用渲染 |
| 路由懒加载 | () => import(...) | 按需加载页面 |
13.3 Nginx 性能调优
worker_processes auto;
worker_rlimit_nofile 65535;
worker_connections 4096;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
open_file_cache max=10000 inactive=60s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
十四、运维 SOP 手册
14.1 日常发布 SOP
1. MR → Code Review → 合并 develop
2. Jenkins 自动触发 SIT 构建部署
3. QA 在 SIT 验证
4. 合并 release → 触发 UAT 构建 → 人工审批
5. 合并 main → 触发 PROD 构建 → 蓝绿/灰度部署
6. 健康检查 → 监控观察 30 分钟
7. 清理旧版本
14.2 紧急回滚 SOP
1. 确认是前端问题
2. 通知技术负责人
3. 执行回滚(K8s rollout undo / Docker 切换镜像 / OSS 恢复)
4. 验证回滚成功
5. 事后复盘
14.3 子应用独立发布 SOP
微前端核心优势 — 子应用可独立发布,不影响其他应用:
# 1. 仅构建该子应用
turbo run build:prod --filter=@cmclink/doc
# 2. 仅替换该子应用静态资源
rsync -avz apps/doc/doc/ server:/usr/share/nginx/html/doc/
# 3. 无需重启 Nginx(静态文件替换即生效)
# 4. 主应用和其他子应用完全不受影响
14.4 新增子应用 Checklist
-
apps/<name>/创建子应用目录 -
vite.config.ts使用@cmclink/vite-config工厂 -
.env.*配置VITE_BASE_PATH/VITE_BASE_NAME -
micro-bridge/registry.ts注册子应用 - Nginx 添加
location /<name>/块 - Dockerfile 添加
COPY --from=builder指令 - Jenkinsfile 添加产物收集
-
turbo.jsonoutputs 添加产物目录 - 测试:独立运行 + 微前端模式
附录 A:配置文件模板
A.1 完整部署目录结构
deploy/ # 新增部署配置目录
├── Dockerfile
├── .dockerignore
├── docker-compose.yml
├── nginx/
│ ├── nginx.conf
│ └── conf.d/
│ └── cmclink.conf
├── k8s/
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── ingress.yaml
│ └── hpa.yaml
└── scripts/
├── sync-to-oss.sh
├── health-check.sh
└── rollback.sh
A.2 Nginx 容器主配置
# deploy/nginx/nginx.conf
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 4096;
multi_accept on;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# JSON 日志格式(便于 ELK 采集)
log_format json escape=json
'{"time":"$time_iso8601",'
'"addr":"$remote_addr",'
'"req":"$request",'
'"status":$status,'
'"size":$body_bytes_sent,'
'"rt":$request_time,'
'"ua":"$http_user_agent"}';
access_log /var/log/nginx/access.log json;
sendfile on;
tcp_nopush on;
keepalive_timeout 65;
client_max_body_size 50m;
gzip on;
gzip_vary on;
gzip_comp_level 6;
gzip_min_length 1024;
gzip_types text/plain text/css text/javascript
application/javascript application/json image/svg+xml;
include /etc/nginx/conf.d/*.conf;
}
附录 B:故障排查 Checklist
B.1 页面白屏
- Nginx 运行状态:
nginx -t && curl localhost/health - index.html 存在:
ls /usr/share/nginx/html/micro-main/index.html - JS/CSS 资源 404:DevTools → Network
- base 路径一致:
VITE_BASE_PATH= Nginxlocation= 子应用entry
B.2 子应用加载失败
- 子应用资源可访问:
curl https://domain/doc/index.html - CORS 头正确:
Access-Control-Allow-Origin: * - 注册表 entry 与部署路径一致
- 浏览器控制台沙箱错误
B.3 部署后缓存未更新
- index.html 的
Cache-Control为no-cache - CDN 已刷新缓存
- 无 Service Worker 缓存
- 强制刷新:
Ctrl + Shift + R
B.4 容器启动失败
- 容器日志:
docker logs cmclink-web - Nginx 配置:
docker exec cmclink-web nginx -t - 端口占用:
netstat -tlnp | grep 80 - 磁盘空间:
df -h
文档维护: 本方案书应随架构演进持续更新。每次重大变更(新增子应用、变更部署方式、升级基础设施)后,请同步更新对应章节。