【实用案例】录音分片上传的核心逻辑和实现案例【文章附有代码】
前言
最近接到一个业务需求:录音文件上传时因为文件过大(超过100M)有报错,
(服务器端对单次文件的上传,有大小限制)。
经过讨论,最后决定使用‘分片’解决问题。
我做了一个小案例(非业务代码),用HTML代码实现前端页面,用express实现后端服务。
一、分片上传的核心原理
分片上传是将大文件分割成多个小片段(分片),逐个上传到服务器,最后在服务器端合并所有分片形成完整文件的技术。这种方式的优势在于:
- 避免单次上传大文件导致的超时问题
- 支持断点续传,某分片失败只需重传该分片
- 减轻服务器单次处理压力,提高系统稳定性
对于录音文件来说,由于可能包含长时间的音频数据(尤其是高清录音),文件体积往往较大,非常适合采用分片上传方案。
二、前端实现核心逻辑
前端主要负责录音、文件分片和上传控制三个部分。
1. 录音功能实现
使用浏览器原生的MediaRecorderAPI 实现录音功能:
// 开始录音
startRecordBtn.addEventListener('click', async () => {const stream = await navigator.mediaDevices.getUserMedia({ audio: true });mediaRecorder = new MediaRecorder(stream);audioChunks = [];mediaRecorder.ondataavailable = (event) => {audioChunks.push(event.data);};mediaRecorder.start();// 更新UI状态...
});// 停止录音
stopRecordBtn.addEventListener('click', () => {mediaRecorder.stop();mediaRecorder.stream.getTracks().forEach(track => track.stop());mediaRecorder.onstop = () => {audioBlob = new Blob(audioChunks, { type: 'audio/webm' });audioUrl = URL.createObjectURL(audioBlob);// 显示上传区域...};
});
2. 分片上传核心实现
创建AudioChunkUploader类处理分片逻辑:
class AudioChunkUploader {constructor(options) {this.options = {chunkSize: 5 * 1024 * 1024, // 5MB每片uploadUrl: 'http://localhost:3000/upload/chunk',mergeUrl: 'http://localhost:3000/upload/merge',...options};this.state = {totalChunks: 0,uploadedChunks: 0,fileMd5: '', // 文件唯一标识isPaused: false};}// 计算文件MD5(用于标识同一文件)async _calculateFileMd5() {return new Promise((resolve) => {const fileReader = new FileReader();const spark = new SparkMD5.ArrayBuffer();fileReader.onload = (e) => {spark.append(e.target.result);this.state.fileMd5 = spark.end();resolve();};fileReader.readAsArrayBuffer(this.options.file);});}// 上传单个分片async _uploadSingleChunk(chunkIndex) {const start = chunkIndex * this.options.chunkSize;const end = Math.min(start + this.options.chunkSize, this.state.fileSize);const chunk = this.options.file.slice(start, end);const formData = new FormData();formData.append('chunk', chunkIndex);formData.append('chunks', this.state.totalChunks);formData.append('fileMd5', this.state.fileMd5);formData.append('file', chunk);return fetch(this.options.uploadUrl, {method: 'POST',body: formData}).then(response => response.json());}// 上传所有分片async _uploadChunks() {for (let i = 0; i < this.state.totalChunks; i++) {if (this.state.isPaused) {// 等待恢复上传await new Promise(resolve => {const check = setInterval(() => {if (!this.state.isPaused) {clearInterval(check);resolve();}}, 100);});}await this._uploadSingleChunk(i);this.state.uploadedChunks = i + 1;// 触发进度更新...}}// 通知服务器合并分片async _mergeChunks() {return fetch(this.options.mergeUrl, {method: 'POST',headers: { 'Content-Type': 'application/json' },body: JSON.stringify({fileMd5: this.state.fileMd5,fileName: this.options.fileName,chunks: this.state.totalChunks})}).then(response => response.json());}
}
3. 播放控制功能
实现带暂停 / 继续功能的音频播放:
let audioPlayer = null;
let lastPlayTime = 0;// 播放录音
playRecordBtn.addEventListener('click', () => {if (!audioPlayer) {audioPlayer = new Audio(audioUrl);if (lastPlayTime > 0) {audioPlayer.currentTime = lastPlayTime;}} else {audioPlayer.currentTime = lastPlayTime;}audioPlayer.play();// 更新按钮状态...
});// 暂停播放
stopPlayBtn.addEventListener('click', () => {lastPlayTime = audioPlayer.currentTime;audioPlayer.pause();// 更新按钮状态...
});
三、后端实现核心逻辑
Express 后端负责接收分片、临时存储和合并文件:
1. 分片接收接口
const multer = require('multer');
const upload = multer({ storage: multer.diskStorage({destination: (req, file, cb) => {// 为每个文件创建独立的临时目录const chunkDir = path.join(tempDir, req.body.fileMd5);if (!fs.existsSync(chunkDir)) {fs.mkdirSync(chunkDir, { recursive: true });}cb(null, chunkDir);},filename: (req, file, cb) => {// 用分片索引作为文件名cb(null, req.body.chunk);}})
});// 处理分片上传
app.post('/upload/chunk', upload.single('file'), (req, res) => {res.json({success: true,message: `分片 ${req.body.chunk} 上传成功`});
});
2. 分片合并接口
app.post('/upload/merge', (req, res) => {const { fileMd5, fileName, chunks } = req.body;const chunkDir = path.join(tempDir, fileMd5);const destPath = path.join(uploadDir, fileName);// 检查所有分片是否上传完成if (fs.readdirSync(chunkDir).length !== parseInt(chunks)) {return res.status(400).json({success: false,message: '分片不完整'});}// 合并分片const writeStream = fs.createWriteStream(destPath);let chunkIndex = 0;const mergeNextChunk = () => {const chunkPath = path.join(chunkDir, chunkIndex.toString());if (fs.existsSync(chunkPath)) {const readStream = fs.createReadStream(chunkPath);readStream.pipe(writeStream, { end: false });readStream.on('end', () => {fs.unlinkSync(chunkPath); // 删除已合并的分片chunkIndex++;mergeNextChunk();});} else {writeStream.end(); // 所有分片合并完成}};mergeNextChunk();writeStream.on('finish', () => {fs.rmdirSync(chunkDir, { recursive: true }); // 清理临时目录res.json({success: true,fileName,filePath: destPath});});
});
四、效果展示
可以在设定好存放上传文件的文件夹下看到上传的录音文件
五、总结与拓展
录音文件分片上传方案的核心在于:
- 前端将录音文件分割成固定大小的分片,计算文件唯一标识
- 逐个上传分片,支持暂停 / 继续功能
- 后端接收分片并临时存储
- 所有分片上传完成后,后端按顺序合并成完整文件
方案可以进一步优化:
- 添加断点续传功能,上传前先查询已上传的分片
- 实现分片上传的并发控制,提高上传速度
- 增加文件校验机制,确保上传文件的完整性
- 对大文件 MD5 计算进行优化,避免页面卡顿
分片上传技术不仅适用于录音文件,也可推广到视频、文档等各种大文件上传场景,是 Web 开发中处理大文件的重要方案。
代码获取,后台联系。