deotalandAi/openspec/changes/monorepo-architecture-design/design.md

13 KiB
Raw Permalink Blame History

Monorepo架构技术设计文档

架构概览

本设计文档详细描述了DeotalandAi项目从分散的独立项目向Monorepo架构迁移的技术实施方案。

技术选型分析

包管理器选择: pnpm vs Yarn vs npm

选择: pnpm

原因:

  • 磁盘空间优化: 硬链接和符号链接减少80%磁盘占用
  • 速度快: 并行安装比npm快2-3倍
  • 工作空间支持: 原生Monorepo支持配置简洁
  • 依赖隔离: 每个包独立node_modules避免依赖污染
  • 严格模式: 防止"吊死依赖"问题

对比分析:

pnpm:
  ✅ 安装速度快
  ✅ 磁盘占用少
  ✅ 工作空间原生支持
  ✅ 依赖严格模式
  ❌ 相对较新,生态还在完善

Yarn Workspaces:
  ✅ 成熟稳定
  ✅ 社区支持好
  ❌ 安装速度中等
  ❌ 磁盘占用大

npm Workspaces:
  ✅ Node.js原生支持
  ❌ 功能相对简单
  ❌ 性能不如pnpm

构建工具选择: Turborepo vs Lerna vs Nx

选择: Turborepo

原因:

  • 任务管道: 声明式任务依赖图,易于理解
  • 缓存机制: 本地和远程缓存,显著提升构建速度
  • 增量构建: 只重新构建变更的包
  • 简单配置: JSON配置学习成本低
  • Vercel支持: 来自Vercel现代化工具链

任务管道设计:

{
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", "build/**"]
    },
    "test": {
      "dependsOn": ["build"],
      "outputs": []
    },
    "lint": {
      "outputs": []
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

项目结构设计

目录结构详解

DeotalandAi/
├── package.json                 # 根级别项目配置
├── pnpm-workspace.yaml         # pnpm工作空间定义
├── turbo.json                  # Turborepo任务配置
├── .npmrc                      # npm配置使用pnpm时
├── .eslintrc.base.json         # 基础ESLint配置
├── .prettierrc                 # Prettier配置
├── .prettierrc                 # Prettier配置
├── .gitignore                  # Git忽略规则
├── README.md                   # 项目总体说明
├── CHANGELOG.md                # 版本变更记录
├── apps/                       # 应用程序
│   ├── frontend/              # 主前端应用
│   │   ├── package.json
│   │   ├── vite.config.js
│   │   ├── index.html
│   │   └── src/
│   │       ├── main.ts
│   │       ├── App.vue
│   │       ├── components/
│   │       ├── views/
│   │       ├── stores/
│   │       └── router/
│   └── designer/              # 设计器应用
│       ├── package.json
│   │   ├── vite.config.js
│   │   ├── index.html
│   │   └── src/
├── packages/                   # 共享包
│   ├── ui/                    # UI组件库
│   │   ├── package.json
│   │   ├── vite.config.js
│   │   ├── src/
│   │   │   ├── index.js       # 统一导出
│   │   │   ├── components/    # Vue组件
│   │   │   ├── styles/        # 样式文件
│   │   │   └── composables/   # 组合式函数
│   │   └── stories/           # Storybook文档
│   ├── utils/                 # 工具库
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── index.ts
│   │   │   ├── api/           # API相关工具
│   │   │   ├── format/        # 格式化工具
│   │   │   ├── validate/      # 验证工具
│   │   │   └── storage/       # 存储工具
│   │   └── tests/
│   └── config/                # 配置文件包
│       ├── package.json
│       ├── src/
│       │   ├── index.js
│       │   ├── vite.js        # Vite配置
│       │   └── eslint.js      # ESLint配置
├── scripts/                    # 构建和部署脚本
│   ├── build.js
│   ├── dev.js
│   ├── deploy.js
│   └── utils.js
└── docs/                      # 项目文档
    ├── architecture.md
    ├── contributing.md
    └── guides/

工作空间配置详解

pnpm-workspace.yaml

packages:
  - 'apps/*'
  - 'packages/*'
  - '!apps/*/dist/**'
  - '!apps/*/.vite/**'

package.json (根级别)

{
  "name": "deotalandai-monorepo",
  "private": true,
  "version": "1.0.0",
  "description": "DeotalandAI Monorepo with Vue3, Element Plus, and shared components",
  "scripts": {
    "dev": "turbo run dev",
    "build": "turbo run build",
    "lint": "turbo run lint",
    "test": "turbo run test",
    "clean": "turbo run clean && rimraf node_modules",
    "type-check": "turbo run type-check",
    "storybook": "turbo run storybook",
    "deploy": "turbo run deploy"
  },
  "devDependencies": {
    "turbo": "^1.10.0",
    "concurrently": "^8.2.0",
    "rimraf": "^5.0.0",
    "eslint": "^8.45.0",
    "prettier": "^3.0.0",
    "@vitejs/plugin-vue": "^6.0.1",
    "vite": "^7.2.2"
  },
  "engines": {
    "node": ">=18.0.0",
    "pnpm": ">=8.0.0"
  }
}

共享组件设计

UI组件库架构

组件分类体系

packages/ui/src/components/
├── base/                      # 基础组件
│   ├── Button/
│   ├── Input/
│   ├── Card/
│   ├── Modal/
│   └── Loading/
├── layout/                    # 布局组件
│   ├── Header/
│   ├── Sidebar/
│   ├── Footer/
│   └── Container/
├── business/                  # 业务组件
│   ├── AgentCard/
│   ├── OrderCard/
│   ├── Gallery/
│   └── PurchaseModal/
└── form/                      # 表单组件
    ├── Form/
    ├── FormItem/
    ├── Upload/
    └── Select/

主题系统设计

// packages/ui/src/styles/theme.ts
export const theme = {
  colors: {
    primary: '#6B46C1',
    secondary: '#8B5CF6',
    success: '#10B981',
    warning: '#F59E0B',
    error: '#EF4444',
    info: '#3B82F6'
  },
  spacing: {
    xs: '4px',
    sm: '8px',
    md: '16px',
    lg: '24px',
    xl: '32px'
  },
  borderRadius: {
    sm: '4px',
    md: '8px',
    lg: '12px',
    full: '9999px'
  },
  typography: {
    fontFamily: {
      sans: ['Inter', 'system-ui', 'sans-serif'],
      mono: ['JetBrains Mono', 'monospace']
    },
    fontSize: {
      xs: '12px',
      sm: '14px',
      md: '16px',
      lg: '18px',
      xl: '20px',
      '2xl': '24px'
    }
  }
}

组件使用示例

<!-- apps/frontend/src/components/AgentCard.vue -->
<template>
  <UiCard class="agent-card">
    <template #header>
      <UiCardHeader>
        <UiAgentCard :agent="agent" />
      </UiCardHeader>
    </template>
    
    <UiCardContent>
      <p>{{ agent.description }}</p>
    </UiCardContent>
    
    <template #footer>
      <UiButton @click="onSelect">
        {{ $t('common.select') }}
      </UiButton>
    </template>
  </UiCard>
</template>

<script setup lang="ts">
import { UiCard, UiButton, UiAgentCard } from '@deotalandai/ui'
import type { Agent } from '@deotalandai/types'

interface Props {
  agent: Agent
}

const props = defineProps<Props>()
const emit = defineEmits<{
  select: [agent: Agent]
}>()

const onSelect = () => {
  emit('select', props.agent)
}
</script>

工具库设计

API工具模块

// packages/utils/src/api/request.js
import axios from 'axios'

export class ApiClient {
  constructor(config) {
    this.instance = axios.create(config)
    this.setupInterceptors()
  }

  setupInterceptors() {
    this.instance.interceptors.request.use(
      (config) => {
        // 添加认证token
        const token = localStorage.getItem('auth_token')
        if (token) {
          config.headers.Authorization = `Bearer ${token}`
        }
        return config
      },
      (error) => Promise.reject(error)
    )

    this.instance.interceptors.response.use(
      (response) => response.data,
      (error) => {
        // 统一错误处理
        return Promise.reject(this.handleError(error))
      }
    )
  }

  handleError(error) {
    if (error.response) {
      const { status, data } = error.response
      return {
        status,
        message: data.message || '请求失败',
        code: data.code
      }
    }
    return {
      status: 0,
      message: error.message || '网络错误',
      code: -1
    }
  }

  async get(url, params) {
    return this.instance.get(url, { params })
  }

  async post(url, data) {
    return this.instance.post(url, data)
  }

  async put(url, data) {
    return this.instance.put(url, data)
  }

  async delete(url) {
    return this.instance.delete(url)
  }
}

export const createApiClient = (baseURL) => {
  return new ApiClient({ baseURL })
}

国际化工具

// packages/utils/src/i18n/index.ts
import { createI18n } from 'vue-i18n'
import type { App } from 'vue'

export const setupI18n = (app: App, locale: string, messages: Record<string, any>) => {
  const i18n = createI18n({
    legacy: false,
    locale,
    fallbackLocale: 'en',
    messages
  })

  app.use(i18n)
  return i18n
}

export const formatDate = (date: Date, locale: string = 'zh-CN') => {
  return new Intl.DateTimeFormat(locale, {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  }).format(date)
}

export const formatCurrency = (amount: number, currency: string = 'CNY', locale: string = 'zh-CN') => {
  return new Intl.NumberFormat(locale, {
    style: 'currency',
    currency
  }).format(amount)
}

类型定义设计

API类型定义

// packages/types/src/api.ts
export interface ApiResponse<T = any> {
  code: number
  message: string
  data: T
  success: boolean
}

export interface PaginatedResponse<T> extends ApiResponse<T[]> {
  pagination: {
    page: number
    pageSize: number
    total: number
    totalPages: number
  }
}

export interface Agent {
  id: string
  name: string
  description: string
  avatar: string
  category: string
  tags: string[]
  created_at: string
  updatedAt: string
}

export interface Order {
  id: string
  agentId: string
  userId: string
  status: 'pending' | 'processing' | 'completed' | 'failed'
  amount: number
  currency: string
  created_at: string
  updatedAt: string
}

组件Props类型

// packages/types/src/components.ts
import type { Agent, Order } from './api'

export interface ButtonProps {
  variant?: 'primary' | 'secondary' | 'outline' | 'ghost'
  size?: 'sm' | 'md' | 'lg'
  disabled?: boolean
  loading?: boolean
  block?: boolean
}

export interface CardProps {
  shadow?: 'none' | 'sm' | 'md' | 'lg'
  border?: boolean
  padding?: 'none' | 'sm' | 'md' | 'lg'
}

export interface AgentCardProps {
  agent: Agent
  selectable?: boolean
  showActions?: boolean
}

export interface ModalProps {
  visible: boolean
  title?: string
  width?: string | number
  confirmText?: string
  cancelText?: string
  maskClosable?: boolean
}

开发工具配置

VS Code工作区配置

// .vscode/settings.json
{
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  "typescript.preferences.importModuleSpecifier": "relative",
  "emmet.includeLanguages": {
    "vue": "html"
  },
  "files.associations": {
    "*.vue": "vue"
  },
  "search.exclude": {
    "**/node_modules": true,
    "**/dist": true,
    "**/build": true,
    "**/.turbo": true
  }
}

调试配置

// .vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Frontend Dev",
      "type": "node",
      "request": "launch",
      "program": "${workspaceFolder}/node_modules/.bin/vite",
      "args": ["--config", "apps/frontend/vite.config.ts"],
      "cwd": "${workspaceFolder}/apps/frontend",
      "console": "integratedTerminal"
    }
  ]
}

性能优化策略

构建优化

  1. 依赖预构建: 使用vite的依赖预构建
  2. 代码分割: 每个应用独立打包
  3. Tree Shaking: 消除未使用的代码
  4. 缓存策略: 利用Turborepo缓存

开发优化

  1. 热模块替换: 跨包的热更新
  2. 增量构建: 只构建变更的包
  3. 并行开发: 同时启动多个应用
  4. 类型检查: 增量TypeScript检查

部署优化

  1. CDN分发: 静态资源CDN部署
  2. 压缩优化: Gzip/Brotli压缩
  3. 缓存头: 合理的缓存策略
  4. 域名分片: 多个域名提升加载速度

迁移风险与缓解

技术风险

  1. 构建复杂度: 复杂的工作空间依赖图

    • 缓解: 使用Turborepo简化配置
  2. 依赖冲突: 不同包的依赖版本冲突

    • 缓解: 使用pnpm的严格模式
  3. 构建时间: 大型Monorepo构建时间长

    • 缓解: 利用Turborepo缓存和增量构建

团队风险

  1. 学习成本: 团队需要学习新的工作方式

    • 缓解: 提供详细的文档和培训
  2. 工具兼容性: 现有工具可能不支持Monorepo

    • 缓解: 选择兼容性好的工具
  3. CI/CD复杂性: 流水线配置更复杂

    • 缓解: 提供完整的CI/CD模板

设计版本: 1.0
创建时间: 2025-11-18
技术负责人:
审核状态: 待审核