React录音转文字组件分享
完整的代码见本系列第三篇:
react-wavesurfer录音组件3:全部代码

1. 完整的产品需求
功能需求
- 录音功能:支持用户通过浏览器麦克风录制音频
- 音频可视化:使用波形图展示录制或上传的音频
- 音频播放控制:支持播放、暂停音频
- 音频管理:支持删除已录制的音频
- 音频转文字:将录制的音频转换为文字内容
- 状态反馈:实时显示录音时长、转文字进度等状态
- 浏览器兼容:检查浏览器对录音功能的支持情况
技术需求
- 响应式设计:适配不同屏幕尺寸
- 错误处理:完善的错误捕获和用户提示
- 资源管理:合理管理Blob URL、WaveSurfer实例等资源
- 组件化:支持受控组件模式,可与父组件通信
- 用户体验:提供直观的操作界面和状态反馈
业务需求
- 第三方服务集成:集成ASR(自动语音识别)服务
- 数据流转:支持将音频数据和识别结果传递给父组件
- 禁用状态:支持组件禁用,用于只读场景
2. 实现逻辑
核心流程
用户操作 → 录音开始 → 实时波形显示 → 录音结束 →
音频处理 → 自动转文字 → 结果显示 → 播放/删除控制
数据流设计
父组件状态 → value prop → 组件内部状态 →
onChange回调 → 更新父组件状态 → onTranscriptionResult回调
状态管理策略
- 本地状态优先:组件内部管理录音、播放等状态
- 受控模式支持:可通过props从外部控制
- 状态同步:监听外部value变化保持同步
3. 实现步骤
步骤1:搭建基础框架
1.1 定义接口类型
typescript
// 音频数据结构:包含原始Blob、播放URL和WaveSurfer实例
interface AudioData {
blob: Blob;
url: string;
wavesurfer: any;
}
// 组件属性:支持受控组件模式、回调函数等
interface AudioRecorderProps {
value?: AudioData | null;
onChange?: (value: AudioData | null) => void;
disabled?: boolean;
onTranscriptionResult?: (text: string) => void;
}
1.2 创建组件函数和基础状态
typescript
const AudioRecorder: React.FC<AudioRecorderProps> = ({
value,
onChange,
disabled = false,
onTranscriptionResult,
}) => {
// 基础状态
const [recording, setRecording] = useState(false);
const [playing, setPlaying] = useState(false);
const [audio, setAudio] = useState<AudioData | null>(value || null);
const [recordingTime, setRecordingTime] = useState<number>(0);
// 转文字相关状态
const [transcribing, setTranscribing] = useState(false);
const [transcriptionResult, setTranscriptionResult] = useState<string>('');
// DOM和实例引用
const waveformRef = useRef<HTMLDivElement>(null);
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
const audioChunksRef = useRef<Blob[]>([]);
const timerRef = useRef<NodeJS.Timeout | null>(null);
// ... 后续功能实现
};
1.3 搭建UI骨架
javascript
return (
<div className={styles.audioRecorderWrapper}>
{/* 录音按钮 */}
<button
onClick={toggleRecording}
className={`${styles.recordButton} ${recording ? styles.recording : ''}`}
disabled={disabled}
type="button"
>
<span className={styles.buttonIcon}>{recording ? '●' : '🎤'}</span>
<span className={styles.buttonText}>
{recording ? `停止 (${formatTime(recordingTime)})` : '开始录音'}
</span>
</button>
{/* 波形显示区域 */}
{audio && !recording && (
<div className={styles.audioContainer}>
<div ref={waveformRef} className={styles.waveform} />
<div className={styles.controlButtons}>
<button onClick={togglePlay} className={styles.playButton}>
{playing ? '⏸' : '▶'}
</button>
<button onClick={deleteAudio} className={styles.deleteButton}>
✕
</button>
</div>
</div>
)}
{/* 转文字功能和结果显示 */}
{audio && !recording && !transcriptionResult && (
<div className={styles.transcribeContainer}>
<button onClick={transcribeAudio}>
{transcribing ? '转文字中...' : '录音转文字'}
</button>
</div>
)}
{transcriptionResult && (
<div className={styles.transcriptionResult}>
{/* 结果展示 */}
</div>
)}
{/* 状态信息 */}
<div className={styles.statusInfo}>
{recording ? '录音中...' : audio ? '✅ 已录制音频' : '请点击按钮录制音频'}
</div>
</div>
);
步骤2:实现录音功能
2.1 检查浏览器支持
typescript
const checkBrowserSupport = (): boolean => {
if (!navigator.mediaDevices?.getUserMedia) {
alert('浏览器不支持录音,请使用 Chrome/Edge/Firefox 并在 localhost 或 HTTPS 下打开');
return false;
}
return true;
};
2.2 开始录音
typescript
const startRecording = async () => {
if (disabled) return;
if (!checkBrowserSupport()) return;
setRecording(true);
setRecordingTime(0);
audioChunksRef.current = [];
setTranscriptionResult(''); // 清除之前的转文字结果
try {
// 获取麦克风权限和流
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
},
});
// 创建MediaRecorder实例
const mediaRecorder = new MediaRecorder(stream, {
mimeType: 'audio/webm;codecs=opus',
});
mediaRecorderRef.current = mediaRecorder;
// 收集录音数据块
mediaRecorder.ondataavailable = (e) => {
if (e.data.size > 0) {
audioChunksRef.current.push(e.data);
}
};
// 录音结束处理
mediaRecorder.onstop = async () => {
const blob = new Blob(audioChunksRef.current, {
type: mediaRecorder.mimeType || 'audio/webm',
});
const url = URL.createObjectURL(blob);
// 清理之前的音频资源
if (audio?.wavesurfer) {
audio.wavesurfer.destroy();
}
// 创建新的音频数据
const newAudioData = { blob, url, wavesurfer: null };
setAudio(newAudioData);
onChange?.(newAudioData);
// 停止麦克风流
stream.getTracks().forEach((track) => track.stop());
// 清理计时器
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
};
// 开始录音
mediaRecorder.start();
// 录音计时器
timerRef.current = setInterval(() => {
setRecordingTime((prev) => prev + 1);
}, 1000);
} catch (err: any) {
console.error('获取麦克风失败', err);
setRecording(false);
// 错误处理和清理
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
// 分类错误提示
let errorMessage = '获取麦克风失败,请检查权限';
if (err.name === 'NotAllowedError') {
errorMessage = '麦克风权限被拒绝';
} else if (err.name === 'NotFoundError') {
errorMessage = '未找到麦克风设备';
}
message.error(errorMessage);
}
};
2.3 停止录音
typescript
const stopRecording = () => {
if (mediaRecorderRef.current && recording) {
mediaRecorderRef.current.stop();
setRecording(false);
transcribeAudio(); // 录音结束后自动转文字
}
};
// 录音切换函数
const toggleRecording = async () => {
if (disabled) return;
if (!recording) {
await startRecording();
} else {
stopRecording();
}
};
步骤3:音频可视化与播放
3.1 初始化WaveSurfer
typescript
useEffect(() => {
if (audio?.url && !audio.wavesurfer && waveformRef.current) {
// 创建WaveSurfer实例
const wavesurfer = WaveSurfer.create({
container: waveformRef.current,
waveColor: '#4a5568',
progressColor: '#4299e1',
cursorColor: '#4299e1',
height: 40,
barWidth: 1,
barGap: 1,
responsive: true,
normalize: true,
});
// 加载音频
wavesurfer.load(audio.url);
// 绑定播放状态事件
wavesurfer.on('finish', () => {
setPlaying(false);
});
wavesurfer.on('pause', () => {
setPlaying(false);
});
wavesurfer.on('play', () => {
setPlaying(true);
});
// 更新状态,包含wavesurfer实例
const newAudioData = { ...audio, wavesurfer };
setAudio(newAudioData);
onChange?.(newAudioData);
}
}, [audio?.url]); // 依赖audio.url,当音频URL变化时重新初始化
3.2 播放控制
typescript
const togglePlay = () => {
if (disabled) return;
if (!audio?.wavesurfer) return;
if (audio.wavesurfer.isPlaying()) {
audio.wavesurfer.pause();
setPlaying(false);
} else {
audio.wavesurfer.play();
setPlaying(true);
}
};
步骤4:音频转文字功能
4.1 转文字函数
typescript
const transcribeAudio = async () => {
if (!audio?.blob || transcribing) return;
setTranscribing(true);
setTranscriptionResult('');
try {
// 创建FormData,准备发送到ASR服务
const formData = new FormData();
const audioFile = new File([audio.blob], 'record.wav', {
type: 'audio/wav',
});
formData.append('audio', audioFile);
// 调用ASR接口
const result = await asrToText(formData);
if (result && result.text) {
setTranscriptionResult(result.text);
// 调用回调函数,将结果传递给父组件
if (onTranscriptionResult) {
onTranscriptionResult(result.text);
}
message.success('音频转文字成功');
} else {
message.warning('未能识别到语音内容');
}
} catch (error: any) {
console.error('音频转文字失败:', error);
message.error(`音频转文字失败: ${error.message || '请稍后重试'}`);
} finally {
setTranscribing(false);
}
};
步骤5:UI交互优化
5.1 删除音频功能
typescript
const deleteAudio = () => {
if (disabled) return;
if (audio) {
// 清理WaveSurfer实例
if (audio.wavesurfer) {
audio.wavesurfer.destroy();
}
// 释放Blob URL资源
if (audio.url) {
URL.revokeObjectURL(audio.url);
}
// 重置状态
setAudio(null);
setPlaying(false);
setTranscriptionResult(''); // 清除转文字结果
// 回调父组件
onChange?.(null);
}
};
5.2 辅助函数
typescript
// 格式化时间显示
const formatTime = (seconds: number): string => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
};
// 监听外部value变化
useEffect(() => {
if (value !== undefined && value !== audio) {
setAudio(value);
}
}, [value]);
// 重置转文字状态
useEffect(() => {
if (!audio) {
setTranscriptionResult('');
}
}, [audio]);
步骤6:资源管理与清理
6.1 组件卸载清理
typescript
useEffect(() => {
return () => {
// 清理计时器
if (timerRef.current) {
clearInterval(timerRef.current);
}
// 清理WaveSurfer实例
if (audio?.wavesurfer) {
audio.wavesurfer.destroy();
}
// 释放Blob URL
if (audio?.url) {
URL.revokeObjectURL(audio.url);
}
// 停止录音器
if (mediaRecorderRef.current) {
mediaRecorderRef.current.stop();
}
};
}, [audio]); // 依赖audio,确保清理最新资源
4. 代码结构讲解
4.1 类型定义(TypeScript接口)
typescript
// 核心数据结构,贯穿整个组件
interface AudioData {
blob: Blob; // 原始音频二进制数据
url: string; // 用于播放的Object URL
wavesurfer: any; // WaveSurfer.js实例引用
}
// 组件对外接口,定义props契约
interface AudioRecorderProps {
value?: AudioData | null; // 受控组件模式
onChange?: (value: AudioData | null) => void; // 数据变化回调
disabled?: boolean; // 禁用状态
onTranscriptionResult?: (text: string) => void; // 转文字结果回调
}
4.2 状态管理(React Hooks)
typescript
// 基础状态
const [recording, setRecording] = useState(false); // 录音进行中
const [playing, setPlaying] = useState(false); // 播放进行中
const [audio, setAudio] = useState<AudioData | null>(value || null); // 当前音频数据
const [recordingTime, setRecordingTime] = useState(0); // 录音时长(秒)
// 转文字扩展状态
const [transcribing, setTranscribing] = useState(false); // 转文字进行中
const [transcriptionResult, setTranscriptionResult] = useState(''); // 识别结果文本
4.3 引用管理(useRef)
typescript
// DOM引用 - 用于WaveSurfer挂载
const waveformRef = useRef<HTMLDivElement>(null);
// 实例引用 - 避免重新创建
const mediaRecorderRef = useRef<MediaRecorder | null>(null); // 录音器实例
const audioChunksRef = useRef<Blob[]>([]); // 录音数据块缓存
const timerRef = useRef<NodeJS.Timeout | null>(null); // 录音计时器
4.4 核心功能模块
4.4.1 录音管理模块
typescript
// 检查浏览器支持(前置条件)
const checkBrowserSupport = (): boolean => {
if (!navigator.mediaDevices?.getUserMedia) {
alert('浏览器不支持录音,请使用 Chrome/Edge/Firefox 并在 localhost 或 HTTPS 下打开');
return false;
}
return true;
};
// 开始录音(核心录音逻辑)
const startRecording = async () => {
// 1. 前置检查
if (disabled) return;
if (!checkBrowserSupport()) return;
// 2. 状态初始化
setRecording(true);
setRecordingTime(0);
audioChunksRef.current = [];
setTranscriptionResult('');
try {
// 3. 获取麦克风权限
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
},
});
// 4. 创建录音器
const mediaRecorder = new MediaRecorder(stream, {
mimeType: 'audio/webm;codecs=opus',
});
mediaRecorderRef.current = mediaRecorder;
// 5. 设置数据处理器
mediaRecorder.ondataavailable = (e) => {
if (e.data.size > 0) {
audioChunksRef.current.push(e.data);
}
};
// 6. 录音结束处理器
mediaRecorder.onstop = async () => {
const blob = new Blob(audioChunksRef.current, {
type: mediaRecorder.mimeType || 'audio/webm',
});
const url = URL.createObjectURL(blob);
// 7. 清理和创建新音频数据
if (audio?.wavesurfer) {
audio.wavesurfer.destroy();
}
const newAudioData = { blob, url, wavesurfer: null };
setAudio(newAudioData);
onChange?.(newAudioData);
stream.getTracks().forEach((track) => track.stop());
// 8. 清理计时器
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
};
// 9. 开始录音并计时
mediaRecorder.start();
timerRef.current = setInterval(() => {
setRecordingTime((prev) => prev + 1);
}, 1000);
} catch (err: any) {
// 10. 错误处理和清理
console.error('获取麦克风失败', err);
setRecording(false);
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
// 11. 分类错误提示
let errorMessage = '获取麦克风失败,请检查权限';
if (err.name === 'NotAllowedError') {
errorMessage = '麦克风权限被拒绝';
} else if (err.name === 'NotFoundError') {
errorMessage = '未找到麦克风设备';
}
message.error(errorMessage);
}
};
// 停止录音
const stopRecording = () => {
if (mediaRecorderRef.current && recording) {
mediaRecorderRef.current.stop();
setRecording(false);
transcribeAudio(); // 自动触发转文字
}
};
4.4.2 音频播放模块
typescript
// WaveSurfer初始化(useEffect)
useEffect(() => {
if (audio?.url && !audio.wavesurfer && waveformRef.current) {
// 1. 创建WaveSurfer实例
const wavesurfer = WaveSurfer.create({
container: waveformRef.current,
waveColor: '#4a5568',
progressColor: '#4299e1',
cursorColor: '#4299e1',
height: 40,
barWidth: 1,
barGap: 1,
responsive: true,
normalize: true,
});
// 2. 加载音频文件
wavesurfer.load(audio.url);
// 3. 绑定播放状态事件
wavesurfer.on('finish', () => {
setPlaying(false);
});
wavesurfer.on('pause', () => {
setPlaying(false);
});
wavesurfer.on('play', () => {
setPlaying(true);
});
// 4. 更新状态,包含wavesurfer实例
const newAudioData = { ...audio, wavesurfer };
setAudio(newAudioData);
onChange?.(newAudioData);
}
}, [audio?.url]); // 当audio.url变化时重新初始化
// 播放控制
const togglePlay = () => {
if (disabled) return;
if (!audio?.wavesurfer) return;
if (audio.wavesurfer.isPlaying()) {
audio.wavesurfer.pause();
setPlaying(false);
} else {
audio.wavesurfer.play();
setPlaying(true);
}
};
4.4.3 音频转文字模块
typescript
const transcribeAudio = async () => {
// 1. 前置检查
if (!audio?.blob || transcribing) return;
// 2. 状态设置
setTranscribing(true);
setTranscriptionResult('');
try {
// 3. 构建FormData
const formData = new FormData();
const audioFile = new File([audio.blob], 'record.wav', {
type: 'audio/wav',
});
formData.append('audio', audioFile);
// 4. 调用ASR服务
const result = await asrToText(formData);
// 5. 处理响应
if (result && result.text) {
setTranscriptionResult(result.text);
if (onTranscriptionResult) {
onTranscriptionResult(result.text);
}
message.success('音频转文字成功');
} else {
message.warning('未能识别到语音内容');
}
} catch (error: any) {
// 6. 错误处理
console.error('音频转文字失败:', error);
message.error(`音频转文字失败: ${error.message || '请稍后重试'}`);
} finally {
// 7. 清理状态
setTranscribing(false);
}
};
4.4.4 资源管理模块
typescript
// 删除音频(清理资源)
const deleteAudio = () => {
if (disabled) return;
if (audio) {
// 1. 销毁WaveSurfer实例
if (audio.wavesurfer) {
audio.wavesurfer.destroy();
}
// 2. 释放Blob URL
if (audio.url) {
URL.revokeObjectURL(audio.url);
}
// 3. 重置状态
setAudio(null);
setPlaying(false);
setTranscriptionResult('');
// 4. 回调父组件
onChange?.(null);
}
};
// 组件卸载时的清理
useEffect(() => {
return () => {
// 清理计时器
if (timerRef.current) {
clearInterval(timerRef.current);
}
// 清理WaveSurfer
if (audio?.wavesurfer) {
audio.wavesurfer.destroy();
}
// 释放URL
if (audio?.url) {
URL.revokeObjectURL(audio.url);
}
// 停止录音器
if (mediaRecorderRef.current) {
mediaRecorderRef.current.stop();
}
};
}, [audio]); // 依赖audio确保清理最新资源
4.5 UI组件结构
jsx
<div className={styles.audioRecorderWrapper}>
{/* 1. 录音按钮 - 核心交互入口 */}
<button
onClick={toggleRecording}
className={`${styles.recordButton} ${recording ? styles.recording : ''}`}
disabled={disabled}
type="button"
>
<span className={styles.buttonIcon}>{recording ? '●' : '🎤'}</span>
<span className={styles.buttonText}>
{recording ? `停止 (${formatTime(recordingTime)})` : '开始录音'}
</span>
</button>
{/* 2. 波形显示区域 - 音频可视化 */}
{audio && !recording && (
<div className={styles.audioContainer}>
{/* WaveSurfer挂载点 */}
<div ref={waveformRef} className={styles.waveform} />
{/* 播放控制按钮组 */}
<div className={styles.controlButtons}>
<button onClick={togglePlay} className={styles.playButton}>
{playing ? '⏸' : '▶'}
</button>
<button onClick={deleteAudio} className={styles.deleteButton}>
✕
</button>
</div>
</div>
)}
{/* 3. 转文字功能区域 */}
{audio && !recording && !transcriptionResult && (
<div className={styles.transcribeContainer}>
<button
onClick={transcribeAudio}
className={`${styles.recordButton} ${transcribing ? styles.transcribing : ''}`}
disabled={disabled}
type="button"
>
<span className={styles.buttonIcon}>{transcribing ? '⏳️' : '🔁'}</span>
<span className={styles.buttonText}>
{transcribing ? '转文字中...' : '录音转文字'}
</span>
</button>
</div>
)}
{/* 4. 转文字结果展示 */}
{transcriptionResult && (
<div className={styles.transcriptionResult}>
<div className={styles.resultHeader}>
<span className={styles.resultLabel}>识别结果:</span>
<button
onClick={() => setTranscriptionResult('')}
className={styles.clearResultButton}
title="清除结果"
type="button"
>
✕
</button>
</div>
<div className={styles.resultText}>{transcriptionResult}</div>
</div>
)}
{/* 5. 状态信息区域 */}
<div className={styles.statusInfo}>
{recording ? (
<div className={styles.recordingStatus}>
<span className={styles.recordingDot}></span>
录音中...
</div>
) : audio ? (
<div className={styles.playbackStatus}>✅ 已录制音频</div>
) : (
<div className={styles.readyStatus}>
{disabled ? '录音组件已禁用' : '请点击按钮录制音频'}
</div>
)}
</div>
</div>
4.6 样式架构(CSS Modules)
css
/* 组件容器 */
.audioRecorderWrapper {
display: flex;
flex-direction: column;
gap: 16px;
padding: 20px;
border: 1px solid #e8e8e8;
border-radius: 8px;
background-color: #fff;
}
/* 录音按钮 - 基础样式和状态样式 */
.recordButton {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 10px 20px;
background-color: #1890ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
}
.recordButton:hover:not(:disabled) {
background-color: #40a9ff;
}
.recordButton.recording {
background-color: #ff4d4f;
animation: pulse 1.5s infinite;
}
.recordButton:disabled {
background-color: #d9d9d9;
cursor: not-allowed;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.7; }
100% { opacity: 1; }
}
/* 波形容器 */
.audioContainer {
display: flex;
align-items: center;
gap: 16px;
}
.waveform {
flex: 1;
min-height: 40px;
background-color: #f5f5f5;
border-radius: 4px;
}
/* 控制按钮组 */
.controlButtons {
display: flex;
gap: 8px;
}
.playButton, .deleteButton {
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #d9d9d9;
border-radius: 4px;
background-color: white;
cursor: pointer;
transition: all 0.3s;
}
.playButton:hover, .deleteButton:hover {
border-color: #1890ff;
color: #1890ff;
}
/* 转文字相关样式 */
.transcribeContainer {
margin-top: 8px;
}
.transcribing {
background-color: #faad14 !important;
}
.transcriptionResult {
margin-top: 16px;
padding: 12px;
background-color: #f6ffed;
border: 1px solid #b7eb8f;
border-radius: 4px;
}
.resultHeader {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.resultLabel {
font-weight: 500;
color: #52c41a;
}
.clearResultButton {
background: none;
border: none;
cursor: pointer;
color: #999;
font-size: 16px;
}
.clearResultButton:hover {
color: #ff4d4f;
}
.resultText {
line-height: 1.5;
color: #333;
}
/* 状态信息 */
.statusInfo {
font-size: 14px;
color: #666;
}
.recordingStatus {
display: flex;
align-items: center;
gap: 8px;
color: #ff4d4f;
}
.recordingDot {
width: 8px;
height: 8px;
background-color: #ff4d4f;
border-radius: 50%;
animation: blink 1s infinite;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
.playbackStatus {
color: #52c41a;
}
.readyStatus {
color: #666;
}
4.7 错误处理机制
typescript
// 录音错误分类处理
const handleRecordingError = (err: any) => {
console.error('获取麦克风失败', err);
setRecording(false);
// 清理资源
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
// 分类错误提示
let errorMessage = '获取麦克风失败,请检查权限';
if (err.name === 'NotAllowedError') {
errorMessage = '麦克风权限被拒绝';
} else if (err.name === 'NotFoundError') {
errorMessage = '未找到麦克风设备';
} else if (err.name === 'NotSupportedError') {
errorMessage = '浏览器不支持录音功能';
}
// 使用Ant Design消息提示
message.error(errorMessage);
};
// 转文字错误处理
const handleTranscriptionError = (error: any) => {
console.error('音频转文字失败:', error);
// 用户友好的错误提示
let userMessage = '音频转文字失败,请稍后重试';
if (error.response) {
// 服务器响应错误
if (error.response.status === 413) {
userMessage = '音频文件过大,请录制短一些的音频';
} else if (error.response.status === 429) {
userMessage = '请求过于频繁,请稍后再试';
}
} else if (error.request) {
// 网络错误
userMessage = '网络连接失败,请检查网络设置';
}
message.error(`${userMessage}: ${error.message || ''}`);
};
5. 技术亮点总结
5.1 现代React特性应用
- 函数组件+Hooks:完全使用函数组件和Hooks,无class组件
- TypeScript:严格的类型定义,提高代码可靠性
- 自定义Hooks模式:逻辑清晰,易于测试和维护
5.2 性能优化策略
- 引用缓存:使用useRef避免重复创建实例
- 精准依赖:useEffect依赖项精确控制,避免不必要的重渲染
- 资源释放:主动管理Blob URL和WaveSurfer实例生命周期
5.3 用户体验优化
- 实时反馈:录音时长、转文字状态实时显示
- 错误友好提示:分类错误处理和用户友好提示
- 防重复操作:防止录音、转文字等操作的重复触发
- 无障碍访问:按钮有明确的title和type属性
5.4 可扩展性设计
- 受控组件模式:支持外部状态控制,易于集成
- 回调机制完善:提供完整的onChange和onTranscriptionResult回调
- 模块化设计:各功能模块职责单一,易于扩展和维护
6. 使用示例
基本使用
typescript
import React, { useState } from 'react';
import AudioRecorder from './AudioRecorder';
const ParentComponent: React.FC = () => {
const [audioData, setAudioData] = useState(null);
const [transcription, setTranscription] = useState('');
return (
<div>
<AudioRecorder
value={audioData}
onChange={(newAudio) => {
setAudioData(newAudio);
console.log('音频数据更新:', newAudio);
}}
onTranscriptionResult={(text) => {
setTranscription(text);
console.log('识别结果:', text);
}}
/>
{transcription && (
<div>
<h3>转文字结果:</h3>
<p>{transcription}</p>
</div>
)}
</div>
);
};
禁用状态使用
typescript
<AudioRecorder
disabled={true}
value={audioData}
onChange={handleAudioChange}
/>
7. 总结
该React录音转文字组件是一个功能完整、设计良好的现代React组件,具有以下特点:
- 功能全面:集录音、播放、可视化、转文字于一体
- 架构清晰:模块化设计,职责分离明确
- 用户体验好:实时反馈、错误提示、状态展示完善
- 性能优秀:资源管理严谨,避免内存泄漏
- 扩展性强:支持受控模式,易于集成到不同场景
该组件可作为语音交互类应用的基础组件,也可作为学习现代React开发、音频处理、第三方服务集成的优秀案例。通过这个组件的开发,我们掌握了如何将复杂的前端功能模块化、如何管理音频相关资源、如何集成第三方服务,以及如何提供良好的用户体验。