Files
EmailBill/.sisyphus/notepads/calendar-refactor/learnings.md
SunCheng 952c75bf08
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 54s
Docker Build & Deploy / Deploy to Production (push) Successful in 9s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s
1
2026-02-03 17:56:32 +08:00

326 lines
9.1 KiB
Markdown

## Vue 3 Composition API Research - Modular Architecture Best Practices
### 研究日期: 2026-02-03
---
## 1. 官方 Vue 3 组件组织原则
### 1.1 Composables 用于代码组织
来源: Vue 官方文档 - https://vuejs.org/guide/reusability/composables
**核心原则:**
- Composables 不仅用于复用,也用于**代码组织**
- 当组件变得过于复杂时,应该将逻辑按**关注点分离**提取到更小的函数中
- 可以将提取的 composables 视为**组件级别的服务**,它们可以相互通信
**官方示例模式:**
```vue
<script setup>
import { useFeatureA } from './featureA.js'
import { useFeatureB } from './featureB.js'
import { useFeatureC } from './featureC.js'
const { foo, bar } = useFeatureA()
const { baz } = useFeatureB(foo)
const { qux } = useFeatureC(baz)
</script>
```
**关键洞察:**
- Composables 应返回**普通对象**包含多个 refs,保持响应式
- 避免返回 reactive 对象,因为解构会失去响应性
- Composables 可以接收其他 composables 的返回值作为参数
---
## 2. 代码分割与懒加载
### 2.1 defineAsyncComponent 用于模块懒加载
来源: Vue 官方文档 - https://github.com/vuejs/docs/blob/main/src/guide/best-practices/performance.md
**适用场景:**
- 将大型组件树分割成独立的 chunks
- 仅在组件渲染时才加载,改善初始加载时间
```js
import { defineAsyncComponent } from 'vue'
// Foo.vue 及其依赖被单独打包成一个 chunk
// 只有在组件被渲染时才会按需获取
const Foo = defineAsyncComponent(() => import('./Foo.vue'))
```
### 2.2 动态导入用于 JS 代码分割
```js
// lazy.js 及其依赖会被分割成单独的 chunk
// 只在 loadLazy() 被调用时才加载
function loadLazy() {
return import('./lazy.js')
}
```
---
## 3. 真实世界的模块化架构模式
### 3.1 Dashboard 模块化架构 - 成功案例
**案例 1: Soybean Admin (MIT License)**
来源: https://github.com/soybeanjs/soybean-admin/blob/main/src/views/home/index.vue
```vue
<script setup lang="ts">
import { computed } from 'vue';
import { useAppStore } from '@/store/modules/app';
import HeaderBanner from './modules/header-banner.vue';
import CardData from './modules/card-data.vue';
import LineChart from './modules/line-chart.vue';
import PieChart from './modules/pie-chart.vue';
import ProjectNews from './modules/project-news.vue';
import CreativityBanner from './modules/creativity-banner.vue';
const appStore = useAppStore();
const gap = computed(() => (appStore.isMobile ? 0 : 16));
</script>
```
**架构特点:**
- Index.vue 作为**容器组件**,只负责布局和响应式计算
- 每个 modules/*.vue 是**独立的功能模块**
- 模块命名清晰: header-banner, card-data, line-chart 等
- 使用 Pinia store 进行状态共享
**案例 2: Art Design Pro (MIT License)**
来源: https://github.com/Daymychen/art-design-pro/blob/main/src/views/dashboard/ecommerce/index.vue
```vue
<script setup lang="ts">
import Banner from './modules/banner.vue'
import TotalOrderVolume from './modules/total-order-volume.vue'
import TotalProducts from './modules/total-products.vue'
import SalesTrend from './modules/sales-trend.vue'
import SalesClassification from './modules/sales-classification.vue'
import TransactionList from './modules/transaction-list.vue'
import HotCommodity from './modules/hot-commodity.vue'
import RecentTransaction from './modules/recent-transaction.vue'
import AnnualSales from './modules/annual-sales.vue'
import ProductSales from './modules/product-sales.vue'
import SalesGrowth from './modules/sales-growth.vue'
import CartConversionRate from './modules/cart-conversion-rate.vue'
import HotProductsList from './modules/hot-products-list.vue'
defineOptions({ name: 'Ecommerce' })
</script>
```
**架构特点:**
- 电商 dashboard 包含 13 个独立模块
- 每个模块代表一个业务功能卡片
- Index.vue **不传递数据**,模块自治
---
## 4. 模块间通信模式
### 4.1 defineEmits 用于子到父通信
来源: Vue 核心仓库 - https://github.com/vuejs/core/blob/main/packages/runtime-core/src/apiSetupHelpers.ts
**TypeScript 类型声明模式:**
```ts
const emit = defineEmits<{
'update:modelValue': [value: string];
'change': [event: Event];
'custom-event': [payload: CustomPayload];
}>();
```
**Runtime 声明模式:**
```js
const emit = defineEmits(['change', 'update'])
```
### 4.2 Props 模式 - 数据传递 vs 自取数据
**案例研究: Halo CMS (GPL-3.0)**
来源: https://github.com/halo-dev/halo/blob/main/ui/console-src/modules/system/users/components/GrantPermissionModal.vue
```vue
<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
import { useI18n } from "vue-i18n";
import { useFetchRoles, useFetchRoleTemplates } from "../composables/use-role";
const props = withDefaults(
defineProps<{
user?: User;
}>(),
{
user: undefined,
}
);
const emit = defineEmits<{
(event: "close"): void;
}>();
// 模块自己获取数据
onMounted(async () => {
await fetchRoles();
});
</script>
```
**模式总结:**
- **Props 传递身份标识** (如 user ID),而非完整数据
- **模块自己获取详细数据** (通过 composables)
- 这样保持模块的**高内聚低耦合**
---
## 5. 何时模块应该自取数据 vs 接收 Props
### 5.1 自取数据的场景
- 模块是**独立的业务单元**(如日历、统计卡片)
- 数据获取逻辑属于模块内部关注点
- 模块需要**定期刷新**或**重新加载**数据
- 多个平行模块各自管理自己的状态
**示例:**
```vue
<!-- 统计卡片模块 - 自己获取数据 -->
<script setup lang="ts">
const { data, loading } = useBudgetStats()
onMounted(() => {
loadStats()
})
</script>
```
### 5.2 接收 Props 的场景
- 模块是**展示组件**(Presentational Component)
- 父组件需要**协调多个子组件**的数据
- 数据来源于**全局状态管理**(如 Pinia store)
- 需要在父组件层面做**数据聚合或转换**
**示例:**
```vue
<!-- 数据展示组件 - 接收 props -->
<script setup lang="ts">
const props = defineProps<{
stats: BudgetStats
loading: boolean
}>()
</script>
```
---
## 6. TypeScript vs JavaScript 在 Vue 3 项目中
### 6.1 EmailBill 项目的选择
**当前状况:**
- ESLint 配置中禁用了 TypeScript 规则
- 使用 `<script setup lang="ts">` 但不强制类型检查
- 轻量级类型提示,不追求严格类型安全
**何时避免 TypeScript:**
- 小型项目,团队更熟悉 JavaScript
- 快速原型开发
- 避免 TypeScript 配置和类型定义的复杂度
- 保持构建速度和开发体验的流畅
**何时使用 TypeScript:**
- 大型团队协作
- 复杂的状态管理和数据流
- 需要严格的 API 契约
- 长期维护的企业级应用
---
## 7. 模块化架构的最佳实践总结
### 7.1 目录结构推荐
```
views/
calendar/
Index.vue # 容器组件,布局和协调
modules/
CalendarView.vue # 日历展示模块(自取数据)
MonthlyStats.vue # 月度统计模块(自取数据)
QuickActions.vue # 快捷操作模块(事件驱动)
composables/
useCalendarData.ts # 日历数据获取逻辑
useMonthlyStats.ts # 统计数据获取逻辑
```
### 7.2 组件职责划分
**Index.vue (容器组件):**
- 布局管理和响应式设计
- 协调模块间的通信(如果需要)
- 全局状态初始化
- **不应包含业务逻辑**
**modules/*.vue (功能模块):**
- 独立的业务功能单元
- 自己管理数据获取和状态
- 通过 emits 向父组件通信
- 高内聚,低耦合
**composables/*.ts (可复用逻辑):**
- 数据获取逻辑
- 业务规则计算
- 状态管理辅助
- 可在多个组件间共享
### 7.3 通信模式推荐
**模块向上通信 (Child → Parent):**
```ts
const emit = defineEmits<{
'date-changed': [date: Date]
'item-clicked': [item: CalendarItem]
}>()
```
**模块间通信 (Sibling ↔ Sibling):**
- 通过**父组件中转**事件
- 或使用**全局事件总线**(如 mitt)
- 或使用**Pinia store** 共享状态
---
## 8. 关键洞察和建议
### 8.1 高内聚模块设计
- 每个模块应该是**自治的**,包含自己的数据获取、状态管理和事件处理
- Index.vue 应该是**轻量级的协调者**,而非数据的中央枢纽
### 8.2 Props vs 自取数据的平衡
- **身份标识和配置通过 props** (如 userId, date, theme)
- **业务数据通过模块自取** (如 stats, calendar items)
### 8.3 避免过度抽象
- 不要为了复用而复用
- 优先考虑**代码的清晰度**而非极致的 DRY
- Composables 应该解决**真实的重复问题**,而非预测性的抽象
---
## 9. 参考资源
**官方文档:**
- Vue 3 Composables: https://vuejs.org/guide/reusability/composables
- Vue 3 Performance: https://github.com/vuejs/docs/blob/main/src/guide/best-practices/performance.md
- Vue 3 State Management: https://vuejs.org/guide/scaling-up/state-management
**真实项目参考:**
- Soybean Admin: https://github.com/soybeanjs/soybean-admin (MIT)
- Art Design Pro: https://github.com/Daymychen/art-design-pro (MIT)
- Halo CMS: https://github.com/halo-dev/halo (GPL-3.0)
- DataEase: https://github.com/dataease/dataease (GPL-3.0)