react-wavesurfer录音组件1:从需求到组件一次说清楚

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组件,具有以下特点:

  1. 功能全面:集录音、播放、可视化、转文字于一体
  2. 架构清晰:模块化设计,职责分离明确
  3. 用户体验好:实时反馈、错误提示、状态展示完善
  4. 性能优秀:资源管理严谨,避免内存泄漏
  5. 扩展性强:支持受控模式,易于集成到不同场景

该组件可作为语音交互类应用的基础组件,也可作为学习现代React开发、音频处理、第三方服务集成的优秀案例。通过这个组件的开发,我们掌握了如何将复杂的前端功能模块化、如何管理音频相关资源、如何集成第三方服务,以及如何提供良好的用户体验。

相关推荐
陈随易2 小时前
聊一聊2025年用AI的思考与总结
前端·后端·程序员
@PHARAOH2 小时前
WHAT - React startTransition vs setTimeout vs debounce
前端·react.js·前端框架
绝美焦栖2 小时前
低版本pdfjs升级
前端·javascript·vue.js
阿里巴巴终端技术2 小时前
二十年,重新出发!第 20 届 D2 技术大会「AI 新」议题全球征集正式开启
前端·react.js·html
阿祖zu2 小时前
2025 AI 总结:技术研发的技能升维与职业路径系统重构的思考
前端·后端·ai编程
IT_陈寒2 小时前
Vite 5分钟性能优化实战:从3秒到300ms的冷启动提速技巧(附可复用配置)
前端·人工智能·后端
迦南giser2 小时前
webpack从0到1详解
前端·javascript·css·webpack·node.js
xkxnq2 小时前
第二阶段:Vue 组件化开发(第 26天)
前端·javascript·vue.js
华玥作者2 小时前
uni-app + Vite 项目中使用 @uni-helper/vite-plugin-uni-pages 实现自动路由配置(超详细)
前端·uni-app·vue·vue3·vite