React實(shí)現(xiàn)音頻文件上傳與試聽(tīng)
實(shí)現(xiàn)一個(gè)react音頻上傳的大致邏輯如下:

1、上方提示語(yǔ)
<Alert
message={$t('支持%s,音頻大小需為%d以內(nèi),音頻采樣率為%e')
.replace('%s', AUDIO_TYPES.join())
.replace('%d', `${AUDIO_MAX_SIZE}KB`)
.replace('%e', '16K')}
type='info'
showIcon
style={{ width: '50%', marginBottom: 16 }}
/>
其中AUDIO_TYPES是我們定義的['MP3']類型數(shù)組集合,AUDIO_MAX_SIZE是100,后續(xù)可修改
2、封裝上傳組件
<AudioUpload>
<Button icon={<UploadOutlined />}>上傳</Button>
</AudioUpload>
可以封裝一個(gè)AudioUpload組件,組件內(nèi)部大概是
const AudioUpload = props => {
const { children } = props;
return (
<Upload>
{children} {/* 自定義上傳按鈕 */}
</Upload>
);
};
當(dāng)然并不會(huì)這么簡(jiǎn)單,大致解釋一下上傳的事件監(jiān)聽(tīng)機(jī)制
// 不是事件冒泡,而是組件嵌套關(guān)系
<Upload> {/* 父組件 */}
<Button> {/* 子組件 */}
上傳
</Button>
</Upload>
具體的流程
首先
// 用戶點(diǎn)擊這個(gè)按鈕
<Button icon={<UploadOutlined />}>
{$t('com.Upload')}
</Button>
AudioUpload組件中的Upload 組件捕獲點(diǎn)擊Upload 組件內(nèi)部的事件監(jiān)聽(tīng)器被觸發(fā)
不是事件冒泡,而是 Upload 組件主動(dòng)監(jiān)聽(tīng)子元素的點(diǎn)擊
// Upload 組件內(nèi)部自動(dòng)執(zhí)行
const hiddenInput = document.querySelector('input[type="file"]');
hiddenInput.click(); // 觸發(fā)文件選擇對(duì)話框
3、Ant Design Upload 的實(shí)現(xiàn)原理
事件委托模式
// Upload 組件內(nèi)部的偽代碼邏輯
class Upload extends React.Component {
componentDidMount() {
// 監(jiān)聽(tīng)整個(gè)上傳區(qū)域的點(diǎn)擊事件
this.uploadArea.addEventListener('click', (e) => {
// 檢查點(diǎn)擊的是否是子元素
if (e.target.closest('.ant-upload-select-button')) {
// 觸發(fā)文件選擇
this.triggerFileSelect();
}
});
}
triggerFileSelect() {
// 顯示文件選擇對(duì)話框
this.fileInput.click();
}
}
4、{children} 的核心作用
提供可點(diǎn)擊的 UI 元素
<Upload>
{children} {/* 這里需要一個(gè)可點(diǎn)擊的元素來(lái)觸發(fā)文件選擇 */}
</Upload>
// 如果 Upload 組件沒(méi)有 children
<Upload>
{/* 空的,沒(méi)有可點(diǎn)擊的元素 */}
</Upload>
// 結(jié)果:用戶無(wú)法點(diǎn)擊任何地方來(lái)觸發(fā)文件選擇
// Upload 組件不知道應(yīng)該監(jiān)聽(tīng)哪個(gè)元素的點(diǎn)擊事件
5、Upload內(nèi)部參數(shù)含義
<Upload
name='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 屬性指定了文件在表單數(shù)據(jù)中的字段名,服務(wù)器端通過(guò)這個(gè)字段名來(lái)獲取上傳的文件。
服務(wù)器端接收:
// 服務(wù)器端會(huì)這樣獲取文件 const uploadedFile = req.files.file; // 通過(guò) 'file' 字段名獲取
name 的其他可能值
// 根據(jù)文件類型命名 name='audio' // 音頻文件 name='image' // 圖片文件 name='document' // 文檔文件 name='video' // 視頻文件 // 根據(jù)業(yè)務(wù)功能命名 name='profile-picture' // 頭像 name='background-music' // 背景音樂(lè) name='notification-sound' // 通知音效
5.2 headers
headers 的作用headers 屬性用于設(shè)置 HTTP 請(qǐng)求的請(qǐng)求頭,通常用于:
身份認(rèn)證
跨域請(qǐng)求
自定義請(qǐng)求信息
服務(wù)器端識(shí)別
headers 的常見(jiàn)用途
// 認(rèn)證相關(guān)
headers={{
'Authorization': 'Bearer ' + token,
'X-API-Key': apiKey,
'User-Token': userToken
}}
// 跨域相關(guān)
headers={{
'Access-Control-Allow-Origin': '*',
'Content-Type': 'multipart/form-data'
}}
// 自定義標(biāo)識(shí)
headers={{
'X-Request-ID': generateRequestId(),
'X-Client-Version': '1.0.0',
'X-Platform': 'web'
}}
// 業(yè)務(wù)相關(guān)
headers={{
'Device-Id': deviceId,
'Session-Id': sessionId,
'Request-Source': 'audio-upload'
}}
5.3 action 屬性
action 的基本作用
1.定義上傳地址
action 屬性指定了文件上傳的目標(biāo) URL,告訴 Upload 組件將文件發(fā)送到哪個(gè)服務(wù)器地址。
2. 觸發(fā)上傳流程
當(dāng)用戶選擇文件并通過(guò)驗(yàn)證后,Upload 組件會(huì)自動(dòng)向 action 指定的地址發(fā)送 HTTP POST 請(qǐng)求,將文件數(shù)據(jù)上傳到服務(wù)器。
action 的不同配置方式
1.靜態(tài) URL
// 最簡(jiǎn)單的配置
<Upload action="/api/upload">
<Button>上傳</Button>
</Upload>
// 完整的 URL
<Upload action="https://api.example.com/upload">
<Button>上傳</Button>
</Upload>
2.動(dòng)態(tài) URL
// 根據(jù)文件信息動(dòng)態(tài)構(gòu)建
action={file => `/upload/${file.name}`}
// 根據(jù)環(huán)境動(dòng)態(tài)選擇
action={file =>
process.env.NODE_ENV === 'production'
? 'https://api.prod.com/upload'
: 'http://localhost:3000/upload'
}
// 根據(jù)文件類型動(dòng)態(tài)選擇
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 的執(zhí)行時(shí)機(jī)
// 1. 用戶選擇文件 // 2. beforeUpload 驗(yàn)證通過(guò) // 3. action 函數(shù)被調(diào)用,獲取上傳地址 // 4. 向該地址發(fā)送文件數(shù)據(jù) // 5. 觸發(fā) onSuccess 或 onError 回調(diào)
使用 Promise.resolve
1、兼容性考慮
// Ant Design Upload 組件期望 action 返回一個(gè) Promise
// 即使我們返回的是同步的字符串,也需要包裝成 Promise
// 正確的方式
action={file => Promise.resolve(uploadUrl)}
// 也可以這樣寫
action={file => new Promise(resolve => resolve(uploadUrl))}
// 或者使用 async/await
action={async file => uploadUrl}
5.4 分塊上傳實(shí)現(xiàn)簡(jiǎn)單方案
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; // 阻止默認(rèn)上傳
}
return true; // 小文件正常上傳
};
return (
<Upload
action={file => {
if (isChunked) {
return '/api/upload/chunk'; // 分塊上傳地址
}
// 原有的上傳地址
return `地址+${file.name}`;
}}
beforeUpload={beforeUpload}
onSuccess={refresh}>
{children}
</Upload>
);
};
5.5 accept屬性
文件選擇階段
<Upload
accept={AUDIO_TYPES.map(item => `.${item}`).join()} // 只顯示 .MP3 文件
// ...
>
瀏覽器原生文件選擇器
通過(guò) accept 屬性過(guò)濾,只顯示 MP3 文件
用戶選擇文件后,瀏覽器將文件對(duì)象傳遞給組件
5.6 beforeUpload屬性
const beforeUpload = file => {
const { name, size } = file; // 從 File 對(duì)象獲取文件信息
// 執(zhí)行各種驗(yàn)證...
// 返回 false 阻止上傳,返回 true 允許上傳
};
5.7 onSuccess屬性
一般是執(zhí)行刷新操作,重新請(qǐng)求服務(wù)器中上傳文件列表,展示到table中
6、音頻播放控制功能分析
主要是在Table中每一行音頻文件的最后放一個(gè)圖標(biāo)
播放音頻
{playUid !== record.uid ? (
<Icon
type='play3'
className='audio-btn'
onClick={() => handlePlayAudio(record)}
/>
) : (
<Icon
type='pause2'
className='audio-btn'
onClick={() => handlePauseAudio()}
/>
)}
其中一開(kāi)始
const [playUid, setPlayUid] = useState(-1);
那么開(kāi)始前就是播放圖標(biāo)
執(zhí)行流程
const handlePlayAudio = record => {
// 1. 提取音頻信息
const { uid, url } = record;
// 2. 設(shè)置播放狀態(tài)
setPlayUid(uid);
// 3. 獲取環(huán)境配置
const urlParamsString = localStorage.getItem('_urlParams');
const urlParams = urlParamsString ? JSON.parse(urlParamsString) : {};
const { Prefix, UserToken, DeviceId } = urlParams;
// 4. 構(gòu)建文件加載地址
const preFix = 地址;
// 5. 先停止上一個(gè)音頻
if (lastAudio.current) {
lastAudio.current.pause();
}
// 6. 根據(jù)部署模式選擇播放方式
if (CLOUDWEB) {
// 云端模式:先下載再播放
// ...
} else {
// 本地模式:直接播放
// ...
}
};
本地模式處理
else {
// 直接使用文件路徑創(chuàng)建音頻對(duì)象
lastAudio.current = new Audio(preFix + url);
// 開(kāi)始播放
lastAudio.current.play();
// 設(shè)置播放結(jié)束和錯(cuò)誤處理
lastAudio.current.onended = () => {
setPlayUid(-1);
};
lastAudio.current.onerror = () => {
setPlayUid(-1);
};
}
暫停當(dāng)前正在播放的音頻
const handlePauseAudio = () => {
// 1. 暫停音頻播放
if (lastAudio.current) {
lastAudio.current.pause();
}
// 2. 重置播放狀態(tài)
setPlayUid(-1);
};
7、文件內(nèi)容處理機(jī)制詳解
7.1 文件對(duì)象結(jié)構(gòu)
const file = {
name: 'audio.mp3', // 文件名
size: 51200, // 文件大小(字節(jié))
type: 'audio/mpeg', // MIME類型
lastModified: 1234567890, // 最后修改時(shí)間
// 文件的實(shí)際二進(jìn)制內(nèi)容存儲(chǔ)在內(nèi)存中,但前端不直接讀取
};
7.2 文件內(nèi)容存儲(chǔ)位置
前端: 只獲取文件的元數(shù)據(jù)(名稱、大小、類型等)
實(shí)際內(nèi)容: 存儲(chǔ)在瀏覽器的內(nèi)存中,作為 File 對(duì)象的一部分
傳輸: 通過(guò) HTTP 請(qǐng)求自動(dòng)傳輸?shù)椒?wù)器
7.3 文件內(nèi)容的傳輸機(jī)制
自動(dòng)傳輸過(guò)程
<Upload
name='file' // 表單字段名
action={file => Promise.resolve(uploadUrl)} // 上傳地址
// ...
/>
傳輸流程:
瀏覽器自動(dòng)創(chuàng)建 FormData 對(duì)象
將 File 對(duì)象作為 file 字段添加到表單
發(fā)送 POST 請(qǐng)求到服務(wù)器
文件內(nèi)容作為請(qǐng)求體的一部分自動(dòng)傳輸
服務(wù)器端接收
// 服務(wù)器端接收到的數(shù)據(jù)格式: // Content-Type: multipart/form-data // // --boundary // Content-Disposition: form-data; name="file"; filename="audio.mp3" // Content-Type: audio/mpeg // // [二進(jìn)制音頻文件內(nèi)容] // --boundary--
上傳文件生命周期
用戶選擇文件 → 文件存儲(chǔ)在瀏覽器內(nèi)存 → 通過(guò)HTTP傳輸?shù)椒?wù)器 → 服務(wù)器存儲(chǔ)
到此這篇關(guān)于React實(shí)現(xiàn)音頻文件上傳與試聽(tīng)的文章就介紹到這了,更多相關(guān)React 音頻上傳與試聽(tīng)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
理解react中受控組件和非受控組件及應(yīng)用場(chǎng)景
當(dāng)涉及到React框架時(shí),了解受控組件和非受控組件是非常重要的概念,本文主要介紹了理解react中受控組件和非受控組件及應(yīng)用場(chǎng)景,具有一定的參考價(jià)值,感興趣的可以了解一下2024-01-01
React虛擬渲染實(shí)現(xiàn)50個(gè)或者一百個(gè)圖表渲染
這篇文章主要為大家介紹了React虛擬渲染實(shí)現(xiàn)50個(gè)或者100個(gè)圖表渲染的實(shí)現(xiàn),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06
前端開(kāi)發(fā)使用Ant Design項(xiàng)目評(píng)價(jià)
這篇文章主要為大家介紹了前端開(kāi)發(fā)使用Ant Design項(xiàng)目評(píng)價(jià),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08
react路由基礎(chǔ)解讀(Router、Link和Route)
這篇文章主要介紹了react路由基礎(chǔ)解讀(Router、Link和Route),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-07-07

