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

|
||||||
|
|
||||||
问:vue3 jsx defineComponent 语法,实际项目中 setup 代码太长,怎么办?
|
## CI/CD 自动化集群部署
|
||||||
答:使用组合式函数 (Composables) - 最推荐
|
|
||||||
|
|
||||||
案例:
|

|
||||||
|
|
||||||
```jsx
|
## 开发日记
|
||||||
// composables/useUserData.js
|
|
||||||
import { ref, onMounted } from 'vue'
|
|
||||||
import api from '@/api'
|
|
||||||
|
|
||||||
export function useUserData(userId) {
|
- [x] 基于DDD理念改造
|
||||||
const user = ref(null)
|
- [x] 拆分领域模块为联邦模块,实现微前端
|
||||||
const loading = ref(false)
|
- [x] 基于 Vue3+OOP+JSX 改造项目代码
|
||||||
const error = ref(null)
|
- [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 {
|
- [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)
|
||||||
user.value = await api.getUser(userId)
|
- [@originjs/vite-plugin-federation](https://www.npmjs.com/package/@originjs/vite-plugin-federation)
|
||||||
} catch (err) {
|
- [Deno](https://deno.com/)
|
||||||
error.value = err.message
|
- [TypeScript](https://www.typescriptlang.org/)
|
||||||
} finally {
|
- [GraphQL](https://graphql.org/)
|
||||||
loading.value = false
|
- [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组件(与业务无关,如Button,Modal)
|
||||||
|
│ ├── 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,
|
render() {
|
||||||
loading,
|
return (
|
||||||
error,
|
<div class="home">
|
||||||
refetch: fetchUser
|
<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
|
interface Props {
|
||||||
// UserProfile.jsx
|
text?: string
|
||||||
import { defineComponent } from 'vue'
|
list?: ListItemType[]
|
||||||
import { useUserData } from '@/composables/useUserData'
|
}
|
||||||
|
|
||||||
export default defineComponent({
|
interface ListItemType {
|
||||||
name: 'UserProfile',
|
id: number
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
props: {
|
@Component()
|
||||||
userId: {
|
export default class Header extends TSX<Props>()(Vue) implements Props {
|
||||||
type: String,
|
|
||||||
required: true
|
@Prop({ default: '默认数值' })
|
||||||
|
text!: string;
|
||||||
|
|
||||||
|
@Prop()
|
||||||
|
list!: ListItemType[];
|
||||||
|
|
||||||
|
created() {
|
||||||
|
console.log("created: ", this)
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
setup(props) {
|
render() {
|
||||||
const { user, loading, error, refetch } = useUserData(props.userId)
|
return (
|
||||||
|
<div class="header">
|
||||||
return () => (
|
头部组件
|
||||||
<div>
|
<p>{ this.text }</p>
|
||||||
{loading.value && <div>Loading...</div>}
|
{ this.$slots.default?.() }
|
||||||
{error.value && <div>Error: {error.value}</div>}
|
{ this.$slots.haveName?.() }
|
||||||
{user.value && (
|
<ul>
|
||||||
<div>
|
{
|
||||||
<h2>{user.value.name}</h2>
|
this.list.map(
|
||||||
<p>{user.value.email}</p>
|
item =>
|
||||||
</div>
|
this.$slots.item?.(item)
|
||||||
)}
|
)
|
||||||
</div>
|
}
|
||||||
)
|
</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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
91
back-end/main.ts
Normal 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
|
After Width: | Height: | Size: 22 KiB |
19
back-end/static/logo.svg
Normal 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
@@ -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
@@ -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
@@ -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
|
After Width: | Height: | Size: 989 KiB |
BIN
doc/newci.png
Normal file
|
After Width: | Height: | Size: 734 KiB |
20
doc/临时命令.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# 包含 [skip actions] 的提交会跳过工作流
|
||||||
|
git commit -m "修复样式问题[skip actions]"
|
||||||
|
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 运行自动化集群部署脚本
|
||||||
|
./docker/deploy-swarm.sh
|
||||||
|
|
||||||
|
# --- 其他命令 ---
|
||||||
|
|
||||||
|
# 删除之前的部署(可选)
|
||||||
|
docker stack rm app-product
|
||||||
|
|
||||||
|
# 查看 app-product 集群运行情况
|
||||||
|
docker stack ps app-product
|
||||||
|
|
||||||
|
# 持续观察服务副本数是否达到预期
|
||||||
|
watch docker stack services app-product
|
||||||
|
```
|
||||||
|
|
||||||
34
doc/代码存放位置.md
Normal 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`(代码中引用)
|
||||||
21
doc/完善微前端架构的建议.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# 完善微前端架构的建议
|
||||||
|
|
||||||
|
除了已有的主模块、订单联邦模块、共享资源模块以及 nacos 服务中心和 traefik api 网关外,还可以考虑以下几个方面来完善微前端架构:
|
||||||
|
|
||||||
|
* **状态管理与通信机制**:微前端架构中,各模块间的状态管理和通信是关键。可以采用 Pinia 等状态管理库,实现跨模块的数据共享。例如,通过在共享资源模块中定义
|
||||||
|
Pinia store,然后在主模块和订单联邦模块中引入并使用,确保所有微前端模块都能访问同一缓存存储,实现数据共享。
|
||||||
|
|
||||||
|
* **路由管理**:虽然 @originjs/vite - plugin - federation
|
||||||
|
插件本身支持模块的动态加载,但仍需要合理设计路由系统,确保不同模块之间的路由切换顺畅。可以在主模块中统一管理路由,根据用户的访问路径,动态加载对应的微前端模块。例如,使用
|
||||||
|
Vue Router 的动态路由功能,根据不同的 URL 路径加载订单联邦模块或共享资源模块的组件。
|
||||||
|
|
||||||
|
* **样式隔离与共享**:需要确保各个微前端模块的样式不会相互干扰。可以采用 CSS Modules、Shadow DOM 或 CSS-in-JS
|
||||||
|
等技术来实现样式隔离。同时,对于一些共享的样式,可以在共享资源模块中进行定义,然后通过模块联邦的方式共享给其他模块。
|
||||||
|
|
||||||
|
* **错误处理机制**
|
||||||
|
:由于微前端模块是独立开发和部署的,可能会出现各种错误。需要建立统一的错误处理机制,捕获并处理微前端模块在加载和运行过程中出现的错误。例如,在主模块中设置全局的错误捕获器,当订单联邦模块或共享资源模块出现错误时,能够及时捕获并展示友好的错误提示信息。
|
||||||
|
|
||||||
|
* **性能优化**:微前端架构中,多个模块的加载可能会影响性能。可以通过代码分割、懒加载等方式来优化性能。@originjs/vite -
|
||||||
|
plugin - federation 插件本身支持代码分割,可将每个微前端模块的代码拆分成多个小块,只在需要时加载相应的代码块。此外,还可以使用缓存策略,减少重复加载。
|
||||||
|
|
||||||
|
* **安全机制**:随着微前端模块的增加,安全问题也需要重视。需要确保各个模块之间的数据传输是安全的,防止跨站脚本攻击(XSS)、跨站请求伪造(CSRF)等安全漏洞。可以采用身份验证、授权、输入验证等安全措施来保障系统的安全。
|
||||||
51
doc/思路整理.md
Normal file
@@ -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 无法退出。。。。
|
||||||
|
|
||||||
|
## 如何解决??
|
||||||
98
doc/杂项.md
@@ -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
@@ -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(); // 对象自己处理状态变更
|
||||||
|
```
|
||||||
18
doc/目录结构.md
@@ -1,30 +1,34 @@
|
|||||||
```txt
|
```txt
|
||||||
src/
|
src/
|
||||||
├── domains/ # 【核心】领域层 - 所有业务领域模块
|
├── domains/ # 【核心】领域层 - 所有业务领域模块
|
||||||
│ └── order/ # 订单领域模块
|
│ └── product/ # 订单领域模块
|
||||||
│ ├── domain/ # 领域模型(充血模型)
|
│ ├── domain/ # 领域模型(充血模型)
|
||||||
│ │ ├── entities/ # 实体(如:Order, OrderLineItem)
|
│ │ ├── entities/ # 实体(如:Product, ProductLineItem)
|
||||||
│ │ ├── value-objects/ # 值对象(如:Money, Address)
|
│ │ ├── value-objects/ # 值对象(如:Money, Address)
|
||||||
|
│ │ ├── types/ # 领域内通用类型
|
||||||
│ │ ├── enums/ # 领域枚举
|
│ │ ├── enums/ # 领域枚举
|
||||||
│ │ └── events/ # 领域事件(如果需要)
|
│ │ └── events/ # 领域事件(如果需要)
|
||||||
│ ├── application/ # 应用服务层 - 协调领域对象完成用例
|
│ ├── application/ # 应用服务层 - 协调领域对象完成用例
|
||||||
│ │ └── services/ # 应用服务(如:OrderApplicationService)
|
│ │ └── services/ # 应用服务(如:ProductApplicationService)
|
||||||
│ ├── infrastructure/ # 基础设施层 - 领域层的具体实现
|
│ ├── infr/ # 基础设施层 - 领域层的具体实现
|
||||||
│ │ └── api/ # 数据获取实现(如:HttpOrderRepository)
|
│ │ └── api/ # 数据获取实现(如:HttpProductRepository)
|
||||||
|
│ │ └── config/ # 请求接口地址的配置
|
||||||
│ ├── ports/ # 端口(接口/抽象类)- 定义契约
|
│ ├── ports/ # 端口(接口/抽象类)- 定义契约
|
||||||
│ │ └── repositories/ # 仓储接口(如:OrderRepository)
|
│ │ └── repositories/ # 仓储接口(如:ProductRepository)
|
||||||
│ └── use-cases/ # (可选)或将用例放在这里
|
│ └── use-cases/ # (可选)或将用例放在这里
|
||||||
├── app/ # 【交付层】应用组装和配置:路由、状态管理、主布局、UI库设置、错误处理、依赖注入容器
|
├── app/ # 【交付层】应用组装和配置:路由、状态管理、主布局、UI库设置、错误处理、依赖注入容器
|
||||||
│ ├── components/ # 通用UI组件(与业务无关,如Button,Modal)
|
│ ├── components/ # 通用UI组件(与业务无关,如Button,Modal)
|
||||||
│ ├── layouts/ # 布局组件
|
│ ├── layouts/ # 布局组件
|
||||||
|
│ ├── di/ # 依赖注入配置
|
||||||
│ ├── router/ # Vue Router 配置
|
│ ├── router/ # Vue Router 配置
|
||||||
│ ├── stores/ # Pinia Store(用于UI状态管理,非核心业务状态)
|
│ ├── stores/ # Pinia Store(用于UI状态管理,非核心业务状态)
|
||||||
│ └── main.ts # 应用入口,依赖注入的组装之地
|
│ └── main.ts # 应用入口,依赖注入的组装之地
|
||||||
│ └── App.vue # 根组件
|
│ └── App.vue # 根组件
|
||||||
├── features/ # 【交付层】基于领域的功能模块:关注具体的业务功能实现,它是舞台上的“演员”和“节目”。
|
├── features/ # 【交付层】基于领域的功能模块:关注具体的业务功能实现,它是舞台上的“演员”和“节目”。
|
||||||
│ └── order/ # 订单功能模块
|
│ └── product/ # 订单功能模块
|
||||||
│ ├── components/ # 订单领域专用的UI组件
|
│ ├── components/ # 订单领域专用的UI组件
|
||||||
│ ├── views/ # 订单相关的页面级Vue组件
|
│ ├── views/ # 订单相关的页面级Vue组件
|
||||||
|
│ ├── router/ # 订单相关的页面级Vue组件
|
||||||
│ ├── composables/ # 订单相关的Vue组合式函数
|
│ ├── composables/ # 订单相关的Vue组合式函数
|
||||||
│ └── index.ts # 订单功能模块的出口
|
│ └── index.ts # 订单功能模块的出口
|
||||||
├── shared/ # 共享资源
|
├── shared/ # 共享资源
|
||||||
|
|||||||
147
docker/nacos-cluster/README.md
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
## nacos-cluster hostname 集群版
|
||||||
|
|
||||||
|
把 Nacos 部署到多台Linux 服务器上的版本配置,下面是**文件说明**:
|
||||||
|
|
||||||
|
- docker-compose.yml: Docker compose + Docker Swarm 的编排文件
|
||||||
|
- nacos_config_export.zip: 存储在Nacos配置中心的项目配置,部署成功后台导入即可
|
||||||
|
- mysql-schema.sql: Nacos 集群公用的数据库,会自动导入到Mysql,不用动
|
||||||
|
- prometheus: Prometheus 的配置文件
|
||||||
|
|
||||||
|
## 条件
|
||||||
|
|
||||||
|
- 至少三台起步的Linux主机(安装有docker),你可以使用虚拟机。
|
||||||
|
- 一个能正常访问Docker的网络环境(拉取镜像)
|
||||||
|
- 耐心,毅力,别慌!
|
||||||
|
|
||||||
|
## 集群配置
|
||||||
|
|
||||||
|
假设我们有三台Linux主机,他们的IP地址为:
|
||||||
|
|
||||||
|
- 1: 192.168.139.84
|
||||||
|
- 2: 192.168.139.15
|
||||||
|
- 3: 192.168.139.178
|
||||||
|
|
||||||
|
选一台你喜欢的(我选1),作为集群的 **Manager**,然后SSH登录进去,获取管理员权限,然后:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 初始化 Swarm 集群
|
||||||
|
docker swarm init --advertise-addr 192.168.139.84
|
||||||
|
```
|
||||||
|
|
||||||
|
命令执行完成后,你会得到这样的返回:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Swarm initialized: current node (z5sucmc80ns8bxrdqhs71dg8x) is now a manager.
|
||||||
|
|
||||||
|
To add a worker to this swarm, run the following command:
|
||||||
|
|
||||||
|
# 复制这个
|
||||||
|
docker swarm join --token SWMTKN-1-3n9xxxxnwvsj6-4rhxx8xxxxzbbul 192.168.139.84:2377
|
||||||
|
|
||||||
|
To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.
|
||||||
|
```
|
||||||
|
|
||||||
|
然后SSH登录**worker**机,也就是 上面的 2,3 服务器,获取SSH权限后,分别执行上面给的命令:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker swarm join --token SWMTKN-1-3n9xxxxnwvsj6-4rhxx8xxxxzbbul 192.168.139.84:2377
|
||||||
|
```
|
||||||
|
|
||||||
|
**这样 2,3 服务器就和1服务器组成了集群**
|
||||||
|
|
||||||
|
可以指执行命令查看是否组成集群:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看所有节点
|
||||||
|
docker node ls
|
||||||
|
```
|
||||||
|
|
||||||
|
注意,如果你的服务器有防火墙,那么请记住放行端口,下面是Ubuntu上的例子:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# --- Docker Swarm 集群通信端口 ---
|
||||||
|
|
||||||
|
# TCP 2377: Swarm 管理端口 (仅在 Manager 和 Worker 之间通信)
|
||||||
|
sudo ufw allow 2377/tcp
|
||||||
|
# TCP/UDP 7946: 节点之间的控制平面和数据平面的 gossip 协议通信
|
||||||
|
sudo ufw allow 7946/tcp
|
||||||
|
sudo ufw allow 7946/udp
|
||||||
|
# UDP 4789: VXLAN 端口,用于 overlay 网络中容器之间的通信
|
||||||
|
sudo ufw allow 4789/udp
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# --- MySQL 数据库端口 (可选) ---
|
||||||
|
|
||||||
|
# TCP 3306: MySQL 数据库端口(需要外部访问数据库时候才开)
|
||||||
|
sudo ufw allow 3306/tcp
|
||||||
|
|
||||||
|
|
||||||
|
# --- Nacos 集群端口 ---
|
||||||
|
|
||||||
|
# Nacos 管理后台端口
|
||||||
|
sudo ufw allow 8080/tcp
|
||||||
|
# 客户端gRPC请求服务端端口,用于客户端向服务端发起连接和请求
|
||||||
|
sudo ufw allow 9848/tcp
|
||||||
|
# Nacos HTTP API 端口,用于Nacos AdminAPI及HTTP OpenAPI的访问
|
||||||
|
sudo ufw allow 8848/tcp
|
||||||
|
|
||||||
|
|
||||||
|
# 重新加载防火墙以应用所有规则
|
||||||
|
sudo ufw reload
|
||||||
|
|
||||||
|
# 查看配置和状态
|
||||||
|
ufw status
|
||||||
|
```
|
||||||
|
|
||||||
|
## 部署运行
|
||||||
|
|
||||||
|
### 运行启动
|
||||||
|
|
||||||
|
将本文件夹内所有文件上传到 1号服务器,然后在文件夹内执行命令:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 部署栈,这个过程有点慢,因为需要拉取镜像,等!
|
||||||
|
docker stack deploy -c docker-compose.yml nacos-cluster
|
||||||
|
|
||||||
|
# 命令完成后,需要等待 Nacos 启动,大概几十秒到一分钟这样!!
|
||||||
|
# 启动成功后,直接查看下面教程【Nacos后台】
|
||||||
|
|
||||||
|
|
||||||
|
# --- 其他命令 ---
|
||||||
|
|
||||||
|
# 部署后,不用了,可以使用这个 删除栈
|
||||||
|
docker stack rm nacos-cluster
|
||||||
|
|
||||||
|
# 可以查看Nacos的 实时日志:
|
||||||
|
docker service logs --follow nacos-cluster_nacos
|
||||||
|
|
||||||
|
# 可以查看Mysql的 实时日志:
|
||||||
|
docker service logs --follow nacos-cluster_mysql
|
||||||
|
|
||||||
|
# 查看 nacos-cluster 集群运行情况
|
||||||
|
docker stack ps nacos-cluster
|
||||||
|
|
||||||
|
# 持续观察服务副本数是否达到预期
|
||||||
|
watch docker stack services nacos-cluster
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nacos后台
|
||||||
|
|
||||||
|
选择一个地址浏览器打开,注册登录账户:
|
||||||
|
|
||||||
|
- (http://192.168.139.84:8080)[http://192.168.139.84:8080]
|
||||||
|
- (http://192.168.139.15:8080)[http://192.168.139.15:8080]
|
||||||
|
- (http://192.168.139.178:8080)[http://192.168.139.178:8080]
|
||||||
|
|
||||||
|
然后打开左侧导航 **配置管理** --> **配置列表** --> **导入配置**
|
||||||
|
选择 nacos_config.zip 导入即可。
|
||||||
|
|
||||||
|
### 其他后台:
|
||||||
|
|
||||||
|
- [Prometheus](http://192.168.139.84:9090)
|
||||||
|
- [Grafana](http://192.168.139.84:3000)
|
||||||
|
- 默认账户:admin
|
||||||
|
- 默认密码:密码admin
|
||||||
|
|
||||||
|
**结束**
|
||||||
162
docker/nacos-cluster/docker-compose.yml
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
# 使用版本 3.8,它与 Swarm 模式兼容
|
||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
# 定义服务
|
||||||
|
services:
|
||||||
|
# MySQL 数据库服务
|
||||||
|
mysql:
|
||||||
|
# 使用指定的 MySQL 镜像
|
||||||
|
image: mysql:8.0.31
|
||||||
|
# 使用环境变量文件,与原来的配置保持一致
|
||||||
|
environment:
|
||||||
|
- MYSQL_ROOT_PASSWORD=root
|
||||||
|
- MYSQL_DATABASE=nacos_devtest
|
||||||
|
- MYSQL_USER=nacos
|
||||||
|
- MYSQL_PASSWORD=nacos
|
||||||
|
- LANG=C.UTF-8
|
||||||
|
# 数据卷挂载,确保 MySQL 数据持久化
|
||||||
|
volumes:
|
||||||
|
- mysql-data:/var/lib/mysql
|
||||||
|
- ./mysql-schema.sql:/docker-entrypoint-initdb.d/mysql-schema.sql
|
||||||
|
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
|
||||||
|
ports:
|
||||||
|
- target: 3306
|
||||||
|
published: 3306
|
||||||
|
protocol: tcp
|
||||||
|
mode: host # 数据库通常使用 host 模式,避免路由网格开销
|
||||||
|
# 健康检查,确保数据库启动完成后再启动 Nacos
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD", "mysqladmin", "ping", "-h", "localhost" ]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 10
|
||||||
|
# Swarm 部署配置
|
||||||
|
deploy:
|
||||||
|
# 副本数为 1,因为单个数据库实例足够
|
||||||
|
replicas: 1
|
||||||
|
# 重启策略:任何情况下容器退出都自动重启
|
||||||
|
restart_policy:
|
||||||
|
condition: any
|
||||||
|
# 将服务加入我们创建的网络
|
||||||
|
networks:
|
||||||
|
- nacos-net
|
||||||
|
|
||||||
|
# Nacos 服务
|
||||||
|
nacos:
|
||||||
|
hostname: "nacos{{.Task.Slot}}"
|
||||||
|
# 使用 Nacos 镜像
|
||||||
|
image: nacos/nacos-server:v3.1.0
|
||||||
|
# 挂载日志目录
|
||||||
|
volumes:
|
||||||
|
- nacos-logs:/home/nacos/logs
|
||||||
|
# 暴露 Nacos 所需的端口
|
||||||
|
ports:
|
||||||
|
- target: 8080
|
||||||
|
published: 8080
|
||||||
|
protocol: tcp
|
||||||
|
mode: ingress # Web 控制台使用路由网格实现负载均衡
|
||||||
|
- target: 7848
|
||||||
|
published: 7848
|
||||||
|
protocol: tcp
|
||||||
|
mode: ingress # 集群通信端口
|
||||||
|
- target: 8848
|
||||||
|
published: 8848
|
||||||
|
protocol: tcp
|
||||||
|
mode: ingress # 主要服务端口,使用路由网格
|
||||||
|
- target: 9848
|
||||||
|
published: 9848
|
||||||
|
protocol: tcp
|
||||||
|
mode: ingress # gRPC 端口
|
||||||
|
- target: 9849
|
||||||
|
published: 9849
|
||||||
|
protocol: tcp
|
||||||
|
mode: ingress # gRPC 端口
|
||||||
|
# 使用环境变量文件
|
||||||
|
environment:
|
||||||
|
- MODE=cluster
|
||||||
|
- PREFER_HOST_MODE=hostname
|
||||||
|
- NACOS_SERVERS=nacos1:8848 nacos2:8848 nacos3:8848
|
||||||
|
- SPRING_DATASOURCE_PLATFORM=mysql
|
||||||
|
- MYSQL_SERVICE_HOST=mysql
|
||||||
|
- MYSQL_SERVICE_DB_NAME=nacos_devtest
|
||||||
|
- MYSQL_SERVICE_PORT=3306
|
||||||
|
- MYSQL_SERVICE_USER=nacos
|
||||||
|
- MYSQL_SERVICE_PASSWORD=nacos
|
||||||
|
- MYSQL_SERVICE_RETRY_COUNT=10
|
||||||
|
- MYSQL_SERVICE_RETRY_DELAY=5000
|
||||||
|
- MYSQL_SERVICE_DB_PARAM=characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true
|
||||||
|
- NACOS_SERVER_AUTH_ENABLE=false
|
||||||
|
- NACOS_CONSOLE_AUTH_ENABLE=false
|
||||||
|
- NACOS_AUTH_ENABLE=false
|
||||||
|
- NACOS_AUTH_IDENTITY_KEY=bmy
|
||||||
|
- NACOS_AUTH_IDENTITY_VALUE=lb714500.
|
||||||
|
- NACOS_AUTH_TOKEN=KjdAdzBIMTBtKElJS0NrWTIxNkJ0RXN4YXlHaipSUmdDZA==
|
||||||
|
# Swarm 部署配置
|
||||||
|
deploy:
|
||||||
|
# 关键:指定副本数为 3,Swarm 会自动创建 3 个 Nacos 容器
|
||||||
|
replicas: 3
|
||||||
|
# 重启策略
|
||||||
|
restart_policy:
|
||||||
|
condition: any
|
||||||
|
# 启动顺序控制
|
||||||
|
# 这是 Swarm 中实现 "depends_on" 的方式
|
||||||
|
# 它会在 mysql 服务健康检查通过后,才开始启动 Nacos 服务
|
||||||
|
update_config:
|
||||||
|
parallelism: 1
|
||||||
|
delay: 30s
|
||||||
|
# 资源限制(可选,可根据你的服务器配置调整)
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '0.50'
|
||||||
|
memory: '512M'
|
||||||
|
reservations:
|
||||||
|
cpus: '0.25'
|
||||||
|
memory: '256M'
|
||||||
|
networks:
|
||||||
|
- nacos-net
|
||||||
|
|
||||||
|
# Prometheus 服务
|
||||||
|
prometheus:
|
||||||
|
image: prom/prometheus:latest
|
||||||
|
volumes:
|
||||||
|
- ./prometheus/prometheus-cluster.yaml:/etc/prometheus/prometheus.yml
|
||||||
|
ports:
|
||||||
|
- target: 9090
|
||||||
|
published: 9090
|
||||||
|
protocol: tcp
|
||||||
|
mode: ingress # 监控服务使用路由网格
|
||||||
|
deploy:
|
||||||
|
replicas: 1
|
||||||
|
restart_policy:
|
||||||
|
condition: on-failure
|
||||||
|
update_config:
|
||||||
|
order: start-first
|
||||||
|
networks:
|
||||||
|
- nacos-net
|
||||||
|
|
||||||
|
# Grafana 服务
|
||||||
|
grafana:
|
||||||
|
image: grafana/grafana:latest
|
||||||
|
ports:
|
||||||
|
- target: 3000
|
||||||
|
published: 3000
|
||||||
|
protocol: tcp
|
||||||
|
mode: ingress # Grafana 使用路由网格
|
||||||
|
deploy:
|
||||||
|
# 通常 Grafana 单实例即可
|
||||||
|
replicas: 1
|
||||||
|
restart_policy:
|
||||||
|
condition: on-failure
|
||||||
|
networks:
|
||||||
|
- nacos-net
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
nacos-logs:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
mysql-data:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
networks:
|
||||||
|
nacos-net:
|
||||||
|
driver: overlay
|
||||||
188
docker/nacos-cluster/mysql-schema.sql
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 1999-2018 Alibaba Group Holding Ltd.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/******************************************/
|
||||||
|
/* 表名称 = config_info */
|
||||||
|
/******************************************/
|
||||||
|
CREATE TABLE `config_info`
|
||||||
|
(
|
||||||
|
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
|
||||||
|
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
|
||||||
|
`group_id` varchar(128) DEFAULT NULL COMMENT 'group_id',
|
||||||
|
`content` longtext NOT NULL COMMENT 'content',
|
||||||
|
`md5` varchar(32) DEFAULT NULL COMMENT 'md5',
|
||||||
|
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
|
||||||
|
`src_user` text COMMENT 'source user',
|
||||||
|
`src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
|
||||||
|
`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
|
||||||
|
`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
|
||||||
|
`c_desc` varchar(256) DEFAULT NULL COMMENT 'configuration description',
|
||||||
|
`c_use` varchar(64) DEFAULT NULL COMMENT 'configuration usage',
|
||||||
|
`effect` varchar(64) DEFAULT NULL COMMENT '配置生效的描述',
|
||||||
|
`type` varchar(64) DEFAULT NULL COMMENT '配置的类型',
|
||||||
|
`c_schema` text COMMENT '配置的模式',
|
||||||
|
`encrypted_data_key` varchar(1024) NOT NULL DEFAULT '' COMMENT '密钥',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_configinfo_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info';
|
||||||
|
|
||||||
|
/******************************************/
|
||||||
|
/* 表名称 = config_info since 2.5.0 */
|
||||||
|
/******************************************/
|
||||||
|
CREATE TABLE `config_info_gray`
|
||||||
|
(
|
||||||
|
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id',
|
||||||
|
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
|
||||||
|
`group_id` varchar(128) NOT NULL COMMENT 'group_id',
|
||||||
|
`content` longtext NOT NULL COMMENT 'content',
|
||||||
|
`md5` varchar(32) DEFAULT NULL COMMENT 'md5',
|
||||||
|
`src_user` text COMMENT 'src_user',
|
||||||
|
`src_ip` varchar(100) DEFAULT NULL COMMENT 'src_ip',
|
||||||
|
`gmt_create` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 'gmt_create',
|
||||||
|
`gmt_modified` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 'gmt_modified',
|
||||||
|
`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
|
||||||
|
`tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',
|
||||||
|
`gray_name` varchar(128) NOT NULL COMMENT 'gray_name',
|
||||||
|
`gray_rule` text NOT NULL COMMENT 'gray_rule',
|
||||||
|
`encrypted_data_key` varchar(256) NOT NULL DEFAULT '' COMMENT 'encrypted_data_key',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_configinfogray_datagrouptenantgray` (`data_id`,`group_id`,`tenant_id`,`gray_name`),
|
||||||
|
KEY `idx_dataid_gmt_modified` (`data_id`,`gmt_modified`),
|
||||||
|
KEY `idx_gmt_modified` (`gmt_modified`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='config_info_gray';
|
||||||
|
|
||||||
|
/******************************************/
|
||||||
|
/* 表名称 = config_tags_relation */
|
||||||
|
/******************************************/
|
||||||
|
CREATE TABLE `config_tags_relation`
|
||||||
|
(
|
||||||
|
`id` bigint(20) NOT NULL COMMENT 'id',
|
||||||
|
`tag_name` varchar(128) NOT NULL COMMENT 'tag_name',
|
||||||
|
`tag_type` varchar(64) DEFAULT NULL COMMENT 'tag_type',
|
||||||
|
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
|
||||||
|
`group_id` varchar(128) NOT NULL COMMENT 'group_id',
|
||||||
|
`tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',
|
||||||
|
`nid` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'nid, 自增长标识',
|
||||||
|
PRIMARY KEY (`nid`),
|
||||||
|
UNIQUE KEY `uk_configtagrelation_configidtag` (`id`,`tag_name`,`tag_type`),
|
||||||
|
KEY `idx_tenant_id` (`tenant_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_tag_relation';
|
||||||
|
|
||||||
|
/******************************************/
|
||||||
|
/* 表名称 = group_capacity */
|
||||||
|
/******************************************/
|
||||||
|
CREATE TABLE `group_capacity`
|
||||||
|
(
|
||||||
|
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
`group_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Group ID,空字符表示整个集群',
|
||||||
|
`quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
|
||||||
|
`usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
|
||||||
|
`max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
|
||||||
|
`max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数,,0表示使用默认值',
|
||||||
|
`max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
|
||||||
|
`max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
|
||||||
|
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_group_id` (`group_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='集群、各Group容量信息表';
|
||||||
|
|
||||||
|
/******************************************/
|
||||||
|
/* 表名称 = his_config_info */
|
||||||
|
/******************************************/
|
||||||
|
CREATE TABLE `his_config_info`
|
||||||
|
(
|
||||||
|
`id` bigint(20) unsigned NOT NULL COMMENT 'id',
|
||||||
|
`nid` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'nid, 自增标识',
|
||||||
|
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
|
||||||
|
`group_id` varchar(128) NOT NULL COMMENT 'group_id',
|
||||||
|
`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
|
||||||
|
`content` longtext NOT NULL COMMENT 'content',
|
||||||
|
`md5` varchar(32) DEFAULT NULL COMMENT 'md5',
|
||||||
|
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
|
||||||
|
`src_user` text COMMENT 'source user',
|
||||||
|
`src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
|
||||||
|
`op_type` char(10) DEFAULT NULL COMMENT 'operation type',
|
||||||
|
`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
|
||||||
|
`encrypted_data_key` varchar(1024) NOT NULL DEFAULT '' COMMENT '密钥',
|
||||||
|
`publish_type` varchar(50) DEFAULT 'formal' COMMENT 'publish type gray or formal',
|
||||||
|
`gray_name` varchar(50) DEFAULT NULL COMMENT 'gray name',
|
||||||
|
`ext_info` longtext DEFAULT NULL COMMENT 'ext info',
|
||||||
|
PRIMARY KEY (`nid`),
|
||||||
|
KEY `idx_gmt_create` (`gmt_create`),
|
||||||
|
KEY `idx_gmt_modified` (`gmt_modified`),
|
||||||
|
KEY `idx_did` (`data_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='多租户改造';
|
||||||
|
|
||||||
|
|
||||||
|
/******************************************/
|
||||||
|
/* 表名称 = tenant_capacity */
|
||||||
|
/******************************************/
|
||||||
|
CREATE TABLE `tenant_capacity`
|
||||||
|
(
|
||||||
|
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
`tenant_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Tenant ID',
|
||||||
|
`quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
|
||||||
|
`usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
|
||||||
|
`max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
|
||||||
|
`max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数',
|
||||||
|
`max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
|
||||||
|
`max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
|
||||||
|
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_tenant_id` (`tenant_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='租户容量信息表';
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE `tenant_info`
|
||||||
|
(
|
||||||
|
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
|
||||||
|
`kp` varchar(128) NOT NULL COMMENT 'kp',
|
||||||
|
`tenant_id` varchar(128) default '' COMMENT 'tenant_id',
|
||||||
|
`tenant_name` varchar(128) default '' COMMENT 'tenant_name',
|
||||||
|
`tenant_desc` varchar(256) DEFAULT NULL COMMENT 'tenant_desc',
|
||||||
|
`create_source` varchar(32) DEFAULT NULL COMMENT 'create_source',
|
||||||
|
`gmt_create` bigint(20) NOT NULL COMMENT '创建时间',
|
||||||
|
`gmt_modified` bigint(20) NOT NULL COMMENT '修改时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_tenant_info_kptenantid` (`kp`,`tenant_id`),
|
||||||
|
KEY `idx_tenant_id` (`tenant_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='tenant_info';
|
||||||
|
|
||||||
|
CREATE TABLE `users`
|
||||||
|
(
|
||||||
|
`username` varchar(50) NOT NULL PRIMARY KEY COMMENT 'username',
|
||||||
|
`password` varchar(500) NOT NULL COMMENT 'password',
|
||||||
|
`enabled` boolean NOT NULL COMMENT 'enabled'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE `roles`
|
||||||
|
(
|
||||||
|
`username` varchar(50) NOT NULL COMMENT 'username',
|
||||||
|
`role` varchar(50) NOT NULL COMMENT 'role',
|
||||||
|
UNIQUE INDEX `idx_user_role` (`username` ASC, `role` ASC) USING BTREE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE `permissions`
|
||||||
|
(
|
||||||
|
`role` varchar(50) NOT NULL COMMENT 'role',
|
||||||
|
`resource` varchar(128) NOT NULL COMMENT 'resource',
|
||||||
|
`action` varchar(8) NOT NULL COMMENT 'action',
|
||||||
|
UNIQUE INDEX `uk_role_permission` (`role`,`resource`,`action`) USING BTREE
|
||||||
|
);
|
||||||
BIN
docker/nacos-cluster/nacos_config_export.zip
Normal file
34
docker/nacos-cluster/prometheus/prometheus-cluster.yaml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# my global config
|
||||||
|
global:
|
||||||
|
scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
|
||||||
|
evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
|
||||||
|
# scrape_timeout is set to the global default (10s).
|
||||||
|
|
||||||
|
# Alertmanager configuration
|
||||||
|
alerting:
|
||||||
|
alertmanagers:
|
||||||
|
- static_configs:
|
||||||
|
- targets:
|
||||||
|
# - alertmanager:9093
|
||||||
|
|
||||||
|
# Load rules once and periodically evaluate them according to the global 'evaluation_interval'.
|
||||||
|
rule_files:
|
||||||
|
# - "first_rules.yml"
|
||||||
|
# - "second_rules.yml"
|
||||||
|
|
||||||
|
# A scrape configuration containing exactly one endpoint to scrape:
|
||||||
|
# Here it's Prometheus itself.
|
||||||
|
scrape_configs:
|
||||||
|
# The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
|
||||||
|
- job_name: 'prometheus'
|
||||||
|
|
||||||
|
# metrics_path defaults to '/metrics'
|
||||||
|
# scheme defaults to 'http'.
|
||||||
|
|
||||||
|
static_configs:
|
||||||
|
- targets: [ 'localhost:9090' ]
|
||||||
|
|
||||||
|
- job_name: 'nacos'
|
||||||
|
metrics_path: '/nacos/actuator/prometheus'
|
||||||
|
static_configs:
|
||||||
|
- targets: [ "nacos1:8848","nacos2:8848","nacos3:8848" ]
|
||||||
10
docker/traefik/README.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
## traefik 网关
|
||||||
|
|
||||||
|
部署在公网的 traefik 网关,代码放到Linux服务器,然后根据实际情况修改 `dynamic.yml` 文件,
|
||||||
|
然后在根目录执行命令即可:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose down && docker-compose up -d
|
||||||
|
# 或者这个命令
|
||||||
|
# docker compose down && docker compose up -d
|
||||||
|
```
|
||||||
27
docker/traefik/docker-compose.yml
Normal file
@@ -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
@@ -0,0 +1,113 @@
|
|||||||
|
http:
|
||||||
|
routers:
|
||||||
|
|
||||||
|
#
|
||||||
|
app-shared-router:
|
||||||
|
rule: Host(`nacos.ddd.com`) && PathPrefix(`/app-shared`)
|
||||||
|
service: app-shared-service
|
||||||
|
observability:
|
||||||
|
metrics: true
|
||||||
|
accessLogs: true
|
||||||
|
tracing: true
|
||||||
|
traceVerbosity: detailed
|
||||||
|
entryPoints:
|
||||||
|
- web
|
||||||
|
|
||||||
|
app-product-router:
|
||||||
|
rule: Host(`nacos.ddd.com`) && PathPrefix(`/app-product`)
|
||||||
|
service: app-product-service
|
||||||
|
observability:
|
||||||
|
metrics: true
|
||||||
|
accessLogs: true
|
||||||
|
tracing: true
|
||||||
|
traceVerbosity: detailed
|
||||||
|
entryPoints:
|
||||||
|
- web
|
||||||
|
|
||||||
|
# 使用域名访问前端main-app
|
||||||
|
main-ddd-router:
|
||||||
|
rule: Host(`main.ddd.com`)
|
||||||
|
service: main-ddd-service
|
||||||
|
observability:
|
||||||
|
metrics: true
|
||||||
|
accessLogs: true
|
||||||
|
tracing: true
|
||||||
|
traceVerbosity: detailed
|
||||||
|
entryPoints:
|
||||||
|
- web
|
||||||
|
|
||||||
|
# Nacos 集群 管理后台的统一访问域名
|
||||||
|
nacosui-ddd-router:
|
||||||
|
rule: Host(`nacosui.ddd.com`)
|
||||||
|
service: nacosui-ddd-service
|
||||||
|
observability:
|
||||||
|
metrics: true
|
||||||
|
accessLogs: true
|
||||||
|
tracing: true
|
||||||
|
traceVerbosity: detailed
|
||||||
|
entryPoints:
|
||||||
|
- web
|
||||||
|
|
||||||
|
# trafik 网关 代理 内网 nacos 集群 的地址
|
||||||
|
nacos-ddd-router:
|
||||||
|
rule: Host(`nacos.ddd.com`)
|
||||||
|
service: nacos-ddd-service
|
||||||
|
observability:
|
||||||
|
metrics: true
|
||||||
|
accessLogs: true
|
||||||
|
tracing: true
|
||||||
|
traceVerbosity: detailed
|
||||||
|
entryPoints:
|
||||||
|
- web
|
||||||
|
|
||||||
|
# trafik 网关的管理后台的域名
|
||||||
|
trafikui-ddd-router:
|
||||||
|
rule: Host(`trafikui.ddd.com`)
|
||||||
|
service: trafikui-ddd-service
|
||||||
|
observability:
|
||||||
|
metrics: true
|
||||||
|
accessLogs: true
|
||||||
|
tracing: true
|
||||||
|
traceVerbosity: detailed
|
||||||
|
entryPoints:
|
||||||
|
- web
|
||||||
|
|
||||||
|
|
||||||
|
services:
|
||||||
|
app-shared-service:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: http://192.168.139.172:1333
|
||||||
|
- url: http://192.168.139.184:1333
|
||||||
|
- url: http://192.168.139.186:1333
|
||||||
|
|
||||||
|
app-product-service:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: http://192.168.139.19:1301
|
||||||
|
- url: http://192.168.139.120:1301
|
||||||
|
- url: http://192.168.139.242:1301
|
||||||
|
|
||||||
|
main-ddd-service:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: http://192.168.31.101:1300
|
||||||
|
|
||||||
|
nacosui-ddd-service:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: http://192.168.139.84:8080
|
||||||
|
- url: http://192.168.139.15:8080
|
||||||
|
- url: http://192.168.139.178:8080
|
||||||
|
|
||||||
|
nacos-ddd-service:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: http://192.168.139.84:8848
|
||||||
|
- url: http://192.168.139.15:8848
|
||||||
|
- url: http://192.168.139.178:8848
|
||||||
|
|
||||||
|
trafikui-ddd-service:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: http://192.168.139.80:8080
|
||||||
15
docker/traefik/package.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "traefik-naco",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.12.2",
|
||||||
|
"js-yaml": "^4.1.0"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC"
|
||||||
|
}
|
||||||
63
docker/traefik/src/config/index.js
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
filePath: path.join(__dirname, '../../dynamic.yml'),
|
||||||
|
nacosApi: {
|
||||||
|
baseUrl: 'http://192.168.31.101:8848/nacos/v1/ns',
|
||||||
|
// baseUrl: 'http://192.168.31.101:8848/nacos/v3/admin/ns',
|
||||||
|
|
||||||
|
apiList: {
|
||||||
|
instanceInfo: '/instance/list',
|
||||||
|
serviceList: '/service/list',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
nacosConfig: {
|
||||||
|
nameSpace: 'public',
|
||||||
|
},
|
||||||
|
// 每隔五秒检查一下最新配置
|
||||||
|
check_time: 10000,
|
||||||
|
// traefik 通用中间件配置
|
||||||
|
traefikConfig: {
|
||||||
|
middlewaresStr: ['compress', 'ipWhiteList', 'rateLimit', 'retries'],
|
||||||
|
middlewaresConfig: {
|
||||||
|
// 限流中间件
|
||||||
|
rateLimit: {
|
||||||
|
rateLimit: {
|
||||||
|
average: 100,
|
||||||
|
burst: 200,
|
||||||
|
period: '1s',
|
||||||
|
sourceCriterion: {
|
||||||
|
ipStrategy: {depth: 2}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 压缩中间件
|
||||||
|
compress: {
|
||||||
|
compress: {
|
||||||
|
excludedContentTypes: ['text/event-stream'],
|
||||||
|
minResponseBodyBytes: 1024
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// IP白名单中间件
|
||||||
|
ipWhiteList: {
|
||||||
|
ipWhiteList: {
|
||||||
|
sourceRange: ['127.0.0.1/32', '192.168.107.0/24']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// // 重定向中间件(HTTP→HTTPS)
|
||||||
|
// redirectToHttps: {
|
||||||
|
// redirectScheme: {
|
||||||
|
// scheme: 'https',
|
||||||
|
// permanent: true
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// 重试中间件(关键修复:将重试配置移到这里)
|
||||||
|
retries: {
|
||||||
|
retry: {
|
||||||
|
attempts: 3,
|
||||||
|
initialInterval: '200ms'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
docker/traefik/src/http/index.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
const axios = require('axios');
|
||||||
|
const config = require('../config');
|
||||||
|
const {AxiosError} = require("axios");
|
||||||
|
|
||||||
|
class Http {
|
||||||
|
instance = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.instance = axios.create({
|
||||||
|
baseURL: config.nacosApi.baseUrl,
|
||||||
|
timeout: 10000,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.ResponseInterceptor();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async get(params) {
|
||||||
|
return await this.instance.get(params.url, {
|
||||||
|
params: params.data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async ResponseInterceptor() {
|
||||||
|
this.instance.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
return Promise.reject(error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new Http
|
||||||
9
docker/traefik/src/index.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
const utils = require('./utils')
|
||||||
|
const config = require('./config')
|
||||||
|
|
||||||
|
const uti = new utils();
|
||||||
|
|
||||||
|
setInterval(
|
||||||
|
uti.getServiceName.bind(uti),
|
||||||
|
config.check_time
|
||||||
|
)
|
||||||
109
docker/traefik/src/utils/index.js
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
const config = require("../config")
|
||||||
|
const http = require("../http")
|
||||||
|
const yaml = require("js-yaml");
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
|
||||||
|
class Utils {
|
||||||
|
|
||||||
|
// 存储 Nacos上 服务列表 的服务名称
|
||||||
|
ServiceNameList = []
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.getServiceName()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取Nacos上的服务名称数组
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async getServiceName() {
|
||||||
|
console.log("1: [getServiceName] 从Nacos 获取服务名..")
|
||||||
|
const {doms} = await http.get({
|
||||||
|
url: config.nacosApi.apiList.serviceList,
|
||||||
|
data: {pageNo: 1, pageSize: 20},
|
||||||
|
});
|
||||||
|
if (doms.length === 0) {
|
||||||
|
throw new Error("No service found for service list");
|
||||||
|
}
|
||||||
|
this.serviceName = doms;
|
||||||
|
console.log("2: [getServiceName] 服务名为:", this.serviceName)
|
||||||
|
await this.generateTraefikConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 Nacos 获取服务实例
|
||||||
|
* @param serviceName
|
||||||
|
* @returns {Promise<*|*[]>}
|
||||||
|
*/
|
||||||
|
async getServiceInstances(serviceName) {
|
||||||
|
const {hosts} = await http.get({
|
||||||
|
url: config.nacosApi.apiList.instanceInfo,
|
||||||
|
data: {serviceName, namespaceId: config.nacosConfig.nameSpace},
|
||||||
|
});
|
||||||
|
return hosts || []
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成 traefik 动态配置
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async generateTraefikConfig() {
|
||||||
|
const traefikConf = {
|
||||||
|
http: {
|
||||||
|
routers: {},
|
||||||
|
services: {},
|
||||||
|
middlewares: config.traefikConfig.middlewaresConfig,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const serviceName of this.serviceName) {
|
||||||
|
const instances = await this.getServiceInstances(serviceName);
|
||||||
|
if (instances.length === 0) {
|
||||||
|
console.warn(`服务 ${serviceName} 无可用实例`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 配置服务(服务名 + 实例列表)
|
||||||
|
traefikConf.http.services[`${serviceName}-service`] = {
|
||||||
|
loadBalancer: {
|
||||||
|
servers: instances.map(inst => ({
|
||||||
|
url: `http://${inst.ip}:${inst.port}`,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. 配置路由(路径前缀 + 绑定服务)
|
||||||
|
traefikConf.http.routers[`${serviceName}-router`] = {
|
||||||
|
rule: `PathPrefix(\`/${serviceName}\`)`, // 如 /main、/product
|
||||||
|
service: `${serviceName}-service`,
|
||||||
|
entryPoints: ['web'],
|
||||||
|
// 应用中间件(压缩、IP白名单、限流、重试等)
|
||||||
|
middlewares: config.traefikConfig.middlewaresStr,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.stringToYaml(traefikConf)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成 yaml 配置文件
|
||||||
|
* @param conf
|
||||||
|
*/
|
||||||
|
stringToYaml(conf) {
|
||||||
|
console.log("3: [stringToYaml] 生成配置:", JSON.stringify(conf, null, 2));
|
||||||
|
console.log()
|
||||||
|
console.log()
|
||||||
|
|
||||||
|
const yamlString = yaml.dump(conf, {
|
||||||
|
indent: 2, // 缩进空格数
|
||||||
|
skipInvalid: true, // 跳过无效类型
|
||||||
|
noRefs: true // 禁止引用(防止循环引用问题)
|
||||||
|
});
|
||||||
|
fs.writeFileSync(config.filePath, yamlString, "utf8");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Utils
|
||||||
34
docker/traefik/traefik.yml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# 启用 Dashboard
|
||||||
|
api:
|
||||||
|
dashboard: true
|
||||||
|
insecure: true # 开发环境允许HTTP访问
|
||||||
|
|
||||||
|
# 入口点配置
|
||||||
|
entryPoints:
|
||||||
|
web:
|
||||||
|
address: ":80" # 监听80端口,Web 流量
|
||||||
|
|
||||||
|
# 服务发现与配置源(关键:子项缩进必须一致)
|
||||||
|
providers:
|
||||||
|
docker: { }
|
||||||
|
# 动态路由规则文件
|
||||||
|
file:
|
||||||
|
filename: "/etc/traefik/dynamic.yml"
|
||||||
|
watch: true # 自动监听文件变化
|
||||||
|
|
||||||
|
log:
|
||||||
|
filePath: "/etc/traefik/log-file.log"
|
||||||
|
format: common
|
||||||
|
level: INFO
|
||||||
|
|
||||||
|
accessLog:
|
||||||
|
filePath: "/etc/traefik/access.log"
|
||||||
|
format: "json"
|
||||||
|
addInternals: true
|
||||||
|
filters:
|
||||||
|
retryAttempts: true
|
||||||
|
minDuration: "2ms"
|
||||||
|
fields:
|
||||||
|
defaultMode: "keep"
|
||||||
|
headers:
|
||||||
|
defaultMode: "keep"
|
||||||
33
front-end/main-app/.env.development
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# 请求协议 http or https(建议线上配置证书后,使用https)
|
||||||
|
VITE_PREFIX=http://
|
||||||
|
|
||||||
|
#
|
||||||
|
# Nacos 集群的地址
|
||||||
|
# 注意:
|
||||||
|
# 1: 内网微前端模块,直接填内网Nacos IP:端口 就行
|
||||||
|
# 2: main-app位于公网,填 网关的公网地址
|
||||||
|
#
|
||||||
|
|
||||||
|
# 网关地址
|
||||||
|
VITE_NACOS_ADDRESS=nacos.ddd.com:80
|
||||||
|
# VITE_NACOS_ADDRESS=192.168.139.84:8848
|
||||||
|
|
||||||
|
|
||||||
|
# 注册到 Nacos 的服务名称(微前端模块名称)
|
||||||
|
VITE_SERVER_NAME=main-app
|
||||||
|
|
||||||
|
# 注册到 Nacos 的端口,也是前端运行的端口
|
||||||
|
VITE_SERVER_PORT=1300
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# 说明:项目要调用哪些 微前端 模块
|
||||||
|
# 格式:VITE_MODEL_XXXX_NAME=微前端模块名称(模块 的 开发者提供)
|
||||||
|
# 作用:通过名称从Nacos配置中心发现模块地址和端口,给 federation 实时加载
|
||||||
|
#
|
||||||
|
|
||||||
|
# product 商品模块
|
||||||
|
VITE_MODEL_PRODUCT_NAME=app-product
|
||||||
|
|
||||||
|
# shared 共享资源模块
|
||||||
|
VITE_MODEL_SHARED_NAME=app-shared
|
||||||
33
front-end/main-app/.env.production
Normal file
@@ -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
|
||||||
32
front-end/main-app/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
18
front-end/main-app/src/app/App.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
45
front-end/main-app/src/app/components/Header.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
1
front-end/main-app/src/app/layouts/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# 临时占位,上传空文件夹,保存文件结构用,后面删除
|
||||||
30
front-end/main-app/src/app/main.ts
Normal 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
|
||||||
|
|
||||||
|
|
||||||
25
front-end/main-app/src/app/router/Config.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { productRouter } from 'product/ProductRouter'
|
||||||
|
import { RouteRecordRaw } from 'vue-router'
|
||||||
|
|
||||||
|
|
||||||
|
const baseRouter: Array<RouteRecordRaw> = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'home',
|
||||||
|
component: () => import('../views/Home/Home'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/about',
|
||||||
|
name: 'about',
|
||||||
|
component: () => import('../views/About/About'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/:pathMatch(.*)*',
|
||||||
|
name: 'NotFound',
|
||||||
|
component: () => import('../views/NotFound')
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 聚合所有功能模块的路由
|
||||||
|
const routes: RouteRecordRaw[] = [ ...baseRouter, ...productRouter ];
|
||||||
|
export default routes;
|
||||||
9
front-end/main-app/src/app/router/index.ts
Normal 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
|
||||||
4
front-end/main-app/src/app/views/About/About.module.less
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
.About {
|
||||||
|
|
||||||
|
}
|
||||||
14
front-end/main-app/src/app/views/About/About.tsx
Normal file
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
5
front-end/main-app/src/app/views/Home/Home.module.less
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
.Home {
|
||||||
|
.title {
|
||||||
|
color: var(--theme-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
76
front-end/main-app/src/app/views/Home/Home.tsx
Normal file
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
24
front-end/main-app/src/app/views/NotFound.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# 临时占位,上传空文件夹,保存文件结构用,后面删除
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# 临时占位,上传空文件夹,保存文件结构用,后面删除
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# 临时占位,上传空文件夹,保存文件结构用,后面删除
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# 临时占位,上传空文件夹,保存文件结构用,后面删除
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# 临时占位,上传空文件夹,保存文件结构用,后面删除
|
||||||
1
front-end/main-app/src/domains/order/infr/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# 临时占位,上传空文件夹,保存文件结构用,后面删除
|
||||||
1
front-end/main-app/src/domains/order/ports/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# 临时占位,上传空文件夹,保存文件结构用,后面删除
|
||||||
1
front-end/main-app/src/features/order/assets/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# 临时占位,上传空文件夹,保存文件结构用,后面删除
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# 临时占位,上传空文件夹,保存文件结构用,后面删除
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# 临时占位,上传空文件夹,保存文件结构用,后面删除
|
||||||
1
front-end/main-app/src/features/order/views/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# 临时占位,上传空文件夹,保存文件结构用,后面删除
|
||||||
40
front-end/main-app/src/shared/lib/nacosInstance.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { nacosServiceManage, RegisterOptions } from "nacos-federation"
|
||||||
|
import { loadEnv } from 'vite';
|
||||||
|
|
||||||
|
console.log("process.env.NODE_ENV: ", process.env.NODE_ENV)
|
||||||
|
|
||||||
|
const env = loadEnv(
|
||||||
|
process.env.NODE_ENV || 'development', // 环境模式(development/production)
|
||||||
|
process.cwd() // 项目根目录(.env文件所在位置)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. 定义配置(同步执行,无await)
|
||||||
|
const nacosConfig: RegisterOptions = {
|
||||||
|
serverList: env.VITE_NACOS_ADDRESS,
|
||||||
|
serviceName: env.VITE_SERVER_NAME,
|
||||||
|
port: Number(env.VITE_SERVER_PORT),
|
||||||
|
namespace: env.VITE_NACOS_NAMESPACE || 'public'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. 声明实例变量(先不初始化)
|
||||||
|
let nacosInstance: nacosServiceManage | null = null;
|
||||||
|
|
||||||
|
// 4. 提供初始化函数(显式调用,避免顶级await)
|
||||||
|
export async function initNacosInstance() {
|
||||||
|
if (nacosInstance) {
|
||||||
|
return nacosInstance; // 已初始化则直接返回
|
||||||
|
}
|
||||||
|
// 首次调用时初始化
|
||||||
|
nacosInstance = await nacosServiceManage.create(nacosConfig);
|
||||||
|
console.log('Nacos 实例初始化完成');
|
||||||
|
return nacosInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 提供获取实例的方法(确保初始化后使用)
|
||||||
|
export async function getNacosInstance() {
|
||||||
|
if (!nacosInstance) {
|
||||||
|
// 若未初始化,先执行初始化
|
||||||
|
return await initNacosInstance();
|
||||||
|
}
|
||||||
|
return nacosInstance;
|
||||||
|
}
|
||||||
46
front-end/main-app/src/shared/types/ shims-tsx.d.ts
vendored
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
front-end/main-app/src/shared/types/env.d.ts
vendored
Normal 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
|
||||||
|
}
|
||||||
11
front-end/main-app/src/shared/types/global.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export interface AppConfig {
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
__APP_CONFIG__: AppConfig;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
44
front-end/main-app/src/shared/types/product.d.ts
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
// 声明领域层类型
|
||||||
|
declare module 'product/ProductDomain' {
|
||||||
|
// 商品实体类型(与联邦模块一致)
|
||||||
|
export class Product {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
price: number;
|
||||||
|
imageUrl: string;
|
||||||
|
stock: number;
|
||||||
|
categoryId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用服务类型
|
||||||
|
export class ProductApplicationService {
|
||||||
|
constructor(repo: ProductRepository);
|
||||||
|
|
||||||
|
findAll(): Promise<Array<any>>; // TODO any 需要修改
|
||||||
|
findById(id: number): Promise<Product>
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HttpProductRepository implements ProductRepository {
|
||||||
|
findAll(): Promise<Array<any>>; // TODO any 需要修改
|
||||||
|
findById(id: number): Promise<Product>
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ProductRepository {
|
||||||
|
findAll(): Promise<Array<any>>; // TODO any 需要修改
|
||||||
|
findById(id: number): Promise<Product>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 声明视图类型(同理补充其他模块)
|
||||||
|
// declare module 'productModule/ProductViews' {
|
||||||
|
// import type { DefineComponent } from 'vue';
|
||||||
|
// export const ProductList: DefineComponent; // 商品列表页
|
||||||
|
// export const ProductDetail: DefineComponent; // 商品详情页
|
||||||
|
// }
|
||||||
|
|
||||||
|
declare module 'product/ProductRouter' {
|
||||||
|
export const productRouter: RouteRecordRaw[];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
30
front-end/main-app/src/shared/types/shared.d.ts
vendored
Normal file
@@ -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;
|
||||||
|
}
|
||||||
5
front-end/main-app/src/shared/types/shims-vue.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
declare module '*.vue' {
|
||||||
|
import type { DefineComponent } from 'vue'
|
||||||
|
const component: DefineComponent<{}, {}, any>
|
||||||
|
export default component
|
||||||
|
}
|
||||||
44
front-end/main-app/tsconfig.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
55
front-end/main-app/vite.config.ts
Normal 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))
|
||||||
|
})
|
||||||
32
front-end/vite-plugin-nacos/README.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
功能分析:
|
||||||
|
|
||||||
|
1 注册服务
|
||||||
|
|
||||||
|
子模块:直接通过nacos 注册,给 内网 的 使用
|
||||||
|
|
||||||
|
主模块:main-app -> Traefik -> ConfigServer(nacos-federation) -> Nacos
|
||||||
|
|
||||||
|
main-app 发送 http 请求
|
||||||
|
/nacos/reg
|
||||||
|
参数:
|
||||||
|
ip,port
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
2 获取配置
|
||||||
|
|
||||||
|
子模块:直接通过 nacos 获取配置
|
||||||
|
|
||||||
|
主模块: main-app -> Traefik -> ConfigServer(nacos-federation) -> Nacos
|
||||||
|
|
||||||
|
|
||||||
|
注意:
|
||||||
|
|
||||||
|
|
||||||
|
增加功能:
|
||||||
|
|
||||||
|
认证授权机制
|
||||||
|
请求限流保护
|
||||||
|
操作日志记录
|
||||||
|
服务治理功能(如权重调整、元数据管理等)
|
||||||
31
front-end/vite-plugin-nacos/package.json
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"name": "vite-plugin-nacos",
|
||||||
|
"version": "1.1.3",
|
||||||
|
"description": "Vite 插件,实现Nacos服务注册与发现插件,用于模块联邦",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"prepublishOnly": "npm run build",
|
||||||
|
"push": "npm run build && npm publish"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"chalk": "^5.6.2",
|
||||||
|
"nacos": "^2.6.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^24.6.1",
|
||||||
|
"typescript": "~5.9.3",
|
||||||
|
"vite": "^7.1.7"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"nacos",
|
||||||
|
"module-federation",
|
||||||
|
"service-discovery"
|
||||||
|
],
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
96
front-end/vite-plugin-nacos/src/index.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
// 从插件实现文件导入
|
||||||
|
export * from './vite-plugin-nacos';
|
||||||
|
export * from './utils';
|
||||||
|
import { nacosServiceManage } from './nacosServiceManage';
|
||||||
|
import { mergeConfig } from 'vite';
|
||||||
|
|
||||||
|
export const init = async (env: Record<string, string>) => {
|
||||||
|
const serverList = env.VITE_NACOS_ADDRESS;
|
||||||
|
const serviceName = env.VITE_SERVER_NAME;
|
||||||
|
const port = Number(env.VITE_SERVER_PORT);
|
||||||
|
|
||||||
|
if (!serverList || !serviceName || Number.isNaN(port)) {
|
||||||
|
throw new Error('[vite-plugin-nacos] 缺少必要环境变量:VITE_NACOS_ADDRESS / VITE_SERVER_NAME / VITE_SERVER_PORT');
|
||||||
|
}
|
||||||
|
|
||||||
|
const nacos = await nacosServiceManage.create({
|
||||||
|
serverList,
|
||||||
|
serviceName,
|
||||||
|
port
|
||||||
|
});
|
||||||
|
|
||||||
|
const isMain = serviceName === 'main-app';
|
||||||
|
|
||||||
|
if (isMain) {
|
||||||
|
console.log('[vite-plugin-nacos] 不需要服务发现,进行配置发现...');
|
||||||
|
|
||||||
|
// 并发获取配置并稳健解析
|
||||||
|
const [baseUrlRaw, httpTimeOut] = await Promise.all([
|
||||||
|
nacos.getConfig('baseUrl', 'DEFAULT_GROUP'),
|
||||||
|
nacos.getConfig('httpTimeOut', 'DEFAULT_GROUP')
|
||||||
|
]);
|
||||||
|
|
||||||
|
let baseUrlConfig: Record<string, any> = {};
|
||||||
|
if (baseUrlRaw) {
|
||||||
|
try {
|
||||||
|
baseUrlConfig = JSON.parse(baseUrlRaw);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[vite-plugin-nacos] baseUrl 配置不是有效 JSON,忽略解析,使用空对象');
|
||||||
|
baseUrlConfig = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof httpTimeOut !== 'undefined' && httpTimeOut !== null) {
|
||||||
|
baseUrlConfig.httpTimeOut = httpTimeOut;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将新配置处理成 define,需要 JSON.stringify
|
||||||
|
const customizeEnv = Object.keys(baseUrlConfig).reduce<Record<string, string>>((acc, key) => {
|
||||||
|
acc[`import.meta.env.VITE_${key.toUpperCase()}`] = JSON.stringify(baseUrlConfig[key]);
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
// 将现有 env 处理成 define,需要 JSON.stringify
|
||||||
|
const existingDefine = Object.keys(env).reduce<Record<string, string>>((acc, key) => {
|
||||||
|
acc[`import.meta.env.${key}`] = JSON.stringify(env[key]);
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
// 使用 mergeConfig 合并 define 字段
|
||||||
|
const merged = mergeConfig({ define: existingDefine }, { define: customizeEnv });
|
||||||
|
|
||||||
|
await closeAllNacos(serviceName);
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 非主模块:进行服务发现
|
||||||
|
console.log('[vite-plugin-nacos] 进行 配置发现,获取依赖模块的地址...');
|
||||||
|
const modelNames: string[] = [];
|
||||||
|
for (const key in env) {
|
||||||
|
if (key.includes('VITE_MODEL_') && env[key]) {
|
||||||
|
modelNames.push(env[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 去重
|
||||||
|
const uniqueModelNames = Array.from(new Set(modelNames));
|
||||||
|
const modelUrl: Record<string, string> = {};
|
||||||
|
|
||||||
|
|
||||||
|
if (uniqueModelNames.length > 0) {
|
||||||
|
const results = await Promise.all(uniqueModelNames.map(async (val) => {
|
||||||
|
const url = await nacos.getConfig(val, 'DEFAULT_GROUP');
|
||||||
|
const envKey = `VITE_MODEL_${val.replace('app-', '').toUpperCase()}_URL`;
|
||||||
|
return [envKey, url] as const;
|
||||||
|
}));
|
||||||
|
for (const [k, v] of results) modelUrl[k] = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[vite-plugin-nacos] modelUrl: ', modelUrl);
|
||||||
|
await closeAllNacos("other-model");
|
||||||
|
return modelUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 手动关闭所有 Nacos 客户端(提供给外部在需要时调用) */
|
||||||
|
export async function closeAllNacos(serviceName: string) {
|
||||||
|
await nacosServiceManage.destroyAll();
|
||||||
|
}
|
||||||
179
front-end/vite-plugin-nacos/src/nacosServiceManage.ts
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
import { NacosConfigClient, NacosNamingClient } from 'nacos';
|
||||||
|
import { formatServiceUrl, getLocalIP } from "./utils";
|
||||||
|
import chalk from 'chalk';
|
||||||
|
|
||||||
|
const logger = console;
|
||||||
|
|
||||||
|
interface RegisterOptions {
|
||||||
|
serverList: string;
|
||||||
|
/** 服务名称(如 'main-app') */
|
||||||
|
serviceName: string;
|
||||||
|
/** 服务端口号 */
|
||||||
|
port: number;
|
||||||
|
/** 命名空间(用于环境隔离,默认 'public') */
|
||||||
|
namespace?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class nacosServiceManage {
|
||||||
|
|
||||||
|
// 静态变量:缓存单例实例(key为配置唯一标识,value为实例)
|
||||||
|
private static instanceMap: Map<string, nacosServiceManage> = new Map();
|
||||||
|
|
||||||
|
public nacosClient!: NacosNamingClient;
|
||||||
|
public configClient!: NacosConfigClient;
|
||||||
|
public initOptions!: RegisterOptions;
|
||||||
|
|
||||||
|
|
||||||
|
private constructor(option: RegisterOptions) {
|
||||||
|
this.initOptions = option;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 静态 create 方法:单例模式入口,同一配置只返回一个实例
|
||||||
|
* @param option 初始化配置
|
||||||
|
* @returns nacosServiceManage 实例(单例)
|
||||||
|
*/
|
||||||
|
public static async create(option: RegisterOptions): Promise<nacosServiceManage> {
|
||||||
|
const configKey = this.getConfigKey(option);
|
||||||
|
|
||||||
|
if (this.instanceMap.has(configKey)) {
|
||||||
|
logger.log(`[nacos-federation] 已存在 ${option.serviceName} 的Nacos实例,直接复用`);
|
||||||
|
return this.instanceMap.get(configKey)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
const instance = new nacosServiceManage(option);
|
||||||
|
// await instance.initClient();
|
||||||
|
await instance.initConfig();
|
||||||
|
|
||||||
|
this.instanceMap.set(configKey, instance);
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 服务注册
|
||||||
|
public async initClient() {
|
||||||
|
this.nacosClient = new NacosNamingClient({
|
||||||
|
logger,
|
||||||
|
serverList: this.initOptions.serverList,
|
||||||
|
namespace: this.initOptions.namespace || 'public',
|
||||||
|
});
|
||||||
|
await this.nacosClient.ready();
|
||||||
|
this.reg()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成配置唯一标识:基于 serverList + namespace + serviceName(确保不同配置对应不同实例)
|
||||||
|
*/
|
||||||
|
private static getConfigKey(option: RegisterOptions): string {
|
||||||
|
const namespace = option.namespace || 'public';
|
||||||
|
return `${option.serverList}-${namespace}-${option.serviceName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅配置变化
|
||||||
|
* @param dataId
|
||||||
|
* @param group
|
||||||
|
*/
|
||||||
|
async subscribe(dataId: string, group: string = "DEFAULT_GROUP") {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
this.configClient.subscribe({ dataId, group }, (content: any) => {
|
||||||
|
resolve(content)
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取配置
|
||||||
|
* @param dataId
|
||||||
|
* @param group
|
||||||
|
*/
|
||||||
|
async getConfig(dataId: string, group: string = "DEFAULT_GROUP") {
|
||||||
|
const res = await this.configClient.getConfig(dataId, group)
|
||||||
|
console.log()
|
||||||
|
console.log(`${chalk.blue('[nacos-cluster-federation] 配置发现:')}\n ${chalk.green('➜')} ${chalk.green(dataId)} 的配置为: ${chalk.green(res)}`)
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从Nacos发现服务地址
|
||||||
|
* @param serviceName 发现选项
|
||||||
|
* @returns 服务地址(如 'http://192.168.1.101:1301')
|
||||||
|
*/
|
||||||
|
public async find(serviceName: string) {
|
||||||
|
const instances = await this.nacosClient.getAllInstances(serviceName);
|
||||||
|
// console.log("instances: ", instances);
|
||||||
|
if (instances.length === 0) {
|
||||||
|
throw new Error(`[nacos-federation] 未发现服务 ${serviceName} 的实例`);
|
||||||
|
}
|
||||||
|
return formatServiceUrl(instances[0].ip, instances[0].port);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化配置服务
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private async initConfig() {
|
||||||
|
this.configClient = new NacosConfigClient({
|
||||||
|
serverAddr: this.initOptions.serverList,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册服务到Nacos并返回客户端实例
|
||||||
|
*/
|
||||||
|
private async reg() {
|
||||||
|
const { serviceName, port } = this.initOptions;
|
||||||
|
const ip = getLocalIP();
|
||||||
|
// @ts-ignore
|
||||||
|
await this.nacosClient.registerInstance(serviceName, { port, ip })
|
||||||
|
logger.log()
|
||||||
|
logger.log(chalk.blue("[nacos-cluster-federation] 服务注册:"))
|
||||||
|
logger.log(` ${chalk.green('➜')} ${chalk.green(serviceName)} (${formatServiceUrl(ip, port)}) 已注册到配置中心!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动销毁实例(如服务下线时)
|
||||||
|
*/
|
||||||
|
public async destroy() {
|
||||||
|
const configKey = nacosServiceManage.getConfigKey(this.initOptions);
|
||||||
|
// 注销 Nacos 命名客户端(如已初始化)
|
||||||
|
try {
|
||||||
|
if (this.nacosClient) {
|
||||||
|
const { serviceName, port } = this.initOptions;
|
||||||
|
const ip = getLocalIP();
|
||||||
|
// @ts-ignore
|
||||||
|
await this.nacosClient.deregisterInstance(serviceName, { ip, port });
|
||||||
|
// @ts-ignore
|
||||||
|
if (typeof this.nacosClient.close === 'function') {
|
||||||
|
// @ts-ignore
|
||||||
|
await this.nacosClient.close();
|
||||||
|
}
|
||||||
|
logger.log(`[nacos-federation] 服务 ${serviceName} 已从Nacos注销`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('[nacos-federation] deregister warn:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭配置客户端(如已初始化)
|
||||||
|
try {
|
||||||
|
if (this.configClient && typeof (this.configClient as any).close === 'function') {
|
||||||
|
await (this.configClient as any).close();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('[nacos-federation] configClient close warn:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从缓存中移除实例
|
||||||
|
nacosServiceManage.instanceMap.delete(configKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 销毁所有实例:用于构建结束/进程退出时清理 */
|
||||||
|
public static async destroyAll(): Promise<void> {
|
||||||
|
const tasks: Promise<any>[] = [];
|
||||||
|
for (const [, instance] of nacosServiceManage.instanceMap) {
|
||||||
|
tasks.push(instance.destroy().catch(err => logger.warn('[nacos-federation] destroyAll warn:', err)));
|
||||||
|
}
|
||||||
|
await Promise.allSettled(tasks);
|
||||||
|
nacosServiceManage.instanceMap.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
52
front-end/vite-plugin-nacos/src/utils.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { networkInterfaces } from "os";
|
||||||
|
|
||||||
|
|
||||||
|
class Utils {
|
||||||
|
/**
|
||||||
|
* 获取本机局域网IPv4地址
|
||||||
|
* @returns 局域网IP地址(如 '192.168.1.100')
|
||||||
|
*/
|
||||||
|
static getLocalIP(): string {
|
||||||
|
const interfaces = networkInterfaces();
|
||||||
|
for (const devName in interfaces) {
|
||||||
|
const iface = interfaces[devName];
|
||||||
|
if (!iface) continue;
|
||||||
|
|
||||||
|
for (const alias of iface) {
|
||||||
|
if (
|
||||||
|
alias.family === 'IPv4' &&
|
||||||
|
alias.address !== '127.0.0.1' &&
|
||||||
|
!alias.internal
|
||||||
|
) {
|
||||||
|
return alias.address;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '127.0.0.1';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化服务地址
|
||||||
|
* @param ip IP地址
|
||||||
|
* @param port 端口号
|
||||||
|
* @returns 完整服务地址(如 'http://192.168.1.100:1300')
|
||||||
|
*/
|
||||||
|
static formatServiceUrl(ip: string, port: number): string {
|
||||||
|
return `http://${ ip }:${ port }`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VITE_DEPEND_MODEL_NAME_LIST 配置 格式化成数组
|
||||||
|
* app-shared,app-test
|
||||||
|
* @param arg ["app-shared", "app-test"]
|
||||||
|
*/
|
||||||
|
static dependModel(arg: string): string[] {
|
||||||
|
return arg.split(',').map(name => name.trim()).filter(Boolean) || [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const {
|
||||||
|
getLocalIP, formatServiceUrl,
|
||||||
|
dependModel
|
||||||
|
} = Utils
|
||||||
104
front-end/vite-plugin-nacos/src/vite-plugin-nacos.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import { loadEnv, Plugin, UserConfig } from 'vite';
|
||||||
|
import { getLocalIP } from './utils';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
|
||||||
|
const logger = console;
|
||||||
|
|
||||||
|
export interface RegisterOptions {
|
||||||
|
serverList: string;
|
||||||
|
/** 服务名称(如 'main-app') */
|
||||||
|
serviceName: string;
|
||||||
|
/** 服务端口号 */
|
||||||
|
port: number;
|
||||||
|
/** 命名空间(用于环境隔离,默认 'public') */
|
||||||
|
namespace?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reg(options?: Partial<RegisterOptions>): Plugin {
|
||||||
|
return {
|
||||||
|
name: 'vite-plugin-nacos',
|
||||||
|
async config(config: UserConfig, { mode }) {
|
||||||
|
// 修改base,增加资源来源地址,修复资源错误加载的问题
|
||||||
|
const env = loadEnv(mode, process.cwd());
|
||||||
|
let baseUrl = ""
|
||||||
|
|
||||||
|
// 主模块 main-app 不加 base 前缀,其他模块都加,
|
||||||
|
// 也是为了命中 traefik 路由代理
|
||||||
|
if (env.VITE_SERVER_NAME != "main-app") {
|
||||||
|
baseUrl = `${env.VITE_SERVER_NAME}/`
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
server: {
|
||||||
|
allowedHosts: [
|
||||||
|
'.ddd.com',
|
||||||
|
],
|
||||||
|
port: Number(env.VITE_SERVER_PORT),
|
||||||
|
host: '::',
|
||||||
|
strictPort: true,
|
||||||
|
cors: true
|
||||||
|
},
|
||||||
|
// base: `${env.VITE_PREFIX}${getLocalIP()}:${env.VITE_SERVER_PORT}/${baseUrl}`
|
||||||
|
base: `/${baseUrl}`
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async configResolved(config) {
|
||||||
|
const env = config.env;
|
||||||
|
},
|
||||||
|
async closeBundle() {
|
||||||
|
// // 服务关闭时注销Nacos实例
|
||||||
|
// if (nacosInstance) {
|
||||||
|
// await nacosInstance.destroy();
|
||||||
|
// }
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Logs(env: Record<string, any>) {
|
||||||
|
const moduleName = []
|
||||||
|
for (const key in env) {
|
||||||
|
if (key.includes("VITE_MODEL_")) {
|
||||||
|
moduleName.push(env[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
moduleName.length == 0 ? moduleName.push('无') : null
|
||||||
|
const webUI = env.VITE_NACOS_ADDRESS.split(":")[0]
|
||||||
|
console.log()
|
||||||
|
|
||||||
|
console.log(dedentTemplateString(`
|
||||||
|
${chalk.blue.bold('微服务工具:')}
|
||||||
|
${chalk.green('➜')} Naco 配置中心: ${env.VITE_PREFIX}${env.VITE_NACOS_ADDRESS}
|
||||||
|
${chalk.green('➜')} WebUi: ${env.VITE_PREFIX}${webUI}:8085
|
||||||
|
${chalk.green('➜')} Prometheus 监控: ${env.VITE_PREFIX}${webUI}:9090
|
||||||
|
${chalk.green('➜')} Grafana 面板: ${env.VITE_PREFIX}${webUI}:3000
|
||||||
|
${chalk.green('➜')} Traefik 网关: ${env.VITE_PREFIX}${env.VITE_TRAEFIK_ADDRESS}
|
||||||
|
${chalk.green('➜')} WebUi: ${env.VITE_PREFIX}${env.VITE_TRAEFIK_ADDRESS}:8880
|
||||||
|
|
||||||
|
${chalk.blue.bold('微前端信息:')}
|
||||||
|
${chalk.green('➜')} 主模块:${chalk.green(env.VITE_SERVER_NAME)}
|
||||||
|
|
||||||
|
${chalk.blue.bold('调用模块:')}
|
||||||
|
${chalk.green('➜')} [ ${chalk.yellow(moduleName)} ]
|
||||||
|
`))
|
||||||
|
console.log()
|
||||||
|
}
|
||||||
|
|
||||||
|
function dedentTemplateString(str: string) {
|
||||||
|
// 分割成行
|
||||||
|
const lines = str.split('\n');
|
||||||
|
|
||||||
|
// 移除首尾空行
|
||||||
|
if (lines[0].trim() === '') lines.shift();
|
||||||
|
if (lines[lines.length - 1].trim() === '') lines.pop();
|
||||||
|
|
||||||
|
// 找到最小缩进量
|
||||||
|
const minIndent = lines.reduce((min, line) => {
|
||||||
|
if (line.trim() === '') return min; // 跳过空行
|
||||||
|
// @ts-ignore
|
||||||
|
const indent = line.match(/^\s*/)[0].length;
|
||||||
|
return indent < min ? indent : min;
|
||||||
|
}, Infinity);
|
||||||
|
|
||||||
|
// 移除统一缩进
|
||||||
|
return lines.map(line => line.slice(minIndent)).join('\n');
|
||||||
|
}
|
||||||
23
front-end/vite-plugin-nacos/tsconfig.json
Normal file
@@ -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
43
package.json
@@ -1,35 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "vue-project",
|
"name": "front-end",
|
||||||
"version": "0.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"description": "",
|
||||||
"type": "module",
|
"keywords": [],
|
||||||
"engines": {
|
"author": "",
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
"license": "ISC",
|
||||||
},
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"update": "pnpm -r update --latest",
|
||||||
"build": "run-p type-check \"build-only {@}\" --",
|
"init": "pnpm install",
|
||||||
"preview": "vite preview",
|
"all": "concurrently \"pnpm run build\" \"pnpm run dev:main\"",
|
||||||
"build-only": "vite build",
|
"build": "pnpm --parallel --filter app-product --filter app-shared dep",
|
||||||
"type-check": "vue-tsc --build"
|
"dev:main": "pnpm --filter main-app dev",
|
||||||
},
|
"build:main": "pnpm --filter main-app dep"
|
||||||
"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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
7
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
packages:
|
||||||
|
- 'front-end/*'
|
||||||
|
- 'docker/traefik'
|
||||||
|
- 'back-end'
|
||||||
|
# 排除不需要的目录(可选)
|
||||||
|
- '!** /node_modules'
|
||||||
|
- '!** /dist'
|
||||||
@@ -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>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
/* color palette from <https://github.com/vuejs/theme> */
|
|
||||||
:root {
|
|
||||||
--vt-c-white: #ffffff;
|
|
||||||
--vt-c-white-soft: #f8f8f8;
|
|
||||||
--vt-c-white-mute: #f2f2f2;
|
|
||||||
|
|
||||||
--vt-c-black: #181818;
|
|
||||||
--vt-c-black-soft: #222222;
|
|
||||||
--vt-c-black-mute: #282828;
|
|
||||||
|
|
||||||
--vt-c-indigo: #2c3e50;
|
|
||||||
|
|
||||||
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
|
||||||
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
|
||||||
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
|
||||||
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
|
||||||
|
|
||||||
--vt-c-text-light-1: var(--vt-c-indigo);
|
|
||||||
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
|
||||||
--vt-c-text-dark-1: var(--vt-c-white);
|
|
||||||
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* semantic color variables for this project */
|
|
||||||
:root {
|
|
||||||
--color-background: var(--vt-c-white);
|
|
||||||
--color-background-soft: var(--vt-c-white-soft);
|
|
||||||
--color-background-mute: var(--vt-c-white-mute);
|
|
||||||
|
|
||||||
--color-border: var(--vt-c-divider-light-2);
|
|
||||||
--color-border-hover: var(--vt-c-divider-light-1);
|
|
||||||
|
|
||||||
--color-heading: var(--vt-c-text-light-1);
|
|
||||||
--color-text: var(--vt-c-text-light-1);
|
|
||||||
|
|
||||||
--section-gap: 160px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--color-background: var(--vt-c-black);
|
|
||||||
--color-background-soft: var(--vt-c-black-soft);
|
|
||||||
--color-background-mute: var(--vt-c-black-mute);
|
|
||||||
|
|
||||||
--color-border: var(--vt-c-divider-dark-2);
|
|
||||||
--color-border-hover: var(--vt-c-divider-dark-1);
|
|
||||||
|
|
||||||
--color-heading: var(--vt-c-text-dark-1);
|
|
||||||
--color-text: var(--vt-c-text-dark-2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
*,
|
|
||||||
*::before,
|
|
||||||
*::after {
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin: 0;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
min-height: 100vh;
|
|
||||||
color: var(--color-text);
|
|
||||||
background: var(--color-background);
|
|
||||||
transition:
|
|
||||||
color 0.5s,
|
|
||||||
background-color 0.5s;
|
|
||||||
line-height: 1.6;
|
|
||||||
font-family:
|
|
||||||
Inter,
|
|
||||||
-apple-system,
|
|
||||||
BlinkMacSystemFont,
|
|
||||||
'Segoe UI',
|
|
||||||
Roboto,
|
|
||||||
Oxygen,
|
|
||||||
Ubuntu,
|
|
||||||
Cantarell,
|
|
||||||
'Fira Sans',
|
|
||||||
'Droid Sans',
|
|
||||||
'Helvetica Neue',
|
|
||||||
sans-serif;
|
|
||||||
font-size: 15px;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 276 B |
@@ -1,35 +0,0 @@
|
|||||||
@import './base.css';
|
|
||||||
|
|
||||||
#app {
|
|
||||||
max-width: 1280px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 2rem;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
a,
|
|
||||||
.green {
|
|
||||||
text-decoration: none;
|
|
||||||
color: hsla(160, 100%, 37%, 1);
|
|
||||||
transition: 0.4s;
|
|
||||||
padding: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (hover: hover) {
|
|
||||||
a:hover {
|
|
||||||
background-color: hsla(160, 100%, 37%, 0.2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
body {
|
|
||||||
display: flex;
|
|
||||||
place-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#app {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
padding: 0 2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,54 +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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -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')
|
|
||||||
|
|
||||||
@@ -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
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { defineComponent } from 'vue'
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "About",
|
|
||||||
setup() {
|
|
||||||
return () => (
|
|
||||||
<div class="about">
|
|
||||||
<h1>我的</h1>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import { defineComponent, reactive, onMounted } from 'vue'
|
|
||||||
import Header from '@/components/Header'
|
|
||||||
import { HttpConfig } from '@shared/config/index'
|
|
||||||
import { UrlCheckUtils } from '@shared/utils'
|
|
||||||
import { useCounterStore } from '@/stores/counter'
|
|
||||||
import { storeToRefs } from 'pinia'
|
|
||||||
|
|
||||||
interface stateType {
|
|
||||||
list: ListItemType[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ListItemType {
|
|
||||||
id: number
|
|
||||||
title: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "Home",
|
|
||||||
setup() {
|
|
||||||
|
|
||||||
const counterStore = useCounterStore()
|
|
||||||
const { increment } = counterStore
|
|
||||||
const { count, doubleCount } = storeToRefs(counterStore)
|
|
||||||
|
|
||||||
const state = reactive<stateType>({
|
|
||||||
list: [
|
|
||||||
{ id: 1, title: '新闻1' },
|
|
||||||
{ id: 2, title: '新闻2' },
|
|
||||||
{ id: 3, title: '新闻3' },
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
// console.log("onMounted: ", HttpConfig.DevBaseUrl);
|
|
||||||
console.log("check: ", UrlCheckUtils.check())
|
|
||||||
})
|
|
||||||
|
|
||||||
// 定义处理子组件事件的函数
|
|
||||||
const handleChildEvent = (message: string) => {
|
|
||||||
console.log('收到子组件事件:', message)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return () => (
|
|
||||||
<div class="home">
|
|
||||||
<Header text={state.list} onSendMessage={handleChildEvent}>
|
|
||||||
{{
|
|
||||||
scoped: (item: ListItemType) => <li key={item.id}>{item.title}</li>,
|
|
||||||
header: () => <div class="header">父到子 具名 插槽</div>,
|
|
||||||
default: () => <div class="default">父到子 default 插槽</div>
|
|
||||||
}}
|
|
||||||
</Header>
|
|
||||||
<h1>首页</h1>
|
|
||||||
<div class="count" onClick={increment}>点我+1:{count.value}</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { defineComponent } from 'vue'
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
setup() {
|
|
||||||
return () => (
|
|
||||||
<div class="404">
|
|
||||||
<h1>404</h1>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -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 = ""
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export default class ThemeConfig {
|
|
||||||
public static primaryColor: string = '#1890ff'; // 主色调
|
|
||||||
public static layoutType: 'side' | 'top' = 'side'; // 侧边栏布局
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
import HttpConfig from './HttpConfig';
|
|
||||||
import ThemeConfig from './ThemeConfig';
|
|
||||||
|
|
||||||
export { HttpConfig, ThemeConfig };
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
import axios, { type AxiosInstance, AxiosError, type AxiosRequestConfig, type InternalAxiosRequestConfig } from "axios";
|
|
||||||
import { UrlCheckUtils } from '@shared/utils/index'
|
|
||||||
|
|
||||||
|
|
||||||
class AxiosInfra {
|
|
||||||
public instance: AxiosInstance;
|
|
||||||
|
|
||||||
public constructor() {
|
|
||||||
this.instance = axios.create({
|
|
||||||
baseURL: UrlCheckUtils.check(),
|
|
||||||
timeout: 10000,
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.ResponseInterceptor();
|
|
||||||
this.RequestInterceptor();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async get(params: AxiosRequestConfig): Promise<any> {
|
|
||||||
return await this.instance.get(<string>params.url, {
|
|
||||||
params: params.data,
|
|
||||||
headers: Object.assign({ Authorization: localStorage.getItem("token") }, params.headers)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async post(params: AxiosRequestConfig): Promise<any> {
|
|
||||||
return await this.instance.post(<string>params.url, params.data, {
|
|
||||||
headers: Object.assign({ Authorization: localStorage.getItem("token") }, params.headers)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Put 请求
|
|
||||||
* 请求参数请参考接口:RequestParams
|
|
||||||
* @param params
|
|
||||||
*/
|
|
||||||
public async put(params: AxiosRequestConfig): Promise<any> {
|
|
||||||
return await this.instance.put(<string>params.url, params.data)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* delete 请求
|
|
||||||
* 请求参数请参考接口:RequestParams
|
|
||||||
* @param params
|
|
||||||
*/
|
|
||||||
public async delete(params: AxiosRequestConfig): Promise<any> {
|
|
||||||
return await this.instance.delete(<string>params.url, { params: params.data })
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 响应拦截器
|
|
||||||
* @constructor
|
|
||||||
*/
|
|
||||||
public async ResponseInterceptor(): Promise<any> {
|
|
||||||
this.instance.interceptors.response.use(
|
|
||||||
(response: any) => {
|
|
||||||
return response.data.result;
|
|
||||||
},
|
|
||||||
(error: AxiosError) => {
|
|
||||||
console.log(`
|
|
||||||
❌ 请求错误 ❌
|
|
||||||
1、基地址: ${error.config?.baseURL}
|
|
||||||
2、短地址: ${error.config?.url}
|
|
||||||
3、请求 方式: ${error.config?.method}
|
|
||||||
4、请求 参数: ${JSON.stringify(error.config?.params)}
|
|
||||||
5、请求 头 : ${JSON.stringify(error.config?.headers)}
|
|
||||||
|
|
||||||
------------------------------------------------------------
|
|
||||||
|
|
||||||
6、错误信息为: ${error.message}
|
|
||||||
7、错误代码为: ${error.stack}
|
|
||||||
`);
|
|
||||||
return Promise.reject(error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加请求拦截器
|
|
||||||
* @constructor
|
|
||||||
*/
|
|
||||||
public async RequestInterceptor(): Promise<any> {
|
|
||||||
this.instance.interceptors.request.use((config: any) => {
|
|
||||||
return config
|
|
||||||
}, function (error: AxiosError) {
|
|
||||||
return Promise.reject(error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export default new AxiosInfra();
|
|
||||||