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

微信小程序开发全攻略·文章版|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. 常见报错与易错点汇总

把本模块高频踩坑集中再强调一遍:

  1. 跳转 tabBar 页失败。 navigateTo / redirectTo 跳 tabBar 页必失败,改用 switchTab 或 reLaunch。

  2. switchTab 带了参数被拦。 switchTab 不能带 query,传参走 globalData 或缓存。

  3. 页面跳着跳着就不动了。 页面栈最多 10 层,navigateTo 用太多会超限,该用 redirectTo/reLaunch 替换。

  4. loading 一直转圈关不掉。 showLoading 必须配 hideLoading,且最好放在 complete/finally 里。

  5. 真机 wx.request 报错。 正式环境要配 request 合法域名、必须 HTTPS、域名需备案;开发期勾"不校验合法域名"。

  6. 下拉刷新动画停不下来。 数据回来后必须 wx.stopPullDownRefresh()。

  7. 上拉加载疯狂发请求。 用 hasMore + loading 两个标志位防止重复触底。

  8. 退出播放页后还在出声。 页面 onUnload 里一定要 audioCtx.destroy() 释放资源。

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

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

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

相关推荐
yunlaodacom2 小时前
华为云国际站服务代理商:BMS数据库延迟突然升高?磁盘I/O、网络与CPU排查方法
网络·数据库·华为云
张宇Joaquin2 小时前
自研SP600系列网卡驱动跟麒麟V10 SP3 2403配套关系
linux·运维·网络
睡一觉就好了。2 小时前
Cookie与Session
网络
niucloud-admin2 小时前
NIUCLOUD LITE AI开发文档—模块开发:微信小程序
微信小程序·小程序·notepad++
步行cgn2 小时前
HTTP 常用请求方法详解
网络·网络协议·http
mengge.cloud2 小时前
1019Namespace ,Contexts ,Kubernetes Pod
运维·服务器·网络·云原生·容器·kubernetes·云计算
艾iYYY3 小时前
【无标题】
linux·开发语言·网络·操作系统
QZ166560951593 小时前
可视化易用·低误报率·跨区域部署:知源-AI数据分类分级系统在金融行业的落地实践
大数据·网络·安全·网络安全
aixingkong9213 小时前
UALink 技术白皮书解读
服务器·网络·硬件架构