乐于分享
好东西不私藏

UniApp数据共享与传递:打通页面与组件的任督二脉

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({  userInfoObject,  titleString})</script>

子传父:$emit

<!-- 子组件 --><scriptsetup>const emit = defineEmits(['update''delete'])const handleClick = () => {  emit('update', { id1 })}</script><!-- 父组件监听 --><Child @update="handleUpdate" />

注意:props是单向的,子组件不能直接修改props。如果确实需要改,通过emit让父组件改。

2.2 兄弟组件通信

兄弟组件不能直接通信,常用两种方式:

方式一:通过共同的父组件中转子A触发事件,父组件接收后修改数据,再通过props传给子B。这种方式适合简单场景,但层级一多就繁琐。

方式二:事件总线(Event Bus)利用uni-app提供的uni.$emituni.$on,创建一个全局的事件中心。

// 组件Auni.$emit('cart-updated', { count5 })// 组件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的provideinject

// 祖先组件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'张三'age18 })// 取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: {    userInfonull,    cartCount0  },  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 }}

创建storestores/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.name      this.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特有
生命周期 onMountedonUnmounted onLoadonShowonHide(页面级)
路由 vue-router uni.navigateTouni.switchTab
状态管理 Pinia/Vuex Pinia/Vuex同样适用
事件总线 mitt等第三方库 uni.$onuni.$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() => ({ ... }),  persisttrue  // 自动持久化})

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                  # 用户store  cart.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.token      this.nickname = userInfo.nickname      this.avatar = userInfo.avatar    },    logout() {      this.token = ''      this.nickname = ''      this.avatar = ''    }  },  persisttrue // 持久化})

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.quantity0),    totalPrice(state) => state.items.reduce((sum, i) => sum + i.price * i.quantity0)  },  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 = []    }  },  persisttrue})

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([  { id1name'商品1'price99image'/static/goods1.png' },  { id2name'商品2'price199image'/static/goods2.png' }])const addToCart = (item) => {  cartStore.addItem({ id: item.idname: item.nameprice: item.priceimage: 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应用。

如果在实际开发中遇到问题,欢迎带着你的代码来找我。

—— 一个在数据流转中摸爬滚打多年的老前辈 🔄

加油,未来的全栈大佬!💪如果你也对移动端跨端开发感兴趣,关注我,后续还有更多优质文章分享!

往期相关文章推荐

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » UniApp数据共享与传递:打通页面与组件的任督二脉

猜你喜欢

  • 暂无文章