第2章 2.2 页面通讯:url 传参与事件总线
🎯 开场 3 分钟:为什么要学这个?
上一章我们搞懂了「页面什么时候出生、什么时候死亡」——生命周期。但光知道页面什么时候创建和销毁还不够,你肯定遇到过这些糟心事:
- 场景 1:从商品列表页点进详情页,我想把商品 ID 传过去,但不知道怎么把数据「塞」给新页面
- 场景 2:收藏了一篇文章,想让首页的收藏数字实时更新,但两个页面八竿子打不着,怎么通知它?
- 场景 3:用户登录成功后,所有打开的页面都要刷新用户信息,总不能一个个
reLaunch吧?
这些问题都指向同一个能力:页面之间的通讯。
学完这一章,你能:
1. 用 URL 参数在页面间「写信」
2. 用事件总线让任意页面「对讲机」式的实时通讯
3. 组合两种方案,应对 90% 的真实项目场景
🧱 基础 25 分钟:核心概念
2.2.1 URL 传参:页面间的「写信」
是什么?
URL 传参就是在跳转页面时,把数据写进地址栏里。比如你看到这样的地址:
\n\n
\n\n
\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', '小明') 后,页面显示"欢迎小明"
- 提示:记得在 onUnload 里 uni.$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 点
- URL 传参:适合简单数据(ID、状态),像「写信」一样通过地址栏传递
- 事件总线(
uni.$emit/$on或mitt):适合复杂场景,让任意页面实时「对讲」 - getCurrentPages:操作页面栈,逆向修改上一页的数据或调用方法
延伸学习
- 📖 uniapp 页面间通信官方文档——更完整的 API 说明
- 📖 mitt 官方仓库——轻量级事件总线,跨框架通用
- 📖 Vue 组件通讯方式总结——理解 Vue 生态的通讯设计思路
互动钩子
你在项目里用过事件总线吗? 比如说页面 A 改了配置,页面 B、C 要同步更新这种场景。遇到过什么坑?欢迎评论区聊聊,老粉优先回复!
下一章剧透:学会了页面间的「写信」和「对讲」,但如果我想让整个应用都知道某个数据变了(比如用户登录状态),该怎么办?下一章我们讲 globalData + Vuex/Pinia,让全局数据像「广播」一样传遍每个角落。

评论(0)