前言
在上传较大的文件时,将文件切割成多个小块,然后每次只发送一小块,等到全部传输完毕之后,服务端将接受的多个小块进行合并,组成上传的文件,这就是前端上传大文件的方式,也就是所谓的以流的方式上传
下面会介绍如下几个快内容
前端代码如何编写
后端代码如何编写(node)
vue 中如何处理
使用插件如何处理
1. 前端代码实现
这里先不通过 vue,而是通过原生的 html、js 的方式实现上传,如此更加容易理解逻辑,等后面再将其转换成 vue 写法 文件上传通过 axios ,所以,可以先配置其 baseurl,我这里为axios.defaults.baseURL =
http://localhost:3000;
html 代码
<div id="app"> <form action=""> <input type="file" name="" id="uploadInput" /> <button id="uploadBtn">上传</button> </form></div>
1.1 选择上传文件
为 文件域 添加 change 事件,当用户选择要上传的文件后,将文件信息赋值给一个变量,方便上传文件时使用
document .getElementById("uploadInput") .addEventListener("change", handleFileChange);let file = null; // 文件被更改 function handleFileChange(event) { const file = event.target.files[0]; if (!file) return; window.file = file; }
1.2 文件上传
文件上传分为如下几个步骤
① 创建切片
② 上传切片
③ 全部上传成功后,告诉后端,后端将所有的切片整合成一个文件
首先编写几个函数,用于切片的处理及上传,最后再组合到一起实现完整功能
1.2.1 创建切片
// 创建切片 const createFileChunks = function (file, size = 1024*100) { // 创建数组,存储文件的所有切片 let fileChunks = []; for (let cur = 0; cur < file.size; cur += size) { // file.slice 方法用于切割文件,从 cur 字节开始,切割到 cur+size 字节 fileChunks.push(file.slice(cur, cur + size)); } return fileChunks; };
createFileChunks 方法接收两个参数
要进行切片的文件对象
切片大小,这里设置默认值为 1024*100,单位为字节
1.2.2 拼接 formData
上传的时候,通过 formData 对象组装要上传的切片数据
/** * 2、拼接 formData * 参数1:存储文件切片信息的数组 * 参数2:上传时的文件名称 */ const concatFormData = function (fileChunks, filename) { /** * map 方法会遍历切片数组 fileChunks中的元素map 方法会遍历切片数组 fileChunks中的元素, * 数组中有多少个切片,创建几个 formData,在其中上传的文件名称、hash值和切片,并将此 formData * 返回,最终chunksList中存储的就是多个 formData(每个切片对应一个 formData) * */ const chunksList = fileChunks.map((chunk, index) => { let formData = new FormData(); // 这个'filename' 字符串的名字要与后端约定好 formData.append("filename", filename); // 作为区分每个切片的编号,后端会以此作为切片的文件名称,此名称也应该与后端约定好 formData.append("hash", index); // 后端会以此作为切片文件的内容 formData.append("chunk", chunk); return { formData, }; }); return chunksList; };
1.2.3 上传切片
遍历上面的 chunksList 数组,调用 axios 对每个 formData 信息进行提交
// 3、上传切片 const uploadChunks=async (chunksList)=>{ const uploadList = chunksList.map(({ formData }) => axios({ method: "post", url: "/upload", data: formData, }) ); await Promise.all(uploadList); }
1.2.4 合并切片
当所有切片都已经上传成功后,告诉后端一声
// 合并切片 const mergeFileChunks = async function (filename) { await axios({ method: "get", url: "/merge", params: { filename, }, }); };
1.2.5 方法组合
上面编写了几个函数,下面将几个方法串联起来,实现切片上传功能
为上传按钮绑定单击事件
document .getElementById("uploadBtn") .addEventListener("click", handleFileUpload);
handleFileUpload 函数
// 大文件上传 async function handleFileUpload(event) { event.preventDefault(); const file = window.file; if (!file) return; // 1、切片切割,第二个参数采用默认值 const fileChunks = createFileChunks(file); // 2、将切片信息拼接成 formData 对象 const chunksList = concatFormData(fileChunks, file.name); // 3、上传切片 await uploadChunks(chunksList); // 4、所有切片上传成功后后,再告诉后端所有切片都已完成 await mergeFileChunks(file.name); console.log("上传完成"); }
1.2.6 完整代码
<!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>大文件上传</title> </head> <body> <div id="app"> <form action=""> <input type="file" name="" id="uploadInput" /> <button id="uploadBtn">上传</button> </form> </div> </body></html><script src="https://unpkg.com/axios/dist/axios.min.js"></script><script> axios.defaults.baseURL = `http://localhost:3000`; let file = null; // 文件被更改 function handleFileChange(event) { const file = event.target.files[0]; if (!file) return; window.file = file; } // 1、创建切片 const createFileChunks = (file, size = 1024 * 100) => { // 创建数组,存储文件的所有切片 let fileChunks = []; for (let cur = 0; cur < file.size; cur += size) { // file.slice 方法用于切割文件,从 cur 字节开始,切割到 cur+size 字节 fileChunks.push(file.slice(cur, cur + size)); } return fileChunks; }; /** * 2、拼接 formData * 参数1:存储文件切片信息的数组 * 参数2:上传时的文件名称 */ const concatFormData = function (fileChunks, filename) { /** * map 方法会遍历切片数组 fileChunks中的元素map 方法会遍历切片数组 fileChunks中的元素, * 数组中有多少个切片,创建几个 formData,在其中上传的文件名称、hash值和切片,并将此 formData * 返回,最终chunksList中存储的就是多个 formData(每个切片对应一个 formData) * */ const chunksList = fileChunks.map((chunk, index) => { let formData = new FormData(); // 这个'filename' 字符串的名字要与后端约定好 formData.append("filename", filename); // 作为区分每个切片的编号,后端会以此作为切片的文件名称,此名称也应该与后端约定好 formData.append("hash", index); // 后端会以此作为切片文件的内容 formData.append("chunk", chunk); return { formData, }; }); return chunksList; }; // 3、上传切片 const uploadChunks = async (chunksList) => { const uploadList = chunksList.map(({ formData }) => axios({ method: "post", url: "/upload", data: formData, }) ); await Promise.all(uploadList); }; // 大文件上传 async function handleFileUpload(event) { event.preventDefault(); const file = window.file; if (!file) return; // 1、切片切割,第二个参数采用默认值 const fileChunks = createFileChunks(file); // 2、将切片信息拼接成 formData 对象 const chunksList = concatFormData(fileChunks, file.name); // 3、上传切片 await uploadChunks(chunksList); // 4、所有切片上传成功后后,再告诉后端所有切片都已完成 await mergeFileChunks(file.name); console.log("上传完成"); } // 合并切片 const mergeFileChunks = async function (filename) { await axios({ method: "get", url: "/merge", params: { filename, }, }); }; document .getElementById("uploadInput") .addEventListener("change", handleFileChange); document .getElementById("uploadBtn") .addEventListener("click", handleFileUpload);</script>
2. 后端代码实现
因为后端不是我们主要关注点,所以直接上代码,就不做太过详细的解释了,有以下几点提起注意
因为前端通过 Promise.all 的方式执行所有的请求,所以切片发送的顺序是随机的,也就是说,后端获取的切片并保存切片的顺序可能是随机的,所以切片文件的名称不一定是从小到大排序的,所以读取切片组成文件时,要先按照切片名称从小答案排序,然后再组合,否则文件可能出错,这在上传大文件的时候非常明显
const multiparty = require("multiparty");const EventEmitter = require("events");const express = require("express");const cors = require("cors");const fs = require("fs");const path = require("path");const { Buffer } = require("buffer");const server = express();server.use(cors());const STATIC_TEMPORARY = path.resolve(__dirname, "static/temporary");const STATIC_FILES = path.resolve(__dirname, "static/files");server.post("/upload", (req, res) => { const multipart = new multiparty.Form(); const myEmitter = new EventEmitter(); const formData = { filename: undefined, hash: undefined, chunk: undefined, }; let isFieldOk = false, isFileOk = false; multipart.parse(req, function (err, fields, files) { formData.filename = fields["filename"][0]; formData.hash = fields["hash"][0]; isFieldOk = true; myEmitter.emit("start"); }); multipart.on("file", function (name, file) { formData.chunk = file; isFileOk = true; myEmitter.emit("start"); }); myEmitter.on("start", function () { if (isFieldOk && isFileOk) { const { filename, hash, chunk } = formData; const dir = `${STATIC_TEMPORARY}/${filename}`; try { if (!fs.existsSync(dir)) fs.mkdirSync(dir); const buffer = fs.readFileSync(chunk.path); const ws = fs.createWriteStream(`${dir}/${hash}`); ws.write(buffer); ws.close(); res.send(`${filename}-${hash} 切片上传成功`); } catch (error) { console.error(error); } isFieldOk = false; isFileOk = false; } });});server.get("/merge", async (req, res) => { const { filename } = req.query; try { let len = 0; const hash_arr = fs.readdirSync(`${STATIC_TEMPORARY}/${filename}`); // 将 hash 值按照大小进行排序 hash_arr.sort((n1, n2) => { return Number(n1) - Number(n2); }); const bufferList = hash_arr.map((hash) => { console.log(hash); const buffer = fs.readFileSync(`${STATIC_TEMPORARY}/${filename}/${hash}`); len += buffer.length; return buffer; }); const buffer = Buffer.concat(bufferList, len); const ws = fs.createWriteStream(`${STATIC_FILES}/${filename}`); ws.write(buffer); ws.close(); res.send(`切片合并完成`); } catch (error) { console.error(error); }});function deleteFolder(filepath) { if (fs.existsSync(filepath)) { fs.readdirSync(filepath).forEach((filename) => { const fp = `${filepath}/${filename}`; if (fs.statSync(fp).isDirectory()) deleteFolder(fp); else fs.unlinkSync(fp); }); fs.rmdirSync(filepath); }}server.listen(3000, () => { console.log("Server is running at http://127.0.0.1:3000");});
3. vue 改造
当然只需要改造前端代码,后端代码是不用修改的
新建单文件组件
document .getElementById("uploadInput") .addEventListener("change", handleFileChange);let file = null; // 文件被更改 function handleFileChange(event) { const file = event.target.files[0]; if (!file) return; window.file = file; }0原文:https://juejin.cn/post/7099098828187385886