这是我参与「第四届青训营」笔记创作活动的第8天
这篇文章来讲讲前端文件上传的相关方法
普通的文件上传
这里说的普通方式即 前端 传 FormData 类型的数据。 前端的处理方式很简单, 只用把它用一个 FormData 包装就行了
let formData = new FormData();
formData.append("file", _file);
formData.append("filename", _file.name);
instance
.post("/upload_single", formData)
.then((res) => {
const { code, url } = res;
if (code === 0) {
alert(`file 上传成功${url}`);
return;
}
console.log(res);
return Promise.reject(data.codeText);
})
.catch((e) => {
console.log(e);
});
后端(node.js) 这里我用了 multipartry 来进行解析。
注意: 这里的 multiparty 它是仅限 FormData 类型的。
如果需要改变文件名称, 就等上传完成后用 fs.rename 实现。
app.post("/upload_single", async (req, res) => {
try {
//todo: 单文件上传核心, 用 multiparty_load 进行处理
let { files, fields } = await multipartry_load(req, true);
let file = (files.file && files.file[0]) || {};
res.send({
code: 0,
codeText: "上传成功",
originFilename: file.originFilename,
url: file.path.replace(baseDir, FONTHOSTNAME),
});
} catch (err) {
res.send({
code: 1,
codeText: err,
});
}
});
const multipartry_load = function (req, auto) {
typeof auto !== "boolean" ? (auto = false) : null;
let config = {
maxFieldsSize: 200 * 1024 * 1024,
};
if (auto) config.uploadDir = uploadDir;
// 解析文件并放到指定目录下
return new Promise(async (resolve, reject) => {
await delay(); //
// 用来将客户端formData 结果解析
new multipartry.Form(config).parse(req, (err, fields, files) => {
if (err) {
reject(err);
return;
}
resolve({
fields,
files,
});
});
});
};
大文件切片上传
原理其实很简单, 就是将文件转成 buffer 数组, 然后再用 slice 方法对数组进行切片, 针对每一部分单个进行上传。
通过计算每个分片的 hash 值, 最后只需要判断是否所有的 hash 都传到了后端。 也能通过 hash 来进行断点续传, 只需要重传 对应的切片即可
如果切片过多, 还可以控制请求的并发量。。。
/**
*
* @param {} file
* @returns
* 根据内容生成hash名字
*/
const changeBuffer = (file) => {
return new Promise((resolve) => {
let fileReader = new FileReader();
fileReader.readAsArrayBuffer(file);
fileReader.onload = (e) => {
let buffer = e.target.result;
const spark = new SparkMD5.ArrayBuffer();
spark.append(buffer);
const HASH = spark.end();
const suffix = /.([0-9a-zA-Z]+)$/.exec(file.name)[1];
resolve({
buffer,
HASH,
suffix,
filename: `${HASH}.${suffix}`,
});
};
});
};
/ 点击开始上传
let chunkList = [];
let alreadyChunkList = [];
console.log(_file);
let maxSize = 1024 * 1024;
let maxCount = Math.ceil(_file.size / maxSize); // 最大允许分割的切片数量为30
let index = 0;
if (!_file) return alert("请先选择图片");
// 获取文件的hash, 整个文件的
const { HASH, suffix } = await changeBuffer(_file);
// 判断当前文件可以切出多少切片
if (maxCount > 10) {
// 如果切片数量大于最大值
maxSize = _file.size / 10; // 则改变切片大小
maxCount = 10;
}
console.log(maxCount, "maxCount");
console.log(maxSize, "maxSize");
// 切片
while (index < maxCount) {
chunkList.push({
file: _file.slice(index * maxSize, (index + 1) * maxSize),
filename: `${HASH}_${index + 1}.${suffix}`,
});
index++;
}
// 先获取已经上传的切片
const data = await instance.post(
"/upload_already",
{
HASH: HASH,
},
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
}
);
index = 0;
// 回调
const complate = async () => {
index++;
let progress = `(${index}/${maxCount})%`; // 进度条
if (index >= maxCount) {
console.log("ok, 切片完成");
}
};
const { fileList } = data;
alreadyChunkList = fileList; // 已经上传的切片
console.log(chunkList, "chunkList");
chunkList = chunkList.map((item) => {
if (
alreadyChunkList.length > 0 &&
alreadyChunkList.includes(item.filename)
) {
debugger;
// 表示切片已经存在
complate();
return;
}
const fm = new FormData();
fm.append("file", item.file);
fm.append("filename", item.filename);
return new Promise((sovle) => {
instance
.post("/upload_chunk", fm)
.then(() => {
complate();
sovle();
})
.catch(() => {
//
});
});
});
Promise.all(chunkList).then(() => {
instance
.post(
"/upload_merge",
{
HASH: HASH,
count: maxCount,
},
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
}
)
.then((res) => {
console.log("ok");
});
});
});
后端
/**
* 上传切片
*/
app.post("/upload_chunk", async (req, res) => {
try {
const { fields, files } = await multipartry_load(req);
const file = (files.file && files.file[0]) || {};
const filename = (fields.filename && fields.filename[0]) || "";
// const path = `${uploadDir}/${filename}`
let isExists = false;
// 创建存放切片的临时目录
const [, HASH] = /^([^_]+)_(\d+)/.exec(filename);
let path = `${uploadDir}/${HASH}`; // 用hash生成一个临时文件夹
!fs.existsSync(path) ? fs.mkdirSync(path) : null; // 判断该文件夹是否存在,不存在的话,新建一个文件夹
path = `${uploadDir}/${HASH}/${filename}`; // 将切片存到临时目录中
isExists = await exists(path);
if (isExists) {
res.send({
code: 0,
codeText: "file is already exists",
url: path.replace(FONTHOSTNAME, HOSTNAME),
});
return;
}
writeFile(res, path, file, filename, true);
} catch (e) {
res.send({
code: 1,
codeText: e,
});
}
});
/**
* 合并切片
*/
app.post("/upload_merge", async (req, res) => {
const { HASH, count } = req.body;
try {
const { filname, path } = await merge(HASH, count);
res.send({
code: 0,
codeText: "merge sucessfully",
url: path.replace(baseDir, FONTHOSTNAME),
});
} catch (e) {
res.send({
code: 1,
codeText: e,
});
}
});
流的形式
那么, 如果在没有FormData的情况怎么上传呢? 最近我在写 webpack 文件上传插件时遇到了一个 Content-Type: application/octet-stream 类型的文件
它获取文件的方法如下, 即通过字节流的形式获取
// 上传这个文件
const req = http.request(`${url}?name=${path.basename(file)}`, {
method: "POST",
headers: {
"Content-Type": "application/octet-stream",
Connection: "keep-alive",
"Transfer-Encoding": "chunked",
},
});
fs.createReadStream(file)
.on("data", (chunk) => {
req.write(chunk);
})
.on("end", () => {
req.end();
fs.unlink(file, () => {
console.log("删除成功");
});
resolve();
});
那么后端如何进行合并呢? 这边是用到了formidable这个包来实现, 具体用法如下. 用法与 multipartry 一样, 只是它也支持流类型。
app.post("/upload", async (req, res) => {
var form = new formidable.IncomingForm(); // 创建上传表单
form.encoding = "utf-8"; // 设置编辑
form.uploadDir = `${__dirname}/map`; // 设置上传目录
form.keepExtensions = true; // 保留后缀
form.maxFieldsSize = 2 * 1024 * 1024; // 文件大小(默认20M)
form.parse(req, function (err, fields, files) {
if (err) {
res.send({
status: 201,
message: err,
});
return;
}
try {
var newPath = form.uploadDir + "/" + req.query.name;
// 若文件流的键名为uplaodFile,则fs.renameSync(files.uplaodFile.path, newPath)
fs.renameSync(files.file.filepath, newPath); //重命名
res.send({ status: 200, message: "文件上传成功" });
} catch (err) {
res.send({
status: 201,
message: err,
});
return;
}
});
});
总结
这次主要是学到了 文件的上传, 切片上传, 以及node环境下的上传。 原理都一样的, 只要知道了怎么获取文件, 传输时需要什么请求头, 后端如何处理, 一般就都解决了。
然后想要优化的话, 可能就是用 webworker 创建一个后台线程去上传等等