Compare commits
54 Commits
02cae30c4c
...
test
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9432116a00 | ||
|
|
5fda36b268 | ||
|
|
84820b1e9b | ||
|
|
550bf0039b | ||
|
|
2653800c3f | ||
|
|
cb8f7a4682 | ||
|
|
7fb0f60ec0 | ||
|
|
ff925d458a | ||
|
|
3c66faf9a7 | ||
|
|
a3ba6c2944 | ||
|
|
04754a1f77 | ||
|
|
ced7662d98 | ||
|
|
174af7efc3 | ||
|
|
537c7ca60e | ||
|
|
21bf47c8a1 | ||
|
|
ac5f544819 | ||
|
|
258318eee8 | ||
|
|
1f5c0556a3 | ||
|
|
1791889441 | ||
|
|
84c4e8bdc7 | ||
|
|
cbc2c25cf4 | ||
|
|
b29001c8c3 | ||
|
|
c8a9bfd77e | ||
|
|
135839d6e2 | ||
|
|
bfe5f57378 | ||
|
|
dabb91ef1e | ||
|
|
e038fdb965 | ||
|
|
32d40e1fa3 | ||
|
|
b3718c4097 | ||
|
|
79a0c9a451 | ||
|
|
f2b53e46db | ||
|
|
f36d1fc413 | ||
|
|
687154f0be | ||
|
|
6c5b25e0d7 | ||
|
|
64db72b3d6 | ||
|
|
886308ee90 | ||
|
|
05bcc3376c | ||
|
|
4e06a0caed | ||
|
|
523e115906 | ||
|
|
1e16236e13 | ||
|
|
064d368ae4 | ||
|
|
75c50d8c97 | ||
|
|
cc23a6e1f0 | ||
|
|
327e80d71e | ||
|
|
af2b3badd0 | ||
|
|
c26c2ed5d5 | ||
|
|
3d0c69c98e | ||
|
|
ca24ab9922 | ||
|
|
7bc20d517b | ||
|
|
55004f2a8c | ||
|
|
dbb7515f8f | ||
|
|
1a435fd81b | ||
|
|
3e56103538 | ||
|
|
ab36c7e805 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,7 +1,10 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
dist
|
||||
docker/nacos-cluster/standalone-logs/*
|
||||
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
@@ -13,6 +16,7 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
|
||||
255
README.md
255
README.md
@@ -2,11 +2,42 @@
|
||||
|
||||
> 持续开发中,及时拉取代码,文档还不够完善,先凑合看!!目前只是demo,代码还会进行大规模封装细化和改造!
|
||||
|
||||
搭建的一套**基于DDD领域驱动设计的微服务前端**,适用于大型企业,开发大型复杂且拓展灵活的前端应用的架构方案。
|
||||
搭建的一套 **基于DDD领域驱动设计的微服务前端** 项目脚手架,适用于大型企业,开发大型复杂且拓展灵活的前端应用的架构方案。
|
||||
|
||||
> 微服务架构中,前端用什么语言不受约束,随意就好!
|
||||
代码编写建议使用 **OOP面向对象 + TypeScript + JSX** 进行开发,让项目更适合开发和维护,获得更好更极致的编码体验。
|
||||
|
||||
本案例中 **[主应用]** 采取的就是OOP风格,当然如果你不熟悉上面的写法,那么可以在
|
||||
【其他模块】中使用Vue2,Vue3都行,但仍然需要遵守DDD的规范进行开发!
|
||||
|
||||
## 架构图
|
||||
|
||||

|
||||
|
||||
## CI/CD 自动化集群部署
|
||||
|
||||

|
||||
|
||||
## 开发日记
|
||||
|
||||
- [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)
|
||||
- [@originjs/vite-plugin-federation](https://www.npmjs.com/package/@originjs/vite-plugin-federation)
|
||||
- [Deno](https://deno.com/)
|
||||
@@ -14,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)
|
||||
@@ -21,35 +54,34 @@
|
||||
- [axios](https://axios-http.com/)
|
||||
- 等等等....
|
||||
|
||||
## 开发任务
|
||||
|
||||
- [x] 基于DDD理念改造
|
||||
- [ ] 基于 vite-plugin-federation 拆分领域模块为联邦模块,实现微前端
|
||||
- [ ] 领域模块实现 docker自动化构建部署上线
|
||||
- [ ] Deno集成GraphQL
|
||||
|
||||
## 文件夹结构
|
||||
|
||||
**根目录:**
|
||||
|
||||
```txt
|
||||
.
|
||||
├── back-end # 【后端】基于Deno写的后端 测试接口,仅供测试
|
||||
├── doc # 一些markdown笔记
|
||||
├── front-end # 前端 微服务 项目
|
||||
│ ├── app-car # 独立的【购物车】领域模块
|
||||
│ ├── app-product # 独立的【商品】领域模块
|
||||
│ ├── .... # 实际项目还会有更多其他的领域模块
|
||||
│ └── 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)
|
||||
@@ -78,7 +110,7 @@ src/
|
||||
│ ├── router/ # 订单相关的页面级Vue组件
|
||||
│ ├── composables/ # 订单相关的Vue组合式函数
|
||||
│ └── index.ts # 订单功能模块的出口
|
||||
├── shared/ # 共享资源
|
||||
├── shared/ # 【app-shared模块】共享资源
|
||||
│ ├── infra(infrastructure) # 基础设施层
|
||||
│ │ ├── http/
|
||||
│ ├── lib/ # 第三方库的封装/工具函数
|
||||
@@ -90,86 +122,143 @@ src/
|
||||
└── index.html # 主HTML文件
|
||||
```
|
||||
|
||||
## 代码风格
|
||||
|
||||
## 问 & 答
|
||||
```tsx
|
||||
// 首页 页面
|
||||
import { Component, Ref, Setup, Vue } from 'vue-facing-decorator'
|
||||
import Header from '../components/Header'
|
||||
import { RouteLocationNormalizedLoaded, Router, useRoute, useRouter } from "vue-router";
|
||||
|
||||
问: 为什么vue项目要抛弃SFC写法而尝试jsx/tsx方式来编码?
|
||||
答:https://juejin.cn/post/6911175470255964174
|
||||
interface ListItemType {
|
||||
id: number
|
||||
title: string
|
||||
}
|
||||
|
||||
@Component()
|
||||
export default class Home extends Vue {
|
||||
|
||||
问:不会 vue3 jsx 语法,怎么办?
|
||||
答:https://juejin.cn/post/7141674726434439176
|
||||
counter: number = 1;
|
||||
|
||||
@Ref
|
||||
readonly refH1!: HTMLDivElement
|
||||
|
||||
问:vue3 jsx defineComponent 语法,实际项目中 setup 代码太长,怎么办?
|
||||
答:使用组合式函数 (Composables)
|
||||
@Setup((props, ctx) => useRouter())
|
||||
router!: Router
|
||||
|
||||
案例:
|
||||
@Setup((props, ctx) => useRoute())
|
||||
route!: RouteLocationNormalizedLoaded
|
||||
|
||||
```jsx
|
||||
// composables/useUserData.js
|
||||
import { ref, onMounted } from 'vue'
|
||||
import api from '@/api'
|
||||
newList: ListItemType[] = [
|
||||
{ id: 1, title: '新闻1' },
|
||||
{ id: 2, title: '新闻2' },
|
||||
{ id: 3, title: '新闻3' },
|
||||
]
|
||||
|
||||
export function useUserData(userId) {
|
||||
const user = ref(null)
|
||||
const loading = ref(false)
|
||||
const error = ref(null)
|
||||
|
||||
const fetchUser = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
user.value = await api.getUser(userId)
|
||||
} catch (err) {
|
||||
error.value = err.message
|
||||
} finally {
|
||||
loading.value = false
|
||||
addCount() {
|
||||
// console.log("ref dom: ", this.refH1)
|
||||
// this.router.push({ name: 'about' })
|
||||
// this.route.query
|
||||
this.counter++
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(fetchUser)
|
||||
testHtml() {
|
||||
return <p>测试html jsx 函数</p>
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
loading,
|
||||
error,
|
||||
refetch: fetchUser
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div class="home">
|
||||
<h1 ref="refH1">首页</h1>
|
||||
<p onClick={ this.addCount }>counter 响应式变量(点我):{ this.counter }</p>
|
||||
<Header
|
||||
list={ this.newList }
|
||||
text={ "测试" }>
|
||||
{ {
|
||||
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>
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
主组件:
|
||||
```tsx
|
||||
// 头部组件
|
||||
import { Component, Prop, TSX, Vue } from 'vue-facing-decorator'
|
||||
|
||||
```jsx
|
||||
// UserProfile.jsx
|
||||
import { defineComponent } from 'vue'
|
||||
import { useUserData } from '@/composables/useUserData'
|
||||
interface Props {
|
||||
text?: string
|
||||
list?: ListItemType[]
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'UserProfile',
|
||||
interface ListItemType {
|
||||
id: number
|
||||
title: string
|
||||
}
|
||||
|
||||
props: {
|
||||
userId: {
|
||||
type: String,
|
||||
required: true
|
||||
@Component()
|
||||
export default class Header extends TSX<Props>()(Vue) implements Props {
|
||||
|
||||
@Prop({ default: '默认数值' })
|
||||
text!: string;
|
||||
|
||||
@Prop()
|
||||
list!: ListItemType[];
|
||||
|
||||
created() {
|
||||
console.log("created: ", this)
|
||||
}
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
const { user, loading, error, refetch } = useUserData(props.userId)
|
||||
|
||||
return () => (
|
||||
<div>
|
||||
{loading.value && <div>Loading...</div>}
|
||||
{error.value && <div>Error: {error.value}</div>}
|
||||
{user.value && (
|
||||
<div>
|
||||
<h2>{user.value.name}</h2>
|
||||
<p>{user.value.email}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
render() {
|
||||
return (
|
||||
<div class="header">
|
||||
头部组件
|
||||
<p>{ this.text }</p>
|
||||
{ this.$slots.default?.() }
|
||||
{ this.$slots.haveName?.() }
|
||||
<ul>
|
||||
{
|
||||
this.list.map(
|
||||
item =>
|
||||
this.$slots.item?.(item)
|
||||
)
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 部署
|
||||
|
||||
需要后端或运维参与,请注意咨询阅读文档,大致步骤如下:
|
||||
|
||||
**全局准备工作:**
|
||||
|
||||
- 1: 准备 >=8台Linux服务器,并提前安装配置好Docker,确保网络可用,建议安装 [1panel](https://1panel.cn/)
|
||||
|
||||
- 2: 在1台Linux服务器上,部署公司自己的 Docker Registry 镜像存储库
|
||||
- 3: 在1台Linux服务器上,部署公司自己 Gitea 仓库
|
||||
|
||||
- 4: 准本 N台运行 前端各模块 的Linux服务器,并提前组成N个Docker Swarm 集群,以供前端各模块部署使用
|
||||
|
||||
- 5: 准本 >=3台运行 Nacos服务中心 的Linux服务器,并提前组成Docker Swarm 集群,提前运行好,确保服务可用
|
||||
- [Nacos 部署文档](./docker/nacos-cluster/README.md)
|
||||
|
||||
- 6: 准本 一台 运行 traefik 网关的Linux服务器,,提前运行好,确保服务可用
|
||||
- [Traefik 部署文档](./docker/traefik/README.md)
|
||||
|
||||
**前端工作:**
|
||||
|
||||
详细请看 各前端模块 存储库内的 帮助文档!!!
|
||||
|
||||
- [商品模块 app-product](https://gitee.com/bmycode/app-product)
|
||||
- [共享模块 app-shared](https://gitee.com/bmycode/app-shared)
|
||||
|
||||
|
||||
115
back-end/main.ts
115
back-end/main.ts
@@ -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);
|
||||
|
||||
@@ -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
318
doc/DockerSwarm.md
Normal 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
BIN
doc/jgt.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 989 KiB |
BIN
doc/newci.png
Normal file
BIN
doc/newci.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 734 KiB |
20
doc/临时命令.md
Normal file
20
doc/临时命令.md
Normal 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
|
||||
```
|
||||
|
||||
21
doc/完善微前端架构的建议.md
Normal file
21
doc/完善微前端架构的建议.md
Normal 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
51
doc/思路整理.md
Normal 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 无法退出。。。。
|
||||
|
||||
## 如何解决??
|
||||
97
doc/杂项必看.md
97
doc/杂项必看.md
@@ -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(); // 对象自己处理状态变更
|
||||
```
|
||||
@@ -10,8 +10,9 @@ src/
|
||||
│ │ └── events/ # 领域事件(如果需要)
|
||||
│ ├── application/ # 应用服务层 - 协调领域对象完成用例
|
||||
│ │ └── services/ # 应用服务(如:ProductApplicationService)
|
||||
│ ├── infrastructure/ # 基础设施层 - 领域层的具体实现
|
||||
│ ├── infr/ # 基础设施层 - 领域层的具体实现
|
||||
│ │ └── api/ # 数据获取实现(如:HttpProductRepository)
|
||||
│ │ └── config/ # 请求接口地址的配置
|
||||
│ ├── ports/ # 端口(接口/抽象类)- 定义契约
|
||||
│ │ └── repositories/ # 仓储接口(如:ProductRepository)
|
||||
│ └── use-cases/ # (可选)或将用例放在这里
|
||||
|
||||
147
docker/nacos-cluster/README.md
Normal file
147
docker/nacos-cluster/README.md
Normal 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**机,也就是 上面的 2,3 服务器,获取SSH权限后,分别执行上面给的命令:
|
||||
|
||||
```bash
|
||||
docker swarm join --token SWMTKN-1-3n9xxxxnwvsj6-4rhxx8xxxxzbbul 192.168.139.84:2377
|
||||
```
|
||||
|
||||
**这样 2,3 服务器就和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
|
||||
|
||||
**结束**
|
||||
162
docker/nacos-cluster/docker-compose.yml
Normal file
162
docker/nacos-cluster/docker-compose.yml
Normal 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:
|
||||
# 关键:指定副本数为 3,Swarm 会自动创建 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
|
||||
188
docker/nacos-cluster/mysql-schema.sql
Normal file
188
docker/nacos-cluster/mysql-schema.sql
Normal 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
|
||||
);
|
||||
BIN
docker/nacos-cluster/nacos_config_export.zip
Normal file
BIN
docker/nacos-cluster/nacos_config_export.zip
Normal file
Binary file not shown.
34
docker/nacos-cluster/prometheus/prometheus-cluster.yaml
Normal file
34
docker/nacos-cluster/prometheus/prometheus-cluster.yaml
Normal 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
10
docker/traefik/README.md
Normal 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
|
||||
```
|
||||
27
docker/traefik/docker-compose.yml
Normal file
27
docker/traefik/docker-compose.yml
Normal 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
113
docker/traefik/dynamic.yml
Normal 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
|
||||
15
docker/traefik/package.json
Normal file
15
docker/traefik/package.json
Normal 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"
|
||||
}
|
||||
63
docker/traefik/src/config/index.js
Normal file
63
docker/traefik/src/config/index.js
Normal 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'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
38
docker/traefik/src/http/index.js
Normal file
38
docker/traefik/src/http/index.js
Normal 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
|
||||
9
docker/traefik/src/index.js
Normal file
9
docker/traefik/src/index.js
Normal 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
|
||||
)
|
||||
109
docker/traefik/src/utils/index.js
Normal file
109
docker/traefik/src/utils/index.js
Normal 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
|
||||
34
docker/traefik/traefik.yml
Normal file
34
docker/traefik/traefik.yml
Normal 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"
|
||||
33
front-end/main-app/.env.development
Normal file
33
front-end/main-app/.env.development
Normal 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
|
||||
33
front-end/main-app/.env.production
Normal file
33
front-end/main-app/.env.production
Normal 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
|
||||
2903
front-end/main-app/package-lock.json
generated
2903
front-end/main-app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,35 +1,32 @@
|
||||
{
|
||||
"name": "vue-project",
|
||||
"name": "main-app",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "run-p type-check \"build-only {@}\" --",
|
||||
"preview": "vite preview",
|
||||
"build-only": "vite build",
|
||||
"dep": "concurrently \"npm run build\" \"npm run preview\"",
|
||||
"preview": "vite preview --port 1300 --strictPort",
|
||||
"build": "vite build --watch",
|
||||
"type-check": "vue-tsc --build"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.12.2",
|
||||
"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": {
|
||||
"@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"
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"@types/node": "^24.6.1",
|
||||
"@vue3-oop/plugin-vue-jsx": "^1.5.1",
|
||||
"element-plus": "^2.11.4",
|
||||
"rollup-plugin-visualizer": "^6.0.3",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "~5.9.3",
|
||||
"vite": "^7.1.7",
|
||||
"vite-federation": "^3.4.4",
|
||||
"vite-plugin-vue-devtools": "^8.0.2"
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { defineComponent } from 'vue'
|
||||
import { Component, Vue } from 'vue-facing-decorator'
|
||||
import { RouterLink, RouterView } from "vue-router";
|
||||
|
||||
export default defineComponent({
|
||||
name: 'App',
|
||||
setup() {
|
||||
return () => (
|
||||
@Component()
|
||||
export default class App extends Vue {
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<div id="nav">
|
||||
<RouterLink to="/">首页</RouterLink> |
|
||||
@@ -15,4 +15,4 @@ export default defineComponent({
|
||||
</>
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 |
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,54 +1,45 @@
|
||||
import { defineComponent, defineEmits, onMounted} from 'vue'
|
||||
import type { JSX } from "vue/jsx-runtime";
|
||||
import { Component, Prop, TSX, Vue } from 'vue-facing-decorator'
|
||||
|
||||
export default defineComponent({
|
||||
name: "Header",
|
||||
props: {
|
||||
text: {
|
||||
type: Array<{ id: number, title: string }>,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
emits: {
|
||||
sendMessage: (message: string) => typeof message === 'string'
|
||||
},
|
||||
interface Props {
|
||||
text?: string
|
||||
list?: ListItemType[]
|
||||
}
|
||||
|
||||
setup(props, { attrs, slots, emit, expose }) {
|
||||
interface ListItemType {
|
||||
id: number
|
||||
title: string
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
console.log("onMounted")
|
||||
})
|
||||
@Component()
|
||||
export default class Header extends TSX<Props>()(Vue) implements Props {
|
||||
|
||||
const handleClick = () => {
|
||||
console.log("handleClick")
|
||||
emit('sendMessage', '子给父的数据')
|
||||
}
|
||||
@Prop({ default: '默认数值' })
|
||||
text!: string;
|
||||
|
||||
const button = (): JSX.Element => {
|
||||
return <button onClick={handleClick}>父向子emit点我</button>
|
||||
}
|
||||
@Prop()
|
||||
list!: ListItemType[];
|
||||
|
||||
return () => (
|
||||
<div>
|
||||
<h1>头部组件</h1>
|
||||
created() {
|
||||
console.log("created: ", this)
|
||||
}
|
||||
|
||||
{ button() }
|
||||
|
||||
{ slots.default?.() }
|
||||
|
||||
{ slots.header?.() }
|
||||
|
||||
<div class="ScopedSlots">
|
||||
<p>作用于插槽:</p>
|
||||
<ul>
|
||||
{
|
||||
props.text.map(item => {
|
||||
return slots.scoped?.(item)
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
render() {
|
||||
return (
|
||||
<div class="header">
|
||||
头部组件
|
||||
<p>{ this.text }</p>
|
||||
{ this.$slots.default?.() }
|
||||
{ this.$slots.haveName?.() }
|
||||
<ul>
|
||||
{
|
||||
this.list.map(
|
||||
item =>
|
||||
this.$slots.item?.(item)
|
||||
)
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import { ProductApplicationService } from "../../domains/product/application/services/ProductApplicationService";
|
||||
import { HttpProductRepository } from "../../domains/product/infr/api/HttpProductRepository";
|
||||
import { ProductRepository } from "../../domains/product/ports/repositories/ProductRepository";
|
||||
|
||||
|
||||
/**
|
||||
* 商品领域的依赖注入配置
|
||||
* 将 接口 与 具体实现 绑定
|
||||
*
|
||||
* 优点:是一个依赖工厂,它隐藏了对象创建的细节,
|
||||
* 让业务代码只需要关心 “做什么”,而不用关心 “依赖从哪里来”
|
||||
* 所有依赖关系都在di目录下集中配置,一目了然,后续维护时能快速找到所有依赖的创建逻辑。
|
||||
*
|
||||
* 松耦合:
|
||||
* 业务代码(ProductApplicationService)只依赖抽象接口(ProductRepository),
|
||||
* 不依赖具体实现。如果未来需要改用其他数据来源(比如 WebSocket),
|
||||
* 只需修改bindProductRepository的返回值,无需改动业务逻辑。
|
||||
*
|
||||
* 易于测试:
|
||||
* 测试时可以临时替换为模拟实现
|
||||
* productModule.bindProductRepository = () => new MockProductRepository();
|
||||
* 这样测试就可以脱离真实 API,使用预设的模拟数据。
|
||||
*/
|
||||
export const productModule = {
|
||||
|
||||
// 绑定 仓储抽象接口 到 HTTP具体实现
|
||||
bindProductRepository: (): ProductRepository => {
|
||||
// return new MockProductRepository 直接从真实接口数据改为测试模拟数据
|
||||
return new HttpProductRepository;
|
||||
},
|
||||
|
||||
// 创建应用服务实例
|
||||
createProductApplicationService: (): ProductApplicationService => {
|
||||
return new ProductApplicationService(
|
||||
productModule.bindProductRepository()
|
||||
);
|
||||
}
|
||||
}
|
||||
1
front-end/main-app/src/app/layouts/README.md
Normal file
1
front-end/main-app/src/app/layouts/README.md
Normal file
@@ -0,0 +1 @@
|
||||
# 临时占位,上传空文件夹,保存文件结构用,后面删除
|
||||
@@ -1,13 +1,30 @@
|
||||
// import './assets/main.css'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
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() {
|
||||
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
|
||||
|
||||
createApp(App)
|
||||
.use(createPinia())
|
||||
.use(router)
|
||||
.mount('#app')
|
||||
|
||||
|
||||
25
front-end/main-app/src/app/router/Config.ts
Normal file
25
front-end/main-app/src/app/router/Config.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { productRouter } from 'product/ProductRouter'
|
||||
import { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
|
||||
const baseRouter: Array<RouteRecordRaw> = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: () => import('../views/Home/Home'),
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'about',
|
||||
component: () => import('../views/About/About'),
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'NotFound',
|
||||
component: () => import('../views/NotFound')
|
||||
}
|
||||
];
|
||||
|
||||
// 聚合所有功能模块的路由
|
||||
const routes: RouteRecordRaw[] = [ ...baseRouter, ...productRouter ];
|
||||
export default routes;
|
||||
@@ -1,26 +1,5 @@
|
||||
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
|
||||
import { productRouter } from '@features/product/router'
|
||||
|
||||
const baseRouter: Array<RouteRecordRaw> = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: () => import('../views/Home'),
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'about',
|
||||
component: () => import('../views/About'),
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'NotFound',
|
||||
component: () => import('../views/NotFound')
|
||||
}
|
||||
];
|
||||
|
||||
// 聚合所有功能模块的路由
|
||||
const routes: RouteRecordRaw[] = [...baseRouter, ...productRouter];
|
||||
import {createRouter, createWebHistory} from 'vue-router'
|
||||
import routes from './Config'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: "About",
|
||||
setup() {
|
||||
return () => (
|
||||
<div class="about">
|
||||
<h1>我的</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
4
front-end/main-app/src/app/views/About/About.module.less
Normal file
4
front-end/main-app/src/app/views/About/About.module.less
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
.About {
|
||||
|
||||
}
|
||||
14
front-end/main-app/src/app/views/About/About.tsx
Normal file
14
front-end/main-app/src/app/views/About/About.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import { defineComponent, reactive, onMounted } from 'vue'
|
||||
import Header from '@/components/Header'
|
||||
import { HttpConfig } from '@shared/config/index'
|
||||
import { UrlCheckUtils } from '@shared/utils'
|
||||
import { useCounterStore } from '@/stores/counter'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
interface stateType {
|
||||
list: ListItemType[]
|
||||
}
|
||||
|
||||
interface ListItemType {
|
||||
id: number
|
||||
title: string
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: "Home",
|
||||
setup() {
|
||||
|
||||
const counterStore = useCounterStore()
|
||||
const { increment } = counterStore
|
||||
const { count, doubleCount } = storeToRefs(counterStore)
|
||||
|
||||
const state = reactive<stateType>({
|
||||
list: [
|
||||
{ id: 1, title: '新闻1' },
|
||||
{ id: 2, title: '新闻2' },
|
||||
{ id: 3, title: '新闻3' },
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
// console.log("onMounted: ", HttpConfig.DevBaseUrl);
|
||||
console.log("check: ", UrlCheckUtils.check())
|
||||
})
|
||||
|
||||
// 定义处理子组件事件的函数
|
||||
const handleChildEvent = (message: string) => {
|
||||
console.log('收到子组件事件:', message)
|
||||
}
|
||||
|
||||
|
||||
return () => (
|
||||
<div class="home">
|
||||
<Header text={state.list} onSendMessage={handleChildEvent}>
|
||||
{{
|
||||
scoped: (item: ListItemType) => <li key={item.id}>{item.title}</li>,
|
||||
header: () => <div class="header">父到子 具名 插槽</div>,
|
||||
default: () => <div class="default">父到子 default 插槽</div>
|
||||
}}
|
||||
</Header>
|
||||
<h1>首页</h1>
|
||||
<div class="count" onClick={increment}>点我+1:{count.value}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
5
front-end/main-app/src/app/views/Home/Home.module.less
Normal file
5
front-end/main-app/src/app/views/Home/Home.module.less
Normal file
@@ -0,0 +1,5 @@
|
||||
.Home {
|
||||
.title {
|
||||
color: var(--theme-color);
|
||||
}
|
||||
}
|
||||
76
front-end/main-app/src/app/views/Home/Home.tsx
Normal file
76
front-end/main-app/src/app/views/Home/Home.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
# 临时占位,上传空文件夹,保存文件结构用,后面删除
|
||||
@@ -0,0 +1 @@
|
||||
# 临时占位,上传空文件夹,保存文件结构用,后面删除
|
||||
@@ -0,0 +1 @@
|
||||
# 临时占位,上传空文件夹,保存文件结构用,后面删除
|
||||
@@ -0,0 +1 @@
|
||||
# 临时占位,上传空文件夹,保存文件结构用,后面删除
|
||||
@@ -0,0 +1 @@
|
||||
# 临时占位,上传空文件夹,保存文件结构用,后面删除
|
||||
1
front-end/main-app/src/domains/order/infr/README.md
Normal file
1
front-end/main-app/src/domains/order/infr/README.md
Normal file
@@ -0,0 +1 @@
|
||||
# 临时占位,上传空文件夹,保存文件结构用,后面删除
|
||||
1
front-end/main-app/src/domains/order/ports/README.md
Normal file
1
front-end/main-app/src/domains/order/ports/README.md
Normal file
@@ -0,0 +1 @@
|
||||
# 临时占位,上传空文件夹,保存文件结构用,后面删除
|
||||
@@ -1,18 +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);
|
||||
}
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
import { ErrorText } from "../enums/ErrorEnums"
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
export enum ErrorText {
|
||||
PriceNotNegativein = "商品 价格 不能为负数",
|
||||
StockNotNegativein = "商品 库存 不能为负数",
|
||||
StockIsGreaterThanZero = "减少的库存数量必须大于0",
|
||||
LowStock = "库存不足",
|
||||
IdNotNull = "商品Id不能为空"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
export interface ProductType {
|
||||
id: number,
|
||||
name: string,
|
||||
description: string,
|
||||
price: number,
|
||||
imageUrl: string,
|
||||
stock: number,
|
||||
categoryId: number,
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { Product } from "../../domain/entities/Product";
|
||||
import { ProductType } from "../../domain/type/ProductType";
|
||||
import { ProductRepository } from "../../ports/repositories/ProductRepository";
|
||||
import http from "@shared/infra/AxiosInfra";
|
||||
|
||||
|
||||
/**
|
||||
* HTTP商品仓储实现 - 实现商品数据的HTTP访问
|
||||
* 这是基础设施层,负责具体的数据获取实现
|
||||
*/
|
||||
export class HttpProductRepository implements ProductRepository {
|
||||
|
||||
async findAll(): Promise<Array<ProductType>> {
|
||||
const result: Array<ProductType> = await http.get({ url: "/productsList" });
|
||||
const items = result.map((item) => Product.fromDTO(item))
|
||||
return items;
|
||||
}
|
||||
|
||||
async findById(id: number): Promise<Product> {
|
||||
const result: ProductType = await http.get({
|
||||
url: "/productsInfo",
|
||||
data: { id }
|
||||
});
|
||||
return Product.fromDTO(result);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
1
front-end/main-app/src/features/order/assets/README.md
Normal file
1
front-end/main-app/src/features/order/assets/README.md
Normal file
@@ -0,0 +1 @@
|
||||
# 临时占位,上传空文件夹,保存文件结构用,后面删除
|
||||
@@ -0,0 +1 @@
|
||||
# 临时占位,上传空文件夹,保存文件结构用,后面删除
|
||||
@@ -0,0 +1 @@
|
||||
# 临时占位,上传空文件夹,保存文件结构用,后面删除
|
||||
1
front-end/main-app/src/features/order/views/README.md
Normal file
1
front-end/main-app/src/features/order/views/README.md
Normal file
@@ -0,0 +1 @@
|
||||
# 临时占位,上传空文件夹,保存文件结构用,后面删除
|
||||
@@ -1,17 +0,0 @@
|
||||
import { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
|
||||
export const productRouter: Array<RouteRecordRaw> = [
|
||||
{
|
||||
path: '/Products',
|
||||
name: 'ProductList',
|
||||
component: () => import('../views/ProductList.tsx'),
|
||||
meta: { title: '商品列表' }
|
||||
},
|
||||
{
|
||||
path: '/ProductDetail',
|
||||
name: 'ProductDetail',
|
||||
component: () => import('../views/ProductDetail.tsx'),
|
||||
meta: { title: '商品详情' }
|
||||
},
|
||||
]
|
||||
@@ -1,53 +0,0 @@
|
||||
import { productModule } from '@/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({
|
||||
name: "productModule-ProductDetail",
|
||||
setup() {
|
||||
const route = useRoute();
|
||||
const productService = productModule.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>
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1,53 +0,0 @@
|
||||
import { defineComponent, onMounted, reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router';
|
||||
import { productModule } from '@/di/productModule';
|
||||
import { ProductType } from '@domains/product/domain/type/ProductType';
|
||||
|
||||
interface storeType {
|
||||
list: Array<ProductType>
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: "productModule-ProductList",
|
||||
setup() {
|
||||
|
||||
const router = useRouter();
|
||||
const productService = productModule.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>商品领域模块 - 商品列表页面</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>
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1,5 +0,0 @@
|
||||
export default class HttpConfig {
|
||||
public static DevBaseUrl: string = "http://127.0.0.1:3000/api"
|
||||
public static prodBaseUrl: string = "http://127.0.0.1:9090/api/"
|
||||
public static apiList: string = ""
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export default class ThemeConfig {
|
||||
public static primaryColor: string = '#1890ff'; // 主色调
|
||||
public static layoutType: 'side' | 'top' = 'side'; // 侧边栏布局
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
import HttpConfig from './HttpConfig';
|
||||
import ThemeConfig from './ThemeConfig';
|
||||
|
||||
export { HttpConfig, ThemeConfig };
|
||||
@@ -1,93 +0,0 @@
|
||||
import axios, { type AxiosInstance, AxiosError, type AxiosRequestConfig, type InternalAxiosRequestConfig } from "axios";
|
||||
import { UrlCheckUtils } from '@shared/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();
|
||||
40
front-end/main-app/src/shared/lib/nacosInstance.ts
Normal file
40
front-end/main-app/src/shared/lib/nacosInstance.ts
Normal 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;
|
||||
}
|
||||
46
front-end/main-app/src/shared/types/ shims-tsx.d.ts
vendored
Normal file
46
front-end/main-app/src/shared/types/ shims-tsx.d.ts
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
import Vue, {VNode} from 'vue'
|
||||
|
||||
declare global {
|
||||
namespace Vue {
|
||||
any
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
namespace JSX {
|
||||
interface ElementAttributesProperty {
|
||||
$props: any // 设置类组件的props类型检查属性
|
||||
}
|
||||
|
||||
// tslint:disable no-empty-interface
|
||||
interface Element extends VNode {
|
||||
}
|
||||
|
||||
// tslint:disable no-empty-interface
|
||||
interface ElementClass extends Vue {
|
||||
}
|
||||
|
||||
interface IntrinsicAttributes {
|
||||
// 给组件元素增加属性
|
||||
on?: any
|
||||
vModel?: any
|
||||
vShow?: boolean
|
||||
value?: any
|
||||
oninput?: Function
|
||||
nativeOnClick?: (event: MouseEvent) => void
|
||||
oncontextmenu?: (event: MouseEvent) => void
|
||||
onmouseup?: (event: MouseEvent) => void
|
||||
onclick?: (event: MouseEvent) => void
|
||||
onclick_prevent?: (event: MouseEvent) => void
|
||||
ref?: string
|
||||
key?: string | number
|
||||
class?: string | string[]
|
||||
slot?: string
|
||||
style?: Partial<CSSStyleDeclaration> | string | object | object[]
|
||||
}
|
||||
|
||||
interface IntrinsicElements {
|
||||
[elem: string]: any
|
||||
}
|
||||
}
|
||||
}
|
||||
15
front-end/main-app/src/shared/types/env.d.ts
vendored
15
front-end/main-app/src/shared/types/env.d.ts
vendored
@@ -1 +1,16 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ViteTypeOptions {
|
||||
// 添加这行代码,你就可以将 ImportMetaEnv 的类型设为严格模式,
|
||||
// 这样就不允许有未知的键值了。
|
||||
// strictImportMetaEnv: unknown
|
||||
}
|
||||
|
||||
interface ImportMetaEnv {
|
||||
// readonly VITE_APP_TITLE: string
|
||||
// 更多环境变量...
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||
11
front-end/main-app/src/shared/types/global.d.ts
vendored
Normal file
11
front-end/main-app/src/shared/types/global.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
export interface AppConfig {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__APP_CONFIG__: AppConfig;
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
44
front-end/main-app/src/shared/types/product.d.ts
vendored
Normal file
44
front-end/main-app/src/shared/types/product.d.ts
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
// 声明领域层类型
|
||||
declare module 'product/ProductDomain' {
|
||||
// 商品实体类型(与联邦模块一致)
|
||||
export class Product {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
price: number;
|
||||
imageUrl: string;
|
||||
stock: number;
|
||||
categoryId: number;
|
||||
}
|
||||
|
||||
// 应用服务类型
|
||||
export class ProductApplicationService {
|
||||
constructor(repo: ProductRepository);
|
||||
|
||||
findAll(): Promise<Array<any>>; // TODO any 需要修改
|
||||
findById(id: number): Promise<Product>
|
||||
}
|
||||
|
||||
export class HttpProductRepository implements ProductRepository {
|
||||
findAll(): Promise<Array<any>>; // TODO any 需要修改
|
||||
findById(id: number): Promise<Product>
|
||||
}
|
||||
|
||||
export class ProductRepository {
|
||||
findAll(): Promise<Array<any>>; // TODO any 需要修改
|
||||
findById(id: number): Promise<Product>
|
||||
}
|
||||
}
|
||||
|
||||
// 声明视图类型(同理补充其他模块)
|
||||
// declare module 'productModule/ProductViews' {
|
||||
// import type { DefineComponent } from 'vue';
|
||||
// export const ProductList: DefineComponent; // 商品列表页
|
||||
// export const ProductDetail: DefineComponent; // 商品详情页
|
||||
// }
|
||||
|
||||
declare module 'product/ProductRouter' {
|
||||
export const productRouter: RouteRecordRaw[];
|
||||
}
|
||||
|
||||
|
||||
30
front-end/main-app/src/shared/types/shared.d.ts
vendored
Normal file
30
front-end/main-app/src/shared/types/shared.d.ts
vendored
Normal 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;
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
declare module '*.vue' {
|
||||
import Vue from 'vue';
|
||||
export default Vue;
|
||||
}
|
||||
5
front-end/main-app/src/shared/types/shims-vue.d.ts
vendored
Normal file
5
front-end/main-app/src/shared/types/shims-vue.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { HttpConfig } from '@shared/config/index'
|
||||
|
||||
export default class UrlCheckUtils {
|
||||
|
||||
public static check() {
|
||||
if (import.meta.env.MODE === 'production') {
|
||||
return HttpConfig.prodBaseUrl;
|
||||
} else {
|
||||
return HttpConfig.DevBaseUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import UrlCheckUtils from './UrlCheckUtils';
|
||||
|
||||
export { UrlCheckUtils };
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": [
|
||||
"src/shared/types/*.d.ts",
|
||||
"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/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,44 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"importHelpers": true,
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"moduleResolution": "Bundler",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": [
|
||||
"ES2020",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"WebWorker"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/app/*"
|
||||
],
|
||||
"@shared/*": [
|
||||
"./src/shared/*"
|
||||
],
|
||||
"@features/*": [
|
||||
"./src/features/*"
|
||||
],
|
||||
"@domains/*": [
|
||||
"./src/domains/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src/shared/types/*.d.ts",
|
||||
"src/**/*.ts",
|
||||
"src/**/*.d.ts",
|
||||
"src/**/*",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,55 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
import path from 'path'
|
||||
|
||||
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 { 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';
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
vueJsx(),
|
||||
vueDevTools(),
|
||||
],
|
||||
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))
|
||||
},
|
||||
},
|
||||
export default defineConfig(async (config: ConfigEnv) => {
|
||||
const env = loadEnv(config.mode, process.cwd());
|
||||
|
||||
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))
|
||||
})
|
||||
|
||||
32
front-end/vite-plugin-nacos/README.md
Normal file
32
front-end/vite-plugin-nacos/README.md
Normal file
@@ -0,0 +1,32 @@
|
||||
功能分析:
|
||||
|
||||
1 注册服务
|
||||
|
||||
子模块:直接通过nacos 注册,给 内网 的 使用
|
||||
|
||||
主模块:main-app -> Traefik -> ConfigServer(nacos-federation) -> Nacos
|
||||
|
||||
main-app 发送 http 请求
|
||||
/nacos/reg
|
||||
参数:
|
||||
ip,port
|
||||
|
||||
|
||||
|
||||
|
||||
2 获取配置
|
||||
|
||||
子模块:直接通过 nacos 获取配置
|
||||
|
||||
主模块: main-app -> Traefik -> ConfigServer(nacos-federation) -> Nacos
|
||||
|
||||
|
||||
注意:
|
||||
|
||||
|
||||
增加功能:
|
||||
|
||||
认证授权机制
|
||||
请求限流保护
|
||||
操作日志记录
|
||||
服务治理功能(如权重调整、元数据管理等)
|
||||
31
front-end/vite-plugin-nacos/package.json
Normal file
31
front-end/vite-plugin-nacos/package.json
Normal 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"
|
||||
}
|
||||
96
front-end/vite-plugin-nacos/src/index.ts
Normal file
96
front-end/vite-plugin-nacos/src/index.ts
Normal 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();
|
||||
}
|
||||
179
front-end/vite-plugin-nacos/src/nacosServiceManage.ts
Normal file
179
front-end/vite-plugin-nacos/src/nacosServiceManage.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
52
front-end/vite-plugin-nacos/src/utils.ts
Normal file
52
front-end/vite-plugin-nacos/src/utils.ts
Normal 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
|
||||
104
front-end/vite-plugin-nacos/src/vite-plugin-nacos.ts
Normal file
104
front-end/vite-plugin-nacos/src/vite-plugin-nacos.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { loadEnv, Plugin, UserConfig } from 'vite';
|
||||
import { getLocalIP } from './utils';
|
||||
import chalk from 'chalk';
|
||||
|
||||
const logger = console;
|
||||
|
||||
export interface RegisterOptions {
|
||||
serverList: string;
|
||||
/** 服务名称(如 'main-app') */
|
||||
serviceName: string;
|
||||
/** 服务端口号 */
|
||||
port: number;
|
||||
/** 命名空间(用于环境隔离,默认 'public') */
|
||||
namespace?: string;
|
||||
}
|
||||
|
||||
export function reg(options?: Partial<RegisterOptions>): Plugin {
|
||||
return {
|
||||
name: 'vite-plugin-nacos',
|
||||
async config(config: UserConfig, { mode }) {
|
||||
// 修改base,增加资源来源地址,修复资源错误加载的问题
|
||||
const env = loadEnv(mode, process.cwd());
|
||||
let baseUrl = ""
|
||||
|
||||
// 主模块 main-app 不加 base 前缀,其他模块都加,
|
||||
// 也是为了命中 traefik 路由代理
|
||||
if (env.VITE_SERVER_NAME != "main-app") {
|
||||
baseUrl = `${env.VITE_SERVER_NAME}/`
|
||||
}
|
||||
|
||||
return {
|
||||
server: {
|
||||
allowedHosts: [
|
||||
'.ddd.com',
|
||||
],
|
||||
port: Number(env.VITE_SERVER_PORT),
|
||||
host: '::',
|
||||
strictPort: true,
|
||||
cors: true
|
||||
},
|
||||
// base: `${env.VITE_PREFIX}${getLocalIP()}:${env.VITE_SERVER_PORT}/${baseUrl}`
|
||||
base: `/${baseUrl}`
|
||||
};
|
||||
},
|
||||
async configResolved(config) {
|
||||
const env = config.env;
|
||||
},
|
||||
async closeBundle() {
|
||||
// // 服务关闭时注销Nacos实例
|
||||
// if (nacosInstance) {
|
||||
// await nacosInstance.destroy();
|
||||
// }
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function Logs(env: Record<string, any>) {
|
||||
const moduleName = []
|
||||
for (const key in env) {
|
||||
if (key.includes("VITE_MODEL_")) {
|
||||
moduleName.push(env[key]);
|
||||
}
|
||||
}
|
||||
moduleName.length == 0 ? moduleName.push('无') : null
|
||||
const webUI = env.VITE_NACOS_ADDRESS.split(":")[0]
|
||||
console.log()
|
||||
|
||||
console.log(dedentTemplateString(`
|
||||
${chalk.blue.bold('微服务工具:')}
|
||||
${chalk.green('➜')} Naco 配置中心: ${env.VITE_PREFIX}${env.VITE_NACOS_ADDRESS}
|
||||
${chalk.green('➜')} WebUi: ${env.VITE_PREFIX}${webUI}:8085
|
||||
${chalk.green('➜')} Prometheus 监控: ${env.VITE_PREFIX}${webUI}:9090
|
||||
${chalk.green('➜')} Grafana 面板: ${env.VITE_PREFIX}${webUI}:3000
|
||||
${chalk.green('➜')} Traefik 网关: ${env.VITE_PREFIX}${env.VITE_TRAEFIK_ADDRESS}
|
||||
${chalk.green('➜')} WebUi: ${env.VITE_PREFIX}${env.VITE_TRAEFIK_ADDRESS}:8880
|
||||
|
||||
${chalk.blue.bold('微前端信息:')}
|
||||
${chalk.green('➜')} 主模块:${chalk.green(env.VITE_SERVER_NAME)}
|
||||
|
||||
${chalk.blue.bold('调用模块:')}
|
||||
${chalk.green('➜')} [ ${chalk.yellow(moduleName)} ]
|
||||
`))
|
||||
console.log()
|
||||
}
|
||||
|
||||
function dedentTemplateString(str: string) {
|
||||
// 分割成行
|
||||
const lines = str.split('\n');
|
||||
|
||||
// 移除首尾空行
|
||||
if (lines[0].trim() === '') lines.shift();
|
||||
if (lines[lines.length - 1].trim() === '') lines.pop();
|
||||
|
||||
// 找到最小缩进量
|
||||
const minIndent = lines.reduce((min, line) => {
|
||||
if (line.trim() === '') return min; // 跳过空行
|
||||
// @ts-ignore
|
||||
const indent = line.match(/^\s*/)[0].length;
|
||||
return indent < min ? indent : min;
|
||||
}, Infinity);
|
||||
|
||||
// 移除统一缩进
|
||||
return lines.map(line => line.slice(minIndent)).join('\n');
|
||||
}
|
||||
23
front-end/vite-plugin-nacos/tsconfig.json
Normal file
23
front-end/vite-plugin-nacos/tsconfig.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "CommonJS",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
// 生成类型声明文件
|
||||
"declarationDir": "./dist"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"src/types/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
16
package.json
Normal file
16
package.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "front-end",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"keywords": [],
|
||||
"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"
|
||||
}
|
||||
}
|
||||
7
pnpm-workspace.yaml
Normal file
7
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
packages:
|
||||
- 'front-end/*'
|
||||
- 'docker/traefik'
|
||||
- 'back-end'
|
||||
# 排除不需要的目录(可选)
|
||||
- '!** /node_modules'
|
||||
- '!** /dist'
|
||||
Reference in New Issue
Block a user