UniApp数据共享与传递:打通页面与组件的任督二脉
书接上篇,我们今天来聊聊UniApp开发中一个核心且容易混乱的话题:数据共享与传递。支持跨平台(H5/小程序/App),附带实现思路和所有源码示例,以及常见避坑指南和开发实践。
此系列文章将带领你从移动端跨平台开发入门到精通,如果你也喜欢关注APP、小程序、公众号、H5等等应用的开发,可以持续关注后续更新,避免错过宝贵的知识分享。
你可能经历过这样的场景:
-
登录成功后,怎么让所有页面都知道用户信息?
-
从列表页跳到详情页,返回时怎么刷新列表?
-
购物车页面修改了商品数量,底部tabBar的角标怎么更新?
-
两个毫无关联的组件,怎么让它们通信?
在UniApp中,因为要同时支持H5、小程序、App,数据传递的方式比纯Vue3要复杂一些。别慌,今天我就带你系统地梳理所有方式,分析它们的适用场景、跨平台坑点,最后用一个完整的示例把知识点串起来。
一、数据共享的需求与场景
先想想我们为什么要共享数据:
-
用户状态:登录信息、token需要在多个页面使用。
-
购物车数据:商品列表页添加商品,购物车页显示,底部tabBar角标更新。
-
设置偏好:主题、语言设置影响全局。
-
实时消息:未读消息数、通知需要在各个页面显示。
这些场景的共同点是:数据一变,多处要跟着变。怎么实现?下面我按通信范围分类讲解。
二、组件间的数据传递
2.1 父子组件通信(最基础)
父传子:props
<!-- 父组件 --><Child:user-info="user":title="pageTitle" /><!-- 子组件 --><scriptsetup>const props = defineProps({userInfo: Object,title: String})</script>
子传父:$emit
<!-- 子组件 --><scriptsetup>const emit = defineEmits(['update', 'delete'])const handleClick = () => {emit('update', { id: 1 })}</script><!-- 父组件监听 --><Child @update="handleUpdate" />
注意:props是单向的,子组件不能直接修改props。如果确实需要改,通过emit让父组件改。
2.2 兄弟组件通信
兄弟组件不能直接通信,常用两种方式:
方式一:通过共同的父组件中转子A触发事件,父组件接收后修改数据,再通过props传给子B。这种方式适合简单场景,但层级一多就繁琐。
方式二:事件总线(Event Bus)利用uni-app提供的uni.$emit和uni.$on,创建一个全局的事件中心。
// 组件Auni.$emit('cart-updated', { count: 5 })// 组件B(通常在onLoad中监听)onLoad() {uni.$on('cart-updated', (data) => {this.cartCount = data.count})}// 组件卸载时必须取消监听,否则内存泄漏onUnload() {uni.$off('cart-updated')}
跨平台注意:uni.$on/$off/$emit在H5、小程序、App都可用,但小程序中页面卸载后监听依然存在,所以一定要在onUnload中取消。
2.3 跨级组件通信(provide/inject)
如果父组件和孙组件(甚至更深)需要通信,一层层传props会很繁琐。这时可以用Vue3的provide和inject。
// 祖先组件import { provide, ref } from 'vue'const user = ref({ name: '张三' })provide('user', user) // 传递响应式对象// 后代组件import { inject } from 'vue'const user = inject('user')
注意:
-
provide默认不是响应式的,如果要响应式,需传递ref或reactive。 -
provide/inject只能用于组件树,不能跨页面。页面跳转后,新页面无法inject之前页面的provide。
三、页面间的数据传递
3.1 URL传参(最常用)
// 发送页uni.navigateTo({url: '/pages/detail/detail?id=123&name=' + encodeURIComponent('张三')})// 接收页(onLoad中接收)onLoad(options) {console.log(options.id, options.name) // 注意:参数都是字符串}
坑点:
-
参数自动
decodeURIComponent,但特殊字符(&、=)需要提前编码。 -
长度限制:小程序约2KB,App和H5更长但不宜过大。
-
只能传字符串,对象需
JSON.stringify再编码。
3.2 本地存储(Storage)
适合需要持久化或跨页面共享的数据。
// 存uni.setStorageSync('user', { name: '张三', age: 18 })// 取const user = uni.getStorageSync('user')
跨平台限制:
-
微信小程序单个key上限1MB,总容量10MB。
-
App无限制,但要注意同步操作可能阻塞UI。
-
H5 localStorage 约5-10MB。
最佳实践:敏感数据(如token)存Storage,大数据(如购物车)可存Pinia+定期同步Storage。
3.3 全局数据(globalData)
在App.vue中定义globalData,可以在所有页面通过getApp().globalData访问。
// App.vue<script>export default {globalData: {userInfo: null,cartCount: 0},onLaunch() {// 初始化}}</script>// 在页面中const app = getApp()app.globalData.userInfo = { name: '张三' }
优点:简单直接,无需额外库。缺点:不是响应式的,数据变化不会自动更新UI。适合存放不常变的数据(如系统配置)。
3.4 状态管理(Pinia/Vuex)
这是最推荐的方式,尤其是大型应用。Pinia是Vue官方推荐的状态管理库,完美支持Vue3和UniApp。
安装Pinia:
npm install pinia
在main.js中注册:
import { createSSRApp } from 'vue'import App from './App.vue'import { createPinia } from 'pinia'export function createApp() {const app = createSSRApp(App)const pinia = createPinia()app.use(pinia)return { app }}
创建store(stores/user.js):
import { defineStore } from 'pinia'export const useUserStore = defineStore('user', {state: () => ({name: '',token: ''}),getters: {isLogin: (state) => !!state.token},actions: {login(userData) {this.name = userData.namethis.token = userData.token},logout() {this.name = ''this.token = ''}}})
在页面中使用:
import { useUserStore } from '@/stores/user'const userStore = useUserStore()console.log(userStore.name)userStore.login({ name: '张三', token: 'xxx' })
优点:
-
响应式,一处修改,所有使用的地方自动更新。
-
支持Vue Devtools调试。
-
跨页面、跨组件完美共享。
注意:Pinia数据默认存在内存中,刷新页面会丢失。如需持久化,可配合pinia-plugin-persistedstate插件。
四、区分UniApp原生操作与Vue3用法
很多Vue3开发者转到UniApp时,容易混淆哪些是Vue的能力,哪些是UniApp的扩展。
| 类别 | Vue3标准 | UniApp特有 |
|---|---|---|
| 生命周期 | onMounted, onUnmounted |
onLoad, onShow, onHide(页面级) |
| 路由 | vue-router |
uni.navigateTo, uni.switchTab等 |
| 状态管理 | Pinia/Vuex | Pinia/Vuex同样适用 |
| 事件总线 | mitt等第三方库 | uni.$on, uni.$emit内置 |
| 获取实例 | getCurrentInstance() |
getCurrentInstance()可用,但某些平台有差异 |
| 模板语法 | 完全相同 | 完全相同 |
核心区别:
-
页面生命周期:Vue3组件用
onMounted,但UniApp页面还有onLoad(仅页面加载一次)、onShow(每次显示触发)等。 -
路由:不能用
vue-router,必须用UniApp的导航API。 -
全局变量:Vue3中挂在
app.config.globalProperties的属性,在微信小程序中可能无效,推荐用Pinia或globalData。
五、难点与注意点(血泪经验)
5.1 事件总线监听未销毁导致多次触发
这是最常见的内存泄漏。一定要在onUnload(页面)或onUnmounted(组件)中取消监听。
onLoad() {uni.$on('event', this.handler)}onUnload() {uni.$off('event', this.handler) // 必须指定相同函数}
如果使用匿名函数,无法取消,所以尽量用命名函数。
5.2 页面返回后数据不刷新
现象:从A页跳到B页,修改数据后返回A页,A页还是旧数据。
原因:A页的数据没有在onShow中刷新。
解决方案:把数据刷新的逻辑写在onShow里,而不是onLoad。
5.3 Storage存对象忘记序列化
// 错误uni.setStorageSync('user', { name: '张三' }) // 某些平台会隐式转换,但最好显式序列化// 正确uni.setStorageSync('user', JSON.stringify({ name: '张三' }))const user = JSON.parse(uni.getStorageSync('user') || '{}')
5.4 Pinia数据刷新丢失
默认Pinia数据只在内存中,刷新页面就没了。如果需要持久化,安装插件:
npm install pinia-plugin-persistedstate
在main.js中:
import { createPinia } from 'pinia'import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'const pinia = createPinia()pinia.use(piniaPluginPersistedstate)
在store中启用:
export const useUserStore = defineStore('user', {state: () => ({ ... }),persist: true // 自动持久化})
5.5 小程序中无法使用Vue.prototype挂载全局方法
在Vue3中,挂载全局方法用app.config.globalProperties,但微信小程序不支持。推荐改用uni.$on/$emit或Pinia的action。
5.6 页面间传对象过大
URL传参有长度限制,对象太大时用Storage或Pinia。
六、跨平台相关问题与解决方案
6.1 小程序页面栈限制
小程序最多10层,超过后navigateTo会失败。解决方案:适时用redirectTo替换。
6.2 H5和App的Storage容量较大,小程序较小
设计时考虑兼容,大数据建议分页或只存关键信息。
6.3 小程序不支持动态设置globalData
globalData在App中定义,小程序也可用,但非响应式。推荐Pinia。
6.4 H5跨域问题
开发时用proxy配置,生产环境后端需配置CORS。
七、完整代码示例:电商购物车数据共享
下面我们通过一个简化的电商示例,演示从登录到购物车的完整数据共享流程。
7.1 项目结构
pages/login/login.vue # 登录页index/index.vue # 商品列表页cart/cart.vue # 购物车页stores/user.js # 用户storecart.js # 购物车storeApp.vuemain.js
7.2 安装Pinia并配置
main.js(关键部分):
import { createSSRApp } from 'vue'import App from './App.vue'import { createPinia } from 'pinia'import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'export function createApp() {const app = createSSRApp(App)const pinia = createPinia()pinia.use(piniaPluginPersistedstate)app.use(pinia)return { app }}
7.3 用户store(stores/user.js)
import { defineStore } from 'pinia'export const useUserStore = defineStore('user', {state: () => ({token: '',nickname: '',avatar: ''}),getters: {isLogin: (state) => !!state.token},actions: {login(userInfo) {this.token = userInfo.tokenthis.nickname = userInfo.nicknamethis.avatar = userInfo.avatar},logout() {this.token = ''this.nickname = ''this.avatar = ''}},persist: true // 持久化})
7.4 购物车store(stores/cart.js)
import { defineStore } from 'pinia'export const useCartStore = defineStore('cart', {state: () => ({items: [] // [{ id, name, price, quantity, image }]}),getters: {totalCount: (state) => state.items.reduce((sum, i) => sum + i.quantity, 0),totalPrice: (state) => state.items.reduce((sum, i) => sum + i.price * i.quantity, 0)},actions: {addItem(item) {const existing = this.items.find(i => i.id === item.id)if (existing) {existing.quantity += item.quantity || 1} else {this.items.push({ ...item, quantity: item.quantity || 1 })}},removeItem(id) {this.items = this.items.filter(i => i.id !== id)},updateQuantity(id, quantity) {const item = this.items.find(i => i.id === id)if (item) item.quantity = quantity},clearCart() {this.items = []}},persist: true})
7.5 登录页(pages/login/login.vue)
<template><viewclass="login"><button @click="mockLogin">模拟登录</button></view></template><scriptsetup>import { useUserStore } from '@/stores/user'const userStore = useUserStore()const mockLogin = () => {userStore.login({token: 'fake_token',nickname: '张三',avatar: '/static/avatar.png'})uni.showToast({ title: '登录成功', icon: 'success' })setTimeout(() => {uni.switchTab({ url: '/pages/index/index' })}, 1500)}</script>
7.6 商品列表页(pages/index/index.vue)
<template><view><viewv-for="item in goods":key="item.id"class="goods-item"><image:src="item.image" /><text>{{ item.name }} ¥{{ item.price }}</text><button @click="addToCart(item)">加入购物车</button></view><viewclass="cart-badge" @click="goToCart">购物车({{ cartStore.totalCount }})</view></view></template><scriptsetup>import { ref } from 'vue'import { useCartStore } from '@/stores/cart'const cartStore = useCartStore()const goods = ref([{ id: 1, name: '商品1', price: 99, image: '/static/goods1.png' },{ id: 2, name: '商品2', price: 199, image: '/static/goods2.png' }])const addToCart = (item) => {cartStore.addItem({ id: item.id, name: item.name, price: item.price, image: item.image })uni.showToast({ title: '已加入购物车', icon: 'success' })}const goToCart = () => {uni.switchTab({ url: '/pages/cart/cart' })}</script>
7.7 购物车页(pages/cart/cart.vue)
<template><view><viewv-for="item in cartStore.items":key="item.id"class="cart-item"><image:src="item.image" /><text>{{ item.name }} ¥{{ item.price }} x {{ item.quantity }}</text><button @click="removeItem(item.id)">删除</button></view><view>总计:¥{{ cartStore.totalPrice }}</view></view></template><scriptsetup>import { useCartStore } from '@/stores/cart'const cartStore = useCartStore()const removeItem = (id) => {cartStore.removeItem(id)}</script>
7.8 事件总线示例:返回刷新列表
假设在详情页修改了商品后返回列表页,需要刷新列表。
列表页(index.vue):
onLoad() {uni.$on('goods-updated', this.loadGoods)}onUnload() {uni.$off('goods-updated', this.loadGoods)}
详情页(detail.vue)修改后:
uni.$emit('goods-updated')uni.navigateBack()
7.9 provide/inject示例:主题色传递
App.vue:
import { provide, ref } from 'vue'const themeColor = ref('#ff5500')provide('themeColor', themeColor)
任意子组件:
import { inject } from 'vue'const themeColor = inject('themeColor')
八、总结与选择建议
通过今天的学习,我们梳理了UniApp中各种数据共享方式。现在你面对一个需求,应该能快速选择最合适的方法:
| 场景 | 推荐方案 | 备选方案 |
|---|---|---|
| 父子组件 | props + emit | – |
| 兄弟组件 | 父组件中转 或 事件总线 | Pinia |
| 跨级组件 | provide/inject | Pinia |
| 同页面内频繁通信 | Pinia | 事件总线 |
| 跨页面共享状态 | Pinia(持久化) | Storage + 手动同步 |
| 一次性传参 | URL传参 | Storage |
| 全局配置 | Pinia | globalData |
最后送你一句话:数据共享是应用的血液,选对方式能让代码更优雅、维护更轻松。希望这篇教程能帮你理清思路,写出更健壮的UniApp应用。
如果在实际开发中遇到问题,欢迎带着你的代码来找我。
—— 一个在数据流转中摸爬滚打多年的老前辈 🔄
加油,未来的全栈大佬!💪如果你也对移动端跨端开发感兴趣,关注我,后续还有更多优质文章分享!


往期相关文章推荐
夜雨聆风