React实现音频文件上传与试听
实现一个
react
音频上传的大致逻辑如下:
1、上方提示语
<Alertmessage={$t('支持%s,音频大小需为%d以内,音频采样率为%e').replace('%s', AUDIO_TYPES.join()).replace('%d', `${AUDIO_MAX_SIZE}KB`).replace('%e', '16K')}type='info'showIconstyle={{ width: '50%', marginBottom: 16 }}/>
其中AUDIO_TYPES
是我们定义的['MP3']
类型数组集合,AUDIO_MAX_SIZE
是100
,后续可修改
2、封装上传组件
<AudioUpload><Button icon={<UploadOutlined />}>上传</Button>
</AudioUpload>
可以封装一个AudioUpload
组件,组件内部大概是
const AudioUpload = props => {const { children } = props;return (<Upload>{children} {/* 自定义上传按钮 */}</Upload>);
};
当然并不会这么简单,大致解释一下上传的事件监听机制
// 不是事件冒泡,而是组件嵌套关系
<Upload> {/* 父组件 */}<Button> {/* 子组件 */}上传</Button>
</Upload>
具体的流程
首先
// 用户点击这个按钮
<Button icon={<UploadOutlined />}>{$t('com.Upload')}
</Button>
AudioUpload
组件中的Upload
组件捕获点击
Upload
组件内部的事件监听器被触发
不是事件冒泡,而是 Upload
组件主动监听子元素的点击
// Upload 组件内部自动执行
const hiddenInput = document.querySelector('input[type="file"]');
hiddenInput.click(); // 触发文件选择对话框
3、Ant Design Upload 的实现原理
事件委托模式
// Upload 组件内部的伪代码逻辑
class Upload extends React.Component {componentDidMount() {// 监听整个上传区域的点击事件this.uploadArea.addEventListener('click', (e) => {// 检查点击的是否是子元素if (e.target.closest('.ant-upload-select-button')) {// 触发文件选择this.triggerFileSelect();}});}triggerFileSelect() {// 显示文件选择对话框this.fileInput.click();}
}
4、{children} 的核心作用
提供可点击的 UI 元素
<Upload>{children} {/* 这里需要一个可点击的元素来触发文件选择 */}
</Upload>
// 如果 Upload 组件没有 children
<Upload>{/* 空的,没有可点击的元素 */}
</Upload>// 结果:用户无法点击任何地方来触发文件选择
// Upload 组件不知道应该监听哪个元素的点击事件
5、Upload内部参数含义
<Uploadname='file'headers={}action={file =>Promise.resolve(`地址 ${file.name}`)}accept={AUDIO_TYPES.map(item => `.${item}`).join()}showUploadList={false}beforeUpload={beforeUpload}onSuccess={refresh}>{children}</Upload>
5.1 name
name
属性指定了文件在表单数据中的字段名,服务器端通过这个字段名来获取上传的文件。
服务器端接收:
// 服务器端会这样获取文件
const uploadedFile = req.files.file; // 通过 'file' 字段名获取
name 的其他可能值
// 根据文件类型命名
name='audio' // 音频文件
name='image' // 图片文件
name='document' // 文档文件
name='video' // 视频文件// 根据业务功能命名
name='profile-picture' // 头像
name='background-music' // 背景音乐
name='notification-sound' // 通知音效
5.2 headers
headers 的作用
headers
属性用于设置 HTTP
请求的请求头,通常用于:
身份认证
跨域请求
自定义请求信息
服务器端识别
headers 的常见用途
// 认证相关
headers={{'Authorization': 'Bearer ' + token,'X-API-Key': apiKey,'User-Token': userToken
}}// 跨域相关
headers={{'Access-Control-Allow-Origin': '*','Content-Type': 'multipart/form-data'
}}// 自定义标识
headers={{'X-Request-ID': generateRequestId(),'X-Client-Version': '1.0.0','X-Platform': 'web'
}}// 业务相关
headers={{'Device-Id': deviceId,'Session-Id': sessionId,'Request-Source': 'audio-upload'
}}
5.3 action 属性
action 的基本作用
1.定义上传地址
action
属性指定了文件上传的目标 URL
,告诉 Upload
组件将文件发送到哪个服务器地址。
2. 触发上传流程
当用户选择文件并通过验证后,Upload
组件会自动向 action
指定的地址发送 HTTP POST
请求,将文件数据上传到服务器。
action 的不同配置方式
1.静态 URL
// 最简单的配置
<Upload action="/api/upload"><Button>上传</Button>
</Upload>// 完整的 URL
<Upload action="https://api.example.com/upload"><Button>上传</Button>
</Upload>
2.动态 URL
// 根据文件信息动态构建
action={file => `/upload/${file.name}`}// 根据环境动态选择
action={file => process.env.NODE_ENV === 'production' ? 'https://api.prod.com/upload' : 'http://localhost:3000/upload'
}// 根据文件类型动态选择
action={file => {if (file.type.startsWith('audio/')) {return '/api/upload/audio';}if (file.type.startsWith('image/')) {return '/api/upload/image';}return '/api/upload/file';
}}
action 的执行时机
// 1. 用户选择文件
// 2. beforeUpload 验证通过
// 3. action 函数被调用,获取上传地址
// 4. 向该地址发送文件数据
// 5. 触发 onSuccess 或 onError 回调
使用 Promise.resolve
1、兼容性考虑
// Ant Design Upload 组件期望 action 返回一个 Promise
// 即使我们返回的是同步的字符串,也需要包装成 Promise// 正确的方式
action={file => Promise.resolve(uploadUrl)}// 也可以这样写
action={file => new Promise(resolve => resolve(uploadUrl))}// 或者使用 async/await
action={async file => uploadUrl}
5.4 分块上传实现简单方案
1. 基本思路
// 在 action 中判断文件大小,大文件走分块上传
action={file => {if (file.size > 10 * 1024 * 1024) { // 大于10MB// 分块上传return '/api/upload/chunk';} else {// 普通上传return '/api/upload/normal';}
}}
具体是
const AudioUpload = props => {const [isChunked, setIsChunked] = useState(false);const handleChunkedUpload = async (file) => {// 分块上传逻辑const chunkSize = 1024 * 1024; // 1MB每块const totalChunks = Math.ceil(file.size / chunkSize);for (let i = 0; i < totalChunks; i++) {const chunk = file.slice(i * chunkSize, (i + 1) * chunkSize);const formData = new FormData();formData.append('chunk', chunk);formData.append('chunkIndex', i);formData.append('totalChunks', totalChunks);await fetch('/api/upload/chunk', {method: 'POST',body: formData});}// 合并文件await fetch('/api/upload/merge', {method: 'POST',body: JSON.stringify({ fileName: file.name, totalChunks })});};const beforeUpload = file => {// 大文件使用分块上传if (file.size > 10 * 1024 * 1024) {setIsChunked(true);handleChunkedUpload(file);return false; // 阻止默认上传}return true; // 小文件正常上传};return (<Uploadaction={file => {if (isChunked) {return '/api/upload/chunk'; // 分块上传地址}// 原有的上传地址return `地址+${file.name}`;}}beforeUpload={beforeUpload}onSuccess={refresh}>{children}</Upload>);
};
5.5 accept属性
文件选择阶段
<Uploadaccept={AUDIO_TYPES.map(item => `.${item}`).join()} // 只显示 .MP3 文件// ...
>
浏览器原生文件选择器
通过 accept
属性过滤,只显示 MP3
文件
用户选择文件后,浏览器将文件对象传递给组件
5.6 beforeUpload属性
const beforeUpload = file => {const { name, size } = file; // 从 File 对象获取文件信息// 执行各种验证...// 返回 false 阻止上传,返回 true 允许上传
};
5.7 onSuccess属性
一般是执行刷新操作,重新请求服务器中上传文件列表,展示到table中
6、音频播放控制功能分析
主要是在Table
中每一行音频文件的最后放一个图标
播放音频
{playUid !== record.uid ? (<Icontype='play3'className='audio-btn'onClick={() => handlePlayAudio(record)}/>) : (<Icontype='pause2'className='audio-btn'onClick={() => handlePauseAudio()}/>)}
其中一开始
const [playUid, setPlayUid] = useState(-1);
那么开始前就是播放图标
执行流程
const handlePlayAudio = record => {// 1. 提取音频信息const { uid, url } = record;// 2. 设置播放状态setPlayUid(uid);// 3. 获取环境配置const urlParamsString = localStorage.getItem('_urlParams');const urlParams = urlParamsString ? JSON.parse(urlParamsString) : {};const { Prefix, UserToken, DeviceId } = urlParams;// 4. 构建文件加载地址const preFix = 地址;// 5. 先停止上一个音频if (lastAudio.current) {lastAudio.current.pause();}// 6. 根据部署模式选择播放方式if (CLOUDWEB) {// 云端模式:先下载再播放// ...} else {// 本地模式:直接播放// ...}
};
本地模式处理
else {// 直接使用文件路径创建音频对象lastAudio.current = new Audio(preFix + url);// 开始播放lastAudio.current.play();// 设置播放结束和错误处理lastAudio.current.onended = () => {setPlayUid(-1);};lastAudio.current.onerror = () => {setPlayUid(-1);};
}
暂停当前正在播放的音频
const handlePauseAudio = () => {// 1. 暂停音频播放if (lastAudio.current) {lastAudio.current.pause();}// 2. 重置播放状态setPlayUid(-1);
};
7、文件内容处理机制详解
7.1 文件对象结构
const file = {name: 'audio.mp3', // 文件名size: 51200, // 文件大小(字节)type: 'audio/mpeg', // MIME类型lastModified: 1234567890, // 最后修改时间// 文件的实际二进制内容存储在内存中,但前端不直接读取
};
7.2 文件内容存储位置
前端: 只获取文件的元数据(名称、大小、类型等)
实际内容: 存储在浏览器的内存中,作为 File 对象的一部分
传输: 通过 HTTP 请求自动传输到服务器
7.3 文件内容的传输机制
自动传输过程
<Uploadname='file' // 表单字段名action={file => Promise.resolve(uploadUrl)} // 上传地址// ...
/>
传输流程:
浏览器自动创建 FormData 对象
将 File 对象作为 file 字段添加到表单
发送 POST 请求到服务器
文件内容作为请求体的一部分自动传输
服务器端接收
// 服务器端接收到的数据格式:
// Content-Type: multipart/form-data
//
// --boundary
// Content-Disposition: form-data; name="file"; filename="audio.mp3"
// Content-Type: audio/mpeg
//
// [二进制音频文件内容]
// --boundary--
上传文件生命周期
用户选择文件 → 文件存储在浏览器内存 → 通过HTTP传输到服务器 → 服务器存储