欢迎光临
我们一直在努力

微信小程序开发全攻略·文章版|05 路由·交互·网络与音乐播放器实战

微信小程序开发全攻略·文章版|05 路由·交互·网络与音乐播放器实战

微信小程序路由·交互·网络实战:从五种页面跳转到底层音乐播放器,一篇打通数据驱动开发

目录

  • 1. 页面路由入门:navigateTo 保留当前页跳转

  • 2. redirectTo:替换当前页,不可返回

  • 3. reLaunch:清空整个页面栈

  • 4. switchTab:跳转到底部 tabBar 页

  • 5. 五种路由 API 对比:一张表说清怎么选

  • 6. 原生交互一:showToast 轻提示

  • 7. 原生交互二:showLoading 加载遮罩

  • 8. 原生交互三:showModal 模态确认框

  • 9. 原生交互四:showActionSheet 底部菜单

  • 10. 动态设置导航栏

  • 11. 网络请求 wx.request:发起 HTTPS 请求

  • 12. wx.request 常用参数与返回值

  • 13. 封装网络请求:Promise + async/await

  • 14. 下拉刷新:开启、监听与收起

  • 15. 下拉刷新实战:配合分页重置

  • 16. 上拉加载:触底加载下一页

  • 17. 完整分页列表:下拉刷新 + 上拉加载

  • 18. 音乐播放器实战一:搜索音乐

  • 19. 音乐播放器实战二:渲染音乐列表

  • 20. 音乐播放器实战三:InnerAudioContext 播放

  • 21. 常见报错与易错点汇总

  • 22. 本模块视频清单

  • 23. 常用路由·交互·网络 API 速查表

  • 24. 学习打卡清单

  • 25. 知识图谱

知识图谱

本模块是一条"从页面跳转 → 用户反馈 → 拉数据 → 分页 → 出成品"的完整链路:

五种路由的选型决策:

你想要的效果

用哪个

从列表进详情,看完能返回

navigateTo

登录成功后离开,不想返回登录页

redirectTo

退出登录/重置流程,清掉所有中间页

reLaunch

切换底部 tabBar

switchTab

返回上一页/上 N 页

navigateBack

分页加载的状态流转:

进入页面 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 播放音频。学完本文,你应当能独立开发一个带后端数据、能分页、能播放音频的完整业务页面。本文适合:已经掌握事件与渲染、想做出"真能跑数据"的小程序的进阶学习者。

相关延伸阅读

  • 李沐《动手学深度学习》191集课程全解析:模块拆解与学习路径

  • 吴恩达《面向开发者的提示词工程》精读

  • 吴恩达《Model Context Protocol (MCP) 教程》精读(一)

  • 多模态大模型教程笔记:ViT·CLIP·SAM·Stable Diffusion

1. 页面路由入门:navigateTo 保留当前页跳转

📝 小程序是多页面应用,页面之间的跳转叫路由。理解路由之前,先理解一个核心数据结构——页面栈。你可以把它想象成一摞盘子:每打开一个新页面,就往盘子堆上再放一个(入栈);用户点返回,就从最上面拿走一个(出栈)。微信规定这摞盘子最多叠 10 层。

wx.navigateTo 是最常用的跳转方式:它保留当前页、再打开一个新页。跳转后当前页留在栈里,用户点返回或调用 navigateBack 都能回到原页。典型场景就是"列表页 → 详情页",看完详情能返回列表。

基本用法和传参:

// 保留当前页,跳转到 detail 页;url 后面用 ?key=value 传参
wx.navigateTo({
url: '/pages/detail/detail?id=1001&name=music'
});

目标页在 onLoad 里接收参数:

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 长得很像,但语义完全不同:它关闭当前页、再打开一个新页。因为当前页被关掉了,所以跳转后这一页不出现在返回栈里——用户无法再返回到刚才那一页。

wx.redirectTo({
url: '/pages/home/home' // 关闭当前登录页,打开首页,无法再返回登录页
});
wx.redirectTo({
url: '/pages/home/home' // 关闭当前登录页,打开首页,无法再返回登录页
});

什么场景需要"不能返回"?最典型的是登录后进入首页:用户登录成功,你不希望他点返回又退回到登录页(否则可能重复登录)。再比如"提交表单成功后跳到结果页",也不希望用户返回重复提交。还有一种常见用法是"引导页":第一次打开小程序展示几屏引导,看完点"进入"就用 redirectTo/reLaunch 替换掉引导页,防止用户滑回引导。它和 navigateTo 的区别一句话:navigateTo = 新开一页、可返回;redirectTo = 替换当前页、不保留原页。 注意 redirectTo 同样不能跳 tabBar 页。

3. reLaunch:清空整个页面栈

📝 wx.reLaunch 是最"霸道"的跳转:它会关闭所有页面,然后打开应用内任意一个页面——包括 tabBar 页。跳转后页面栈里只剩新页面孤零零一层,用户无论如何都返回不了。

wx.reLaunch({
url: '/pages/login/login' // 清空全部页面栈,回到登录页
});
wx.reLaunch({
url: '/pages/login/login' // 清空全部页面栈,回到登录页
});

它适合"推倒重来"的场景:退出登录回到启动页、强制重置一段流程。比如用户在一个多步表单里点了"取消",你想让他彻底回到首页、把之前所有中间页都清掉,就用 reLaunch。和 redirectTo 对比一下就清楚了:redirectTo 只换当前这一页,下面还压着别的页;reLaunch 是把整个盘子掀了重摆,栈里只剩新页。正因为它能跳到 tabBar 页,退出登录后回首页这种"既要清栈又要去 tab"的需求,reLaunch 是唯一干净的选择。

4. switchTab:跳转到底部 tabBar 页

📝 wx.switchTab 是一个专门用途的 API:它只能跳转到你在 app.json 的 tabBar 里配置过的那几个底部导航页,并且会关闭其他所有非 tabBar 页面。

wx.switchTab({
url: '/pages/index/index' // 必须是 tabBar 页面
});
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 是"往回走"的:

wx.navigateBack({
delta: 1
}); // 返回上一页;delta=2 返回上两页
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 弹出一个轻量级提示,几秒钟后自动消失,用于"提交成功""已复制""加载中"这类不需要用户决策的反馈。

wx.showToast({
title: '提交成功', // 最多两行
icon: 'success', // success / error / none / loading
duration: 2000, // 时长 ms,默认 1500
mask: false // 是否显示透明蒙层防止穿透
});
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 关闭。 正因为如此,它非常适合包在网络请求外面。

wx.showLoading({
title: '加载中…',
mask: true
});

wx.request({
url: 'https://example.com/api',
success(res) {
/* 处理数据 */
},
complete() {
wx.hideLoading(); // 无论成功失败都要关闭
}
});
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 弹出模态对话框,有标题、内容、确定/取消两个按钮,用于需要用户做决策的场景,比如"确认删除吗?""有新版本是否更新?"。

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 从屏幕底部弹起一组选项,适合"分享到哪里""拍照还是从相册选"这种多选一操作。

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 改导航栏颜色。

wx.setNavigationBarTitle({
title: '当前播放:七里香' // 进入页面/选中歌曲后动态改标题
});
wx.setNavigationBarTitle({
title: '当前播放:七里香' // 进入页面/选中歌曲后动态改标题
});
wx.setNavigationBarColor({
frontColor: '#ffffff', // 仅支持 #ffffff / #000000
backgroundColor: '#07c160', // 导航栏背景色
animation: {
duration: 300,
timingFunc: 'easeIn'
}
});
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,用于从服务器拉取列表、搜索结果等数据。

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 数据的写法:

wx.request({
url: 'https://example.com/login',
method: 'POST',
data: { code: 'abc123' },
header: { 'content-type': 'application/json' },
success(res) {
console.log(res.data);
}
});
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:

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' }); } };

业务页面里这样调用,扁平又清爽:

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 开启下拉刷新:

{
"enablePullDownRefresh": true,
"backgroundColor": "#f6f6f6",
"backgroundTextStyle": "dark"
}

{ "enablePullDownRefresh": true, "backgroundColor": "#f6f6f6", "backgroundTextStyle": "dark" }

页面 js 处理下拉刷新:

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 页、用新数据覆盖旧列表,而不是追加。如果刷新还往后面堆数据,就会和旧数据混在一起,出现重复条目。

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();
});
}
});
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 配置距底部多少像素就提前触发,让加载更跟手。

{
"onReachBottomDistance": 80
}

{ "onReachBottomDistance": 80 }

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. 完整分页列表:下拉刷新 + 上拉加载

📝 这是把前两集合成的"分页三件套",也是几乎所有信息流列表(朋友圈、商品流、聊天记录)的标准实现,建议直接背下来:

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 两个标志位,这段底部提示也能做条件渲染。

<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 事件用起来。

搜索页结构:

<view class="search-bar">
<input placeholder="输入歌曲名搜索" value="{{keyword}}" bindinput="onKeyword" bindconfirm="doSearch" />
<button bindtap="doSearch">搜索</button>
</view>

搜索逻辑:

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 结构:

<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>

点击列表项的处理:

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。

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:

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 跑通了一个「搜索 → 列表 → 播放」的完整音乐播放器闭环。

    建议你按文末的打卡清单,把那个音乐播放器从搜索到播放完整跑一遍——亲手跑通一遍,比看十遍笔记都管用。下一模块将进入登录鉴权和本地缓存,届时你就能把这一篇学到的路由、交互、网络、分页能力,组合成一个真正可上线的完整小程序。

    一篇学到的路由、交互、网络、分页能力,组合成一个真正可上线的完整小程序。

    赞(0)
    未经允许不得转载:171主机测评 » 微信小程序开发全攻略·文章版|05 路由·交互·网络与音乐播放器实战
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址