Files
DDDMicroFrontend/README.md
2025-09-19 02:49:19 +08:00

9.7 KiB
Raw Blame History

DDD领域驱动设计的微服务前端案例

持续开发中及时拉取代码文档还不够完善先凑合看目前只是demo代码还会进行大规模封装细化和改造

搭建的一套 基于DDD领域驱动设计的微服务前端 项目脚手架,适用于大型企业,开发大型复杂且拓展灵活的前端应用的架构方案。

代码编写建议使用 OOP面向对象 + TypeScript + JSX 进行开发,让项目更适合开发和维护,获得更好更极致的编码体验。

本案例中 [主应用] 采取的就是OOP风格当然如果你不熟悉上面的写法那么可以在 【其他模块】中使用Vue2Vue3都行但仍然需要遵守DDD的规范进行开发

技术栈:

文件夹结构

根目录:

.
├── back-end          # 【后端】基于Deno写的后端 测试接口,仅供测试
├── doc               # 一些markdown笔记
├── front-end         # 前端 微服务 项目
│   ├── app-product   # 独立的【商品】领域模块
│   ├── app-shared    # 独立的 共享资源 模块
│   ├── ....          # 实际项目还会有更多其他的领域模块
│   └── main-app      # 主应用(入口应用)
└── README.md         # 帮助文档

main-app 目录:

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

代码风格

// 首页 页面
import { Autobind, Component, Link, Mut, VueComponent } from 'vue3-oop'
import Header from '@/components/Header'

interface ListItemType {
    id: number
    title: string
}

@Component()
export default class Home extends VueComponent {

    @Mut()
    count: number = 1

    @Mut()
    text: string = "Home props 传给 Header 的字符串"

    @Mut()
    newList: ListItemType[] = [
        { id: 1, title: '新闻1' },
        { id: 2, title: '新闻2' },
        { id: 3, title: '新闻3' },
    ]

    @Link() H1?: HTMLDivElement

    @Autobind()
    async addCount() {
        this.count++
        this.text = "22222"
        console.log(this.H1)
    }

    render() {
        return (
            <div class="home" ref="H1">
                <h1>首页</h1>
                <p onClick={ this.addCount }>count响应式变量(点我):{ this.count }</p>
                <Header
                    v-slots={ {
                        default: () => <div class="default">父到子 default 插槽</div>,
                        haveName: () => <div class="haveName">父到子 具名 插槽</div>,
                        item: (val) => <li key={ val.id }>{ val.title }</li>
                    } }
                    list={ this.newList }
                    text={ this.text }>
                </Header>
            </div>
        )
    }
}
// 头部组件
import { Component, ComponentProps, Hook, useProps, VueComponent } from 'vue3-oop'
import { JSX } from "vue/jsx-runtime";
import { VNodeChild } from "vue";

interface ListItemType {
    id: number
    title: string
}

interface HeaderProps {
    text?: string;
    list: ListItemType[]
    slots: {
        default: () => JSX.Element;
        haveName: () => JSX.Element;
        item: (val: ListItemType) => VNodeChild;
    }
}

@Component()
export default class Header extends VueComponent<HeaderProps> {

    // required 的 true or false 需要和 类型中 text的? 对应
    static defaultProps: ComponentProps<HeaderProps> = {
        text: { default: '默认数值', required: true },
        list: { default: () => [] }
    };

    @Hook('Mounted')
    mounted() {
        console.log("this: ", this);
        console.log('Header 组件的 mounted 生命周期...')
    }

    render() {
        const { text, list } = useProps<HeaderProps>();
        return (
            <div class="header">
                头部组件
                <p>{ text }</p>
                { this.context.slots.default?.() }
                { this.context.slots.haveName?.() }
                <ul>
                    {
                        list.map(
                            item =>
                                this.context.slots.item?.(item)
                        )
                    }
                </ul>
            </div>
        )
    }
}

开发任务

  • 基于DDD理念改造
  • 基于 vite-plugin-federation 拆分领域模块为联邦模块,实现微前端
  • 基于Vue3+OOP+JSX改造项目代码
  • 领域模块实现 docker 自动化构建部署上线
  • Deno集成GraphQL

问 & 答

问: 为什么vue项目要抛弃SFC写法而尝试jsx/tsx方式来编码

答:https://juejin.cn/post/6911175470255964174

问:不会 vue3 jsx 语法,怎么办?

答:https://juejin.cn/post/7141674726434439176

vue3 jsx defineComponent 语法,实际项目中 setup 代码太长,怎么办?

答:使用组合式函数 (Composables)

案例:

// composables/useUserData.js
import {ref, onMounted} from 'vue'
import api from '@/api'

export function useUserData(userId) {
    const user = ref(null)
    const loading = ref(false)
    const error = ref(null)

    const fetchUser = async () => {
        loading.value = true
        try {
            user.value = await api.getUser(userId)
        } catch (err) {
            error.value = err.message
        } finally {
            loading.value = false
        }
    }

    onMounted(fetchUser)

    return {
        user,
        loading,
        error,
        refetch: fetchUser
    }
}

主组件:

// UserProfile.jsx
import {defineComponent} from 'vue'
import {useUserData} from '@/composables/useUserData'

export default defineComponent({
    name: 'UserProfile',

    props: {
        userId: {
            type: String,
            required: true
        }
    },

    setup(props) {
        const {user, loading, error, refetch} = useUserData(props.userId)

        return () => (
            <div>
                {loading.value && <div>Loading...</div>}
                {error.value && <div>Error: {error.value}</div>}
                {user.value && (
                    <div>
                        <h2>{user.value.name}</h2>
                        <p>{user.value.email}</p>
                    </div>
                )}
            </div>
        )
    }
})