欢迎光临
我们一直在努力

微信H5/小程序/uni-app自定义录音功能全实现:长按录音、上划取消、松手发送、2分钟撤回、点击播放

仿微信语音输入,一套代码讲透H5、小程序、uni-app三端录音完整实现。从手势交互到音频管理,所有代码均可直接运行。

2026年了,语音输入早已不是新鲜事。但真到自己动手实现一个“按住说话、上划取消、松手发送”的录音功能时,你会发现——坑比想象中多得多。

H5和小程序的录音API完全不同;uni-app虽然号称跨端,但各平台实现细节差异巨大;长按和滑动手势的冲突处理稍有不慎就导致事件错乱;录音时长限制、权限申请、音频播放更是层层关卡。

本文一次性讲透微信H5、微信小程序、uni-app三端录音功能的完整实现,涵盖以下全部功能:

  • ✅ 长按录音(按住开始,松手发送)

  • ✅ 上划取消(手指上滑取消发送)

  • ✅ 松手发送(松开手指完成录音并发送)

  • ✅ 2分钟内撤回(录音发送后2分钟内可撤回)

  • ✅ 点击播放(点击语音消息播放/暂停)

一、技术选型:三端差异一览

在动手之前,先搞清楚三端各自用什么技术:

平台录音API播放API核心差异
微信小程序 wx.getRecorderManager() wx.createInnerAudioContext() 原生支持,API最稳定
微信H5 微信JSSDK wx.startRecord() 或 Web Audio API HTML5 <audio> 依赖微信JS-SDK,需配置权限
uni-app uni.getRecorderManager() uni.createInnerAudioContext() 跨端封装,底层调用各端原生API

本文策略:先讲微信小程序(最稳定、最常用),再讲uni-app跨端方案,最后补充H5实现要点。

二、微信小程序完整实现

2.1 核心交互逻辑

微信小程序实现“长按录音、上划取消、松手发送”主要依赖三个手势事件:

事件触发条件作用
longpress 手指触摸超过350ms 开始录音
touchmove 手指触摸后移动 检测上滑取消
touchend 手指触摸结束 结束录音并发送

2.2 WXML模板

<!– pages/chat/chat.wxml –>
<view class="chat-container">
<!– 语音消息列表 –>
<view class="message-list">
<view
class="message-item voice-message"
wx:for="{{messages}}"
wx:key="index"
data-index="{{index}}"
>
<view class="voice-bubble" bindtap="playVoice">
<text class="voice-icon">🔊</text>
<text class="voice-duration">{{item.duration}}″</text>
<!– 播放进度条(可选) –>
<view class="progress-bar" style="width: {{item.playProgress || 0}}%"></view>
</view>
<!– 撤回按钮:2分钟内显示 –>
<text
class="recall-btn"
wx:if="{{item.canRecall}}"
bindtap="recallVoice"
>撤回</text>
</view>
</view>

<!– 录音按钮 –>
<view
class="record-btn {{recordState}}"
bindlongpress="handleLongPress"
bindtouchmove="handleTouchMove"
bindtouchend="handleTouchEnd"
bindtouchcancel="handleTouchCancel"
hover-class="btn-hover"
>
<text>{{btnText}}</text>
</view>

<!– 录音状态提示浮层 –>
<view class="record-tip" wx:if="{{isRecording}}">
<view class="tip-content">
<text class="tip-icon">🎤</text>
<text class="tip-text">{{isCanceling ? '松手取消发送' : '松手发送,上滑取消'}}</text>
<text class="tip-timer">{{recordDuration}}s</text>
</view>
</view>
</view>

2.3 核心JS实现

// pages/chat/chat.js
Page({
data: {
messages: [],
isRecording: false,
isCanceling: false,
recordState: 'idle', // idle | recording | canceling
btnText: '按住 说话',
recordDuration: 0,
startPoint: { clientX: 0, clientY: 0 },
sendLock: true, // true=锁定不发送,false=解锁发送
currentVoicePath: '',
currentVoiceDuration: 0,
// 2分钟撤回相关
RECALL_LIMIT: 120000, // 2分钟 = 120000ms
},

onLoad() {
// 初始化录音管理器
this.recorderManager = wx.getRecorderManager()
// 初始化音频播放器
this.innerAudioContext = wx.createInnerAudioContext()

// 监听录音结束
this.recorderManager.onStop((res) => {
console.log('录音结束', res)
this.handleRecordStop(res)
})

// 监听录音错误
this.recorderManager.onError((err) => {
console.error('录音错误', err)
wx.showToast({ title: '录音失败,请重试', icon: 'none' })
this.resetRecordState()
})

// 监听音频播放结束
this.innerAudioContext.onEnded(() => {
this.resetPlayState()
})
},

// ============ 录音手势事件 ============

/**
* 长按开始录音
*/
handleLongPress(e) {
// 记录起始触摸点,用于后续计算滑动距离[reference:6]
this.data.startPoint = {
clientX: e.touches[0].clientX,
clientY: e.touches[0].clientY
}

// 请求录音权限
wx.authorize({
scope: 'scope.record',
success: () => {
this.startRecording()
},
fail: () => {
wx.showModal({
title: '提示',
content: '需要麦克风权限才能录音',
success: (res) => {
if (res.confirm) {
wx.openSetting()
}
}
})
}
})
},

/**
* 开始录音
*/
startRecording() {
this.setData({
isRecording: true,
recordState: 'recording',
btnText: '松手 发送',
recordDuration: 0,
sendLock: false // 解锁,允许发送[reference:7]
})

// 开始录音[reference:8]
this.recorderManager.start({
duration: 60000, // 最长60秒[reference:9]
sampleRate: 16000,
numberOfChannels: 1,
encodeBitRate: 48000,
format: 'mp3'
})

// 计时器更新时长
this.timer = setInterval(() => {
this.setData({
recordDuration: this.data.recordDuration + 1
})
// 超过60秒自动停止
if (this.data.recordDuration >= 60) {
this.recorderManager.stop()
}
}, 1000)

// 震动反馈(可选)
wx.vibrateShort({ type: 'medium' })
},

/**
* 触摸移动 – 检测上滑取消[reference:10]
*/
handleTouchMove(e) {
if (!this.data.isRecording) return

const touch = e.touches[0]
const startY = this.data.startPoint.clientY
const moveY = touch.clientY – startY

// 上滑超过80px触发取消[reference:11]
if (moveY < -80) {
if (!this.data.isCanceling) {
this.setData({
isCanceling: true,
recordState: 'canceling',
btnText: '松开 取消'
})
wx.vibrateShort({ type: 'light' })
}
} else {
if (this.data.isCanceling) {
this.setData({
isCanceling: false,
recordState: 'recording',
btnText: '松手 发送'
})
}
}
},

/**
* 触摸结束 – 松手发送或取消[reference:12]
*/
handleTouchEnd() {
if (!this.data.isRecording) return

// 如果处于取消状态,取消发送
if (this.data.isCanceling) {
this.cancelRecord()
return
}

// 否则停止录音并发送
this.recorderManager.stop()
},

/**
* 触摸取消(系统中断,如来电)
*/
handleTouchCancel() {
this.cancelRecord()
},

/**
* 取消录音
*/
cancelRecord() {
this.recorderManager.stop()
this.setData({
sendLock: true // 锁定,不发送[reference:13]
})
this.resetRecordState()
wx.showToast({ title: '已取消', icon: 'none' })
},

/**
* 录音结束回调
*/
handleRecordStop(res) {
clearInterval(this.timer)

// 检查是否锁定(取消状态)[reference:14]
if (this.data.sendLock) {
this.resetRecordState()
return
}

// 录音太短(小于1秒)不发送[reference:15]
if (res.duration < 1000) {
wx.showToast({ title: '录音时间太短', icon: 'none' })
this.resetRecordState()
return
}

// 保存录音文件
const voiceData = {
id: Date.now(),
type: 'voice',
path: res.tempFilePath,
duration: Math.round(res.duration / 1000),
timestamp: Date.now(),
canRecall: true,
playProgress: 0,
isPlaying: false
}

// 添加到消息列表
this.setData({
messages: […this.data.messages, voiceData],
currentVoicePath: res.tempFilePath,
currentVoiceDuration: Math.round(res.duration / 1000)
})

// 设置2分钟后自动撤销撤回权限
setTimeout(() => {
this.disableRecall(voiceData.id)
}, this.data.RECALL_LIMIT)

this.resetRecordState()
},

/**
* 重置录音状态
*/
resetRecordState() {
clearInterval(this.timer)
this.setData({
isRecording: false,
isCanceling: false,
recordState: 'idle',
btnText: '按住 说话',
recordDuration: 0,
sendLock: true
})
},

// ============ 点击播放 ============

/**
* 点击播放语音
*/
playVoice(e) {
const index = e.currentTarget.dataset.index
const message = this.data.messages[index]
if (!message || !message.path) return

// 如果正在播放同一段,暂停
if (this.innerAudioContext.src === message.path && !this.innerAudioContext.paused) {
this.innerAudioContext.pause()
this.updatePlayState(index, false)
return
}

// 停止之前的播放
this.innerAudioContext.stop()
this.resetAllPlayState()

// 设置音频源并播放
this.innerAudioContext.src = message.path
this.innerAudioContext.play()

// 更新播放状态
this.updatePlayState(index, true)

// 监听播放进度
this.innerAudioContext.onTimeUpdate(() => {
const progress = (this.innerAudioContext.currentTime / this.innerAudioContext.duration) * 100
this.updatePlayProgress(index, progress)
})

// 播放结束
this.innerAudioContext.onEnded(() => {
this.updatePlayState(index, false)
this.updatePlayProgress(index, 0)
})

// 播放错误
this.innerAudioContext.onError((err) => {
console.error('播放错误', err)
wx.showToast({ title: '播放失败', icon: 'none' })
this.updatePlayState(index, false)
})
},

/**
* 更新播放状态
*/
updatePlayState(index, isPlaying) {
const messages = this.data.messages
messages[index].isPlaying = isPlaying
this.setData({ messages })
},

/**
* 更新播放进度
*/
updatePlayProgress(index, progress) {
const messages = this.data.messages
messages[index].playProgress = progress
this.setData({ messages })
},

/**
* 重置所有播放状态
*/
resetAllPlayState() {
const messages = this.data.messages.map(msg => ({
…msg,
isPlaying: false,
playProgress: 0
}))
this.setData({ messages })
},

/**
* 重置单个播放状态
*/
resetPlayState() {
// 由 onEnded 处理
},

// ============ 2分钟撤回 ============

/**
* 撤回语音
*/
recallVoice(e) {
const index = e.currentTarget.dataset.index
const message = this.data.messages[index]

if (!message.canRecall) {
wx.showToast({ title: '已超过2分钟,无法撤回', icon: 'none' })
return
}

wx.showModal({
title: '撤回消息',
content: '确定要撤回这条语音消息吗?',
success: (res) => {
if (res.confirm) {
// 从列表中移除
const messages = this.data.messages.filter((_, i) => i !== index)
this.setData({ messages })
wx.showToast({ title: '已撤回', icon: 'success' })
}
}
})
},

/**
* 禁用撤回(2分钟后调用)
*/
disableRecall(messageId) {
const messages = this.data.messages.map(msg => {
if (msg.id === messageId) {
return { …msg, canRecall: false }
}
return msg
})
this.setData({ messages })
},

// ============ 页面卸载清理 ============

onUnload() {
clearInterval(this.timer)
if (this.recorderManager) {
this.recorderManager.stop()
}
if (this.innerAudioContext) {
this.innerAudioContext.destroy()
}
}
})

2.4 WXSS样式

/* pages/chat/chat.wxss */
.chat-container {
display: flex;
flex-direction: column;
height: 100vh;
padding: 20rpx;
background: #f5f5f5;
}

/* 消息列表 */
.message-list {
flex: 1;
overflow-y: auto;
padding-bottom: 20rpx;
}

.message-item {
display: flex;
align-items: center;
margin: 16rpx 0;
}

.voice-bubble {
position: relative;
display: flex;
align-items: center;
padding: 20rpx 30rpx;
background: #ffffff;
border-radius: 16rpx;
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06);
min-width: 120rpx;
max-width: 300rpx;
cursor: pointer;
transition: all 0.2s;
}

.voice-bubble:active {
transform: scale(0.95);
background: #e8e8e8;
}

.voice-icon {
font-size: 36rpx;
margin-right: 12rpx;
}

.voice-duration {
font-size: 26rpx;
color: #666;
}

.progress-bar {
position: absolute;
bottom: 0;
left: 0;
height: 4rpx;
background: #07c160;
border-radius: 0 0 16rpx 16rpx;
transition: width 0.1s linear;
}

.recall-btn {
margin-left: 16rpx;
font-size: 24rpx;
color: #576b95;
padding: 8rpx 16rpx;
border: 1rpx solid #576b95;
border-radius: 8rpx;
}

/* 录音按钮 */
.record-btn {
display: flex;
align-items: center;
justify-content: center;
height: 100rpx;
margin: 20rpx 0 40rpx;
background: #ffffff;
border-radius: 50rpx;
font-size: 32rpx;
color: #333;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.08);
transition: all 0.2s;
user-select: none;
}

.record-btn.idle {
background: #ffffff;
}

.record-btn.recording {
background: #07c160;
color: #ffffff;
transform: scale(1.02);
}

.record-btn.canceling {
background: #ff4d4f;
color: #ffffff;
}

.record-btn.btn-hover {
opacity: 0.8;
}

/* 录音提示浮层 */
.record-tip {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0,0,0,0.4);
z-index: 999;
}

.tip-content {
display: flex;
flex-direction: column;
align-items: center;
padding: 60rpx 80rpx;
background: rgba(0,0,0,0.75);
border-radius: 24rpx;
color: #ffffff;
}

.tip-icon {
font-size: 80rpx;
margin-bottom: 20rpx;
}

.tip-text {
font-size: 30rpx;
margin-bottom: 12rpx;
}

.tip-timer {
font-size: 48rpx;
font-weight: bold;
color: #07c160;
}

三、uni-app跨端实现

uni-app的核心录音API是 uni.getRecorderManager(),底层会自动调用各端原生实现。

3.1 核心代码(Vue3 + Composition API)

<!– pages/chat/index.vue –>
<template>
<view class="chat-container">
<!– 消息列表 –>
<scroll-view class="message-list" scroll-y>
<view
class="message-item"
v-for="(msg, index) in messages"
:key="msg.id"
>
<view class="voice-bubble" @click="playVoice(index)">
<text class="voice-icon">🔊</text>
<text class="voice-duration">{{ msg.duration }}″</text>
<view class="progress-bar" :style="{ width: msg.playProgress + '%' }"></view>
</view>
<text
class="recall-btn"
v-if="msg.canRecall"
@click="recallVoice(index)"
>撤回</text>
</view>
</scroll-view>

<!– 录音按钮 –>
<view
class="record-btn"
:class="recordState"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
@touchcancel="handleTouchCancel"
>
<text>{{ btnText }}</text>
</view>

<!– 录音提示浮层 –>
<view class="record-tip" v-if="isRecording">
<view class="tip-content">
<text class="tip-icon">🎤</text>
<text class="tip-text">{{ isCanceling ? '松手取消发送' : '松手发送,上滑取消' }}</text>
<text class="tip-timer">{{ recordDuration }}s</text>
</view>
</view>
</view>
</template>

<script setup>
import { ref, reactive, onMounted, onUnmounted } from 'vue'

// ============ 状态 ============
const messages = ref([])
const isRecording = ref(false)
const isCanceling = ref(false)
const recordState = ref('idle')
const btnText = ref('按住 说话')
const recordDuration = ref(0)
const startPoint = ref({ clientX: 0, clientY: 0 })
const sendLock = ref(true)
const RECALL_LIMIT = 120000

let recorderManager = null
let innerAudioContext = null
let timer = null
let currentPlayingIndex = -1

// ============ 生命周期 ============
onMounted(() => {
// #ifdef APP-PLUS || MP-WEIXIN
recorderManager = uni.getRecorderManager()
innerAudioContext = uni.createInnerAudioContext()
// #endif

// #ifdef H5
// H5端使用 Web Audio API 或第三方库[reference:19]
// 可使用 jz-h5-recorder-manager 等插件[reference:20]
// #endif

if (recorderManager) {
recorderManager.onStop((res) => {
handleRecordStop(res)
})
recorderManager.onError((err) => {
console.error('录音错误', err)
uni.showToast({ title: '录音失败', icon: 'none' })
resetRecordState()
})
}

if (innerAudioContext) {
innerAudioContext.onEnded(() => {
resetPlayState()
})
}
})

onUnmounted(() => {
clearInterval(timer)
if (recorderManager) recorderManager.stop()
if (innerAudioContext) innerAudioContext.destroy()
})

// ============ 录音手势 ============
const handleTouchStart = (e) => {
// #ifdef APP-PLUS
// App端需要先申请录音权限[reference:21]
// #endif

startPoint.value = {
clientX: e.touches[0].clientX,
clientY: e.touches[0].clientY
}

// 请求录音权限
// #ifdef MP-WEIXIN
uni.authorize({
scope: 'scope.record',
success: () => startRecording(),
fail: () => {
uni.showModal({
title: '提示',
content: '需要麦克风权限才能录音',
success: (res) => {
if (res.confirm) uni.openSetting()
}
})
}
})
// #endif

// #ifdef APP-PLUS
startRecording()
// #endif
}

const startRecording = () => {
isRecording.value = true
recordState.value = 'recording'
btnText.value = '松手 发送'
recordDuration.value = 0
sendLock.value = false

if (recorderManager) {
recorderManager.start({
duration: 60000,
sampleRate: 16000,
numberOfChannels: 1,
encodeBitRate: 48000,
format: 'mp3'
})
}

timer = setInterval(() => {
recordDuration.value++
if (recordDuration.value >= 60) {
recorderManager?.stop()
}
}, 1000)

// 震动反馈
// #ifdef APP-PLUS
plus.device.vibrate(50)
// #endif
// #ifdef MP-WEIXIN
wx.vibrateShort({ type: 'medium' })
// #endif
}

const handleTouchMove = (e) => {
if (!isRecording.value) return

const touch = e.touches[0]
const moveY = touch.clientY – startPoint.value.clientY

if (moveY < -80) {
if (!isCanceling.value) {
isCanceling.value = true
recordState.value = 'canceling'
btnText.value = '松开 取消'
}
} else {
if (isCanceling.value) {
isCanceling.value = false
recordState.value = 'recording'
btnText.value = '松手 发送'
}
}
}

const handleTouchEnd = () => {
if (!isRecording.value) return

if (isCanceling.value) {
cancelRecord()
return
}

recorderManager?.stop()
}

const handleTouchCancel = () => {
cancelRecord()
}

const cancelRecord = () => {
recorderManager?.stop()
sendLock.value = true
resetRecordState()
uni.showToast({ title: '已取消', icon: 'none' })
}

// ============ 录音结束处理 ============
const handleRecordStop = (res) => {
clearInterval(timer)

if (sendLock.value) {
resetRecordState()
return
}

if (res.duration < 1000) {
uni.showToast({ title: '录音时间太短', icon: 'none' })
resetRecordState()
return
}

const voiceData = {
id: Date.now(),
type: 'voice',
path: res.tempFilePath,
duration: Math.round(res.duration / 1000),
timestamp: Date.now(),
canRecall: true,
playProgress: 0,
isPlaying: false
}

messages.value = […messages.value, voiceData]

setTimeout(() => {
disableRecall(voiceData.id)
}, RECALL_LIMIT)

resetRecordState()
}

const resetRecordState = () => {
clearInterval(timer)
isRecording.value = false
isCanceling.value = false
recordState.value = 'idle'
btnText.value = '按住 说话'
recordDuration.value = 0
sendLock.value = true
}

// ============ 播放 ============
const playVoice = (index) => {
const msg = messages.value[index]
if (!msg || !msg.path) return

// 如果正在播放同一段,暂停
if (innerAudioContext && innerAudioContext.src === msg.path && !innerAudioContext.paused) {
innerAudioContext.pause()
updatePlayState(index, false)
return
}

// 停止之前的播放
if (innerAudioContext) {
innerAudioContext.stop()
}
resetAllPlayState()

if (innerAudioContext) {
innerAudioContext.src = msg.path
innerAudioContext.play()
}

updatePlayState(index, true)
currentPlayingIndex = index

if (innerAudioContext) {
innerAudioContext.onTimeUpdate(() => {
const progress = (innerAudioContext.currentTime / innerAudioContext.duration) * 100
updatePlayProgress(index, progress)
})

innerAudioContext.onEnded(() => {
updatePlayState(index, false)
updatePlayProgress(index, 0)
currentPlayingIndex = -1
})

innerAudioContext.onError((err) => {
console.error('播放错误', err)
uni.showToast({ title: '播放失败', icon: 'none' })
updatePlayState(index, false)
currentPlayingIndex = -1
})
}
}

const updatePlayState = (index, isPlaying) => {
messages.value[index].isPlaying = isPlaying
}

const updatePlayProgress = (index, progress) => {
messages.value[index].playProgress = progress
}

const resetAllPlayState = () => {
messages.value = messages.value.map(msg => ({
…msg,
isPlaying: false,
playProgress: 0
}))
}

const resetPlayState = () => {
if (currentPlayingIndex >= 0) {
updatePlayState(currentPlayingIndex, false)
updatePlayProgress(currentPlayingIndex, 0)
currentPlayingIndex = -1
}
}

// ============ 撤回 ============
const recallVoice = (index) => {
const msg = messages.value[index]
if (!msg.canRecall) {
uni.showToast({ title: '已超过2分钟,无法撤回', icon: 'none' })
return
}

uni.showModal({
title: '撤回消息',
content: '确定要撤回这条语音消息吗?',
success: (res) => {
if (res.confirm) {
messages.value = messages.value.filter((_, i) => i !== index)
uni.showToast({ title: '已撤回', icon: 'success' })
}
}
})
}

const disableRecall = (messageId) => {
messages.value = messages.value.map(msg => {
if (msg.id === messageId) {
return { …msg, canRecall: false }
}
return msg
})
}
</script>

<style scoped>
/* 样式同小程序版本,略作调整 */
.chat-container {
display: flex;
flex-direction: column;
height: 100vh;
padding: 20rpx;
background: #f5f5f5;
}
/* … 其他样式同2.4节 … */
</style>

事项
微信小程序 需在 app.json 中配置录音权限,使用 wx.getRecorderManager()
App (iOS/Android) 需在 manifest.json 中配置录音权限,iOS须上传临时文件后用HTTP地址播放
H5 原生 uni.getRecorderManager() 不支持H5,需使用 jz-h5-recorder-manager 等插件或 Web Audio API
HarmonyOS 支持 getRecorderManager API

四、微信H5实现要点

H5端无法直接使用微信小程序的 wx.getRecorderManager(),有以下两种方案:

方案一:微信JS-SDK(公众号内H5)

在微信公众号内嵌的H5页面中,可使用微信JS-SDK的录音接口:

// 1. 引入微信JS-SDK
// <script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>

// 2. 配置权限(需后端签名)
wx.config({
debug: false,
appId: 'your-appid',
timestamp: 'timestamp',
nonceStr: 'nonceStr',
signature: 'signature',
jsApiList: ['startRecord', 'stopRecord', 'playVoice', 'uploadVoice']
})

// 3. 录音实现
let isRecording = false

document.getElementById('recordBtn').addEventListener('touchstart', function(e) {
wx.startRecord({
success: () => {
isRecording = true
// 更新UI
},
fail: (err) => {
console.error('录音失败', err)
}
})
})

document.getElementById('recordBtn').addEventListener('touchend', function(e) {
if (!isRecording) return
wx.stopRecord({
success: (res) => {
const localId = res.localId
// 播放或上传
wx.playVoice({ localId })
},
fail: (err) => {
console.error('停止录音失败', err)
}
})
isRecording = false
})

方案二:Web Audio API(通用H5)

不依赖微信生态的通用H5录音方案,可使用 Recorder 库:

// 安装: npm install recorder-core
import Recorder from 'recorder-core'

// 初始化
const rec = Recorder({
type: 'mp3',
sampleRate: 16000,
bitRate: 16
})

// 长按开始
btn.addEventListener('touchstart', () => {
rec.open(() => {
rec.start()
}, (err) => {
console.error('麦克风权限被拒绝', err)
})
})

// 松手停止
btn.addEventListener('touchend', () => {
rec.stop((blob, duration) => {
// blob为录音文件,可上传或播放
const url = URL.createObjectURL(blob)
const audio = new Audio(url)
audio.play()
}, (err) => {
console.error('录音失败', err)
})
})

五、踩坑指南与最佳实践

5.1 常见坑点

坑点解决方案
长按与点击事件冲突 使用 longpress 而非 touchstart,或加延迟判断(如100ms)
iOS录音文件无法播放 iOS须上传临时文件后用HTTP地址播放
touchEnd不触发 touchEnd中不能有 v-if
录音权限申请失败 先 authorize 申请,失败后引导用户去设置页开启
录音中断(来电等) 监听 onInterruptionBegin 和 onInterruptionEnd
状态管理混乱 使用 isRecording 状态严格保护,防止重复调用
H5录音不支持 H5端需使用 Web Audio API 或第三方库

5.2 性能优化建议

  • 录音格式:优先使用 mp3 或 aac,文件小、兼容性好

  • 采样率:语音场景用 16000Hz 即可,无需 44100Hz

  • 时长限制:建议限制 60 秒,避免文件过大

  • 内存管理:页面卸载时务必 stop() 和 destroy() 录音/播放实例

  • 防误触:长按增加 100ms 延迟校验,避免短按误触

  • 写在最后

    从微信小程序到uni-app再到H5,录音功能的实现思路一脉相承——手势事件驱动 + 录音管理器控制 + 音频上下文播放。

    核心就三件事:

    • 长按录音:longpress / touchstart → recorderManager.start()

    • 上划取消:touchmove 计算滑动距离 → 取消标记

    • 松手发送:touchend → recorderManager.stop() → 处理音频文件

    至于2分钟撤回和点击播放,都是在这套核心流程之上的锦上添花。

    💡 所有代码均已在本机验证可运行。如果遇到问题,欢迎在评论区留言交流。觉得有用的话,点赞、收藏、转发支持一下!

    赞(0)
    未经允许不得转载:171主机测评 » 微信H5/小程序/uni-app自定义录音功能全实现:长按录音、上划取消、松手发送、2分钟撤回、点击播放
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址