第三篇:Vue3 Composition API在UniApp中的实战应用
引言:为什么选择 Vue3 Composition API?
在上一篇中,我们使用 ref 创建了计数器的响应式数据。但你是否想过,当组件逻辑变得复杂时(比如一个商品详情页需要处理价格计算、库存判断、用户登录状态、优惠券选择等),传统的 Options API(data、methods、computed 分散在不同选项)会让代码变得难以维护?
这就是 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>
对应关系:
onBeforeMount→beforeMountonMounted→mountedonBeforeUpdate→beforeUpdateonUpdated→updatedonBeforeUnmount→beforeDestroyonUnmounted→destroyedonErrorCaptured→errorCapturedonRenderTracked/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 代码亮点解析
-
逻辑分组清晰:
- 商品数据 (
product) - 用户状态 (
isLoggedIn) - 规格选择 (
selectedSpecs,selectSpec) - 价格计算 (
finalPrice) - 库存判断 (
stockStatus) - 按钮状态 (
canAddToCart,canBuy) - 生命周期与副作用 (
onMounted,watch)
- 商品数据 (
-
响应式系统:
- 使用
reactive管理复杂对象。 - 使用
ref管理简单状态。 computed自动更新派生数据。
- 使用
-
交互反馈:
- 按钮根据状态禁用。
- 库存状态用颜色区分。
- 点击规格高亮显示。
-
可维护性:
- 如果未来要添加“优惠券”逻辑,只需新增一个
couponref 和相关计算逻辑,不会干扰现有代码。
- 如果未来要添加“优惠券”逻辑,只需新增一个
五、自定义 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 中的应用:
✅ 熟练使用 ref、reactive、computed、watch 等核心API
✅ 理解 <script setup> 语法糖的优势
✅ 掌握 Composition API 版本的生命周期钩子
✅ 通过电商详情页案例实践复杂逻辑组织
✅ 学会创建自定义Hook实现逻辑复用
你已经具备了用现代化方式开发 UniApp 组件的能力。
下一篇文章预告:《模板语法精讲:插值、指令、事件绑定与双向数据流》
我们将深入探讨 UniApp 模板的每一个细节:
- 插值表达式
{{ }}的高级用法 - 条件渲染
v-ifvsv-show的性能差异 - 列表渲染
v-for的key为何如此重要 - 事件修饰符
.stop、.prevent的实际应用场景 v-model如何实现双向绑定及自定义组件支持
参考资料
互动时间:你打算把哪个业务逻辑封装成自定义Hook?欢迎在评论区分享你的想法!
更多推荐


所有评论(0)