如果后端只返回 Blob 字段,前端需要做以下调整:

1. 调整数据结构
typescript
interface AudioData {
blob: Blob; // 必需的 Blob 对象
url?: string; // 可选的 URL(前端生成)
wavesurfer?: any; // WaveSurfer 实例
}
// 或者
interface AudioDataFromBackend {
blob: Blob; // 后端返回的二进制音频数据
// 其他元数据
mimeType?: string;
duration?: number;
fileName?: string;
}
2. 前端处理逻辑
方案A:在父组件中转换
typescript
// 父组件中处理
const ParentComponent: React.FC = () => {
const [audioValue, setAudioValue] = useState<AudioData | null>(null);
// 从后端获取 Blob
const fetchAudio = async () => {
try {
// 假设 API 返回 Blob
const response = await services.EasyFindController.getAudioBlob();
// 从响应中提取 Blob
const blob = response.data; // 或者 response.blob
// 生成 URL
const url = URL.createObjectURL(blob);
// 构建 AudioData
const audioData: AudioData = {
blob,
url,
wavesurfer: null
};
setAudioValue(audioData);
} catch (error) {
console.error('获取音频失败', error);
}
};
return (
<AudioRecorder
value={audioValue}
onChange={handleChange}
/>
);
};
方案B:修改 AudioRecorder 组件,使其能直接处理 Blob
typescript
// AudioRecorder 组件中增加 Blob 处理逻辑
interface AudioRecorderProps {
value?: AudioData | Blob | null; // 支持 Blob 类型
// ... 其他 props
}
const AudioRecorder: React.FC<AudioRecorderProps> = ({
value,
// ... 其他 props
}) => {
const [audio, setAudio] = useState<AudioData | null>(null);
// 处理不同类型的 value
useEffect(() => {
if (value === null || value === undefined) {
setAudio(null);
return;
}
if (value instanceof Blob) {
// 处理纯 Blob
const url = URL.createObjectURL(value);
const audioData: AudioData = {
blob: value,
url,
wavesurfer: null
};
setAudio(audioData);
} else if ('blob' in value && value.blob) {
// 已经是 AudioData 类型
setAudio(value as AudioData);
}
}, [value]);
// ... 其他代码
};
3. API 响应示例
方案A:直接返回 Blob(Content-Type: audio/wav)
typescript
// 后端 API
// GET /api/audio/record-123
// Response Headers: Content-Type: audio/wav
// 前端调用
const getAudioBlob = async (audioId: string): Promise<Blob> => {
const response = await fetch(`/api/audio/${audioId}`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
throw new Error('获取音频失败');
}
return await response.blob();
};
方案B:返回包含 Blob 的 JSON
json
{
"code": 200,
"message": "success",
"data": {
"blob": "base64编码的音频数据",
"mimeType": "audio/wav",
"duration": 15.5,
"fileName": "record-123.wav"
}
}
typescript
// 处理 base64 字符串转换为 Blob
const base64ToBlob = (base64: string, mimeType: string): Blob => {
const byteCharacters = atob(base64);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
return new Blob([byteArray], { type: mimeType });
};
// 使用示例
const base64Response = await services.EasyFindController.getAudioBase64();
const blob = base64ToBlob(base64Response.data.blob, base64Response.data.mimeType);
4. 修改 AudioRecorder 组件以适应纯 Blob
typescript
// 在现有 AudioRecorder 组件中修改
const AudioRecorder: React.FC<AudioRecorderProps> = ({
value,
onChange,
// ... 其他 props
}) => {
const [internalAudio, setInternalAudio] = useState<AudioData | null>(null);
// 统一处理外部传入的 value
useEffect(() => {
if (value === undefined) return;
const processValue = async () => {
let processedAudio: AudioData | null = null;
if (value === null) {
processedAudio = null;
} else if (value instanceof Blob) {
// 处理纯 Blob
const url = URL.createObjectURL(value);
processedAudio = {
blob: value,
url,
wavesurfer: null
};
} else if ('blob' in value && value.blob) {
// 已经是 AudioData 类型
if (!value.url && value.blob) {
// 如果有 blob 但没有 url,生成 url
const url = URL.createObjectURL(value.blob);
processedAudio = {
...value,
url,
wavesurfer: null
};
} else {
processedAudio = value as AudioData;
}
}
setInternalAudio(processedAudio);
};
processValue();
}, [value]);
// 修改内部状态变化的处理
const handleInternalChange = (newAudio: AudioData | null) => {
setInternalAudio(newAudio);
if (onChange) {
// 根据需求决定返回什么
if (newAudio) {
// 返回完整的 AudioData
onChange(newAudio);
// 或者只返回 Blob
// onChange(newAudio.blob);
} else {
onChange(null);
}
}
};
// 使用 internalAudio 替代原来的 audio 状态
// 修改所有使用 audio 的地方为 internalAudio
// ...
};
5. 完整示例:父组件使用
typescript
// 父组件
const MyComponent: React.FC = () => {
const [audioBlob, setAudioBlob] = useState<Blob | null>(null);
// 从后端加载音频
const loadAudio = async () => {
try {
// 方法1:直接获取 Blob
const response = await fetch('/api/audio/123');
const blob = await response.blob();
setAudioBlob(blob);
// 方法2:通过 API 服务获取
// const result = await services.EasyFindController.getAudio(123);
// setAudioBlob(result.data);
} catch (error) {
console.error('加载音频失败', error);
}
};
// 处理音频变化
const handleAudioChange = (audioDataOrBlob: AudioData | Blob | null) => {
if (audioDataOrBlob instanceof Blob) {
setAudioBlob(audioDataOrBlob);
// 保存到后端
saveAudioToBackend(audioDataOrBlob);
} else if (audioDataOrBlob && 'blob' in audioDataOrBlob) {
setAudioBlob(audioDataOrBlob.blob);
// 保存到后端
saveAudioToBackend(audioDataOrBlob.blob);
} else {
setAudioBlob(null);
// 通知后端删除
deleteAudioFromBackend();
}
};
const saveAudioToBackend = async (blob: Blob) => {
const formData = new FormData();
formData.append('audio', blob, 'recording.wav');
try {
await services.EasyFindController.uploadAudio(formData);
} catch (error) {
console.error('上传音频失败', error);
}
};
return (
<div>
<button onClick={loadAudio}>加载音频</button>
<AudioRecorder
value={audioBlob} // 直接传递 Blob
onChange={handleAudioChange}
onTranscriptionResult={(text) => {
console.log('识别结果:', text);
}}
/>
</div>
);
};
6. 建议的最佳实践
- 后端返回 Blob:简单直接,适合音频文件传输
- 前端统一处理:在父组件或 AudioRecorder 组件中将 Blob 转换为 AudioData
- 资源管理 :注意使用
URL.revokeObjectURL()释放资源 - 类型安全:使用 TypeScript 类型保护确保类型安全
typescript
// 类型保护函数
const isBlob = (value: any): value is Blob => {
return value instanceof Blob;
};
const isAudioData = (value: any): value is AudioData => {
return value && typeof value === 'object' && 'blob' in value;
};
这样设计可以保持组件的灵活性,既能处理完整的 AudioData 对象,也能处理纯 Blob 对象。