大文件前端上传
- 前端进行切片
___切片是干什么的?比如说一个100MB的文件想要上传到服务器,我们可以用类似于上传图片一样的方式上传给后端,但是吧 因为这个文件内存实在是太大了,可能因为各方面原因使我们没法保证可以成功上传服务器。
___这个时候我们可以使用切片,顾名思义,就是把这个100MB的文件切成若干个小文件,比如1MB大小的文件,然后我们发送上传请求,一个接一个的去上传。最后上传成功后,我们让后端合并一下,然后返回给我们服务器上这个文件的地址就可以了。
//上传函数
function handleChange(e) {
const file = e.target.files[0];
//调用函数,进行切片
const fileList = handleThunk(file);
//遍历返回回来的切片数组,添加切片的文件名等信息
const uploadList = fileList.map((item, index) => {
//创建FormData格式的文件
const fileData = new FormData();
fileData.append("chunk", item.tempFile);
fileData.append("name", uuid + "@@" + index);
fileData.append("filename", uuid);
return axios.post("/upload_file_thunk", fileData);
});
//如果所有切片请求全都成功了,我们就发送一个合并请求
Promise.all(uploadList).then((res) => {
//所有切片上传成功
axios
.post("/upload_file_end", {
filename: uuid, //生成文件名
extname: file.name.split(".").slice(-1)[0], //文件后缀名,用来让后端给合并后的文件
})
.then((res) => {
console.log(res);
});
});
}
//切片函数
function handleThunk(file, size = 1024 * 1024 * 0.5) {
let current = 0; //这个文件切片之后下一个切片的大小
const res = []; //切片结果数组
//循环地去切片,当前所有切过的文件大小大于总大小就跳出循环,说明已经切完了
while (current < file.size) {
res.push({
tempFile: file.slice(current, current + size),
});
current += size;
}
return res;
}
- 后端接收切片
在看后端代码之前,我们先熟悉了解一下node的两个模块fs和path
- path.join()
path.join() 方法使用特定于平台的分隔符作为定界符将所有给定的 path 片段连接在一起,然后规范化生成的路径。
- path.resolve()
path.resolve() 方法将路径或路径片段的序列解析为绝对路径。
- path.dirname()
path.dirname() 方法返回 path 的目录名,类似于 Unix dirname 命令。 尾随的目录分隔符被忽略
- fs.createReadStream()
createReadStream 方法有两个参数,第一个参数是读取文件的路径,第二个参数为 options 选项,
createReadStream 的返回值为 fs.ReadStream 对象,读取文件的数据在不指定 encoding 时,默认为 Buffer。
创建可写流返回值的 rs 上的 pipe 方法是专门用来连接可读流和可写流的,可以将一个文件读来的内容通过流写到另一个文件中。pipe前是读文件,pipe后的括号中是写文件。
在创建可读流后默认是不会读取文件内容的,默认创建一个流 是非流动模式,默认不会读取数据
- fs.createWriteStream()
createWriteStream 方法有两个参数,第一个参数是读取文件的路径,第二个参数为 options 选项。
createWriteStream 返回值为 fs.WriteStream 对象,第一次写入时会真的写入文件中,继续写入,会写入到缓存中。
- fs.readdirSync()
读取目录的内容。
- fs.rm()
fs.rm()方法用于删除给定路径下的文件。也可以递归地使用它来删除目录。
fs.rm('./dummy.txt', { recursive:true }, (err) => {
if(err){
// File deletion failed
console.error(err.message);
return;
}
console.log("File deleted successfully");
})
接收代码如下:
//接收切片
router.post("/upload_file_thunk", async (ctx, next) => {
try {
const files = ctx.request.body;
const chunk = ctx.request.files.chunk;
//创建文件读入流
const readStream = fs.createReadStream(chunk.filepath);
//保存文件目录---这个文件目录,需要我们先手动创建
//path.join主要是把这些参数合并成一个新的路径
const file = path.join(__dirname, "../public/uploads/thunk/");
//创建文件写入流
const writeStream = fs.createWriteStream(file + files.name);
readStream.pipe(writeStream);
//把传来的切片统一存储
ctx.body = {
title: "成功",
};
} catch (error) {
console.log(error);
ctx.body = {
title: "失败",
data: error,
};
}
});
- 后端合并切片
//合并切片
router.post("/upload_file_end", async (ctx, next) => {
try {
const { filename, extname } = ctx.request.body;
//合并文件
thunkStreamMerge(
"../public/uploads/thunk/",
"../public/uploads/" + filename + "." + extname
);
ctx.body = {
title: "成功",
};
} catch (error) {
console.log(error);
ctx.body = {
title: "失败",
data: error,
};
}
});
合并代码
/**
*文件合并
* @param {*} sourceFiles 源文件目录:存放所有切片文件的目录
* @param {*} targetFiles 目标文件:合并之后的文件名
*/
function thunkStreamMerge(sourceFiles, targetFiles) {
const list = fs.readdirSync(path.resolve(__dirname, sourceFiles));
const fileWriteStream = fs.createWriteStream(
path.resolve(__dirname, targetFiles)
);
//进行递归调用合并文件
thunkStreamMergeProgress(list, fileWriteStream, sourceFiles);
}
/**
* 合并每一个切片
* @param {*} fileList 文件数据
* @param {*} fileWriteStream 最终的写入结果
* @param {*} sourceFiles 文件路径
*/
function thunkStreamMergeProgress(fileList, fileWriteStream, sourceFiles) {
console.log(fileList.length);
if (!fileList.length) {
return fileWriteStream.end("console.log('完成了')");
}
const currentFile = path.resolve(__dirname, sourceFiles, fileList.shift());
const currentReadSteam = fs.createReadStream(currentFile);
//写入文件内容,括号内的会覆盖readStream的内容
currentReadSteam.pipe(fileWriteStream, { end: false });
//合并后,删除切片
fs.rm(currentFile, { recursive: true }, (err) => {
if (err) {
console.error(err.message);
return;
}
});
currentReadSteam.on("end", () => {
thunkStreamMergeProgress(fileList, fileWriteStream, sourceFiles);
});
}
参考
- https://vimsky.com/examples/usage/node-js-fs-rm-method.html
- https://nodejs.cn/api/fs.html
- https://zhuanlan.zhihu.com/p/131627741
- https://juejin.cn/post/6844903681255538695#heading-12