第2章 2.3 全局数据:globalData + Vuex/Pinia

🎯 开场:先讲个血泪故事

你有没有遇到过这种情况——

小明的购物车在 A 页面加了一件衣服,切到 B 页面看了一眼商品详情,再切回来,购物车居然空了?或者在列表页点了个收藏,回到首页发现没同步过去

这就是「页面之间数据不共享」的问题。

上一章我们学了 url 传参和事件总线,它们解决的是「页面之间通信」的问题。但如果你有这样一个需求:

  • 用户的登录状态 → 所有页面都要能读到
  • 购物车商品 → 多个页面都要能改
  • 主题设置(白天/黑夜)→ 多个页面都要能切换

这时候 url 传参不够用,事件总线也繁琐——你需要的是一个所有页面都能随时访问的「全局数据仓库」

学完这一章,你就能手写到一套「加入购物车 → 所有页面实时更新」的完整方案。


🧱 基础:3种武器对比

globalData:最简单,但数据会「断电」

uniapp 提供了一个 getApp() 函数,能拿到小程序实例,上面有\n\nSimple tech illustration expla\n\nAI comic creation scene, creat\n\n个 globalData 对象,就是全局数据。

生活类比:globalData 就像你家门口的快递柜。所有人(页面)都能来取快递(读数据),也能往里放快递(写数据)。但注意——如果小程序被关掉(切后台/重启),快递柜会被清空,数据就丢了。

// App.vue 中定义全局数据
const app = getApp()
app.globalData.userInfo = { name: '小明', level: 3 }
app.globalData.cartCount = 0
// 任意页面.js 中读取
const app = getApp()
console.log(app.globalData.userInfo.name)  // 输出: 小明
console.log(app.globalData.cartCount)       // 输出: 0

为什么用 globalData
- 5 行代码就能上手
- 小程序官方自带,无需安装
- 适合数据量少、实时性要求不高的场景

缺点
- 没有响应式,更新后页面不会自动刷新
- 数据会丢失(小程序关闭就没了)
- 数据多了不好管理

Vuex:数据永不丢失,但配置繁琐

Vuex 是 Vue 生态的状态管理库,uniapp 支持 Vue2 用 Vuex 3,支持 Vue3 用 Vuex 4。

生活类比:Vuex 像国家中央银行。每个省(页面)都能把钱存进去、取出来,但所有的交易记录都永久保存,而且任何一笔存取操作,所有部门都能立即收到通知

核心概念有 4 个(记住这 4 个词就行):

概念 作用 类比
State 存放数据的仓库 银行的保险箱
Mutations 唯一能修改 State 的方式 密码锁,只有它能开门
Actions 处理异步操作(如请求API) 银行柜员,可以做复杂业务
Getters 从 State 派生出新数据 自动利息计算器

先来看最简用法(Vue2 + Vuex 3):

// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const store = new Vuex.Store({
// ★ State:放数据
state: {
userInfo: null,
cartCount: 0,
cartItems: []
},

// ★ Mutations:同步修改数据(唯一合法修改方式)
mutations: {
setUser(state, user) {
  state.userInfo = user
},
addToCart(state, item) {
  state.cartItems.push(item)
  state.cartCount = state.cartItems.length
},
clearCart(state) {
  state.cartItems = []
  state.cartCount = 0
}
},

// ★ Actions:处理异步/复杂逻辑
actions: {
login({ commit }, { username, password }) {
  // 模拟登录API
  return new Promise((resolve) => {
    setTimeout(() => {
      commit('setUser', { name: username, level: 1 })
      resolve()
    }, 500)
  })
}
},

// ★ Getters:派生数据
getters: {
isLoggedIn: state => state.userInfo !== null,
cartTotal: state => state.cartItems.reduce((sum, item) => sum + item.price, 0)
}
})

export default store

main.js 中注入:

import Vue from 'vue'
import App from './App'
import store from './store'

Vue.config.productionTip = false
Vue.prototype.$store = store  // 关键:把 store 挂到 Vue 原型上

const app = new Vue({
store,
...App
})
app.$mount()

在页面中使用:

// pages/cart/cart.vue
export default {
computed: {
// 读取 state
userInfo() {
  return this.$store.state.userInfo
},
cartCount() {
  return this.$store.state.cartCount
},
cartItems() {
  return this.$store.state.cartItems
},
// 读取 getter
isLoggedIn() {
  return this.$store.getters.isLoggedIn
},
cartTotal() {
  return this.$store.getters.cartTotal
}
},
methods: {
addItem(item) {
  // 调用 mutation 修改数据
  this.$store.commit('addToCart', item)
},
async login() {
  // 调用 action(异步)
  await this.$store.dispatch('login', {
    username: 'xiaoming',
    password: '123456'
  })
}
}
}

模板中直接用

<template>
<view>
<text v-if="isLoggedIn">{{ userInfo.name }},等级 {{ userInfo.level }}</text>
<text v-else>请先登录</text>

<view>购物车共 {{ cartCount }} 件,合计 {{ cartTotal }} 元</view>

<button @click="addItem({ name: 'T恤', price: 99 })">加一件T恤</button>
<button @click="login">登录</button>
</view>
</template>

Pinia:Vuex 的升级版,更简单

Vuex 4 之后官方又在推 Pinia,它比 Vuex 更轻量,API 更直觉,而且支持 Vue 3 的组合式 API

生活类比:Pinia 就像一个智能快递柜,比 Vuex 这个银行更轻便,支持扫码开箱(像 ref 一样简单),还不用记那些拗口的 Mutations/Actions 区别。

安装 Pinia(如果用 HBuilderX 创建的 Vue3 项目,已自带):

npm install pinia

store/cart.js 定义一个购物车 store:

import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCartStore = defineStore('cart', () => {
// ★ State(用 ref)
const items = ref([])
const userInfo = ref(null)

// ★ Getters(用 computed)
const count = computed(() => items.value.length)
const totalPrice = computed(() => 
items.value.reduce((sum, item) => sum + item.price, 0)
)
const isLoggedIn = computed(() => userInfo.value !== null)

// ★ Actions(直接写函数)
function addItem(item) {
items.value.push(item)
}

function removeItem(index) {
items.value.splice(index, 1)
}

function clearCart() {
items.value = []
}

async function login(username, password) {
// 模拟登录
return new Promise((resolve) => {
  setTimeout(() => {
    userInfo.value = { name: username, level: 1 }
    resolve()
  }, 500)
})
}

// 必须 return 才能在外面用
return {
items,
userInfo,
count,
totalPrice,
isLoggedIn,
addItem,
removeItem,
clearCart,
login
}
})

main.js 中注册:

import { createSSRApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'

export function createApp() {
const app = createApp(App)
app.use(createPinia())
return { app }
}

在页面中使用:

<!-- pages/cart/cart.vue -->
<template>
<view class="container">
<text v-if="cart.isLoggedIn">
  欢迎,{{ cart.userInfo.name }}
</text>
<text v-else>未登录</text>

<view class="info">
  <text>购物车:{{ cart.count }} 件</text>
  <text>合计:{{ cart.totalPrice }} 元</text>
</view>

<button @click="addItem({ name: '卫衣', price: 199 })">加件卫衣</button>
<button @click="doLogin">登录</button>
</view>
</template>

<script setup>
import { useCartStore } from '@/store/cart'

// ★ 一行代码拿到 store(响应式的!)
const cart = useCartStore()

function addItem(item) {
cart.addItem(item)  // 直接调用,不用 commit 了
}

async function doLogin() {
await cart.login('xiaoming', '123456')
}
</script>

为什么 Pinia 比 Vuex 香
- 不用记 Mutations/Actions 区别:直接写函数改数据
- 原生支持 Vue 3 的响应式refreactive → 自动响应
- 自动补全友好:IDE 能提示 store 里有什么
- 去掉了 Vuex 的模块(module)概念:虽然更灵活了,但小项目不需要


🔥 实战:3个递进项目

项目1:全球数据晴雨表(5分钟)

目标:用 globalData 做一个实时更新的「温度计」,所有页面共享。

// App.vue
export default {
onLaunch() {
const app = getApp()
// 初始化全局温度数据
if (!app.globalData.temperature) {
  app.globalData.temperature = 25
}
if (!app.globalData.weather) {
  app.globalData.weather = '晴'
}
}
}
// pages/index/index.vue
const app = getApp()

export default {
data() {
return {
  // 本地显示用
  temp: app.globalData.temperature,
  weather: app.globalData.weather
}
},
methods: {
changeTemp(delta) {
  app.globalData.temperature += delta
  this.temp = app.globalData.temperature  // 注意:需要手动同步
},
changeWeather() {
  const weathers = ['晴', '多云', '雨', '雪']
  const idx = weathers.indexOf(app.globalData.weather)
  app.globalData.weather = weathers[(idx + 1) % weathers.length]
  this.weather = app.globalData.weather  // 手动同步
}
}
}
<template>
<view class="container">
<text class="temp">{{ temp }}°C</text>
<text class="weather">{{ weather }}</text>
<button @click="changeTemp(1)">升温</button>
<button @click="changeTemp(-1)">降温</button>
<button @click="changeWeather">换天气</button>
<text class="tip">切换到其他页面再回来,数据是共享的</text>
</view>
</template>

预期输出:点击按钮,温度和天气会变化;切到其他页面再回来,值保持不变。

解释:globalData 像个公共白板,谁都能读写,但注意——globalData 变了,页面不会自动刷新,所以要手动同步到 data() 里。


项目2:用 Pinia 管理「小明的购物车」(15分钟)

目标:实现一个购物车,支持:添加商品、删除商品、计算总价、清空购物车、持久化到本地存储。

// store/shop.js
import { defineStore } from 'pinia'
import { ref, computed, watch } from 'vue'

export const useShopStore = defineStore('shop', () => {
const goods = ref([
{ id: 1, name: 'T恤', price: 99, stock: 20 },
{ id: 2, name: '牛仔裤', price: 199, stock: 15 },
{ id: 3, name: '运动鞋', price: 399, stock: 10 },
{ id: 4, name: '帽子', price: 49, stock: 30 }
])

const cart = ref([])

// 派生数据
const cartCount = computed(() => cart.value.reduce((sum, item) => sum + item.num, 0))
const cartTotal = computed(() => cart.value.reduce((sum, item) => sum + item.price * item.num, 0))
const canCheckout = computed(() => cart.value.length > 0)

// 操作方法
function addToCart(good) {
const existed = cart.value.find(item => item.id === good.id)
if (existed) {
  existed.num++
} else {
  cart.value.push({ ...good, num: 1 })
}
}

function removeFromCart(goodId) {
const idx = cart.value.findIndex(item => item.id === goodId)
if (idx !== -1) {
  cart.value.splice(idx, 1)
}
}

function changeNum(goodId, num) {
const item = cart.value.find(item => item.id === goodId)
if (item) {
  item.num = Math.max(1, num)
}
}

function clearCart() {
cart.value = []
}

// ★ 持久化:每次 cart 变化,自动保存到本地
watch(cart, (newCart) => {
uni.setStorage({
  key: 'cartData',
  data: JSON.stringify(newCart)
})
}, { deep: true })

// ★ 启动时从本地恢复
function loadCart() {
const saved = uni.getStorageSync('cartData')
if (saved) {
  cart.value = JSON.parse(saved)
}
}

return {
goods,
cart,
cartCount,
cartTotal,
canCheckout,
addToCart,
removeFromCart,
changeNum,
clearCart,
loadCart
}
})
// pages/shop/shop.vue
import { useShopStore } from '@/store/shop'
import { onMounted } from 'vue'

const shop = useShopStore()

export default {
onLoad() {
shop.loadCart()  // 页面加载时恢复购物车
},
setup() {
return { shop }
}
}
<template>
<view class="shop-page">
<view class="header">
  <text class="title">小明优选商城</text>
  <view class="cart-info">
    <text>购物车:{{ shop.cartCount }} 件</text>
    <text>合计:{{ shop.cartTotal }} 元</text>
  </view>
</view>

<!-- 商品列表 -->
<view class="goods-list">
  <view class="good-item" v-for="good in shop.goods" :key="good.id">
    <text class="good-name">{{ good.name }}</text>
    <text class="good-price">¥{{ good.price }}</text>
    <button size="mini" @click="shop.addToCart(good)">加入购物车</button>
  </view>
</view>

<!-- 购物车列表 -->
<view class="cart-section" v-if="shop.cart.length > 0">
  <text class="section-title">购物车内容</text>
  <view class="cart-item" v-for="item in shop.cart" :key="item.id">
    <text>{{ item.name }} x {{ item.num }}</text>
    <text>小计:{{ item.price * item.num }}元</text>
    <button size="mini" @click="shop.removeFromCart(item.id)">删除</button>
  </view>
  <view class="cart-actions">
    <button @click="shop.clearCart">清空购物车</button>
    <button type="primary" :disabled="!shop.canCheckout">结算({{ shop.cartTotal }}元)</button>
  </view>
</view>
</view>
</template>

预期输出
- 商品列表显示 4 件商品
- 点击「加入购物车」,商品进入购物车区域,数量+1
- 购物车合计实时更新
- 关闭小程序再打开,购物车数据从本地存储恢复

解释:Pinia 的 watch 自动把购物车序列化存到 uni.setStorageloadCart 在页面加载时读回来——这其实是下一章要讲的本地存储的预览。


项目3:「多页面数据同步小工具」(15分钟)

目标:做一个「收藏夹」功能,在列表页收藏,在个人中心页看到;用 Pinia + 事件总线组合实现多页面实时同步。

// store/favorite.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useFavoriteStore = defineStore('favorite', () => {
const items = ref([])
const count = computed(() => items.value.length)

function toggle(item) {
const idx = items.value.findIndex(i => i.id === item.id)
if (idx !== -1) {
  items.value.splice(idx, 1)
} else {
  items.value.push(item)
}
}

function isFavorited(itemId) {
return items.value.some(i => i.id === itemId)
}

function clear() {
items.value = []
}

return { items, count, toggle, isFavorited, clear }
})
// utils/eventBus.js(事件总线,复习上一章)
export const eventBus = {
on(event, callback) {
uni.$on(event, callback)
},
emit(event, data) {
uni.$emit(event, data)
},
off(event, callback) {
uni.$off(event, callback)
}
}
// pages/goods/list.vue(商品列表页)
import { useFavoriteStore } from '@/store/favorite'

const fav = useFavoriteStore()

export default {
data() {
return {
  goods: [
    { id: 1, name: '蓝牙耳机', price: 299, desc: '无线降噪' },
    { id: 2, name: '机械键盘', price: 599, desc: '青轴手感' },
    { id: 3, name: '人体工学椅', price: 1299, desc: '久坐不累' },
    { id: 4, name: '台灯', price: 99, desc: '护眼灯光' }
  ]
}
},
computed: {
isFav() {
  return (id) => fav.isFavorited(id)
}
},
methods: {
toggleFav(item) {
  fav.toggle(item)
  // ★ 通知其他页面刷新
  eventBus.emit('favChanged', { id: item.id, action: this.isFav(item.id) ? 'add' : 'remove' })
  uni.showToast({
    title: this.isFav(item.id) ? '已收藏' : '已取消',
    icon: 'none'
  })
}
}
}
// pages/user/favorites.vue(个人中心-收藏页)
import { useFavoriteStore } from '@/store/favorite'

const fav = useFavoriteStore()

export default {
onShow() {
// ★ 监听收藏变化(从其他页面发来的事件)
eventBus.on('favChanged', (data) => {
  console.log('收藏变化:', data)
  // Pinia 已经是响应式的,这里只是额外做个日志/埋点
})
},
onUnload() {
eventBus.off('favChanged')
},
setup() {
return { fav }
}
}
<!-- 商品列表页模板 -->
<template>
<view class="goods-list">
<view class="good-item" v-for="good in goods" :key="good.id">
  <view class="good-info">
    <text class="good-name">{{ good.name }}</text>
    <text class="good-desc">{{ good.desc }}</text>
    <text class="good-price">¥{{ good.price }}</text>
  </view>
  <button :class="isFav(good.id) ? 'fav-btn active' : 'fav-btn'" @click="toggleFav(good)">
    {{ isFav(good.id) ? '♥ 已收藏' : '♡ 收藏' }}
  </button>
</view>
</view>
</template>
<!-- 收藏页模板 -->
<template>
<view class="fav-page">
<text class="title">我的收藏夹 ({{ fav.count }})</text>
<view v-if="fav.count === 0" class="empty">
  <text>空空如也,快去收藏几件宝贝吧~</text>
</view>
<view class="fav-item" v-for="item in fav.items" :key="item.id">
  <text>{{ item.name }} - ¥{{ item.price }}</text>
  <button size="mini" @click="fav.toggle(item)">取消收藏</button>
</view>
<button v-if="fav.count > 0" @click="fav.clear">清空收藏</button>
</view>
</template>

预期输出
- 在商品列表页点击「收藏」,按钮变红
- 切换到「我的收藏」页面,收藏夹里已有该商品
- 在收藏页点击「取消收藏」,切回列表页,按钮恢复未收藏状态

解释:Pinia 作为中央数据源负责存取,事件总线负责跨页面通知——两者配合,既有持久化又有实时响应。


💪 进阶:3个坑 + 1个技巧

坑1:globalData 不是响应式的

// ❌ 错误:改了 globalData,页面不会自动更新
const app = getApp()
app.globalData.count = 10

// ✅ 正确:需要手动同步到 data
data() {
return { count: app.globalData.count }
},
methods: {
add() {
app.globalData.count++
this.count = app.globalData.count  // 手动刷新
}
}

坑2:Vuex/Pinia 在子包中访问不到

uniapp 的分包机制里,子包不能直接用主包的 store。

// ❌ 错误:子包中 this.$store 是 undefined
// ✅ 正确:子包单独引入
import { useCartStore } from '@/store/cart'
const cart = useCartStore()

坑3:Pinia 的 watch 在 H5 端可能失效

// ❌ 错误:在 setup 外使用 watch
watch(cart, () => {})  // 不生效

// ✅ 正确:在 setup 内使用(Composition API 规范)
import { watch } from 'vue'
setup() {
watch(cart, () => {}, { deep: true })
}

坑4:globalData 初始化时机

// ❌ 错误:在 App.vue onLaunch 中立即读取 globalData(可能还没准备好)
onLaunch() {
console.log(getApp().globalData.xxx)  // undefined
}

// ✅ 正确:用 onShow 或者延迟读取
onLaunch() {
setTimeout(() => {
console.log(getApp().globalData.xxx)
}, 100)
}

坑5:Vuex 的 mutations 必须同步

// ❌ 错误:mutations 里写异步
mutations: {
async setUser(state, user) {
const res = await api.getUser(user.id)  // 别这么干!
state.userInfo = res.data
}
}

// ✅ 正确:异步放 Actions
actions: {
async setUser({ commit }, userId) {
const res = await api.getUser(userId)
commit('setUser', res.data)
}
}

性能小贴士:computed 缓存

// ❌ 浪费:模板里每次都做计算
<text>总价:{{ items.reduce((sum, i) => sum + i.price * i.num, 0) }}元</text>

// ✅ 正确:用 computed 缓存
const total = computed(() => items.reduce((sum, i) => sum + i.price * i.num, 0))
// 模板里 {{ total }},只有 items 变时才重新计算

调试技巧:Vue Devtools

在 H5 调试时,装上 Vue Devtools 插件,能看到 Pinia/Vuex 的 state 实时变化、mutations 记录、action 调用链——比 console.log 优雅 100 倍。

// 如果非要用 console 调试,推荐格式化输出
console.log('cart变化:', JSON.parse(JSON.stringify(cart.value)))
// 直接 console.log(cart.value) 可能看到 Proxy 对象,不直观

✏️ 练习题

练习1(2分钟):globalData 温度计

  • 输入:在 globalData 中设置 temperature = 20
  • 预期输出:页面显示 20°C
  • 提示:用 getApp().globalData.temperature 读取

练习2(3分钟):给购物车加「限购」判断

  • 输入:商品 stock = 5,用户想买 8
  • 预期输出:提示「库存不足,最多买5件」
  • 提示:在 addToCart 方法里加个 if 判断

练习3(5分钟):用 Pinia 实现「点赞」功能

  • 输入:一组文章列表,点击某篇的点赞按钮
  • 预期输出:该文章点赞数+1,点赞状态切换
  • 提示:参考项目3的 toggle 方法,写一个 toggleLike(article)

练习4(8分钟):组合 globalData + Pinia

  • 输入:把项目2的购物车数据同步到 globalData(作为备份)
  • 预期输出:两处数据保持一致
  • 提示:在 Pinia 的 watch 里同步写入 getApp().globalData.cart

练习5(5分钟):读图找错

  • 题目:用户提供一张报错截图(通常是 Cannot read property 'xxx' of undefined
  • 预期输出:分析出是哪个变量未定义/未初始化
  • 提示:检查是否在 onLoad 而不是在 onLaunch 中获取 getApp()

作业:做一个「阅读历史」小工具

需求描述:做一个「阅读历史」功能,记录用户看过的文章列表,支持:查看历史、清除历史、显示阅读数量。

功能点
1. 在 store/history.js 用 Pinia 定义 history state,支持 addHistory(article)removeHistory(id)clearHistory()
2. 主页显示文章列表,点击「阅读」记录到历史
3. 「我的」页面显示历史列表,支持单条删除和清空
4. 加分项:结合 uni.setStorage 实现持久化(下一章的内容可以先自学)

验收标准
- 能添加历史、删除单条、清空全部
- 切换页面数据不丢失
- 代码有注释


📚 总结

本文学了3个核心点
1. globalData 最简单,适合临时共享,但不会自动响应式
2. Vuex/Pinia 是专业的状态管理库,支持响应式、持久化、跨页面同步
3. Pinia 比 Vuex 更轻量,推荐 Vue3 项目优先使用

延伸资源
- Pinia 官方文档(中文友好,示例清晰)
- Vuex 4 迁移指南(如果需要维护 Vue2 项目)
- uniapp 官方示例《实战:制作一个记事本》(练习全局数据管理)

互动钩子

你的项目里用 globalData 还是 Pinia?遇到过「数据突然没了」的坑吗?评论区聊聊,帮你分析分析!老粉优先回复~


下章预告

学会了全局数据管理,你会发现一个问题——如果用户关掉小程序再打开,数据全丢了。下一章我们要解决的,就是「数据怎么永久保存」的问题……

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。