尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

微信小程序录音功能开发实战:RecorderManager、权限与真机调试避坑

微信小程序录音功能开发实战:RecorderManager、权限与真机调试避坑 简介这是一份面向高校学生与微信小程序初学者的期末大作业项目定位为2024年录音小程序完整可运行源码。资源包共18个文件结构规整6个js文件承载页面逻辑与录音API调用6个json文件完成全局及页面配置3个wxss与2个wxml分别负责样式和页面结构另附1个docx文档辅助理解项目设计与使用方式整体仅52KB轻量易部署。已有142人学习浏览适合需要完成课程设计或快速上手小程序录音场景的同学。通过阅读源码可梳理录音、播放、文件管理等基础功能的实现思路也能学习小程序工程目录组织和配置方法便于在此基础上二次开发。1. 录音小程序怎么就成了期末大作业的“硬骨头”期末大作业选“录音小程序-2024”看起来像是一个带麦克风图标和录音按钮的简单页面但真动手才会发现里面全是微信小程序特有的“暗坑”录音权限要分平台适配、实时帧数据和临时文件路径要区分用途、安卓和 iOS 的回调执行顺序还不一样。很多同学做到一半才发现普通网页里用不了几行代码的录音逻辑在小程序里要被wx.getRecorderManager()的不少回调牵着走。这篇文章不绕弯子直接按“最小可用录音小程序”的完成路径讲一遍开发环境怎么搭、录音与播放的核心代码怎么写、本地存储与导出怎么落盘、真机调试时哪些参数最容易坑人。适合正在做微信小程序期末大作业、想拿一个“能完整演示”的项目交差又想顺便搞清楚音频底层逻辑的读者。2. 微信小程序录音功能的前置原理与开发环境搭建2.1 为什么录音必须走微信小程序原生的 RecorderManager微信小程序里不能像 H5 那样直接用navigator.mediaDevices.getUserMedia()因为小程序运行在微信客户端提供的 WebView 与原生能力之间麦克风属于系统级硬件资源必须通过微信官方 API 申请并释放。录音相关的核心接口是wx.getRecorderManager()它返回一个全局唯一的RecorderManager实例负责启动录音、暂停、停止以及监听音频帧回调。官方同时提供了wx.createInnerAudioContext()负责播放本地录音文件这两个 API 配合起来就是一个录音小程序最底层的技术底座。从期末大作业的角度看选原生 RecorderManager 而不是自己封装底层音频采集主要理由有三条一是接口稳定从基础库 2.0.0 开始就支持开发者工具和真机都能跑二是它自动处理了大部分格式编码和文件写入逻辑不用自己碰 PCM 数据流三是权限流程是微信强制收口的app.json里配置permission字段后用户拒绝授权时还有统一的失败回调可以兜底。如果选 uniapp 微信小程序方向开发也要注意它的uni.getRecorderManager()底层依然是对微信原生对象的封装调试时仍回到微信开发者工具来抓问题。2.2 开发者工具初始化与 app.json 的权限声明在微信开发者工具里新建项目时选择“小程序”模式AppID 可以是测试号也可以用自己的小程序 AppID。测试号不影响本地编译但录音、播放等硬件能力在开发者工具里只能模拟最终必须用真机调试验证。初始化完成后第一件事是打开app.json在permission字段中声明麦克风用途否则部分基础库版本会在调用录音时直接报错。{ pages: [ pages/index/index, pages/record/record ], permission: { scope.record: { desc: 用于录制音频并保存到本地列表 } }, requiredPrivateInfos: [] }这段配置里scope.record就是录音权限的声明desc是给用户在授权弹窗里看到的文字。requiredPrivateInfos这个字段一般留空即可只有用到位置等敏感接口时才需要声明。注意permission里的desc不能写成空字符串否则微信在真机上会拒绝拉起授权弹窗。配置完成后在页面中调用wx.getRecorderManager()之前最好先通过wx.getSetting()检查一下用户是否已经授权麦克风避免首次调用时体验断裂。2.3 录音参数选型与格式坑位RecorderManager.start()支持传入多种参数但期末作业不建议一上来就追求高规格音频质量因为录音文件要保存到本地并可能展示大小码率太高会让wx.env.user_data_path目录很快被撑满。常用参数组合是const recorderManager wx.getRecorderManager(); recorderManager.start({ duration: 60000, sampleRate: 16000, numberOfChannels: 1, encodeBitRate: 48000, format: mp3 });duration单位是毫秒超过设定时长会自动触发onStopsampleRate16000 对语音类录音已经足够清晰同时体积比 44100 小很多numberOfChannels设 1 表示单声道双声道会让文件体积直接翻倍且在小程序播放器里没有实际听感优势encodeBitRate需要与采样率匹配16000 采样率下 48000 比特率是官方推荐值。format支持mp3和aac两种常用格式开发者工具里模拟器对aac支持好一些但真机返回的临时文件后缀会按照实际格式生成做列表展示时不要把后缀写死在代码里。3. 录音小程序完整功能实现从录制到播放的最小闭环3.1 获取 RecorderManager 实例与生命周期监听每个页面里调用wx.getRecorderManager()时拿到的都是同一个全局单例因此不要在onLoad里重复初始化监听器。常见做法是在onLoad中统一注册onStart、onStop、onError、onPause、onResume回调页面卸载时通过recorderManager.offStop()等方式解除监听防止内存泄漏。Page({ data: { recording: false, paused: false, tempFilePath: , recordTime: 0 }, onLoad() { this.recorderManager wx.getRecorderManager(); this.innerAudioContext wx.createInnerAudioContext(); this.recorderManager.onStart(() { this.setData({ recording: true, paused: false }); }); this.recorderManager.onPause(() { this.setData({ paused: true }); }); this.recorderManager.onStop((res) { const { tempFilePath, fileSize, duration } res; this.setData({ recording: false, paused: false, tempFilePath, fileSize, recordTime: duration }); this.saveRecordToList(tempFilePath, fileSize, duration); }); this.recorderManager.onError((err) { console.error(录音失败, err); wx.showToast({ title: 录音失败, icon: none }); this.setData({ recording: false, paused: false }); }); } });监听器注册完成后所有状态变化都以回调为准。这里把saveRecordToList封装成独立方法是为了后续把录音数据写入本地缓存时保持页面逻辑干净。这里要注意onStop里的tempFilePath是临时目录下的文件路径小程序重启后不保证仍然存在所以必须马上保存或转存。3.2 录音操作按钮组开始、暂停、继续、停止录音页面的操作逻辑要围绕“当前处于什么状态”来写不能只用一个布尔值判断。因为paused和recording是两种可以同时为真的状态按钮的文案和禁用态需要根据组合状态切换。下面是完整的按钮交互代码toggleRecord() { if (!this.data.recording) { this.startRecord(); } else if (this.data.paused) { this.recorderManager.resume(); this.setData({ paused: false }); } else { this.recorderManager.pause(); this.setData({ paused: true }); } }, startRecord() { wx.getSetting({ success: (res) { if (res.authSetting[scope.record]) { this.recorderManager.start({ duration: 60000, sampleRate: 16000, numberOfChannels: 1, encodeBitRate: 48000, format: mp3 }); } else { wx.authorize({ scope: scope.record, success: () this.recorderManager.start({ duration: 60000, sampleRate: 16000, numberOfChannels: 1, encodeBitRate: 48000, format: mp3 }), fail: () wx.showModal({ title: 需要麦克风权限, content: 请在设置中开启录音权限, confirmText: 去设置, success: (modalRes) { if (modalRes.confirm) wx.openSetting(); } }) }); } } }); }, stopRecord() { if (this.data.recording) { this.recorderManager.stop(); } }这段逻辑把首次授权、二次授权和权限拒绝分开处理。wx.authorize只能弹出一次授权框如果用户第一次点了拒绝后续再调用authorize不会再弹窗只能通过wx.openSetting()引导用户去小程序设置页手动打开麦克风权限。所以先getSetting再决定走哪条分支是更稳的方案。录音过程中如果用户切到后台onStop会被立即触发并返回半截录音文件前端需要把这种情况也算作正常结束。3.3 本地录音文件播放与进度条展示播放录音用wx.createInnerAudioContext()创建音频实例需要特别注意的是 Android 上src支持wxfile://开头的临时路径而开发者工具里有时需要把路径转成http://tmp/...格式。保险做法是直接把onStop返回的tempFilePath赋值给innerAudioContext.src然后调用play()微信内部会统一处理。播放结束后要手动释放实例否则同一个音频实例反复播放会出现onError回调报错。playRecord() { if (!this.data.tempFilePath) return; this.innerAudioContext.stop(); this.innerAudioContext.src this.data.tempFilePath; this.innerAudioContext.play(); this.innerAudioContext.onTimeUpdate(() { this.setData({ playProgress: this.innerAudioContext.currentTime }); }); }onTimeUpdate在 iOS 上大约每 250 毫秒触发一次Android 上触发频率不太固定因此不要用它做精确到帧的展示。进度条组件可以直接把currentTime除以duration得到百分比在 WXML 里用stylewidth: {{progress}}%来实现。若自定义播放进度跳转则先seek再继续播放不过期末作业场景里做到“能播、能停、能显示进度”就已经足够覆盖评分点。4. 录音列表的本地落盘wx.env.user_data_path 与缓存管理4.1 用 wx.setStorageSync 保存录音元数据录音文件本身如果直接保存到wx.env.user_data_path需要手动拼接文件名并调用FileSystemManager.saveFile而更省事的做法是先用wx.getFileSystemManager()把临时文件移动到一个固定目录。这里先决定只保存元数据列表文件路径和时长全部用setStorageSync存起来。saveRecordToList(tempFilePath, fileSize, duration) { const fs wx.getFileSystemManager(); const savedDir ${wx.env.USER_DATA_PATH}/recordings; const timestamp Date.now(); const ext tempFilePath.split(.).pop(); const targetPath ${savedDir}/record_${timestamp}.${ext}; fs.mkdirSync(savedDir, true); fs.saveFileSync(tempFilePath, targetPath); const recordList wx.getStorageSync(recordList) || []; recordList.unshift({ id: timestamp, path: targetPath, size: fileSize, duration, createTime: this.formatTime(new Date()) }); wx.setStorageSync(recordList, recordList); this.setData({ recordList }); }mkdirSync第二个参数传true表示递归创建父目录避免第一次运行时因目录不存在而报错。saveFileSync会把临时文件完整复制到目标位置复制完成后临时文件是否被清掉都不影响数据安全。存Storage时只放元数据而不放音频二进制是为了避免主包缓存膨胀微信小程序本地缓存单 key 上限是 1MB音频文件动辄几百 KB直接塞进 Storage 很容易超出限制。4.2 录音列表渲染与删除清理页面数据里维护recordList数组WXML 部分用wx:for循环渲染每一项展示文件名、时长和文件大小。删除录音时不仅要删列表项还要同步删除user_data_path下的实体文件否则会留下幽灵文件白占存储空间。removeRecord(e) { const id e.currentTarget.dataset.id; const fs wx.getFileSystemManager(); const recordList wx.getStorageSync(recordList) || []; const target recordList.find(item item.id id); if (target) { fs.unlinkSync(target.path); } const newList recordList.filter(item item.id ! id); wx.setStorageSync(recordList, newList); this.setData({ recordList: newList }); wx.showToast({ title: 已删除, icon: success }); }这里在saveFile成功后用recordList.unshift将最新录音放到列表头部配合wx:for的key属性可以避免页面重渲染时出现列表项状态错乱。文件删除后记得同步更新 Storage两个动作必须放在同一个事务视角内否则异常退出时会出现列表里能点、真机上文件已经打不开的情况。4.3 真机调试与文件路径的查错窗口期末答辩时最容易翻车的一点模拟器里能录音也能播放换到真机上点录音按钮直接闪退或者回调onError。多数情况下问题出在路径写死或权限未配置。调试时可临时在onStop回调里打印完整的tempFilePath再通过wx.getFileSystemManager().readdirSync(wx.env.USER_DATA_PATH)检查文件是否真的写入成功。微信开发者工具的“ Storage ”面板和“文件系统”面板都可以直接查看运行时的数据如果列表为空优先检查onLoad监听是否注册成功。另一个坑是 iOS 上saveFileSync对文件后缀有严格校验format: mp3返回的临时路径后缀可能是.mp3但如果你在start时故意不传formatAndroid 会默认输出.m4a两种后缀混在一个列表里时播放前要按实际路径扩展名判断。5. 录音小程序开发避坑iOS、Android 与初始化加载的适配微调5.1 iOS 与 Android 的录音行为差异很多开发者在做微信小程序录音时只按开发者工具的行为去写逻辑真机一套就发现完全不是一回事。iOS 上RecorderManager.onStop返回的duration是实际录制时长而 Android 部分机型在异常停止时会把duration报告成 0前端需要对duration小于等于 0 的情况做降级处理。iOS 对音频会话的管理也更严格如果录音过程中有电话进来或 Siri 被唤起onInterruptionBegin相关事件会打断录音流程这时页面状态需要自动回到初始状态否则用户回来继续点“停止”会收到onError。代码里应对两种平台的通用做法是在onStop与onError都执行状态复位且不要在onError里弹出阻断性wx.showModal否则高频错误回调会让用户无法操作页面。5.2 自定义加载页与录音页跳转参数传递标题里“修改刚进入的加载页面”这个需求在录音小程序里很常见默认app.json的首屏是pages/index/index但很多同学希望先展示一个带 Logo 的启动加载页等录音核心模块初始化完成后再跳转。这里不推荐在onLoad里用wx.navigateTo做重定向因为用户能看到页面闪烁且加载页会被保留在路由栈中。常见做法是把加载页作为第一个页面在onReady里用wx.redirectTo跳转这样加载页会从路由栈中移除用户按返回键时不会回到启动图。onReady() { setTimeout(() { wx.redirectTo({ url: /pages/record/record }); }, 800); }wx.redirectTo不能跳转到 tabBar 页面如果录音主页是 tab 页需要改用wx.switchTab。加载页停留时间也可以用wx.getLaunchOptionsSync()里的场景值做判断来自分享卡片进入时直接跳过加载页可以让演示流程更流畅。这个技巧在期末答辩时很加分因为很多项目都是静态跳转完全没考虑启动场景差异。5.3 顶栏导航高度与录音页面布局避坑录音按钮的位置如果使用了position: fixed在带有胶囊按钮的微信页面里就要考虑顶部导航栏高度。不同机型的导航栏高度会随系统字体缩放变化可以通过wx.getMenuButtonBoundingClientRect()拿到胶囊的位置信息动态计算自定义导航栏的高度。getNavBarHeight() { const menuRect wx.getMenuButtonBoundingClientRect(); const systemInfo wx.getSystemInfoSync(); this.setData({ navBarHeight: menuRect.top menuRect.height 8, menuRight: menuRect.right }); }拿到的navBarHeight可以直接设置到.nav-placeholder的 style 里避免录音控制区被胶囊按键覆盖。小程序录音页通常需要常亮屏幕防止息屏中断录音这时要额外调用wx.setKeepScreenOn({ keepScreenOn: true })在页面隐藏时关闭。这两个细节都是“不做也能跑做了才专业”的加分项目尤其适合写进期末大作业的演示说明里。6. 微信小程序录音数据导出与验证技巧一份可自查的落盘清单期末大作业不仅要“做得出来”还要“演示得清楚”。建议在录音列表页增加一个“导出所有录音信息”的按钮把所有录音元数据汇总成可读文本通过wx.setClipboardData复制到剪贴板或者生成 JSON 文件写入本地并用wx.shareFileMessage发送。这个功能能直观证明项目的数据落盘是真实可查的而不是只存在内存里的假列表。exportRecordList() { const recordList wx.getStorageSync(recordList) || []; const summary recordList.map(item ({ 文件名: item.path.split(/).pop(), 时长: ${item.duration}ms, 大小: ${(item.size / 1024).toFixed(2)}KB, 录制时间: item.createTime })); wx.setClipboardData({ data: JSON.stringify(summary, null, 2), success: () { wx.showToast({ title: 已复制到剪贴板, icon: success }); } }); }这个导出函数同时承担了自查职责如果recordList里每个文件路径在wx.env.USER_DATA_PATH下都能找到实体文件那数据链路就是完整的。答辩前按四条路径快速自测首次授权弹窗是否出现授权后点击录音按钮recording状态是否变化停止后列表是否新增一条记录且播放正常重启小程序后列表是否能从 Storage 恢复展示。四条全过录音小程序的期末大作业核心评分点基本就都拿到了。剩下一次录音时长建议选 60 秒以内避免存储膨胀答辩现场操作也更干净。本文还有配套的精品资源点击获取
返回列表