Files
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

359 lines
8.6 KiB
Markdown
Raw Permalink 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: UseXxx Function Pattern for Feature Encapsulation
impact: HIGH
impactDescription: 如果不使用 useXxx 模式,功能逻辑会变得分散,难以理解每个功能暴露了什么,也难以追踪功能之间的依赖关系
type: best-practice
tags: [vue3, composition-api, script-setup, use-pattern, code-organization]
---
# UseXxx 函数模式:功能封装
**影响等级:高** - useXxx 模式将相关逻辑封装到自包含的函数中,使功能易于理解、测试和复用。
## 任务清单
- [ ] 将功能逻辑封装在 `useFeatureName()` 函数中
- [ ] 仅返回外部需要的值和方法
- [ ] 将功能实现放在 script 底部
- [ ] 在顶部使用解构声明功能使用
- [ ] 功能函数命名清晰,能反映其用途
## 问题所在
没有封装时,相关的变量、计算属性、监听器和方法散落在代码各处,难以理解哪些代码属于哪个功能。
**BAD - 功能逻辑分散:**
```vue
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
// 搜索功能分散
const searchQuery = ref('')
const searchResults = ref([])
const isSearching = ref(false)
// 分页功能混在一起
const currentPage = ref(1)
const pageSize = ref(10)
const totalItems = ref(0)
// 随意放置的 computed
const hasResults = computed(() => searchResults.value.length > 0)
// 另一个功能开始
const selectedItem = ref(null)
// 搜索函数
async function handleSearch() {
isSearching.value = true
searchResults.value = await fetchResults(searchQuery.value)
isSearching.value = false
}
// 分页函数
function changePage(page: number) {
currentPage.value = page
handleSearch()
}
// watch 分散
watch(searchQuery, handleSearch)
// 选择函数
function selectItem(item: any) {
selectedItem.value = item
}
</script>
```
**GOOD - 使用 useXxx 模式封装:**
```vue
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
// 功能声明
// 搜索功能 - 一目了然的接口
const {
searchQuery,
searchResults,
isSearching,
hasResults,
handleSearch
} = useSearch()
// 分页功能 - 自包含
const {
currentPage,
pageSize,
totalItems,
changePage
} = usePagination({ onPageChange: handleSearch })
// 选择功能 - 独立
const {
selectedItem,
selectItem
} = useSelection()
// ============ 功能实现 ============
function useSearch() {
const searchQuery = ref('')
const searchResults = ref<SearchResult[]>([])
const isSearching = ref(false)
const hasResults = computed(() => searchResults.value.length > 0)
const handleSearch = async () => {
isSearching.value = true
try {
searchResults.value = await fetchResults(searchQuery.value)
} finally {
isSearching.value = false
}
}
watch(searchQuery, handleSearch)
return {
searchQuery,
searchResults,
isSearching,
hasResults,
handleSearch
}
}
function usePagination(options: { onPageChange: () => void }) {
const currentPage = ref(1)
const pageSize = ref(10)
const totalItems = ref(0)
const changePage = (page: number) => {
currentPage.value = page
options.onPageChange()
}
return {
currentPage,
pageSize,
totalItems,
changePage
}
}
function useSelection() {
const selectedItem = ref<SearchResult | null>(null)
const selectItem = (item: SearchResult) => {
selectedItem.value = item
}
return {
selectedItem,
selectItem
}
}
</script>
```
## 核心原则
### 1. 清晰的返回接口
return 语句记录了该功能暴露了什么:
```typescript
function useSearch() {
// 内部状态 - 不返回
const abortController = ref<AbortController | null>(null)
// 公共状态
const searchQuery = ref('')
const searchResults = ref<SearchResult[]>([])
// 公共方法
const handleSearch = async () => { /* ... */ }
return {
// 只暴露需要的内容
searchQuery,
searchResults,
handleSearch
}
}
```
### 2. 自包含的逻辑
每个 useXxx 函数包含所有相关的内容:
- 状态(ref、reactive
- 计算属性
- 监听器
- 生命周期钩子
- 方法
```typescript
function useSearch() {
// 状态
const query = ref('')
const results = ref([])
// 计算属性
const isEmpty = computed(() => results.value.length === 0)
// 监听器
watch(query, debounce(search, 300))
// 生命周期
onMounted(() => {
if (query.value) search()
})
// 方法
async function search() { /* ... */ }
return { query, results, isEmpty, search }
}
```
### 3. 通过参数进行依赖注入
将依赖作为参数传递,实现跨功能通信:
```typescript
function usePagination(options: {
onPageChange?: () => void
initialPage?: number
} = {}) {
const currentPage = ref(options.initialPage ?? 1)
const goToPage = (page: number) => {
currentPage.value = page
options.onPageChange?.()
}
return { currentPage, goToPage }
}
// 使用方式
const { handleSearch } = useSearch()
const { currentPage, goToPage } = usePagination({
onPageChange: handleSearch
})
```
### 4. Store 桥接模式
当组合式函数封装 store 访问时,需提供干净的接口来隐藏 store 实现细节:
```typescript
// 页面图标管理 hooks - 封装 store 访问
export const usePageIcon = () => {
const appStore = useAppStoreWithOut()
// 当前 page
const curPage = computed(() => appStore.selectCategory.key)
// 当前 page icons
const curPageIcons = computed(() => appStore.pageIconMap[curPage.value] || [])
// 新增 page icon
const addPageIcon = (icon: PageItemWithOptionalKey, page?: string) => {
if (!appStore.pageIconMap[page || curPage.value]) {
appStore.addPageIconInfo(page || curPage.value)
}
const icons = appStore.pageIconMap[page || curPage.value]
if (!icon.key) {
icon.key = `${page || curPage.value}-icon-${icon.type}-${icons.length}`
}
appStore.updatePageIconInfo(page || curPage.value, icons.concat(icon as PageItem))
}
// 更新 page icon
const updatePageIcon = (icon: PageItem, page: string) => {
const icons = appStore.pageIconMap[page]
const targetIdx = icons.findIndex((i) => i.key === icon.key)
if (targetIdx !== -1) {
icons[targetIdx] = icon
appStore.updatePageIconInfo(page, icons)
}
}
// 删除 page icon
const removePageIcon = (icon: PageItem, page?: string) => {
const icons = appStore.pageIconMap[page || curPage.value]
if (icons) {
appStore.updatePageIconInfo(
page || curPage.value,
icons.filter((item) => item.key !== icon.key)
)
}
}
return {
curPage,
curPageIcons,
addPageIcon,
updatePageIcon,
removePageIcon
}
}
```
**Store 桥接模式的收益:**
- 组件无需了解 store 内部结构
- 业务逻辑集中在一处
- 便于在无需更新所有组件的情况下更改 store 结构
-`useXxxStoreWithOut` 无缝配合,适用于非组件场景
## 命名约定
| 模式 | 示例 | 使用场景 |
|---------|---------|----------|
| `useXxx` | `useSearch()` | 组件内部的功能封装 |
| `useXxx` | `useUserStore()` | 外部组合式函数导入 |
| `useXxxStoreWithOut` | `useAppStoreWithOut()` | 组件外部的 store 访问 |
| `useXxx`(桥接) | `usePageIcon()` | Store 桥接组合式函数 |
## 何时提取到外部文件
满足以下条件时移至外部文件:
- 跨多个组件使用
- 复杂度足够高,需要单独测试
- 不依赖父组件状态
- 作为 store 数据的桥接层
```typescript
// hooks/web/usePageIcon.ts
export const usePageIcon = () => {
const appStore = useAppStoreWithOut()
// ...
return { curPageIcons, addPageIcon, updatePageIcon, removePageIcon }
}
// 在组件中
import { usePageIcon } from '@/hooks/web/usePageIcon'
const { curPageIcons, addPageIcon } = usePageIcon()
```
## 功能实现检查清单
编写 `useXxx` 函数时:
- [ ] 所有相关状态都在函数内部(不散落在外部)
- [ ] 从状态派生的计算属性都在函数内部
- [ ] 响应状态变化的监听器都在函数内部
- [ ] 生命周期钩子(`onMounted``onBeforeUnmount`)都在函数内部
- [ ] 清理逻辑(`removeEventListener``off`)在 `onBeforeUnmount`
- [ ] 只返回组件需要的内容
- [ ] 内部实现细节被隐藏
## 参考
- [Vue.js Composables](https://vuejs.org/guide/reusability/composables.html)
- [Vue.js Composition API FAQ](https://vuejs.org/guide/extras/composition-api-faq.html)