创建日期:2026-09-08 | 最近更新:2026-09-08 基于 Vue 3.5.42 / Vite 8 / Pinia 4(2026-09 npm 版本核对)。组合式/类型写法以官方文档为准;前端代码非本文重点实测对象,文中不含伪造运行日志。
组合式进阶 × TypeScript × 工程化
一句话:Composition API 最大的红利不是「换个写法」,而是让逻辑能被抽成
useXxx()函数去复用、测试、组合——这是 Vue2mixins给不了的(命名冲突、来源不明、类型稀烂)。这篇把「怎么写真正好用的组合式函数」讲到能上生产,并补齐 Pinia setup store、Router、Vite 的工程化拼图。
1. 组合式函数(Composable)是什么、为什么比 mixins 强
一个「用 Vue 响应式原语实现、命名以 use 开头、返回状态与操作」的普通函数:
// composables/useCounter.js
import { ref, computed } from 'vue'
export function useCounter(init = 0) {
const count = ref(init)
const double = computed(() => count.value * 2)
const inc = () => count.value++
return { count, double, inc } // 谁调谁拿到一份「自己的状态」
}
<script setup>
const { count, inc } = useCounter(10) // 组件里一行引入
</script>
| mixins | composable |
|---|---|
| 命名冲突(同名字段互相覆盖) | 解构赋值,冲突在你这层显式解决 |
来源不明(this.xxx 从哪来) | 状态/函数从 useX() 返回,来源可见 |
| 类型几乎没法推 | 返回对象天然可推 TS 类型 |
| 隐式共享同一 data 命名空间 | 每个调用方独立实例(除非刻意共享) |
三条纪律(好 composable 的标准):
- 每个调用拿独立状态(函数内
ref/reactive),除非你明确要做「单例共享 store」(§4); - 只在组件/别的 composable 的 setup 上下文里调用(要能
onMounted、onScopeDispose); use前缀:既是约定,也让 linter/工具能识别「这里用了响应式上下文」。
2. 进阶模式:接受 ref 与普通值的通用入参
3.3+ 提供 toValue()——统一处理「传 ref 也行、传普通值也行」:
// composables/useDebounce.js —— 对任意值做防抖
import { ref, watch, toValue } from 'vue'
export function useDebounce(source, delay = 300) {
const debounced = ref(toValue(source)) // 支持 ref / getter / 普通值
watch(source, (v) => {
const timer = setTimeout(() => (debounced.value = v), delay)
// 记得清理(§3)
})
return debounced
}
组件里既可用 useDebounce(searchRef),也能 useDebounce('abc')——接口对调用方友好。
一个带清理与异步取消的完整例子
// composables/useSearch.js —— 输入防抖 + 请求竞态取消(真实项目高频需求)
import { ref, watch, onScopeDispose } from 'vue'
import { apiSearch } from '@/api'
export function useSearch(delay = 300) {
const keyword = ref('')
const results = ref([])
const loading = ref(false)
const controller = ref(null)
watch(keyword, async (kw) => {
loading.value = true
controller.value?.abort() // 取消上一个还没回来的请求
const ctrl = new AbortController(); controller.value = ctrl
try {
results.value = kw ? await apiSearch(kw, ctrl.signal) : []
} finally {
if (controller.value === ctrl) loading.value = false // 只认最新那个
}
})
// 组件卸载 / 外层 scope 销毁时自动清理 —— 这就是「组合式函数也能带生命周期」
onScopeDispose(() => controller.value?.abort())
return { keyword, results, loading }
}
onScopeDispose 是关键:composable 里注册的清理逻辑,会跟着调用它的组件一起销毁——这是 mixins 做不到的「资源生命周期随作用域走」。
3. 再深一层:EffectScope(库作者必看)
默认每个组件都是一个 effect scope,组件卸载 → 内部所有 watch/effect/computed 自动停。想让一堆独立逻辑组成一个可整体启停的域,用 effectScope:
import { effectScope } from 'vue'
const scope = effectScope()
scope.run(() => { /* 这里面的 watch/effect 都属于这个 scope */ })
// 想整批停止:scope.stop()
适用:第三方组合式库(用户调 useX() 时替你建 scope,卸载时整体释放)、SSR/测试隔离。日常业务代码一般不需要手写 scope,知道「卸载自动停」的机制即可。
4. Pinia setup store:把「状态」也写成组合式
Vuex → Pinia 后(篇 2),setup store 是组合式爱好者的自然归宿——state 就是 ref,getters 就是 computed,actions 就是函数:
// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
const token = ref<string | null>(null)
const name = ref('')
const isLogin = computed(() => !!token.value)
function login(t: string, n: string) { token.value = t; name.value = n }
function logout() { token.value = null; name.value = '' }
return { token, name, isLogin, login, logout }
})
// 组件里(保持同一份共享状态,不是每组件一份)
const store = useUserStore()
store.login('abc', 'Lin')
- Pinia 的 store 是单例且跨组件共享(这点和 composable「每人一份」相反,别搞混);
- setup store 里 return 的 ref 会自动被 Pinia 处理成
store.token(不用.value)——它内部帮你解包了; - 在组件外(路由守卫/API 模块)用 store:要确保 Pinia 已激活——
app.use(pinia)之后即可;若在初始化前用,先import { setActivePinia, createPinia } from 'pinia'; setActivePinia(createPinia())。
5. TypeScript:把类型红利吃满
组件 props / emits / model 全类型化
<script setup lang="ts">
// 泛型 props + withDefaults 给默认值
const props = withDefaults(defineProps<{
items: Item[]
title?: string
max?: number
}>(), { title: '默认标题', max: 10 })
// 类型化 emits
const emit = defineEmits<{
(e: 'update:title', value: string): void
(e: 'save', payload: { id: number }): void
}>()
// 类型化 v-model(defineModel 3.4+)
const model = defineModel<string>()
</script>
泛型组件(3.3+)
<script setup lang="ts" generic="T extends { id: string | number }">
defineProps<{ rows: T[]; selected: T | null }>()
</script>
让一个组件在父级用不同实体类型时,props 自动跟随——表格/下拉这类「通用容器」组件直接受益。
组合式函数的类型(返回推断即可,必要时显式接口)
export interface UseFetch<T> { data: Ref<T | undefined>; loading: Ref<boolean>; run(): Promise<void> }
export function useFetch<T>(url: string): UseFetch<T> { /* … */ }
provide / inject 的类型(InjectionKey)
import { type InjectionKey } from 'vue'
export const ThemeKey: InjectionKey<Ref<string>> = Symbol('theme')
// 注入方:const theme = inject(ThemeKey) // 直接拿到 Ref<string> | undefined
6. 工程化拼图:Vite + Router
Vite(替代 webpack 的默认答案)
// vite.config.ts
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } },
server: { port: 5173, proxy: { '/api': 'http://localhost:8080' } }, // 开发代理
})
- 环境变量:
import.meta.env.VITE_API_BASE(只有VITE_前缀会暴露给前端); npm create vue@latest直接出带 TS/ESLint/路由/Pinia 的骨架;- 构建产物交给生产服务器或 Docker(
vite build)。
Router 组合式用法
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
router.push({ name: 'detail', params: { id: route.params.id } })
类型增强路由 meta:
// 想在 beforeEnter/守卫里拿到带类型的 meta
declare module 'vue-router' {
interface RouteMeta { requiresAuth?: boolean; title?: string }
}
7. 一份「团队组合式分层」参考
把「逻辑放哪」定出规矩,比炫技更重要:
| 层 | 放什么 | 例子 |
|---|---|---|
composables/useXxx | 与 UI 无关的通用可复用逻辑 | useDebounce / useFetch / useLocalStorage |
stores/* | 跨组件共享状态 | useUserStore / useCartStore |
composables/useDomain | 聚合「一个业务域」的多个 composable/store | useOrderFlow() |
api/* | 纯请求函数(无响应式) | apiSearch / apiOrder |
组合式函数的原则:UI 越少越好——把「防抖」「请求」「存取」写成纯逻辑,组件只负责把它接到模板上。这样逻辑能单测、能跨框架片段复用,组件薄得像壳。
关联
- 上一篇:模板编译与渲染优化
- 全系列入口:Vue2 → Vue3 快车道
- 同源心智:本站 nanostores(原子 store 与「状态即 store」的边界)
参考
- 组合式函数深入:cn.vuejs.org/guide/reusability/composables
- Pinia:pinia.vuejs.org | Vue Router:router.vuejs.org | Vite:vitejs.cn
- 版本(2026-09):Vue 3.5.42 / Vite 8 / Pinia 4 / vue-router 5(API 与 v4 主线一致)