Compare commits

..

29 Commits

Author SHA1 Message Date
编码猿
9432116a00 修改 动态配置 2025-10-07 21:48:31 +08:00
编码猿
5fda36b268 更新文档 2025-10-07 21:47:12 +08:00
编码猿
84820b1e9b 移除nginx 2025-10-07 21:45:03 +08:00
编码猿
550bf0039b 移除 app-shared 为独立模块 2025-10-07 21:14:15 +08:00
编码猿
2653800c3f 从主项目中删除 app-product,抽离为独立仓库存储!! 2025-10-07 04:37:32 +08:00
编码猿
cb8f7a4682 修改cicd为gitea action,简化操作 2025-10-07 00:59:16 +08:00
编码猿
7fb0f60ec0 实现cicd的案例,等待集成 2025-10-05 05:46:17 +08:00
编码猿
ff925d458a 完善架构,修改很多地方 2025-10-04 00:08:33 +08:00
编码猿
3c66faf9a7 【重要】traefik 配置 2025-09-30 10:01:58 +08:00
编码猿
a3ba6c2944 【重要】重构 并实现新的架构 2025-09-30 10:00:10 +08:00
编码猿
04754a1f77 实现 nacos 的分布式集群部署配置 2025-09-29 05:07:42 +08:00
编码猿
ced7662d98 优化 nacos-federation 2025-09-26 02:21:04 +08:00
编码猿
174af7efc3 优化全局样式和 element ui框架,修改nacos-federation,增加打包依赖分析等 2025-09-26 01:50:49 +08:00
编码猿
537c7ca60e 添加全局样式和 element ui框架 2025-09-24 23:56:55 +08:00
编码猿
21bf47c8a1 修改帮助文档 2025-09-23 12:08:40 +08:00
编码猿
ac5f544819 增加控制台日志,方便访问各种服务 2025-09-23 12:05:54 +08:00
编码猿
258318eee8 将vite.config.ts中的base 改到插件 nacos-federation中实现,简化代码 2025-09-23 11:35:02 +08:00
编码猿
1f5c0556a3 修改base,增加资源来源地址,修复资源错误加载的问题 2025-09-23 11:20:41 +08:00
编码猿
1791889441 更新文档 2025-09-23 03:32:47 +08:00
编码猿
84c4e8bdc7 修改 nacos-federation 插件,实现 接口请求基地址从 配置中心获取,并注入到环境变量中 2025-09-23 03:26:10 +08:00
编码猿
cbc2c25cf4 修改忽略文件 2025-09-23 01:02:19 +08:00
编码猿
b29001c8c3 修改忽略文件 2025-09-23 01:01:08 +08:00
编码猿
c8a9bfd77e 修改忽略文件 2025-09-23 01:00:34 +08:00
编码猿
135839d6e2 封装自动生成 traefik 代理配置的 nodejs 插件代码 2025-09-23 00:59:56 +08:00
编码猿
bfe5f57378 插件nacos-federation增加对vite版本的支持,简化代码! 2025-09-22 04:22:03 +08:00
编码猿
dabb91ef1e 实现 nacos + traefik,改了很多东西!!! 2025-09-22 02:27:53 +08:00
编码猿
e038fdb965 增加任务 2025-09-20 02:26:42 +08:00
编码猿
32d40e1fa3 @vitejs/plugin-vue-jsx 不支持装饰器语法,所以换成@vitejs/plugin-vue-jsx插件,不需要任何配置就可以了 2025-09-19 13:12:17 +08:00
编码猿
b3718c4097 修改插件的实现和文档 2025-09-19 13:01:26 +08:00
104 changed files with 2511 additions and 2754 deletions

2
.gitignore vendored
View File

@@ -1,7 +1,7 @@
.DS_Store
node_modules
dist
docker/nacos-cluster/standalone-logs/*
pnpm-lock.yaml
package-lock.json

208
README.md
View File

@@ -9,6 +9,33 @@
本案例中 **[主应用]** 采取的就是OOP风格当然如果你不熟悉上面的写法那么可以在
【其他模块】中使用Vue2Vue3都行但仍然需要遵守DDD的规范进行开发
## 架构图
![架构图](./doc/jgt.png)
## CI/CD 自动化集群部署
![cicd](./doc/newci.png)
## 开发日记
- [x] 基于DDD理念改造
- [x] 拆分领域模块为联邦模块,实现微前端
- [x] 基于 Vue3+OOP+JSX 改造项目代码
- [x] 基于 Nacos 开发 nacos-federation 插件,实现各模块的服务注册&发现,统一管理远程地址
- [x] 修改 nacos-federation 插件实现其成为vite的插件进一步简化代码
- [x] 修改 nacos-federation 插件,实现 接口请求基地址从 配置中心获取,并注入到环境变量中
- [x] 配置 Traefik实现微前端网关限流与熔断重试IP 白名单,压缩
- [x] 封装自动生成 Traefik 代理配置的 nodejs 插件代码
- [x] 修改base增加资源来源地址修复资源错误加载的问题
- [x] 将vite.config.ts中的base 改到插件 nacos-federation中实现简化代码
- [x] 增加控制台日志,方便访问各种服务
- [x] pnpm 统一管理集中项目依赖,运行&构建等
- [x] 领域模块实现 docker 自动化构建部署上线
- [ ] Deno集成GraphQL
- [ ] ....
- [ ] 爬快手api实现一个完整项目测试脚手架不足
## 技术栈:
- [DDD领域驱动设计](https://zh.wikipedia.org/zh-cn/%E9%A0%98%E5%9F%9F%E9%A9%85%E5%8B%95%E8%A8%AD%E8%A8%88)
@@ -18,6 +45,8 @@
- [GraphQL](https://graphql.org/)
- [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)
@@ -31,22 +60,28 @@
```txt
.
├── back-end # 【后端】基于Deno写的后端 测试接口,仅供测试
├── doc # 一些markdown笔记
├── front-end # 前端 微服务 项目
│ ├── app-product # 独立的【商品】领域模块
── app-shared # 独立的 共享资源 模块
│ ├── .... # 实际项目还会有更多其他的领域模块
── main-app # 主应用(入口应用)
└── README.md # 帮助文档
├── back-end # 【后端】基于Deno写的后端 测试接口,仅供测试
├── doc # 一些markdown笔记
├── docker # 微服务系统下的一些工具
│ ├── nacos # 动态服务发现配置管理平台
── traefik # 微服务网关
├── front-end # 前端 微服务 项目
── app-product # 独立的【商品】领域模块
│ ├── app-shared # 独立的 共享资源 模块
│ ├── nacos-federation # 开发的Nacos服务注册与发现的插件简化优雅代码
│ ├── .... # 实际项目还会有更多其他的领域模块
│ └── main-app # 主应用(入口应用)
├── package.json # pnpm的顶级配置文件可以控制模块运行相同命令
├── pnpm-workspace.yaml # 管理多个包的依赖关系
└── README.md # 帮助文档
```
**main-app 目录:**
**DDD前端分层目录:**
```txt
src/
├── domains/ # 【核心】领域层 - 所有业务领域模块
│ └── product/ # 订单领域模块(app-product
│ └── product/ # app-product模块】订单领域模块
│ ├── domain/ # 领域模型(充血模型)
│ │ ├── entities/ # 实体Product, ProductLineItem
│ │ ├── value-objects/ # 值对象Money, Address
@@ -75,7 +110,7 @@ src/
│ ├── router/ # 订单相关的页面级Vue组件
│ ├── composables/ # 订单相关的Vue组合式函数
│ └── index.ts # 订单功能模块的出口
├── shared/ # 共享资源
├── shared/ # 【app-shared模块】共享资源
│ ├── infra(infrastructure) # 基础设施层
│ │ ├── http/
│ ├── lib/ # 第三方库的封装/工具函数
@@ -91,8 +126,9 @@ src/
```tsx
// 首页 页面
import { Autobind, Component, Link, Mut, VueComponent } from 'vue3-oop'
import Header from '@/components/Header'
import { Component, Ref, Setup, Vue } from 'vue-facing-decorator'
import Header from '../components/Header'
import { RouteLocationNormalizedLoaded, Router, useRoute, useRouter } from "vue-router";
interface ListItemType {
id: number
@@ -100,44 +136,52 @@ interface ListItemType {
}
@Component()
export default class Home extends VueComponent {
export default class Home extends Vue {
@Mut()
count: number = 1
counter: number = 1;
@Mut()
text: string = "Home props 传给 Header 的字符串"
@Ref
readonly refH1!: HTMLDivElement
@Setup((props, ctx) => useRouter())
router!: Router
@Setup((props, ctx) => useRoute())
route!: RouteLocationNormalizedLoaded
@Mut()
newList: ListItemType[] = [
{ id: 1, title: '新闻1' },
{ id: 2, title: '新闻2' },
{ id: 3, title: '新闻3' },
]
@Link() H1?: HTMLDivElement
@Autobind()
async addCount() {
this.count++
this.text = "22222"
console.log(this.H1)
addCount() {
// console.log("ref dom: ", this.refH1)
// this.router.push({ name: 'about' })
// this.route.query
this.counter++
}
testHtml() {
return <p>测试html jsx 函数</p>
}
render() {
return (
<div class="home" ref="H1">
<h1>首页</h1>
<p onClick={ this.addCount }>count响应式变量(点我):{ this.count }</p>
<div class="home">
<h1 ref="refH1">首页</h1>
<p onClick={ this.addCount }>counter 响应式变量(点我):{ this.counter }</p>
<Header
v-slots={ {
list={ this.newList }
text={ "测试" }>
{ {
default: () => <div class="default">父到子 default 插槽</div>,
haveName: () => <div class="haveName">父到子 具名 插槽</div>,
item: (val) => <li key={ val.id }>{ val.title }</li>
item: (val: ListItemType) => <li key={ val.id }>{ val.title }</li>
} }
list={ this.newList }
text={ this.text }>
</Header>
{ this.testHtml }
</div>
)
}
@@ -146,53 +190,43 @@ export default class Home extends VueComponent {
```tsx
// 头部组件
import { Component, ComponentProps, Hook, useProps, VueComponent } from 'vue3-oop'
import { JSX } from "vue/jsx-runtime";
import { VNodeChild } from "vue";
import { Component, Prop, TSX, Vue } from 'vue-facing-decorator'
interface Props {
text?: string
list?: ListItemType[]
}
interface ListItemType {
id: number
title: string
}
interface HeaderProps {
text?: string;
list: ListItemType[]
slots: {
default: () => JSX.Element;
haveName: () => JSX.Element;
item: (val: ListItemType) => VNodeChild;
}
}
@Component()
export default class Header extends VueComponent<HeaderProps> {
export default class Header extends TSX<Props>()(Vue) implements Props {
// required 的 true or false 需要和 类型中 text的? 对应
static defaultProps: ComponentProps<HeaderProps> = {
text: { default: '默认数值', required: true },
list: { default: () => [] }
};
@Prop({ default: '默认数值' })
text!: string;
@Hook('Mounted')
mounted() {
console.log("this: ", this);
console.log('Header 组件的 mounted 生命周期...')
@Prop()
list!: ListItemType[];
created() {
console.log("created: ", this)
}
render() {
const { text, list } = useProps<HeaderProps>();
return (
<div class="header">
头部组件
<p>{ text }</p>
{ this.context.slots.default?.() }
{ this.context.slots.haveName?.() }
<p>{ this.text }</p>
{ this.$slots.default?.() }
{ this.$slots.haveName?.() }
<ul>
{
list.map(
this.list.map(
item =>
this.context.slots.item?.(item)
this.$slots.item?.(item)
)
}
</ul>
@@ -202,49 +236,29 @@ export default class Header extends VueComponent<HeaderProps> {
}
```
## 开发任务
## 部署
- [x] 基于DDD理念改造
- [x] 基于 vite-plugin-federation 拆分领域模块为联邦模块,实现微前端
- [x] 基于Vue3+OOP+JSX改造项目代码
- [ ] 领域模块实现 docker 自动化构建部署上线
- [ ] Deno集成GraphQL
- [ ] ....
- [ ] 爬快手api实现一个完整项目测试脚手架不足
需要后端或运维参与,请注意咨询阅读文档,大致步骤如下:
## 运行 & 打包
**全局准备工作:**
```bash
# 1 下载代码
git clone https://gitee.com/bmycode/dddmicro-frontend.git
- 1: 准备 >=8台Linux服务器并提前安装配置好Docker确保网络可用建议安装 [1panel](https://1panel.cn/)
# 2 运行后端
cd back-end # 进入 后端 文件夹
deno install # 安装依赖
deno run dev # 运行后端
- 2: 在1台Linux服务器上部署公司自己的 Docker Registry 镜像存储库
- 3: 在1台Linux服务器上部署公司自己 Gitea 仓库
# 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/)
- 4: 准本 N台运行 前端各模块 的Linux服务器并提前组成N个Docker Swarm 集群,以供前端各模块部署使用
# 4 部署上线
pnpm run build:main # 构建并预览主应用预览打包后的主应用没问题ctrl + c 停止。
# 子模块在 第三步 已经打包好了,不需要重新打包了。
- 5: 准本 >=3台运行 Nacos服务中心 的Linux服务器并提前组成Docker Swarm 集群,提前运行好,确保服务可用
- [Nacos 部署文档](./docker/nacos-cluster/README.md)
# 上传部署 各模块内的 dist/ 文件夹到服务器上,注意修改 vite.config.ts 内 plugins.federation.remotes.xxx 的地址!
# 后期会实现集中管理 地址配置,并根据环境变量动态加载!!
- 6: 准本 一台 运行 traefik 网关的Linux服务器提前运行好确保服务可用
- [Traefik 部署文档](./docker/traefik/README.md)
```
**前端工作:**
## 问 & 答
详细请看 各前端模块 存储库内的 帮助文档!!!
**问: 为什么vue项目要抛弃SFC写法而尝试jsx/tsx方式来编码**
> https://juejin.cn/post/6911175470255964174
- [商品模块 app-product](https://gitee.com/bmycode/app-product)
- [共享模块 app-shared](https://gitee.com/bmycode/app-shared)
**问:不会 vue3 jsx 语法,怎么办?**
> 答https://juejin.cn/post/7141674726434439176

View File

@@ -1,82 +1,91 @@
import { App, staticFiles, cors } from "fresh";
import { App, cors, staticFiles } from "fresh";
import { define, type State } from "./utils.ts";
export const app = new App<State>();
app.use(cors({
origin: "*",
allowHeaders: ["*"],
allowMethods: ["POST", "GET", "OPTIONS"],
exposeHeaders: ["Content-Length", "X-Kuma-Revision"],
maxAge: 600,
credentials: true,
origin: "*",
allowHeaders: ["*"],
allowMethods: ["POST", "GET", "OPTIONS"],
exposeHeaders: ["Content-Length", "X-Kuma-Revision"],
maxAge: 600,
credentials: true,
}))
app.use(staticFiles());
// 模拟产品数据
const mockProductsList = [
{
id: 1,
name: "Xiaomi Buds 5 Pro",
description: "高音质无线蓝牙耳机续航可达30小时",
price: 1199.19,
imageUrl: "https://cdn.cnbj0.fds.api.mi-img.com/b2c-shopapi-pms/pms_1740638541.0557913.png",
stock: 2000,
categoryId: 1,
},
{
id: 2,
name: "机械键盘MK71pro",
description: "青轴机械键盘,全键无冲",
price: 899.00,
imageUrl: "https://cdn.cnbj0.fds.api.mi-img.com/b2c-shopapi-pms/67D4F43CD4C647AD8F45892D6567E14A.png",
stock: 1000,
categoryId: 1,
},
{
id: 3,
name: "Xiaomi Watch S4 Sport",
description: "多功能智能手表支持心率监测和GPS",
price: 1699.00,
imageUrl: "https://cdn.cnbj0.fds.api.mi-img.com/b2c-shopapi-pms/pms_1721233615.78223053.png",
stock: 200,
categoryId: 1,
}
{
id: 1,
name: "Xiaomi Buds 5 Pro",
description: "高音质无线蓝牙耳机续航可达30小时",
price: 1199.19,
imageUrl: "https://cdn.cnbj0.fds.api.mi-img.com/b2c-shopapi-pms/pms_1740638541.0557913.png",
stock: 2000,
categoryId: 1,
},
{
id: 2,
name: "机械键盘MK71pro",
description: "青轴机械键盘,全键无冲",
price: 899.00,
imageUrl: "https://cdn.cnbj0.fds.api.mi-img.com/b2c-shopapi-pms/67D4F43CD4C647AD8F45892D6567E14A.png",
stock: 1000,
categoryId: 1,
},
{
id: 3,
name: "Xiaomi Watch S4 Sport",
description: "多功能智能手表支持心率监测和GPS",
price: 1699.00,
imageUrl: "https://cdn.cnbj0.fds.api.mi-img.com/b2c-shopapi-pms/pms_1721233615.78223053.png",
stock: 200,
categoryId: 1,
}
];
const Res = <T extends Array<Object> | Object>(data: T) => {
return JSON.stringify({
code: 200,
message: "success",
result: data
});
return JSON.stringify({
code: 200,
message: "success",
result: data
});
}
// http://127.0.0.1:3000/api/productsList
// http://127.0.0.1:3210/api/productsList
app.get("/api/productsList", (ctx: { params: { name: any; }; }) => {
return new Response(Res(mockProductsList));
return new Response(Res(mockProductsList));
});
// http://127.0.0.1:3000/api/productsInfo?id=3
// http://127.0.0.1:3210/api/productsInfo?id=3
app.get("/api/productsInfo", (req: Request, ctx: { params: { id: number; }; }) => {
const id = (new URL(req.url)).searchParams.get('id')
const id = (new URL(req.url)).searchParams.get('id')
if (!id) {
return new Response(Res("缺少id参数"));
}
if (!id) {
return new Response(Res("缺少id参数"));
}
const result = mockProductsList.find(product => product.id == Number(id))
if (!result) {
return new Response(Res("找不到数据"));
}
const result = mockProductsList.find(product => product.id == Number(id))
if (!result) {
return new Response(Res("找不到数据"));
}
return new Response(Res(result));
return new Response(Res(result));
});
// test 测试接口
app.get("/nacos", (ctx: { params: { name: any; }; }) => {
console.log("test 测试接口 被运行");
return new Response(JSON.stringify({
title: "test测试接口"
}));
});
const exampleLoggerMiddleware = define.middleware((ctx: { req: { method: any; url: any; }; next: () => any; }) => {
console.log(`${ctx.req.method} ${ctx.req.url}`);
return ctx.next();
console.log(`${ctx.req.method} ${ctx.req.url}`);
return ctx.next();
});
app.use(exampleLoggerMiddleware);

View File

@@ -2,11 +2,12 @@ import { defineConfig } from "vite";
import { fresh } from "@fresh/plugin-vite";
export default defineConfig({
server: {
port: 3000, // 设置Vite开发服务器端口
strictPort: true, // 严格使用指定端口,如果被占用则报错
// 可选:允许跨域请求
cors: true,
},
plugins: [fresh()],
server: {
host: "0.0.0.0",
port: 3210, // 设置Vite开发服务器端口
strictPort: true, // 严格使用指定端口,如果被占用则报错
// 可选:允许跨域请求
cors: true,
},
plugins: [ fresh() ],
});

318
doc/DockerSwarm.md Normal file
View File

@@ -0,0 +1,318 @@
以下是完整的 Docker Swarm 命令大全:
## 1. Swarm 集群管理命令
### 初始化和管理 Swarm
```bash
# 初始化 Swarm 集群
docker swarm init --advertise-addr <IP地址>
# 初始化并指定默认地址池
docker swarm init --advertise-addr <IP地址> --default-addr-pool <网段> --default-addr-pool-mask-length <掩码>
# 查看 Swarm 集群信息
docker swarm inspect
# 查看 Swarm 加入令牌
docker swarm join-token worker # 工作节点令牌
docker swarm join-token manager # 管理节点令牌
# 更新令牌
docker swarm join-token --rotate worker
docker swarm join-token --rotate manager
# 离开 Swarm 集群
docker swarm leave # 工作节点离开
docker swarm leave --force # 管理节点强制离开
# 解锁 Swarm 集群
docker swarm unlock
docker swarm unlock-key # 查看解锁密钥
docker swarm unlock-key --rotate # 更新解锁密钥
```
## 2. 节点管理命令
### 节点操作
```bash
# 查看所有节点
docker node ls
# 查看节点详细信息
docker node inspect <节点名称或ID>
# 查看节点上的任务
docker node ps <节点名称或ID>
# 提升节点为管理节点
docker node promote <节点名称或ID>
# 降级节点为工作节点
docker node demote <节点名称或ID>
# 移除节点
docker node rm <节点名称或ID>
# 更新节点
docker node update --availability active <节点名称或ID> # 激活节点
docker node update --availability drain <节点名称或ID> # 排空节点(停止任务)
docker node update --availability pause <节点名称或ID> # 暂停节点
# 添加标签到节点
docker node update --label-add <key>=<value> <节点名称或ID>
# 移除节点标签
docker node update --label-rm <key> <节点名称或ID>
```
## 3. 服务管理命令
### 服务创建和操作
```bash
# 创建服务
docker service create --name <服务名> <镜像>
# 创建服务(完整参数示例)
docker service create \
--name nginx \
--replicas 3 \
--publish published=80,target=80 \
--mount type=bind,source=/host/path,target=/container/path \
--env ENV_VAR=value \
--constraint 'node.role==worker' \
--limit-cpu 0.5 \
--limit-memory 512M \
nginx:latest
# 查看所有服务
docker service ls
# 查看服务详细信息
docker service inspect <服务名>
# 查看服务任务状态
docker service ps <服务名>
# 查看服务日志
docker service logs <服务名>
docker service logs --follow <服务名> # 实时日志
docker service logs --tail 100 <服务名> # 最后100行日志
# 扩展服务副本数
docker service scale <服务名>=<数量>
# 更新服务
docker service update --image <新镜像> <服务名>
docker service update --replicas <数量> <服务名>
docker service update --force <服务名> # 强制更新(重新部署)
# 删除服务
docker service rm <服务名>
# 回滚服务到上次更新
docker service rollback <服务名>
```
### 服务更新参数
```bash
# 镜像相关
--image <镜像:标签> # 更新镜像
--rollback # 回滚到上一版本
# 副本和部署策略
--replicas <数量> # 设置副本数量
--update-parallelism <数量> # 同时更新的任务数
--update-delay <时间> # 更新间隔如10s
--update-failure-action <动作> # 失败动作pause/continue
--update-monitor <时间> # 监控时间如30s
--update-order <顺序> # 更新顺序start-first/stop-first
# 资源限制
--limit-cpu <值> # CPU限制如0.5
--limit-memory <值> # 内存限制如512M
--reserve-cpu <值> # 保留CPU
--reserve-memory <值> # 保留内存
# 网络和端口
--publish-add <端口映射> # 添加端口映射
--publish-rm <端口映射> # 移除端口映射
--network-add <网络> # 添加网络
--network-rm <网络> # 移除网络
# 约束和标签
--constraint-add <约束> # 添加约束
--constraint-rm <约束> # 移除约束
--label-add <标签> # 添加标签
--label-rm <标签> # 移除标签
# 环境变量
--env-add <变量> # 添加环境变量
--env-rm <变量> # 移除环境变量
--secret-add <密钥> # 添加密钥
--secret-rm <密钥> # 移除密钥
```
## 4. 栈Stack管理命令
### 使用 Docker Compose 文件部署
```bash
# 部署栈
docker stack deploy -c docker-compose.yml <栈名>
# 查看所有栈
docker stack ls
# 查看栈中的服务
docker stack services <栈名>
# 查看栈中服务的任务
docker stack ps <栈名>
# 删除栈
docker stack rm <栈名>
```
## 5. 网络管理命令
### Swarm 网络操作
```bash
# 查看网络
docker network ls
# 创建覆盖网络
docker network create --driver overlay --attachable <网络名>
# 查看网络详细信息
docker network inspect <网络名>
# 删除网络
docker network rm <网络名>
```
## 6. 配置和密钥管理
### 配置管理
```bash
# 创建配置
docker config create <配置名> <文件路径>
# 查看配置列表
docker config ls
# 查看配置内容
docker config inspect <配置名>
# 删除配置
docker config rm <配置名>
```
### 密钥管理
```bash
# 创建密钥
docker secret create <密钥名> <文件路径>
echo "secret content" | docker secret create <密钥名> -
# 查看密钥列表
docker secret ls
# 查看密钥内容
docker secret inspect <密钥名>
# 删除密钥
docker secret rm <密钥名>
```
## 7. 常用查询和监控命令
### 集群状态查询
```bash
# 查看集群系统信息
docker system df # 磁盘使用情况
docker system events # 系统事件
docker system info # 系统信息
# 监控资源使用
docker stats # 实时资源统计
docker stats --no-stream # 一次性资源统计
# 查看版本信息
docker version
```
## 8. 高级部署配置示例
### 使用 Docker Compose 文件
```yaml
# docker-compose.yml
version: '3.8'
services:
web:
image: nginx:latest
deploy:
replicas: 3
update_config:
parallelism: 2
delay: 10s
failure_action: rollback
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
cpus: '0.1'
memory: 128M
placement:
constraints:
- node.role == worker
ports:
- "80:80"
networks:
- webnet
networks:
webnet:
driver: overlay
attachable: true
```
### 部署栈
```bash
docker stack deploy -c docker-compose.yml myapp
```
## 9. 故障排查命令
### 排查和调试
```bash
# 查看节点连接状态
docker node ls
# 查看服务详细状态
docker service inspect --pretty <服务名>
# 查看任务日志
docker service logs --tail 50 <服务名>
# 在节点上执行命令
docker exec -it <容器ID> /bin/bash
# 查看 Swarm 集群事件
docker events
```

BIN
doc/jgt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 989 KiB

BIN
doc/newci.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 KiB

20
doc/临时命令.md Normal file
View File

@@ -0,0 +1,20 @@
# 包含 [skip actions] 的提交会跳过工作流
git commit -m "修复样式问题[skip actions]"
```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 @@
# 完善微前端架构的建议
除了已有的主模块、订单联邦模块、共享资源模块以及 nacos 服务中心和 traefik api 网关外,还可以考虑以下几个方面来完善微前端架构:
* **状态管理与通信机制**:微前端架构中,各模块间的状态管理和通信是关键。可以采用 Pinia 等状态管理库,实现跨模块的数据共享。例如,通过在共享资源模块中定义
Pinia store然后在主模块和订单联邦模块中引入并使用确保所有微前端模块都能访问同一缓存存储实现数据共享。
* **路由管理**:虽然 @originjs/vite - plugin - federation
插件本身支持模块的动态加载,但仍需要合理设计路由系统,确保不同模块之间的路由切换顺畅。可以在主模块中统一管理路由,根据用户的访问路径,动态加载对应的微前端模块。例如,使用
Vue Router 的动态路由功能,根据不同的 URL 路径加载订单联邦模块或共享资源模块的组件。
* **样式隔离与共享**:需要确保各个微前端模块的样式不会相互干扰。可以采用 CSS Modules、Shadow DOM 或 CSS-in-JS
等技术来实现样式隔离。同时,对于一些共享的样式,可以在共享资源模块中进行定义,然后通过模块联邦的方式共享给其他模块。
* **错误处理机制**
:由于微前端模块是独立开发和部署的,可能会出现各种错误。需要建立统一的错误处理机制,捕获并处理微前端模块在加载和运行过程中出现的错误。例如,在主模块中设置全局的错误捕获器,当订单联邦模块或共享资源模块出现错误时,能够及时捕获并展示友好的错误提示信息。
* **性能优化**:微前端架构中,多个模块的加载可能会影响性能。可以通过代码分割、懒加载等方式来优化性能。@originjs/vite -
plugin - federation 插件本身支持代码分割,可将每个微前端模块的代码拆分成多个小块,只在需要时加载相应的代码块。此外,还可以使用缓存策略,减少重复加载。
* **安全机制**随着微前端模块的增加安全问题也需要重视。需要确保各个模块之间的数据传输是安全的防止跨站脚本攻击XSS、跨站请求伪造CSRF等安全漏洞。可以采用身份验证、授权、输入验证等安全措施来保障系统的安全。

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

@@ -1,10 +1,12 @@
### 依赖注意
DDD 中允许 “外层依赖内层”(交付层依赖领域层),但不允许 “内层依赖外层”。
- 正确features交付层 → domains领域层
- 错误domains领域层 → features交付层
#### 避免类型重复定义
如果不在视图中直接使用领域类型,就需要在交付层重新定义一套类似的类型(如 “视图专用商品类型”),这会导致:
- 代码冗余和不一致风险
@@ -12,14 +14,15 @@ DDD 中允许 “外层依赖内层”(交付层依赖领域层),但不允
- 业务规则分散(领域层的类型约束可能在视图层被忽略)
### 横切关注点
- 特征:横向,影响多个模块,那些无法放入任何一个业务模块,而是会"横着"贯穿多个甚至所有模块的技术性需求。
- 案例:日志记录、身份认证、授权、事务管理、异常处理、缓存、性能监控
## 业务关注点
- 特征:通常局限于一个业务模块内
- 案例:计算订单总额、验证用户邮箱格式、处理库存扣减
### 对象职责的设计理念对比:
#### 贫血模型
@@ -30,33 +33,33 @@ DDD 中允许 “外层依赖内层”(交付层依赖领域层),但不允
```typescript
// 1. 贫血的Order实体只有数据没有行为
class Order {
public id: string;
public items: OrderItem[];
public total: number; // 总额需要外部来计算和设置
public status: string;
public id: string;
public items: OrderItem[];
public total: number; // 总额需要外部来计算和设置
public status: string;
}
// 2. 所有业务逻辑都在外部的“服务”里
class OrderService {
calculateTotal(order: Order): void {
let total = 0;
for (const item of order.items) {
total += item.price * item.quantity;
calculateTotal(order: Order): void {
let total = 0;
for (const item of order.items) {
total += item.price * item.quantity;
}
order.total = total; // 从外部修改对象的数据
}
order.total = total; // 从外部修改对象的数据
}
markAsPaid(order: Order): void {
if (order.total <= 0) {
throw new Error('订单金额无效');
markAsPaid(order: Order): void {
if (order.total <= 0) {
throw new Error('订单金额无效');
}
order.status = 'paid'; // 从外部修改对象的状态
}
order.status = 'paid'; // 从外部修改对象的状态
}
}
// 使用方式
const order = new Order();
order.items = [...];
order.items = [ ... ];
const orderService = new OrderService();
orderService.calculateTotal(order); // 服务来算总额
orderService.markAsPaid(order); // 服务来改状态
@@ -70,42 +73,42 @@ orderService.markAsPaid(order); // 服务来改状态
```typescript
// 充血的Order实体数据 + 行为
class Order {
public id: string;
private _items: OrderItem[];
private _total: number; // 总额是内部计算的结果
public status: string;
public id: string;
private _items: OrderItem[];
private _total: number; // 总额是内部计算的结果
public status: string;
constructor(items: OrderItem[]) {
this._items = items;
this._total = this.calculateTotal(); // 构造时自己计算总额
this.status = 'created';
}
// 业务逻辑封装在实体内部
private calculateTotal(): number {
return this._items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
}
// 一个公开的业务方法
public markAsPaid(): void {
// 它自己 knows 自己的业务规则
if (this._total <= 0) {
throw new Error('订单金额无效,无法支付');
constructor(items: OrderItem[]) {
this._items = items;
this._total = this.calculateTotal(); // 构造时自己计算总额
this.status = 'created';
}
this.status = 'paid';
}
// 提供访问内部数据的方法(如果需要)
get total(): number {
return this._total;
}
// 业务逻辑封装在实体内部
private calculateTotal(): number {
return this._items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
}
get items(): ReadonlyArray<OrderItem> {
return [...this._items]; // 返回副本,保护内部数据
}
// 一个公开的业务方法
public markAsPaid(): void {
// 它自己 knows 自己的业务规则
if (this._total <= 0) {
throw new Error('订单金额无效,无法支付');
}
this.status = 'paid';
}
// 提供访问内部数据的方法(如果需要)
get total(): number {
return this._total;
}
get items(): ReadonlyArray<OrderItem> {
return [ ...this._items ]; // 返回副本,保护内部数据
}
}
// 使用方式:对象是聪明的,告诉它做什么就行,而不是一步步操作它
const order = new Order([...]);
const order = new Order([ ... ]);
order.markAsPaid(); // 对象自己处理状态变更
```

View File

@@ -0,0 +1,147 @@
## nacos-cluster hostname 集群版
把 Nacos 部署到多台Linux 服务器上的版本配置,下面是**文件说明**
- docker-compose.yml: Docker compose + Docker Swarm 的编排文件
- nacos_config_export.zip: 存储在Nacos配置中心的项目配置部署成功后台导入即可
- mysql-schema.sql: Nacos 集群公用的数据库会自动导入到Mysql不用动
- prometheus: Prometheus 的配置文件
## 条件
- 至少三台起步的Linux主机安装有docker你可以使用虚拟机。
- 一个能正常访问Docker的网络环境拉取镜像
- 耐心,毅力,别慌!
## 集群配置
假设我们有三台Linux主机他们的IP地址为
- 1: 192.168.139.84
- 2: 192.168.139.15
- 3: 192.168.139.178
选一台你喜欢的(我选1),作为集群的 **Manager**然后SSH登录进去获取管理员权限然后
```bash
# 初始化 Swarm 集群
docker swarm init --advertise-addr 192.168.139.84
```
命令执行完成后,你会得到这样的返回:
```bash
Swarm initialized: current node (z5sucmc80ns8bxrdqhs71dg8x) is now a manager.
To add a worker to this swarm, run the following command:
# 复制这个
docker swarm join --token SWMTKN-1-3n9xxxxnwvsj6-4rhxx8xxxxzbbul 192.168.139.84:2377
To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.
```
然后SSH登录**worker**机,也就是 上面的 23 服务器获取SSH权限后分别执行上面给的命令
```bash
docker swarm join --token SWMTKN-1-3n9xxxxnwvsj6-4rhxx8xxxxzbbul 192.168.139.84:2377
```
**这样 23 服务器就和1服务器组成了集群**
可以指执行命令查看是否组成集群:
```bash
# 查看所有节点
docker node ls
```
注意如果你的服务器有防火墙那么请记住放行端口下面是Ubuntu上的例子
```bash
# --- Docker Swarm 集群通信端口 ---
# TCP 2377: Swarm 管理端口 (仅在 Manager 和 Worker 之间通信)
sudo ufw allow 2377/tcp
# TCP/UDP 7946: 节点之间的控制平面和数据平面的 gossip 协议通信
sudo ufw allow 7946/tcp
sudo ufw allow 7946/udp
# UDP 4789: VXLAN 端口,用于 overlay 网络中容器之间的通信
sudo ufw allow 4789/udp
# --- MySQL 数据库端口 (可选) ---
# TCP 3306: MySQL 数据库端口(需要外部访问数据库时候才开)
sudo ufw allow 3306/tcp
# --- Nacos 集群端口 ---
# Nacos 管理后台端口
sudo ufw allow 8080/tcp
# 客户端gRPC请求服务端端口用于客户端向服务端发起连接和请求
sudo ufw allow 9848/tcp
# Nacos HTTP API 端口用于Nacos AdminAPI及HTTP OpenAPI的访问
sudo ufw allow 8848/tcp
# 重新加载防火墙以应用所有规则
sudo ufw reload
# 查看配置和状态
ufw status
```
## 部署运行
### 运行启动
将本文件夹内所有文件上传到 1号服务器然后在文件夹内执行命令
```bash
# 部署栈,这个过程有点慢,因为需要拉取镜像,等!
docker stack deploy -c docker-compose.yml nacos-cluster
# 命令完成后,需要等待 Nacos 启动,大概几十秒到一分钟这样!!
# 启动成功后直接查看下面教程【Nacos后台】
# --- 其他命令 ---
# 部署后,不用了,可以使用这个 删除栈
docker stack rm nacos-cluster
# 可以查看Nacos的 实时日志:
docker service logs --follow nacos-cluster_nacos
# 可以查看Mysql的 实时日志:
docker service logs --follow nacos-cluster_mysql
# 查看 nacos-cluster 集群运行情况
docker stack ps nacos-cluster
# 持续观察服务副本数是否达到预期
watch docker stack services nacos-cluster
```
### Nacos后台
选择一个地址浏览器打开,注册登录账户:
- (http://192.168.139.84:8080)[http://192.168.139.84:8080]
- (http://192.168.139.15:8080)[http://192.168.139.15:8080]
- (http://192.168.139.178:8080)[http://192.168.139.178:8080]
然后打开左侧导航 **配置管理** --> **配置列表** --> **导入配置**
选择 nacos_config.zip 导入即可。
### 其他后台:
- [Prometheus](http://192.168.139.84:9090)
- [Grafana](http://192.168.139.84:3000)
- 默认账户admin
- 默认密码密码admin
**结束**

View File

@@ -0,0 +1,162 @@
# 使用版本 3.8,它与 Swarm 模式兼容
version: "3.8"
# 定义服务
services:
# MySQL 数据库服务
mysql:
# 使用指定的 MySQL 镜像
image: mysql:8.0.31
# 使用环境变量文件,与原来的配置保持一致
environment:
- MYSQL_ROOT_PASSWORD=root
- MYSQL_DATABASE=nacos_devtest
- MYSQL_USER=nacos
- MYSQL_PASSWORD=nacos
- LANG=C.UTF-8
# 数据卷挂载,确保 MySQL 数据持久化
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
ports:
- target: 3306
published: 3306
protocol: tcp
mode: host # 数据库通常使用 host 模式,避免路由网格开销
# 健康检查,确保数据库启动完成后再启动 Nacos
healthcheck:
test: [ "CMD", "mysqladmin", "ping", "-h", "localhost" ]
interval: 5s
timeout: 10s
retries: 10
# Swarm 部署配置
deploy:
# 副本数为 1因为单个数据库实例足够
replicas: 1
# 重启策略:任何情况下容器退出都自动重启
restart_policy:
condition: any
# 将服务加入我们创建的网络
networks:
- nacos-net
# Nacos 服务
nacos:
hostname: "nacos{{.Task.Slot}}"
# 使用 Nacos 镜像
image: nacos/nacos-server:v3.1.0
# 挂载日志目录
volumes:
- nacos-logs:/home/nacos/logs
# 暴露 Nacos 所需的端口
ports:
- 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
- PREFER_HOST_MODE=hostname
- NACOS_SERVERS=nacos1:8848 nacos2:8848 nacos3:8848
- SPRING_DATASOURCE_PLATFORM=mysql
- MYSQL_SERVICE_HOST=mysql
- MYSQL_SERVICE_DB_NAME=nacos_devtest
- MYSQL_SERVICE_PORT=3306
- MYSQL_SERVICE_USER=nacos
- MYSQL_SERVICE_PASSWORD=nacos
- MYSQL_SERVICE_RETRY_COUNT=10
- MYSQL_SERVICE_RETRY_DELAY=5000
- MYSQL_SERVICE_DB_PARAM=characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true
- NACOS_SERVER_AUTH_ENABLE=false
- NACOS_CONSOLE_AUTH_ENABLE=false
- NACOS_AUTH_ENABLE=false
- NACOS_AUTH_IDENTITY_KEY=bmy
- NACOS_AUTH_IDENTITY_VALUE=lb714500.
- NACOS_AUTH_TOKEN=KjdAdzBIMTBtKElJS0NrWTIxNkJ0RXN4YXlHaipSUmdDZA==
# Swarm 部署配置
deploy:
# 关键:指定副本数为 3Swarm 会自动创建 3 个 Nacos 容器
replicas: 3
# 重启策略
restart_policy:
condition: any
# 启动顺序控制
# 这是 Swarm 中实现 "depends_on" 的方式
# 它会在 mysql 服务健康检查通过后,才开始启动 Nacos 服务
update_config:
parallelism: 1
delay: 30s
# 资源限制(可选,可根据你的服务器配置调整)
resources:
limits:
cpus: '0.50'
memory: '512M'
reservations:
cpus: '0.25'
memory: '256M'
networks:
- nacos-net
# Prometheus 服务
prometheus:
image: prom/prometheus:latest
volumes:
- ./prometheus/prometheus-cluster.yaml:/etc/prometheus/prometheus.yml
ports:
- target: 9090
published: 9090
protocol: tcp
mode: ingress # 监控服务使用路由网格
deploy:
replicas: 1
restart_policy:
condition: on-failure
update_config:
order: start-first
networks:
- nacos-net
# Grafana 服务
grafana:
image: grafana/grafana:latest
ports:
- target: 3000
published: 3000
protocol: tcp
mode: ingress # Grafana 使用路由网格
deploy:
# 通常 Grafana 单实例即可
replicas: 1
restart_policy:
condition: on-failure
networks:
- nacos-net
volumes:
nacos-logs:
driver: local
mysql-data:
driver: local
networks:
nacos-net:
driver: overlay

View File

@@ -0,0 +1,188 @@
/*
* Copyright 1999-2018 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/******************************************/
/* 表名称 = config_info */
/******************************************/
CREATE TABLE `config_info`
(
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(128) DEFAULT NULL COMMENT 'group_id',
`content` longtext NOT NULL COMMENT 'content',
`md5` varchar(32) DEFAULT NULL COMMENT 'md5',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
`src_user` text COMMENT 'source user',
`src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
`c_desc` varchar(256) DEFAULT NULL COMMENT 'configuration description',
`c_use` varchar(64) DEFAULT NULL COMMENT 'configuration usage',
`effect` varchar(64) DEFAULT NULL COMMENT '配置生效的描述',
`type` varchar(64) DEFAULT NULL COMMENT '配置的类型',
`c_schema` text COMMENT '配置的模式',
`encrypted_data_key` varchar(1024) NOT NULL DEFAULT '' COMMENT '密钥',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfo_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info';
/******************************************/
/* 表名称 = config_info since 2.5.0 */
/******************************************/
CREATE TABLE `config_info_gray`
(
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(128) NOT NULL COMMENT 'group_id',
`content` longtext NOT NULL COMMENT 'content',
`md5` varchar(32) DEFAULT NULL COMMENT 'md5',
`src_user` text COMMENT 'src_user',
`src_ip` varchar(100) DEFAULT NULL COMMENT 'src_ip',
`gmt_create` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 'gmt_create',
`gmt_modified` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 'gmt_modified',
`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
`tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',
`gray_name` varchar(128) NOT NULL COMMENT 'gray_name',
`gray_rule` text NOT NULL COMMENT 'gray_rule',
`encrypted_data_key` varchar(256) NOT NULL DEFAULT '' COMMENT 'encrypted_data_key',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfogray_datagrouptenantgray` (`data_id`,`group_id`,`tenant_id`,`gray_name`),
KEY `idx_dataid_gmt_modified` (`data_id`,`gmt_modified`),
KEY `idx_gmt_modified` (`gmt_modified`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='config_info_gray';
/******************************************/
/* 表名称 = config_tags_relation */
/******************************************/
CREATE TABLE `config_tags_relation`
(
`id` bigint(20) NOT NULL COMMENT 'id',
`tag_name` varchar(128) NOT NULL COMMENT 'tag_name',
`tag_type` varchar(64) DEFAULT NULL COMMENT 'tag_type',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(128) NOT NULL COMMENT 'group_id',
`tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',
`nid` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'nid, 自增长标识',
PRIMARY KEY (`nid`),
UNIQUE KEY `uk_configtagrelation_configidtag` (`id`,`tag_name`,`tag_type`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_tag_relation';
/******************************************/
/* 表名称 = group_capacity */
/******************************************/
CREATE TABLE `group_capacity`
(
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`group_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Group ID空字符表示整个集群',
`quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额0表示使用默认值',
`usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
`max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限单位为字节0表示使用默认值',
`max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数0表示使用默认值',
`max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限单位为字节0表示使用默认值',
`max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_group_id` (`group_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='集群、各Group容量信息表';
/******************************************/
/* 表名称 = his_config_info */
/******************************************/
CREATE TABLE `his_config_info`
(
`id` bigint(20) unsigned NOT NULL COMMENT 'id',
`nid` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'nid, 自增标识',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(128) NOT NULL COMMENT 'group_id',
`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
`content` longtext NOT NULL COMMENT 'content',
`md5` varchar(32) DEFAULT NULL COMMENT 'md5',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
`src_user` text COMMENT 'source user',
`src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
`op_type` char(10) DEFAULT NULL COMMENT 'operation type',
`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
`encrypted_data_key` varchar(1024) NOT NULL DEFAULT '' COMMENT '密钥',
`publish_type` varchar(50) DEFAULT 'formal' COMMENT 'publish type gray or formal',
`gray_name` varchar(50) DEFAULT NULL COMMENT 'gray name',
`ext_info` longtext DEFAULT NULL COMMENT 'ext info',
PRIMARY KEY (`nid`),
KEY `idx_gmt_create` (`gmt_create`),
KEY `idx_gmt_modified` (`gmt_modified`),
KEY `idx_did` (`data_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='多租户改造';
/******************************************/
/* 表名称 = tenant_capacity */
/******************************************/
CREATE TABLE `tenant_capacity`
(
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`tenant_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Tenant ID',
`quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额0表示使用默认值',
`usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
`max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限单位为字节0表示使用默认值',
`max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数',
`max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限单位为字节0表示使用默认值',
`max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='租户容量信息表';
CREATE TABLE `tenant_info`
(
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`kp` varchar(128) NOT NULL COMMENT 'kp',
`tenant_id` varchar(128) default '' COMMENT 'tenant_id',
`tenant_name` varchar(128) default '' COMMENT 'tenant_name',
`tenant_desc` varchar(256) DEFAULT NULL COMMENT 'tenant_desc',
`create_source` varchar(32) DEFAULT NULL COMMENT 'create_source',
`gmt_create` bigint(20) NOT NULL COMMENT '创建时间',
`gmt_modified` bigint(20) NOT NULL COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_info_kptenantid` (`kp`,`tenant_id`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='tenant_info';
CREATE TABLE `users`
(
`username` varchar(50) NOT NULL PRIMARY KEY COMMENT 'username',
`password` varchar(500) NOT NULL COMMENT 'password',
`enabled` boolean NOT NULL COMMENT 'enabled'
);
CREATE TABLE `roles`
(
`username` varchar(50) NOT NULL COMMENT 'username',
`role` varchar(50) NOT NULL COMMENT 'role',
UNIQUE INDEX `idx_user_role` (`username` ASC, `role` ASC) USING BTREE
);
CREATE TABLE `permissions`
(
`role` varchar(50) NOT NULL COMMENT 'role',
`resource` varchar(128) NOT NULL COMMENT 'resource',
`action` varchar(8) NOT NULL COMMENT 'action',
UNIQUE INDEX `uk_role_permission` (`role`,`resource`,`action`) USING BTREE
);

Binary file not shown.

View File

@@ -0,0 +1,34 @@
# my global config
global:
scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
# scrape_timeout is set to the global default (10s).
# Alertmanager configuration
alerting:
alertmanagers:
- static_configs:
- targets:
# - alertmanager:9093
# Load rules once and periodically evaluate them according to the global 'evaluation_interval'.
rule_files:
# - "first_rules.yml"
# - "second_rules.yml"
# A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself.
scrape_configs:
# The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
- job_name: 'prometheus'
# metrics_path defaults to '/metrics'
# scheme defaults to 'http'.
static_configs:
- targets: [ 'localhost:9090' ]
- job_name: 'nacos'
metrics_path: '/nacos/actuator/prometheus'
static_configs:
- targets: [ "nacos1:8848","nacos2:8848","nacos3:8848" ]

10
docker/traefik/README.md Normal file
View File

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

View File

@@ -0,0 +1,27 @@
version: '3.8'
services:
traefik:
image: traefik:latest
container_name: traefik
networks:
- traefik-net
ports:
- "80:80"
- "8080:8080"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./traefik.yml:/etc/traefik/traefik.yml
- ./dynamic.yml:/etc/traefik/dynamic.yml
- ./log-file.log:/etc/traefik/log-file.log
- ./access.log:/etc/traefik/access.log
restart: always
whoami:
image: traefik/whoami:latest
labels:
- "traefik.http.routers.whoami.rule=Host(`whoami.localhost`)"
networks:
traefik-net:
driver: bridge

113
docker/traefik/dynamic.yml Normal file
View File

@@ -0,0 +1,113 @@
http:
routers:
#
app-shared-router:
rule: Host(`nacos.ddd.com`) && PathPrefix(`/app-shared`)
service: app-shared-service
observability:
metrics: true
accessLogs: true
tracing: true
traceVerbosity: detailed
entryPoints:
- web
app-product-router:
rule: Host(`nacos.ddd.com`) && PathPrefix(`/app-product`)
service: app-product-service
observability:
metrics: true
accessLogs: true
tracing: true
traceVerbosity: detailed
entryPoints:
- web
# 使用域名访问前端main-app
main-ddd-router:
rule: Host(`main.ddd.com`)
service: main-ddd-service
observability:
metrics: true
accessLogs: true
tracing: true
traceVerbosity: detailed
entryPoints:
- web
# Nacos 集群 管理后台的统一访问域名
nacosui-ddd-router:
rule: Host(`nacosui.ddd.com`)
service: nacosui-ddd-service
observability:
metrics: true
accessLogs: true
tracing: true
traceVerbosity: detailed
entryPoints:
- web
# trafik 网关 代理 内网 nacos 集群 的地址
nacos-ddd-router:
rule: Host(`nacos.ddd.com`)
service: nacos-ddd-service
observability:
metrics: true
accessLogs: true
tracing: true
traceVerbosity: detailed
entryPoints:
- web
# trafik 网关的管理后台的域名
trafikui-ddd-router:
rule: Host(`trafikui.ddd.com`)
service: trafikui-ddd-service
observability:
metrics: true
accessLogs: true
tracing: true
traceVerbosity: detailed
entryPoints:
- web
services:
app-shared-service:
loadBalancer:
servers:
- url: http://192.168.139.172:1333
- url: http://192.168.139.184:1333
- url: http://192.168.139.186:1333
app-product-service:
loadBalancer:
servers:
- 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
nacosui-ddd-service:
loadBalancer:
servers:
- url: http://192.168.139.84:8080
- url: http://192.168.139.15:8080
- url: http://192.168.139.178:8080
nacos-ddd-service:
loadBalancer:
servers:
- url: http://192.168.139.84:8848
- url: http://192.168.139.15:8848
- url: http://192.168.139.178:8848
trafikui-ddd-service:
loadBalancer:
servers:
- url: http://192.168.139.80:8080

View File

@@ -0,0 +1,15 @@
{
"name": "traefik-naco",
"version": "1.0.0",
"description": "",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"axios": "^1.12.2",
"js-yaml": "^4.1.0"
},
"keywords": [],
"author": "",
"license": "ISC"
}

View File

@@ -0,0 +1,63 @@
const path = require("path");
module.exports = {
filePath: path.join(__dirname, '../../dynamic.yml'),
nacosApi: {
baseUrl: 'http://192.168.31.101:8848/nacos/v1/ns',
// baseUrl: 'http://192.168.31.101:8848/nacos/v3/admin/ns',
apiList: {
instanceInfo: '/instance/list',
serviceList: '/service/list',
}
},
nacosConfig: {
nameSpace: 'public',
},
// 每隔五秒检查一下最新配置
check_time: 10000,
// traefik 通用中间件配置
traefikConfig: {
middlewaresStr: ['compress', 'ipWhiteList', 'rateLimit', 'retries'],
middlewaresConfig: {
// 限流中间件
rateLimit: {
rateLimit: {
average: 100,
burst: 200,
period: '1s',
sourceCriterion: {
ipStrategy: {depth: 2}
}
}
},
// 压缩中间件
compress: {
compress: {
excludedContentTypes: ['text/event-stream'],
minResponseBodyBytes: 1024
}
},
// IP白名单中间件
ipWhiteList: {
ipWhiteList: {
sourceRange: ['127.0.0.1/32', '192.168.107.0/24']
}
},
// // 重定向中间件HTTP→HTTPS
// redirectToHttps: {
// redirectScheme: {
// scheme: 'https',
// permanent: true
// }
// },
// 重试中间件(关键修复:将重试配置移到这里)
retries: {
retry: {
attempts: 3,
initialInterval: '200ms'
}
}
}
}
}

View File

@@ -0,0 +1,38 @@
const axios = require('axios');
const config = require('../config');
const {AxiosError} = require("axios");
class Http {
instance = null;
constructor() {
this.instance = axios.create({
baseURL: config.nacosApi.baseUrl,
timeout: 10000,
headers: {
"Content-Type": "application/json"
}
})
this.ResponseInterceptor();
}
async get(params) {
return await this.instance.get(params.url, {
params: params.data,
});
}
async ResponseInterceptor() {
this.instance.interceptors.response.use(
(response) => {
return response.data;
},
(error) => {
return Promise.reject(error)
})
}
}
module.exports = new Http

View File

@@ -0,0 +1,9 @@
const utils = require('./utils')
const config = require('./config')
const uti = new utils();
setInterval(
uti.getServiceName.bind(uti),
config.check_time
)

View File

@@ -0,0 +1,109 @@
const config = require("../config")
const http = require("../http")
const yaml = require("js-yaml");
const path = require('path');
const fs = require('fs');
class Utils {
// 存储 Nacos上 服务列表 的服务名称
ServiceNameList = []
constructor() {
this.getServiceName()
}
/**
* 获取Nacos上的服务名称数组
* @returns {Promise<void>}
*/
async getServiceName() {
console.log("1: [getServiceName] 从Nacos 获取服务名..")
const {doms} = await http.get({
url: config.nacosApi.apiList.serviceList,
data: {pageNo: 1, pageSize: 20},
});
if (doms.length === 0) {
throw new Error("No service found for service list");
}
this.serviceName = doms;
console.log("2: [getServiceName] 服务名为:", this.serviceName)
await this.generateTraefikConfig()
}
/**
* 从 Nacos 获取服务实例
* @param serviceName
* @returns {Promise<*|*[]>}
*/
async getServiceInstances(serviceName) {
const {hosts} = await http.get({
url: config.nacosApi.apiList.instanceInfo,
data: {serviceName, namespaceId: config.nacosConfig.nameSpace},
});
return hosts || []
}
/**
* 生成 traefik 动态配置
* @returns {Promise<void>}
*/
async generateTraefikConfig() {
const traefikConf = {
http: {
routers: {},
services: {},
middlewares: config.traefikConfig.middlewaresConfig,
}
};
for (const serviceName of this.serviceName) {
const instances = await this.getServiceInstances(serviceName);
if (instances.length === 0) {
console.warn(`服务 ${serviceName} 无可用实例`);
continue;
}
// 1. 配置服务(服务名 + 实例列表)
traefikConf.http.services[`${serviceName}-service`] = {
loadBalancer: {
servers: instances.map(inst => ({
url: `http://${inst.ip}:${inst.port}`,
})),
}
};
// 2. 配置路由(路径前缀 + 绑定服务)
traefikConf.http.routers[`${serviceName}-router`] = {
rule: `PathPrefix(\`/${serviceName}\`)`, // 如 /main、/product
service: `${serviceName}-service`,
entryPoints: ['web'],
// 应用中间件压缩、IP白名单、限流、重试等
middlewares: config.traefikConfig.middlewaresStr,
};
}
this.stringToYaml(traefikConf)
}
/**
* 生成 yaml 配置文件
* @param conf
*/
stringToYaml(conf) {
console.log("3: [stringToYaml] 生成配置:", JSON.stringify(conf, null, 2));
console.log()
console.log()
const yamlString = yaml.dump(conf, {
indent: 2, // 缩进空格数
skipInvalid: true, // 跳过无效类型
noRefs: true // 禁止引用(防止循环引用问题)
});
fs.writeFileSync(config.filePath, yamlString, "utf8");
}
}
module.exports = Utils

View File

@@ -0,0 +1,34 @@
# 启用 Dashboard
api:
dashboard: true
insecure: true # 开发环境允许HTTP访问
# 入口点配置
entryPoints:
web:
address: ":80" # 监听80端口Web 流量
# 服务发现与配置源(关键:子项缩进必须一致)
providers:
docker: { }
# 动态路由规则文件
file:
filename: "/etc/traefik/dynamic.yml"
watch: true # 自动监听文件变化
log:
filePath: "/etc/traefik/log-file.log"
format: common
level: INFO
accessLog:
filePath: "/etc/traefik/access.log"
format: "json"
addInternals: true
filters:
retryAttempts: true
minDuration: "2ms"
fields:
defaultMode: "keep"
headers:
defaultMode: "keep"

View File

@@ -1,10 +0,0 @@
npm install
npm run build
npm run serve
构建与部署:
- 在构建生产环境时,先构建主模块,并将其产出(特别是 remoteEntry.js 和对应的资产文件)部署到服务器,并获取其 URL。
- 然后,在子模块的 vite.config.ts 中,将 remotes 的 URL 更新为生产环境主模块 remoteEntry.js 的绝对 URL。
- 最后再构建子模块。

View File

@@ -1,16 +0,0 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -1,34 +0,0 @@
{
"name": "app-product",
"version": "0.0.0",
"private": true,
"type": "module",
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"scripts": {
"dev": "vite",
"dep": "concurrently \"npm run build\" \"npm run preview\"",
"preview": "vite preview --port 1301 --strictPort",
"build": "vite build --watch",
"type-check": "vue-tsc --build"
},
"dependencies": {
"pinia": "^3.0.3",
"vue": "^3.5.18",
"vue-router": "^4.0.6"
},
"devDependencies": {
"@originjs/vite-plugin-federation": "^1.4.1",
"@tsconfig/node22": "^22.0.2",
"@types/node": "^22.16.5",
"@vitejs/plugin-vue": "^6.0.1",
"@vitejs/plugin-vue-jsx": "^5.0.1",
"@vue/tsconfig": "^0.7.0",
"npm-run-all2": "^8.0.4",
"typescript": "~5.8.0",
"vite": "^7.0.6",
"vite-plugin-vue-devtools": "^8.0.0",
"vue-tsc": "^3.0.4"
}
}

View File

@@ -1,22 +0,0 @@
import {defineComponent} from 'vue'
export default defineComponent({
setup() {
return () => (
<>
<div class="order-federation-placeholder">
<h2>-</h2>
<p>
</p>
<h2></h2>
<ul>
<li>1</li>
<li>2</li>
</ul>
<p></p>
</div>
</>
)
}
})

View File

@@ -1,22 +0,0 @@
import { ErrorText } from "../../domain/enums/ErrorEnums";
import { ProductRepository } from "../../ports/repositories/ProductRepository";
/**
* 商品应用服务 - 协调领域对象完成业务用例
* 应用服务是领域层的外观,封装了领域逻辑的调用
*/
export class ProductApplicationService {
// 依赖注入仓储接口,而不是具体实现
constructor(private productRepository: ProductRepository) { }
async getProductList() {
return await this.productRepository.findAll();
}
async getProductInfo(id: number) {
if (!id) {
throw new Error(ErrorText.IdNotNull)
}
return await this.productRepository.findById(id);
}
}

View File

@@ -1,38 +0,0 @@
// 从商品联邦模块导入领域层资源
import {ProductRepository} from "@domains/product/ports/repositories/ProductRepository";
import {HttpProductRepository} from "@domains/product/infr/api/HttpProductRepository";
import {ProductApplicationService} from "@domains/product/application/services/ProductApplicationService";
/**
* 商品领域的依赖注入配置
* 将 接口 与 具体实现 绑定
*
* 优点:是一个依赖工厂,它隐藏了对象创建的细节,
* 让业务代码只需要关心 “做什么”,而不用关心 “依赖从哪里来”
* 所有依赖关系都在di目录下集中配置一目了然后续维护时能快速找到所有依赖的创建逻辑。
*
* 松耦合:
* 业务代码ProductApplicationService只依赖抽象接口ProductRepository
* 不依赖具体实现。如果未来需要改用其他数据来源(比如 WebSocket
* 只需修改bindProductRepository的返回值无需改动业务逻辑。
*
* 易于测试:
* 测试时可以临时替换为模拟实现
* productModule.bindProductRepository = () => new MockProductRepository();
* 这样测试就可以脱离真实 API使用预设的模拟数据。
*/
export const product = {
// 绑定 仓储抽象接口 到 HTTP具体实现
bindProductRepository: (): ProductRepository => {
// return new MockProductRepository 直接从真实接口数据改为测试模拟数据
return new HttpProductRepository;
},
// 创建应用服务实例
createProductApplicationService: (): ProductApplicationService => {
return new ProductApplicationService(
product.bindProductRepository()
);
}
}

View File

@@ -1,141 +0,0 @@
import { ErrorText } from "../enums/ErrorEnums"
/**
* 商品实体 - 包含商品的核心属性和行为
* 在DDD中实体具有唯一标识和生命周期
*/
export class Product {
private readonly _id: number;
private _name: string;
private _description: string;
private _price: number;
private _imageUrl: string;
private _stock: number;
private _categoryId: number;
constructor(
id: number,
name: string,
description: string,
price: number,
imageUrl: string,
stock: number,
categoryId: number,
) {
this._id = id;
this._name = name;
this._description = description;
this._price = price;
this._imageUrl = imageUrl;
this._stock = stock;
this._categoryId = categoryId;
}
// 实体唯一标识
get id(): number {
return this._id;
}
public get name(): string {
return this._name;
}
public set name(value: string) {
this._name = value;
}
public get description(): string {
return this._description;
}
public set description(value: string) {
this._description = value;
}
public get price(): number {
return this._price;
}
public set price(value: number) {
if (value < 0) {
throw new Error(ErrorText.PriceNotNegativein);
}
this._price = value;
}
public get imageUrl(): string {
return this._imageUrl;
}
public set imageUrl(value: string) {
this._imageUrl = value;
}
public get stock(): number {
return this._stock;
}
public set stock(value: number) {
if (value < 0) {
throw new Error(ErrorText.StockNotNegativein)
}
this._stock = value;
}
public get categoryId(): number {
return this._categoryId;
}
public set categoryId(value: number) {
this._categoryId = value;
}
/**
* 检查商品是否有库存
*/
hasStock() {
return this._stock > 0
}
/**
* 减少商品库存
* @param quantity 减少的数量
*/
reduceStock(quantity: number): void {
if (quantity <= 0) {
throw new Error(ErrorText.StockIsGreaterThanZero)
}
if (this._stock < quantity) {
throw new Error(ErrorText.LowStock)
}
this._stock -= quantity;
}
/**
* Class类转化为普通json数据
*/
toDTO(): Record<string, string | number> {
return {
id: this._id,
name: this._name,
description: this._description,
price: this._price,
imageUrl: this._imageUrl,
stock: this._stock,
categoryId: this._categoryId
}
}
/**
* 从json数据创建Product类实例
* 转化为领域实体 Product
*/
static fromDTO(data: Record<string, any>): Product {
return new Product(
data.id,
data.name,
data.description,
data.price,
data.imageUrl,
data.stock,
data.categoryId,
);
}
}

View File

@@ -1,7 +0,0 @@
export enum ErrorText {
PriceNotNegativein = "商品 价格 不能为负数",
StockNotNegativein = "商品 库存 不能为负数",
StockIsGreaterThanZero = "减少的库存数量必须大于0",
LowStock = "库存不足",
IdNotNull = "商品Id不能为空"
}

View File

@@ -1 +0,0 @@
# 临时占位,上传空文件夹,保存文件结构用,后面删除

View File

@@ -1,9 +0,0 @@
export interface ProductType {
id: number,
name: string,
description: string,
price: number,
imageUrl: string,
stock: number,
categoryId: number,
}

View File

@@ -1 +0,0 @@
# 临时占位,上传空文件夹,保存文件结构用,后面删除

View File

@@ -1,6 +0,0 @@
export * from './application/services/ProductApplicationService';
export * from './domain/entities/Product';
export * from './domain/enums/ErrorEnums';
export * from './domain/type/ProductType';
export * from './infr/api/HttpProductRepository';
export * from './ports/repositories/ProductRepository';

View File

@@ -1,27 +0,0 @@
import {Product} from "@domains/product/domain/entities/Product.ts";
import {ProductType} from "@domains/product/domain/type/ProductType.ts";
import {ProductRepository} from "@domains/product/ports/repositories/ProductRepository";
import {http} from "shared/infra";
import HttpConfig from "../config/HttpConfig";
/**
* HTTP商品仓储实现 - 实现商品数据的HTTP访问
* 这是基础设施层,负责具体的数据获取实现
*/
export class HttpProductRepository implements ProductRepository {
async findAll(): Promise<Array<ProductType>> {
const result: Array<ProductType> = await http.get({ url: HttpConfig.apiUrlList.productsListUrl });
const items = result.map((item) => Product.fromDTO(item))
return items;
}
async findById(id: number): Promise<Product> {
const result: ProductType = await http.get({
url: HttpConfig.apiUrlList.productsInfoUrl,
data: { id }
});
return Product.fromDTO(result);
}
}

View File

@@ -1,11 +0,0 @@
interface apiUrlListType {
productsListUrl: string
productsInfoUrl: string
}
export default class HttpConfig {
public static apiUrlList: apiUrlListType = {
productsListUrl: '/productsList',
productsInfoUrl: '/productsInfo',
}
}

View File

@@ -1,20 +0,0 @@
import { Product } from "../../domain/entities/Product"
import { ProductType } from "../../domain/type/ProductType"
/**
* 商品仓储接口 - 定义商品数据访问的契约
* 在DDD中端口定义了领域层与外部世界的交互方式
*/
export interface ProductRepository {
/**
* 获取商品列表
*/
findAll(): Promise<Array<ProductType>>;
/**
* 根据ID获取商品详情
* @param id 商品ID
*/
findById(id: number): Promise<Product>
}

View File

@@ -1 +0,0 @@
# 临时占位,上传空文件夹,保存文件结构用,后面删除

View File

@@ -1 +0,0 @@
# 临时占位,上传空文件夹,保存文件结构用,后面删除

View File

@@ -1 +0,0 @@
# 临时占位,上传空文件夹,保存文件结构用,后面删除

View File

@@ -1,17 +0,0 @@
import { RouteRecordRaw } from 'vue-router';
export const productRouter: Array<RouteRecordRaw> = [
{
path: '/Products',
name: 'ProductList',
component: () => import('../views/ProductList.jsx'),
meta: { title: '商品列表' }
},
{
path: '/ProductDetail',
name: 'ProductDetail',
component: () => import('../views/ProductDetail.jsx'),
meta: { title: '商品详情' }
},
]

View File

@@ -1,60 +0,0 @@
import {product} from '@domains/product/di/productModule';
import {ProductType} from '@domains/product/domain/type/ProductType';
import {defineComponent, onMounted, reactive} from 'vue'
import {useRoute} from 'vue-router'
interface storeType {
item: ProductType
id: number
}
export default defineComponent({
setup() {
const route = useRoute();
const productService = product.createProductApplicationService();
const state = reactive<storeType>({
item: {} as ProductType,
id: 0
})
onMounted(() => {
if (!route.query.id) {
throw new Error("缺少id参数");
}
state.id = Number(route.query.id)
console.log("id: ", state.id);
loadProductDetail()
})
const loadProductDetail = async () => {
state.item = await productService.getProductInfo(state.id);
console.log("state.item: ", state.item);
}
return () => (
<div class="ProductDetail">
<h1> - </h1>
<hr />
<p>id{state.id}</p>
{state.item.name}
<ul>
{
Object.entries(state.item).map(([key, value]) =>
<li>{key}: {value}</li>
)
}
</ul>
</div>
)
}
})
// export default defineComponent({
// setup() {
// return () => (
// <div>商品详情</div>
// )
// }
// })

View File

@@ -1,60 +0,0 @@
import {defineComponent, onMounted, reactive} from 'vue'
import {useRouter} from 'vue-router';
import {product} from '@domains/product/di/productModule';
import {ProductType} from '@domains/product/domain/type/ProductType';
interface storeType {
list: Array<ProductType>
}
export default defineComponent({
setup() {
const router = useRouter();
const productService = product.createProductApplicationService();
const state = reactive<storeType>({
list: []
})
onMounted(() => {
loadProductList()
})
const loadProductList = async () => {
state.list = await productService.getProductList();
console.log("state.list: ", state.list);
}
return () => (
<div class="ProductList">
<h1> - 3</h1>
<hr />
<h2>Deno </h2>
<ul>
{
state.list.map(item =>
<li key={item.id} onClick={() => {
router.push({
name: 'ProductDetail',
query: { id: item.id }
});
}}>
{item.id} | {item.name} | ¥{item.price}
</li>
)
}
</ul>
</div>
)
}
})
// export default defineComponent({
// setup() {
// return () => (
// <div>商品列表</div>
// )
// }
// })

View File

@@ -1,11 +0,0 @@
// import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App'
createApp(App)
.use(createPinia())
.mount('#app')

View File

@@ -1,3 +0,0 @@
declare module 'shared/infra' {
export const http: any;
}

View File

@@ -1,33 +0,0 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*",
"src/**/*.tsx",
"src/**/*.vue"
],
"exclude": [
"src/**/__tests__/*"
],
"compilerOptions": {
"verbatimModuleSyntax": false,
"jsx": "preserve",
"jsxImportSource": "vue",
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"paths": {
"@/*": [
"./src/app/*"
],
"@shared/*": [
"./src/shared/*"
],
"@features/*": [
"./src/features/*"
],
"@domains/*": [
"./src/domains/*"
]
}
}
}

View File

@@ -1,11 +0,0 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
]
}

View File

@@ -1,19 +0,0 @@
{
"extends": "@tsconfig/node22/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*",
"eslint.config.*"
],
"compilerOptions": {
"noEmit": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"]
}
}

View File

@@ -1,51 +0,0 @@
import {defineConfig} 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 {fileURLToPath, URL} from "node:url";
export default defineConfig({
server: {
port: 1301,
host: '0.0.0.0',
strictPort: true,
},
plugins: [
federation({
name: 'product',
remotes: {
shared: 'http://localhost:1333/assets/remoteEntry.js'
},
exposes: { // 暴露给主应用的资源(按功能分组)
'./ProductDomain': './src/domains/product/index.ts', // 领域层(实体、服务、接口等)
'./ProductRouter': './src/features/product/router/index.ts' // 商品路由
},
shared: {
vue: {import: false, generate: false, requiredVersion: '^3.5.18'},
'vue-router': {import: false, generate: false, requiredVersion: '^4.0.6'},
pinia: {import: false, generate: false, requiredVersion: '^3.0.3'},
}
}),
vue(),
vueJsx(),
vueDevTools()
],
build: {
target: 'esnext',
cssCodeSplit: true,
rollupOptions: {
output: {
minifyInternalExports: false
}
}
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src/', import.meta.url)),
'@shared': fileURLToPath(new URL('./src/shared/', import.meta.url)),
'@features': fileURLToPath(new URL('./src/features/', import.meta.url)),
'@domains': fileURLToPath(new URL('./src/domains/', import.meta.url))
},
},
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,23 +0,0 @@
{
"name": "app-shared",
"version": "1.0.0",
"description": "",
"keywords": [],
"author": "",
"license": "ISC",
"scripts": {
"dev": "vite",
"dep": "concurrently \"npm run build\" \"npm run preview\"",
"preview": "vite preview --port 1333 --strictPort",
"build": "vite build --watch",
"type-check": "vue-tsc --build"
},
"dependencies": {
"axios": "^1.12.2"
},
"devDependencies": {
"@originjs/vite-plugin-federation": "^1.4.1",
"typescript": "~5.8.0",
"vite": "^7.0.6"
}
}

View File

@@ -1,5 +0,0 @@
export class HttpConfig {
public static DevBaseUrl: string = "http://127.0.0.1:3000/api"
public static prodBaseUrl: string = "http://127.0.0.1:3000/api/"
public static apiList: string = ""
}

View File

@@ -1,4 +0,0 @@
export class ThemeConfig {
public static primaryColor: string = '#1890ff'; // 主色调
public static layoutType: 'side' | 'top' = 'side'; // 侧边栏布局
}

View File

@@ -1,2 +0,0 @@
export * from './HttpConfig';
export * from './ThemeConfig';

View File

@@ -1 +0,0 @@
# 临时占位,上传空文件夹,保存文件结构用,后面删除

View File

@@ -1 +0,0 @@
# 临时占位,上传空文件夹,保存文件结构用,后面删除

View File

@@ -1,3 +0,0 @@
export * from './infra';
export * from './utils';
export * from './config';

View File

@@ -1,93 +0,0 @@
import axios, {AxiosError, type AxiosInstance, type AxiosRequestConfig} from "axios";
import {UrlCheckUtils} from '../utils/index'
class AxiosInfra {
public instance: AxiosInstance;
public constructor() {
this.instance = axios.create({
baseURL: UrlCheckUtils.check(),
timeout: 10000,
headers: {
"Content-Type": "application/json"
}
});
this.ResponseInterceptor();
this.RequestInterceptor();
}
public async get(params: AxiosRequestConfig): Promise<any> {
return await this.instance.get(<string>params.url, {
params: params.data,
headers: Object.assign({ Authorization: localStorage.getItem("token") }, params.headers)
});
}
public async post(params: AxiosRequestConfig): Promise<any> {
return await this.instance.post(<string>params.url, params.data, {
headers: Object.assign({ Authorization: localStorage.getItem("token") }, params.headers)
})
}
/**
* Put 请求
* 请求参数请参考接口RequestParams
* @param params
*/
public async put(params: AxiosRequestConfig): Promise<any> {
return await this.instance.put(<string>params.url, params.data)
}
/**
* delete 请求
* 请求参数请参考接口RequestParams
* @param params
*/
public async delete(params: AxiosRequestConfig): Promise<any> {
return await this.instance.delete(<string>params.url, { params: params.data })
}
/**
* 响应拦截器
* @constructor
*/
public async ResponseInterceptor(): Promise<any> {
this.instance.interceptors.response.use(
(response: any) => {
return response.data.result;
},
(error: AxiosError) => {
console.log(`
❌ 请求错误 ❌
1、基地址: ${error.config?.baseURL}
2、短地址: ${error.config?.url}
3、请求 方式: ${error.config?.method}
4、请求 参数: ${JSON.stringify(error.config?.params)}
5、请求 头 : ${JSON.stringify(error.config?.headers)}
------------------------------------------------------------
6、错误信息为: ${error.message}
7、错误代码为: ${error.stack}
`);
return Promise.reject(error)
})
}
/**
* 添加请求拦截器
* @constructor
*/
public async RequestInterceptor(): Promise<any> {
this.instance.interceptors.request.use((config: any) => {
return config
}, function (error: AxiosError) {
return Promise.reject(error)
})
}
}
export default new AxiosInfra();

View File

@@ -1 +0,0 @@
export {default as http} from "./AxiosInfra"

View File

@@ -1 +0,0 @@
# 临时占位,上传空文件夹,保存文件结构用,后面删除

View File

@@ -1 +0,0 @@
/// <reference types="vite/client" />

View File

@@ -1,12 +0,0 @@
import {HttpConfig} from '../config'
export class UrlCheckUtils {
public static check() {
if (import.meta.env.MODE === 'production') {
return HttpConfig.prodBaseUrl;
} else {
return HttpConfig.DevBaseUrl;
}
}
}

View File

@@ -1 +0,0 @@
export * from './UrlCheckUtils';

View File

@@ -1,40 +0,0 @@
{
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*"
],
"exclude": [
"node_modules",
"dist",
"**/*.test.ts"
],
"compilerOptions": {
"moduleResolution": "bundler",
"verbatimModuleSyntax": false,
"target": "ES2020",
"module": "ESNext",
// 模块导出优化
"resolveJsonModule": true,
// 允许导入JSON文件
"isolatedModules": true,
// 确保每个文件可被独立转译Vite等工具要求
"skipLibCheck": true,
// 跳过库类型检查,加快编译速度
"paths": {
"@/*": [
"./src/app/*"
],
"@shared/*": [
"./src/shared/*"
],
"@features/*": [
"./src/features/*"
],
"@domains/*": [
"./src/domains/*"
]
}
}
}

View File

@@ -1,36 +0,0 @@
import {defineConfig} from 'vite'
import federation from '@originjs/vite-plugin-federation';
// https://vite.dev/config/
export default defineConfig({
server: {
port: 1333,
host: '0.0.0.0',
strictPort: true,
},
plugins: [
federation({
name: 'shared',
exposes: {
"./config": "./src/config/index.ts",
"./infra": "./src/infra/index.ts",
"./utils": "./src/utils/index.ts",
},
shared: {
axios: {requiredVersion: '^1.12.2'}
}
})
],
build: {
target: 'esnext',
minify: false,
cssCodeSplit: true,
rollupOptions: {
input: './src/index.ts',
output: {
format: 'esm',
minifyInternalExports: false
}
}
}
})

View File

@@ -0,0 +1,33 @@
# 请求协议 http or https建议线上配置证书后使用https
VITE_PREFIX=http://
#
# Nacos 集群的地址
# 注意:
# 1: 内网微前端模块直接填内网Nacos IP:端口 就行
# 2: main-app位于公网填 网关的公网地址
#
# 网关地址
VITE_NACOS_ADDRESS=nacos.ddd.com:80
# VITE_NACOS_ADDRESS=192.168.139.84:8848
# 注册到 Nacos 的服务名称(微前端模块名称)
VITE_SERVER_NAME=main-app
# 注册到 Nacos 的端口,也是前端运行的端口
VITE_SERVER_PORT=1300
#
# 说明:项目要调用哪些 微前端 模块
# 格式VITE_MODEL_XXXX_NAME=微前端模块名称(模块 的 开发者提供)
# 作用通过名称从Nacos配置中心发现模块地址和端口给 federation 实时加载
#
# product 商品模块
VITE_MODEL_PRODUCT_NAME=app-product
# shared 共享资源模块
VITE_MODEL_SHARED_NAME=app-shared

View File

@@ -0,0 +1,33 @@
# 请求协议 http or https建议线上配置证书后使用https
VITE_PREFIX=http://
#
# Nacos 集群的地址
# 注意:
# 1: 内网微前端模块直接填内网Nacos IP:端口 就行
# 2: main-app位于公网填 网关的公网地址
#
# 网关地址
VITE_NACOS_ADDRESS=nacos.ddd.com:80
# VITE_NACOS_ADDRESS=192.168.139.84:8848
# 注册到 Nacos 的服务名称(微前端模块名称)
VITE_SERVER_NAME=main-app
# 注册到 Nacos 的端口,也是前端运行的端口
VITE_SERVER_PORT=1300
#
# 说明:项目要调用哪些 微前端 模块
# 格式VITE_MODEL_XXXX_NAME=微前端模块名称(模块 的 开发者提供)
# 作用通过名称从Nacos配置中心发现模块地址和端口给 federation 实时加载
#
# product 商品模块
VITE_MODEL_PRODUCT_NAME=app-product
# shared 共享资源模块
VITE_MODEL_SHARED_NAME=app-shared

View File

@@ -3,9 +3,6 @@
"version": "0.0.0",
"private": true,
"type": "module",
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"scripts": {
"dev": "vite",
"dep": "concurrently \"npm run build\" \"npm run preview\"",
@@ -15,19 +12,21 @@
},
"dependencies": {
"pinia": "^3.0.3",
"vue": "^3.5.18",
"vue-router": "^4.0.6"
"vite-plugin-nacos": "link:../vite-plugin-nacos",
"vue": "^3.5.22",
"vue-facing-decorator": "^4.0.1",
"vue-router": "^4.5.1"
},
"devDependencies": {
"@abraham/reflection": "^0.13.0",
"@originjs/vite-plugin-federation": "^1.4.1",
"@types/node": "^24.5.2",
"@element-plus/icons-vue": "^2.3.2",
"@types/node": "^24.6.1",
"@vue3-oop/plugin-vue-jsx": "^1.5.1",
"injection-js": "^2.5.0",
"element-plus": "^2.11.4",
"rollup-plugin-visualizer": "^6.0.3",
"tslib": "^2.8.1",
"typescript": "~5.8.0",
"vite": "^7.0.6",
"vite-plugin-vue-devtools": "^8.0.0",
"vue3-oop": "^1.2.1"
"typescript": "~5.9.3",
"vite": "^7.1.7",
"vite-federation": "^3.4.4",
"vite-plugin-vue-devtools": "^8.0.2"
}
}
}

View File

@@ -1,8 +1,8 @@
import { Component, VueComponent } from 'vue3-oop'
import { Component, Vue } from 'vue-facing-decorator'
import { RouterLink, RouterView } from "vue-router";
@Component()
export class App extends VueComponent {
export default class App extends Vue {
render() {
return (
<>
@@ -15,4 +15,4 @@ export class App extends VueComponent {
</>
)
}
}
}

View File

@@ -1,86 +0,0 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

Before

Width:  |  Height:  |  Size: 276 B

View File

@@ -1,35 +0,0 @@
@import './base.css';
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
font-weight: normal;
}
a,
.green {
text-decoration: none;
color: hsla(160, 100%, 37%, 1);
transition: 0.4s;
padding: 3px;
}
@media (hover: hover) {
a:hover {
background-color: hsla(160, 100%, 37%, 0.2);
}
}
@media (min-width: 1024px) {
body {
display: flex;
place-items: center;
}
#app {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 0 2rem;
}
}

View File

@@ -1,54 +1,45 @@
import { Component, ComponentProps, Hook, useProps, VueComponent } from 'vue3-oop'
import { JSX } from "vue/jsx-runtime";
import { VNodeChild } from "vue";
import { Component, Prop, TSX, Vue } from 'vue-facing-decorator'
interface Props {
text?: string
list?: ListItemType[]
}
interface ListItemType {
id: number
title: string
}
interface HeaderProps {
text?: string;
list: ListItemType[]
slots: {
default: () => JSX.Element;
haveName: () => JSX.Element;
item: (val: ListItemType) => VNodeChild;
}
}
@Component()
export default class Header extends VueComponent<HeaderProps> {
export default class Header extends TSX<Props>()(Vue) implements Props {
// required 的 true or false 需要和 类型中 text的? 对应
static defaultProps: ComponentProps<HeaderProps> = {
text: { default: '默认数值', required: true },
list: { default: () => [] }
};
@Prop({ default: '默认数值' })
text!: string;
@Hook('Mounted')
mounted() {
console.log("this: ", this);
console.log('Header 组件的 mounted 生命周期...')
@Prop()
list!: ListItemType[];
created() {
console.log("created: ", this)
}
render() {
const { text, list } = useProps<HeaderProps>();
return (
<div class="header">
<p>{ text }</p>
{ this.context.slots.default?.() }
{ this.context.slots.haveName?.() }
<p>{ this.text }</p>
{ this.$slots.default?.() }
{ this.$slots.haveName?.() }
<ul>
{
list.map(
this.list.map(
item =>
this.context.slots.item?.(item)
this.$slots.item?.(item)
)
}
</ul>
</div>
)
}
}
}

View File

@@ -1,21 +1,30 @@
// import './assets/main.css'
import '@abraham/reflection'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { App } from './App'
import App from './App'
import router from './router'
import { initNormalizeStyles } from "shared/normalize";
import elementPlus from "shared/element-plus";
import { initPubStyles } from 'shared/pub';
class Main {
constructor() {
createApp(App)
.use(createPinia())
const app = createApp(App)
app.use(createPinia())
.use(router)
.mount('#app')
// TODO 需要解决的地方
console.log("import.meta.env: ", import.meta.env)
window.__APP_CONFIG__ = import.meta.env;
elementPlus.init(app)
initNormalizeStyles()
initPubStyles()
}
}
new Main

View File

@@ -1,17 +1,17 @@
import {productRouter} from 'product/ProductRouter'
import {RouteRecordRaw} from 'vue-router'
import { productRouter } from 'product/ProductRouter'
import { RouteRecordRaw } from 'vue-router'
const baseRouter: Array<RouteRecordRaw> = [
{
path: '/',
name: 'home',
component: () => import('../views/Home'),
component: () => import('../views/Home/Home'),
},
{
path: '/about',
name: 'about',
component: () => import('../views/About'),
component: () => import('../views/About/About'),
},
{
path: '/:pathMatch(.*)*',
@@ -21,5 +21,5 @@ const baseRouter: Array<RouteRecordRaw> = [
];
// 聚合所有功能模块的路由
const routes: RouteRecordRaw[] = [...baseRouter, ...productRouter];
const routes: RouteRecordRaw[] = [ ...baseRouter, ...productRouter ];
export default routes;

View File

@@ -1,12 +0,0 @@
import {Component, VueComponent} from 'vue3-oop'
@Component()
export default class About extends VueComponent {
render() {
return (
<div class="about">
<h1></h1>
</div>
)
}
}

View File

@@ -0,0 +1,4 @@
.About {
}

View File

@@ -0,0 +1,14 @@
import styles from "./About.module.less";
import { Component, Vue } from 'vue-facing-decorator'
@Component()
export default class About extends Vue {
render() {
return (
<div class={ styles.About }>
<h1></h1>
</div>
)
}
}

View File

@@ -1,51 +0,0 @@
import { Autobind, Component, Link, Mut, VueComponent } from 'vue3-oop'
import Header from '@/components/Header'
interface ListItemType {
id: number
title: string
}
@Component()
export default class Home extends VueComponent {
@Mut()
count: number = 1
@Mut()
text: string = "Home props 传给 Header 的字符串"
@Mut()
newList: ListItemType[] = [
{ id: 1, title: '新闻1' },
{ id: 2, title: '新闻2' },
{ id: 3, title: '新闻3' },
]
@Link() H1?: HTMLDivElement
@Autobind()
async addCount() {
this.count++
this.text = "22222"
console.log(this.H1)
}
render() {
return (
<div class="home" ref="H1">
<h1></h1>
<p onClick={ this.addCount }>count响应式变量{ this.count }</p>
<Header
v-slots={ {
default: () => <div class="default"> default </div>,
haveName: () => <div class="haveName"> </div>,
item: (val) => <li key={ val.id }>{ val.title }</li>
} }
list={ this.newList }
text={ this.text }>
</Header>
</div>
)
}
}

View File

@@ -0,0 +1,5 @@
.Home {
.title {
color: var(--theme-color);
}
}

View File

@@ -0,0 +1,76 @@
import styles from "./Home.module.less";
import { Component, Ref, Setup, Vue } from 'vue-facing-decorator'
import Header from '@/components/Header'
import { RouteLocationNormalizedLoaded, Router, useRoute, useRouter } from "vue-router";
import { ElButton, Icons } from "shared/element-plus";
interface ListItemType {
id: number
title: string
}
@Component()
export default class Home extends Vue {
counter: number = 1;
@Ref
readonly refH1!: HTMLDivElement
@Setup((props, ctx) => useRouter())
router!: Router
@Setup((props, ctx) => useRoute())
route!: RouteLocationNormalizedLoaded
newList: ListItemType[] = [
{ id: 1, title: '新闻1' },
{ id: 2, title: '新闻2' },
{ id: 3, title: '新闻3' },
]
async created() {
}
async addCount() {
// console.log(await nacos-cluster.find('app-product'))
// console.log("ref dom: ", this.refH1)
// this.router.push({ name: 'about' })
// this.route.query
this.counter++
}
testHtml() {
return <p>html jsx </p>
}
render() {
return (
<div class={ styles.Home }>
<h1 ref="refH1" class={ styles.title }></h1>
<ElButton type="primary" icon={ Icons.Search }>element-plus按钮</ElButton>
<p onClick={ this.addCount }>counter { this.counter }</p>
<Header
list={ this.newList }
text={ "测试" }
// v-slots= { {
// default: () => <div class="default">父到子 default 插槽</div>,
// haveName: () => <div class="haveName">父到子 具名 插槽</div>,
// item: (val: ListItemType) => <li key={ val.id }>{ val.title }</li>
// } }
>
{ {
default: () => <div class="default"> default </div>,
haveName: () => <div class="haveName"> </div>,
item: (val: ListItemType) => <li key={ val.id }>{ val.title }</li>
} }
</Header>
{ this.testHtml() }
</div>
)
}
}

View File

@@ -1,11 +1,24 @@
import { defineComponent } from 'vue'
// import { defineComponent } from 'vue'
//
// export default defineComponent({
// setup() {
// return () => (
// <div class="404">
// <h1>404</h1>
// </div>
// )
// }
// })
export default defineComponent({
setup() {
return () => (
import { Component, Vue } from 'vue-facing-decorator'
@Component()
export default class About extends Vue {
render() {
return (
<div class="404">
<h1>404</h1>
</div>
)
}
})
}

View File

@@ -0,0 +1,40 @@
import { nacosServiceManage, RegisterOptions } from "nacos-federation"
import { loadEnv } from 'vite';
console.log("process.env.NODE_ENV: ", process.env.NODE_ENV)
const env = loadEnv(
process.env.NODE_ENV || 'development', // 环境模式development/production
process.cwd() // 项目根目录(.env文件所在位置
);
// 2. 定义配置同步执行无await
const nacosConfig: RegisterOptions = {
serverList: env.VITE_NACOS_ADDRESS,
serviceName: env.VITE_SERVER_NAME,
port: Number(env.VITE_SERVER_PORT),
namespace: env.VITE_NACOS_NAMESPACE || 'public'
};
// 3. 声明实例变量(先不初始化)
let nacosInstance: nacosServiceManage | null = null;
// 4. 提供初始化函数显式调用避免顶级await
export async function initNacosInstance() {
if (nacosInstance) {
return nacosInstance; // 已初始化则直接返回
}
// 首次调用时初始化
nacosInstance = await nacosServiceManage.create(nacosConfig);
console.log('Nacos 实例初始化完成');
return nacosInstance;
}
// 5. 提供获取实例的方法(确保初始化后使用)
export async function getNacosInstance() {
if (!nacosInstance) {
// 若未初始化,先执行初始化
return await initNacosInstance();
}
return nacosInstance;
}

View File

@@ -1 +1,16 @@
/// <reference types="vite/client" />
interface ViteTypeOptions {
// 添加这行代码,你就可以将 ImportMetaEnv 的类型设为严格模式,
// 这样就不允许有未知的键值了。
// strictImportMetaEnv: unknown
}
interface ImportMetaEnv {
// readonly VITE_APP_TITLE: string
// 更多环境变量...
}
interface ImportMeta {
readonly env: ImportMetaEnv
}

View File

@@ -0,0 +1,11 @@
export interface AppConfig {
[key: string]: any;
}
declare global {
interface Window {
__APP_CONFIG__: AppConfig;
}
}
export {};

View File

@@ -10,9 +10,11 @@ declare module 'product/ProductDomain' {
stock: number;
categoryId: number;
}
// 应用服务类型
// 应用服务类型
export class ProductApplicationService {
constructor(repo: ProductRepository);
findAll(): Promise<Array<any>>; // TODO any 需要修改
findById(id: number): Promise<Product>
}
@@ -39,6 +41,4 @@ declare module 'product/ProductRouter' {
export const productRouter: RouteRecordRaw[];
}
declare module 'shared/utils' {
export const UrlCheckUtils: any;
}

View File

@@ -0,0 +1,30 @@
/// <reference types="vite/client" />
declare module 'shared/utils' {
export const UrlCheckUtils: any;
}
// 2. 为 "shared/element-plus" 模块提供精确的类型声明
declare module 'shared/element-plus' {
import * as ElementPlus from 'element-plus';
import * as ElementPlusIcons from '@element-plus/icons-vue';
import type { App } from 'vue';
export * from 'element-plus';
export import Icons = ElementPlusIcons;
export function init(app: App): void;
const defaultExport: typeof ElementPlus & {
Icons: typeof ElementPlusIcons;
init: typeof init;
};
export default defaultExport;
}
declare module 'shared/normalize' {
export const initNormalizeStyles: () => void;
}
declare module 'shared/pub' {
export function initPubStyles(): void;
}

View File

@@ -1,5 +1,5 @@
declare module '*.vue' {
import type {DefineComponent} from 'vue'
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}

View File

@@ -3,9 +3,7 @@
"target": "ESNext",
"module": "ESNext",
"importHelpers": true,
"isolatedModules": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"useDefineForClassFields": false,
"moduleResolution": "Bundler",
"strict": true,
@@ -16,7 +14,8 @@
"lib": [
"ES2020",
"dom",
"dom.iterable"
"dom.iterable",
"WebWorker"
],
"skipLibCheck": true,
"paths": {

View File

@@ -1,47 +1,55 @@
import {fileURLToPath, URL} from 'node:url'
import path from 'path'
import {defineConfig} from 'vite'
import { defineConfig, loadEnv, ConfigEnv, mergeConfig } from 'vite'
import vueJsx from '@vue3-oop/plugin-vue-jsx'
import federation from 'vite-federation';
import { reg, init } from 'vite-plugin-nacos';
import { visualizer } from 'rollup-plugin-visualizer';
import federation from '@originjs/vite-plugin-federation';
export default defineConfig(async (config: ConfigEnv) => {
const env = loadEnv(config.mode, process.cwd());
export default defineConfig({
server: {
port: 1300,
host: '0.0.0.0',
strictPort: true,
},
plugins: [
vueJsx(),
federation({
name: 'mainApp', // 主应用模块名(联邦模块需通过此名引用)
remotes: { // 配置远程联邦模块地址(开发/生产需对应)
product: 'http://localhost:1301/assets/remoteEntry.js',
shared: 'http://localhost:1333/assets/remoteEntry.js'
},
shared: {
vue: { requiredVersion: '^3.5.18' },
'vue-router': { requiredVersion: '^4.0.6' },
pinia: { requiredVersion: '^3.0.3' }
}
})
],
build: {
target: 'esnext',
minify: false,
cssCodeSplit: true,
rollupOptions: {
output: {
minifyInternalExports: false
}
}
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src/app/', import.meta.url)),
'@shared': fileURLToPath(new URL('./src/shared/', import.meta.url)),
'@features': fileURLToPath(new URL('./src/features/', import.meta.url)),
'@domains': fileURLToPath(new URL('./src/domains/', import.meta.url))
},
},
return mergeConfig({
plugins: [
reg(),
federation({
name: env.VITE_SERVER_NAME,
remotes: {
product: `${env.VITE_PREFIX}${env.VITE_NACOS_ADDRESS}/${env.VITE_MODEL_PRODUCT_NAME}/assets/remoteEntry.js`,
shared: `${env.VITE_PREFIX}${env.VITE_NACOS_ADDRESS}/${env.VITE_MODEL_SHARED_NAME}/assets/remoteEntry.js`
},
shared: {
vue: { requiredVersion: '^3.5.18' },
'vue-router': { requiredVersion: '^4.0.6' },
pinia: { requiredVersion: '^3.0.3' }
}
}),
vueJsx(),
visualizer({
title: "主应用依赖分析",
filename: 'dist/stats.html', // 分析文件输出路径
open: false, // 构建完成后自动打开
gzipSize: true, // 显示gzip后的大小
brotliSize: true, // 显示brotli后的大小
})
],
build: {
target: 'esnext',
minify: false,
cssCodeSplit: true,
rollupOptions: {
output: {
minifyInternalExports: false
}
}
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src/app/'),
'@shared': path.resolve(__dirname, './src/shared/'),
'@features': path.resolve(__dirname, './src/features/'),
'@domains': path.resolve(__dirname, './src/domains/')
},
},
}, await init(env))
})

View File

@@ -1,4 +0,0 @@
packages:
- 'main-app'
- 'app-product'
- 'app-shared'

View File

@@ -0,0 +1,32 @@
功能分析:
1 注册服务
子模块直接通过nacos 注册,给 内网 的 使用
主模块main-app -> Traefik -> ConfigServernacos-federation -> Nacos
main-app 发送 http 请求
/nacos/reg
参数:
ipport
2 获取配置
子模块:直接通过 nacos 获取配置
主模块: main-app -> Traefik -> ConfigServernacos-federation -> Nacos
注意:
增加功能:
认证授权机制
请求限流保护
操作日志记录
服务治理功能(如权重调整、元数据管理等)

View File

@@ -0,0 +1,31 @@
{
"name": "vite-plugin-nacos",
"version": "1.1.3",
"description": "Vite 插件实现Nacos服务注册与发现插件用于模块联邦",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"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.6.1",
"typescript": "~5.9.3",
"vite": "^7.1.7"
},
"keywords": [
"nacos",
"module-federation",
"service-discovery"
],
"author": "",
"license": "MIT"
}

View File

@@ -0,0 +1,96 @@
// 从插件实现文件导入
export * from './vite-plugin-nacos';
export * from './utils';
import { nacosServiceManage } from './nacosServiceManage';
import { mergeConfig } from 'vite';
export const init = async (env: Record<string, string>) => {
const serverList = env.VITE_NACOS_ADDRESS;
const serviceName = env.VITE_SERVER_NAME;
const port = Number(env.VITE_SERVER_PORT);
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

@@ -0,0 +1,179 @@
import { NacosConfigClient, NacosNamingClient } from 'nacos';
import { formatServiceUrl, getLocalIP } from "./utils";
import chalk from 'chalk';
const logger = console;
interface RegisterOptions {
serverList: string;
/** 服务名称(如 'main-app' */
serviceName: string;
/** 服务端口号 */
port: number;
/** 命名空间(用于环境隔离,默认 'public' */
namespace?: string;
}
export class nacosServiceManage {
// 静态变量缓存单例实例key为配置唯一标识value为实例
private static instanceMap: Map<string, nacosServiceManage> = new Map();
public nacosClient!: NacosNamingClient;
public configClient!: NacosConfigClient;
public initOptions!: RegisterOptions;
private constructor(option: RegisterOptions) {
this.initOptions = option;
}
/**
* 静态 create 方法:单例模式入口,同一配置只返回一个实例
* @param option 初始化配置
* @returns nacosServiceManage 实例(单例)
*/
public static async create(option: RegisterOptions): Promise<nacosServiceManage> {
const configKey = this.getConfigKey(option);
if (this.instanceMap.has(configKey)) {
logger.log(`[nacos-federation] 已存在 ${option.serviceName} 的Nacos实例直接复用`);
return this.instanceMap.get(configKey)!;
}
const instance = new nacosServiceManage(option);
// await instance.initClient();
await instance.initConfig();
this.instanceMap.set(configKey, instance);
return instance;
}
// 服务注册
public async initClient() {
this.nacosClient = new NacosNamingClient({
logger,
serverList: this.initOptions.serverList,
namespace: this.initOptions.namespace || 'public',
});
await this.nacosClient.ready();
this.reg()
}
/**
* 生成配置唯一标识:基于 serverList + namespace + serviceName确保不同配置对应不同实例
*/
private static getConfigKey(option: RegisterOptions): string {
const namespace = option.namespace || 'public';
return `${option.serverList}-${namespace}-${option.serviceName}`;
}
/**
* 订阅配置变化
* @param dataId
* @param group
*/
async subscribe(dataId: string, group: string = "DEFAULT_GROUP") {
return new Promise(async (resolve, reject) => {
this.configClient.subscribe({ dataId, group }, (content: any) => {
resolve(content)
});
})
}
/**
* 获取配置
* @param dataId
* @param group
*/
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)}`)
return res;
}
/**
* 从Nacos发现服务地址
* @param serviceName 发现选项
* @returns 服务地址(如 'http://192.168.1.101:1301'
*/
public async find(serviceName: string) {
const instances = await this.nacosClient.getAllInstances(serviceName);
// console.log("instances: ", instances);
if (instances.length === 0) {
throw new Error(`[nacos-federation] 未发现服务 ${serviceName} 的实例`);
}
return formatServiceUrl(instances[0].ip, instances[0].port);
}
/**
* 初始化配置服务
* @private
*/
private async initConfig() {
this.configClient = new NacosConfigClient({
serverAddr: this.initOptions.serverList,
});
}
/**
* 注册服务到Nacos并返回客户端实例
*/
private async reg() {
const { serviceName, port } = this.initOptions;
const ip = getLocalIP();
// @ts-ignore
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)}) 已注册到配置中心!`);
}
/**
* 手动销毁实例(如服务下线时)
*/
public async destroy() {
const configKey = nacosServiceManage.getConfigKey(this.initOptions);
// 注销 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

@@ -0,0 +1,52 @@
import { networkInterfaces } from "os";
class Utils {
/**
* 获取本机局域网IPv4地址
* @returns 局域网IP地址如 '192.168.1.100'
*/
static getLocalIP(): string {
const interfaces = networkInterfaces();
for (const devName in interfaces) {
const iface = interfaces[devName];
if (!iface) continue;
for (const alias of iface) {
if (
alias.family === 'IPv4' &&
alias.address !== '127.0.0.1' &&
!alias.internal
) {
return alias.address;
}
}
}
return '127.0.0.1';
}
/**
* 格式化服务地址
* @param ip IP地址
* @param port 端口号
* @returns 完整服务地址(如 'http://192.168.1.100:1300'
*/
static formatServiceUrl(ip: string, port: number): string {
return `http://${ ip }:${ port }`;
}
/**
* VITE_DEPEND_MODEL_NAME_LIST 配置 格式化成数组
* app-shared,app-test
* @param arg ["app-shared", "app-test"]
*/
static dependModel(arg: string): string[] {
return arg.split(',').map(name => name.trim()).filter(Boolean) || [];
}
}
export const {
getLocalIP, formatServiceUrl,
dependModel
} = Utils

Some files were not shown because too many files have changed in this diff Show More