前端切片上传文件(可暂停),以链接形式返回
前言
不管是在使用他人的网页还是自己设计的网页,有一项功能可以说是很常见的,上传文件。常见的有图片(.jpg .png)、音频(.MP3 .MP4),文档(.doc)等。一般都要前后端的配合,但是懂得都懂前端还有NodeJS,所以开始干活。(一定还有更厉害的方案,更厉害的代码,有什么更好的实现,可以在评论中说说,最好直接上代码)
样例展示
搭环境
我们还是使用我们稍微熟悉点的NodeJS框架-KOA2
安装依赖
npm install koa --save
npm install koa-router --save
npm i formidable --save
npm install koa-static --save
npm i @koa/cors --save
测试环境
环境没有问题
var Koa = require('koa');
var app = new Koa();
var Router = require('koa-router')();
const fs = require('fs')
const formidable = require('formidable')
const path = require('path')
const koaStatic = require('koa-static')
app.use(koaStatic(path.join(__dirname, 'public')))
//开启跨域
const cors = require("@koa/cors")
app.use(cors())
app.use(async (ctx, next) => {
ctx.set('Access-Control-Allow-Origin', '*');
ctx.set('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild');
ctx.set('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
if (ctx.method == 'OPTIONS') {
ctx.body = 200;
} else {
await next();
}
});
Router.get('/', async (ctx) => {
ctx.body = "ok"
})
app
.use(Router.routes()) //启动路由
.use(Router.allowedMethods());
app.listen(3000);
思路及实现
前端思路
既然是文件的切片上传,那么切片要有吧,我们使用Blob
的slice
方法,我们就可以对二进制文件进行拆分。一个个文件有了那就上传吧,我们可以使用axios上传文件,最后差一个控制暂停和继续的,可以理解为取消请求和重新发送请求
,只不过在重新发送请求的时候要判断哪些切片是上传过的
。(代码有点长,尽量打上注释)
前端代码
<template>
<input type="file" @change="onChange" ref="img" />
<button @click="onContinue">继续</button>
<button @click="noSuspend">暂停</button>
<div class="progress-frame">
<div class="progress"></div>
</div>
</template>
<script >
import { onMounted, ref } from "vue";
import axios from "axios";
//为下面取消请求做准备
const CancelToken = axios.CancelToken;
export default {
name: "App",
setup() {
let img = ref();
//用来计算进度条 (alreadyUpload/uploadTotal)*100
let alreadyUpload = ref(0);
let uploadTotal = ref(1);
//请求取消的数组
let cancel = ref([]);
let isChange = ref(false);
async function onChange() {
let sum = [];
let file = img.value.files[0];
let size = 1024 * 20; //20kB 切片大小,可以调大点
let fileChunks = [];
let index = 0; //切片序号
isChange.value = true;
// 获取已上传的切片
await axios({
method: "get",
url: "http://127.0.0.1:3000/alreadyFile",
params: {
filename: file.name,
},
//用于取消请求
cancelToken: new CancelToken(function executor(c) {
cancel.value.push(c);
}),
})
.then((response) => {
//返回值是一个已上传切片数组
sum = response.data;
alreadyUpload.value = sum.length;
})
// eslint-disable-next-line @typescript-eslint/no-empty-function
.catch(() => {});
// 对二进制文件进行切片
for (let cur = 0; cur < file.size; cur += size) {
fileChunks.push({
hash: index++,
chunk: file.slice(cur, cur + size),
});
}
uploadTotal.value = fileChunks.length;
// 判断切片是否全部上传完毕
if (sum.length != fileChunks.length) {
for (let i = 0; i < fileChunks.length; i++) {
let item = fileChunks[i];
let formData = new FormData();
formData.append("filename", file.name);
formData.append("hash", String(item.hash));
formData.append("chunk", item.chunk);
//判断某个切片是否上传完毕
if (sum.indexOf(item.hash) == -1) {
// 上传切片
axios({
method: "post",
url: "http://127.0.0.1:3000/upload",
data: formData,
cancelToken: new CancelToken(function executor(c) {
cancel.value.push(c);
}),
})
.then(() => {
alreadyUpload.value = alreadyUpload.value + 1;
})
// eslint-disable-next-line @typescript-eslint/no-empty-function
.catch(() => {});
} else {
continue;
}
}
} else {
console.log("上传已结束");
}
//请求文件链接
await axios({
method: "get",
url: "http://127.0.0.1:3000/fileLink",
params: {
filename: file.name,
},
cancelToken: new CancelToken(function executor(c) {
cancel.value.push(c);
}),
})
.then((response) => {
//返回文件链接
console.log(response.data);
})
// eslint-disable-next-line @typescript-eslint/no-empty-function
.catch(() => {});
}
//点击暂停按钮
const noSuspend = () => {
console.log("请求已取消");
cancel.value.forEach((val) => {
val();
});
};
//点击继续按钮
const onContinue = () => {
if (isChange.value) {
onChange();
}
};
return {
img,
onChange,
alreadyUpload,
uploadTotal,
noSuspend,
onContinue,
};
},
};
</script>
<style scoped>
.progress-frame {
width: 500px;
height: 32px;
background-color: aqua;
}
.progress {
width: v-bind("(alreadyUpload/uploadTotal)*100+'%'");
height: 32px;
background-color: red;
}
</style>
后端思路及代码
这个地方合在一起说,后端我们准备两个文件夹一个用来保存同一个文件的切片,一个用来保存切片合并后的文件(也就是链接返回的文件)。
在全局里配置路径,一个是切片的文件目录,一个是切片合并后的文件目录
const TEMPORARY_FILES = path.join(__dirname, 'temporary')
const STATIC_FILES = path.join(__dirname, 'public')
获取已上传切片
添加一个路由,返回已上传切片的名字数组
Router.get("/alreadyFile", async (ctx) => {
const { filename } = ctx.query;
const already = []
//fs.readdirSync,该方法将返回一个包含“指定目录下所有文件名称”的数组对象
//切片文件目下没有 `${filename}`的文件夹,那就是还没开始上传
if (!fs.existsSync(`${TEMPORARY_FILES}\\${filename}`)) {
ctx.body = already;
}
else {
//有文件夹,那就获取该文件夹下面切片的文件名
fs.readdirSync(`${TEMPORARY_FILES}\\${filename}`).forEach((name) => {
already.push(Number(name))
})
ctx.body = already
}
})
返回的就是 0-18的字符串数组['1','2','3',...,'18']
切片的上传
添加路由
主要功能是创建切片,并放入同一个文件夹,再把文件夹放入切片目录下。也是重点。
Router.post('/upload', async (ctx) => {
let form = new formidable.IncomingForm();
form.parse(ctx.req, (err, value, files) => {
//切片保存在temporary目录下的那个文件夹下
let dir = `${TEMPORARY_FILES}\\${value.filename}`
//第几个切片
let hash = value.hash;
let chunk = files.chunk;
const buffer = fs.readFileSync(chunk.filepath)
try {
// 是否存在这个文件夹
if (!fs.existsSync(dir)) {
//创建
fs.mkdirSync(dir)
}
// 创建切片文件
const ws = fs.createWriteStream(`${dir}\\${hash}`)
// 切片写入
ws.write(buffer)
ws.close()
} catch (error) {
console.error(error)
}
})
ctx.body = "ok"
})
返回文件链接
添加路由
主要功能是看是否存已经合并的文件,如果存在返回文件链接,否则先合并切片再返回文件链接。
Router.get('/fileLink', async (ctx) => {
const { filename } = ctx.query
try {
// 在public目录下是否存在这个文件,有就直接返回链接,ctx.origin获取域名
if (fs.existsSync(`${STATIC_FILES}\\${filename}`)) {
ctx.body = { "url": `${ctx.origin}/${filename}` }
}
} catch (error) {
console.error(error);
}
try {
let len = 0
//fs.readdirSync,该方法将返回一个包含“指定目录下所有文件名称”的数组对象
const bufferList = fs.readdirSync(`${TEMPORARY_FILES}\\${filename}`).map((hash, index) => {
//读取切片数据
const buffer = fs.readFileSync(`${TEMPORARY_FILES}\\${filename}\\${index}`)
len += buffer.length
return buffer
});
//合并切片文件
// 返回一个连接了 list 中所有 Buffer 的新 Buffe
const buffer = Buffer.concat(bufferList, len);
//在public下创建文件
const ws = fs.createWriteStream(`${STATIC_FILES}\\${filename}`)
ws.write(buffer);
ws.close();
} catch (error) {
console.error(error);
}
ctx.body = { "url": `${ctx.origin}/${filename}` }
})
结语
文件的切片上传差不多就是这样,不过其中还是有很多问题,比如两个文件的名字一样怎么办,还有文件很大切太多了,那会同时发很多的请求等。有懂的可以评论说一下,博主也不是很懂。对于文件的切片上传,说简单理解了后感觉也是挺简单的,说难吧也是挺难的,这代码没有人的指点出不来出不来或许还是菜了。
转载自:https://juejin.cn/post/7085399558993379359