完善架构,修改很多地方

This commit is contained in:
编码猿
2025-10-04 00:08:33 +08:00
parent 3c66faf9a7
commit ff925d458a
32 changed files with 576 additions and 1602 deletions

View File

@@ -9,6 +9,10 @@
本案例中 **[主应用]** 采取的就是OOP风格当然如果你不熟悉上面的写法那么可以在
【其他模块】中使用Vue2Vue3都行但仍然需要遵守DDD的规范进行开发
## 架构图
![架构图](./doc/jgt.png)
## 开发日记
- [x] 基于DDD理念改造
@@ -39,6 +43,7 @@
- [docker](https://www.docker.com/)
- [drone](https://drone.cool/)
- [nacos](https://nacos.io/?spm=5238cd80.47ee59c.0.0.189fcd36fF0ca9)
- [traefik](https://traefik.io/)
- [nginx](https://nginx.org/)
- [Vue3](https://cn.vuejs.org/)
- [Jsx](https://legacy.reactjs.org/docs/introducing-jsx.html)
@@ -68,7 +73,7 @@
└── README.md # 帮助文档
```
** DDD前端分层目录**
**DDD前端分层目录**
```txt
src/
@@ -228,39 +233,21 @@ export default class Header extends TSX<Props>()(Vue) implements Props {
}
```
## ~~【失效】运行 & 打包~~
## 部署
```bash
# 1 下载代码
git clone https://gitee.com/bmycode/dddmicro-frontend.git
需要后端或运维参与,请注意咨询阅读文档,大致步骤如下:
# 2 运行后端
cd back-end # 进入 后端 文件夹
deno install # 安装依赖
deno run dev # 运行后端
**全局准备工作:**
# 3 运行前端
cd front-end # 进入 前端 文件夹
pnpm install # 会同时为 所有模块 安装依赖
pnpm add -g concurrently # 全局安装 并发命令执行 插件,局部安装也行,随你。
pnpm run all # 构建 app-product 和 app-shared 并启动预览,并开发阶段启动 main-app 模块
# 注意:命令跑完后 ctrl + c 停止,重跑一次!!因为子模块内命令并发执行预览跑在构建之前,解决办法很简单,后面再搞。
pnpm run all
# 打开 main-app 主模块访问地址即可:[http://localhost:1300/](http://localhost:1300/)
- 1: 准备 >=8台Linux服务器并提前安装配置好Docker确保网络可用建议安装 [1panel](https://1panel.cn/)
- 2: 在1台Linux服务器上部署公司自己的 Docker Registry 镜像存储库
- 3: 准本 N台运行 前端各模块 的Linux服务器并提前组成N个Docker Swarm 集群,以供前端各模块部署使用
- 4: 准本 >=3台运行 Nacos服务中心 的Linux服务器并提前组成Docker Swarm 集群,提前运行好,确保服务可用
- [Nacos 部署文档](./docker/nacos-cluster/README.md)
- 5: 准本 一台 运行 traefik 网关的Linux服务器提前运行好确保服务可用
- [Traefik 部署文档](./docker/traefik/README.md)
# 4 部署上线
pnpm run build:main # 构建并预览主应用预览打包后的主应用没问题ctrl + c 停止。
# 子模块在 第三步 已经打包好了,不需要重新打包了。
# 上传部署 各模块内的 dist/ 文件夹到服务器上,注意修改 vite.config.ts 内 plugins.federation.remotes.xxx 的地址!
# 后期会实现集中管理 地址配置,并根据环境变量动态加载!!
**前端工作:**
```
## 问 & 答
**问: 为什么vue项目要抛弃SFC写法而尝试jsx/tsx方式来编码**
> https://juejin.cn/post/6911175470255964174
**问:不会 vue3 jsx 语法,怎么办?**
> 答https://juejin.cn/post/7141674726434439176
详细请看 **front-end/xxx/README.md** 内的帮助文档!!!

BIN
doc/jgt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 989 KiB

51
doc/思路整理.md Normal file
View File

@@ -0,0 +1,51 @@
## 遇到的问题
尝试将app-product 模块进行集群部署上线的时候我们会编写dockerfile对项目进行docker build打包
dockerfile 其中命令:
```bash
RUN npx vite build --base=/app-product/
```
正常流程是:
vite build 会使用项目中的 vite.config.ts进行前端项目的构建产生 dist 文件夹基于nginx提供给用户访问
但这时候,我们在 vite.config.ts 使用自己开发的插件 vite-plugin-nacos 将前端注册到服务中心
因为和服务中心链接后,插件会一直保持心跳导致无法断开,此时 `vite build` 已经 产生 dist了但是因为心跳阻塞了docker build 的后续执行,导致项目无法打包成镜像,也就无法开展后续流程进行集群部署。
## 新的改变!!
所有微前端都不需要进行服务注册,因为前端由浏览器访问,注册实例毫无意义,无法保持心跳,就无法检查服务的健康!
## 新的架构
<!-- 在 实现 **新的改变** 之前,需要熟悉现有系统的架构和设计,如下:
外网:
main-app是整个项目的入口和壳应用
网关traefik
内网:
app-product 商品模块
app-shared 共享模块 -->
局域网
所有模块之间,通过 配置发现 来获取 所依赖 远程模块 的 实际网址
外网
模块通过 网关 反向代理来访问 所依赖 远程模块
## 新的架构带来的新的问题
局域网模块 之间通过 配置发现也会和服务注册一样和Nacos集群保持链接导致 vite build 无法退出。。。。
## 如何解决??

View File

@@ -18,14 +18,15 @@ services:
volumes:
- mysql-data:/var/lib/mysql
- ./mysql-schema.sql:/docker-entrypoint-initdb.d/mysql-schema.sql
command:
--character-set-server=utf8mb4
--collation-server=utf8mb4_unicode_ci
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
ports:
- "3306:3306"
- target: 3306
published: 3306
protocol: tcp
mode: host # 数据库通常使用 host 模式,避免路由网格开销
# 健康检查,确保数据库启动完成后再启动 Nacos
healthcheck:
test: [ "CMD", "mysqladmin" ,"ping", "-h", "localhost" ]
test: [ "CMD", "mysqladmin", "ping", "-h", "localhost" ]
interval: 5s
timeout: 10s
retries: 10
@@ -50,11 +51,26 @@ services:
- nacos-logs:/home/nacos/logs
# 暴露 Nacos 所需的端口
ports:
- "8080:8080"
- "7848:7848"
- "8848:8848"
- "9848:9848"
- "9849:9849"
- target: 8080
published: 8080
protocol: tcp
mode: ingress # Web 控制台使用路由网格实现负载均衡
- target: 7848
published: 7848
protocol: tcp
mode: ingress # 集群通信端口
- target: 8848
published: 8848
protocol: tcp
mode: ingress # 主要服务端口,使用路由网格
- target: 9848
published: 9848
protocol: tcp
mode: ingress # gRPC 端口
- target: 9849
published: 9849
protocol: tcp
mode: ingress # gRPC 端口
# 使用环境变量文件
environment:
- MODE=cluster
@@ -88,7 +104,6 @@ services:
update_config:
parallelism: 1
delay: 30s
# 资源限制(可选,可根据你的服务器配置调整)
resources:
limits:
@@ -106,7 +121,10 @@ services:
volumes:
- ./prometheus/prometheus-cluster.yaml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"
- target: 9090
published: 9090
protocol: tcp
mode: ingress # 监控服务使用路由网格
deploy:
replicas: 1
restart_policy:
@@ -116,12 +134,14 @@ services:
networks:
- nacos-net
# Grafana 服务
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
- target: 3000
published: 3000
protocol: tcp
mode: ingress # Grafana 使用路由网格
deploy:
# 通常 Grafana 单实例即可
replicas: 1
@@ -139,4 +159,4 @@ volumes:
networks:
nacos-net:
driver: overlay
driver: overlay

View File

@@ -1,2 +1,10 @@
## traefik 网关
部署在公网的 traefik 网关代码放到Linux服务器然后根据实际情况修改 `dynamic.yml` 文件,
然后在根目录执行命令即可:
```bash
docker-compose down && docker-compose up -d
docker compose down && docker compose up -d
# 或者这个命令
# docker compose down && docker compose up -d
```

View File

@@ -1,5 +1,7 @@
http:
routers:
# 共享模块的 路由 拦截 配置
app-shared-router:
rule: Host(`nacos.ddd.com`) && PathPrefix(`/app-shared`)
service: app-shared-service
@@ -9,8 +11,9 @@ http:
tracing: true
traceVerbosity: detailed
entryPoints:
- web
- web
# 商品模块的 路由 拦截 配置
app-product-router:
rule: Host(`nacos.ddd.com`) && PathPrefix(`/app-product`)
service: app-product-service
@@ -20,9 +23,9 @@ http:
tracing: true
traceVerbosity: detailed
entryPoints:
- web
- web
# 使用域名访问前端main-app
# 使用域名访问前端 main-app 主模块
main-ddd-router:
rule: Host(`main.ddd.com`)
service: main-ddd-service
@@ -34,7 +37,7 @@ http:
entryPoints:
- web
# Nacos 集群 管理后台的统一访问域名
# Nacos 集群 管理后台的统一 访问域名
nacosui-ddd-router:
rule: Host(`nacosui.ddd.com`)
service: nacosui-ddd-service
@@ -44,7 +47,7 @@ http:
tracing: true
traceVerbosity: detailed
entryPoints:
- web
- web
# trafik 网关 代理 内网 nacos 集群 的地址
nacos-ddd-router:
@@ -56,7 +59,7 @@ http:
tracing: true
traceVerbosity: detailed
entryPoints:
- web
- web
# trafik 网关的管理后台的域名
trafikui-ddd-router:
@@ -71,7 +74,7 @@ http:
- web
services:
services:
app-shared-service:
loadBalancer:
servers:
@@ -80,12 +83,14 @@ http:
app-product-service:
loadBalancer:
servers:
- url: http://192.168.31.101:1301
- url: http://192.168.139.19:1301
- url: http://192.168.139.120:1301
- url: http://192.168.139.242:1301
main-ddd-service:
loadBalancer:
servers:
- url: http://192.168.31.101:1300
- url: http://192.168.31.101:1300
nacosui-ddd-service:
loadBalancer:

View File

@@ -6,7 +6,7 @@
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"axios": "^1.6.8",
"axios": "^1.12.2",
"js-yaml": "^4.1.0"
},
"keywords": [],

View File

@@ -0,0 +1,14 @@
node_modules
dist
.git
.DS_Store
*.log
docker/ # 排除 docker 配置文件夹本身
.editorconfig
.eslintrc.js
.prettierrc
.vscode
.idea
*.swp
*.swo

View File

@@ -1,7 +1,7 @@
# 请求协议 http or https建议线上配置证书后使用https
VITE_PREFIX=http://
# Nacos 配置中心 ip:端口 | 域名
# 内网 中的 Nacos 配置中心 ip:端口 | 域名
#VITE_NACOS_ADDRESS=cluster.nacos.com
VITE_NACOS_ADDRESS=192.168.139.84:8848

View File

@@ -1,16 +1,17 @@
# 请求协议 http or https建议线上配置证书后使用https
VITE_PREFIX=http://
# Nacos 配置中心 ip:端口 | 域名
# 内网 中的 Nacos 配置中心 ip:端口 | 域名
#VITE_NACOS_ADDRESS=cluster.nacos.com
VITE_NACOS_ADDRESS=192.168.139.84:8848
# 注册到 Nacos 的服务名称(微前端模块名称
VITE_SERVER_NAME=app-product
# 注册到 Nacos 的端口,也是前端运行的端口
VITE_SERVER_PORT=1301
#
# 说明:项目要调用哪些 微前端 模块
# 格式VITE_MODEL_XXXX_NAME=微前端模块名称(模块 的 开发者提供)
@@ -19,3 +20,4 @@ VITE_SERVER_PORT=1301
# shared 共享资源模块
VITE_MODEL_SHARED_NAME=app-shared

View File

@@ -1,10 +1,63 @@
npm install
npm run build
npm run serve
## app-product 商品模块(微前端模块)
从项目中拆分出来,用于演示的微前端模块,主要实现 商品相关功能。本案例演示中提供:
- 调用 微前端 app-shared 模块
- 调接口,获取商品首页的数据和展示
- 调接口,点击商品列表进入商品详情页
构建与部署:
## 运行
- 在构建生产环境时,先构建主模块,并将其产出(特别是 remoteEntry.js 和对应的资产文件)部署到服务器,并获取其 URL。
- 然后,在子模块的 vite.config.ts 中,将 remotes 的 URL 更新为生产环境主模块 remoteEntry.js 的绝对 URL。
- 最后再构建子模块。
下面文档分为**开发**和**上线**两部分进行说明!
### 开发
```bash
# 安装依赖
npm install
# 开发阶段 运行 项目
# PS注意此命令运行的项目【无法】被其他 微前端模块 使用,仅用于本地独立开发,独立测试
npm run dev
# 启动 实时打包 & 实时预览
# PS注意此命令运行的项目【可以】被其他 微前端模块 调用,可用于本地开发和微前端联合开发
npm run dep
```
### 上线
**docker/ 文件夹说明**
- `app-product.yml` - Dcoker Swarm 集群部署配置
- `deploy-swarm.sh` - 自动化部署脚本
- `docker-compose.yml` - 开发阶段容器编排
- `Dockerfile` - 打包构建前端代码为镜像
- `nginx.conf` - 镜像内前端被访问的Nginx配置
前端工作:
- 1: 编辑 前端 .env.development 和 .env.production 环境变量(只需修改一次)
- 2: 确认或编辑 docker/Dockerfiledocker/nginx.conf
- 3: 编辑 docker/deploy-swarm.sh
还有很多细节未展开,后面再详细说,大致确认无误后。
在前端集群的管理机上进入项目根目录,执行下面命令部署:
```bash
# 运行自动化集群部署脚本
./docker/deploy-swarm.sh
# --- 其他命令 ---
# 删除之前的部署(可选)
docker stack rm app-product
# 查看 app-product 集群运行情况
docker stack ps app-product
# 持续观察服务副本数是否达到预期
watch docker stack services app-product
```

View File

@@ -0,0 +1,21 @@
FROM node:24-alpine AS builder
WORKDIR /app
COPY package.json ./
# 直接使用 npm install 并解决依赖问题
RUN npm install --no-audit --no-fund --legacy-peer-deps
# 拷贝源码并构建
COPY . .
# 【前端修改】如果要以子路径 /app-product/ 部署,请用 base 指定前缀,同 .env中VITE_SERVER_NAME
RUN npx vite build --base=/app-product/
FROM nginx:alpine
# 构建产物放到 /usr/share/nginx/html/app-product
COPY --from=builder /app/dist /usr/share/nginx/html/app-product
# 覆盖 Nginx 配置,支持 SPA 子路径
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -0,0 +1,39 @@
version: "3.8" # Docker Compose 文件版本3.8 支持 Swarm 模式的所有功能
services:
app-product:
image: app-product:latest
ports:
- target: 80 # 容器内部端口
published: 1301 # 宿主机对外暴露端口
protocol: tcp # 协议类型
mode: ingress # 模式:使用路由网格
deploy:
replicas: 3 # 副本数量,在集群中运行 3 个容器实例
placement:
max_replicas_per_node: 1 # 每个 Swarm 节点最多运行 1 个副本,确保高可用性
restart_policy:
condition: any # 重启条件:任何情况下容器退出都重启
delay: 5s # 重启延迟 5 秒
max_attempts: 3 # 最大重启尝试次数
update_config: # 滚动更新配置
parallelism: 1 # 并行更新数量:一次只更新 1 个容器
delay: 10s # 更新间隔:每个容器更新后等待 10 秒再更新下一个
failure_action: rollback # 更新失败时自动回滚到上一版本
order: start-first # 更新顺序:先启动新容器,确认健康后再停止旧容器
rollback_config: # 回滚配置
parallelism: 1 # 回滚时并行数量
delay: 5s # 回滚延迟
resources: # 资源限制
limits: # 资源上限
cpus: "0.50" # 最多使用 0.5 个 CPU 核心
memory: "256M" # 最多使用 256MB 内存
reservations: # 资源预留
cpus: "0.25" # 预留 0.25 个 CPU 核心
memory: "128M" # 预留 128MB 内存
networks:
- frontend # 连接到 frontend 网络
networks:
frontend: # 定义 frontend 网络
driver: overlay # 使用 overlay 网络驱动,支持跨主机通信

View File

@@ -0,0 +1,100 @@
#!/bin/bash
# deploy-swarm.sh
set -e
# 仓库用户名
USER_NAME="bmy"
# 仓库密码
USER_PASSWORD="lb714500."
# 仓库地址
REGISTRY="registry.bmycode.help"
# 镜像名称
IMAGE_NAME="${REGISTRY}/app-product"
# tag 标签
TAG="v$(date +%Y%m%d-%H%M%S)"
# 完整路径
FULL_IMAGE="${IMAGE_NAME}:${TAG}"
# 工作服务器列表(多个服务器)
WORKER_HOSTS=("192.168.139.120" "192.168.139.242")
WORKER_USER="root"
WORKER_PASSWORD="lb714500."
echo "=== 1 登录私有镜像仓库 ==="
if ! echo "${USER_PASSWORD}" | docker login ${REGISTRY} -u ${USER_NAME} --password-stdin; then
echo "错误: 无法登录到私有仓库 ${REGISTRY}"
exit 1
fi
echo "=== 2 构建 app-product 镜像 ==="
docker build \
-f docker/Dockerfile \
-t ${FULL_IMAGE} \
-t ${IMAGE_NAME}:latest \
.
echo "=== 3 推送镜像到私有仓库 ==="
docker push ${FULL_IMAGE}
docker push ${IMAGE_NAME}:latest
echo "=== 4 在工作服务器上拉取最新镜像 ==="
for WORKER_HOST in "${WORKER_HOSTS[@]}"; do
echo ""
echo "正在连接到工作服务器: ${WORKER_HOST}"
if sshpass -p "${WORKER_PASSWORD}" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${WORKER_USER}@${WORKER_HOST} "
echo '在工作服务器 ${WORKER_HOST} 上登录私有仓库...'
if echo '${USER_PASSWORD}' | docker login ${REGISTRY} -u ${USER_NAME} --password-stdin; then
echo '拉取最新镜像...'
docker pull ${FULL_IMAGE}
docker pull ${IMAGE_NAME}:latest
echo '镜像拉取完成'
echo '清理旧的 app-product 镜像...'
# 删除除了本次 FULL_IMAGE 和 latest 之外的所有 app-product 镜像
docker images ${IMAGE_NAME} --format 'table {{.Repository}}:{{.Tag}}' | \
grep -v '${FULL_IMAGE}' | \
grep -v '${IMAGE_NAME}:latest' | \
grep '${IMAGE_NAME}' | \
xargs -r docker rmi -f || true
else
echo '错误: 无法在工作服务器上登录私有仓库'
exit 1
fi
"; then
echo "✓ 服务器 ${WORKER_HOST} 镜像拉取成功"
else
echo "✗ 服务器 ${WORKER_HOST} 镜像拉取失败"
exit 1
fi
done
echo "=== 5 更新 docker-compose 文件中的镜像标签 ==="
sed "s|image: app-product:latest|image: ${FULL_IMAGE}|g" docker/app-product.yml > docker/app-product-deploy.yml
echo "=== 6 部署到 Swarm 集群 ==="
docker stack deploy -c docker/app-product-deploy.yml app-product
echo "=== 7 恢复原始配置文件 ==="
rm -f docker/app-product-deploy.yml
echo "=== 8 清理本地多余镜像 ==="
# 清理本地:删除除了本次 FULL_IMAGE 和 latest 之外的所有 app-product 镜像
echo "清理本地旧的 app-product 镜像..."
docker images ${IMAGE_NAME} --format 'table {{.Repository}}:{{.Tag}}' | \
grep -v "${FULL_IMAGE}" | \
grep -v "${IMAGE_NAME}:latest" | \
grep "${IMAGE_NAME}" | \
xargs -r docker rmi -f || true
# 清理悬空镜像
docker image prune -f
echo "=== 9 验证部署状态 ==="
docker stack ps app-product
echo "=== 部署完成 ==="

View File

@@ -0,0 +1,13 @@
version: "3.8"
services:
app-product:
build:
context: ..
dockerfile: docker/Dockerfile
image: app-product:1.0.0
container_name: app-product
ports:
- "1301:80"
restart: unless-stopped

View File

@@ -0,0 +1,13 @@
server {
listen 80;
server_name *.ddd.com;
location /app-product/ {
alias /usr/share/nginx/html/app-product/;
try_files $uri $uri/ /app-product/index.html;
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
}
}

View File

@@ -8,31 +8,32 @@
},
"scripts": {
"dev": "vite",
"dep": "concurrently \"npm run build\" \"npm run preview\"",
"dep": "concurrently \"vite build --watch\" \"sleep 5 && npm run preview\"",
"preview": "vite preview --port 1301 --strictPort",
"build": "vite build --watch",
"build": "vite build",
"type-check": "vue-tsc --build"
},
"dependencies": {
"less": "^4.4.1",
"pinia": "^3.0.3",
"vite-plugin-nacos": "link:../vite-plugin-nacos",
"vue": "^3.5.21",
"vite-plugin-nacos": "^1.1.3",
"vue": "^3.5.22",
"vue-router": "^4.5.1"
},
"devDependencies": {
"@element-plus/icons-vue": "^2.3.2",
"@originjs/vite-plugin-federation": "^1.4.1",
"@tsconfig/node22": "^22.0.2",
"@types/node": "^22.18.6",
"@types/node": "^24.6.1",
"@vitejs/plugin-vue": "^6.0.1",
"@vitejs/plugin-vue-jsx": "^5.1.1",
"@vue/tsconfig": "^0.7.0",
"element-plus": "^2.11.3",
"@vue/tsconfig": "^0.8.1",
"element-plus": "^2.11.4",
"npm-run-all2": "^8.0.4",
"rollup-plugin-visualizer": "^6.0.3",
"typescript": "~5.8.3",
"vite": "^7.1.6",
"typescript": "~5.9.3",
"vite": "^7.1.7",
"vite-federation": "^3.4.4",
"vite-plugin-vue-devtools": "^8.0.2",
"vue-tsc": "^3.0.7"
"vue-tsc": "^3.1.0"
}
}

View File

@@ -17,7 +17,7 @@ export default defineComponent({
<h2></h2>
<ul>
<li>1</li>
<li>2</li>
<li>2</li>
</ul>
<p></p>
</div>

View File

@@ -2,7 +2,7 @@ import { defineConfig, loadEnv } from 'vite';
import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx'
import vueDevTools from 'vite-plugin-vue-devtools'
import federation from '@originjs/vite-plugin-federation';
import federation from 'vite-federation';
import { fileURLToPath, URL } from "node:url";
import { visualizer } from 'rollup-plugin-visualizer';
import { reg, init } from 'vite-plugin-nacos';
@@ -10,9 +10,13 @@ import { reg, init } from 'vite-plugin-nacos';
export default defineConfig(async ({ mode }) => {
const env = loadEnv(mode, process.cwd());
const { VITE_MODEL_SHARED_URL } = await init(env)
// console.log("VITE_MODEL_SHARED_URL: ", VITE_MODEL_SHARED_URL);
return {
plugins: [
reg(),
federation({
name: env.VITE_SERVER_NAME,
remotes: {

File diff suppressed because it is too large Load Diff

View File

@@ -7,24 +7,24 @@
"license": "ISC",
"scripts": {
"dev": "vite",
"dep": "concurrently \"npm run build\" \"npm run preview\"",
"dep": "concurrently \"vite build --watch\" \"npm run preview\"",
"preview": "vite preview --port 1333 --strictPort",
"build": "vite build --watch",
"build": "vite build",
"type-check": "vue-tsc --build"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.2",
"@originjs/vite-plugin-federation": "1.3.8",
"axios": "^1.12.2",
"element-plus": "^2.11.3",
"element-plus": "^2.11.4",
"vite-plugin-nacos": "link:../vite-plugin-nacos",
"vue": "^3.5.21"
"vue": "^3.5.22"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.1",
"@vitejs/plugin-vue-jsx": "^5.1.1",
"rollup-plugin-visualizer": "^6.0.3",
"typescript": "~5.8.3",
"vite": "^7.1.6"
"typescript": "~5.9.3",
"vite": "^7.1.7",
"vite-federation": "^3.4.4"
}
}
}

View File

@@ -1,5 +1,5 @@
import { defineConfig, loadEnv, UserConfig } from 'vite'
import federation from "@originjs/vite-plugin-federation";
import federation from "vite-federation";
// @ts-ignore
import vueJsx from '@vitejs/plugin-vue-jsx'
@@ -17,6 +17,7 @@ export default defineConfig(async ({ mode }): Promise<UserConfig> => {
reg(),
federation({
name: env.VITE_SERVER_NAME,
transformFileTypes: ['.css', '.less'],
exposes: {
"./config": "./src/config/index.ts",
"./infra": "./src/infra/index.ts",

View File

@@ -1,12 +1,17 @@
# 请求协议 http or https建议线上配置证书后使用https
VITE_PREFIX=http://
# Nacos 配置中心 ip:端口 | 域名
VITE_NACOS_ADDRESS=cluster.nacos.com
#VITE_NACOS_ADDRESS=192.168.139.84:8848
#
# Nacos 集群的地址
# 注意:
# 1: 内网微前端模块直接填内网Nacos IP:端口 就行
# 2: main-app位于公网填 网关的公网地址
#
# 网关地址
VITE_NACOS_ADDRESS=nacos.ddd.com:80
# VITE_NACOS_ADDRESS=192.168.139.84:8848
# traefik 网关的地址
VITE_TRAEFIK_ADDRESS=192.168.31.101
# 注册到 Nacos 的服务名称(微前端模块名称)
VITE_SERVER_NAME=main-app

View File

@@ -13,20 +13,20 @@
"dependencies": {
"pinia": "^3.0.3",
"vite-plugin-nacos": "link:../vite-plugin-nacos",
"vue": "^3.5.21",
"vue": "^3.5.22",
"vue-facing-decorator": "^4.0.1",
"vue-router": "^4.5.1"
},
"devDependencies": {
"@element-plus/icons-vue": "^2.3.2",
"@originjs/vite-plugin-federation": "^1.4.1",
"@types/node": "^24.5.2",
"@types/node": "^24.6.1",
"@vue3-oop/plugin-vue-jsx": "^1.5.1",
"element-plus": "^2.11.3",
"element-plus": "^2.11.4",
"rollup-plugin-visualizer": "^6.0.3",
"tslib": "^2.8.1",
"typescript": "~5.8.3",
"vite": "^7.1.6",
"typescript": "~5.9.3",
"vite": "^7.1.7",
"vite-federation": "^3.4.4",
"vite-plugin-vue-devtools": "^8.0.2"
}
}
}

View File

@@ -2,7 +2,7 @@ import path from 'path'
import { defineConfig, loadEnv, ConfigEnv, mergeConfig } from 'vite'
import vueJsx from '@vue3-oop/plugin-vue-jsx'
import federation from '@originjs/vite-plugin-federation';
import federation from 'vite-federation';
import { reg, init } from 'vite-plugin-nacos';
import { visualizer } from 'rollup-plugin-visualizer';

View File

@@ -1,6 +1,6 @@
{
"name": "vite-plugin-nacos",
"version": "1.1.0",
"version": "1.1.3",
"description": "Vite 插件实现Nacos服务注册与发现插件用于模块联邦",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@@ -8,17 +8,18 @@
"dist"
],
"scripts": {
"build": "tsc -w",
"prepublishOnly": "npm run build"
"build": "tsc",
"prepublishOnly": "npm run build",
"push": "npm run build && npm publish"
},
"dependencies": {
"chalk": "^5.6.2",
"nacos": "^2.6.0"
},
"devDependencies": {
"@types/node": "^24.5.2",
"typescript": "~5.8.3",
"vite": "^7.1.6"
"@types/node": "^24.6.1",
"typescript": "~5.9.3",
"vite": "^7.1.7"
},
"keywords": [
"nacos",
@@ -27,4 +28,4 @@
],
"author": "",
"license": "MIT"
}
}

View File

@@ -1,56 +1,96 @@
// 从插件实现文件导入
export * from './vite-plugin-nacos';
export * from './utils';
import { log } from 'console';
import { nacosServiceManage } from './nacosServiceManage';
import { mergeConfig } from 'vite';
export const init = async (env: Record<string, string>) => {
const nacos = await nacosServiceManage.create({
serverList: env.VITE_NACOS_ADDRESS,
serviceName: env.VITE_SERVER_NAME,
port: Number(env.VITE_SERVER_PORT)
});
const serverList = env.VITE_NACOS_ADDRESS;
const serviceName = env.VITE_SERVER_NAME;
const port = Number(env.VITE_SERVER_PORT);
if (env.VITE_SERVER_NAME === "main-app") {
log("不需要服务发现,进行配置发现...")
const baseUrl = await nacos.getConfig("baseUrl", "DEFAULT_GROUP")
const baseUrlConfig = baseUrl ? JSON.parse(baseUrl) : {};
baseUrlConfig.httpTimeOut = await nacos.getConfig("httpTimeOut", "DEFAULT_GROUP")
// 将新配置处理一下,需要 JSON.stringify
const customizeEnv: Record<string, Record<string, string>> = { define: {} };
Object.keys(baseUrlConfig).forEach(key => {
customizeEnv.define[`import.meta.env.VITE_${key.toUpperCase()}`] = JSON.stringify(baseUrlConfig[key]);
});
// 将老配置处理一下,需要 JSON.stringify
const existingDefine: Record<string, Record<string, string>> = { define: {} }
Object.keys(env).forEach(key => {
existingDefine.define[`import.meta.env.${key}`] = JSON.stringify(env[key])
})
return mergeConfig(existingDefine, customizeEnv)
} else {
log("进行服务发现,获取依赖模块的地址...")
let modelName = []
for (const key in env) {
if (key.includes("VITE_MODEL_")) modelName.push(env[key])
}
const modelUrl: Record<string, string> = {}
if (modelName.length !== 0) {
for (const val of modelName) modelUrl[`VITE_MODEL_${val.replace("app-", "").toUpperCase()}_URL`] = await nacos.find(val)
}
log("modelUrl: ", modelUrl)
return modelUrl
if (!serverList || !serviceName || Number.isNaN(port)) {
throw new Error('[vite-plugin-nacos] 缺少必要环境变量VITE_NACOS_ADDRESS / VITE_SERVER_NAME / VITE_SERVER_PORT');
}
const nacos = await nacosServiceManage.create({
serverList,
serviceName,
port
});
const isMain = serviceName === 'main-app';
if (isMain) {
console.log('[vite-plugin-nacos] 不需要服务发现,进行配置发现...');
// 并发获取配置并稳健解析
const [baseUrlRaw, httpTimeOut] = await Promise.all([
nacos.getConfig('baseUrl', 'DEFAULT_GROUP'),
nacos.getConfig('httpTimeOut', 'DEFAULT_GROUP')
]);
let baseUrlConfig: Record<string, any> = {};
if (baseUrlRaw) {
try {
baseUrlConfig = JSON.parse(baseUrlRaw);
} catch (e) {
console.warn('[vite-plugin-nacos] baseUrl 配置不是有效 JSON忽略解析使用空对象');
baseUrlConfig = {};
}
}
if (typeof httpTimeOut !== 'undefined' && httpTimeOut !== null) {
baseUrlConfig.httpTimeOut = httpTimeOut;
}
// 将新配置处理成 define需要 JSON.stringify
const customizeEnv = Object.keys(baseUrlConfig).reduce<Record<string, string>>((acc, key) => {
acc[`import.meta.env.VITE_${key.toUpperCase()}`] = JSON.stringify(baseUrlConfig[key]);
return acc;
}, {});
// 将现有 env 处理成 define需要 JSON.stringify
const existingDefine = Object.keys(env).reduce<Record<string, string>>((acc, key) => {
acc[`import.meta.env.${key}`] = JSON.stringify(env[key]);
return acc;
}, {});
// 使用 mergeConfig 合并 define 字段
const merged = mergeConfig({ define: existingDefine }, { define: customizeEnv });
await closeAllNacos(serviceName);
return merged;
}
// 非主模块:进行服务发现
console.log('[vite-plugin-nacos] 进行 配置发现,获取依赖模块的地址...');
const modelNames: string[] = [];
for (const key in env) {
if (key.includes('VITE_MODEL_') && env[key]) {
modelNames.push(env[key]);
}
}
// 去重
const uniqueModelNames = Array.from(new Set(modelNames));
const modelUrl: Record<string, string> = {};
if (uniqueModelNames.length > 0) {
const results = await Promise.all(uniqueModelNames.map(async (val) => {
const url = await nacos.getConfig(val, 'DEFAULT_GROUP');
const envKey = `VITE_MODEL_${val.replace('app-', '').toUpperCase()}_URL`;
return [envKey, url] as const;
}));
for (const [k, v] of results) modelUrl[k] = v;
}
}
console.log('[vite-plugin-nacos] modelUrl: ', modelUrl);
await closeAllNacos("other-model");
return modelUrl;
}
/** 手动关闭所有 Nacos 客户端(提供给外部在需要时调用) */
export async function closeAllNacos(serviceName: string) {
await nacosServiceManage.destroyAll();
}

View File

@@ -37,12 +37,12 @@ export class nacosServiceManage {
const configKey = this.getConfigKey(option);
if (this.instanceMap.has(configKey)) {
logger.log(`[nacos-federation] 已存在 ${ option.serviceName } 的Nacos实例直接复用`);
logger.log(`[nacos-federation] 已存在 ${option.serviceName} 的Nacos实例直接复用`);
return this.instanceMap.get(configKey)!;
}
const instance = new nacosServiceManage(option);
await instance.initClient();
// await instance.initClient();
await instance.initConfig();
this.instanceMap.set(configKey, instance);
@@ -65,7 +65,7 @@ export class nacosServiceManage {
*/
private static getConfigKey(option: RegisterOptions): string {
const namespace = option.namespace || 'public';
return `${ option.serverList }-${ namespace }-${ option.serviceName }`;
return `${option.serverList}-${namespace}-${option.serviceName}`;
}
/**
@@ -90,7 +90,7 @@ export class nacosServiceManage {
async getConfig(dataId: string, group: string = "DEFAULT_GROUP") {
const res = await this.configClient.getConfig(dataId, group)
console.log()
console.log(`${ chalk.blue('[nacos-cluster-federation] 配置发现:') }\n ${ chalk.green('➜') } ${ chalk.green(dataId) } 的配置为: ${ chalk.green(res) }`)
console.log(`${chalk.blue('[nacos-cluster-federation] 配置发现:')}\n ${chalk.green('➜')} ${chalk.green(dataId)} 的配置为: ${chalk.green(res)}`)
return res;
}
@@ -103,7 +103,7 @@ export class nacosServiceManage {
const instances = await this.nacosClient.getAllInstances(serviceName);
// console.log("instances: ", instances);
if (instances.length === 0) {
throw new Error(`[nacos-federation] 未发现服务 ${ serviceName } 的实例`);
throw new Error(`[nacos-federation] 未发现服务 ${serviceName} 的实例`);
}
return formatServiceUrl(instances[0].ip, instances[0].port);
}
@@ -128,7 +128,7 @@ export class nacosServiceManage {
await this.nacosClient.registerInstance(serviceName, { port, ip })
logger.log()
logger.log(chalk.blue("[nacos-cluster-federation] 服务注册:"))
logger.log(` ${ chalk.green('➜') } ${ chalk.green(serviceName) } (${ formatServiceUrl(ip, port) }) 已注册到配置中心!`);
logger.log(` ${chalk.green('➜')} ${chalk.green(serviceName)} (${formatServiceUrl(ip, port)}) 已注册到配置中心!`);
}
/**
@@ -136,16 +136,44 @@ export class nacosServiceManage {
*/
public async destroy() {
const configKey = nacosServiceManage.getConfigKey(this.initOptions);
// 注销Nacos服务实例
const { serviceName, port } = this.initOptions;
const ip = getLocalIP();
// @ts-ignore
await this.nacosClient.deregisterInstance(serviceName, { ip, port });
logger.log(`[nacos-federation] 服务 ${ serviceName } 已从Nacos注销`);
// 注销 Nacos 命名客户端(如已初始化)
try {
if (this.nacosClient) {
const { serviceName, port } = this.initOptions;
const ip = getLocalIP();
// @ts-ignore
await this.nacosClient.deregisterInstance(serviceName, { ip, port });
// @ts-ignore
if (typeof this.nacosClient.close === 'function') {
// @ts-ignore
await this.nacosClient.close();
}
logger.log(`[nacos-federation] 服务 ${serviceName} 已从Nacos注销`);
}
} catch (e) {
logger.warn('[nacos-federation] deregister warn:', e);
}
// 关闭配置客户端(如已初始化)
try {
if (this.configClient && typeof (this.configClient as any).close === 'function') {
await (this.configClient as any).close();
}
} catch (e) {
logger.warn('[nacos-federation] configClient close warn:', e);
}
// 从缓存中移除实例
nacosServiceManage.instanceMap.delete(configKey);
}
/** 销毁所有实例:用于构建结束/进程退出时清理 */
public static async destroyAll(): Promise<void> {
const tasks: Promise<any>[] = [];
for (const [, instance] of nacosServiceManage.instanceMap) {
tasks.push(instance.destroy().catch(err => logger.warn('[nacos-federation] destroyAll warn:', err)));
}
await Promise.allSettled(tasks);
nacosServiceManage.instanceMap.clear();
}
}

View File

@@ -1,6 +1,5 @@
import { loadEnv, Plugin, UserConfig } from 'vite';
import { NacosConfigClient, NacosNamingClient } from 'nacos';
import { getLocalIP, dependModel, formatServiceUrl } from './utils';
import { getLocalIP } from './utils';
import chalk from 'chalk';
const logger = console;
@@ -39,51 +38,12 @@ export function reg(options?: Partial<RegisterOptions>): Plugin {
strictPort: true,
cors: true
},
// TODO dist 上线到服务器后http://192.168.31.101:1301/ 会出问题
// http(https)://前端运行的服务器所在ip/前端模块名
base: `${env.VITE_PREFIX}${getLocalIP()}:${env.VITE_SERVER_PORT}/${baseUrl}`
// base: `${env.VITE_PREFIX}${getLocalIP()}:${env.VITE_SERVER_PORT}/${baseUrl}`
base: `/${baseUrl}`
};
},
async configResolved(config) {
const env = config.env;
// const registerOptions: RegisterOptions = {
// serverList: env.VITE_NACOS_ADDRESS || options?.serverList!,
// serviceName: env.VITE_SERVER_NAME || options?.serviceName!,
// port: Number(env.VITE_SERVER_PORT || options?.port),
// namespace: options?.namespace || 'public'
// };
//
// // 验证必要配置
// if (!registerOptions.serverList) {
// throw new Error('[nacos-cluster-federation] 缺少Nacos服务列表配置请设置VITE_NACOS_ADDRESS环境变量或插件选项');
// }
//
// if (!registerOptions.serviceName) {
// throw new Error('[nacos-cluster-federation] 缺少服务名称配置请设置VITE_SERVER_NAME环境变量或插件选项');
// }
// if (!registerOptions.port) {
// throw new Error('[nacos-cluster-federation] 缺少服务端口配置请设置VITE_SERVER_PORT环境变量或插件选项');
// }
// // 初始化Nacos实例
// nacosInstance = await nacosServiceManage.create(registerOptions);
//
// const baseUrl = await nacosInstance.getConfig("baseUrl", "DEFAULT_GROUP")
// const baseUrlConfig = baseUrl ? JSON.parse(baseUrl) : {};
//
// baseUrlConfig.httpTimeOut = await nacosInstance.getConfig("httpTimeOut", "DEFAULT_GROUP")
//
//
// const customizeEnv: Record<string, string> = {};
// Object.keys(baseUrlConfig).forEach(key => {
// const envKey = `import.meta.env.VITE_${ key.toUpperCase() }`;
// customizeEnv[envKey] = JSON.stringify(baseUrlConfig[key]);
// });
//
// // @ts-ignore
// Object.assign(config.define, customizeEnv);
// Logs(env)
},
async closeBundle() {
// // 服务关闭时注销Nacos实例

View File

@@ -1,5 +0,0 @@
import { nacosServiceManage } from "./src";
const main = async () => {
const nacos = new nacosServiceManage.create({})
}

View File

@@ -6,10 +6,11 @@
"author": "",
"license": "ISC",
"scripts": {
"update": "pnpm -r update --latest",
"init": "pnpm install",
"all": "concurrently \"pnpm run build\" \"pnpm run dev:main\"",
"build": "pnpm --parallel --filter app-product --filter app-shared dep",
"dev:main": "pnpm --filter main-app dev",
"build:main": "pnpm --filter main-app dep"
}
}
}