js文件切片上传
此文为完整的
单文件上传多文件上传就当是课后作业了🥳;
这里重点说一下,由于是切片上传,所有后端拿到的是blob 的数据流
由前端和后端两个部分组成
参考一些资料,记不清是那些了,总结如下:
学习流程
选择大文件 => 文件切片 => 上传切片 => 合并切片 => 返回URL
下载demo 源码边看边学
前端
vue2 + el
选择大文件
<el-upload
ref="file"
:http-request="handleFileUpload"
action="#"
class="avatar-uploader"
:show-file-list="false"
>
<el-button type="primary">上传文件</el-button>
</el-upload>
......
/**
* @description: 自定义处理要上传的文件
* @param {*} e
*/
async handleFileUpload(e) {
console.log("① 要上传的文件:", e);
const { file = null } = e;
if (!file) {
return;
}
this.file = file;
this.upload();
},
......
文件切片
- 对文件计算切片信息
- 根据切片信息对文件进行分割,返回
ArrayBuffer数组
/**
* @description: 开始切片上传
*/
async upload() {
// 根据文件大小计算切片长度
const chunksTemp = createFileChunk(this.file);
console.log("② 切片长度计算完成:", chunksTemp);
// 根据切片长度进行切片 、 对整个文件进行hash
const self = this;
const hash = await calculateHash(chunksTemp, (progress) => {
self.hashProgress = progress;
});
this.hash = hash;
console.log("③ 要上传文件hash映射值:", hash);
......
},
分析切片信息
- 若已存在:
返回 url - 断点续传
- 第一次上传
// 查询是否上传,或者是否继续断点上传
this.$http
.post("/checkfile", {
hash,
ext: this.file.name.split(".").pop(),
})
.then((res) => {
if (!res || !res.data) {
return;
}
const { uploaded, uploadedList } = res.data;
if (uploaded) {
console.log("④ 文件已存在,无需上传");
return this.$message.success("秒传成功");
}
// 组装上传数据
const { chunks, requests } = splicingUploadParams(
chunksTemp,
this.hash,
uploadedList
);
// 将拼接后的切片数据赋值给原始数据
this.chunks = chunks;
// 上传需要上传的切片信息
this.uploadChunks(requests);
});
上传切片数组每一个文件
/**
* @description: 上传需要上传的切片信息
* @param {*} requests
*/
async uploadChunks(requests) {
console.log("需要上传的切片:", requests);
// 并发,发送切片请求 3 代表一次并发3个请求上传
startUpload("/uploadfile", this.chunks, requests, 3).then(() => {
console.log("所有切片上传完成✅");
this.mergeFile();
});
},
合并上传的切片
/**
* @description: 发送合并请求
*/
mergeFile() {
this.$http
.post("/mergeFile", {
ext: this.file.name.split(".").pop(),
size: CHUNK_SIZE,
hash: this.hash,
})
.then((res) => {
if (res && res.data) {
console.log(res.data);
}
});
},
后端
koa + fs-extra
/checkfile分析切片信息/uploadfile上传接口/mergeFile合并接口- 合并切片,并保存
const Koa = require("koa");
const Router = require("koa-router");
const koaBody = require("koa-body");
const path = require("path");
const fse = require("fs-extra");
const app = new Koa();
const router = new Router();
const UPLOAD_DIR = path.resolve(__dirname, "public");
app.use(
koaBody({
multipart: true, // 支持文件上传
})
);
router.post("/checkfile", async (ctx) => {
const body = ctx.request.body;
console.log(body);
const { ext, hash } = body;
const filePath = path.resolve(UPLOAD_DIR, `${hash}.${ext}`);
let uploaded = false;
let uploadedList = [];
if (fse.existsSync(filePath)) {
uploaded = true;
} else {
uploadedList = await getUploadedList(path.resolve(UPLOAD_DIR, hash));
}
ctx.body = {
code: 0,
data: {
uploaded, // 是否存在该文件
uploadedList, // 已上传的切片数组
},
};
});
/**
* @description: 获取目录下已上传的切片数组
* @param {*} dirPath
*/
async function getUploadedList(dirPath) {
return fse.existsSync(dirPath)
? (await fse.readdir(dirPath)).filter((name) => name[0] !== ".")
: [];
}
router.post("/uploadfile", async (ctx) => {
const body = ctx.request.body;
const file = ctx.request.files.chunk;
// console.log('上传接收到的:body:',body);
const { hash, name, totalBlock } = body;
const chunkPath = path.resolve(UPLOAD_DIR, hash);
if (!fse.existsSync(chunkPath)) {
await fse.mkdir(chunkPath);
}
await fse.move(file.filepath, `${chunkPath}/${name}`);
ctx.body = {
code: 0,
message: `切片上传成功`,
};
});
/**
* @description: 合并所有切片
* @param {*} ext 文件扩展名
* @param {*} size 设定的切片大小
* @param {*} hash 文件的hash值
*/
router.post("/mergeFile", async (ctx) => {
const body = ctx.request.body;
const { ext, size, hash } = body;
// 文件最终路径
const filePath = path.resolve(UPLOAD_DIR, `${hash}.${ext}`);
await mergeFile(filePath, size, hash);
ctx.body = {
code: 0,
data: {
url: `/public/${hash}.${ext}`,
},
};
});
/**
* @description: 读取所有切片数据,并排序
* @param {*} filePath 文件最终路径
*/
async function mergeFile(filePath, size, hash) {
// 1. 读取所有切片
const chunkDir = path.resolve(UPLOAD_DIR, hash);
let chunks = await fse.readdir(chunkDir);
// 2. 排序切片,得到切片地址数组
chunks = chunks.sort((a, b) => a.split("-")[1] - b.split("-")[1]);
chunks = chunks.map((cpath) => path.resolve(chunkDir, cpath));
await mergeChunks(chunks, filePath, size);
}
function mergeChunks(files, dest, CHUNK_SIZE) {
/**
* @description:
* @param {*} filePath 切片地址
* @param {*} writeStream 切片写入目标信息实例
*/
const pipeStream = (filePath, writeStream) => {
return new Promise((resolve, reject) => {
// 读取切片数据
const readStream = fse.createReadStream(filePath);
// 读取完成删除切片
readStream.on("end", () => {
fse.unlinkSync(filePath);
resolve();
});
// 切片数据拼接到目标地址中,用于合并生成新的文件
readStream.pipe(writeStream);
});
};
const pipes = files.map((file, index) => {
return pipeStream(
file,
fse.createWriteStream(dest, {
start: index * CHUNK_SIZE,
end: (index + 1) * CHUNK_SIZE,
})
);
});
return Promise.all(pipes);
}
app.use(router.routes());
app.listen(7001, () => {
console.log("server running at 7001");
});
总结
此文为最基础的大文件分割上传,希望对家有所帮助。
由于代码写的太久了,忘记了参考的文章,如有雷同请告知;