Vue3 Composition API 最佳实践,别再乱写 setup 了

从 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、生命周期里记得清理。把这些规范定好,团队代码风格就统一了。

相关推荐
xcs194056 小时前
前端 vue 的前端页面debugger 进不去
前端·javascript·vue.js
雪芽蓝域zzs9 小时前
第 5 节:Marker 点击弹出 InfoWindow 信息窗口
vue.js
大包子12 小时前
structuredClone vs lodash.cloneDeep:核心差异与选型指南
javascript·vue.js
雪芽蓝域zzs12 小时前
第 3 节:地图点击事件,点击获取经纬度
vue.js
WayneX13 小时前
开源 Vue 3 组件库 Morya UI:把组件、文档、AI 工具链一起做进一个包
前端·vue.js·前端框架
hai_android13 小时前
Chat 聊天模块功能总结
前端·javascript·vue.js
志尊宝15 小时前
Vue3 零基础每日笔记(076):防抖节流的正确使用——搜索框、按钮防重复提交、滚动监听
javascript·vue.js·笔记
顽疲16 小时前
从零用 Java 实现小红书 SpringBoot Vue UniApp(23)笔记话题多对多:一条笔记进多个话题池
java·vue.js·spring boot
能源革命16 小时前
Vue 网格拖拽布局组件全景指南:7 大方案深度对比
前端·javascript·vue.js
daols8816 小时前
vue 表格组件 vxe-table 实现自定义多行编辑功能
前端·javascript·vue.js·vxe-table