资讯动态

微信小程序在线音乐Demo源码拆解:从路由到播放器

发布时间:2026/9/15 23:14:35 来源:尧图企业网站定制
简介这是一份面向微信小程序初学者的在线音乐功能 demo源自开源项目 wechat-wxapp-music-dev适配 1028 版本开发者工具。项目已包含完整源码和运行截图导入开发者工具后即可直接预览音乐播放器的实际效果非常适合用来理解小程序的基本工程结构与开发流程。压缩包共 41 个文件包括 8 个 WXML 页面结构文件、8 个 JS 业务逻辑文件、7 个 WXSS 样式文件、5 个 JSON 配置文件和 10 张运行截图另有 README 文档和 TypeScript 类型定义便于查看说明和扩展维护其中 WXML 负责页面骨架WXSS 负责界面样式JS 处理交互逻辑JSON 管理全局及页面配置。整体压缩后仅约 72KB目录划分清晰有 pages 页面、utils 工具、全局 app 配置等模块可以快速定位到列表页、播放页的核心代码。已有 467 人浏览学习适合想通过完整实例掌握音乐列表展示、播放状态切换、页面跳转等常见功能的中初级小程序开发者。1. 为什么在线音乐demo值得逐行看微信小程序里的在线音乐demo很多人拿到手就跑一遍然后丢进收藏夹其实这套源码的价值远不止“能响”。它同时踩中了页面路由、数据请求、音频播放、登录鉴权和版本兼容几个硬骨头基本把音频类小程序会遇到的典型问题都摊开放在你面前。尤其标着“适用1028版本”意味着工程配置、基础库调用和踩坑记录都落在某个可复现的环境上跟着源码排错不会出现“我这里行你那里不行”的版本漂移。前端新手能靠它建立小程序项目结构意识做过一两年但没碰过音频场景的人也能从播放器状态管理和适配细节里找到可复用的写法。下面从目录拆到发布配置把能抄的代码都还原一遍。2. 源码工程结构app.json注册与目录约定2.1 从根目录文件反推小程序骨架解压后项目根目录叫wechat-wxapp-music-dev第一眼看上去文件不算多但每一层都有讲究。用目录树把它们铺开wechat-wxapp-music-dev ├── images # 静态图片资源 ├── pages # 页面目录按业务模块拆分子文件夹 ├── utils # 公共工具函数比如request.js、format.js ├── typings # 微信小程序API的TypeScript声明文件 ├── app.js # 小程序入口逻辑 ├── app.json # 全局配置注册页面和窗口表现 ├── app.wxss # 全局样式 ├── jsconfig.json # JavaScript工程配置给IDE做提示 ├── project.config.json # 项目配置AppID、基础库版本、编译设置 ├── .gitignore # git忽略规则 ├── .wing # 开发者工具工作区状态 └── README.md # 项目说明pages下面一般按功能分文件夹比如pages/index/和pages/player/每个页面文件夹里放.js、.json、.wxml、.wxss四个文件这是微信小程序约定俗成的结构。utils里放的是请求封装和时间格式化音乐demo里还会多一个处理播放列表的工具函数。typings虽然不参与运行但缺了它很多编辑器会把wx标红。.wing是工具生成的工作区文件提交到git对你没帮助建议在.gitignore里保留忽略规则。下面用一张表把核心文件的作用和编译关系梳理清楚文件/目录是否参与编译主要作用pages是页面逻辑与视图utils是公共JS工具请求、格式化typings否类型声明仅供IDE提示app.json是页面注册、窗口和超时配置app.wxss是全局样式jsconfig.json否编辑器工程配置project.config.json是项目编译与基础库版本设置2.2 app.json页面注册顺序决定启动页app.json是小程序的“总开关”页面必须在这里注册才能被跳转。demo的第一屏是音乐列表所以pages数组第一项是pages/index/index。一个典型的配置长这样{ pages: [ pages/index/index, pages/player/player ], window: { navigationBarTitleText: 在线音乐, navigationBarBackgroundColor: #ffffff, navigationBarTextStyle: black, backgroundColor: #f6f6f6, backgroundTextStyle: dark }, networkTimeout: { request: 10000 } }pages数组的顺序不是随便写的数组第一个元素是冷启动时的落地页。window控制所有页面的全局导航栏如果播放页想隐藏导航栏可以在该页面的player.json里单独写navigationStyle: custom覆盖掉全局配置。networkTimeout这里设置的是request超时为10秒在线音乐请求时长和网络环境有关10秒是一个比较稳妥的起步值用户在一个弱网里等太久体验会很差。如果demo里有底部导航需求还需要在tabBar里配置list每一项至少包含pagePath和text图标可选。这里多说一句tabBar页面不能使用wx.navigateTo跳转这一点新手最容易踩。2.3 jsconfig.json与typings让IDE认识wx新电脑上打开demo第一件事往往是写了两行代码发现编辑器报“找不到名称wx”。这不是代码坏了是工程缺类型提示。jsconfig.json在这里的角色是告诉编辑器项目用JavaScript严格模式同时把typings里的声明文件包含进来{ compilerOptions: { target: ES6, module: commonjs, checkJs: true, baseUrl: . }, include: [ **/*.js, typings/**/*.d.ts ] }target设成ES6意味着代码里可以放心用Promise和箭头函数但真机上低版本安卓会由开发者工具做ES6转ES5这个在编译设置里控制。checkJs开启后编辑器会对普通.js文件做类型检查帮你提前发现undefined变量和参数类型不匹配。include里的typings/**/*.d.ts是关键如果不写这一行声明文件不会被加载wx.request悬空的问题依然存在。这个文件不会影响小程序编译但能省掉一半查文档的时间。2.4 app.wxss全局样式rpx单位的换算逻辑app.wxss里定义的样式作用到所有页面音乐类项目最常见的全局样式是通用按钮和列表容器。page { background-color: #f6f6f6; font-family: -apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Microsoft YaHei, sans-serif; } .container { padding: 24rpx; } .btn-music { display: inline-flex; align-items: center; justify-content: center; height: 72rpx; padding: 0 32rpx; color: #ffffff; background-color: #07c160; border-radius: 36rpx; }注意这里的rpx是微信小程序的响应式像素以屏幕宽度750rpx为基准。在iPhone 6375px上1rpx等于0.5px在更宽的Android设备上会自动缩放。写全局样式时建议统一用rpx而不是px否则组件层级一多不同设备上的间距会明显不齐。demo里如果沿用网页思路大量写px真机上就会变得左右不对称。2.5 将demo导入微信开发者工具工程配置看得差不多了实际操作一步打开微信开发者工具选择“项目-导入项目”目录定位到wechat-wxapp-music-devAppID可以先用测试号或点“测试号”自动生成基础库版本保持在调试面板默认范围。导入后如果首页白屏优先检查app.json里pages数组第一项是否存在文件如果控制台报request:fail多半是本地调试时未勾选“不校验合法域名”这个问题后面第6章还会详细处理。这样一个包含注册、样式、类型提示、工具链的骨架就算跑通了。3. 页面数据流列表加载与播放器状态管理3.1 列表页的onLoad数据加载在线音乐首页要做的事情很明确进入页面时请求歌曲列表把数据渲染到列表项上。demo里pages/index/index.js通常会这么写const { request } require(../../utils/request) Page({ data: { songList: [], loading: true }, onLoad() { this.fetchSongs() }, fetchSongs() { this.setData({ loading: true }) request({ url: https://api.example.com/songs, method: GET }) .then((res) { // 接口返回的data是歌曲数组字段按后端协议来 this.setData({ songList: res.data || [], loading: false }) }) .catch(() { this.setData({ loading: false }) wx.showToast({ title: 加载失败, icon: none }) }) } })onLoad在一个页面实例的生命周期里只触发一次适合放初始请求。setData每次调用都会把data里的字段同步到视图层这里要当心一个边界如果歌曲列表是后端一次性返回两千条直接setData会卡住页面。实际项目里应该做分页用onReachBottom触发下一页加载把新数据concat到原数组后面再setData整数组。3.2 InnerAudioContext在线播放的核心实例以前的老项目会用audio组件但它是原生组件层级比你自己的view高想在上面盖一层自定义动画几乎不可能。现在在线音乐小程序的主流做法是用wx.createInnerAudioContext()创建音频实例它没有界面完全是逻辑层控制的播放内核const audio wx.createInnerAudioContext() audio.src https://cdn.example.com/music/xxx.mp3 audio.autoplay false audio.onPlay(() { // 更新播放按钮状态 this.setData({ playing: true }) }) audio.onEnded(() { // 自然播放结束自动下一首 this.next() }) audio.onError((err) { console.error(audio error, err) wx.showToast({ title: 音频加载失败, icon: none }) })创建实例时参数如下src是音频网络地址需要在后台配置到downloadFile合法域名autoplay建议设成false由用户点击播放按钮后手动audio.play()避免进入页面时突然出声onPlay、onEnded、onError是实例级监听器。每次切换歌曲先把src赋成一个新的地址就好。要注意的是在页面onUnload里必须调用audio.destroy()否则音频实例会被残留页面卸载后音乐还在播用户也找不到地方暂停。如果切歌时发现下一首没生效常见处理是先audio.stop()再赋值新src最后play()。表格audio组件与InnerAudioContext的差异对比维度audio组件InnerAudioContext是否有界面有原生播放器界面无界面纯逻辑样式覆盖受限可自由定制事件监听bindplay/bindpauseonPlay/onPause生命周期组件卸载即释放手动destroy适用场景简单语音播放音乐播放器、自定义控制条3.3 播放进度与时间同步播放器界面上的进度条和时间依赖onTimeUpdate事件。这个事件触发频率在250毫秒到1秒之间如果你把每次回调都setData到视图层性能会受影响。常见做法是每秒更新一次或者只在差异大于200毫秒时更新audio.onTimeUpdate(() { const currentTime audio.currentTime const duration audio.duration if (duration 0) return if (Math.abs(currentTime - this.data.currentTime) 0.2) { this.setData({ currentTime, progress: (currentTime / duration) * 100 }) } })这里audio.duration在刚加载完成时可能为0要等onCanplay事件后再读取。progress是进度条的百分比拖到进度条时反过来换算onSliderChange(e) { const duration audio.duration const position (e.detail.value / 100) * duration audio.seek(position) }seek是InnerAudioContext提供的跳转方法如果把audio.seek和onTimeUpdate里的setData同时执行进度条会有轻微回跳一般是因为seek后触发的回调里带着旧的currentTime。解决方法是把进度条交互中的setData交给用户手势控制播放器内部的时间同步只更新到本地变量。3.4 页面生命周期与音频的冲突处理音乐播放页的一个特殊点在于用户切到后台、跳出页面、甚至锁屏音频是否还要继续。默认情况下小程序切到后台后音频会被暂停只有配置了requiredBackgroundModes并申请到“后台音乐播放”权限才可能在后台持续播放。demo里通常不会做这么重的权限申请所以我们要在生命周期里做适配onHide() { // 页面隐藏时不打断播放但记录当前状态 this.setData({ pageVisible: false }) }, onUnload() { audio.stop() audio.destroy() }有的开发者会在onHide里audio.pause()这样用户从列表页切到播放页后音乐会一直停在那儿体验并不好。比较合理的策略是单页播放器页面隐藏时继续播放并在下次onShow时恢复播放按钮状态如果是列表页跳播放页则从列表页带歌曲ID过来播放页再决定是否从缓存位置续播。用wx.navigateTo跳转时上一个页面实例还在栈里onHide触发但不会销毁音频实例放在播放页里可以继续运行。4. 微信登录与请求鉴权code换token的完整姿势4.1 wx.login拿到code但code不是token音乐类小程序要拿用户歌单、同步收藏状态第一步都是微信登录。微信登录里最容易混淆的概念是code和tokenwx.login返回的code是一个临时凭证有效期五分钟只能使用一次它不能直接作为登录态。正确流程是把这个code发给自己的后端后端拿着code去微信服务器换openid和session_key之后再生成一个业务token返回给小程序。function wxLogin() { return new Promise((resolve, reject) { wx.login({ success: (res) { if (res.code) { resolve(res.code) } else { reject(new Error(wx.login返回空code)) } }, fail: reject }) }) }小程序端能拿到的只有codeopenid和session_key永远不能暴露在客户端一旦被拿到就能伪造请求。所以后端在换token时要记录openid与token的对应关系并设置合理的有效期。常见错误是在onLoad里每次请求都先wx.login再带code访问接口。这会导致后端频繁调用code2Session而且code有时效性用户停留久了code过期反而请求失败。4.2 封装utils/request.js把token自动注入headerdemo里的utils目录下几乎都有一个请求封装文件它的职责包括统一拼接baseURL、超时处理、把token塞进header、识别业务错误码。一个可用的版本是这样const request (options) { const token wx.getStorageSync(token) return new Promise((resolve, reject) { wx.request({ url: options.url, method: options.method || GET, data: options.data || {}, header: { Content-Type: application/json, Authorization: token ? Bearer ${token} : }, timeout: options.timeout || 10000, success: (res) { // 约定业务码0为成功具体以后端字段为准 if (res.data.code 0) { resolve(res.data.data) } else if (res.statusCode 401) { // token失效重新走登录流程 handleLoginAndRetry(options, resolve, reject) } else { reject(res) } }, fail: reject }) }) }这里把Authorization放在请求头里是常见做法token存在wx.getStorageSync里。注意wx.request的success回调里有两个状态需要区分HTTP状态码res.statusCode和业务状态码res.data.code。401通常意味着登录过期不能简单reject更合理的是删除本地token重新执行wx.login换新token然后重放原来的请求。handleLoginAndRetry就是这个用途但要注意避免请求失败后无限循环重试最多重放一次。4.3 wx.checkSession与登录态管理wx.login的code并不能用wx.checkSession去验证checkSession检查的是微信会话密钥session_key是否还有效。在音乐小程序里token的有效期通常由后端控制前端只需要关心接口返回401的时机function checkLogin() { const token wx.getStorageSync(token) if (!token) { // 没token先走登录 return Promise.resolve(false) } return new Promise((resolve) { wx.checkSession({ success: () resolve(true), fail: () { wx.removeStorageSync(token) resolve(false) } }) }) }这里存在一个容易被忽略的坑wx.checkSession只保证微信侧的会话有效并不能证明业务token没过期。正确做法是后端在每个接口校验token前端以401响应为唯一依据不要用checkSession的结果替代token判断。4.4 登录态在音乐场景下的实际应用登录态和页面行为的关系可以按下面的逻辑来设计用户状态页面行为接口策略未登录可浏览热门歌曲点击收藏跳登录匿名接口不带token已登录展示我的歌单、收藏、每日推荐带token失败401刷新token过期提示后静默重新登录refresh后重放原请求在demo的播放页里如果用户未登录就允许播放前30秒这种类似试听的场景不能只靠前端判断需要后端在遇到未带token的请求时返回一个特殊业务码前端再提示“登录后收听完整版”。如果只是本地demo用wx.getStorageSync里有没有token来切换播放按钮的状态即可。5. 兼容与适配1028版本、顶部导航栏与缓存路径5.1 “适用1028版本”到底指什么很多下载包里会标注“适用1028版本”这通常指微信开发者工具的某个稳定版本号也可能是开发者调试时使用的基础库里程碑版本。project.config.json里的libVersion字段就是应对这个场景的{ compileType: miniprogram, appid: touristappid, projectname: wechat-wxapp-music, libVersion: 2.30.4, setting: { urlCheck: true, es6: true, minified: true } }libVersion是调试基础库版本选择范围在微信开发者工具“详情-本地设置”里可见。demo说“适用1028”我们不必精确纠结这个数字关键是明白版本号决定了可用API的集合。比如wx.getMenuButtonBoundingClientRect在较早的基础库里返回值不稳定在1028前后的版本里才正常工作。把工程从别人那里拿过来先看libVersion和本地面板是否一致再决定要不要更新代码。5.2 顶部导航栏高度自定义导航栏的兼容算法在线音乐播放页为了效果好看经常使用自定义导航栏。自定义导航栏不是只写一个padding-top就行不同机型的胶囊按钮位置不一样。下面是业内普遍使用的计算方式const sysInfo wx.getSystemInfoSync() const menuRect wx.getMenuButtonBoundingClientRect() const statusBarHeight sysInfo.statusBarHeight const navBarHeight (menuRect.top - statusBarHeight) * 2 menuRect.heightstatusBarHeight是状态栏高度iPhone X系列通常在44px左右普通Android机约20到28pxmenuRect是胶囊按钮的包围盒包含top,bottom,height,width。导航栏高度的常见误算是menuRect.bottom menuRect.top那个值会超出实际导航栏。正确公式是上方间距乘2加胶囊高度目的是让自定义导航栏的内容和系统导航栏视觉对齐。得到高度后应用到节点的padding-top再用height: 0占位或者绝对定位都可以。5.3 把歌词和封面缓存到wx.env.USER_DATA_PATH在线音乐播放器在线听歌会消耗流量歌词、封面这类文本和图片适合做本地缓存。小程序有一个专门的数据目录wx.env.USER_DATA_PATH它对应的是用户数据目录不同用户互不影响。保存文件的方法const fs wx.getFileSystemManager() wx.downloadFile({ url: https://cdn.example.com/lyrics/123.lrc, success(res) { const tempFilePath res.tempFilePath const savedPath ${wx.env.USER_DATA_PATH}/lyrics/123.lrc fs.saveFile({ tempFilePath, filePath: savedPath, success() { // 现在可以从savedPath读取 console.log(缓存完成, savedPath) }, fail(err) { console.error(缓存失败, err) } }) } })wx.downloadFile下载临时文件临时文件在小程序退出后会被清理所以要通过FileSystemManager.saveFile把文件转存到用户目录。filePath传自定义路径时目录必须已经存在saveFile不会自动创建多级目录所以要先fs.mkdir。另一种做法是不传filePath让系统随机生成一个路径再把它存到wx.setStorageSync里映射。缓存文件要有容量上限可以在onShow时用fs.readdir统计目录大小超出限制后按修改时间删除旧文件。5.4 老接口迁移getUserInfo与chooseAvatar老一批demo里常写wx.getUserInfo弹窗拿用户头像昵称这个接口现在会直接被跳过或返回灰色头像。如今合规做法是让用户主动填写用button的open-typechooseAvatar替代button open-typechooseAvatar bindchooseavataronChooseAvatar 选择微信头像 /buttononChooseAvatar回调里能拿到临时头像地址再上传到自己的服务器或者直接用wx.cloud存储。昵称用input的typenickname用户可以在键盘上快速填入微信昵称。这两个改动不涉及基础库大版本跳跃但能避免应用审核时被驳回。5.5 常见编译与真机报错对照报错信息原因处理app.json: 未找到 pages/xx/xx.json页面路径写错或文件缺失检查目录名和文件名大小写request:fail url not in domain list合法域名未配置后台添加request/downloadFile域名invalid code, rid: ...code过期或被二次使用重新wx.logingetMenuButtonBoundingClientRect is not a function基础库版本过低升级libVersion基础库Cannot read property xxx of undefinedsetData数据路径问题用JSON.stringify打印返回数据6. 发布前的改造技巧换源、降级与静态检查6.1 把静态歌曲列表替换成真实接口demo跑通后第一步就是把写死在页面里的data.songList换成接口返回。后端字段不会恰好和前端一致所以要做一层字段映射const list (res.data || []).map((item, index) ({ id: item.id || index, title: item.songName, artist: item.singer, cover: item.coverUrl || /images/default-cover.png, src: item.playUrl }))src就是前面audio.src要用的地址它必须走downloadFile合法域名。id尽量用后端唯一ID不要用数组下标因为切歌、删除、排序后下标会乱。6.2 封面图加载失败后的降级外链图片随时可能失效给图片加一个binderror事件失败时替换成本地占位图。动态路径的更新方式用索引字符串image src{{item.cover}}>onImageError(e) { const index e.currentTarget.dataset.index this.setData({ [songList[${index}].cover]: /images/fallback.png }) }这里setData使用数组下标路径只更新对应的那一项不会触发整个列表的diff重算在长列表里能省下不少渲染开销。6.3 上传前检查合法域名与构建npm在开发者工具里本地可以勾选“不校验合法域名”来方便调试但真机预览和发布版本必须关闭这个选项。然后把接口域名分别加到小程序后台的request合法域名和downloadFile合法域名。如果demo引用了npm包记得在工具里执行“构建npm”之后miniprogram_npm目录才会生成真机才能找到依赖。上传前把project.config.json里的urlCheck设成true再跑一遍“预览”确认登录、播放、切歌、断网提示四个场景都没问题。最后用命令行工具做一次静态检查也能防漏npx miniprogram-ci preview --project project.config.json --qrcode-format terminalminiprogram-ci会帮你检查项目配置错误并把预览二维码打印到终端适合集成到CI流程里。如果团队里多人维护这一步能省掉不少“我本地明明可以”的沟通成本。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价