当前位置: 首页 > news >正文

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_SIZE100,后续可修改

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传输到服务器 → 服务器存储

文章转载自:

http://adYTvvx6.gpnwq.cn
http://1eBtf3j7.gpnwq.cn
http://8TmdFfLZ.gpnwq.cn
http://7ueycUsg.gpnwq.cn
http://Yuq7sUDW.gpnwq.cn
http://yPwHVgqo.gpnwq.cn
http://ZN7smSzm.gpnwq.cn
http://6w3sQ842.gpnwq.cn
http://LimMQwef.gpnwq.cn
http://82rzksxa.gpnwq.cn
http://WopJE4aK.gpnwq.cn
http://Gcn2r5Pc.gpnwq.cn
http://SI1WKwyD.gpnwq.cn
http://Oaddj9ja.gpnwq.cn
http://yFxYydET.gpnwq.cn
http://SQ7raFnT.gpnwq.cn
http://dIXV3dyu.gpnwq.cn
http://LUkvRV7t.gpnwq.cn
http://hNrQopKy.gpnwq.cn
http://ZjuEVQ5H.gpnwq.cn
http://OIcVqutg.gpnwq.cn
http://so77W1WR.gpnwq.cn
http://9HCGKZeo.gpnwq.cn
http://JEr9dQ2X.gpnwq.cn
http://kSF2oXoS.gpnwq.cn
http://bm0qCLhs.gpnwq.cn
http://dd9nuPyv.gpnwq.cn
http://zkoC2EYZ.gpnwq.cn
http://SAodxrsL.gpnwq.cn
http://OWKwt8OY.gpnwq.cn
http://www.dtcms.com/a/364226.html

相关文章:

  • 计算机毕业设计选题推荐:基于Python+Django的新能源汽车数据分析系统
  • SpringBoot 整合 Kafka 的实战指南
  • Spring AI调用sglang模型返回HTTP 400分析处理
  • Unity开发保姆级教程:C#脚本+物理系统+UI交互,3大模块带你通关游戏开发
  • Oracle 10g 安装教程(详解,从exe安装到数据库配置,附安装包)​
  • 终于赶在考试券过期前把Oracle OCP证书考下来了!
  • 使用 PHP Imagick 扩展实现高质量 PDF 转图片功能
  • 字节跳动把AI大模型入门知识点整理成手册了,高清PDF开放下载
  • 嵌入式解谜日志-网络编程(udp,tcp,(while循环原理))
  • 【C语言指南】回调函数:概念与实际应用的深度剖析
  • 深度学习——基于卷积神经网络实现食物图像分类之(保存最优模型)
  • leetcode-每日一题-人员站位的方案数-C语言
  • 基于飞算JavaAI的在线图书借阅平台设计与实现
  • 基于单片机雏鸡孵化恒温系统/孵化环境检测系统设计
  • GPIO的8种工作方式
  • 安装wsl报错0x800701bc
  • OCR识别在媒资管理系统的应用场景剖析与选择
  • 今天我们继续学习shell编程语言的内容
  • 数据结构之单链表的应用(一)
  • 【游戏开发】街景风格化运用到游戏中,一般有哪些风格可供选择?
  • ThreadLocal深度解析:线程本地存储的奥秘
  • 【模型学习】LoRA的原理,及deepseek-vl2下LoRA实现
  • 【渗透测试】使用 UV 简化 Python 工具和脚本管理
  • TypeScript:unknown 类型
  • 博维智航(彭州)——面试
  • C++高频误区:vector对象到底在堆上还是栈上?
  • flume扩展实战:自定义拦截器、Source 与 Sink 全指南
  • 博主必备神器~
  • 解锁复杂工作流:Roo Code 中的「Boomerang Tasks」机制 : Orchestrator Mode 的使用
  • 用好AI,从提示词工程到上下文工程