完成 微前端拆分

This commit is contained in:
编码猿
2025-09-18 01:50:50 +08:00
parent 7bc20d517b
commit ca24ab9922
27 changed files with 59 additions and 384 deletions

View File

@@ -1,3 +1,5 @@
## DDD领域驱动设计的微服务前端案例
> 持续开发中及时拉取代码文档还不够完善先凑合看目前只是demo代码还会进行大规模封装细化和改造

View File

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

View File

@@ -0,0 +1,3 @@
declare module 'mainApp/productService' {
export const product: any;
}

View File

@@ -7,10 +7,10 @@
"node": "^20.19.0 || >=22.12.0"
},
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"dev": "vite --port 5000 --strictPort",
"serve": "vite preview --port 5000 --strictPort",
"build": "vite build",
"all": "npm run build && npm run serve",
"type-check": "vue-tsc --build"
},
"dependencies": {

View File

@@ -1,7 +1,9 @@
import { ProductApplicationService } from "../../domains/product/application/services/ProductApplicationService";
import { HttpProductRepository } from "../../domains/product/infr/api/HttpProductRepository";
import { ProductRepository } from "../../domains/product/ports/repositories/ProductRepository";
// 从商品联邦模块导入领域层资源
import {
ProductApplicationService,
HttpProductRepository,
ProductRepository
} from 'productModule/ProductDomain';
/**
* 商品领域的依赖注入配置
@@ -21,7 +23,7 @@ import { ProductRepository } from "../../domains/product/ports/repositories/Prod
* productModule.bindProductRepository = () => new MockProductRepository();
* 这样测试就可以脱离真实 API使用预设的模拟数据。
*/
export const productModule = {
export const product = {
// 绑定 仓储抽象接口 到 HTTP具体实现
bindProductRepository: (): ProductRepository => {
@@ -32,7 +34,7 @@ export const productModule = {
// 创建应用服务实例
createProductApplicationService: (): ProductApplicationService => {
return new ProductApplicationService(
productModule.bindProductRepository()
product.bindProductRepository()
);
}
}

View File

@@ -1,5 +1,5 @@
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import { productRouter } from '@features/product/router'
import { productRouter } from 'productModule/ProductRouter'
const baseRouter: Array<RouteRecordRaw> = [
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import { HttpConfig } from '@shared/config/index'
import { HttpConfig } from '@shared/config/HttpConfig'
export default class UrlCheckUtils {

View File

@@ -4,6 +4,7 @@ import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import vueDevTools from 'vite-plugin-vue-devtools'
import federation from '@originjs/vite-plugin-federation';
// https://vite.dev/config/
export default defineConfig({
@@ -11,7 +12,33 @@ export default defineConfig({
vue(),
vueJsx(),
vueDevTools(),
federation({
name: 'mainApp', // 主应用模块名(联邦模块需通过此名引用)
remotes: { // 配置远程联邦模块地址(开发/生产需对应)
productModule: 'http://localhost:5001/assets/remoteEntry.js'
},
exposes: {
"./shared": "./src/shared/index.ts",
"./productService": "./src/app/di/productModule.ts",
},
shared: {
vue: { requiredVersion: '^3.5.18' },
axios: { requiredVersion: '^1.12.2' },
'vue-router': { requiredVersion: '^4.0.6' },
pinia: { requiredVersion: '^3.0.3' }
}
})
],
build: {
target: 'esnext',
minify: false,
cssCodeSplit: true,
rollupOptions: {
output: {
minifyInternalExports: false
}
}
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src/app/', import.meta.url)),