# Vue Composables 使用组合式 API 封装有状态逻辑的可复用函数。 ## 核心规则 1. **优先使用 VueUse** — 编写自定义函数前先查阅 [vueuse.org](https://vueuse.org) 2. **不要使用异步组合式函数** — 在其他组合式函数中被 `await` 后会丢失生命周期上下文 3. **仅在顶层调用** — 绝不要在事件处理函数、条件分支或循环中调用 4. **使用 `readonly()` 导出** — 保护内部状态不被外部修改 5. **SSR 使用 `useState()`** — 优先使用 Nuxt 的 `useState()`,而非全局 ref ## 快速参考 | 模式 | 示例 | | --------- | ------------------------------------------------- | | 命名 | `useAuth`、`useCounter`、`useDebounce` | | 状态 | `const count = ref(0)` | | 计算属性 | `const double = computed(() => count.value * 2)` | | 生命周期 | `onMounted(() => ...)`、`onUnmounted(() => ...)` | | 返回值 | `return { count, increment }` | ## 结构 ```ts // composables/useCounter.ts import { readonly, ref } from 'vue' export function useCounter(initialValue = 0) { const count = ref(initialValue) function increment() { count.value++ } function decrement() { count.value-- } function reset() { count.value = initialValue } return { count: readonly(count), // 如果不允许修改,使用 readonly increment, decrement, reset, } } ``` ## 命名 **始终以 `use` 开头:** `useAuth`、`useLocalStorage`、`useDebounce` **文件名即函数名:** `useAuth.ts` 导出 `useAuth` ## 最佳实践 **应该这样做:** - 返回包含具名属性的对象(便于解构) - 接受 options 配置对象 - 对不应修改的状态使用 `readonly()` - 处理清理工作(`onUnmounted`、`onScopeDispose`) - 为复杂函数添加 JSDoc ## 生命周期 钩子在组件上下文中执行: ```ts export function useEventListener(target: EventTarget, event: string, handler: Function) { onMounted(() => target.addEventListener(event, handler)) onUnmounted(() => target.removeEventListener(event, handler)) } ``` **监听器清理(Vue 3.5+):** ```ts import { watch, onWatcherCleanup } from 'vue' export function usePolling(url: Ref) { watch(url, (newUrl) => { const interval = setInterval(() => { fetch(newUrl).then(/* ... */) }, 1000) // 当监听器重新运行或停止时执行清理 onWatcherCleanup(() => { clearInterval(interval) }) }) } ``` **`onWatcherCleanup()` 的优势:** - 比返回清理函数更简洁 - 可与异步监听器配合使用 - 可在同一个监听器中多次调用 ## 异步模式 ```ts export function useAsyncData(fetcher: () => Promise) { const data = ref(null) const error = ref(null) const loading = ref(false) async function execute() { loading.value = true error.value = null try { data.value = await fetcher() } catch (e) { error.value = e as Error } finally { loading.value = false } } execute() return { data, error, loading, refetch: execute } } ``` **数据获取:** 优先使用 Pinia Colada 查询,而非自定义组合式函数。 ## VueUse > 关于 VueUse 组合式函数参考,请使用 `vueuse` 技能。 编写自定义组合式函数前请先查阅 VueUse——大多数常用模式已有现成实现。 > **关于 Nuxt 专属组合式函数**(useFetch、useRequestURL):参见 `nuxt` 技能的 nuxt-composables.md ## 进阶模式 ### 单例组合式函数 在所有使用同一组合式函数的组件之间共享状态: ```ts import { createSharedComposable } from '@vueuse/core' function useMapControlsBase() { const mapInstance = ref(null) const flyTo = (coords: [number, number]) => mapInstance.value?.flyTo(coords) return { mapInstance, flyTo } } export const useMapControls = createSharedComposable(useMapControlsBase) ``` ### 可取消的带 AbortController 的 Fetch ```ts export function useSearch() { let abortController: AbortController | null = null watch(query, async (newQuery) => { abortController?.abort() abortController = new AbortController() try { const data = await $fetch('/api/search', { query: { q: newQuery }, signal: abortController.signal, }) } catch (e) { if (e.name !== 'AbortError') throw e } }) } ``` ### 基于步骤的状态机 ```ts export function useSendFlow() { const step = ref<'input' | 'confirm' | 'success'>('input') const amount = ref('') const next = () => { if (step.value === 'input') step.value = 'confirm' else if (step.value === 'confirm') step.value = 'success' } return { step, amount, next } } ``` ### 仅客户端守卫 ```ts export function useUserLocation() { const location = ref(null) if (import.meta.client) { navigator.geolocation.getCurrentPosition(pos => location.value = pos) } return { location } } ``` ### 自定义元素组合式函数(Vue 3.5+) 对于自定义元素组件,使用内置辅助函数: ```ts import { useHost, useShadowRoot } from 'vue' export function useCustomElement() { const host = useHost() // 宿主元素引用 const shadowRoot = useShadowRoot() // Shadow DOM 根节点 onMounted(() => { console.log('Host:', host) console.log('Shadow:', shadowRoot) }) return { host, shadowRoot } } ``` **适用场景:** - 自定义元素中使用 `