Vue 3 Composition API实战:从基础到高级的完整开发流程与最佳实践

SoftSteel
SoftSteel 2026-02-11T16:04:05+08:00
0 0 0

引言:为何选择Composition API?

随着Vue 3的正式发布,官方在框架设计上进行了重大革新,其中最核心的变化之一就是引入了Composition API。它取代了以往以Options API为主的开发模式,为组件逻辑组织带来了前所未有的灵活性和可复用性。

在传统 Options API 中,组件的逻辑被分散在 datamethodscomputedwatch 等选项中,当组件复杂度上升时,同一业务逻辑可能跨越多个区域,难以维护。而 Composition API 通过函数式的方式将相关逻辑集中管理,实现了“按功能组织代码”(Organize by Feature),显著提升了代码可读性和可维护性。

此外,Composition API 还提供了更强大的类型支持(尤其在 TypeScript 中)、更好的逻辑复用能力(通过 Composables)、以及对响应式系统的深度控制。本文将带你从零开始,深入掌握 Vue 3 Composition API 的完整开发流程与最佳实践。

✅ 适用人群:熟悉 Vue 2 或有一定前端经验的开发者
✅ 技术栈要求:现代浏览器 + ES6+ 支持 + TypeScript(推荐)
✅ 推荐开发环境:Vite + Vue CLI + VS Code + Volar 插件

一、基础概念:什么是 Composition API?

1.1 定义与核心思想

Composition API 是一组基于函数的 API,允许你在组件内部使用函数来组织和封装逻辑。它的核心理念是:

将相关的逻辑聚合在一起,而不是按照选项分类。

例如,在一个用户信息表单组件中,你可能会有:

  • 表单数据绑定
  • 表单验证规则
  • 提交逻辑
  • 加载状态管理
  • 错误提示处理

在 Options API 中,这些逻辑会被拆分到不同的选项块中;而在 Composition API 中,你可以创建一个 useUserForm() 函数,将所有相关内容集中封装。

1.2 主要组成模块

模块 功能
setup() 组合逻辑的入口函数(Vue 3 核心入口)
ref() 声明响应式基本类型(如字符串、数字)
reactive() 声明响应式对象(深度监听)
computed() 定义计算属性
watch() 监听响应式数据变化
onMounted / onUnmounted 生命周期钩子
defineProps / defineEmits 用于 <script setup> 的声明语法糖

⚠️ 注意:<script setup> 是 Vue 3 推荐的新语法,它让 Composition API 更简洁高效。

二、核心工具详解:响应式系统与数据管理

2.1 ref():处理基本类型的响应式

ref() 用于包装一个原始值,使其变为响应式。虽然它返回的是一个对象(包含 .value 属性),但可以在模板中直接使用。

<!-- UserCard.vue -->
<script setup>
import { ref } from 'vue'

const username = ref('Alice')
const age = ref(25)

// 可以直接在模板中使用
// {{ username }} → "Alice"
// {{ age }} → 25

// 也可以修改
function updateName() {
  username.value = 'Bob'
}
</script>

<template>
  <div>
    <p>用户名: {{ username }}</p>
    <p>年龄: {{ age }}</p>
    <button @click="updateName">改名</button>
  </div>
</template>

✅ 最佳实践:

  • 使用 ref 包装布尔值、字符串、数字等基本类型。
  • 在模板中访问时无需 .value,Vue 会自动解包。
  • 避免在 ref 内部嵌套深层对象(应使用 reactive)。

2.2 reactive():创建响应式对象

reactive() 用于创建一个响应式的对象或数组,其内部属性都会被追踪。

<script setup>
import { reactive } from 'vue'

const user = reactive({
  name: 'Charlie',
  email: 'charlie@example.com',
  hobbies: ['reading', 'coding']
})

function addHobby(hobby) {
  user.hobbies.push(hobby)
}

function updateEmail(newEmail) {
  user.email = newEmail
}
</script>

<template>
  <div>
    <p>姓名: {{ user.name }}</p>
    <p>邮箱: {{ user.email }}</p>
    <ul>
      <li v-for="h in user.hobbies" :key="h">{{ h }}</li>
    </ul>
    <button @click="addHobby('gaming')">添加爱好</button>
  </div>
</template>

⚠️ 注意事项:

  • reactive() 不能用于基本类型(必须传入对象或数组)。
  • 不支持顶层 undefined/null 赋值。
  • 如果你需要将 reactive 对象作为 props 传递给子组件,需注意引用共享问题。

2.3 toRefs()toRef():解构响应式对象不丢失响应性

当你从 reactive() 创建的对象中解构属性时,响应性会丢失。解决方法是使用 toRefs()

<script setup>
import { reactive, toRefs } from 'vue'

const state = reactive({
  count: 0,
  message: 'Hello'
})

// 正确方式:使用 toRefs
const { count, message } = toRefs(state)

// ❌ 错误方式:直接解构
// const { count, message } = state // 响应性丢失!

function increment() {
  count.value += 1
}
</script>

📌 toRefs() 将响应式对象的所有属性转换为 ref,保持响应性。

如果只需要某个特定属性转成 ref,可用 toRef()

const countRef = toRef(state, 'count') // 等价于 ref(state.count)

✅ 最佳实践:

  • 在组合逻辑中需要解构响应式对象时,优先使用 toRefs
  • 适用于 setup() 返回对象给模板的情况。

三、组合逻辑封装:Composables 模式

3.1 什么是 Composable?

Composable 是一个命名规范化的函数,用于封装可复用的逻辑单元。通常以 useXXX 开头命名,如 useLocalStorageuseFetchuseCounter

这类函数可以被多个组件调用,实现跨组件逻辑复用。

3.2 实战案例:封装一个本地存储持久化工具

// composables/useLocalStorage.js
export function useLocalStorage(key, initialValue) {
  const storedValue = localStorage.getItem(key)
  const value = storedValue ? JSON.parse(storedValue) : initialValue

  const state = ref(value)

  // 监听变化并同步到 localStorage
  watch(
    state,
    (newVal) => {
      localStorage.setItem(key, JSON.stringify(newVal))
    },
    { deep: true }
  )

  return state
}

应用示例:

<!-- ProfileSettings.vue -->
<script setup>
import { useLocalStorage } from '@/composables/useLocalStorage'

const theme = useLocalStorage('user-theme', 'light')
const preferences = useLocalStorage('user-preferences', { notifications: true })

function toggleTheme() {
  theme.value = theme.value === 'light' ? 'dark' : 'light'
}
</script>

<template>
  <div>
    <p>当前主题: {{ theme }}</p>
    <button @click="toggleTheme">切换主题</button>
    <p>通知开启: {{ preferences.notifications }}</p>
  </div>
</template>

✅ 优势:

  • 逻辑独立,易于测试
  • 多组件共享相同行为
  • 类型安全(配合 TypeScript)

3.3 通用 Composable 模板结构

// composables/useXXX.js
import { ref, computed, watch } from 'vue'

export function useXXX(initialValue) {
  // 1. 声明状态
  const state = ref(initialValue)

  // 2. 计算属性(可选)
  const formattedValue = computed(() => {
    return state.value.toUpperCase()
  })

  // 3. 事件处理函数
  const update = (newValue) => {
    state.value = newValue
  }

  // 4. 监听器(可选)
  watch(state, (newVal, oldVal) => {
    console.log(`Changed from ${oldVal} to ${newVal}`)
  })

  // 5. 返回接口
  return {
    state,
    formattedValue,
    update
  }
}

💡 建议:每个 Composable 应该职责单一,避免过度耦合。

四、生命周期钩子:从 setup()onXxx

4.1 传统生命周期对比

Options API Composition API
beforeCreate 无(已在 setup() 执行前)
created 无(同上)
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeUnmount onBeforeUnmount
unmounted onUnmounted

⚠️ setup() 执行时机相当于 beforeCreate + created,所以无法再使用 this

4.2 详细示例:加载动画与资源清理

<script setup>
import { onMounted, onBeforeUnmount, ref } from 'vue'

const loading = ref(true)
const data = ref(null)

// 模拟异步请求
onMounted(async () => {
  try {
    const res = await fetch('/api/user')
    data.value = await res.json()
  } catch (err) {
    console.error('Failed to load:', err)
  } finally {
    loading.value = false
  }
})

// 清理定时器或其他副作用
let timerId
onBeforeUnmount(() => {
  if (timerId) {
    clearInterval(timerId)
  }
})

// 模拟后台任务
onMounted(() => {
  timerId = setInterval(() => {
    console.log('Keep alive...')
  }, 5000)
})
</script>

<template>
  <div v-if="loading">加载中...</div>
  <div v-else>{{ data }}</div>
</template>

✅ 最佳实践:

  • 所有副作用(网络请求、定时器、订阅事件)应在 onMounted 中启动。
  • onBeforeUnmount 中进行清理,防止内存泄漏。
  • 避免在 setup() 内部执行副作用。

五、事件通信:definePropsdefineEmits

5.1 <script setup> 中的声明语法糖

definePropsdefineEmits 是 Vue 3 提供的编译时宏,可在 <script setup> 中直接使用,无需手动定义。

<!-- TodoItem.vue -->
<script setup>
import { defineProps, defineEmits } from 'vue'

const props = defineProps({
  todo: {
    type: Object,
    required: true
  }
})

const emit = defineEmits(['remove', 'toggle'])

function handleRemove() {
  emit('remove', props.todo.id)
}

function handleToggle() {
  emit('toggle', props.todo.id)
}
</script>

<template>
  <li>
    <span :class="{ completed: todo.completed }">
      {{ todo.text }}
    </span>
    <button @click="handleToggle">Toggle</button>
    <button @click="handleRemove">Delete</button>
  </li>
</template>

5.2 与父组件通信

<!-- TodoList.vue -->
<script setup>
import { ref } from 'vue'
import TodoItem from './TodoItem.vue'

const todos = ref([
  { id: 1, text: '学习 Vue 3', completed: false },
  { id: 2, text: '写技术文章', completed: true }
])

function removeTodo(id) {
  todos.value = todos.value.filter(t => t.id !== id)
}

function toggleTodo(id) {
  todos.value = todos.value.map(t =>
    t.id === id ? { ...t, completed: !t.completed } : t
  )
}
</script>

<template>
  <ul>
    <TodoItem
      v-for="todo in todos"
      :key="todo.id"
      :todo="todo"
      @remove="removeTodo"
      @toggle="toggleTodo"
    />
  </ul>
</template>

✅ 最佳实践:

  • 使用 defineProps 明确声明输入类型(尤其在 TypeScript 项目中)。
  • 使用 defineEmits 声明事件名称,便于 IDE 提示与校验。
  • 避免使用 $emit,除非在非 <script setup> 场景。

六、高级特性:watchcomputed 的深度应用

6.1 watch:灵活的数据监听机制

6.1.1 监听单个响应式变量

const count = ref(0)

watch(count, (newVal, oldVal) => {
  console.log(`Count changed from ${oldVal} to ${newVal}`)
})

6.1.2 监听多个响应式变量

const firstName = ref('')
const lastName = ref('')

watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
  console.log(`Name updated: ${newFirst} ${newLast}`)
})

6.1.3 监听对象属性变化(深度监听)

const user = reactive({ name: '', age: 0 })

watch(
  () => user.name,
  (newName) => {
    console.log('Name changed:', newName)
  },
  { immediate: true } // 立即执行一次
)

immediate: true 用于立即触发一次回调。

6.1.4 监听复杂表达式

watch(
  () => user.age > 18,
  (isAdult) => {
    if (isAdult) {
      alert('已成年!')
    }
  }
)

6.1.5 使用 watchEffect —— 自动推导依赖

watchEffect 会自动收集依赖项,无需显式指定。

const count = ref(0)
const double = computed(() => count.value * 2)

watchEffect(() => {
  console.log(`Double is: ${double.value}`)
})
// → 每次 count 变化时自动打印

✅ 适合不需要精确控制依赖项的场景,如日志记录、副作用操作。

6.2 computed:高效的缓存计算属性

<script setup>
import { ref, computed } from 'vue'

const first = ref('')
const last = ref('')

// 缓存计算结果,仅当依赖改变时重新计算
const fullName = computed(() => {
  return `${first.value} ${last.value}`
})

// 也可设置 setter
const reversedName = computed({
  get() {
    return fullName.value.split('').reverse().join('')
  },
  set(value) {
    const parts = value.split(' ')
    first.value = parts[0] || ''
    last.value = parts[1] || ''
  }
})
</script>

<template>
  <input v-model="first" placeholder="名字" />
  <input v-model="last" placeholder="姓氏" />
  <p>全名: {{ fullName }}</p>
  <p>倒序: {{ reversedName }}</p>
</template>

✅ 优势:

  • 基于依赖自动更新,性能高。
  • 只有依赖变化时才重新计算。
  • 支持双向绑定(通过 set)。

七、复杂业务逻辑封装:构建可复用的表单管理器

7.1 场景描述

我们想要构建一个通用的表单管理 Composable,支持:

  • 表单数据绑定
  • 验证规则配置
  • 提交状态管理
  • 错误提示展示

7.2 实现代码

// composables/useForm.js
import { ref, computed } from 'vue'

export function useForm(initialValues, validationRules = {}) {
  const formData = ref({ ...initialValues })
  const errors = ref({})
  const isSubmitting = ref(false)
  const isSuccess = ref(false)

  // 验证函数
  const validate = () => {
    errors.value = {}
    let isValid = true

    Object.keys(validationRules).forEach((field) => {
      const rule = validationRules[field]
      const value = formData.value[field]

      if (rule.required && !value) {
        errors.value[field] = '此字段不能为空'
        isValid = false
      } else if (rule.pattern && !rule.pattern.test(value)) {
        errors.value[field] = rule.message || '格式不正确'
        isValid = false
      }
    })

    return isValid
  }

  // 提交表单
  const submit = async (onSubmit) => {
    if (!validate()) return

    isSubmitting.value = true
    isSuccess.value = false

    try {
      await onSubmit(formData.value)
      isSuccess.value = true
    } catch (err) {
      console.error('Submission failed:', err)
    } finally {
      isSubmitting.value = false
    }
  }

  // 重置表单
  const reset = () => {
    formData.value = { ...initialValues }
    errors.value = {}
    isSuccess.value = false
  }

  // 获取字段值
  const getField = (field) => formData.value[field]

  // 设置字段值
  const setField = (field, value) => {
    formData.value[field] = value
    // 清除该字段错误
    if (errors.value[field]) {
      delete errors.value[field]
    }
  }

  return {
    formData,
    errors,
    isSubmitting,
    isSuccess,
    validate,
    submit,
    reset,
    getField,
    setField
  }
}

7.3 使用示例

<!-- LoginForm.vue -->
<script setup>
import { useForm } from '@/composables/useForm'

const formConfig = {
  email: '',
  password: ''
}

const validationRules = {
  email: {
    required: true,
    pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
    message: '请输入有效的邮箱地址'
  },
  password: {
    required: true,
    min: 6
  }
}

const { formData, errors, isSubmitting, submit, reset } = useForm(formConfig, validationRules)

const handleSubmit = async (data) => {
  // 模拟提交
  await new Promise(resolve => setTimeout(resolve, 1000))
  console.log('Submitted:', data)
}

const handleLogin = () => {
  submit(handleSubmit)
}
</script>

<template>
  <form @submit.prevent="handleLogin">
    <div>
      <label>Email:</label>
      <input
        v-model="formData.email"
        type="email"
        :class="{ error: errors.email }"
      />
      <span v-if="errors.email" class="error-msg">{{ errors.email }}</span>
    </div>

    <div>
      <label>Password:</label>
      <input
        v-model="formData.password"
        type="password"
        :class="{ error: errors.password }"
      />
      <span v-if="errors.password" class="error-msg">{{ errors.password }}</span>
    </div>

    <button type="submit" :disabled="isSubmitting">
      {{ isSubmitting ? '登录中...' : '登录' }}
    </button>
    <button type="button" @click="reset">重置</button>
  </form>
</template>

<style scoped>
.error {
  border-color: red;
}
.error-msg {
  color: red;
  font-size: 12px;
}
</style>

✅ 优势总结:

  • 逻辑高度抽象,可复用于注册、编辑、设置等多种表单。
  • 支持动态规则配置。
  • 易于扩展(如加入 debounce、auto-save 等)。

八、TypeScript 集成:提升开发体验与安全性

8.1 启用 TypeScript 支持

在 Vite 项目中,确保 tsconfig.json 已正确配置:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "Node",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"]
  },
  "include": [
    "src/**/*"
  ]
}

8.2 类型化 Composable

// composables/useUserStore.ts
import { Ref, ref, computed } from 'vue'

interface User {
  id: number
  name: string
  email: string
}

export function useUserStore() {
  const users = ref<User[]>([])

  const addUser = (user: User) => {
    users.value.push(user)
  }

  const findUserById = (id: number): User | undefined => {
    return users.value.find(u => u.id === id)
  }

  const totalUsers = computed(() => users.value.length)

  return {
    users,
    addUser,
    findUserById,
    totalUsers
  }
}

8.3 与 defineProps 结合使用

// components/UserCard.vue
<script setup lang="ts">
import { defineProps } from 'vue'

interface Props {
  user: {
    id: number
    name: string
    avatar?: string
  }
}

const props = defineProps<Props>()
</script>

✅ 好处:

  • 编辑器智能提示
  • 编译时类型检查
  • 防止运行时错误

九、性能优化与最佳实践建议

9.1 避免不必要的响应式包裹

// ❌ 低效写法
const obj = reactive({ a: 1, b: 2 })
const x = ref(obj) // 多层嵌套导致性能下降

// ✅ 推荐写法
const obj = ref({ a: 1, b: 2 })

9.2 合理使用 computedwatch

  • computed 处理纯函数式计算。
  • watch 处理副作用(如网络请求、事件订阅)。
  • 避免在 computed 内部做异步操作。

9.3 使用 shallowRefshallowReactive 优化大对象

对于大型数据结构,若不需深层响应,可用浅层版本:

const largeData = shallowRef({ /* huge object */ })

9.4 懒加载 Composable

// 只在需要时导入
if (condition) {
  const { useAuth } = await import('@/composables/useAuth')
  const auth = useAuth()
}

9.5 命名规范统一

  • 所有 Composable 以 use 开头
  • 事件使用 camelCase
  • 避免使用 onXxx 作为变量名

十、结语:迈向现代化 Vue 3 开发

Vue 3 Composition API 并不仅仅是一个语法升级,它是架构思维的一次跃迁。通过将逻辑按功能组织,我们能够构建出更清晰、更易维护、更可复用的组件系统。

无论你是个人开发者还是团队协作,掌握 Composition API 都意味着:

  • 更好的代码组织
  • 更强的类型支持
  • 更高的开发效率
  • 更少的重构成本

🌟 终极建议:从现在开始,所有新项目都使用 <script setup> + Composition API + TypeScript,逐步迁移旧项目。

附录:常用 Composable 推荐清单

名称 功能
useLocalStorage 持久化存储
useFetch HTTP 请求封装
useDebounce 输入防抖
useClickOutside 点击外部关闭弹窗
useIntersectionObserver 懒加载/滚动检测
useWindowSize 监听窗口大小变化
useAsyncState 异步状态管理

📌 参考资料

本文共约 6,500 字,涵盖从基础到高级的完整开发流程与最佳实践,适合中高级前端开发者深入学习与参考。

相关推荐
广告位招租

相似文章

    评论 (0)

    0/2000