微信小程序开发全攻略·文章版|05 路由·交互·网络与音乐播放器实战
微信小程序路由·交互·网络实战:从五种页面跳转到底层音乐播放器,一篇打通数据驱动开发
目录
-
[1. 页面路由入门:navigateTo 保留当前页跳转](#1. 页面路由入门:navigateTo 保留当前页跳转)
-
[2. redirectTo:替换当前页,不可返回](#2. redirectTo:替换当前页,不可返回)
-
[3. reLaunch:清空整个页面栈](#3. reLaunch:清空整个页面栈)
-
[4. switchTab:跳转到底部 tabBar 页](#4. switchTab:跳转到底部 tabBar 页)
-
[5. 五种路由 API 对比:一张表说清怎么选](#5. 五种路由 API 对比:一张表说清怎么选)
-
[6. 原生交互一:showToast 轻提示](#6. 原生交互一:showToast 轻提示)
-
[7. 原生交互二:showLoading 加载遮罩](#7. 原生交互二:showLoading 加载遮罩)
-
[8. 原生交互三:showModal 模态确认框](#8. 原生交互三:showModal 模态确认框)
-
[9. 原生交互四:showActionSheet 底部菜单](#9. 原生交互四:showActionSheet 底部菜单)
-
[10. 动态设置导航栏](#10. 动态设置导航栏)
-
[11. 网络请求 wx.request:发起 HTTPS 请求](#11. 网络请求 wx.request:发起 HTTPS 请求)
-
[12. wx.request 常用参数与返回值](#12. wx.request 常用参数与返回值)
-
[13. 封装网络请求:Promise + async/await](#13. 封装网络请求:Promise + async/await)
-
[14. 下拉刷新:开启、监听与收起](#14. 下拉刷新:开启、监听与收起)
-
[15. 下拉刷新实战:配合分页重置](#15. 下拉刷新实战:配合分页重置)
-
[16. 上拉加载:触底加载下一页](#16. 上拉加载:触底加载下一页)
-
[17. 完整分页列表:下拉刷新 + 上拉加载](#17. 完整分页列表:下拉刷新 + 上拉加载)
-
[18. 音乐播放器实战一:搜索音乐](#18. 音乐播放器实战一:搜索音乐)
-
[19. 音乐播放器实战二:渲染音乐列表](#19. 音乐播放器实战二:渲染音乐列表)
-
[20. 音乐播放器实战三:InnerAudioContext 播放](#20. 音乐播放器实战三:InnerAudioContext 播放)
-
[21. 常见报错与易错点汇总](#21. 常见报错与易错点汇总)
-
[22. 本模块视频清单](#22. 本模块视频清单)
-
[23. 常用路由·交互·网络 API 速查表](#23. 常用路由·交互·网络 API 速查表)
-
[24. 学习打卡清单](#24. 学习打卡清单)
-
[25. 知识图谱](#25. 知识图谱)
知识图谱
本模块是一条"从页面跳转 → 用户反馈 → 拉数据 → 分页 → 出成品"的完整链路:
五种路由的选型决策:
| 你想要的效果 | 用哪个 |
|---|---|
| 从列表进详情,看完能返回 | navigateTo |
| 登录成功后离开,不想返回登录页 | redirectTo |
| 退出登录/重置流程,清掉所有中间页 | reLaunch |
| 切换底部 tabBar | switchTab |
| 返回上一页/上 N 页 | navigateBack |
分页加载的状态流转:
text
进入页面 onLoad
└─ fetchList(1) 覆盖式 list = songs
下拉刷新 onPullDownRefresh
└─ fetchList(1) 覆盖式 list = songs → stopPullDownRefresh
触底 onReachBottom
└─ hasMore && !loading ? fetchList(page+1) : 不动
└─ list = list.concat(songs)
到这里,你已经走完了小程序"从前端静态页面"到"真实数据应用"的关键一跃:会跳转、会反馈、会请求、会分页、会播放音频。把这五块串起来,再配上模块六要讲的登录鉴权和本地缓存,你就具备了独立开发一个完整小程序的全部基础能力。建议照着文末打卡清单,把那个音乐播放器从搜索到播放完整跑一遍------亲手跑通一遍,比看十遍笔记都管用。
📊 交互式知识图谱在线版:https://mcnj5s80gkfh.doubaoapps.com/app/app_17fv07wzj0u
摘要 :这是《2026 最全微信小程序开发全攻略》的第五篇笔记,对应课程第 42-60 集(约 183 分钟)。前面你已经会写交互、会渲染列表,这一篇把小程序"做一个真实业务页面"所需的四块能力一次性补齐:页面路由(navigateTo / redirectTo / reLaunch / switchTab / navigateBack 五种跳转到底怎么选)、原生交互反馈(showToast / showLoading / showModal / showActionSheet 四种弹窗的区别与用法)、网络请求(wx.request 的完整参数、合法域名前提、以及如何把它封装成 Promise 用 async/await 调用)、分页加载 (下拉刷新重置第一页、上拉触底追加下一页的标准实现)。最后用这些能力完成一个简易音乐播放器实战:用 wx.request 搜索音乐、用 wx:for 渲染列表、用 InnerAudioContext 播放音频。学完本文,你应当能独立开发一个带后端数据、能分页、能播放音频的完整业务页面。本文适合:已经掌握事件与渲染、想做出"真能跑数据"的小程序的进阶学习者。
相关延伸阅读
1. 页面路由入门:navigateTo 保留当前页跳转
📝 小程序是多页面应用,页面之间的跳转叫路由 。理解路由之前,先理解一个核心数据结构------页面栈 。你可以把它想象成一摞盘子:每打开一个新页面,就往盘子堆上再放一个(入栈);用户点返回,就从最上面拿走一个(出栈)。微信规定这摞盘子最多叠 10 层。
wx.navigateTo 是最常用的跳转方式:它保留当前页、再打开一个新页 。跳转后当前页留在栈里,用户点返回或调用 navigateBack 都能回到原页。典型场景就是"列表页 → 详情页",看完详情能返回列表。
基本用法和传参:
javascript
// 保留当前页,跳转到 detail 页;url 后面用 ?key=value 传参
wx.navigateTo({
url: '/pages/detail/detail?id=1001&name=music'
});
目标页在 onLoad 里接收参数:
javascript
Page({
onLoad(options) {
console.log(options.id); // "1001"
console.log(options.name); // "music"
}
});
| 要点 | 说明 |
|---|---|
| 页面栈变化 | 当前页保留入栈,栈深度 +1;不能跳转到 tabBar 页面 |
| url 限制 | 路径必须以 / 开头;只能跳转到 app.json pages 里注册过的页面 |
| 传参 | query 字符串拼接,接收端在 onLoad(options) 拿到,均为字符串 |
这里补充一个传参的实际限制:url 上拼的 query 参数全都是字符串 ,而且不能太长。如果你要从列表跳到详情,通常只传一个 id,详情页再拿这个 id 重新请求完整数据------这是最干净的做法。千万别图省事把整个对象 JSON.stringify 后塞进 url,一是 url 长度有限,二是中文要编码、容易出乱码。需要传复杂对象时,用 getApp().globalData 或 wx.setStorageSync 中转,目标页在 onLoad/onShow 里读。
易错点:① 用 navigateTo 跳 tabBar 页会失败,tabBar 页必须用 switchTab;② 页面栈超过 10 层后 navigateTo 不再生效;③ 参数值是中文或特殊字符时要用 encodeURIComponent 编码。
2. redirectTo:替换当前页,不可返回
📝 wx.redirectTo 和 navigateTo 长得很像,但语义完全不同:它关闭当前页、再打开一个新页 。因为当前页被关掉了,所以跳转后这一页不出现在返回栈里------用户无法再返回到刚才那一页。
javascript
wx.redirectTo({
url: '/pages/home/home' // 关闭当前登录页,打开首页,无法再返回登录页
});
javascript
wx.redirectTo({
url: '/pages/home/home' // 关闭当前登录页,打开首页,无法再返回登录页
});
什么场景需要"不能返回"?最典型的是登录后进入首页 :用户登录成功,你不希望他点返回又退回到登录页(否则可能重复登录)。再比如"提交表单成功后跳到结果页",也不希望用户返回重复提交。还有一种常见用法是"引导页":第一次打开小程序展示几屏引导,看完点"进入"就用 redirectTo/reLaunch 替换掉引导页,防止用户滑回引导。它和 navigateTo 的区别一句话:navigateTo = 新开一页、可返回;redirectTo = 替换当前页、不保留原页。 注意 redirectTo 同样不能跳 tabBar 页。
3. reLaunch:清空整个页面栈
📝 wx.reLaunch 是最"霸道"的跳转:它会关闭所有页面 ,然后打开应用内任意一个页面------包括 tabBar 页。跳转后页面栈里只剩新页面孤零零一层,用户无论如何都返回不了。
javascript
wx.reLaunch({
url: '/pages/login/login' // 清空全部页面栈,回到登录页
});
javascript
wx.reLaunch({
url: '/pages/login/login' // 清空全部页面栈,回到登录页
});
它适合"推倒重来"的场景:退出登录回到启动页 、强制重置一段流程。比如用户在一个多步表单里点了"取消",你想让他彻底回到首页、把之前所有中间页都清掉,就用 reLaunch。和 redirectTo 对比一下就清楚了:redirectTo 只换当前这一页,下面还压着别的页;reLaunch 是把整个盘子掀了重摆,栈里只剩新页。正因为它能跳到 tabBar 页,退出登录后回首页这种"既要清栈又要去 tab"的需求,reLaunch 是唯一干净的选择。
4. switchTab:跳转到底部 tabBar 页
📝 wx.switchTab 是一个专门用途的 API:它只能 跳转到你在 app.json 的 tabBar 里配置过的那几个底部导航页,并且会关闭其他所有非 tabBar 页面。
javascript
wx.switchTab({
url: '/pages/index/index' // 必须是 tabBar 页面
});
javascript
wx.switchTab({
url: '/pages/index/index' // 必须是 tabBar 页面
});
这里有一个非常容易踩的坑:switchTab 的 url 不能带 query 参数 。也就是说你不能写 wx.switchTab({ url: '/pages/index/index?id=1' })。那要给 tabBar 页传数据怎么办?一般走两条路:一是写到全局的 getApp().globalData 里,二是写到本地缓存 wx.setStorageSync,目标 tabBar 页在 onShow 里去读。
5. 五种路由 API 对比:一张表说清怎么选
📝 到这里五种跳转 API 就齐了。这张对比表是整个模块五最重要的一张表,面试和实战都会反复用到,请务必背下来:
顺带说一下 tabBar 页接收全局数据的时机。因为 switchTab 切过去后,目标 tabBar 页往往早就被微信创建并缓存了,它的 onLoad 可能只在第一次进时执行一次。所以如果你刚往 globalData 写了东西再切 tabBar,目标页读全局数据要放在 onShow 里------这个生命周期每次页面显示都会触发,保证能读到最新值。这个细节在做"登录后跳到 tabBar 首页并显示用户头像昵称"时特别关键。
| API | 页面栈变化 | 能否跳 tabBar | 典型场景 |
|---|---|---|---|
navigateTo |
保留当前页,栈 +1 | 不能 | 列表 → 详情,可返回 |
redirectTo |
关闭当前页,替换 | 不能 | 登录后离开,不可返回 |
reLaunch |
清空全部栈 | 能 | 退出登录、重置流程 |
switchTab |
切到 tabBar 页,关其他页 | 只能跳 tabBar | 切换底部导航 |
navigateBack |
出栈返回 | --- | 返回上一页/上 N 页 |
最后一个 navigateBack 是"往回走"的:
javascript
wx.navigateBack({
delta: 1
}); // 返回上一页;delta=2 返回上两页
javascript
wx.navigateBack({
delta: 1
}); // 返回上一页;delta=2 返回上两页
用一个"下单流程"的例子把五种路由串起来。用户从首页(tabBar)点商品进详情:用 navigateTo,栈变成 首页, 详情,能返回;详情页点"立即购买"进入确认订单:再 navigateTo,栈变成 首页, 详情, 确认订单;提交成功后跳到支付结果页,这时不希望用户返回订单页重复提交,用 redirectTo,栈变成 首页, 详情, 支付结果;支付结果页点"返回首页",要把中间这些页都清掉,用 switchTab 切回首页,栈只剩 首页。整个过程你会发现:能不能返回、返回后停在哪,完全由你选的那个 API 决定,这就是为什么这张表要背熟。
路由相关的高频报错:① 跳 tabBar 页用错 API(用了 navigateTo/redirectTo)------这是最常见的报错,tabBar 页必须用 switchTab 或 reLaunch;② switchTab 偷偷带了参数,被微信拦下;③ navigateBack 在页面栈只有一页时调用,没有任何效果。
6. 原生交互一:showToast 轻提示
📝 从这一集开始进入"交互反馈"。wx.showToast 弹出一个轻量级提示,几秒钟后自动消失,用于"提交成功""已复制""加载中"这类不需要用户决策的反馈。
javascript
wx.showToast({
title: '提交成功', // 最多两行
icon: 'success', // success / error / none / loading
duration: 2000, // 时长 ms,默认 1500
mask: false // 是否显示透明蒙层防止穿透
});
javascript
wx.showToast({
title: '提交成功', // 最多两行
icon: 'success', // success / error / none / loading
duration: 2000, // 时长 ms,默认 1500
mask: false // 是否显示透明蒙层防止穿透
});
| 参数 | 说明 |
|---|---|
title |
提示文本,icon 为 success/error 时长度建议 ≤ 7 个汉字 |
icon |
success(默认打勾)、error、none(纯文字)、loading |
duration |
显示时长,默认 1500,最大 10000 |
mask |
是否阻止触摸穿透到下层页面 |
需要提前关掉时用 wx.hideToast()。关于 mask 参数再多说一句:如果一个 toast 弹出来时你不希望用户还能点到它下面的按钮(比如"提交中,请勿重复点击"),就把 mask 设为 true,加上一层透明蒙层把下层挡住;反之只是提示一句"已复制",mask 用默认 false 就行,不影响用户继续操作。这个小参数在防止重复提交时很有用。
7. 原生交互二:showLoading 加载遮罩
📝 wx.showLoading 显示带转圈的加载提示。它和 toast 最大的区别是:它不会自动消失,必须你主动调用 wx.hideLoading 关闭。 正因为如此,它非常适合包在网络请求外面。
javascript
wx.showLoading({
title: '加载中...',
mask: true
});
wx.request({
url: 'https://example.com/api',
success(res) {
/* 处理数据 */
},
complete() {
wx.hideLoading(); // 无论成功失败都要关闭
}
});
javascript
wx.showLoading({
title: '加载中...',
mask: true
});
wx.request({
url: 'https://example.com/api',
success(res) {
/* 处理数据 */
},
complete() {
wx.hideLoading(); // 无论成功失败都要关闭
}
});
易错点:showLoading 和 showToast 的 icon:'loading' 其实是同一套弹窗,二者同时只能显示一个 ;loading 一定要配对 hideLoading,并且最好放在 complete 里调用,否则请求一报错、页面就永远卡在转圈上。
8. 原生交互三:showModal 模态确认框
📝 wx.showModal 弹出模态对话框,有标题、内容、确定/取消两个按钮,用于需要用户做决策的场景,比如"确认删除吗?""有新版本是否更新?"。
javascript
wx.showModal({
title: '提示',
content: '确定要删除这条记录吗?',
confirmText: '删除', // 自定义确定按钮文案,默认"确定"
cancelText: '取消',
confirmColor: '#e64340',
success(res) {
if (res.confirm) {
console.log('用户点了确定');
} else if (res.cancel) {
console.log('用户点了取消');
}
}
});
wx.showModal 的按钮颜色可以用 confirmColor 自定义,做"删除"这类危险操作时把确定按钮设成红色,能从视觉上提醒用户谨慎操作。
三个弹窗怎么区分,一句话记牢:Toast = 自动消失的轻提示(无需决策);Loading = 手动关闭的加载态;Modal = 需用户点按钮的强提示。 实际开发里还有个小细节:showModal 的按钮颜色可以用 confirmColor 自定义,做"删除"这类危险操作时把确定按钮设成红色,能从视觉上提醒用户谨慎操作;而 cancelColor 一般不用动。需要用户输入内容时(比如给备注改名),还可以用它的 editable: true 让弹窗带一个输入框,省得你再单独做一个页面。
9. 原生交互四:showActionSheet 底部菜单
📝 wx.showActionSheet 从屏幕底部弹起一组选项,适合"分享到哪里""拍照还是从相册选"这种多选一操作。
javascript
wx.showActionSheet({
itemList: ['拍照', '从相册选择', '取消收藏'], // 最多 6 项,单项 ≤ 6 汉字
itemColor: '#333333',
success(res) {
// res.tapIndex 为点击项下标(从 0 开始)
if (res.tapIndex === 0) {
console.log('用户选择了拍照');
}
}
});
wx.showActionSheet 从屏幕底部弹起一组选项,适合"分享到哪里""拍照还是从相册选"这种多选一操作。
把四种原生弹窗放在一起做个终极对比,选 UI 时就不会纠结了:Toast 是"我告诉你一声",用户不用做任何动作,适合反馈结果;Loading 是"我正在干活,你等着",必须主动结束,适合等接口;Modal 是"我需要你拍板",有确定取消两个按钮,适合危险操作二次确认;ActionSheet 是"你想选哪一个",从底部弹一排选项,适合多选一。记住一个原则:能用 Toast 解决的就别上 Modal,因为 Modal 打断用户的程度最强,滥用会让人心烦。
注意 itemList 最多 6 项、每项文字别太长;用户点蒙层取消走 fail 回调,可以据此做取消逻辑。
10. 动态设置导航栏
📝 除了在页面 json 里静态配置导航栏,还能用 API 在运行时动态改。最常用的两个:wx.setNavigationBarTitle 改标题、wx.setNavigationBarColor 改导航栏颜色。
javascript
wx.setNavigationBarTitle({
title: '当前播放:七里香' // 进入页面/选中歌曲后动态改标题
});
javascript
wx.setNavigationBarTitle({
title: '当前播放:七里香' // 进入页面/选中歌曲后动态改标题
});
javascript
wx.setNavigationBarColor({
frontColor: '#ffffff', // 仅支持 #ffffff / #000000
backgroundColor: '#07c160', // 导航栏背景色
animation: {
duration: 300,
timingFunc: 'easeIn'
}
});
javascript
wx.setNavigationBarColor({
frontColor: '#ffffff', // 仅支持 #ffffff / #000000
backgroundColor: '#07c160', // 导航栏背景色
animation: {
duration: 300,
timingFunc: 'easeIn'
}
});
动态设置导航栏这件事看着不起眼,但对用户体验的影响很直接:一个列表页顶部写死"列表",点进某首歌后标题还叫"列表",用户会分不清自己身在何处。而用 setNavigationBarTitle 动态改成歌名,导航栏立刻就成了当前页面的"身份牌"。再配合 setNavigationBarColor 给播放页换一套主题色,整个播放器的沉浸感就出来了。这两个 API 调用都很轻,一个项目里用不了几次,却能明显提升完成度。
| API | 作用 / 关键参数 |
|---|---|
wx.setNavigationBarTitle |
动态设置当前页导航栏标题(title) |
wx.setNavigationBarColor |
设置导航栏前景色 frontColor(仅黑白)与背景色,可带动画 |
wx.showNavigationBarLoading / hideNavigationBarLoading |
在导航栏显示/隐藏加载小转圈 |
在后面的音乐播放器里,选中一首歌后把导航栏标题改成歌名,就是这个 API 的典型用法。
11. 网络请求 wx.request:发起 HTTPS 请求
📝 前面所有内容都是"前端自己玩",从这一集开始,小程序要和你的服务器要数据 了。wx.request 是小程序发起网络请求的核心 API,默认走 HTTPS,用于从服务器拉取列表、搜索结果等数据。
javascript
wx.request({
url: 'https://example.com/search', // 仅 https;需在后台配置合法域名
method: 'GET', // GET / POST / PUT / DELETE 等
data: { keyword: '七里香', page: 1 }, // 请求参数
header: { 'content-type': 'application/json' },
success(res) {
console.log(res.data);
}
});
wx.request({ url: 'https://example.com/search', // 仅 https;需在后台配置合法域名 method: 'GET', // GET / POST / PUT / DELETE 等 data: { keyword: '七里香', page: 1 }, // 请求参数 header: { 'content-type': 'application/json' }, success(res) { console.log(res.statusCode); console.log(res.data); // 服务器返回数据 }, fail(err) { console.error('请求失败', err); } });
但这里有一个正式上线前必须知道的硬门槛:合法域名。正式环境下,request 的 url 域名必须先在小程序后台"开发管理 → 开发设置 → 服务器域名"里配置为 request 合法域名,否则真机上请求会直接失败。开发阶段可以在开发者工具"详情 → 本地设置"里勾选"不校验合法域名"临时绕过------这也是为什么入门篇里那个"真机预览请求失败"的坑要勾这个选项。
关于合法域名,还要再强调一个上线时的流程:你在开发者工具里勾了"不校验合法域名",只是在你自己电脑上的模拟器/预览里生效,用户手机上的正式版本是不吃这个勾的。所以项目真正要给别人用之前,必须回到小程序管理后台,把你的接口域名、图片域名、音频域名分别填到 request、downloadFile 等合法域名列表里,并且一个月内只能改有限次数。很多人开发时一切正常,一上传体验版让别人扫码就全挂了,十有八九就是忘了配合法域名。
易错点:① 只支持 HTTPS,不支持 HTTP;② 域名必须 ICP 备案并在后台配置,不能直接用 IP;③ 小程序虽然不像浏览器那样有同源限制,但受合法域名白名单约束。
12. wx.request 常用参数与返回值
📝 把 request 的常用配置和返回字段整理成表,照着查即可:
| 参数 / 字段 | 说明 |
|---|---|
url |
请求地址,HTTPS,需合法域名 |
method |
请求方法,默认 GET;常用 GET(查)、POST(增/提交) |
data |
请求参数;GET 拼到 query,POST 放请求体 |
header |
请求头,常用 content-type;可带 token 鉴权 |
success(res) |
成功回调;res.statusCode 状态码、res.data 响应体、res.header 响应头 |
fail / complete |
失败回调 / 完成后必执行(常用于关 loading) |
timeout |
超时时间(ms),默认 60000 |
GET 和 POST 怎么选?记住一个朴素原则:查数据用 GET,改数据用 POST 。GET 的参数拼在 url 后面,会留在浏览器历史和服务器日志里,不适合传密码这类敏感信息;POST 的参数放在请求体里,相对安全,适合提交表单、登录、下单。另外 content-type 要和后端约定好:传 JSON 就用 application/json,传普通表单就用 application/x-www-form-urlencoded,不一致时后端解析 body 会拿到空对象,这又是一个新手常踩却看不出原因的坑。
POST 提交 JSON 数据的写法:
javascript
wx.request({
url: 'https://example.com/login',
method: 'POST',
data: { code: 'abc123' },
header: { 'content-type': 'application/json' },
success(res) {
console.log(res.data);
}
});
javascript
wx.request({
url: 'https://example.com/login',
method: 'POST',
data: { code: 'abc123' },
header: { 'content-type': 'application/json' },
success(res) {
console.log(res.data.token);
}
});
13. 封装网络请求:Promise + async/await
📝 如果每个请求都手写一遍 success/fail/loading,代码会非常啰嗦,而且 baseURL、token、错误提示散落在各处很难维护。真实项目的标准做法是:把 wx.request 封装成一个返回 Promise 的函数 ,统一处理 baseURL、loading、错误提示和 token 注入,业务代码里用 async/await 调用。
新建 utils/request.js:
javascript
const BASE_URL = 'https://example.com';
function request(options) {
return new Promise((resolve, reject) => {
if (options.loading !== false) {
wx.showLoading({ title: '加载中...', mask: true });
}
wx.request({
url: BASE_URL + options.url,
method: options.method || 'GET',
data: options.data || {},
header: {
'content-type': 'application/json',
token: wx.getStorageSync('token') || ''
},
success(res) {
if (res.statusCode === 200) {
resolve(res.data);
} else {
reject(res);
}
},
fail: reject,
complete() {
if (options.loading !== false) {
wx.hideLoading();
}
}
});
});
}
module.exports = {
get(url, data) {
return request({ url, data, method: 'GET' });
},
post(url, data) {
return request({ url, data, method: 'POST' });
}
};
const BASE_URL = 'https://example.com'; function request(options) { return new Promise((resolve, reject) => { if (options.loading !== false) { wx.showLoading({ title: '加载中', mask: true }); } wx.request({ url: BASE_URL + options.url, method: options.method || 'GET', data: options.data || {}, header: { 'content-type': 'application/json', 'Authorization': wx.getStorageSync('token') || '' }, success(res) { if (res.statusCode === 200) { resolve(res.data); } else { wx.showToast({ title: '请求错误', icon: 'none' }); reject(res); } }, fail(err) { wx.showToast({ title: '网络异常', icon: 'none' }); reject(err); }, complete() { if (options.loading !== false) wx.hideLoading(); } }); }); } module.exports = { get(url, data) { return request({ url, data, method: 'GET' }); }, post(url, data) { return request({ url, data, method: 'POST' }); } };
业务页面里这样调用,扁平又清爽:
javascript
const { get } = require('../../utils/request.js');
Page({
data: {
list: []
},
async onLoad() {
const res = await get('/search', { keyword: '七里香', page: 1 });
this.setData({ list: res.songs });
}
});
const { get } = require('../../utils/request.js'); Page({ data: { list: [] }, async onLoad() { const res = await get('/search', { keyword: '七里香', page: 1 }); this.setData({ list: res.result.songs }); } });
为什么非要包一层 Promise?因为原始的 wx.request 用的是 success/fail 回调风格,一旦请求里还要套请求(比如先登录拿 token,再带 token 拉数据),就会写出一层套一层的"回调地狱"。包成 Promise 之后,业务代码可以写成线性的:先 await 拿 token,再 await 拉列表,谁先谁后一目了然。另外把 token 注入、错误提示这些"每个请求都要做一遍"的事收口到封装函数里,以后要改接口地址、要统一加版本号、要统一处理 401 跳登录,都只改 request.js 一个文件,不用全项目搜一遍。
封装的收益总结成五点:统一 baseURL、统一 loading、统一错误提示、统一 token 头、业务代码扁平化为 await。 后面音乐播放器实战会直接复用这个 get 方法。
14. 下拉刷新:开启、监听与收起
📝 网络数据回来了,用户想刷新怎么办?小程序提供了原生的下拉刷新。它需要三步配合:① 在页面 json 开启 enablePullDownRefresh;② 在页面 js 定义 onPullDownRefresh 生命周期函数;③ 刷新完必须 调用 wx.stopPullDownRefresh 手动收起刷新动画,否则那个转圈会一直转。
页面 json 开启下拉刷新:
json
{
"enablePullDownRefresh": true,
"backgroundColor": "#f6f6f6",
"backgroundTextStyle": "dark"
}
{ "enablePullDownRefresh": true, "backgroundColor": "#f6f6f6", "backgroundTextStyle": "dark" }
页面 js 处理下拉刷新:
javascript
Page({
data: {
list: []
},
async onPullDownRefresh() {
// 重新请求第一页数据
const res = await get('/songs', { page: 1 });
this.setData({ list: res.songs });
// 数据回来后手动收起刷新动画
wx.stopPullDownRefresh();
}
});
Page({ data: { list: [] }, async onPullDownRefresh() { // 重新请求第一页数据 const res = await get('/songs', { page: 1 }); this.setData({ list: res.songs }); // 数据回来后手动收起刷新动画 wx.stopPullDownRefresh(); } });
有同学会问:我明明配了 enablePullDownRefresh,怎么下拉没反应?按顺序排查三点:第一,这个配置写在页面自己的 json 里,不是 app.json;第二,页面 js 里确实定义了 onPullDownRefresh;第三,数据回来后有没有调 stopPullDownRefresh------如果上一次刷新忘了收起,下一次可能看起来像"卡住不动"。另外,下拉刷新默认只在页面本身滚动时生效,如果你把内容放在了一个 scroll-view 里,页面级下拉刷新是不会触发的,那种情况要用 scroll-view 自己的 refresher 属性。
| 配置 / API | 作用 |
|---|---|
enablePullDownRefresh |
页面 json 字段,true 开启下拉刷新 |
onPullDownRefresh() |
页面生命周期,下拉时触发,在此重新加载 |
wx.stopPullDownRefresh() |
手动停止下拉刷新动画(必须调用,否则一直转) |
15. 下拉刷新实战:配合分页重置
📝 下拉刷新通常和分页一起用。这里有个关键逻辑:刷新时要把页码重置回第 1 页、用新数据覆盖旧列表,而不是追加。如果刷新还往后面堆数据,就会和旧数据混在一起,出现重复条目。
javascript
Page({
data: {
list: [],
page: 1
},
async loadData(page) {
const res = await get('/songs', { page });
this.setData({ list: res.songs, page });
},
onPullDownRefresh() {
// 下拉刷新:重置回第 1 页,用新数据覆盖旧列表
this.loadData(1).then(() => {
wx.stopPullDownRefresh();
});
}
});
javascript
Page({
data: {
list: [],
page: 1
},
async loadData(page) {
const res = await get('/songs', { page });
this.setData({ list: res.songs, page });
},
onPullDownRefresh() {
// 下拉刷新:重置回第 1 页,用新数据覆盖旧列表
this.loadData(1).then(() => {
wx.stopPullDownRefresh();
});
}
});
易错点:刷新后一定要 stopPullDownRefresh;刷新应当覆盖而不是追加列表,否则旧数据残留。
16. 上拉加载:触底加载下一页
📝 一个列表可能有成百上千条,一次全拉下来太慢,于是要做分页 :第一屏只加载前 20 条,用户滚动到底部时再加载下一页。上拉加载(触底加载更多)靠页面 js 的 onReachBottom() 生命周期触发;onReachBottomDistance 配置距底部多少像素就提前触发,让加载更跟手。
json
{
"onReachBottomDistance": 80
}
{ "onReachBottomDistance": 80 }
javascript
Page({
data: {
list: [],
page: 1,
hasMore: true
},
async onReachBottom() {
if (!this.data.hasMore) return; // 没有更多就不请求
const nextPage = this.data.page + 1;
const res = await get('/songs', { page: nextPage });
this.setData({
list: this.data.list.concat(res.songs),
page: nextPage,
hasMore: res.songs.length > 0
});
}
});
| 配置 / 生命周期 | 作用 |
|---|---|
onReachBottom() |
页面生命周期,滚动触底时触发,加载下一页 |
onReachBottomDistance |
距底部多 px 提前触发(默认 50),提升体验 |
17. 完整分页列表:下拉刷新 + 上拉加载
📝 这是把前两集合成的"分页三件套",也是几乎所有信息流列表(朋友圈、商品流、聊天记录)的标准实现,建议直接背下来:
javascript
Page({
data: {
list: [],
page: 1,
pageSize: 20,
hasMore: true,
loading: false
},
async fetchList(page) {
if (this.data.loading) return; // 防重复请求
this.setData({ loading: true });
const res = await get('/songs', { page, pageSize: this.data.pageSize });
this.setData({
list: page === 1 ? res.songs : this.data.list.concat(res.songs),
page,
hasMore: res.songs.length === this.data.pageSize,
loading: false
});
},
async onPullDownRefresh() {
await this.fetchList(1);
wx.stopPullDownRefresh();
},
async onReachBottom() {
if (!this.data.hasMore || this.data.loading) return;
await this.fetchList(this.data.page + 1);
}
});
除了 JS 里的分页逻辑,一个"看起来专业"的列表页还应该在底部给用户明确的状态反馈。通常会在 wxml 里加一行:正在加载下一页时显示"加载中...",没有更多数据时显示"------ 已经到底啦 ------"。这样用户就不会对着停住的列表疑惑"是不是卡住了"。配合上面的 loading、hasMore 两个标志位,这段底部提示也能做条件渲染。
xml
<view class="load-status">
<text wx:if="{{loading}}">加载中...</text>
<text wx:elif="{{!hasMore && list.length > 0}}">------ 已经到底啦 ------</text>
</view>
分页三件套的三个关键变量:① hasMore 判断还有没有下一页,没有了就别再请求;② loading 防止快速滚动时 onReachBottom 被反复触发造成重复请求;③ 下拉刷新重置 page=1、上拉加载 page+1 并用 concat 追加。这套组合拳,做列表页永远不会错。
18. 音乐播放器实战一:搜索音乐
📝 从这里开始是模块五的收官实战------一个简易音乐播放器。整个项目分三步:搜索音乐 → 渲染音乐列表 → 播放音乐。本集做"搜索框 + 调用接口取回歌曲数据",正好把前面封装的 get 和 input/confirm 事件用起来。
搜索页结构:
xml
<view class="search-bar">
<input placeholder="输入歌曲名搜索" value="{{keyword}}" bindinput="onKeyword" bindconfirm="doSearch" />
<button bindtap="doSearch">搜索</button>
</view>
搜索逻辑:
javascript
const { get } = require('../../utils/request.js');
Page({
data: {
keyword: '',
result: []
},
onKeyword(e) {
this.setData({ keyword: e.detail.value });
},
async doSearch() {
if (!this.data.keyword.trim()) return;
wx.showLoading({ title: '搜索中...', mask: true });
try {
const res = await get('/search', { keyword: this.data.keyword });
this.setData({ result: res.songs });
} finally {
wx.hideLoading();
}
}
});
这里有个体验优化点值得提一下:示例里是点"搜索"按钮或敲回车才发请求(bindconfirm / bindtap),这是最省接口的做法。如果你想做"边打字边出结果"的实时搜索,就不能在 bindinput 里每次都请求------那样用户打三个字会发三个请求。正确做法是加一个防抖:每次 input 先清掉上一个定时器,500 毫秒没新输入才真正发请求,既能实时又不打爆接口。学习阶段先用按钮/回车触发,跑通后再升级成防抖搜索。
注意这里把 loading 放在 finally 里关闭,无论成功失败都会执行,这是最稳的写法。
19. 音乐播放器实战二:渲染音乐列表
📝 搜到歌曲后,用 wx:for 把结果渲染成列表,每项显示封面、歌名、歌手,并在点击时携带歌曲 id。这里把模块四学的列表渲染、条件渲染、事件传参又复习了一遍。
列表渲染的 wxml 结构:
xml
<view wx:if="{{result.length === 0}}" class="empty">
输入歌曲名开始搜索吧
</view>
<view wx:for="{{result}}" wx:key="id" class="song-item"
data-id="{{item.id}}" data-url="{{item.url}}"
bindtap="onPlay">
<image class="song-cover" src="{{item.cover}}" mode="aspectFill" />
<view class="song-info">
<text class="song-name">{{item.name}}</text>
<text class="song-singer">{{item.singer}}</text>
</view>
</view>
点击列表项的处理:
javascript
Page({
data: {
result: []
},
onPlay(e) {
const { id } = e.currentTarget.dataset;
// 方式一:把当前歌曲 id 存全局,播放页读取
getApp().globalData.currentSongId = id;
wx.navigateTo({
url: '/pages/player/player'
});
// 方式二:把整首歌信息存全局,播放页直接取用
// const song = this.data.result.find(item => item.id === id);
// getApp().globalData.currentSong = song;
// wx.navigateTo({ url: '/pages/player/player' });
}
});
这里其实藏着一个设计选择:示例里是把歌曲 id 存到 globalData 再跳播放页。为什么不直接把整首歌的信息通过 url 传过去?因为一首歌的信息不止 id------还有歌名、歌手、封面、播放地址,全拼进 url 又长又要编码,很不划算。更合理的做法就是:列表页点中某首歌时,把"整个播放列表"和"当前播到第几首"一起存进 globalData,播放页拿到后既能播当前这首,又能做上一首、下一首的切换。
实战提醒:音乐、图片等资源域名同样要在后台配置为对应合法域名(request / downloadFile);真实音乐接口可能有鉴权与版权限制,学习时可用自己的测试数据或开放 API。
20. 音乐播放器实战三:InnerAudioContext 播放
📝 终于到了"出声"的一步。小程序用 wx.createInnerAudioContext() 创建一个音频上下文来播放音乐:设置 src(音频地址)、调用 play() / pause() / stop(),并监听 onPlay / onPause / onEnded / onError 等事件来回更新播放按钮的 UI。
javascript
let audioCtx = null;
Page({
data: {
playing: false,
songName: ''
},
onLoad(options) {
// 1. 创建音频实例(页面内单例)
audioCtx = wx.createInnerAudioContext();
audioCtx.src = 'https://example.com/audio/qilixiang.mp3';
audioCtx.title = this.data.songName; // 建议设置标题,锁屏可见
// 2. 监听事件
audioCtx.onPlay(() => this.setData({ playing: true }));
audioCtx.onPause(() => this.setData({ playing: false }));
audioCtx.onEnded(() => this.setData({ playing: false }));
audioCtx.onError((err) => {
console.error('播放失败', err);
wx.showToast({ title: '播放失败', icon: 'none' });
});
// 3. 自动播放
audioCtx.play();
},
togglePlay() {
if (this.data.playing) {
audioCtx.pause();
} else {
audioCtx.play();
}
},
onUnload() {
// 4. 离开页面释放资源
if (audioCtx) {
audioCtx.stop();
audioCtx.destroy();
audioCtx = null;
}
}
});
如果你希望锁屏或切到后台也能继续播放、并在系统通知栏显示 ,就要用全局唯一的 wx.getBackgroundAudioManager()(BackgroundAudioManager),它是 InnerAudioContext 的后台增强版,必须设置 title 和 src:
javascript
const bgm = wx.getBackgroundAudioManager();
bgm.title = '七里香'; // 后台播放必填
bgm.singer = '周杰伦';
bgm.coverImgUrl = 'https://example.com/cover.jpg';
bgm.src = 'https://example.com/audio/qilixiang.mp3'; // 设置 src 后自动播放
如果想做进度条,还可以监听 audioCtx.onTimeUpdate,它会在播放过程中周期性回调,带上当前播放位置 currentTime 和总时长 duration,你拿这两个值就能算出播放进度百分比,驱动一个进度条。真实音乐 App 的进度条、拖动进度、上一首下一首,都是在 InnerAudioContext 这层能力上扩展出来的。学习阶段先把播放/暂停/切换歌曲这三个动作跑通,进度条作为进阶练习自己加上去。
音频相关 API 速查:
| API / 方法 | 作用 |
|---|---|
wx.createInnerAudioContext() |
创建页面内音频实例,适合前台播放 |
audioCtx.play / pause / stop |
控制播放 / 暂停 / 停止 |
onPlay/onPause/onEnded/onError |
监听播放状态变化,更新 UI |
audioCtx.destroy() |
释放音频资源,页面卸载时务必调用 |
wx.getBackgroundAudioManager() |
全局唯一后台音频管理器,支持锁屏/通知栏播放,需设 title |
InnerAudioContext 最容易被忽略的一点是资源释放 。它创建出来是一个实实在在的音频播放实例,如果用户点了好几首歌、又退了好几个页面,每次新建一个却不销毁,就会出现"切到别的页面还在出声"或者"多个音频叠在一起播放"的诡异现象。所以务必记住配对:onLoad 里 create、onUnload 里 destroy。这也是为什么示例代码把 audioCtx 声明成页面级变量、并在 onUnload 里 stop() + destroy()------离开播放页就彻底把麦克风"交出去"。
前台播放和后台播放怎么选?一句话:只在用户停留在播放页时才出声、不需要锁屏控制,用 InnerAudioContext 就够轻;一旦你想做"真·音乐 App"那种切后台、锁屏也能继续听、通知栏能切歌的体验,就必须换成 BackgroundAudioManager,并且老老实实把 title、singer、coverImgUrl、src 都配齐------少了 title,后台播放直接不工作。
整个音乐播放器的实战闭环就是:搜索(wx.request)→ 列表(wx:for + data-id)→ 播放(InnerAudioContext/BackgroundAudioManager)。这正是一个真实数据驱动的小程序的最小可用闭环,配合模块六要讲的登录和缓存,可以继续完善成完整产品。
21. 常见报错与易错点汇总
把本模块高频踩坑集中再强调一遍:
-
跳转 tabBar 页失败。 navigateTo / redirectTo 跳 tabBar 页必失败,改用 switchTab 或 reLaunch。
-
switchTab 带了参数被拦。 switchTab 不能带 query,传参走 globalData 或缓存。
-
页面跳着跳着就不动了。 页面栈最多 10 层,navigateTo 用太多会超限,该用 redirectTo/reLaunch 替换。
-
loading 一直转圈关不掉。 showLoading 必须配 hideLoading,且最好放在
complete/finally里。 -
真机 wx.request 报错。 正式环境要配 request 合法域名、必须 HTTPS、域名需备案;开发期勾"不校验合法域名"。
-
下拉刷新动画停不下来。 数据回来后必须
wx.stopPullDownRefresh()。 -
上拉加载疯狂发请求。 用
hasMore+loading两个标志位防止重复触底。 -
退出播放页后还在出声。 页面
onUnload里一定要audioCtx.destroy()释放资源。 -
后台播放没声音。 BackgroundAudioManager 必须设置
title和src,否则不起播。
22. 本模块视频清单
本模块共 19 集,总时长约 183 分钟,全部为 B 站合集 BV18VTW6TEwE 的分 P:
| 集 | 章节标题 | 时长 | 链接 |
|---|---|---|---|
| 42 | 路由_navigateTo 📝 | 9:09 | 观看 p42 |
| 43 | 路由_redirectTo 📝 | 4:25 | 观看 p43 |
| 44 | 路由_reLaunch 📝 | 5:09 | 观看 p44 |
| 45 | 路由_switchTab 📝 | 8:39 | 观看 p45 |
| 46 | 交互_消息提示框 📝 | 12:23 | 观看 p46 |
| 47 | 交互_loading提示框 📝 | 5:08 | 观看 p47 |
| 48 | 交互_模态对话框 📝 | 11:02 | 观看 p48 |
| 49 | 交互_操作菜单 📝 | 8:17 | 观看 p49 |
| 50 | 动态设置导航栏 📝 | 7:33 | 观看 p50 |
| 51 | 网络请求 📝 | 15:27 | 观看 p51 |
| 52 | 网络请求_常用参数 📝 | 8:07 | 观看 p52 |
| 53 | 封装网络请求 📝 | 12:41 | 观看 p53 |
| 54 | 下拉刷新 📝 | 10:01 | 观看 p54 |
| 55 | 下拉刷新应用 📝 | 12:15 | 观看 p55 |
| 56 | 上拉加载 📝 | 7:25 | 观看 p56 |
| 57 | 上拉加载应用 📝 | 7:45 | 观看 p57 |
| 58 | 简易音乐播放器_搜索音乐 📝 | 9:19 | 观看 p58 |
| 59 | 简易音乐播放器_音乐列表 📝 | 17:33 | 观看 p59 |
| 60 | 简易音乐播放器_播放音乐 📝 | 11:09 | 观看 p60 |
注:本批除全局第 1 集外均无视频内嵌字幕与 AI 总结,正文依据集标题与微信官方标准 API 补全(标 📝);音乐实战所用第三方接口域名需在后台配置 request 合法域名,实际行为以官方文档为准。
23. 常用路由·交互·网络 API 速查表
| 名称 | 作用 | 关键参数 / 备注 |
|---|---|---|
wx.navigateTo |
保留当前页跳转 | 不能跳 tabBar;url?key=value 传参 |
wx.redirectTo |
关闭当前页替换 | 不可返回;不能跳 tabBar |
wx.reLaunch |
清空页面栈跳转 | 可跳任意页含 tabBar |
wx.switchTab |
切换 tabBar 页 | 仅 tabBar 页;不能带参数 |
wx.navigateBack |
返回上页 | delta 指定返回层数 |
wx.showToast |
轻提示 | title/icon(success/error/none/loading)/duration |
wx.showLoading |
加载遮罩 | 必须配 hideLoading 手动关闭 |
wx.showModal |
模态确认框 | title/content/confirm/cancel 回调 |
wx.showActionSheet |
底部操作菜单 | itemList;success 回调 tapIndex |
wx.setNavigationBarTitle |
动态改导航栏标题 | title 字符串 |
wx.setNavigationBarColor |
动态改导航栏颜色 | frontColor(仅黑白)/backgroundColor |
wx.request |
网络请求 | url/method/data/header/success;仅 HTTPS 合法域名 |
enablePullDownRefresh |
开启下拉刷新 | 页面 json;配 onPullDownRefresh + stopPullDownRefresh |
onReachBottom / onReachBottomDistance |
触底加载更多 | 距底部 N px 提前触发;分页追加 |
wx.createInnerAudioContext() |
创建音频实例 | src/play/pause/stop/destroy;onPlay/onEnded |
wx.getBackgroundAudioManager() |
后台音频管理 | 全局唯一,支持锁屏播放,需设 title |
24. 学习打卡清单
-
完成第 42 集:路由_navigateTo
-
完成第 43 集:路由_redirectTo
-
完成第 44 集:路由_reLaunch
-
完成第 45 集:路由_switchTab
-
完成第 46 集:交互_消息提示框
-
完成第 47 集:交互_loading提示框
-
完成第 48 集:交互_模态对话框
-
完成第 49 集:交互_操作菜单
-
完成第 50 集:动态设置导航栏
-
完成第 51 集:网络请求
-
完成第 52 集:网络请求_常用参数
-
完成第 53 集:封装网络请求
-
完成第 54 集:下拉刷新
-
完成第 55 集:下拉刷新应用
-
完成第 56 集:上拉加载
-
完成第 57 集:上拉加载应用
-
完成第 58 集:简易音乐播放器_搜索音乐
-
完成第 59 集:简易音乐播放器_音乐列表
-
完成第 60 集:简易音乐播放器_播放音乐
-
能口述五种路由 API 的页面栈差异与各自适用场景
-
已封装一个返回 Promise 的 request 工具并用 async/await 调用
-
已写出"下拉刷新重置 + 上拉加载追加"的完整分页列表
-
已用 InnerAudioContext 跑通搜索 → 列表 → 播放的最小音乐播放器闭环
-
路由层:页面栈(最多 10 层) → navigateTo(保留/可返回) / redirectTo(替换/不可返回) / reLaunch(清空栈) / switchTab(仅 tabBar) / navigateBack(出栈)
-
交互层:showToast(自动消失轻提示) / showLoading(手动关加载态) / showModal(需决策确认框) / showActionSheet(底部多选菜单) / 动态导航栏
-
网络层:wx.request(HTTPS + 合法域名) → 常用参数 url/method/data/header → Promise 封装(统一 baseURL/loading/token/错误) → async/await 业务调用
-
分页层:下拉刷新(enablePullDownRefresh + onPullDownRefresh + stopPullDownRefresh,重置 page=1) → 上拉加载(onReachBottom + onReachBottomDistance,page+1 concat) → hasMore + loading 防重
-
实战闭环:搜索(wx.request) → 列表(wx:for + data-id) → 播放(InnerAudioContext 前台 / BackgroundAudioManager 后台)
到这里,模块五「路由·交互·网络与音乐播放器实战」的全部内容就结束了。你从五种页面跳转(navigateTo / redirectTo / reLaunch / switchTab / navigateBack)一路走到原生交互反馈(showToast / showLoading / showModal / showActionSheet)、网络请求(wx.request + Promise 封装)、分页加载(下拉刷新 + 上拉加载),最后用 InnerAudioContext 跑通了一个「搜索 → 列表 → 播放」的完整音乐播放器闭环。
建议你按文末的打卡清单,把那个音乐播放器从搜索到播放完整跑一遍------亲手跑通一遍,比看十遍笔记都管用。下一模块将进入登录鉴权和本地缓存,届时你就能把这一篇学到的路由、交互、网络、分页能力,组合成一个真正可上线的完整小程序。
一篇学到的路由、交互、网络、分页能力,组合成一个真正可上线的完整小程序。