vue 实现文件上传和下载和vue实现分片上传和断点续传
vue中的文件上传主要分为两步:前台获取到文件和提交到后台获取文件前台获取文件,主要是采用input框来实现<input type="file" ref="clearFile"@change="getFile($event)" multiple="multiplt"class="add-file-right-input"style="margin-left:70px;" accept=".d
·
vue中的文件上传主要分为两步:前台获取到文件和提交到后台
获取文件
前台获取文件,主要是采用input框来实现
<input type="file" ref="clearFile"
@change="getFile($event)" multiple="multiplt"
class="add-file-right-input"
style="margin-left:70px;" accept=".docx,.doc,.pdf">
通过file类型的input框实现文件上传;然后通过设置
multiple="multiplt"实现了多文件上传,并且使用accept
实现了上传文件类型限制;最后通过监听change事件,
前台获取到上传的文件。
getFile(event){
var file = event.target.files;
for(var i = 0;i<file.length;i++){
// 上传类型判断
var imgName = file[i].name;
var idx = imgName.lastIndexOf(".");
if (idx != -1){
var ext = imgName.substr(idx+1).toUpperCase();
ext = ext.toLowerCase( );
if (ext!='pdf' && ext!='doc' && ext!='docx'){
}else{
this.addArr.push(file[i]);
}
}else{
}
}
},
通过change事件中的event.target.files就能获取到上传的文件了,
在上面再次对获取的文件进行了类型限制。
下一步数据提交
获取到文件数据后,就需要将数据提交到后台,
这里可以采用FormData的方式提交。
submitAddFile(){
if(0 == this.addArr.length){
this.$message({
type: 'info',
message: '请选择要上传的文件'
});
return;
}
var formData = new FormData();
formData.append('num', this.addType);
formData.append('linkId',this.addId);
formData.append('rfilename',this.addFileName);
for(var i=0;i<this.addArr.length;i++){
formData.append('fileUpload',this.addArr[i]);
}
let config = {
headers: {
'Content-Type': 'multipart/form-data',
'Authorization': this.token
}
};
this.axios.post(apidate.uploadEnclosure,formData,config)
.then((response) => {
if(response.data.info=="success"){this.$message({
type: 'success',
message: '附件上传成功!'
});
}
})
}
在进行数据提交的时候,有两点需要注意:
formData对象和Content-Type,处理好着两点以后,就和其他的接口一样了。
文件在线打开
在PC端,有很多文件都试采用下载的方式,但是在手机端,更多的是直接在线打开。
如果要实现文件的在线打开,可以借助于a标签的href属性实现
<ul>
<li v-for="(item,index) in noticeList" v-bind:key="index"
class="person-list" @click="notice(index)">
<div class="person-list-name">
<a v-bind:href="[filePrefix+item.uuid_name]">
{{item.file_name}}</a>
</div>
<div class="person-list-time">上传时间:
{{item.create_time}}</div>
</li>
</ul>
因为使用这种方式进行文件打开的时候,需要有完整的路径名称,
但是在从后台获取到列表的时候,通常是相对路径,所以需要进行路径拼接
v-bind:href="[filePrefix+item.uuid_name]"
图片上传和预览
通过在上传文件以后,就可以拿到文件的名称进行展示。
但是如果是用这种方式进行图片上传,展示不再是图片名称了
,而应该是图片展示。
例如,要实现上面这种效果,使用input进行图片上传
<div class="list-img">
<ul>
<li v-for="(item,index) in imgArr" :key="index">
<img :src="item.path" alt="" >
<a @click="todel(index)"></a>
</li>
<li>
<div class="addImg" v-if="imgArr.length<3">
<span class="add">上传图片</span>
<span class="add">(最多上传3张)</span>
<input type="file" @change="getFile($event)" accept=".jpg,.png,.bmp,.gif">
</div>
</li>
</ul>
</div>
getFile(event){
var file = event.target.files;
for(var i = 0;i<file.length;i++){
// 上传类型判断
var imgName = file[i].name;
var idx = imgName.lastIndexOf(".");
if (idx != -1){
var ext = imgName.substr(idx+1).toUpperCase();
ext = ext.toLowerCase( );
if (ext!='jpg' && ext!='png' && ext!='bmp' && ext!='gif'){
}else{
this.imgArr.push(file[i]);
}
}else{
}
}
},
具体的就是,input获取到的图片,是不能立即展示的,两者根本不是一回事
要展示使用input上传的图片,需要使用FileReader。
getFile(event){
var file = event.target.files;
let that = this;
for(var i = 0;i<file.length;i++){
// 上传类型判断
var imgName = file[i].name;
var idx = imgName.lastIndexOf(".");
if (idx != -1){
var ext = imgName.substr(idx+1).toUpperCase();
ext = ext.toLowerCase( );
if (ext!='jpg' && ext!='png' && ext!='bmp' && ext!='gif'){
}else{
that.imgArr.push(file[i]);
}
}else{
}
//展示上传的图片
let reader = new FileReader()
reader.readAsDataURL(file[i])
reader.onload = function(evt) {
img.src= evt.target.result
这个就是转化后的图片地址
赋值给 img.src属性 就可以
}
}
},
废话不多说直接上代码
<template>
<section id="app">
<section>
<input
type="file"
:disabled="status !== Status.wait"
@change="handleFileChange"
/>
<el-button @click="handleUpload" :disabled="uploadDisabled"
>上传</el-button
>
<el-button @click="handleResume" v-if="status === Status.pause"
>恢复</el-button
>
<el-button
v-else
:disabled="status !== Status.uploading || !container.hash"
@click="handlePause"
>暂停</el-button
>
</section>
<section>
<section>计算文件 hash</section>
<el-progress :percentage="hashPercentage"></el-progress>
<section>总进度</section>
<el-progress :percentage="fakeUploadPercentage"></el-progress>
</section>
<el-table :data="data">
<el-table-column
prop="hash"
label="切片hash"
align="center"
></el-table-column>
<el-table-column label="大小(KB)" align="center" width="120">
<template v-slot="{ row }">
{{ row.size | transformByte }}
</template>
</el-table-column>
<el-table-column label="进度" align="center">
<template v-slot="{ row }">
<el-progress
:percentage="row.percentage"
color="#909399"
></el-progress>
</template>
</el-table-column>
</el-table>
</section>
</template>
<script>
const SIZE = 128 * 1024; // 切片大小
const Status = {
wait: "wait",
pause: "pause",
uploading: "uploading",
};
export default {
name: "app",
filters: {
transformByte(val) {
return Number((val / 1024).toFixed(0));
},
},
data: () => ({
Status,
container: {
file: null,
hash: "",
worker: null,
},
hashPercentage: 0,
data: [],
requestList: [],
status: Status.wait,
// 当暂停时会取消 xhr 导致进度条后退
// 为了避免这种情况,需要定义一个假的进度条
fakeUploadPercentage: 0,
}),
computed: {
uploadDisabled() {
return (
!this.container.file ||
[Status.pause, Status.uploading].includes(this.status)
);
},
uploadPercentage() {
if (!this.container.file || !this.data.length) return 0;
const loaded = this.data
.map((item) => item.size * item.percentage)
.reduce((acc, cur) => acc + cur);
return parseInt((loaded / this.container.file.size).toFixed(2));
},
},
watch: {
uploadPercentage(now) {
if (now > this.fakeUploadPercentage) {
this.fakeUploadPercentage = now;
}
},
},
methods: {
handlePause() {
this.status = Status.pause;
this.resetData();
},
resetData() {
this.requestList.forEach((xhr) => xhr?.abort());
this.requestList = [];
if (this.container.worker) {
this.container.worker.onmessage = null;
}
},
async handleResume() {
this.status = Status.uploading;
const { uploadedList } = await this.verifyUpload(
this.container.file.name,
this.container.hash
);
await this.uploadChunks(uploadedList);
},
// xhr
request({
url,
method = "post",
data,
headers = {},
onProgress = (e) => e,
requestList,
}) {
return new Promise((resolve) => {
const xhr = new XMLHttpRequest();
xhr.upload.onprogress = onProgress;
xhr.open(method, url);
Object.keys(headers).forEach((key) =>
xhr.setRequestHeader(key, headers[key])
);
xhr.send(data);
xhr.onload = (e) => {
// 将请求成功的 xhr 从列表中删除
if (requestList) {
const xhrIndex = requestList.findIndex((item) => item === xhr);
requestList.splice(xhrIndex, 1);
}
resolve({
data: e.target.response,
});
};
// 暴露当前 xhr 给外部
requestList?.push(xhr);
});
},
// 生成文件切片
createFileChunk(file, size = SIZE) {
const fileChunkList = [];
let cur = 0;
while (cur < file.size) {
fileChunkList.push({ file: file.slice(cur, cur + size) });
cur += size;
}
return fileChunkList;
},
// 生成文件 hash(web-worker)
calculateHash(fileChunkList) {
return new Promise((resolve) => {
this.container.worker = new Worker("/hash.js");
this.container.worker.postMessage({ fileChunkList });
this.container.worker.onmessage = (e) => {
const { percentage, hash } = e.data;
this.hashPercentage = percentage;
if (hash) {
resolve(hash);
}
};
});
},
handleFileChange(e) {
const [file] = e.target.files;
if (!file) return;
console.log(file);
this.resetData();
Object.assign(this.$data, this.$options.data());
this.container.file = file;
},
async handleUpload() {
if (!this.container.file) return;
this.status = Status.uploading;
const fileChunkList = this.createFileChunk(this.container.file);
this.container.hash = await this.calculateHash(fileChunkList);
const { shouldUpload, uploadedList } = await this.verifyUpload(
this.container.file.name,
this.container.hash
);
if (!shouldUpload) {
this.$message.success("秒传:上传成功");
this.status = Status.wait;
return;
}
this.data = fileChunkList.map(({ file }, index) => ({
fileHash: this.container.hash,
index,
hash: this.container.hash + "-" + index,
chunk: file,
size: file.size,
percentage: uploadedList.includes(this.container.hash + "-" + index)
? 100
: 0,
}));
await this.uploadChunks(uploadedList);
},
// 上传切片,同时过滤已上传的切片
async uploadChunks(uploadedList = []) {
const requestList = this.data
.filter(({ hash }) => !uploadedList.includes(hash))
.map(({ chunk, hash, index }) => {
const formData = new FormData();
formData.append("chunk", chunk);
formData.append("hash", hash);
formData.append("filename", this.container.file.name);
formData.append("fileHash", this.container.hash);
return { formData, index };
})
.map(async ({ formData, index }) =>
this.request({
url: "http://localhost:3000",
data: formData,
onProgress: this.createProgressHandler(this.data[index]),
requestList: this.requestList,
})
);
await Promise.all(requestList);
// 之前上传的切片数量 + 本次上传的切片数量 = 所有切片数量时
// 合并切片
if (uploadedList.length + requestList.length === this.data.length) {
await this.mergeRequest();
}
},
// 通知服务端合并切片
async mergeRequest() {
await this.request({
url: "http://localhost:3000/merge",
headers: {
"content-type": "application/json",
},
data: JSON.stringify({
size: SIZE,
fileHash: this.container.hash,
filename: this.container.file.name,
}),
});
this.$message.success("上传成功");
this.status = Status.wait;
},
// 根据 hash 验证文件是否曾经已经被上传过
// 没有才进行上传
async verifyUpload(filename, fileHash) {
const { data } = await this.request({
url: "http://localhost:3000/verify",
headers: {
"content-type": "application/json",
},
data: JSON.stringify({
filename,
fileHash,
}),
});
return JSON.parse(data);
},
// 用闭包保存每个 chunk 的进度数据
createProgressHandler(item) {
return (e) => {
console.log(item.hash, parseInt(String((e.loaded / e.total) * 100)));
item.percentage = parseInt(String((e.loaded / e.total) * 100));
};
},
},
};
</script>
服务端代码
index.js
const Controller = require("./controller");
const http = require("http");
const server = http.createServer();
const controller = new Controller();
server.on("request", async (req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Headers", "*");
if (req.method === "OPTIONS") {
res.status = 200;
res.end();
return;
}
if (req.url === "/verify") {
await controller.handleVerifyUpload(req, res);
return;
}
if (req.url === "/merge") {
await controller.handleMerge(req, res);
return;
}
if (req.url === "/") {
await controller.handleFormData(req, res);
}
});
server.listen(3000, () => console.log("正在监听 3000 端口"));
controller.js
const multiparty = require("multiparty");
const path = require("path");
const fse = require("fs-extra");
const extractExt = (filename) =>
filename.slice(filename.lastIndexOf("."), filename.length); // 提取后缀名
const UPLOAD_DIR = path.resolve(__dirname, "..", "target"); // 大文件存储目录
const pipeStream = (path, writeStream) =>
new Promise((resolve) => {
const readStream = fse.createReadStream(path);
readStream.on("end", () => {
fse.unlinkSync(path);
resolve();
});
readStream.pipe(writeStream);
});
// 合并切片
const mergeFileChunk = async (filePath, fileHash, size) => {
const chunkDir = path.resolve(UPLOAD_DIR, fileHash);
const chunkPaths = await fse.readdir(chunkDir);
// 根据切片下标进行排序
// 否则直接读取目录的获得的顺序可能会错乱
chunkPaths.sort((a, b) => a.split("-")[1] - b.split("-")[1]);
await Promise.all(
chunkPaths.map((chunkPath, index) =>
pipeStream(
path.resolve(chunkDir, chunkPath),
// 指定位置创建可写流
fse.createWriteStream(filePath, {
start: index * size,
end: (index + 1) * size,
})
)
)
);
fse.rmdirSync(chunkDir); // 合并后删除保存切片的目录
};
const resolvePost = (req) =>
new Promise((resolve) => {
let chunk = "";
req.on("data", (data) => {
chunk += data;
});
req.on("end", () => {
resolve(JSON.parse(chunk));
});
});
// 返回已经上传切片名
const createUploadedList = async (fileHash) =>
fse.existsSync(path.resolve(UPLOAD_DIR, fileHash))
? await fse.readdir(path.resolve(UPLOAD_DIR, fileHash))
: [];
module.exports = class {
// 合并切片
async handleMerge(req, res) {
const data = await resolvePost(req);
const { fileHash, filename, size } = data;
const ext = extractExt(filename);
const filePath = path.resolve(UPLOAD_DIR, `${fileHash}${ext}`);
await mergeFileChunk(filePath, fileHash, size);
res.end(
JSON.stringify({
code: 0,
message: "file merged success",
})
);
}
// 处理切片
async handleFormData(req, res) {
const multipart = new multiparty.Form();
multipart.parse(req, async (err, fields, files) => {
if (err) {
console.error(err);
res.status = 500;
res.end("process file chunk failed");
return;
}
const [chunk] = files.chunk;
const [hash] = fields.hash;
const [fileHash] = fields.fileHash;
const [filename] = fields.filename;
const filePath = path.resolve(
UPLOAD_DIR,
`${fileHash}${extractExt(filename)}`
);
const chunkDir = path.resolve(UPLOAD_DIR, fileHash);
// 文件存在直接返回
if (fse.existsSync(filePath)) {
res.end("file exist");
return;
}
// 切片目录不存在,创建切片目录
if (!fse.existsSync(chunkDir)) {
await fse.mkdirs(chunkDir);
}
// fs-extra 专用方法,类似 fs.rename 并且跨平台
// fs-extra 的 rename 方法 windows 平台会有权限问题
// https://github.com/meteor/meteor/issues/7852#issuecomment-255767835
await fse.move(chunk.path, path.resolve(chunkDir, hash));
res.end("received file chunk");
});
}
// 验证是否已上传/已上传切片下标
async handleVerifyUpload(req, res) {
const data = await resolvePost(req);
const { fileHash, filename } = data;
const ext = extractExt(filename);
const filePath = path.resolve(UPLOAD_DIR, `${fileHash}${ext}`);
if (fse.existsSync(filePath)) {
res.end(
JSON.stringify({
shouldUpload: false,
})
);
} else {
res.end(
JSON.stringify({
shouldUpload: true,
uploadedList: await createUploadedList(fileHash),
})
);
}
}
};
更多推荐
已为社区贡献2条内容
所有评论(0)