从 Options API 转到 Composition API 半年了,踩了不少坑,也总结了一些写法。分享一下我们团队最终沉淀下来的规范。
为什么用 Composition API
Options API 的问题:同一个功能的代码分散在 data、computed、methods、watch 里。一个搜索功能要跳四个地方看代码。
Composition API 的核心价值是按功能组织代码,而不是按选项类型:
vue
<script setup>
// 搜索相关的全在这里
const keyword = ref('')
const results = ref([])
const loading = ref(false)
const debouncedSearch = useDebounceFn((kw) => {
loading.value = true
searchApi(kw).then(res => {
results.value = res.data
loading.value = false
})
}, 300)
watch(keyword, (val) => debouncedSearch(val))
// 分页相关的全在这里
const currentPage = ref(1)
const pageSize = ref(20)
const total = ref(0)
</script>
ref vs reactive
ts
// ref:适合基本类型和需要整体替换的引用类型
const count = ref(0)
const user = ref<User | null>(null)
// reactive:适合对象且不需要整体替换的场景
const form = reactive({
username: '',
email: '',
role: 'USER'
})
我们团队的约定:优先用 ref,只有明确不需要整体替换的表单/配置对象才用 reactive。原因是 reactive 解构会丢失响应性,新手容易踩坑。
ts
// 踩坑:解构丢失响应性
const form = reactive({ name: '', age: 0 })
let { name } = form // name 是普通字符串,不是响应式
name = 'Alice' // form.name 不会变
// 正确:用 toRefs
const { name, age } = toRefs(form)
自定义 Hook(composable)
把可复用的逻辑抽成 composable,命名以 use 开头:
ts
// composables/useLoading.ts
export function useLoading<T>(asyncFn: () => Promise<T>) {
const loading = ref(false)
const error = ref<Error | null>(null)
const data = ref<T | null>(null) as Ref<T | null>
async function execute() {
loading.value = true
error.value = null
try {
data.value = await asyncFn()
} catch (e) {
error.value = e as Error
} finally {
loading.value = false
}
}
return { loading, error, data, execute }
}
// 使用
const { loading, error, data, execute } = useLoading(() => fetchUserList())
onMounted(execute)
ts
// composables/usePagination.ts
export function usePagination<T>(fetchFn: (page: number, size: number) => Promise<T[]>) {
const list = ref<T[]>([]) as Ref<T[]>
const currentPage = ref(1)
const pageSize = ref(20)
const total = ref(0)
const loading = ref(false)
async function load() {
loading.value = true
try {
const [data, count] = await Promise.all([
fetchFn(currentPage.value, pageSize.value),
// 假设接口返回 [数据, 总数]
])
list.value = data
total.value = count
} finally {
loading.value = false
}
}
function changePage(page: number) {
currentPage.value = page
load()
}
onMounted(load)
return { list, currentPage, pageSize, total, loading, changePage }
}
watch 和 watchEffect 的区别
ts
// watch:明确指定监听谁,有新旧值
watch(keyword, (newVal, oldVal) => {
console.log(`${oldVal} → ${newVal}`)
search(newVal)
})
// watchEffect:自动收集依赖,没有参数
watchEffect(() => {
// 里面用到的响应式数据都会被追踪
search(keyword.value)
})
建议 :大部分场景用 watch,因为依赖关系明确、好调试。watchEffect 适合"初始化时自动跑一次"的场景。
生命周期
ts
import { onMounted, onUnmounted, onBeforeMount } from 'vue'
onMounted(() => {
console.log('组件挂载完成')
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
// 一定要清理!
window.removeEventListener('resize', handleResize)
})
onUnmounted 里清理定时器、事件监听、WebSocket 连接。不清理就是内存泄漏。
defineExpose
<script setup> 默认是封闭的,父组件通过 ref 访问子组件时拿不到任何东西。需要暴露接口:
vue
<!-- 子组件 -->
<script setup>
const inputRef = ref<HTMLInputElement>()
function focus() {
inputRef.value?.focus()
}
function clear() {
// ...
}
// 只暴露 focus 和 clear
defineExpose({ focus, clear })
</script>
总结
Composition API 的核心原则:按功能组织代码、composable 复用逻辑、优先用 ref、watch 优于 watchEffect、生命周期里记得清理。把这些规范定好,团队代码风格就统一了。