资讯动态

H5仿微信语音聊天实现:录音兼容、触摸交互与状态机设计实战

发布时间:2026/10/19 14:36:33 来源:尧图企业网站定制
去年做的一个移动端项目里带有一个仿微信语音聊天的 H5 模块我到现在都还记得那段时间被录音兼容性支配的感觉。这个需求表面看很简单按住说话、松开发送、点击播放几个交互而已。但真正动手之后才发现麦克风权限、录音格式、浏览器自动播放限制、触摸事件冲突、滚动性能这些坑每一个都能让功能“看起来能用实体验很拉垮”。这篇内容把我实际落地的方案完整拆开讲包括交互状态机设计、getUserMedia 与 MediaRecorder 的用法、按住说话的手势处理、播放器状态管理、跨端格式兼容以及最后整理出的一份真实问题排查清单。无论是你在做在线客服、社区聊天还是企业内部沟通工具只要涉及 H5 语音消息应该都能直接参考。1. 功能拆解与技术方案选型1.1 还原微信语音的交互状态机微信语音表面上是一个气泡加一段录音条但拆开之后至少有六个状态空闲、准备录音、正在录音、准备取消、正在播放、播放结束。每一个状态都对应一套 UI 和一套数据。做 H5 版本之前我习惯先把状态机画出来而不是直接写触摸事件。我在项目里定义的状态机是这样的默认是 IDLE用户按下录音按钮进入 RECORDING此时如果手指滑出取消区则进入 WILL_CANCEL松开后回到 IDLE 并丢弃录音如果正常松开则进入 SENDING把音频传给服务端并发送消息消息落地之后气泡处于 UNREAD 或 READ 状态点击气泡进入 PLAYING播放自然结束回到 IDLE。这个状态机的好处在于所有 UI 变化都由状态去驱动而不是在 touchstart、touchmove、touchend 里一遍遍改 class。后来排查“录音断了但界面还停在录音中”的问题时我第一时间就是去查哪个分支状态没有正确回退很快就能定位。如果一开始就散乱地写事件回调这种问题会变得极难追踪。1.2 H5 方案的原委与场景限制有人会问既然要“仿微信”为什么不用原生 SDK。原因很简单我们只需要在微信内置浏览器以及其他第三方浏览器里打开一个 H5 页面就能完成语音对话不想为了这一个功能单独下载 App也没有原生开发资源投入。H5 的边界就在这里浏览器只给你一个录音能力和一个播放能力其余的交互逻辑全部要靠自己堆。移动端 H5 仿微信语音功能的核心 API 很明确navigator.mediaDevices.getUserMedia采集麦克风音频MediaRecorder把音频流编码成文件HTMLAudioElement负责播放。现代安卓 WebView 和 iOS Safari 对这三者的支持已经比较完整不过具体的编码格式和事件行为仍有不少差异这一点后面会专门讲。1.3 技术路线对比纯 H5、原生混合、服务端转码这块我在选型时做了个快速对比本质上不是哪个技术先进而是哪种方式能最快把体验做到可接受方案录音端上传格式播放端优点缺点H5 MediaRecorder 录音浏览器webm/opus 或 mp4H5 播放不用装 SDK开发快格式兼容性要处理原生录音 H5 播放原生 SDKaac/mp3H5 播放音质、格式可控需要原生开发配合H5 录音 服务端转码浏览器原始格式H5 播放服务端统一处理转码链路长实时性差我最终选择了第一个方案也就是纯 H5 MediaRecorder重点解决格式兼容问题。录音端统一采集前端负责触摸交互和上传服务端只做存储和消息分发不参与音频处理。整个链路最短问题最容易定位。服务端转码这条路我评估过适合用户量特别大、产品形态复杂的场景但对一个聊天模块来说过早优化了。1.4 为什么坚持用 Web API 而不是录音插件市面上也有一些 H5 录音插件比如基于 Flash 的旧方案早就废了还有通过 WebSocket 把音频流传给服务端处理的方案但这些都增加了一层代理出问题的时候很难判断是插件的问题还是自己的问题。浏览器原生提供的 getUserMedia 和 MediaRecorder 虽然 API 不算友好但胜在无依赖、可控性高。这一点在处理线上 bug 时特别明显。原生 API 出了问题我可以直接在浏览器调试工具里看错误信息也可以写一段最小复现代码快速验证。用第三方的封装遇到问题就得先去翻它的源码时间成本完全不一样。所以我一般建议团队里如果只是做语音消息这类中等复杂度的功能原生 API 完全够用。2. 录音能力适配与实现详解2.1 权限申请首次进入就申请还是按需申请亲测的结论是按需申请但只给用户一个明确的引导层。微信语音按钮在输入框附近用户第一次点“按住说话”时先弹一个引导层上面写明“需要使用麦克风录制语音消息”然后再触发 getUserMedia。这样用户有心理预期不会觉得浏览器莫名其妙要权限。为什么不能一进页面就申请不是因为技术上做不到而是体验上太冒犯。很多用户点进来只是想看看消息列表结果先弹一个麦克风权限框大概率会点拒绝。而且一旦拒绝浏览器会把该域的麦克风权限记成“拒绝”再想重新请求时很多浏览器会直接不弹窗开发者只能引导用户去设置页手动开启非常被动。获取权限的标准写法async function requestMicAccess() { if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { throw new Error(当前浏览器不支持麦克风采集); } const stream await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true, channelCount: 1 } }); return stream; }这里我特意开了回声消除和降噪实测在嘈杂环境下录出来的声音比不开降噪的明显干净一些。channelCount 设置成 1也就是单声道后面生成的音频文件会小不少对语音消息这类场景完全够用。如果你是做音乐类产品那另说单声道肯定不够但语音消息的核心目标是“听清楚人声”不需要立体声。有一点必须提醒getUserMedia 要求页面是 HTTPS 环境。本地开发用 localhost 可以但一旦上真机联调就必须挂证书或者用内网穿透工具把测试环境变成 HTTPS。否则权限请求压根不会触发这是很多人第一次真机调试时最容易懵的地方。我在某个项目里就遇到过一次代码逻辑完全没错但总是报undefined is not an object最后发现是因为测试环境是 HTTP。2.2 MediaRecorder 初始化格式、码率与兼容检测拿到 stream 之后就可以创建 MediaRecorder 实例了let recorder null; let chunks []; let startTime 0; let timer null; function startRecording(stream) { chunks []; const options { audioBitsPerSecond: 32000 }; // 优先选浏览器默认支持的格式再手动指定 if (typeof MediaRecorder ! undefined) { const mimeType pickSupportedMimeType(); if (mimeType) { options.mimeType mimeType; } } recorder new MediaRecorder(stream, options); recorder.ondataavailable (e) { if (e.data e.data.size 0) { chunks.push(e.data); } }; recorder.onstop handleRecordingComplete; recorder.start(); startTime Date.now(); startTimer(); } function pickSupportedMimeType() { const candidates [ audio/webm;codecsopus, audio/webm, audio/mp4, audio/aac ]; for (let i 0; i candidates.length; i) { if (MediaRecorder.isTypeSupported(candidates[i])) { return candidates[i]; } } return ; }audioBitsPerSecond 我选了 32000也就是 32kbps。语音消息不需要高保真32kbps 的 opus 编码已经完全能听清人声而且一段 60 秒的录音大概只有 240KB比 128kbps 的版本小四倍。如果你的场景对音质要求高可以提升到 64kbps但一般情况下没必要低码率可以明显降低上传压力和 CDN 成本。分段逻辑上我没有用 timeslice 参数而是让录音结束时统一触发 ondataavailable。微信语音是“按住录音松开发送”录音时长通常在 1 秒到 60 秒之间一次请求生成一整段音频即可不需要分片上传。如果真的要录很长的音可以用recorder.start(1000)让每秒钟产生一个 Blob 分片这样中途崩溃也能抢救一部分数据。不过 60 秒以内的语音分片不会带来实际收益。2.3 结束录音生成文件并上传停止录音的代码要考虑优雅停止MediaRecorder 的 stop 是异步的必须等到 onstop 回调里才能拿到完整数据function stopRecording() { if (!recorder || recorder.state inactive) { return; } recorder.stop(); stream.getTracks().forEach((track) track.stop()); // 一定要释放麦克风 } function handleRecordingComplete() { const blob new Blob(chunks, { type: recorder.mimeType || audio/webm }); const duration Math.round((Date.now() - startTime) / 1000); const url URL.createObjectURL(blob); // 后续处理上传、生成气泡、记录时长 }最容易被忽略的是stream.getTracks().forEach(track track.stop())。如果不手动停掉麦克风 track页面上会一直显示“正在使用麦克风”部分手机顶部还会出现红色录音标识很尴尬。而且不释放的话下一次再 getUserMedia 有可能因为占用冲突而失败。我见过有同事开发时只调了recorder.stop()结果连着录几条消息之后麦克风就再也打不开了其实就是 track 没释放导致资源被占满。2.4 录音中的实时时长反馈除了拿到最后的音频按住说话期间还要给用户实时反馈。微信的做法是录音时按钮区域变化、显示计时条、上滑显示取消提示。我这边用 setInterval 每秒更新一次录音时长function startTimer() { stopTimer(); timer setInterval(() { const seconds Math.floor((Date.now() - startTime) / 1000); updateRecordingUI(seconds); }, 500); } function stopTimer() { if (timer) { clearInterval(timer); timer null; } }这里间隔设 500ms 而不是 1000ms是为了让秒数跳变更平滑。虽然最终显示的整数秒是一样的但 500ms 的刷新节奏让计时数字看起来更跟手代价只是多几次 setInterval 回调可以忽略。2.5 录音格式的跨端问题与处理策略移动端浏览器的 MediaRecorder 支持格式不统一。Android Chrome 和多数安卓 WebView 输出audio/webm;codecsopusiOS Safari 则输出audio/mp4或audio/aac。这两个格式不能跨端播放安卓录的 webm 在 iOS 上基本放不出声音iOS 录的 mp4 在部分安卓 WebView 里也播放不了。这个问题有两条解决路径路径一统一前端采集 PCM再编码成 WAV。用AudioContext的AudioWorklet或ScriptProcessorNode拿 PCM 原始数据自己拼 WAV 文件。这个方案兼容性最好WAV 在两端都能放但代码量很大而且 AudioWorklet 在 iOS 老版本上支持不完整。路径二服务端转码。MediaRecorder 拿到 blob 上传给服务端服务端统一转成 mp3 或 aac 再落库。这个方案实现成本最低只需在后端加一个转码接口。缺点是上传的文件格式多样后端要能识别 mp4、webm、opus 等不同容器。我实际采用路径二。录音 blob 上传时带上 sourceType后端按格式转成 mp3 落库前端播放统一用 mp3。这样所有端播放格式一致音质稳定排查起来也很省心。路径一适合没有服务端转码条件、纯前端实现的场景但代价是代码复杂度会上升一个量级。3. 按住说话与滑动手势交互落地3.1 事件选型touch 事件而不是 click微信语音的核心交互是按住说话、松开发送、上滑取消这个只能用 touch 事件click 做不到按住的效果。需要注意的是 touch 和 click 会同时触发所以要在 touchstart 里调用preventDefault()或者在 touchend 里屏蔽后续的合成 click。否则会出现一个很典型的 bug用户按住说话时不仅触发了录音还在松开后触发了一次点击事件把录音按钮点成了“切换到文字输入模式”。我封装了一个 PressRecorder 类管理按下、移动、释放三个动作并对外提供回调class PressRecorder { constructor(options) { this.button options.button; this.startCallback options.onStart; this.cancelCallback options.onCancel; this.sendCallback options.onSend; this.uncancelCallback options.onUncancel; this.cancelDistance options.cancelDistance || 80; this.startY 0; this.cancelled false; this.longPressStarted false; } bind() { this.button.addEventListener(touchstart, this.handleTouchStart.bind(this)); this.button.addEventListener(touchmove, this.handleTouchMove.bind(this)); this.button.addEventListener(touchend, this.handleTouchEnd.bind(this)); this.button.addEventListener(touchcancel, this.handleTouchCancel.bind(this)); } handleTouchStart(e) { e.preventDefault(); this.startY e.touches[0].clientY; this.cancelled false; this.longPressStarted true; this.startCallback this.startCallback(e); } handleTouchMove(e) { if (!this.longPressStarted) return; const moveY e.touches[0].clientY; const distance this.startY - moveY; if (distance this.cancelDistance) { if (!this.cancelled) { this.cancelled true; this.cancelCallback this.cancelCallback(e); } } else if (this.cancelled) { this.cancelled false; // 手指又滑回录音区域需要恢复录音中的 UI this.uncancelCallback this.uncancelCallback(e); } } handleTouchEnd(e) { if (!this.longPressStarted) return; this.longPressStarted false; if (this.cancelled) { this.cancelCallback this.cancelCallback(e); } else { this.sendCallback this.sendCallback(e); } } handleTouchCancel() { this.longPressStarted false; this.cancelCallback this.cancelCallback(); } }这里上滑取消的判断用了 startY 和当前 clientY 的差值也就是手指向上滑动的距离没有依赖按钮自身的坐标区域。微信内部其实还会判断手指是否滑出按钮区域但简化成距离阈值已经够用。阈值我设为 80px因为不同屏幕尺寸下绝对坐标差会很大按距离算更通用。3.2 触摸交互的两个坑第一个坑是 touchcancel。来电、系统通知下拉、指纹支付、页面切后台这些场景浏览器会触发 touchcancel 而不是 touchend。如果只在 touchend 里处理发消息这些场景会造成录音一直不结束、界面一直卡在录音中的 bug。所以我单独处理了 touchcancel统一按取消处理并且确保所有状态都回退到 IDLE。第二个坑是极快速的点击。用户有时候只是在按钮上快速点一下间隔不到 200ms。这种操作不应该真的开始录音否则会生成一堆 0.1 秒的无效语音。我加了一个最小录音时长判断长按不足 300ms 就不发送UI 上提示“说话时间太短”。function handleTouchEnd() { const duration Date.now() - pressStartTime; if (duration 300) { resetRecording(); showToast(说话时间太短); return; } stopAndSend(); }3.3 录音中的视觉反馈与页面滚动处理录音态我做了整套 UI录音按钮切换到按下态上方浮出录音面板面板里是一个麦克风图标和计时器手指上滑时图标变成取消样式并伴随轻微震动感。这个震动在支持navigator.vibrate的安卓机上做了iOS 不支持也没关系只是增强反馈而已。面板的显示和隐藏用 transition 过渡录音面板用一个全屏透明的遮罩层包住。为什么用全屏遮罩因为要保证手指在屏幕上滑动时移动端浏览器不会因为触发了页面滚动而打断 touch 事件。遮罩层配合 touchmove 的 preventDefault能保证整个滑动过程完全可控。实际编码时要注意preventDefault 不能加在 document 上否则可能把整个页面的滚动都禁掉。我是在录音面板的触摸区域上统一处理recordMask.addEventListener(touchmove, (e) { if (isRecording) { e.preventDefault(); } }, { passive: false });passive: false 是关键。现代浏览器默认把 touchmove 监听器当成 passive也就是告诉浏览器“我不会阻止默认行为”这样可以提升滚动性能。但如果我要 preventDefault就必须显式声明 passive: false否则浏览器会直接忽略 preventDefault页面照样滚动取消滑动手势就会失效。4. 语音播放、消息气泡与未读状态管理4.1 点击播放和浏览器自动播放限制播放部分一开始我以为直接用new Audio(url).play()就完事了结果在移动端踩了自动播放限制的坑。浏览器要求用户必须在页面做了点击等手势之后才能播放带声音的媒体。我们的场景是“点击气泡才播放”这本身满足用户手势触发的条件所以正常点击是人没有问题的。麻烦的是消息列表里有一堆语音用户连续点击第一条、第二条、第三条的时候中间的暂停和再播放并不是每次都有新的手势有些浏览器会把第二、第三次 play 当成非手势触发而拦截。我的解决办法是复用一个 Audio 实例而不是每条消息 new 一个const audioPlayer new Audio(); let currentMessageId null; function playVoice(message) { if (currentMessageId message.id) { if (!audioPlayer.paused) { audioPlayer.pause(); setMessageState(currentMessageId, idle); return; } } if (audioPlayer.src) { audioPlayer.pause(); } audioPlayer.src message.audioUrl; audioPlayer.play().catch(() { // 自动播放被拦截提示用户再次点击 showToast(再次点击播放语音); }); currentMessageId message.id; setMessageState(currentMessageId, playing); }复用同一个播放器实例的另一个好处是当用户点击新的一条语音时旧那条一定会停止不会出现两条语音同时响的诡异状态。用户点击正在播放的语音一般约定是暂停这也是上面代码里先判断 currentMessageId 的原因。如果后续需要做“连续播放多条语音”的功能这个单例播放器也更容易扩展只要在 ended 事件里找下一条即可。4.2 播放过程中的音频事件处理播放状态必须要监听 audio 的 timeupdate 和 ended。timeupdate 用来驱动播放动画ended 用来把动画归位并标记已读状态。audioPlayer.addEventListener(timeupdate, () { if (currentMessageId) { const currentTime audioPlayer.currentTime; const duration audioPlayer.duration; updateWaveAnimation(currentMessageId, currentTime, duration); } }); audioPlayer.addEventListener(ended, () { if (currentMessageId) { setMessageState(currentMessageId, idle); markAsRead(currentMessageId); currentMessageId null; } });还有个坑iOS 上duration在音频刚加载完时可能返回 NaN要等loadedmetadata事件之后再获取。如果拿到 NaN 就去计算播放进度会导致气泡动画闪一下然后卡住。我习惯把 duration 保存到 message 对象里录音结束时就已经拿到了时长播放进度直接用这个值不依赖 audio.duration。这样即使 metadata 加载慢动画也不会受影响。4.3 气泡宽度换算与播放动画微信语音气泡的宽度跟时长正相关大概范围是 80px 到 200px。换算我用了线性比例1 秒以内取最短宽度60 秒以上封顶中间每秒增加约 2px。具体公式function getBubbleWidth(duration) { const min 80; const max 200; const ratio 2.2; const width min duration * ratio; return Math.min(max, Math.max(min, Math.round(width))); }播放动画我对比过两种方案。第一种是纯 CSS 动画把一段播放中的音节图标无限循环左右跳动第二种是用 CSS 变量或 JS 驱动一个填充进度条。微信那种双音节跳动在低端机上如果使用 JavaScript 频繁修改样式性能会比较差。我最终使用 CSS animation动画帧限定在 transform 和 opacity 上性能会好很多。进度条则是用 transform: scaleX 实现从 0 渐变到 1同样避免 reflow。4.4 未读小红点的本地标记未读状态不需要后端参与太多只要一条消息收到时默认 unread播放结束后标记为 read并同步到本地 storage。如果后端也要做已读回执则在 ended 事件里调 markRead(messageId) 接口即可。function markAsRead(messageId) { const key voice_read_${sessionId}; const readMap JSON.parse(localStorage.getItem(key) || {}); readMap[messageId] true; localStorage.setItem(key, JSON.stringify(readMap)); // 同时更新页面上的气泡小红点 const bubble document.querySelector([data-message-id${messageId}]); if (bubble) { bubble.classList.remove(unread); } }这里有一点要注意不要用 index 下标当消息的唯一标识。消息列表一旦有删除、插入操作下标会变很容易出现“上一条消息被标记成已读”的错乱。我在实际开发中遇到过后来统一改用服务端返回的消息 ID 作为 key问题才彻底解决。5. 兼容性、性能优化与问题排查记录5.1 最大兼容性坑录音格式的浏览器差异前面提过MediaRecorder 在 Android 上输出 webm/opus在 iOS 上输出 mp4这两种格式不能互换播放。我在这里再补充一个真实排查过程方便你踩到时能快速判断。现象是安卓用户录完音发给 iOS 用户iOS 用户点击语音气泡进度条在走但完全没有声音。iOS 用户录的音发给安卓部分安卓机型同样没声部分机型能放但只有沙沙声。当时我先用浏览器控制台检查 audio 元素的 error 事件发现报的是MEDIA_ELEMENT_ERROR: Format error基本可以确认是格式不兼容。后来在后端加了转码接口后统一把音频转成 mp3这个线上问题就消失了。所以我的建议是如果你的消息可能会在多个端之间互通不要在 H5 层硬扛格式问题去后端加一层转码是最省事的。5.2 典型问题排查速查表我把开发过程中反复出现过的问题整理成了表格方便团队新人排查现象原因解决方案点击录音没反应页面不是 HTTPS或麦克风权限被拒绝确认 HTTPS重新引导授权录音前 0.5 秒没声音部分浏览器在 track 激活后才开始采集录音前先 getUserMedia延迟 start 100msiOS 静音开关拨到静音后播放没声音浏览器遵循静音开关播放时提示用户关闭静音或用 Web Audio 播放有限支持录音时手机自动熄屏录音未阻止休眠录音中请求 Wake Lock API上滑取消时页面跟着滚动未阻止 touchmove 默认行为在录音面板上 touchmove preventDefault播放时两条语音同时响每个消息独立创建了 Audio 实例全局唯一 audioPlayer录音文件太大码率设置过高audioBitsPerSecond 调低到 32000安卓录的语音 iOS 播放无声音webm 与 mp4 容器不互通服务端统一转码 mp3 或 aac5.3 长时间录音的内存与并发上传优化微信限制语音最长 60 秒我们产品也做了同样限制时间到了强制结束并发送。为什么做这个限制一是产品形态合适二是 MediaRecorder 在部分浏览器上长时间录制会积累大量 Blob内存压力很大。一分钟 32kbps 的录音只有几百 KB但如果放任用户录 10 分钟就算内存不爆上传体验也会很差。上传时我用 FormData 直接传 blobfunction uploadVoice(blob, duration, conversationId) { const formData new FormData(); formData.append(file, blob, voice.webm); formData.append(duration, duration); formData.append(conversationId, conversationId); // 用 fetch 或 XHR 上传 }上传过程中UI 会处于“正在发送”的 loading 状态。松开发送后气泡先不发出去而是显示一个本地半透明占位气泡等上传成功再替换成正式气泡。如果上传失败就提示重发不要直接把气泡发出去。否则用户看到自己发了一条语音但对方始终收不到体验非常糟糕。并发上传也要考虑。如果用户快速连发几条语音浏览器会同时建立多个上传请求这在弱网环境里可能互相抢占带宽导致每条都很慢。我在项目中加了简单的队列同一时间最多上传两条其余排队。这个优化虽然不起眼但和“快速连发”场景非常相关。5.4 页面滚动性能与渲染优化语音消息多起来之后滚动性能下降是必然的。我做了三件事第一气泡列表加了虚拟滚动只渲染可视区域内的消息。语音消息的 DOM 结构比纯文本复杂有气泡、时长、未读点、播放动画如果几百条一次性渲染低端机的滚动帧率会直接掉到二十几帧甚至白屏。虚拟滚动实现不难关键是每条消息要有确定的高度语音消息的高度比较稳定适合做虚拟列表。第二所有动画都用 transform 和 opacity不碰 top、left、height。CSS 动画如果触发 reflow滚动时会有明显卡顿。特别是录音面板弹出和气泡跳动我都改成了 transform 实现。第三语音消息的音频 URL 用 CDN 地址或 blob 地址不要用 base64。base64 会额外膨胀约 33% 的体积还会卡渲染主线程。上传完成后直接用返回的 CDN 地址加载音频HTMLAudio 本身就是流式加载不用等整段下载完再播。6. 实测体验优化与返工后的心得6.1 “仿微信”体验清单里容易漏掉的小点很多产品需求只写“仿微信语音功能”如果不把体验清单列清楚做出来就是四不像。我复盘自己这个模块真正提升体验的点包括这些录音时按钮区域高亮并轻微缩放释放时要有回弹效果。微信有一个极短促的缩放动画掉了的话会明显觉得生硬。录音面板里的计时数字采用等宽字体避免数字跳动时宽度变化造成视觉抖动。中文用户可能不太在意但 UI 同学提过好多次。取消录音时用 toast 提示“已取消”不能毫无反馈。播放中的语音气泡要有明显的进度反馈不能只有开始和结束两个状态。哪怕只是底色缓慢填充也比静悄悄的播放体验好很多。新收到的语音消息默认显示红色未读点播放后消失。这个细节看似小但辨识度极高也是微信用户最熟悉的一个记忆点。我个人觉得H5 仿原生语音交互的“手感”比录音质量更影响用户评价。录音质量只要别太差用户都能接受。但松手没有反馈、录音一秒就断、上滑取消时页面跟着滚这类问题用户会立刻感知到而且很容易定性为“这个功能是坏的”。6.2 与后端协作时提前对齐的事项如果只是前端一个人把功能做完再丢给后端很容易返工。我建议提前对齐四件事一是录音文件的最终存储格式mp3 还是 aac。如果后端不转码只是原样存那前端就必须自己解决 webm 和 mp4 的播放兼容。二是时长字段是前端传还是后端解析。我建议前端传 duration后端同时二次解析实际时长两者取较小值防止伪造数据。三是消息顺序的并发问题。语音消息上传速度受网络影响有可能先点发送的反而是后到服务端要约定用本地时间戳或客户端自增 id 排序否则消息列表乱序排查成本极高。四是上传超时策略。弱网下 60 秒录音上传可能需十几秒前端超时要放宽到 30 秒以上并在 UI 上保留重发入口。6.3 后续可以扩展的方向这个 H5 语音模块上线后有几个方向被证明是高频需求语音转文字服务端接 ASR把文本作为消息附带的字段下发前端在气泡下方展示转写结果多级手势把上滑取消做成更细腻的幂等逻辑避免手指在取消区边缘反复横跳时动画来回闪烁音质自适应弱网时自动降码率强网时用更高的码率。我最看好语音转文字因为语音消息阅读成本高有文字附带后消息打开率会有明显提升。不过那是另一个项目了这里不再展开。写到最后再把这次移动端 H5 仿微信语音功能的经验浓缩一下录音、播放这些浏览器 API 是基本功真正决定功能成败的是那个看不见的状态机、对触摸事件的细心处理以及浏览器兼容性的穷举。做这类功能别急着铺代码先把状态流转想清楚把兼容性问题列表列好后面的实现就是顺水推舟。真踩到坑的时候也别慌把现象记下来用最小示例去验证原因多数都是格式或者权限的问题。

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

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

免费获取报价 →
↑