1.大致流程
分为以下几步:
- 1.前端接收BGM并进行
切片
- 2.将每份
切片
都进行上传
- 3.后端接收到所有
切片
,创建一个文件夹
存储这些切片
- 4.后端将此
文件夹
里的所有切片合并为完整的BGM文件
- 5.删除
文件夹
,因为切片
不是我们最终想要的,可删除
- 6.当服务器已存在某一个文件时,再上传需要实现
“秒传”

2.前端实现切片
简单来说就是,咱们上传文件时,选中文件后,浏览器会把这个文件转成一个Blob对象
,而这个对象的原型上上有一个slice
方法,这个方法是大文件能够切片的原理,可以利用这个方法来给打文件切片
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| <input type="file" @change="handleFileChange" /> <el-button @click="handleUpload"> 上传 </el-button>
data() { return { fileObj: { file: null } }; }, methods: { handleFileChange(e) { const [file] = e.target.files if (!file) return this.fileObj.file = file }, handleUpload () { const fileObj = this.fileObj if (!fileObj.file) return const chunkList = this.createChunk(fileObj.file) console.log(chunkList) }, createChunk(file, size = 5 * 1024 * 1024) { const chunkList = [] let cur = 0 while(cur < file.size) { chunkList.push({ file: file.slice(cur, cur size) }) cur = size } return chunkList } 复制代码
|
例子我就用我最近很喜欢听得一首歌嘉宾-张远
,他的大小是32M

点击上传,看看chunkList
长什么样子吧:

证明我们切片成功了!!!分成了7个切片
3.上传切片并展示进度条
我们先封装一个请求方法,使用的是axios
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import axios from "axios";
axiosRequest({ url, method = "post", data, headers = {}, onUploadProgress = (e) => e, }) { return new Promise((resolve, reject) => { axios[method](url, data, { headers, onUploadProgress, }) .then((res) => { resolve(res); }) .catch((err) => { reject(err); }); }); } 复制代码
|
接着上一步,我们获得了所有切片
,接下来要把这些切片保存起来,并逐一去上传
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| handleUpload() { const fileObj = this.fileObj; if (!fileObj.file) return; const chunkList = this.createChunk(fileObj.file); this.fileObj.chunkList = chunkList.map(({ file }, index) => ({ file, size: file.size, percent: 0, chunkName: `${fileObj.file.name}-${index}`, fileName: fileObj.file.name, index, })); this.uploadChunks(); }, 复制代码
|
uploadChunks
就是执行上传所有切片的函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| async uploadChunks() { const requestList = this.fileObj.chunkList .map(({ file, fileName, index, chunkName }) => { const formData = new FormData(); formData.append("file", file); formData.append("fileName", fileName); formData.append("chunkName", chunkName); return { formData, index }; }) .map(({ formData, index }) => this.axiosRequest({ url: "http://localhost:3000/upload", data: formData, onUploadProgress: this.createProgressHandler( this.fileObj.chunkList[index] ), }) ); await Promise.all(requestList); }, createProgressHandler(item) { return (e) => { item.percent = parseInt(String((e.loaded / e.total) * 100)); }; }, 复制代码
|
我不知道他们后端Java
是怎么做的,我这里使用Nodejs
模拟一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| const http = require("http"); const path = require("path"); const fse = require("fs-extra"); const multiparty = require("multiparty");
const server = http.createServer(); const UPLOAD_DIR = path.resolve(__dirname, ".", `qiepian`);
server.on("request", async (req, res) => { res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Headers", "*"); if (req.method === "OPTIONS") { res.status = 200; res.end(); return; } console.log(req.url)
if (req.url === '/upload') { const multipart = new multiparty.Form();
multipart.parse(req, async (err, fields, files) => { if (err) { console.log('errrrr', err) return; } const [file] = files.file; const [fileName] = fields.fileName; const [chunkName] = fields.chunkName; const chunkDir = path.resolve(UPLOAD_DIR, `${fileName}-chunks`); if (!fse.existsSync(chunkDir)) { await fse.mkdirs(chunkDir); } await fse.move(file.path, `${chunkDir}/${chunkName}`); res.end( JSON.stringify({ code: 0, message: "切片上传成功" })); }); } })
server.listen(3000, () => console.log("正在监听 3000 端口")); 复制代码
|
接下来就是页面上进度条的显示了,其实很简单,我们想要展示总进度条
,和各个切片的进度条
,各个切片的进度条我们都有了,我们只需要算出总进度
就行,怎么算呢?这么算:各个切片百分比 * 各个切片的大小 / 文件总大小
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <div style="width: 300px"> 总进度: <el-progress :percentage="totalPercent"></el-progress> 切片进度: <div v-for="item in fileObj.chunkList" :key="item"> <span>{{ item.chunkName }}:</span> <el-progress :percentage="item.percent"></el-progress> </div> </div>
computed: { totalPercent() { const fileObj = this.fileObj; if (fileObj.chunkList.length === 0) return 0; const loaded = fileObj.chunkList .map(({ size, percent }) => size * percent) .reduce((pre, next) => pre next); return parseInt((loaded / fileObj.file.size).toFixed(2)); }, }, 复制代码
|
我们再次上传音乐,查看效果:

后端也成功保存了

4.合并切片为BGM
好了,咱们已经保存好所有切片,接下来就要开始合并切片
了,我们会发一个/merge
请求,叫后端合并这些切片,前端代码添加合并的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| async uploadChunks() { const requestList = this.fileObj.chunkList .map(({ file, fileName, index, chunkName }) => { const formData = new FormData(); formData.append("file", file); formData.append("fileName", fileName); formData.append("chunkName", chunkName); return { formData, index }; }) .map(({ formData, index }) => this.axiosRequest({ url: "http://localhost:3000/upload", data: formData, onUploadProgress: this.createProgressHandler( this.fileObj.chunkList[index] ), }) ); await Promise.all(requestList);
this.mergeChunks() }, mergeChunks(size = 5 * 1024 * 1024) { this.axiosRequest({ url: "http://localhost:3000/merge", headers: { "content-type": "application/json", }, data: JSON.stringify({ size, fileName: this.fileObj.file.name }), }); } 复制代码
|
后端增加/merge
接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| const resolvePost = req => new Promise(res => { let chunk = '' req.on('data', data => { chunk = data }) req.on('end', () => { res(JSON.parse(chunk)) })
}) const pipeStream = (path, writeStream) => { console.log('path', path) return new Promise(resolve => { const readStream = fse.createReadStream(path); readStream.on("end", () => { fse.unlinkSync(path); resolve(); }); readStream.pipe(writeStream); }); }
const mergeFileChunk = async (filePath, fileName, size) => { const chunkDir = path.resolve(UPLOAD_DIR, `${fileName}-chunks`); let chunkPaths = null chunkPaths = await fse.readdir(chunkDir); chunkPaths.sort((a, b) => a.split("-")[1] - b.split("-")[1]); const arr = chunkPaths.map((chunkPath, index) => { return pipeStream( path.resolve(chunkDir, chunkPath), fse.createWriteStream(filePath, { start: index * size, end: (index 1) * size }) ) }) await Promise.all(arr) }; if (req.url === '/merge') { const data = await resolvePost(req); const { fileName, size } = data; const filePath = path.resolve(UPLOAD_DIR, fileName); await mergeFileChunk(filePath, fileName, size); res.end( JSON.stringify({ code: 0, message: "文件合并成功" }) ); } 复制代码
|
现在我们重新上传音乐,发现切片上传成功了,也合并成功了:

5.删除切片
上一步我们已经完成了切片合并
这个功能了,那之前那些存在后端的切片就没用了,不然会浪费服务器的内存,所以我们在确保合并成功后,可以将他们删除
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| const mergeFileChunk = async (filePath, fileName, size) => { const chunkDir = path.resolve(UPLOAD_DIR, `${fileName}-chunks`); let chunkPaths = null chunkPaths = await fse.readdir(chunkDir); chunkPaths.sort((a, b) => a.split("-")[1] - b.split("-")[1]); const arr = chunkPaths.map((chunkPath, index) => { return pipeStream( path.resolve(chunkDir, chunkPath), fse.createWriteStream(filePath, { start: index * size, end: (index 1) * size }) ) }) await Promise.all(arr) fse.rmdirSync(chunkDir); }; 复制代码
|
我们再次上传,再看看,那个储存此音乐的切片文件夹被我们删了
:

6.秒传功能
所谓的秒传功能
,其实没那么高大上,通俗点说就是,当你上传一个文件时,后端会判断服务器上有无这个文件,有的话就不执行上传,并返回给你“上传成功”
,想要执行此功能,后端需要重新写一个接口/verify
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| if (req.url === "/verify") { const data = await resolvePost(req); const { fileName } = data; const filePath = path.resolve(UPLOAD_DIR, fileName); console.log(filePath) if (fse.existsSync(filePath)) { res.end( JSON.stringify({ shouldUpload: false }) ); } else { res.end( JSON.stringify({ shouldUpload: true }) ); } 复制代码
|
前端在上传文件
步骤也要做拦截:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| async handleUpload() { const fileObj = this.fileObj; if (!fileObj.file) return; const { shouldUpload } = await this.verifyUpload( fileObj.file.name, ); if (!shouldUpload) { alert("秒传:上传成功"); return; } const chunkList = this.createChunk(fileObj.file); this.fileObj.chunkList = chunkList.map(({ file }, index) => ({ file, size: file.size, percent: 0, chunkName: `${fileObj.file.name}-${index}`, fileName: fileObj.file.name, index, })); this.uploadChunks(); }, async verifyUpload (fileName) { const { data } = await this.axiosRequest({ url: "http://localhost:3000/verify", headers: { "content-type": "application/json", }, data: JSON.stringify({ fileName, }), }); return data } 复制代码
|
现在我们重新上传音乐,因为服务器上已经存在了张远-嘉宾
这首歌了,所以,直接alert出秒传:上传成功

暂停续传
1.大致流程
暂停续传其实很简单,比如一个文件被切成10片
,当你上传成功5片
后,突然暂停,那么下次点击续传
时,只需要过滤掉之前已经上传成功的那5片
就行,怎么实现呢?其实很简单,只需要点击续传
时,请求/verity
接口,返回切片文件夹里
现在已成功上传的切片列表,然后前端过滤后再把还未上传的切片
的继续上传就行了,后端的/verify
接口需要做一些修改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| if (req.url === "/verify") { const createUploadedList = async fileName => fse.existsSync(path.resolve(UPLOAD_DIR, fileName)) ? await fse.readdir(path.resolve(UPLOAD_DIR, fileName)) : []; const data = await resolvePost(req); const { fileName } = data; const filePath = path.resolve(UPLOAD_DIR, fileName); console.log(filePath) if (fse.existsSync(filePath)) { res.end( JSON.stringify({ shouldUpload: false }) ); } else { res.end( JSON.stringify({ shouldUpload: true, uploadedList: await createUploadedList(`${fileName}-chunks`) }) ); } } 复制代码
|
2.暂停上传
前端增加一个暂停按钮
和pauseUpload
事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| <el-button @click="pauseUpload"> 暂停 </el-button>
const CancelToken = axios.CancelToken; const source = CancelToken.source();
axiosRequest({ url, method = "post", data, headers = {}, onUploadProgress = (e) => e, }) { return new Promise((resolve, reject) => { axios[method](url, data, { headers, onUploadProgress, cancelToken: source.token }) .then((res) => { resolve(res); }) .catch((err) => { reject(err); }); }); }, pauseUpload() { source.cancel("中断上传!"); source = CancelToken.source(); } 复制代码
|
3.续传
增加一个续传
按钮,并增加一个keepUpload
事件
1 2 3 4 5 6 7 8 9
| <el-button @click="keepUpload"> 续传 </el-button>
async keepUpload() { const { uploadedList } = await this.verifyUpload( this.fileObj.file.name ); this.uploadChunks(uploadedList); } 复制代码
|
4.优化进度条
1 2 3 4 5 6 7 8 9
| 续传`中,由于那些没有上传的切片会`从零开始`传,所以会导致`总进度条`出现`倒退现象`,所以我们要对`总进度条`做一下优化,确保他不会`倒退`,做法就是维护一个变量,这个变量只有在`总进度大于他`时他才会`更新成总进度 总进度: <el-progress :percentage="tempPercent"></el-progress>
watch: { totalPercent (newVal) { if (newVal > this.tempPercent) this.tempPercent = newVal } },
|
作者:Sunshine_Lin链接:https://juejin.cn/post/6982877680068739085 来源:掘金著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
个人使用vue实现代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
| <template> <div id="app"> <input type="file" @change="fileChange" /> <button @click="upload">提交</button> </div> </template>
<script> import HelloWorld from "./components/HelloWorld";
export default { name: "App", data() { return { checkedFile: null, spliceFileList: [], }; }, components: { HelloWorld, }, methods: { fileChange(e) { this.checkedFile = e.target.files; console.log(this.checkedFile); }, upload() { let size = 0; const splitSize = 10 * 1024; const fileList = []; while (size < this.checkedFile[0].size) { fileList.push(this.checkedFile[0].slice(size, size + splitSize)); size += splitSize; } this.spliceFileList = fileList.map((file, index) => { const formData = new FormData(); formData.append("fileName", this.checkedFile[0].name); formData.append("file", file); return { formData, index, }; }); this.sendFile(); }, sendFile() { this.spliceFileList.forEach((file) => { let xhr = new XMLHttpRequest(); xhr.open("post", "http://www.shaoyuhong.cn/lx104.php"); xhr.send(file.formData); xhr.onreadystatechange = function () { if (this.readyState === 4 && this.status === 200) { console.log(this.responseText); } }; }); }, }, }; </script>
<style> #app { font-family: "Avenir", Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; } </style>
|