Files
pulse-memory/skills/vue-composition-api-best-practices/reference/cross-feature-dependencies.md
T
Pulse ae39e45460 feat: biblioteca inteligente libs/ + 5 novas skills (20 skills total)
NOVAS SKILLS:
- next-best-practices      v0.1.0  (CLEAN) — Next.js App Router, RSC, caching, data
- nextjs-patterns          v1.0.0  (CLEAN) — Next.js 15: Server Actions, route handlers
- vite                     v1.0.0  (CLEAN) — env vars, aliases, proxy, CJS compat
- uncle-bob                v1.0.0  (CLEAN) — Clean Code, SOLID, Clean Architecture
- clean-code-review        v1.0.0  (CLEAN) — naming, guard clauses, anti-patterns, refactoring
- vue                      v1.0.0  (CLEAN) — Vue framework
- vue-composition-api-best-practices v1.0.0 (CLEAN) — composables, Pinia, reactivity

BIBLIOTECA INTELIGENTE libs/ (10 dominios, 11 arquivos):
- typescript/ — TS safe + generics gotchas
- react/ — Next.js App Router + Vite config
- vue/ — Composition API + Pinia
- linux/ — System diagnostic cheatsheet
- database/ — PostgreSQL + MySQL patterns
- browser/ — Chromium CLI + E2E testing
- security/ — SAST audit (OWASP Top 10)
- best-practices/ — Clean Code + SOLID + Clean Architecture
- deploy/ — Docker multi-stack + OpenClaw ops
- + INDEX.md como guia de navegacao

.learnings/ — LRN-20260519-003 criado (biblioteca compartilhada)
2026-05-19 21:03:25 -03:00

432 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: Handling Cross-Feature Dependencies
impact: MEDIUM
impactDescription: 跨功能依赖管理不当会导致紧密耦合、不可预测的行为,以及功能交互时难以调试
type: best-practice
tags: [vue3, composition-api, dependencies, coupling, architecture, event-bus]
---
# 处理跨功能依赖
**影响级别:MEDIUM** - 当功能需要交互时,合理的依赖管理可确保行为可预测、代码可维护。
## 任务清单
- [ ] 通过函数参数显式传递依赖
- [ ] 避免通过外层作用域闭包产生隐式依赖
- [ ] 使用回调函数进行跨功能通信
- [ ] 对于多对多通信,使用事件总线并自动清理
- [ ] 考虑依赖方向(单向,避免循环)
- [ ] 优先使用 Store 桥接组合式函数访问共享状态
## 问题所在
功能之间经常需要交互,但隐式依赖会使代码难以理解和测试。
**BAD - 通过外层作用域产生隐式依赖:**
```vue
<script setup lang="ts">
import { ref, watch } from 'vue'
// 功能 1:搜索
const searchQuery = ref('')
const searchResults = ref([])
async function handleSearch() {
searchResults.value = await fetchResults(searchQuery.value)
}
// 功能 2:分页 — 隐式依赖 handleSearch
const currentPage = ref(1)
watch(currentPage, () => {
handleSearch() // 这属于哪个功能?
})
// 功能 3:筛选 — 也依赖搜索
const activeFilter = ref('all')
watch(activeFilter, () => {
handleSearch() // 又一个隐式依赖
})
// 问题:更改筛选条件会重置页码,但顺序很重要!
// 哪个 watch 先触发?不清楚!
</script>
```
**GOOD - 通过参数显式传递依赖:**
```vue
<script setup lang="ts">
import { ref, watch } from 'vue'
// 功能声明
const {
searchQuery,
searchResults,
handleSearch
} = useSearch()
const {
currentPage,
changePage
} = usePagination({
onPageChange: handleSearch
})
const {
activeFilter,
setFilter
} = useFilter({
onFilterChange: () => {
changePage(1)
handleSearch()
}
})
// ============ 功能实现 ============
function useSearch() {
const searchQuery = ref('')
const searchResults = ref<SearchResult[]>([])
const handleSearch = async () => {
searchResults.value = await fetchResults(searchQuery.value)
}
return { searchQuery, searchResults, handleSearch }
}
function usePagination(options: { onPageChange: () => void }) {
const currentPage = ref(1)
const changePage = (page: number) => {
currentPage.value = page
options.onPageChange()
}
return { currentPage, changePage }
}
function useFilter(options: { onFilterChange: () => void }) {
const activeFilter = ref('all')
const setFilter = (filter: string) => {
activeFilter.value = filter
options.onFilterChange()
}
return { activeFilter, setFilter }
}
</script>
```
## 依赖模式
### 模式 1:回调模式(推荐用于简单通信)
通过回调进行直接的跨功能通信:
```typescript
function usePagination(options: {
onPageChange?: (page: number) => void
}) {
const currentPage = ref(1)
const goToPage = (page: number) => {
currentPage.value = page
options.onPageChange?.(page)
}
return { currentPage, goToPage }
}
// 用法
const { handleSearch } = useSearch()
const { currentPage, goToPage } = usePagination({
onPageChange: () => handleSearch()
})
```
### 模式 2Ref 注入模式
传递响应式 ref 实现共享状态:
```typescript
function useSearch(query: Ref<string>) {
const results = ref([])
watch(query, async (q) => {
results.value = await fetchResults(q)
})
return { results }
}
function useSearchInput() {
const query = ref('')
const debouncedQuery = refDebounced(query, 300)
return { query, debouncedQuery }
}
// 用法 — 显式依赖
const { query, debouncedQuery } = useSearchInput()
const { results } = useSearch(debouncedQuery)
```
### 模式 3:组合式函数编排模式
创建更高层级的组合式函数来组合多个功能:
```typescript
function useSearchWithPagination() {
const { searchQuery, searchResults, handleSearch } = useSearch()
const { currentPage, pageSize, changePage } = usePagination({
onPageChange: handleSearch
})
const searchWithParams = () => {
return handleSearch({
query: searchQuery.value,
page: currentPage.value,
size: pageSize.value
})
}
return {
searchQuery, searchResults,
currentPage, pageSize,
searchWithParams, changePage
}
}
```
### 模式 4:事件总线模式(适用于复杂的多对多场景)
使用 `mitt` 并自动清理,实现解耦通信:
```typescript
// hooks/web/useEmitt.ts
import { onUnmounted } from 'vue'
import { mittBus } from '@/utils/mitt'
export function useEmitt() {
const listeners: Array<{ event: string; handler: (...args: any[]) => void }> = []
function on(event: string, handler: (...args: any[]) => void) {
mittBus.on(event, handler)
listeners.push({ event, handler })
}
function emit(event: string, ...args: any[]) {
mittBus.emit(event, ...args)
}
// 组件卸载时自动解绑所有通过此 hook 注册的事件
onUnmounted(() => {
listeners.forEach(({ event, handler }) => {
mittBus.off(event, handler)
})
listeners.length = 0
})
return { on, emit }
}
```
**用法 — 发送方:**
```typescript
// Layout.vue - 发送事件
const { emit } = useEmitt()
emit('open-contextmenu', { event: e })
```
**用法 — 接收方(自动清理):**
```typescript
// MiContextMenu.vue - 监听事件
const { on } = useEmitt()
on('open-contextmenu', (data) => {
// 在事件位置处理上下文菜单
})
// 无需手动 off,组件卸载时自动清理
// 多次调用 on() 注册多个监听器,全部会在卸载时清理
```
> 另见:[组合式函数设计模式 - 模式 3:生命周期感知](composable-design-patterns.md#模式-3生命周期感知) 了解 `useEmitt` 作为 Lifecycle-Aware 模式的完整设计原理。
**何时使用事件总线 vs 回调:**
| 场景 | 模式 | 原因 |
|----------|---------|--------|
| 直接父子关系 | 回调/Props | 简单、显式、类型安全 |
| 同一组件内的兄弟功能 | 回调 | 依赖流清晰 |
| 不同层级树的跨组件通信 | 事件总线 | 需要解耦 |
| 一个事件多个监听器 | 事件总线 | 一对多关系 |
| 功能需要响应 store 变化 | Store 桥接 | 单一数据源 |
### 模式 5:Store 桥接用于共享状态
当多个功能需要相同的 store 数据时,使用 Store 桥接组合式函数,而不是直接访问 store:
```typescript
// ✅ Good — Store 桥接提供统一接口
// hooks/web/useSideCategory.ts
export const useSideCategory = () => {
const appStore = useAppStoreWithOut()
const selectCategory = computed(() => appStore.selectCategory)
const sidebarCategories = computed(() => appStore.sidebarCategories)
const removeCategory = (val: string) => {
// 复杂的业务逻辑集中在这里
const idx = sidebarCategories.value.findIndex((s) => s.key === val)
if (idx !== -1) {
const newCategories = [...sidebarCategories.value]
newCategories.splice(idx, 1)
appStore.deletePageIconInfo(val) // 同时清理相关数据
// 处理选中状态...
appStore.setSidebarCategories(newCategories)
}
}
return { selectCategory, sidebarCategories, removeCategory }
}
// ❌ Bad — 组件中分散的直接 store 访问
// ComponentA.vue
const appStore = useAppStore()
const idx = appStore.sidebarCategories.findIndex(...)
appStore.deletePageIconInfo(val)
appStore.setSidebarCategories(...)
// ComponentB.vue - 重复的逻辑!
const appStore = useAppStore()
const idx = appStore.sidebarCategories.findIndex(...)
appStore.deletePageIconInfo(val)
appStore.setSidebarCategories(...)
```
## 依赖方向规则
### ✅ Good — 单向依赖
```
┌─────────────┐
│ 父组件 │
│ Component │
└──────┬──────┘
│ 传递依赖
┌─────────────┐ ┌─────────────┐
│ 功能 A │────▶│ 功能 B │
│ (搜索) │ │ (分页) │
└─────────────┘ └─────────────┘
```
### ✅ Good — 跨树通信使用事件总线
```
┌──────────┐ emit ┌──────────────┐
│ Layout │───────────────▶│ ContextMenu │
└──────────┘ └──────────────┘
┌──────────┐ emit ┌──────────────┐
│ Search │───────────────▶│ Suggestion │
└──────────┘ └──────────────┘
```
### ❌ 避免 — 循环依赖
```
┌─────────────┐ ┌─────────────┐
│ 功能 A │◀───▶│ 功能 B │
│ │ │ │
└─────────────┘ └─────────────┘
循环依赖!
```
## 常见场景
### 搜索 + 筛选 + 分页
```vue
<script setup lang="ts">
// 清晰的依赖链
const { searchQuery } = useSearchInput()
const { activeFilter } = useFilter()
const { currentPage, pageSize } = usePagination()
// 数据获取整合所有参数
const { data, loading, refetch } = useDataFetch({
query: searchQuery,
filter: activeFilter,
page: currentPage,
size: pageSize
})
// 筛选变化时重置页码
watch(activeFilter, () => {
currentPage.value = 1
refetch()
})
</script>
```
### 通过事件总线跨组件通信
```vue
<!-- Layout.vue - 触发 -->
<script setup lang="ts">
const { emit } = useEmitt()
const openContextmenu = (e: PointerEvent) => {
emit('open-contextmenu', { event: e })
}
</script>
<!-- MiContextMenu.vue - 监听 -->
<script setup lang="ts">
const { on } = useEmitt()
on('open-contextmenu', ({ event }) => {
// 在事件位置显示上下文菜单
})
</script>
```
### 表单 + 验证 + 提交
```vue
<script setup lang="ts">
const { fields, updateField } = useFormFields({
name: '',
email: ''
})
const { errors, validate } = useFormValidation(fields, {
name: { required: true },
email: { required: true, email: true }
})
const { submit, isSubmitting } = useFormSubmit({
onSubmit: async () => {
if (!validate()) return
await submitForm(fields)
}
})
</script>
```
## 参考资料
- [Vue.js Composables](https://vuejs.org/guide/reusability/composables.html)
- [Vue.js Reactivity in Depth](https://vuejs.org/guide/extras/reactivity-in-depth.html)
- [mitt - Tiny Event Emitter](https://github.com/developit/mitt)