引言:为什么选择 Vue3 Composition API?

在上一篇中,我们使用 ref 创建了计数器的响应式数据。但你是否想过,当组件逻辑变得复杂时(比如一个商品详情页需要处理价格计算、库存判断、用户登录状态、优惠券选择等),传统的 Options API(datamethodscomputed 分散在不同选项)会让代码变得难以维护?

这就是 Vue3 Composition API 的用武之地。

Composition API 允许你按逻辑关注点组织代码,而不是被强制分割到不同的选项中。你可以将“价格计算”相关的所有逻辑放在一起,“用户状态”相关的逻辑放在一起,大大提升了代码的可读性和可复用性。

更重要的是,Composition API 天然支持逻辑复用。你可以把通用逻辑(如表单验证、网络请求、本地存储)封装成自定义Hook,然后在多个组件中自由组合使用,彻底告别 mixins 的命名冲突和难以追踪的问题。

今天,我将带你从零开始,掌握 Composition API 的核心概念,并通过一个电商商品详情页的完整案例,展示其在 UniApp 中的强大威力。


一、Composition API 核心概念速览

1.1 setup() 函数:Composition API 的入口

setup() 是组件的入口函数,在组件创建之前执行,this 指向 undefined

<script>
export default {
  setup() {
    // 所有 Composition API 逻辑写在这里
    return {
      // 返回的数据和方法,供模板使用
    }
  }
}
</script>
1.2 <script setup> 语法糖:更简洁的写法

这是 Vue3 推荐的语法,无需显式 return,直接在顶层声明的变量和函数都会自动暴露给模板。

<script setup>
// 这里写的代码相当于在 setup() 函数内
</script>

对比:

写法 优点 缺点
setup() 函数 兼容性好,逻辑清晰 需要手动 return
<script setup> 语法简洁,自动暴露,支持 TypeScript 更好 需要构建工具支持

推荐:新项目一律使用 <script setup>


二、核心响应式 API 实战

2.1 ref():创建基础类型响应式数据

用于字符串、数字、布尔值等。

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

// 创建响应式数据
const count = ref(0)
const name = ref('张三')
const isActive = ref(true)

// 访问值:.value
console.log(count.value) // 0

// 修改值
const increment = () => {
  count.value++
}

// 在模板中使用时,不需要 .value
</script>

<template>
  <view>
    <text>计数: {{ count }}</text> <!-- 自动解包 -->
    <button @click="increment">+</button>
  </view>
</template>

💡 注意:在 setup() 内部访问 ref 值必须用 .value,但在模板中会自动解包。

2.2 reactive():创建对象/数组响应式数据

用于复杂对象或数组。

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

const user = reactive({
  name: '李四',
  age: 25,
  hobbies: ['编程', '音乐']
})

// 直接修改,不需要 .value
const updateAge = () => {
  user.age += 1
}

// 添加新属性也是响应式的
user.email = 'lisi@example.com'
</script>

<template>
  <view>
    <text>姓名: {{ user.name }}</text>
    <text>年龄: {{ user.age }}</text>
    <button @click="updateAge">增加年龄</button>
  </view>
</template>

⚠️ 限制reactive() 不能用于基础类型,且替换整个对象会丢失响应性:

user = { name: '王五' } // ❌ 不再响应式!
2.3 computed():创建计算属性
<script setup>
import { ref, computed } from 'vue'

const price = ref(100)
const quantity = ref(2)
const taxRate = ref(0.1)

// 计算总价(含税)
const totalPrice = computed(() => {
  return price.value * quantity.value * (1 + taxRate.value)
})

// 可写计算属性
const discount = computed({
  get() {
    return price.value > 50 ? 0.9 : 1.0
  },
  set(newValue) {
    if (newValue < 0.8) {
      price.value = 50 // 设置最低价
    }
  }
})
</script>

<template>
  <view>
    <text>单价: {{ price }}</text>
    <text>数量: {{ quantity }}</text>
    <text>总价: {{ totalPrice }}</text>
    <text>折扣: {{ discount }}</text>
  </view>
</template>
2.4 watch()watchEffect():侦听数据变化
  • watch():侦听特定数据源,适合执行副作用(如数据持久化、API调用)。
  • watchEffect():立即执行传入的函数,并自动追踪其依赖,当依赖变化时重新执行。
<script setup>
import { ref, watch, watchEffect } from 'vue'

const searchQuery = ref('')
const results = ref([])

// 侦听 searchQuery 变化
watch(searchQuery, async (newVal, oldVal) => {
  if (newVal.length > 2) {
    // 模拟API调用
    results.value = await fetchSearchResults(newVal)
  }
})

// watchEffect:自动追踪依赖
const isLoggedIn = ref(false)
const userInfo = ref(null)

watchEffect(async () => {
  if (isLoggedIn.value) {
    userInfo.value = await fetchUserInfo()
  } else {
    userInfo.value = null
  }
})

// 清除监听(通常在onUnmounted中)
// const stop = watch(...)
// stop()
</script>

三、生命周期钩子:Composition API 版本

<script setup> 中,生命周期钩子需要从 vue 显式导入。

<script setup>
import { 
  onMounted, 
  onUpdated, 
  onUnmounted, 
  onBeforeMount 
} from 'vue'

onBeforeMount(() => {
  console.log('组件挂载前')
})

onMounted(() => {
  console.log('组件已挂载')
  // 常用于发起网络请求、初始化第三方库
})

onUpdated(() => {
  console.log('组件更新')
})

onUnmounted(() => {
  console.log('组件卸载')
  // 清理定时器、事件监听器
})
</script>

对应关系:

  • onBeforeMountbeforeMount
  • onMountedmounted
  • onBeforeUpdatebeforeUpdate
  • onUpdatedupdated
  • onBeforeUnmountbeforeDestroy
  • onUnmounteddestroyed
  • onErrorCapturederrorCaptured
  • onRenderTracked / onRenderTriggered:调试用

四、实战:电商商品详情页(Composition API 版)

让我们用 Composition API 重构一个复杂的商品详情页。

4.1 需求分析
  • 显示商品信息(名称、价格、图片、库存)
  • 规格选择(颜色、尺寸)
  • 实时计算总价
  • 判断库存是否充足
  • 用户登录状态影响购买按钮
  • 页面加载时获取商品数据
4.2 代码实现
<template>
  <view class="product-detail">
    <!-- 轮播图 -->
    <swiper class="banner" :autoplay="true" :interval="3000" :duration="500">
      <swiper-item v-for="(img, index) in product.images" :key="index">
        <image class="image" :src="img" mode="aspectFill"></image>
      </swiper-item>
    </swiper>

    <!-- 商品信息 -->
    <view class="info">
      <text class="title">{{ product.name }}</text>
      <text class="price">¥{{ finalPrice }}</text>
      <text class="stock" :class="{ 'low': stockStatus === 'low', 'out': stockStatus === 'out' }">
        库存:{{ product.stock }} 件
      </text>
    </view>

    <!-- 规格选择 -->
    <view class="specs">
      <view class="spec-item" v-for="spec in specs" :key="spec.type">
        <text class="label">{{ spec.type }}:</text>
        <view class="options">
          <text 
            v-for="option in spec.options" 
            :key="option.value"
            :class="['option', { active: selectedSpecs[spec.type] === option.value }]"
            @click="selectSpec(spec.type, option.value)"
          >
            {{ option.label }}
          </text>
        </view>
      </view>
    </view>

    <!-- 操作按钮 -->
    <view class="actions">
      <button 
        class="btn btn-cart" 
        :disabled="!canAddToCart"
        @click="addToCart"
      >
        加入购物车
      </button>
      <button 
        class="btn btn-buy" 
        :disabled="!canBuy"
        @click="buyNow"
      >
        立即购买
      </button>
    </view>
  </view>
</template>

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

// 模拟商品数据
const product = reactive({
  id: 1,
  name: '高端无线蓝牙耳机',
  price: 299,
  originalPrice: 399,
  images: [
    'https://example.com/headphone1.jpg',
    'https://example.com/headphone2.jpg'
  ],
  stock: 15,
  specs: {
    color: [
      { value: 'black', label: '黑色', extraPrice: 0 },
      { value: 'white', label: '白色', extraPrice: 20 }
    ],
    size: [
      { value: 'standard', label: '标准版', extraPrice: 0 },
      { value: 'pro', label: 'Pro版', extraPrice: 100 }
    ]
  }
})

// 用户状态
const isLoggedIn = ref(true) // 模拟登录状态

// 规格选择状态
const selectedSpecs = reactive({
  color: 'black',
  size: 'standard'
})

// 计算最终价格
const finalPrice = computed(() => {
  let basePrice = product.price
  let extra = 0
  
  // 计算规格附加价格
  Object.keys(selectedSpecs).forEach(key => {
    const options = product.specs[key]
    const selectedOption = options.find(opt => opt.value === selectedSpecs[key])
    if (selectedOption) {
      extra += selectedOption.extraPrice
    }
  })
  
  return basePrice + extra
})

// 库存状态
const stockStatus = computed(() => {
  if (product.stock <= 0) return 'out'
  if (product.stock < 10) return 'low'
  return 'normal'
})

// 是否可以加入购物车
const canAddToCart = computed(() => {
  return product.stock > 0 && isLoggedIn.value
})

// 是否可以立即购买
const canBuy = computed(() => {
  return canAddToCart.value
})

// 选择规格
const selectSpec = (type, value) => {
  selectedSpecs[type] = value
  console.log(`选择了 ${type}: ${value}`)
}

// 加入购物车
const addToCart = () => {
  uni.showToast({ title: '已加入购物车', icon: 'success' })
  // 实际开发中调用API
}

// 立即购买
const buyNow = () => {
  uni.navigateTo({ url: '/pages/order/confirm' })
}

// 模拟页面加载
onMounted(async () => {
  uni.showLoading({ title: '加载中...' })
  // 模拟网络延迟
  await new Promise(resolve => setTimeout(resolve, 1000))
  uni.hideLoading()
  console.log('商品数据加载完成')
})

// 侦听规格变化
watch(selectedSpecs, (newVal, oldVal) => {
  console.log('规格已变更:', newVal)
}, { deep: true })

// 如果商品ID变化(如从列表页跳转),重新加载数据
// const route = useRoute() // 需要 vue-router-uni
// watch(() => route.params.id, loadProductData)
</script>

<style lang="scss" scoped>
$product-bg: #f8f8f8;
$primary-color: #007AFF;
$text-dark: #333;
$text-gray: #666;

.product-detail {
  min-height: 100vh;
  background-color: $product-bg;
}

.banner {
  height: 750rpx;
  .image {
    width: 100%;
    height: 100%;
  }
}

.info {
  padding: 20rpx 30rpx;
  background: white;
  border-bottom: 1rpx solid #eee;
  .title {
    font-size: 36rpx;
    font-weight: bold;
    color: $text-dark;
    display: block;
    margin-bottom: 20rpx;
  }
  .price {
    font-size: 42rpx;
    color: #e60000;
    font-weight: bold;
  }
  .stock {
    font-size: 28rpx;
    color: $text-gray;
    margin-left: 20rpx;
    &.low { color: #faad14; }
    &.out { color: #f5222d; }
  }
}

.specs {
  padding: 30rpx;
  background: white;
  margin-top: 20rpx;
  .spec-item {
    margin-bottom: 30rpx;
    .label {
      font-size: 30rpx;
      color: $text-dark;
      margin-right: 20rpx;
    }
    .options {
      display: flex;
      flex-wrap: wrap;
      gap: 20rpx;
      .option {
        padding: 15rpx 30rpx;
        border: 1rpx solid #ddd;
        border-radius: 8rpx;
        font-size: 28rpx;
        color: $text-gray;
        &.active {
          background: $primary-color;
          color: white;
          border-color: $primary-color;
        }
      }
    }
  }
}

.actions {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  display: flex;
  background: white;
  border-top: 1rpx solid #eee;
  .btn {
    flex: 1;
    height: 100rpx;
    line-height: 100rpx;
    text-align: center;
    font-size: 32rpx;
    &.btn-cart {
      background: #fff;
      color: $text-dark;
      border-right: 1rpx solid #eee;
    }
    &.btn-buy {
      background: $primary-color;
      color: white;
    }
    &:disabled {
      opacity: 0.6;
    }
  }
}
</style>
4.3 代码亮点解析
  1. 逻辑分组清晰

    • 商品数据 (product)
    • 用户状态 (isLoggedIn)
    • 规格选择 (selectedSpecs, selectSpec)
    • 价格计算 (finalPrice)
    • 库存判断 (stockStatus)
    • 按钮状态 (canAddToCart, canBuy)
    • 生命周期与副作用 (onMounted, watch)
  2. 响应式系统

    • 使用 reactive 管理复杂对象。
    • 使用 ref 管理简单状态。
    • computed 自动更新派生数据。
  3. 交互反馈

    • 按钮根据状态禁用。
    • 库存状态用颜色区分。
    • 点击规格高亮显示。
  4. 可维护性

    • 如果未来要添加“优惠券”逻辑,只需新增一个 coupon ref 和相关计算逻辑,不会干扰现有代码。

五、自定义 Hook:逻辑复用的艺术

Composition API 最大的优势是逻辑复用。我们可以把通用逻辑封装成自定义 Hook。

5.1 示例:useStorage - 本地存储Hook
// composables/useStorage.js
import { ref, watch } from 'vue'

export function useStorage(key, initialValue) {
  // 从本地获取
  const data = uni.getStorageSync(key)
  const storedValue = data ? JSON.parse(data) : initialValue
  
  const value = ref(storedValue)
  
  // 监听变化,同步到本地存储
  watch(value, (newValue) => {
    uni.setStorageSync(key, JSON.stringify(newValue))
  }, { deep: true })
  
  return value
}

使用:

<script setup>
import { useStorage } from '@/composables/useStorage'

// 持久化用户偏好
const theme = useStorage('theme', 'light')
const language = useStorage('language', 'zh-CN')

const toggleTheme = () => {
  theme.value = theme.value === 'light' ? 'dark' : 'light'
}
</script>
5.2 示例:useRequest - 网络请求Hook
// composables/useRequest.js
import { ref } from 'vue'

export function useRequest(apiFunc) {
  const data = ref(null)
  const loading = ref(false)
  const error = ref(null)
  
  const run = async (...args) => {
    loading.value = true
    error.value = null
    try {
      const res = await apiFunc(...args)
      data.value = res
      return res
    } catch (err) {
      error.value = err.message
      throw err
    } finally {
      loading.value = false
    }
  }
  
  return { data, loading, error, run }
}

使用:

<script setup>
import { useRequest } from '@/composables/useRequest'
import { getProductDetail } from '@/api/product'

const { data: product, loading, error, run: loadProduct } = useRequest(getProductDetail)

onMounted(() => {
  loadProduct(123) // 加载ID为123的商品
})
</script>

六、总结与预告

在本篇中,我们全面掌握了 Vue3 Composition API 在 UniApp 中的应用:

✅ 熟练使用 refreactivecomputedwatch 等核心API
✅ 理解 <script setup> 语法糖的优势
✅ 掌握 Composition API 版本的生命周期钩子
✅ 通过电商详情页案例实践复杂逻辑组织
✅ 学会创建自定义Hook实现逻辑复用

你已经具备了用现代化方式开发 UniApp 组件的能力。

下一篇文章预告:《模板语法精讲:插值、指令、事件绑定与双向数据流》

我们将深入探讨 UniApp 模板的每一个细节:

  • 插值表达式 {{ }} 的高级用法
  • 条件渲染 v-if vs v-show 的性能差异
  • 列表渲染 v-forkey 为何如此重要
  • 事件修饰符 .stop.prevent 的实际应用场景
  • v-model 如何实现双向绑定及自定义组件支持

参考资料

互动时间:你打算把哪个业务逻辑封装成自定义Hook?欢迎在评论区分享你的想法!

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐