Compare commits

..

62 Commits

Author SHA1 Message Date
编码猿
9432116a00 修改 动态配置 2025-10-07 21:48:31 +08:00
编码猿
5fda36b268 更新文档 2025-10-07 21:47:12 +08:00
编码猿
84820b1e9b 移除nginx 2025-10-07 21:45:03 +08:00
编码猿
550bf0039b 移除 app-shared 为独立模块 2025-10-07 21:14:15 +08:00
编码猿
2653800c3f 从主项目中删除 app-product,抽离为独立仓库存储!! 2025-10-07 04:37:32 +08:00
编码猿
cb8f7a4682 修改cicd为gitea action,简化操作 2025-10-07 00:59:16 +08:00
编码猿
7fb0f60ec0 实现cicd的案例,等待集成 2025-10-05 05:46:17 +08:00
编码猿
ff925d458a 完善架构,修改很多地方 2025-10-04 00:08:33 +08:00
编码猿
3c66faf9a7 【重要】traefik 配置 2025-09-30 10:01:58 +08:00
编码猿
a3ba6c2944 【重要】重构 并实现新的架构 2025-09-30 10:00:10 +08:00
编码猿
04754a1f77 实现 nacos 的分布式集群部署配置 2025-09-29 05:07:42 +08:00
编码猿
ced7662d98 优化 nacos-federation 2025-09-26 02:21:04 +08:00
编码猿
174af7efc3 优化全局样式和 element ui框架,修改nacos-federation,增加打包依赖分析等 2025-09-26 01:50:49 +08:00
编码猿
537c7ca60e 添加全局样式和 element ui框架 2025-09-24 23:56:55 +08:00
编码猿
21bf47c8a1 修改帮助文档 2025-09-23 12:08:40 +08:00
编码猿
ac5f544819 增加控制台日志,方便访问各种服务 2025-09-23 12:05:54 +08:00
编码猿
258318eee8 将vite.config.ts中的base 改到插件 nacos-federation中实现,简化代码 2025-09-23 11:35:02 +08:00
编码猿
1f5c0556a3 修改base,增加资源来源地址,修复资源错误加载的问题 2025-09-23 11:20:41 +08:00
编码猿
1791889441 更新文档 2025-09-23 03:32:47 +08:00
编码猿
84c4e8bdc7 修改 nacos-federation 插件,实现 接口请求基地址从 配置中心获取,并注入到环境变量中 2025-09-23 03:26:10 +08:00
编码猿
cbc2c25cf4 修改忽略文件 2025-09-23 01:02:19 +08:00
编码猿
b29001c8c3 修改忽略文件 2025-09-23 01:01:08 +08:00
编码猿
c8a9bfd77e 修改忽略文件 2025-09-23 01:00:34 +08:00
编码猿
135839d6e2 封装自动生成 traefik 代理配置的 nodejs 插件代码 2025-09-23 00:59:56 +08:00
编码猿
bfe5f57378 插件nacos-federation增加对vite版本的支持,简化代码! 2025-09-22 04:22:03 +08:00
编码猿
dabb91ef1e 实现 nacos + traefik,改了很多东西!!! 2025-09-22 02:27:53 +08:00
编码猿
e038fdb965 增加任务 2025-09-20 02:26:42 +08:00
编码猿
32d40e1fa3 @vitejs/plugin-vue-jsx 不支持装饰器语法,所以换成@vitejs/plugin-vue-jsx插件,不需要任何配置就可以了 2025-09-19 13:12:17 +08:00
编码猿
b3718c4097 修改插件的实现和文档 2025-09-19 13:01:26 +08:00
编码猿
79a0c9a451 增加 开发任务 2025-09-19 03:21:23 +08:00
编码猿
f2b53e46db 增加 开发任务 2025-09-19 03:21:05 +08:00
编码猿
f36d1fc413 修改帮助文档 2025-09-19 03:18:25 +08:00
编码猿
687154f0be 修改帮助文档 2025-09-19 03:16:33 +08:00
编码猿
6c5b25e0d7 修改帮助文档 2025-09-19 02:49:19 +08:00
编码猿
64db72b3d6 修改帮助文档 2025-09-19 02:39:34 +08:00
编码猿
886308ee90 修改ts配置 2025-09-19 02:32:54 +08:00
编码猿
05bcc3376c 修改ts配置 2025-09-19 02:32:15 +08:00
编码猿
4e06a0caed 提交根目录配置文件 2025-09-19 02:22:04 +08:00
编码猿
523e115906 提交根目录配置文件 2025-09-19 02:20:41 +08:00
编码猿
1e16236e13 基于Vue3+OOP+JSX改造项目代码,增加pnpm统一管理依赖 2025-09-19 02:20:18 +08:00
编码猿
064d368ae4 修改package.json的启动命令 2025-09-18 04:40:50 +08:00
编码猿
75c50d8c97 Merge remote-tracking branch 'gitee/master' 2025-09-18 04:14:28 +08:00
编码猿
cc23a6e1f0 完成对app-shared 的拆分 2025-09-18 04:12:50 +08:00
编码猿
327e80d71e 暂时移除TS的类型提示,后期还是要改! 2025-09-18 02:54:57 +08:00
编码猿
af2b3badd0 提交 2025-09-18 02:51:28 +08:00
编码猿
c26c2ed5d5 提交 2025-09-18 01:54:17 +08:00
编码猿
3d0c69c98e 完成 微前端拆分 提交遗漏文件 2025-09-18 01:52:30 +08:00
编码猿
ca24ab9922 完成 微前端拆分 2025-09-18 01:50:50 +08:00
编码猿
7bc20d517b 简单修改 2025-09-17 21:59:50 +08:00
编码猿
55004f2a8c update README.md.
修改文档

Signed-off-by: 编码猿 <www.2271608011@qq.com>
2025-09-17 00:09:22 +00:00
编码猿
dbb7515f8f 修改文档 2025-09-17 02:54:35 +08:00
编码猿
1a435fd81b 修改文档 2025-09-17 02:47:01 +08:00
编码猿
3e56103538 去除多余文件 2025-09-17 02:40:50 +08:00
编码猿
ab36c7e805 增加占位文件 2025-09-17 02:39:03 +08:00
编码猿
02cae30c4c 333 2025-09-17 02:31:37 +08:00
编码猿
2c3cffdfa5 33 2025-09-17 02:07:57 +08:00
编码猿
858018e43d 测试案例完成 2025-09-17 01:52:10 +08:00
编码猿
61c84b0842 完成deno后端测试用的接口 2025-09-16 23:40:58 +08:00
编码猿
c156637f03 移动README.md位置 2025-09-16 22:46:54 +08:00
编码猿
c3d4b7e1e4 修改文件结构 2025-09-16 22:46:14 +08:00
编码猿
715d3ff0bb 3 2025-09-16 22:10:26 +08:00
编码猿
62fdd169c5 222 2025-09-16 22:10:08 +08:00
108 changed files with 4547 additions and 3622 deletions

6
.gitignore vendored
View File

@@ -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

302
README.md
View File

@@ -1,84 +1,264 @@
## DDD领域驱动设计的微服务前端案例
> 持续开发中及时拉取代码文档还不够完善先凑合看目前只是demo代码还会进行大规模封装细化和改造
## 问 & 答
搭建的一套 **基于DDD领域驱动设计的微服务前端** 项目脚手架,适用于大型企业,开发大型复杂且拓展灵活的前端应用的架构方案。
问: 为什么vue项目要抛弃SFC写法而尝试jsx/tsx方式来编码
https://juejin.cn/post/6911175470255964174
代码编写建议使用 **OOP面向对象 + TypeScript + JSX** 进行开发,让项目更适合开发和维护,获得更好更极致的编码体验。
本案例中 **[主应用]** 采取的就是OOP风格当然如果你不熟悉上面的写法那么可以在
【其他模块】中使用Vue2Vue3都行但仍然需要遵守DDD的规范进行开发
问:不会 vue3 jsx 语法,怎么办?
https://juejin.cn/post/7141674726434439176
## 架构图
![架构图](./doc/jgt.png)
vue3 jsx defineComponent 语法,实际项目中 setup 代码太长,怎么办?
答:使用组合式函数 (Composables) - 最推荐
## CI/CD 自动化集群部署
案例:
![cicd](./doc/newci.png)
```jsx
// composables/useUserData.js
import { ref, onMounted } from 'vue'
import api from '@/api'
## 开发日记
export function useUserData(userId) {
const user = ref(null)
const loading = ref(false)
const error = ref(null)
- [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实现一个完整项目测试脚手架不足
const fetchUser = async () => {
loading.value = true
try {
user.value = await api.getUser(userId)
} catch (err) {
error.value = err.message
} finally {
loading.value = false
## 技术栈:
- [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/)
- [TypeScript](https://www.typescriptlang.org/)
- [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)
- [Vite](https://cn.vite.dev/)
- [axios](https://axios-http.com/)
- 等等等....
## 文件夹结构
**根目录:**
```txt
.
├── 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 # 帮助文档
```
**DDD前端分层目录**
```txt
src/
├── domains/ # 【核心】领域层 - 所有业务领域模块
│ └── product/ # 【app-product模块】订单领域模块
│ ├── domain/ # 领域模型(充血模型)
│ │ ├── entities/ # 实体Product, ProductLineItem
│ │ ├── value-objects/ # 值对象Money, Address
│ │ ├── types/ # 领域内通用类型
│ │ ├── enums/ # 领域枚举
│ │ └── events/ # 领域事件(如果需要)
│ ├── application/ # 应用服务层 - 协调领域对象完成用例
│ │ └── services/ # 应用服务ProductApplicationService
│ ├── infrastructure/ # 基础设施层 - 领域层的具体实现
│ │ └── api/ # 数据获取实现HttpProductRepository
│ ├── ports/ # 端口(接口/抽象类)- 定义契约
│ │ └── repositories/ # 仓储接口ProductRepository
│ └── use-cases/ # (可选)或将用例放在这里
├── app/ # 【交付层】应用组装和配置路由、状态管理、主布局、UI库设置、错误处理、依赖注入容器
│ ├── components/ # 通用UI组件与业务无关如ButtonModal
│ ├── layouts/ # 布局组件
│ ├── di/ # 依赖注入配置
│ ├── router/ # Vue Router 配置
│ ├── stores/ # Pinia Store用于UI状态管理非核心业务状态
│ └── main.ts # 应用入口,依赖注入的组装之地
│ └── App.vue # 根组件
├── features/ # 【交付层】基于领域的功能模块:关注具体的业务功能实现,它是舞台上的“演员”和“节目”。
│ └── product/ # 订单功能模块app-product
│ ├── components/ # 订单领域专用的UI组件
│ ├── views/ # 订单相关的页面级Vue组件
│ ├── router/ # 订单相关的页面级Vue组件
│ ├── composables/ # 订单相关的Vue组合式函数
│ └── index.ts # 订单功能模块的出口
├── shared/ # 【app-shared模块】共享资源
│ ├── infra(infrastructure) # 基础设施层
│ │ ├── http/
│ ├── lib/ # 第三方库的封装/工具函数
│ ├── utils/ # 纯工具函数
│ └── types/ # 全局通用的TypeScript类型定义
│ └── constants/ # 全局常量(如错误码、正则)
│ └── exceptions/ # 通用异常如NotFoundError
└── public/ # 静态资源目录
└── 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";
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' },
]
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)

13
back-end/.gitignore vendored Normal file
View File

@@ -0,0 +1,13 @@
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# Fresh build directory
_fresh/
# npm + other dependencies
node_modules/
vendor/

17
back-end/README.md Normal file
View File

@@ -0,0 +1,17 @@
# Fresh project
Your new Fresh project is ready to go. You can follow the Fresh "Getting
Started" guide here: https://fresh.deno.dev/docs/getting-started
### Usage
Make sure to install Deno:
https://docs.deno.com/runtime/getting_started/installation
Then start the project in development mode:
```
deno task dev
```
This will watch the project directory and restart as necessary.

144
back-end/assets/styles.css Normal file
View File

@@ -0,0 +1,144 @@
*,
*::before,
*::after {
box-sizing: border-box;
}
* {
margin: 0;
}
button {
color: inherit;
}
button, [role="button"] {
cursor: pointer;
}
code {
font-family:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
"Courier New", monospace;
font-size: 1em;
}
img,
svg {
display: block;
}
img,
video {
max-width: 100%;
height: auto;
}
html {
line-height: 1.5;
-webkit-text-size-adjust: 100%;
font-family:
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
"Noto Color Emoji";
}
.transition-colors {
transition-property: background-color, border-color, color, fill, stroke;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.my-6 {
margin-bottom: 1.5rem;
margin-top: 1.5rem;
}
.text-4xl {
font-size: 2.25rem;
line-height: 2.5rem;
}
.mx-2 {
margin-left: 0.5rem;
margin-right: 0.5rem;
}
.my-4 {
margin-bottom: 1rem;
margin-top: 1rem;
}
.mx-auto {
margin-left: auto;
margin-right: auto;
}
.px-4 {
padding-left: 1rem;
padding-right: 1rem;
}
.py-8 {
padding-bottom: 2rem;
padding-top: 2rem;
}
.bg-\[\#86efac\] {
background-color: #86efac;
}
.text-3xl {
font-size: 1.875rem;
line-height: 2.25rem;
}
.py-6 {
padding-bottom: 1.5rem;
padding-top: 1.5rem;
}
.px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.py-1 {
padding-bottom: 0.25rem;
padding-top: 0.25rem;
}
.border-gray-500 {
border-color: #6b7280;
}
.bg-white {
background-color: #fff;
}
.flex {
display: flex;
}
.gap-8 {
grid-gap: 2rem;
gap: 2rem;
}
.font-bold {
font-weight: 700;
}
.max-w-screen-md {
max-width: 768px;
}
.flex-col {
flex-direction: column;
}
.items-center {
align-items: center;
}
.justify-center {
justify-content: center;
}
.border-2 {
border-width: 2px;
}
.rounded-sm {
border-radius: 0.25rem;
}
.hover\:bg-gray-200:hover {
background-color: #e5e7eb;
}
.tabular-nums {
font-variant-numeric: tabular-nums;
}
.min-h-screen {
min-height: 100vh;
}
.fresh-gradient {
background-color: rgb(134, 239, 172);
background-image: linear-gradient(
to right bottom,
rgb(219, 234, 254),
rgb(187, 247, 208),
rgb(254, 249, 195)
);
}

2
back-end/client.ts Normal file
View File

@@ -0,0 +1,2 @@
// Import CSS files here for hot module reloading to work.
import "./assets/styles.css";

53
back-end/deno.json Normal file
View File

@@ -0,0 +1,53 @@
{
"nodeModulesDir": "auto",
"tasks": {
"check": "deno fmt --check . && deno lint . && deno check",
"dev": "vite",
"build": "vite build",
"start": "deno serve -A _fresh/server.js",
"update": "deno run -A -r jsr:@fresh/update ."
},
"lint": {
"rules": {
"tags": [
"fresh",
"recommended"
]
}
},
"exclude": [
"**/_fresh/*"
],
"imports": {
"fresh": "jsr:@fresh/core@^2.1.0",
"@fresh/plugin-vite": "jsr:@fresh/plugin-vite@^1.0.3",
"vite": "npm:vite@^7.1.3"
},
"compilerOptions": {
"lib": [
"dom",
"dom.asynciterable",
"dom.iterable",
"deno.ns"
],
"jsxPrecompileSkipElements": [
"a",
"img",
"source",
"body",
"html",
"head",
"title",
"meta",
"script",
"link",
"style",
"base",
"noscript",
"template"
],
"types": [
"vite/client"
]
}
}

1231
back-end/deno.lock generated Normal file

File diff suppressed because it is too large Load Diff

91
back-end/main.ts Normal file
View File

@@ -0,0 +1,91 @@
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,
}))
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,
}
];
const Res = <T extends Array<Object> | Object>(data: T) => {
return JSON.stringify({
code: 200,
message: "success",
result: data
});
}
// http://127.0.0.1:3210/api/productsList
app.get("/api/productsList", (ctx: { params: { name: any; }; }) => {
return new Response(Res(mockProductsList));
});
// 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')
if (!id) {
return new Response(Res("缺少id参数"));
}
const result = mockProductsList.find(product => product.id == Number(id))
if (!result) {
return new Response(Res("找不到数据"));
}
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();
});
app.use(exampleLoggerMiddleware);

BIN
back-end/static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

19
back-end/static/logo.svg Normal file
View File

@@ -0,0 +1,19 @@
<svg width="40" height="40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M34.092 8.845C38.929 20.652 34.092 27 30 30.5c1 3.5-2.986 4.222-4.5 2.5-4.457 1.537-13.512 1.487-20-5C2 24.5 4.73 16.714 14 11.5c8-4.5 16-7 20.092-2.655Z"
fill="#FFDB1E"
/>
<path
d="M14 11.5c6.848-4.497 15.025-6.38 18.368-3.47C37.5 12.5 21.5 22.612 15.5 25c-6.5 2.587-3 8.5-6.5 8.5-3 0-2.5-4-5.183-7.75C2.232 23.535 6.16 16.648 14 11.5Z"
fill="#fff"
stroke="#FFDB1E"
/>
<path
d="M28.535 8.772c4.645 1.25-.365 5.695-4.303 8.536-3.732 2.692-6.606 4.21-7.923 4.83-.366.173-1.617-2.252-1.617-1 0 .417-.7 2.238-.934 2.326-1.365.512-4.223 1.29-5.835 1.29-3.491 0-1.923-4.754 3.014-9.122.892-.789 1.478-.645 2.283-.645-.537-.773-.534-.917.403-1.546C17.79 10.64 23 8.77 25.212 8.42c.366.014.82.35.82.629.41-.14 2.095-.388 2.503-.278Z"
fill="#FFE600"
/>
<path
d="M14.297 16.49c.985-.747 1.644-1.01 2.099-2.526.566.121.841-.08 1.29-.701.324.466 1.657.608 2.453.701-.715.451-1.057.852-1.452 2.106-1.464-.611-3.167-.302-4.39.42Z"
fill="#fff"
/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

9
back-end/utils.ts Normal file
View File

@@ -0,0 +1,9 @@
import { createDefine } from "fresh";
// This specifies the type of "ctx.state" which is used to share
// data among middlewares, layouts and routes.
export interface State {
shared: string;
}
export const define = createDefine<State>();

13
back-end/vite.config.ts Normal file
View File

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

318
doc/DockerSwarm.md Normal file
View File

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

BIN
doc/jgt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 989 KiB

BIN
doc/newci.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 KiB

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

@@ -0,0 +1,20 @@
# 包含 [skip actions] 的提交会跳过工作流
git commit -m "修复样式问题[skip actions]"
```bash
# 运行自动化集群部署脚本
./docker/deploy-swarm.sh
# --- 其他命令 ---
# 删除之前的部署(可选)
docker stack rm app-product
# 查看 app-product 集群运行情况
docker stack ps app-product
# 持续观察服务副本数是否达到预期
watch docker stack services app-product
```

34
doc/代码存放位置.md Normal file
View File

@@ -0,0 +1,34 @@
### 1. 领域层Domains
- 实体类:`src/domains/product/domain/entities/Product.ts`
- 仓储接口(端口):`src/domains/product/ports/repositories/ProductRepository.ts`
### 2. 基础设施层Infrastructure
- HTTP仓储实现`src/domains/product/infrastructure/api/HttpProductRepository.ts`
- 通用HTTP客户端`src/shared/infra/http/httpClient.ts`
### 3. 应用服务层Application
- 商品应用服务:`src/domains/product/application/services/ProductApplicationService.ts`
### 4. 依赖注入配置
- 商品领域DI配置`src/app/di/productModule.ts`
### 5. 路由配置(交付层)
- 商品路由:`src/features/product/router/index.ts`
- 主路由配置:`src/app/router/index.ts`
### 6. 功能实现(交付层)
- 商品列表页面:`src/features/product/views/ProductList.tsx`
- 商品详情页面:`src/features/product/views/ProductDetail.tsx`
- 商品卡片组件:`src/features/product/components/ProductCard.tsx`
### 7. 通用UI组件交付层
- 分页组件:`src/app/components/Pagination.tsx`
- 加载组件:`src/app/components/Loading.tsx`(代码中引用)
- 按钮组件:`src/app/components/Button.tsx`(代码中引用)

View File

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

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

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

View File

@@ -1,98 +0,0 @@
### 横切关注点
- 特征:横向,影响多个模块,那些无法放入任何一个业务模块,而是会"横着"贯穿多个甚至所有模块的技术性需求。
- 案例:日志记录、身份认证、授权、事务管理、异常处理、缓存、性能监控
## 业务关注点
- 特征:通常局限于一个业务模块内
- 案例:计算订单总额、验证用户邮箱格式、处理库存扣减
### 对象职责的设计理念对比:
#### 贫血模型
- 特征:对象仅仅是数据的容器(一堆属性的集合),没有任何业务逻辑。所有操作这些数据的逻辑都放在外部的“服务”或“工具”类中。
- 比喻:像一个没有灵魂的空壳,或者一个数据结构。它只负责装数据,至于数据怎么用、怎么变,它不管。
```typescript
// 1. 贫血的Order实体只有数据没有行为
class Order {
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;
}
order.total = total; // 从外部修改对象的数据
}
markAsPaid(order: Order): void {
if (order.total <= 0) {
throw new Error('订单金额无效');
}
order.status = 'paid'; // 从外部修改对象的状态
}
}
// 使用方式
const order = new Order();
order.items = [...];
const orderService = new OrderService();
orderService.calculateTotal(order); // 服务来算总额
orderService.markAsPaid(order); // 服务来改状态
```
#### 充血模型
- 特征:对象不仅包含数据,还包含与这些数据紧密相关的业务逻辑。它是有行为和责任的。
- 比喻:像一个有智慧的专家。它不仅拥有数据,还知道如何操作和处理自己的数据。
```typescript
// 充血的Order实体数据 + 行为
class Order {
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('订单金额无效,无法支付');
}
this.status = 'paid';
}
// 提供访问内部数据的方法(如果需要)
get total(): number {
return this._total;
}
get items(): ReadonlyArray<OrderItem> {
return [...this._items]; // 返回副本,保护内部数据
}
}
// 使用方式:对象是聪明的,告诉它做什么就行,而不是一步步操作它
const order = new Order([...]);
order.markAsPaid(); // 对象自己处理状态变更
```

114
doc/杂项必看.md Normal file
View File

@@ -0,0 +1,114 @@
### 依赖注意
DDD 中允许 “外层依赖内层”(交付层依赖领域层),但不允许 “内层依赖外层”。
- 正确features交付层 → domains领域层
- 错误domains领域层 → features交付层
#### 避免类型重复定义
如果不在视图中直接使用领域类型,就需要在交付层重新定义一套类似的类型(如 “视图专用商品类型”),这会导致:
- 代码冗余和不一致风险
- 类型转换的额外成本
- 业务规则分散(领域层的类型约束可能在视图层被忽略)
### 横切关注点
- 特征:横向,影响多个模块,那些无法放入任何一个业务模块,而是会"横着"贯穿多个甚至所有模块的技术性需求。
- 案例:日志记录、身份认证、授权、事务管理、异常处理、缓存、性能监控
## 业务关注点
- 特征:通常局限于一个业务模块内
- 案例:计算订单总额、验证用户邮箱格式、处理库存扣减
### 对象职责的设计理念对比:
#### 贫血模型
- 特征:对象仅仅是数据的容器(一堆属性的集合),没有任何业务逻辑。所有操作这些数据的逻辑都放在外部的“服务”或“工具”类中。
- 比喻:像一个没有灵魂的空壳,或者一个数据结构。它只负责装数据,至于数据怎么用、怎么变,它不管。
```typescript
// 1. 贫血的Order实体只有数据没有行为
class Order {
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;
}
order.total = total; // 从外部修改对象的数据
}
markAsPaid(order: Order): void {
if (order.total <= 0) {
throw new Error('订单金额无效');
}
order.status = 'paid'; // 从外部修改对象的状态
}
}
// 使用方式
const order = new Order();
order.items = [ ... ];
const orderService = new OrderService();
orderService.calculateTotal(order); // 服务来算总额
orderService.markAsPaid(order); // 服务来改状态
```
#### 充血模型
- 特征:对象不仅包含数据,还包含与这些数据紧密相关的业务逻辑。它是有行为和责任的。
- 比喻:像一个有智慧的专家。它不仅拥有数据,还知道如何操作和处理自己的数据。
```typescript
// 充血的Order实体数据 + 行为
class Order {
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('订单金额无效,无法支付');
}
this.status = 'paid';
}
// 提供访问内部数据的方法(如果需要)
get total(): number {
return this._total;
}
get items(): ReadonlyArray<OrderItem> {
return [ ...this._items ]; // 返回副本,保护内部数据
}
}
// 使用方式:对象是聪明的,告诉它做什么就行,而不是一步步操作它
const order = new Order([ ... ]);
order.markAsPaid(); // 对象自己处理状态变更
```

View File

@@ -1,30 +1,34 @@
```txt
src/
├── domains/ # 【核心】领域层 - 所有业务领域模块
│ └── order/ # 订单领域模块
│ └── product/ # 订单领域模块
│ ├── domain/ # 领域模型(充血模型)
│ │ ├── entities/ # 实体(如:Order, OrderLineItem
│ │ ├── entities/ # 实体(如:Product, ProductLineItem
│ │ ├── value-objects/ # 值对象Money, Address
│ │ ├── types/ # 领域内通用类型
│ │ ├── enums/ # 领域枚举
│ │ └── events/ # 领域事件(如果需要)
│ ├── application/ # 应用服务层 - 协调领域对象完成用例
│ │ └── services/ # 应用服务(如:OrderApplicationService
│ ├── infrastructure/ # 基础设施层 - 领域层的具体实现
│ │ └── api/ # 数据获取实现HttpOrderRepository
│ │ └── services/ # 应用服务(如:ProductApplicationService
│ ├── infr/ # 基础设施层 - 领域层的具体实现
│ │ └── api/ # 数据获取实现HttpProductRepository
│ │ └── config/ # 请求接口地址的配置
│ ├── ports/ # 端口(接口/抽象类)- 定义契约
│ │ └── repositories/ # 仓储接口(如:OrderRepository
│ │ └── repositories/ # 仓储接口(如:ProductRepository
│ └── use-cases/ # (可选)或将用例放在这里
├── app/ # 【交付层】应用组装和配置路由、状态管理、主布局、UI库设置、错误处理、依赖注入容器
│ ├── components/ # 通用UI组件与业务无关如ButtonModal
│ ├── layouts/ # 布局组件
│ ├── di/ # 依赖注入配置
│ ├── router/ # Vue Router 配置
│ ├── stores/ # Pinia Store用于UI状态管理非核心业务状态
│ └── main.ts # 应用入口,依赖注入的组装之地
│ └── App.vue # 根组件
├── features/ # 【交付层】基于领域的功能模块:关注具体的业务功能实现,它是舞台上的“演员”和“节目”。
│ └── order/ # 订单功能模块
│ └── product/ # 订单功能模块
│ ├── components/ # 订单领域专用的UI组件
│ ├── views/ # 订单相关的页面级Vue组件
│ ├── router/ # 订单相关的页面级Vue组件
│ ├── composables/ # 订单相关的Vue组合式函数
│ └── index.ts # 订单功能模块的出口
├── shared/ # 共享资源

View File

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

View File

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

View File

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

Binary file not shown.

View File

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

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

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,32 @@
{
"name": "main-app",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"dep": "concurrently \"npm run build\" \"npm run preview\"",
"preview": "vite preview --port 1300 --strictPort",
"build": "vite build --watch",
"type-check": "vue-tsc --build"
},
"dependencies": {
"pinia": "^3.0.3",
"vite-plugin-nacos": "link:../vite-plugin-nacos",
"vue": "^3.5.22",
"vue-facing-decorator": "^4.0.1",
"vue-router": "^4.5.1"
},
"devDependencies": {
"@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"
}
}

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,18 @@
import { Component, Vue } from 'vue-facing-decorator'
import { RouterLink, RouterView } from "vue-router";
@Component()
export default class App extends Vue {
render() {
return (
<>
<div id="nav">
<RouterLink to="/"></RouterLink> |
<RouterLink to="/about"></RouterLink> |
<RouterLink to="/Products"></RouterLink>
</div>
<RouterView></RouterView>
</>
)
}
}

View File

@@ -0,0 +1,45 @@
import { Component, Prop, TSX, Vue } from 'vue-facing-decorator'
interface Props {
text?: string
list?: ListItemType[]
}
interface ListItemType {
id: number
title: string
}
@Component()
export default class Header extends TSX<Props>()(Vue) implements Props {
@Prop({ default: '默认数值' })
text!: string;
@Prop()
list!: ListItemType[];
created() {
console.log("created: ", this)
}
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>
)
}
}

View File

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

View File

@@ -0,0 +1,30 @@
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

View 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;

View File

@@ -0,0 +1,9 @@
import {createRouter, createWebHistory} from 'vue-router'
import routes from './Config'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
})
export default router

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
}
}
}

View File

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

View File

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

View File

@@ -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[];
}

View File

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

View File

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

View File

@@ -0,0 +1,44 @@
{
"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"
]
}

View File

@@ -0,0 +1,55 @@
import path from 'path'
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';
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))
})

View File

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

View File

@@ -0,0 +1,31 @@
{
"name": "vite-plugin-nacos",
"version": "1.1.3",
"description": "Vite 插件实现Nacos服务注册与发现插件用于模块联邦",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"build": "tsc",
"prepublishOnly": "npm run build",
"push": "npm run build && npm publish"
},
"dependencies": {
"chalk": "^5.6.2",
"nacos": "^2.6.0"
},
"devDependencies": {
"@types/node": "^24.6.1",
"typescript": "~5.9.3",
"vite": "^7.1.7"
},
"keywords": [
"nacos",
"module-federation",
"service-discovery"
],
"author": "",
"license": "MIT"
}

View File

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

View File

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

View File

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

View 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');
}

View 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"
]
}

2903
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,35 +1,16 @@
{
"name": "vue-project",
"version": "0.0.0",
"private": true,
"type": "module",
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"name": "front-end",
"version": "1.0.0",
"description": "",
"keywords": [],
"author": "",
"license": "ISC",
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build"
},
"dependencies": {
"axios": "^1.12.2",
"pinia": "^3.0.3",
"vue": "^3.5.18",
"vue-router": "^4.0.6"
},
"devDependencies": {
"@originjs/vite-plugin-federation": "^1.4.1",
"@tsconfig/node22": "^22.0.2",
"@types/node": "^22.16.5",
"@vitejs/plugin-vue": "^6.0.1",
"@vitejs/plugin-vue-jsx": "^5.0.1",
"@vue/tsconfig": "^0.7.0",
"npm-run-all2": "^8.0.4",
"typescript": "~5.8.0",
"vite": "^7.0.6",
"vite-plugin-vue-devtools": "^8.0.0",
"vue-tsc": "^3.0.4"
"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
View File

@@ -0,0 +1,7 @@
packages:
- 'front-end/*'
- 'docker/traefik'
- 'back-end'
# 排除不需要的目录(可选)
- '!** /node_modules'
- '!** /dist'

View File

@@ -1,17 +0,0 @@
import { defineComponent } from 'vue'
import { RouterLink, RouterView } from "vue-router";
export default defineComponent({
name: 'App',
setup() {
return () => (
<>
<div id="nav">
<RouterLink to="/"></RouterLink> |
<RouterLink to="/about"></RouterLink>
</div>
<RouterView></RouterView>
</>
)
}
})

View File

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

View File

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

Before

Width:  |  Height:  |  Size: 276 B

View File

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

View File

@@ -1,54 +0,0 @@
import { defineComponent, defineEmits, onMounted} from 'vue'
import type { JSX } from "vue/jsx-runtime";
export default defineComponent({
name: "Header",
props: {
text: {
type: Array<{ id: number, title: string }>,
default: () => []
}
},
emits: {
sendMessage: (message: string) => typeof message === 'string'
},
setup(props, { attrs, slots, emit, expose }) {
onMounted(() => {
console.log("onMounted")
})
const handleClick = () => {
console.log("handleClick")
emit('sendMessage', '子给父的数据')
}
const button = (): JSX.Element => {
return <button onClick={handleClick}>emit点我</button>
}
return () => (
<div>
<h1></h1>
{ button() }
{ slots.default?.() }
{ slots.header?.() }
<div class="ScopedSlots">
<p></p>
<ul>
{
props.text.map(item => {
return slots.scoped?.(item)
})
}
</ul>
</div>
</div>
)
}
})

View File

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

View File

@@ -1,24 +0,0 @@
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: () => import('../views/Home'),
},
{
path: '/about',
name: 'about',
component: () => import('../views/About'),
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('../views/NotFound')
}
],
})
export default router

View File

@@ -1,12 +0,0 @@
import { defineComponent } from 'vue'
export default defineComponent({
name: "About",
setup() {
return () => (
<div class="about">
<h1></h1>
</div>
)
}
})

View File

@@ -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>
)
}
})

View File

@@ -1,11 +0,0 @@
import { defineComponent } from 'vue'
export default defineComponent({
setup() {
return () => (
<div class="404">
<h1>404</h1>
</div>
)
}
})

View File

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

View File

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

View File

@@ -1,4 +0,0 @@
import HttpConfig from './HttpConfig';
import ThemeConfig from './ThemeConfig';
export { HttpConfig, ThemeConfig };

View File

@@ -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();

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