微信小程序录音功能开发详解:从API到文件持久化

📅 发布时间:2026/9/12 17:33:48
微信小程序录音功能开发详解:从API到文件持久化
简介面向高校相关专业学生这份2024年微信小程序期末大作业以录音功能为核心完整呈现小程序开发的前后端思路。项目包含录音、播放、编辑、管理及分享等常见功能模块适合作为课程设计、毕业项目或微信小程序入门实践参考。压缩包共18个文件约52KB涵盖6个JavaScript逻辑文件、6个JSON配置与数据文件、3个WXSS样式文件、2个WXML页面结构文件以及1个Word说明文档结构清晰便于快速定位页面、配置与功能逻辑。已有142人学习浏览具有一定参考价值。通过阅读代码与目录可了解录音小程序的页面组织、事件绑定、音频数据存储和文件管理方式也能为独立开发或二次完善录音类小程序提供可直接运行的基础工程模板。1. 录音小程序期末大作业打开「新建文件夹」之前先看这几点从课程群或二手资料里拿到的「新建文件夹 (3).zip」解压后往往又是一个同名文件夹再往里才是v-luyinqi这才是真正的微信小程序工程。项目里app.json、app.js、pages、utils、project.config.json一应俱全可以直接导入微信开发者工具预览。它用原生微信小程序框架完成录音、播放、保存、管理这一整条链路覆盖wx.getRecorderManager录音、InnerAudioContext播放、FileSystemManager文件持久化等核心 API。2024 年做这类录音项目除了业务功能还要处理授权弹窗、隐私声明和真机兼容性这些正是期末答辩时老师追问最多的点。下面按拆项目的顺序把配置、链路、管理到提交前检查一次说清。2. 从 app.json 到 pages先读懂录音小程序的工程骨架解压后先别急着点“编译”。很多同学拿到手第一件事是打开开发者工具选“导入项目”结果提示找不到 appid或者模拟器里白屏原因就是没看清根目录的配置。也有人把这个工程拖进 HBuilderX 想当成 uniapp 打开结果是原生微信小程序工程目录结构和manifest.json完全不同。v-luyinqi里真正决定小程序行为的是app.json、project.config.json、sitemap.json而project.private.config.json只是本地私人配置不该出现在交上去的压缩包里。把这几份文件拆开看才算开始读懂这个录音项目。2.1 根目录四大配置appid、页面注册与权限声明app.json是全局配置也是小程序框架第一个读取的文件。录音小程序的pages数组通常像下面这样{ pages: [ pages/index/index, pages/record/record, pages/play/play ], window: { navigationBarTitleText: 录音助手, navigationBarBackgroundColor: #ffffff, navigationBarTextStyle: black }, permission: { scope.record: { desc: 用于录制你的声音并生成录音文件 } }, style: v2, sitemapLocation: sitemap.json }这里permission字段声明的是录音权限scope.record的用途描述用户第一次点击录音按钮微信弹出的授权框中会显示这段desc。很多期末项目只调用wx.authorize却不在配置里写说明开发者工具里不报错真机上也能弹窗但提交审核时隐私检查会提示接口用途不明。所以这段描述要和实际功能对齐。pages数组第一项是启动页。我一般把录音页放在第一位演示时打开就是录音界面少点一次。style: v2是新版组件样式如果你的自定义 wxss 里用了大量 padding 覆盖v2 下 button 默认高度会变化界面可能和截图对不上。这不是代码错了去掉style: v2或调整 wxss 即可。project.config.json记录appid、libVersion、compileType。大作业提交前要把 appid 改成自己的小程序 appid测试号无法使用部分真机能力。很多网上下载的代码里 appid 还是别人的导入后无法在真机上预览因为每个 appid 需要在小程序后台把当前微信号加为开发者。project.private.config.json是微信开发者工具在本地生成的里面有port、projectname等。比如这里 projectname 显示为“新建文件夹 (3)”如果直接压缩上交老师看到文件名和项目名不一致会打低印象分。这个文件在复制工程时可以不提交。2.2 sitemap.json 和 .eslintrc.js不影响运行但影响编译体验sitemap.json控制微信是否索引小程序内的页面。录音页面没有 SEO 需求按默认写 rules 即可{ rules: [ { action: allow, page: * } ] }如果app.json里写了sitemapLocation: sitemap.json但压缩包里没有这个文件每次编译控制台都会报“sitemap 文件未找到”。遇到这种情况检查是 sitemap 被删了还是路径写错了。.eslintrc.js同理如果开发者工具开启了“保存时自动检查”代码不规范会直接在编辑器里标红录音回调里嵌套过多 if/else 会被提示复杂度超标。建议在录音回调里用提前返回减少嵌套。2.3 pages 目录与 utils 工具函数的分工pages 下一般拆成三个页面录音页、列表页、播放页。它们的职责尽量单一避免一个页面里既处理录音状态又处理列表交互。下面一张表说明各自对应的页面任务页面职责关键能力pages/record/record录音、暂停、停止、计时wx.getRecorderManagerpages/list/list列表展示、播放、删除、重命名InnerAudioContext、FileSystemManagerpages/play/play播放详情、分享、上传可选InnerAudioContext、onShareAppMessage把录音和列表放在同一个页面也能跑但会带来状态问题录音时切到后台再回来onShow里要判断录音状态停止后又要刷新列表逻辑全搅在一起。拆开后录音页在onUnload里停止录音并保存列表页在onShow时重新读 storage各管各的出问题也好排查。2.3.1 utils 里的时间格式化与文件名生成几乎每个录音页面都要用到两个函数把毫秒转成00:00格式的时间以及生成不重名的文件名。我习惯放在utils/format.js里function formatDuration(ms) { const totalSeconds Math.floor(ms / 1000); const minutes Math.floor(totalSeconds / 60); const seconds totalSeconds % 60; return ${minutes.toString().padStart(2, 0)}:${seconds.toString().padStart(2, 0)}; } function generateFileName() { const d new Date(); const datePart ${d.getFullYear()}${(d.getMonth() 1).toString().padStart(2, 0)}${d .getDate() .toString() .padStart(2, 0)}; return rec_${datePart}_${d.getTime()}; } module.exports { formatDuration, generateFileName };formatDuration接收RecorderManager返回的duration单位毫秒内部先取整到秒再用padStart补零。generateFileName用本地日期加毫秒时间戳生成文件名注意不要用toISOString()它会返回 UTC 时间在 iOS 上比北京时间少 8 小时导致文件名时间比实际录音时间晚一天。另外文件名不要写死后缀iOS 和 Android 返回的临时文件后缀可能有差异保存时沿用原后缀最稳。提示如果文件名用Date.now()快速连续保存时可能出现重名录音操作间隔至少 1 毫秒时间戳一般够用如果你在批量导入录音文件就需要再加随机数。把配置过一遍后下一步是录音主链路。3. wx.getRecorderManager 录音链路授权、参数与文件落盘录音小程序的核心不是页面长什么样而是录音数据能不能在真机上稳定落盘。这里涉及的链路是用户点击录音 - 授权 - RecorderManager.start - onStop 返回临时文件 - FileSystemManager 保存到用户目录 - 写入列表。任何一环出错录音功能就“点得动但存不下”。3.1 授权不能只在 onLoad 里调录音属于用户隐私行为必须在用户主动触发后调用授权。常见做法是把授权逻辑封装成一个函数在按钮点击事件里执行function ensureRecordAuth(callback) { wx.getSetting({ success(res) { const auth res.authSetting[scope.record]; if (auth true) { callback(); } else if (auth undefined) { wx.authorize({ scope: scope.record, success: () callback(), fail: () openSettingModal() }); } else { openSettingModal(); } } }); } function openSettingModal() { wx.showModal({ title: 需要录音权限, content: 请在设置中允许使用麦克风, confirmText: 去设置, success(res) { if (res.confirm) wx.openSetting(); } }); }逻辑说明authSetting[scope.record]有三种状态。undefined表示从未请求过这时用wx.authorize弹原生授权框true表示已允许直接执行录音false表示用户之前拒绝过再次调用authorize不会弹窗必须引导到wx.openSetting手动打开。调用时写成ensureRecordAuth(() this.startRecord())授权通过后再进入录音逻辑。注意不建议在onLoad里直接wx.authorize微信官方没有完全禁止但模拟器上会频繁弹窗真机上也会被当成不良体验。3.2 RecorderManager 的 start 参数与回调注册通过wx.getRecorderManager()获取全局唯一的录音管理器。录音开始前先注册回调否则可能漏掉onStop事件const recorderManager wx.getRecorderManager(); function initRecorder() { recorderManager.onStart(() { console.log(录音开始); }); recorderManager.onStop((res) { if (!res.tempFilePath) return; persistRecord(res); }); recorderManager.onError((err) { console.error(录音失败, err); wx.showToast({ title: 录音启动失败, icon: none }); }); } function startRecord() { recorderManager.start({ duration: 600000, sampleRate: 16000, numberOfChannels: 1, encodeBitRate: 48000, format: mp3 }); wx.setKeepScreenOn({ keepScreenOn: true }); }start的参数在语音录音场景可以固定下来但每个参数都有含义参数推荐值说明duration600000最大时长单位毫秒10 分钟超过会自动停止触发 onStopsampleRate16000语音录音 16k 够用要录音乐可以用 44100文件体积会增大numberOfChannels1单声道录音手机麦克风采集人声不需要双声道encodeBitRate48000与采样率匹配16k 采样率配 48kbps音质清晰且文件小formatmp3两端通用的音频格式播放端不需要额外解码wx.setKeepScreenOn保持屏幕常亮录音过程中如果屏幕熄灭部分机型会中断录音。这个 API 要在start成功后调用并在停止录音时恢复false否则浪费电量。3.3 从 tempFilePath 到用户目录持久化录音文件onStop返回的res.tempFilePath是临时目录下的文件小程序在适当时候会清理临时目录。要保存录音需要把文件复制到wx.env.USER_DATA_PATH下这个路径是用户数据目录清理微信缓存时会被清掉但在正常情况下可以长期保存。const fs wx.getFileSystemManager(); const ROOT_DIR ${wx.env.USER_DATA_PATH}/recordings; function persistRecord(res) { try { fs.mkdirSync(ROOT_DIR, true); } catch (e) { // 目录已存在时忽略 } const ext res.tempFilePath.split(.).pop(); const fileName ${generateFileName()}.${ext}; const destPath ${ROOT_DIR}/${fileName}; fs.saveFile({ tempFilePath: res.tempFilePath, filePath: destPath, success() { const record { name: fileName, path: destPath, duration: res.duration, size: res.fileSize ? res.fileSize : 0, createTime: Date.now() }; saveRecordToList(record); }, fail(err) { console.error(保存失败, err); wx.showToast({ title: 保存失败, icon: none }); } }); }说明fs.mkdirSync(ROOT_DIR, true)的第二个参数 true 表示递归创建目录目录已存在时会抛错所以用try/catch包住。res.tempFilePath.split(.).pop()拿到原始后缀前面说了不要自己硬编码.mp3。fs.saveFile会复制临时文件到指定filePath如果不传filePath系统会在wx.env.USER_DATA_PATH下生成一个随机文件名。这里指定路径方便和列表记录对应。3.4 录音真机上的几类经典问题真机调试和模拟器差异很大常见问题集中在四个方面授权拒绝后再次点击按钮部分 Android 机型不进fail回调而是直接无响应需要在点击前判断authSettingAndroid 连接蓝牙耳机时录音可能无声或声音极小可以在录音页提示用户断开蓝牙iOS 上录音过程中来电或闹钟打断onStop可能返回空tempFilePath需要先判断空值连续快速点击开始/停止onStop回调可能触发两次第二次tempFilePath为空用一个isRecording布尔值加锁。这些坑在期末演示时最容易遇到建议在录音页加一个兜底如果res.tempFilePath为空直接丢弃这条记录并提示“录音被打断请重录”。注意录音文件一旦保存到用户目录用户清理微信缓存时可能被清掉大作业不需要做数据备份但要能接受这个事实。4. 录音列表InnerAudioContext 播放、重命名与分享录音落盘之后用户面对的就是一个“播放/管理”列表。这部分比拼的不是音频能力而是对文件系统、页面生命周期和微信分享机制的熟悉程度。我的做法是列表页单独用onShow拉取最新数据播放状态用全局唯一的 InnerAudioContext 维护。4.1 InnerAudioContext 播放与状态同步wx.createInnerAudioContext()创建的是音频实例同一时间最好只播一路。如果页面里每次点击都新建一个实例旧的不会自动销毁实例会越积越多。正确做法是在页面里维护一个currentPlayId切换音频前先stop()旧实例function playRecord(record) { const that this; innerAudioContext.stop(); innerAudioContext.src record.path; innerAudioContext.play(); innerAudioContext.onPlay(() { that.setData({ currentPlayId: record.id }); }); innerAudioContext.onEnded(() { that.setData({ currentPlayId: null }); }); innerAudioContext.onError(() { wx.showToast({ title: 播放失败, icon: none }); that.setData({ currentPlayId: null }); }); }这里有个细节先stop()再设置src否则第二次点击另一段录音时src还没更新就开始播放上一段。监听器onPlay、onEnded如果每次点击都绑定就会重复触发。建议在页面onUnload里调用innerAudioContext.destroy()或者把监听器放到onReady里只注册一次。4.2 用 Storage 存元数据用文件系统做兜底录音列表不需要把整个音频文件放进 Storage存元数据就够了。每条记录包含字段类型说明idstring唯一标识可以用文件名namestring展示用名称支持重命名pathstring用户目录下的绝对路径durationnumber录音时长毫秒sizenumber文件大小字节createTimenumber创建时间戳存储和读取最简单的方式是wx.setStorageSync和wx.getStorageSync。但 storage 里的路径可能因为用户清理缓存失效所以列表页onShow时要遍历一遍记录用fs.accessSync检查文件是否存在不存在的直接从列表里移除function filterInvalidRecords(list) { return list.filter((item) { let exists true; try { fs.accessSync(item.path); } catch (e) { exists false; } return exists; }); }accessSync只检查文件是否存在不打开文件速度很快。这个校验逻辑虽然简单却是列表页不出现“点击播放没反应”的关键。4.3 重命名与删除两个文件系统 API 要会用重命名用fs.renameSync直接操作function renameRecord(record, newName) { const ext record.path.split(.).pop(); const dir record.path.substring(0, record.path.lastIndexOf(/) 1); const newPath ${dir}${newName}.${ext}; try { fs.renameSync(record.path, newPath); record.name ${newName}.${ext}; record.path newPath; updateRecordInStorage(record); } catch (e) { wx.showToast({ title: 重命名失败, icon: none }); } }这里需要注意不要用record.path.replace(record.name, newName)因为replace会替换路径中所有匹配的部分如果目录名里恰好也有同名片段路径就乱了。删除则用fs.removeSavedFile或fs.unlink。如果文件是用fs.saveFile保存的官方推荐removeSavedFile用fs.writeFile写入的就对应unlink。两者参数都是filePath在录音小程序里可以互相替换但保持 API 一致更规范。4.4 分享录音本地文件不能直接 shareAppMessage老师可能会问“录音能不能发到微信好友”。wx.shareAppMessage分享的是页面不是本地文件。常见做法是把音频上传到云存储或自己的服务器拿到 URL 后在分享页面里通过参数传递。大作业如果没有后端可以做一个简化方案在播放详情页启用onShareAppMessage分享消息里带path参数好友打开后通过options.path读取本地路径。但这个路径在另一台设备上不存在所以只适用于同一台手机预览答辩时口头说明即可。提示如果要认真做分享用微信云开发的cloud.uploadFile把录音上传到云存储目录再生成临时链接。这一版工程如果没接云开发不要自己硬加会把整个项目复杂度拉高。5. 提交前检查清单录音权限声明、真机调试与容量兜底期末项目交到老师手里时代码能不能跑是底线但演示时不出状况才是加分项。录音小程序有几个检查项和普通展示型小程序不一样我按踩过的坑列一下。5.1 确认 app.json 里有 permission 声明没有permission字段真机上wx.authorize也能弹窗但配置里写明scope.record的用途能避免隐私检查时被追问。检查app.json里是否包含下面这段permission: { scope.record: { desc: 用于录制你的声音并生成录音文件 } }5.2 真机调试优先模拟器只做 UI开发者工具的模拟器能调录音但拿到的tempFilePath是电脑本机路径和真机行为完全不同。录音、授权、保存必须在“真机调试”模式下通一遍。建议按这个顺序预览 - 首次授权 - 录 10 秒 - 停止 - 到列表播放 - 重启小程序看文件还在不在。重启后文件还在说明保存逻辑是通的。5.3 演示前清空历史录音如果项目在老师机器上打开会继承上一次的 storage 和用户目录文件。演示前可以做一个“清空全部录音”按钮或者直接在小程序“清除缓存”里处理避免点开列表出现上一届同学的录音文件。不建议在onLaunch里清空那是给用户的功能不是给开发者的。也可以在列表页长按标题触发隐藏操作或者提交前手动清理工程里不该提交的本地配置rm -rf project.private.config.json5.4 增加录音剩余时间提示与空文件过滤RecorderManager的onTimeUpdate回调可以实时返回res.duration在录音页展示剩余时间。这算一个超过基础分的小功能但要注意在onTimeUpdate里频繁setData会重新渲染整个页面建议只更新一个文本节点。还有一个兜底技巧停止录音后用fs.statSync获取文件大小如果文件小于 1KB大概率是空白录音直接删除并提示用户重录避免列表里出现一堆崩溃记录。本文还有配套的精品资源点击获取