第2章 2.2 页面通讯:url 传参与事件总线

🎯 开场 3 分钟:为什么要学这个?

上一章我们搞懂了「页面什么时候出生、什么时候死亡」——生命周期。但光知道页面什么时候创建和销毁还不够,你肯定遇到过这些糟心事:

  • 场景 1:从商品列表页点进详情页,我想把商品 ID 传过去,但不知道怎么把数据「塞」给新页面
  • 场景 2:收藏了一篇文章,想让首页的收藏数字实时更新,但两个页面八竿子打不着,怎么通知它?
  • 场景 3:用户登录成功后,所有打开的页面都要刷新用户信息,总不能一个个 reLaunch 吧?

这些问题都指向同一个能力:页面之间的通讯

学完这一章,你能:
1. 用 URL 参数在页面间「写信」
2. 用事件总线让任意页面「对讲机」式的实时通讯
3. 组合两种方案,应对 90% 的真实项目场景


🧱 基础 25 分钟:核心概念

2.2.1 URL 传参:页面间的「写信」

是什么?

URL 传参就是在跳转页面时,把数据写进地址栏里。比如你看到这样的地址:
\n\nSimple tech illustration expla\n\nAI comic creation scene, creat\n\n

/pages/detail/detail?id=10086&name=苹果

问号后面那堆 id=10086&name=苹果 就是我们传过去的参数,像信封里的纸条一样。

生活类比:你让朋友帮你带话——「帮我跟她说,冰箱里有苹果」,这句话就是通过地址栏「带过去」的。

怎么用?

跳转时带参数(来源页)

// 方式1:uni.navigateTo 带参跳转
uni.navigateTo({
url: '/pages/detail/detail?id=10086&name=苹果'
})

// 方式2:编码后跳转(参数有特殊字符时必须)
const params = encodeURIComponent(JSON.stringify({goods: {id: 1, price: 99}}))
uni.navigateTo({
url: `/pages/detail/detail?extra=${params}`
})

接收参数(目标页)

// onLoad 是页面加载时自动执行的,options 就是地址栏里的参数
onLoad(options) {
console.log('收到id:', options.id)        // 输出: 10086
console.log('收到name:', options.name)    // 输出: 苹果

// 如果传的是 JSON 字符串,要解码
if (options.extra) {
const data = JSON.parse(decodeURIComponent(options.extra))
console.log('复杂数据:', data)          // 输出: {goods: {id: 1, price: 99}}
}
}

注意! 参数只能传字符串,数字、布尔、对象都要先转成字符串。对象建议用 JSON.stringify + encodeURIComponent 打包。


2.2.2 getCurrentPages:查看页面「户口本」

是什么?

getCurrentPages() 返回当前所有已打开的页面实例,就像查看一栋楼里住了哪些住户一样。

为什么要用?

有时候你想操作其他页面(比如往另一个页面塞数据、调用它的方法),这个 API 就是入口。

怎么用?

// 获取当前页面栈(所有已打开的页面)
const pages = getCurrentPages()
console.log('当前共', pages.length, '个页面')

// 获取上一个页面(就是跳转来源)
const prevPage = pages[pages.length - 2]
console.log('上一个页面:', prevPage.route)

// 获取上一页实例,可以操作它的 data
prevPage.setData({
title: '我从详情页改了你的标题'
})

// 甚至可以调用上一页的方法(如果定义了的话)
if (prevPage.onMyEvent) {
prevPage.onMyEvent('Hello from detail!')
}

坑来了:小程序有页面栈数量限制(最多 10 层),H5 和 App 端没有这个限制。如果你在开发小程序,要避免循环跳转导致栈溢出。


2.2.3 事件总线:页面的「对讲机」

是什么?

事件总线是一种「发布-订阅」模式。一个页面「发布」一个事件,其他页面「订阅」这个事件,就能收到通知——完全不用知道对方在哪,像对讲机一样喊一声所有人都能听到。

生活类比:想象办公室的对讲机,客服说「有客户来啦」,所有销售都能收到,但他们不需要提前知道谁会呼叫。

怎么用?(uniapp 内置方案)

uniapp 内置了全局事件总线:uni.$emit(发信号)、uni.$on(收信号)、uni.$off(取消订阅)。

订阅方(先监听)

// 在 A 页面监听一个叫 'updateCart' 的事件
onLoad() {
uni.$on('updateCart', (count) => {
console.log('购物车更新了,当前有', count, '件商品')
this.cartCount = count
})
},

// 页面卸载时一定要取消监听,否则会造成内存泄漏
onUnload() {
uni.$off('updateCart')
}

发布方(触发事件)

// 在 B 页面,当购物车变化时,发布事件
methods: {
addToCart(goods) {
this.cart.push(goods)
// 通知所有监听者:购物车变了
uni.$emit('updateCart', this.cart.length)
}
}

怎么用?(mitt 方案,更灵活)

uniapp 的内置事件总线有个局限:只能在 uniapp 环境用。如果你写的是跨端代码,或者想用更成熟的库,可以用 mitt

// 先安装:npm install mitt
// 然后创建一个事件总线文件 eventBus.js
import mitt from 'mitt'

const emitter = mitt()

export default emitter

// ========== 华丽的分割线 ==========

// A 页面:监听事件
import emitter from '@/utils/eventBus.js'

onMounted(() => {
emitter.on('userLogin', (userInfo) => {
console.log('有人登录了:', userInfo.name)
this.user = userInfo
})
})

onUnmounted(() => {
emitter.off('userLogin')  // 取消监听
})

// B 页面:触发事件
import emitter from '@/utils/eventBus.js'

methods: {
doLogin() {
const userInfo = { id: 1, name: '小明' }
emitter.emit('userLogin', userInfo)
}
}

🔥 实战 35 分钟:3 个递进的小项目

项目 1(5 分钟):列表页跳转详情页

需求:从商品列表页点进去,详情页显示你点了哪个商品。

完整代码 - 列表页(pages/list/list.vue)

<script>
export default {
data() {
return {
  goodsList: [
    { id: 1, name: '苹果', price: 5.5 },
    { id: 2, name: '香蕉', price: 3.2 },
    { id: 3, name: '橘子', price: 4.0 }
  ]
}
},
methods: {
goDetail(goods) {
  // 跳转详情页,顺便把商品信息带过去
  uni.navigateTo({
    url: `/pages/detail/detail?id=${goods.id}&name=${goods.name}&price=${goods.price}`
  })
}
}
}
</script>

<template>
<view>
<text>商品列表</text>
<view v-for="item in goodsList" :key="item.id" @click="goDetail(item)">
  <text>{{ item.name }} - ¥{{ item.price }}</text>
</view>
</view>
</template>

完整代码 - 详情页(pages/detail/detail.vue)

<script>
export default {
data() {
return {
  goodsName: '',
  goodsPrice: 0
}
},
onLoad(options) {
// 接收 URL 参数
this.goodsName = options.name
this.goodsPrice = options.price
console.log('打开的是:', this.goodsName, '价格:', this.goodsPrice)
}
}
</script>

<template>
<view>
<text>商品详情</text>
<text>{{ goodsName }} - ¥{{ goodsPrice }}</text>
</view>
</template>

预期输出(点击"苹果"后):

打开的是: 苹果 价格: 5.5

项目 2(15 分钟):跨页面实时通讯——购物车数量实时更新

需求:商品详情页点「加入购物车」,列表页的购物车数字要实时 +1,不用刷新页面。

完整代码 - 列表页(pages/shop/list.vue)

<script>
export default {
data() {
return {
  cartCount: 0
}
},
onLoad() {
// 页面加载时订阅购物车更新事件
uni.$on('cartUpdated', (count) => {
  console.log('收到购物车更新:', count)
  this.cartCount = count
})
},
onUnload() {
// 页面卸载时取消订阅,防止内存泄漏
uni.$off('cartUpdated')
}
}
</script>

<template>
<view>
<view class="cart-bar">
  <text>购物车({{ cartCount }})</text>
</view>
<view v-for="item in [1,2,3]" :key="item">
  <text>商品{{ item }}</text>
  <!-- 跳转详情页 -->
  <navigator url="/pages/shop/detail?id=${item}">
    <text>查看详情</text>
  </navigator>
</view>
</view>
</template>

完整代码 - 详情页(pages/shop/detail.vue)

<script>
export default {
data() {
return {
  cart: []
}
},
onLoad(options) {
console.log('商品ID:', options.id)
},
methods: {
addToCart() {
  this.cart.push({ id: Date.now() })
  // 重点:发布事件,通知所有监听者
  uni.$emit('cartUpdated', this.cart.length)
  uni.showToast({ title: '已加入购物车' })
}
}
}
</script>

<template>
<view>
<text>商品详情页</text>
<button @click="addToCart">加入购物车</button>
</view>
</template>

预期输出(连续点击3次"加入购物车"后,返回列表页,购物车显示 3):

收到购物车更新: 1
收到购物车更新: 2
收到购物车更新: 3

这个例子展示了事件总线的核心价值:两个没有直接关系的页面,通过「发布-订阅」实现了实时同步。


项目 3(15 分钟):组合拳——带缓存的待办清单

需求:做一个待办清单,能新增任务,任务完成后标记,刷新页面不丢失数据。

完整代码 - pages/todo/todo.vue

<script>
// 注意:这是为了让项目串起来,加入了事件总线机制
export default {
data() {
return {
  todos: [],
  newTask: ''
}
},
onLoad() {
// 从本地存储读取已保存的任务
const saved = uni.getStorageSync('todos')
if (saved) {
  this.todos = JSON.parse(saved)
}

// 监听刷新事件(模拟从其他页面触发刷新)
uni.$on('refreshTodos', () => {
  const saved = uni.getStorageSync('todos')
  if (saved) {
    this.todos = JSON.parse(saved)
  }
})
},
onUnload() {
uni.$off('refreshTodos')
},
methods: {
addTask() {
  if (!this.newTask.trim()) {
    return uni.showToast({ title: '任务不能为空', icon: 'none' })
  }

  this.todos.push({
    id: Date.now(),
    text: this.newTask,
    done: false
  })

  this.newTask = ''
  this.saveAndNotify()
},

toggleTask(id) {
  const task = this.todos.find(t => t.id === id)
  if (task) {
    task.done = !task.done
    this.saveAndNotify()
  }
},

deleteTask(id) {
  const index = this.todos.findIndex(t => t.id === id)
  if (index > -1) {
    this.todos.splice(index, 1)
    this.saveAndNotify()
  }
},

saveAndNotify() {
  // 保存到本地
  uni.setStorageSync('todos', JSON.stringify(this.todos))
  // 通知订阅者数据已更新(如果有其他页面监听)
  uni.$emit('todosUpdated', this.todos.length)
}
}
}
</script>

<template>
<view class="container">
<text class="title">我的待办</text>

<view class="input-row">
  <input v-model="newTask" placeholder="输入新任务" />
  <button @click="addTask">添加</button>
</view>

<view v-for="task in todos" :key="task.id" class="task-item">
  <checkbox :checked="task.done" @click="toggleTask(task.id)" />
  <text :class="{ done: task.done }">{{ task.text }}</text>
  <button size="mini" @click="deleteTask(task.id)">删除</button>
</view>

<view v-if="todos.length === 0">
  <text>暂无任务,添加一个吧~</text>
</view>
</view>
</template>

<style>
.container { padding: 20rpx; }
.title { font-size: 18px; font-weight: bold; margin-bottom: 20rpx; display: block; }
.input-row { display: flex; margin-bottom: 20rpx; }
.task-item { display: flex; align-items: center; padding: 10rpx 0; border-bottom: 1px solid #eee; }
.done { text-decoration: line-through; color: #999; }
</style>

预期输出

添加任务"买苹果" → 列表显示"买苹果"(未勾选)
点击勾选 → "买苹果"划线显示
退出再进入 → 任务依然存在

💪 进阶 20 分钟:常见坑 + 性能小贴士

❌ 坑 1:URL 参数带特殊字符

// ❌ 错误:参数有中文或特殊符号会丢失或报错
uni.navigateTo({
url: '/pages/detail/detail?name=苹果&info=好看,好吃'
})

// ✅ 正确:编码后再传输
const info = encodeURIComponent('好看,好吃')
uni.navigateTo({
url: `/pages/detail/detail?name=苹果&info=${info}`
})

❌ 坑 2:对象参数直接塞 URL

// ❌ 错误:对象会被转成 [object Object]
const goods = { id: 1, name: '苹果' }
uni.navigateTo({
url: `/pages/detail/detail?goods=${goods}`  // goods=[object Object]
})

// ✅ 正确:JSON 序列化 + 编码
uni.navigateTo({
url: `/pages/detail/detail?goods=${encodeURIComponent(JSON.stringify(goods))}`
})

// 接收方
onLoad(options) {
const goods = JSON.parse(decodeURIComponent(options.goods))
}

❌ 坑 3:忘记取消事件订阅

// ❌ 错误:每次进入页面都监听,重复触发
onLoad() {
uni.$on('update', this.handleUpdate)
},

// ✅ 正确:页面卸载时取消
onUnload() {
uni.$off('update', this.handleUpdate)
},

❌ 坑 4:getCurrentPages 在 onLoad 之前调用

// ❌ 错误:onLoad 时页面还没完全入栈
onLoad(options) {
const pages = getCurrentPages()
const current = pages[pages.length - 1]  // 可能拿不到上一页
}

// ✅ 正确:如果是操作上一页,等一等再用
onLoad(options) {
setTimeout(() => {
const pages = getCurrentPages()
const prev = pages[pages.length - 2]
if (prev) {
  prev.setData({ fromDetail: true })
}
}, 100)
}

❌ 坑 5:事件名拼写不一致

// 发布时
uni.$emit('cartUpdated', count)

// 订阅时(拼写错了)
uni.$on('cartUpdate', count)  // 永远收不到!

// ✅ 正确:统一定义事件名常量
// constants.js
export const EVENTS = {
CART_UPDATED: 'cartUpdated',
USER_LOGIN: 'userLogin'
}

// 使用
uni.$emit(EVENTS.CART_UPDATED, count)
uni.$on(EVENTS.CART_UPDATED, count)

💡 性能小优化:避免频繁触发事件

// ❌ 低效:每加一个商品就通知一次
methods: {
addItem(item) {
this.items.push(item)
uni.$emit('itemsUpdated', this.items.length)  // 频繁触发
}
}

// ✅ 优化:批量操作后只通知一次
methods: {
addItems(newItems) {
this.items.push(...newItems)
uni.$emit('itemsUpdated', this.items.length)  // 只触发一次
}
}

🔧 调试技巧:事件监听器

// 在控制台查看当前所有监听的事件(临时加的调试代码)
uni.$on('__allEvents', (event, ...args) => {
console.log('事件:', event, '参数:', args)
})

// 或者用 mitt 的 emitter,把所有事件打出来
emitter.on('*', (type, data) => {
console.log('事件总线:', type, data)
})

✏️ 练习题 + 作业题

练习题(5 道,10 分钟内完成)

练习 1(2 分钟):URL 参数取值
- 输入:url: '/pages/detail/detail?id=100&name=香蕉'
- 预期输出:控制台打印 100香蕉
- 提示:onLoad 函数的 options 参数就是地址栏参数

练习 2(2 分钟):事件订阅基础
- 输入:写一个页面,监听 loginSuccess 事件,用户名存储到 data.userName
- 预期输出:uni.$emit('loginSuccess', '小明') 后,页面显示"欢迎小明"
- 提示:记得在 onUnloaduni.$off

练习 3(2 分钟):修改项目 1,加入价格参数
- 输入:在列表页传递价格 { id: 1, name: '苹果', price: 5.5 }
- 预期输出:详情页显示 "苹果 - ¥5.5"
- 提示:URL 参数都是字符串,数字传过去还是字符串

练习 4(3 分钟): mitt 事件总线
- 输入:用 mitt 实现:页面 A 每 3 秒发布一次 tick 事件,页面 B 监听并更新计时器显示
- 预期输出:页面 B 的数字每秒 +1
- 提示:需要 setInterval + emitter.emit

练习 5(3 分钟):分析报错
- 输入:用户说「我在详情页加了购物车,但列表页的数字不更新」
- 预期输出:写出可能的原因(至少 3 条)
- 提示:从订阅时机、事件名拼写、页面栈位置等角度考虑


作业题(30 分钟 - 2 小时)

作业:做一个「跨页面备忘录」

  • 需求描述:做一个简单的备忘录,支持新建、查看、删除笔记。关键是要用到 URL 传参和事件总线。
  • 功能点
    1. 首页显示笔记列表(用 v-for 循环)
    2. 点击笔记标题跳转到详情页(URL 传参:笔记 ID)
    3. 详情页显示笔记内容,支持删除(删除后通知首页刷新)
    4. 新增笔记页面(新增后通知首页刷新)
  • 加分项
    1. 数据持久化(用 uni.setStorageSync 保存到本地)
    2. 空状态提示(没有笔记时显示引导文案)
  • 验收标准
  • 能跑起来(不报错)
  • 首页能显示新增的笔记
  • 删除后首页列表自动更新(事件总线)
  • 提交方式:评论区贴核心代码片段或 GitHub 链接

📚 总结 + 资源

本章核心 3 点

  1. URL 传参:适合简单数据(ID、状态),像「写信」一样通过地址栏传递
  2. 事件总线uni.$emit/$onmitt):适合复杂场景,让任意页面实时「对讲」
  3. getCurrentPages:操作页面栈,逆向修改上一页的数据或调用方法

延伸学习

互动钩子

你在项目里用过事件总线吗? 比如说页面 A 改了配置,页面 B、C 要同步更新这种场景。遇到过什么坑?欢迎评论区聊聊,老粉优先回复!


下一章剧透:学会了页面间的「写信」和「对讲」,但如果我想让整个应用都知道某个数据变了(比如用户登录状态),该怎么办?下一章我们讲 globalData + Vuex/Pinia,让全局数据像「广播」一样传遍每个角落。

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