前端大文件上传,即以流的方式上传
前言
在上传较大的文件时,将文件切割成多个小块,然后每次只发送一小块,等到全部传输完毕之后,服务端将接受的多个小块进行合并,组成上传的文件,这就是前端上传大文件的方式,也就是所谓的以流的方式上传
下面会介绍如下几个快内容
- 前端代码如何编写
- 后端代码如何编写(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 改造
当然只需要改造前端代码,后端代码是不用修改的
新建单文件组件
<template>
<div>
<form action="">
<input type="file" @change="handleFileChange($event)" />
<button @click.prevent="handleFileUpload()">上传</button>
</form>
</div>
</template>
<script>
import axios from "axios";
axios.defaults.baseURL = "http://localhost:3000";
export default {
data() {
return {
file: null,
};
},
methods: {
handleFileChange(event) {
const file = event.target.files[0];
if (!file) return;
this.file = file;
},
// 1、创建切片
createFileChunks(size = 1024 * 100) {
// 创建数组,存储文件的所有切片
let fileChunks = [];
for (let cur = 0; cur < this.file.size; cur += size) {
// file.slice 方法用于切割文件,从 cur 字节开始,切割到 cur+size 字节
fileChunks.push(this.file.slice(cur, cur + size));
}
return fileChunks;
},
/**
* 2、拼接 formData
* 参数1:存储文件切片信息的数组
* 参数2:上传时的文件名称
*/
concatFormData(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、上传切片
async uploadChunks(chunksList) {
const uploadList = chunksList.map(({ formData }) =>
axios({
method: "post",
url: "/upload",
data: formData,
})
);
await Promise.all(uploadList);
},
// 大文件上传
async handleFileUpload() {
console.log(1);
const file = this.file;
if (!file) return;
// 1、切片切割,第二个参数采用默认值
const fileChunks = this.createFileChunks();
// 2、将切片信息拼接成 formData 对象
const chunksList = this.concatFormData(fileChunks, this.file.name);
// 3、上传切片
await this.uploadChunks(chunksList);
// 4、所有切片上传成功后后,再告诉后端所有切片都已完成
await this.mergeFileChunks(this.file.name);
console.log("上传完成");
},
// 合并切片
async mergeFileChunks(filename) {
await axios({
method: "get",
url: "/merge",
params: {
filename,
},
});
},
},
};
</script>
转载自:https://juejin.cn/post/7099098828187385886