---
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
```
**GOOD - 使用 useXxx 模式封装:**
```vue
```
## 核心原则
### 1. 清晰的返回接口
return 语句记录了该功能暴露了什么:
```typescript
function useSearch() {
// 内部状态 - 不返回
const abortController = ref(null)
// 公共状态
const searchQuery = ref('')
const searchResults = ref([])
// 公共方法
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)