ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

仿网易云音乐小程序源码解析:全局音频播放与setData优化

仿网易云音乐小程序源码解析:全局音频播放与setData优化 简介这份微信小程序源码包以仿网易云音乐为实战案例面向小程序开发者与前端学习人群帮助理解音乐类应用在移动端的界面搭建、页面交互与数据绑定逻辑。资源共171个文件压缩后4.77MB主要包含107张界面截图、16个JavaScript逻辑文件、15个WXML页面结构、14个WXSS样式表及13个JSON配置辅以使用说明文档与项目配置文件便于对照界面预览与源码进行学习。已有905人浏览学习可作为课堂作业、毕业设计或个人练手项目的参考模板。源码目录划分清晰涵盖全局配置、工具函数、首页、排行榜等典型模块读者可借助截图快速定位对应页面从启动流程到组件封装逐步拆解提升小程序开发实战能力。1. 先拆文件这套仿网易云音乐小程序源码的骨架在哪把压缩包解压后先看到使用说明.docx和几张以cm2_default_play_bg-ip62x.jpg为代表的图片然后是散落在各个路径下的index.js。表面上文件很碎实际上这是一个标准的微信小程序目录骨架同名index.js只是不同页面的入口toplist.js对应排行榜页app.js负责全局生命周期。这套源码没有后端全部用 JavaScript 和 mock 数据实现能模拟首页推荐、榜单切换和音频播放三块功能。适合已经能读写 JS、但没完整跟过小程序项目的开发者作为入口项目。我会先带你看懂app.js的全局播放器设计再拆index.js的数据加载最后落到toplist.js的音频交互。按这个顺序你拿到的不是一段能跑就扔的代码而是能改造成自己项目的骨架。2. app.js 与 util.js小程序启动顺序和全局状态管理2.1 用文件清单还原目录结构从压缩包文件名能反推出这套源码只需要三个关键实体app.js负责注册小程序实例util.js暴露时间格式化、数字格式化等纯函数pages/index和pages/toplist两个页面承担主要交互。整理成工程目录会是这样wx-music-demo/ ├── app.js # 小程序入口App() 注册实例 ├── app.json # 全局配置路由window ├── app.wxss # 全局样式 ├── utils/ │ └── util.js # 时间/播放数格式化无 wx API ├── pages/ │ ├── index/ # 首页推荐 │ │ ├── index.js │ │ ├── index.wxml │ │ ├── index.wxss │ │ └── index.json │ ├── toplist/ # 排行榜页 │ │ ├── toplist.js │ │ ├── toplist.wxml │ │ ├── toplist.wxss │ │ └── toplist.json └── images/ └── cm2_default_play_bg-ip62x.jpg同名index.js容易误导人微信小程序规定每个页面可以用index作为文件名只要路径不同app.json的pages数组就能区分。很多初学者把pages/index/index.js误认为重复文件删掉导入后就会出现“页面未找到”的报错。拿到使用说明.docx先看它让哪个目录作为项目根目录常见做法是新建空目录把上述结构整体拷入后再用微信开发者工具导入不要单独拖入单个index.js。2.2 app.js 里为什么要挂一个 audioCtxapp.js在这类音乐类小程序中不只是做onLaunch生命周期更关键的是放全局播放状态。写法如下// app.js App({ onLaunch() { // 演示项目不需要 wx.login直接初始化全局对象 // 页面加载完成后可通过 getApp() 访问这里的字段 }, globalData: { currentMusic: null, // 当前播放的音乐对象 isPlaying: false, // 是否正在播放 audioCtx: null // 音频实例统一挂全局避免页面销毁被回收 }, setPlaying(music) { this.globalData.currentMusic music; this.globalData.isPlaying true; return this.globalData.isPlaying; } });逻辑说明在微信小程序里Page对象有独立生命周期页面onUnload后实例会被销毁。如果每个页面各自wx.createInnerAudioContext()切到下一个页面时播放就会中断。把audioCtx挂到globalData等于让音频实例活在App这个顶层作用域。这里有个容易被忽略的参数InnerAudioContext在 iOS 上默认受静音键影响想做成音乐 App 那样开了静音也能出声需要在创建后调用wx.setInnerAudioOption({ obeyMuteSwitch: false })这个动作建议放在app.js的onLaunch里只执行一次。2.3 util.js 暴露哪些纯函数源码里的util.js不适合堆业务逻辑常见做法是只放纯函数。以音乐列表为例后端给的时间戳往往是毫秒但音频currentTime是秒直接相减会算错进度。这里两个函数够用// utils/util.js // 毫秒时间戳格式化为 mm:ss function formatTime(ms) { if (typeof ms string) { ms Number(ms); } const total Math.floor(ms / 1000); const minute Math.floor(total / 60) .toString() .padStart(2, 0); const second (total % 60).toString().padStart(2, 0); return ${minute}:${second}; } // 播放数格式化超过 1 万显示 x.x 万 function formatCount(n) { const num Number(n) || 0; if (num 10000) { return ${(num / 10000).toFixed(1)}万; } return ${num}; } module.exports { formatTime, formatCount };参数说明formatTime的入参单位是毫秒内部做了字符串到数字的兜底转换避免从wxml数据里取到 string 时floor结果变成NaN。formatCount在十万、百万量级下仍足够。想扩充时不需要改动页面里的业务代码只需要给工具函数增加分支这属于典型的纯函数拆法后续单元测试也好解耦。小程序开发语言整体还是 JavaScript但受限于渲染层与逻辑层通信util.js里不要写任何wx.开头的 API保持函数与宿主环境隔离。这样在module.exports之后才能直接被页面 require 而不会产生依赖循环。常用函数按这种方式补就行函数名入参返回使用场景formatTimems 毫秒时间戳mm:ss 字符串播放进度、歌曲时长展示formatCountnumber1.2万 / 3400播放数栏目formatDateDateYYYY-MM-DD每日推荐日期formatDate在原源码里不一定要有如果你要在推荐页展示“今天的心动歌单”就照上面的模式补一个不要在index.js里手写getMonth()的拼串逻辑。3. index.js 首页推荐mock 数据、onReachBottom 与 setData 优化3.1 用前端假数据撑起整个首页这份源码的首页 index.js 没有真实接口所有推荐曲目来自一个本地数组。这样做的好处是开箱即跑不需要绑域名坏处是你一旦替换成真实接口要同时处理wx.request的域名白名单和Promise封装。通常我会把请求封装抽到独立request.js或util.js中页面里不直接写wx.request// pages/index/index.js const app getApp(); const { formatTime } require(../../utils/util.js); Page({ data: { recommendList: [], isLoading: false, loaded: false }, onLoad() { this.loadRecommend(); }, loadRecommend() { // 原项目演示用从本地 mock 数组加载 const mockList this.prepareMockList(); this.setData({ recommendList: mockList.map(item ({ id: item.id, name: item.name, singer: item.singer, duration: formatTime(item.duration), cover: item.cover })) }); }, prepareMockList() { // 这里只需返回 [{ id, name, singer, duration, cover }] return []; } });逻辑说明onLoad是页面生命周期中最早可选数据的位置比onReady早适合做首屏初始化。prepareMockList()在生产项目里要换成请求函数返回的 Promise这里保留一层data字段映射而不是直接把接口响应塞给recommendList因为后端字段名经常改映射层能挡住字段名污染。mock 数据字段要贴近真实接口id必须是字符串因为在dataset里取出来也是字符串提前统一类型排在后面坑。3.2 滚动触底加载下一页需要传哪几个参数真实的音乐榜单不可能一次性渲染几十条首页通常配onReachBottom做分页。此时请求参数应该是page、pageSize、before代码形态如下async loadMore() { if (this.data.isLoading || this.data.loaded) { return; } this.setData({ isLoading: true }); const page this.data.page 1; const params { page: page, pageSize: 20, before: this.data.recommendList[this.data.recommendList.length - 1]?.id }; const list await this.request(/api/recommend, params); this.setData({ page, // 追加拼接不是整页替换 recommendList: this.data.recommendList.concat(list), isLoading: false, loaded: list.length params.pageSize }); }逐段说明isLoading防止触底事件在请求还没回来时连续触发before是游标分页比page * pageSize更适合榜单数据因为榜单排序经常变页码会错位。返回列表短于pageSize时置loadedtrue此后不再请求。这里的?.是 JavaScript 的 optional chaining 语法微信开发者工具基础库2.11.0以上支持不要在旧项目里直接使用。参数类型说明常见误区pagenumber页码与服务端约定从 1 开始pageSizenumber条数通常 10/20/50过大导致 setData 卡顿beforestring/number上一页最后一条的 id不是所有接口都支持游标需看文档3.3 setData 是微信小程序性能瓶颈的集中地index.js里一旦直接this.setData({ list: 大数组 })渲染层会重新 diff 整个列表卡顿感非常明显。我一般会切分成三步第一步先setData({ loading: true })拉起骨架屏第二步在拿到接口数据后合并为一次setData第三步对图片使用lazy-load避免开启页面时同时加载几十张封面。需要提示的是setData单次数据量超过 1024KB 会直接报错。遇到多图项目不要试图把 base64 图片塞进data而是把imgUrl放进数组网络图片交给视图层去拉取。本地静态图cm2_default_play_bg-ip62x.jpg体积较大放在images目录里供播放页背景使用不进data是正确的。另一个细节setData支持路径更新例如this.setData({ list[3].name: 夜曲 })只修改某一条数据比整数组替换有效率得多。对顶部“播放/暂停”这种高频状态就可以用activeIndex的路径更新来减少渲染范围。4. toplist.js 排行榜交互从列表渲染到音频播放状态同步4.1 排行榜页的数据结构设计音乐类产品里排行榜页面最核心的数据结构是“榜单数组”和“当前选中项”。源码里的toplist.js可以简化成两个字段// pages/toplist/toplist.js Page({ data: { toplist: [ { id: 1, name: 云音乐热歌榜, songs: [] }, { id: 2, name: 抖音排行榜, songs: [] } ], activeSongId: -1 }, onTapSong(e) { const { index } e.currentTarget.dataset; const idx Number(index); // dataset 里拿到的都是字符串 const song this.data.toplist[idx]; const prevId this.data.activeSongId; this.setData({ activeSongId: song.id prevId ? -1 : song.id }); this.togglePlay(song, song.id ! prevId); } })这里的dataset是wxml里>// 页面内部方法 togglePlay(music, isNew) { const app getApp(); if (!app.globalData.audioCtx) { app.globalData.audioCtx wx.createInnerAudioContext(); // 以音乐类 App 为参考iOS 静音键不影响 App 内播放 wx.setInnerAudioOption({ obeyMuteSwitch: false }); } const audio app.globalData.audioCtx; if (isNew) { audio.stop(); audio.src music.src; audio.play(); } else if (audio.paused) { audio.play(); } else { audio.pause(); } }参数说明isNew是判断点击的是不是新歌点到同一首歌时切到暂停。obeyMuteSwitch参数只对 iOS 生效Android 默认忽略。如果要求后台播放要用wx.getBackgroundAudioManager()它能把音频交给系统播放器切后台不中断但需要用户主动点击播放才能获得音频焦点。两者的对比表如下API后台播放生命周期适合场景InnerAudioContext不支持跟随创建者App 不被销毁则存活页面内嵌播放器、小播放条BackgroundAudioManager支持全局单例仿网易云正在播放页、锁屏控制4.3 播放进度条和时间显示用监听事件对齐很多源码只做到audio.play()就结束了导致播放按钮亮了但进度条不动。正确做法是监听audio.onTimeUpdate把当前播放时间和总时长写回页面audio.onTimeUpdate(() { if (!audio.duration) return; const percent Math.round((audio.currentTime / audio.duration) * 100); const page getCurrentPages()[getCurrentPages().length - 1]; if (page page.setData) { page.setData({ progress: percent, currentTime: formatTime(audio.currentTime * 1000), totalTime: formatTime(audio.duration * 1000) }); } });注意事项currentTime和duration的单位是秒这里乘 1000 转毫秒再交给formatTime保持一致。回调里用getCurrentPages()拿当前页面是因为这个回调挂载在全局audioCtx上和页面不在同一作用域直接用this会得到undefined。另外onTimeUpdate的更新频率约 200ms 到 1s 不等不要在里面做setData以外的重操作否则低端机有明显卡顿。要做歌词逐行滚动需要另外取当前时间戳二分查找歌词段落索引和进度更新逻辑分离避免每帧全量扫数组。5. 把这套源码改成自己项目时最省事的三步调整5.1 修改刚进入的加载页面和导航栏颜色很多同学导入后第一件事想换掉默认的pages/index/index。第一步不是改文件而是打开app.json把pages数组的第一项换成新页面路径第二步同步修改window.navigationBarTitleText和navigationBarBackgroundColor。例如{ pages: [ pages/player/player, pages/index/index, pages/toplist/toplist ], window: { navigationBarTitleText: 私房音乐, navigationBarBackgroundColor: #C20C0C, navigationBarTextStyle: white } }这里需要注意pages数组的首项决定了编译后打开的首屏但如果你的页面属于 tabBar还要保证 tabBar 的pagePath列表里没有漏项否则控制台会报错。5.2 把静态播放背景图换成动态兜底压缩包里的cm2_default_play_bg-ip62x.jpg是为了在加载网络封面时兜底。建议在视图层写两个背景层底层放默认图上层放封面image。给封面图绑定binderror出错时置为coverErrored隐藏上层即可。view classplayer-bg image src{{song.cover}} modeaspectFill binderroronCoverError / image src{{defaultBg}} classbg-fold / /viewJS 在data里维护defaultBg: /images/cm2_default_play_bg-ip62x.jpg。注意这里读取本地图片要写绝对路径不要写成../images/...否则在wxml中会被解析成异常路径导致 404。5.3 验证播放器稳定性onError 和 errCode改完导航和背景还要验证音频链路。InnerAudioContext的onError回调里的errCode基本能定位问题10001通常是文件不存在或格式不支持请确认音频地址是否可外链-1是未知错误多半是本地路径和真实 src 混用20002是请求资源超时。调试代码只需要一行audio.onError((res) { console.warn([audio error], res.errCode, res.errMsg); });如果你发现真机上一切正常唯独 iOS 静音键一关就没声音先检查wx.setInnerAudioOption的obeyMuteSwitch是否被重置为默认值这个配置在开发者工具里很容易被跳过真机上必须重新调用一次。测试时把 errCode 打出来能立刻定位到是本地文件缺失还是网络超时。本文还有配套的精品资源点击获取
返回列表