-
1、前端页面(不使用elementui自带的上传组件)
![在这里插入图片描述]()
-
2、自定义上传按钮的原理
<input type="file" style="display:none;" id="file" />
<el-button type="primary" @click="uploadFile">选择上传文件</el-button>
<el-button type="warning" @click="confirmUpload">开始上传</el-button>
一个原始的上传文件的我们设置为隐藏状态,另外设置一个美观的按钮来做点击上传按钮,当点击上传文件按钮的时候去触发原始上传文件按钮的点击事件,这时候就可以在电脑端打开选择文件面板了,然后监听原始按钮的change事件,根据选择的文件存储到data中,点击开始上传按钮才发送ajax请求
-
3、前端实现上传的代码片段
import axios from 'axios';
export default {
name: 'App',
data () {
return {
currentFile: null,
}
},
methods: {
uploadFile () {
const that = this;
const fileNode = document.getElementById('file');
fileNode.click();
fileNode.addEventListener('change', function (ev) {
that.currentFile = ev.target.files[0];
})
},
beforeUpload (file) {
if (!file) {
this.$message.warning('请选择上传的文件');
return false;
}
const { type, size } = file;
if (!/(png|gif|jpeg|jpg)$/i.test(type)) {
this.$message.warning("文件合适不正确");
return false;
}
if (size > 5 * 1024 * 1024) {
this.$message.warning('文件过大,请上传小于5MB的文件');
return false;
}
return true;
},
confirmUpload () {
let formData = new FormData();
const currentFile = this.currentFile;
const flag = this.beforeUpload(currentFile);
if (!flag) {
return false;
};
formData.append('file', currentFile, currentFile.name);
formData.append('title', '测试文件');
const headers = {
}
axios.post('/upload1', formData, { headers }).then(res => {
this.currentFile = null;
console.log(res);
})
}
},
components: {
}
}
-
4、使用express后端接收上传文件
const express = require('express')
const bodyParser = require('body-parser')
const multiparty = require('multiparty')
const PORT = 8888
const app = express()
app.use(bodyParser.urlencoded({
extended: false,
limit: '1024mb'
}))
app.use(bodyParser.json());
const uploadDir = `${__dirname}/upload`
function handleMultiparty (req, res, tem = false) {
return new Promise((resolve, reject) => {
const options = {
maxFieldsSize: 200 * 1024 * 1024
}
if (!tem) {
options.uploadDir = uploadDir
}
const form = new multiparty.Form(options)
form.parse(req, function (err, fields, files) {
if (err) {
res.send({
code: 1,
message: JSON.stringify(err)
})
reject(err);
return false;
}
resolve({
fields,
files,
})
})
})
}
app.post('/upload1', async(req, res) => {
const { files, fields} = await handleMultiparty(req, res);
console.log(fields, 'formData中携带的参数');
const file = files.file[0];
res.send({
code: 0,
originalFilename: file.originalFilename,
path: file.path.replace(__dirname, `http://127.0.0.1:${PORT}`)
})
})
app.listen(PORT, () => {
console.log(`服务已经启动,请访问localhost:${PORT}`)
})
-
5、前端完整代码App.vue
-
1、前端中定义将文件转换为bs64字符的方法
fileParse (file, type = "base64") {
return new Promise(resolve => {
let fileRead = new FileReader();
if (type === "base64") {
fileRead.readAsDataURL(file);
} else if (type === "buffer") {
fileRead.readAsArrayBuffer(file);
}
fileRead.onload = (ev) => {
resolve(ev.target.result);
};
});
}
-
2、文件上传
async confirmUpload () {
let formData = new FormData();
const currentFile = this.currentFile;
const flag = this.beforeUpload(currentFile);
if (!flag) {
return false;
};
const result = await this.fileParse(currentFile, 'base64');
const postData = {
chunk: encodeURIComponent(result),
filename: currentFile.name,
title: '测试文件',
};
const headers = {
}
axios.post('/upload2', postData, { headers }).then(res => {
this.currentFile = null;
console.log(res);
})
},
-
3、后端接收前端处理的数据并将转换为Buffer存储到服务器端
const SparkMD5 = require('spark-md5');
const fs = require('fs');
app.use(bodyParser.json({
limit: '50mb'
}));
...
app.post('/upload2', async(req, res) => {
const {chunk, filename, title} = req.body;
console.log(title)
const chunk1 = decodeURIComponent(chunk);
const chunk2 = chunk1.replace(/^data:image\/\w+;base64,/, "");
const chunk3 = Buffer.from(chunk2, 'base64');
const spark = new SparkMD5.ArrayBuffer();
const suffix = /\.([0-9a-zA-Z]+)$/.exec(filename)[1];
spark.append(chunk3);
const path = `${uploadDir}/${spark.end()}.${suffix}`;
fs.writeFileSync(path, chunk3);
res.send({
code: 0,
originalFilename: filename,
path: path.replace(__dirname, `http://127.0.0.1:${PORT}`)
});
});
-
4、前端完整代码App2.vue
-
1、将大文件拆分成很多小文件来上传
...
import SparkMD5 from "spark-md5";
...
async confirmUpload () {
let formData = new FormData();
const currentFile = this.currentFile;
const flag = this.beforeUpload(currentFile);
if (!flag) {
return false;
};
const fileBuffer = await this.fileParse(currentFile, 'buffer');
let spark = new SparkMD5.ArrayBuffer();
spark.append(fileBuffer);
const hash = spark.end();
const suffix = /\.([0-9a-zA-Z]+)$/i.exec(currentFile.name)[1];
let partList = [];
const partSize = currentFile.size / 100;
let cur = 0;
for (let i = 0; i < 100; i++) {
let item = {
chunk: currentFile.slice(cur, cur + partSize),
filename: `${hash}_${i}.${suffix}`,
}
cur += partSize;
partList.push(item);
}
this.partList = partList;
this.hash = hash;
this.sendRequest();
},
-
2、根据文件切片发起ajax请求
async sendRequest () {
let requestList = [];
const headers = {
}
this.partList.forEach((item, index) => {
const fn = () => {
let formData = new FormData();
formData.append('chunk', item.chunk);
formData.append('filename', item.filename);
axios.post('/upload3', formData, { headers }).then(res => {
const data = res.data;
if (data.code == 0) {
this.total += 1;
this.partList.splice(index, 1);
}
})
}
requestList.push(fn);
});
let currentIndex = 0;
const send = async () => {
if (this.abort) return;
if (currentIndex >= requestList.length) {
this.complete();
return;
}
await requestList[currentIndex]();
currentIndex++;
send();
}
send();
},
-
3、全部切片上传完成后通知后端上传完成
complete () {
axios.get('/merge', {
params: {
hash: this.hash,
}
}).then(res => {
console.log(res, '上传完成');
})
},
-
4、模拟暂停与开始
handleBtn () {
if (this.btn) {
this.abort = false;
this.btn = false;
this.sendRequest();
return;
}
this.btn = true;
this.abort = true;
}
-
5、代码见App3.vue
-
1、接收文件切片
app.post('/upload3', async (req, res) => {
const {fields,files} = await handleMultiparty(req, res, true);
const [chunk] = files.chunk;
const [filename] = fields.filename;
const hash = /([0-9a-zA-Z]+)_\d+/.exec(filename)[1];
const dir = `${uploadDir}/${hash}`;
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}
const path = `${dir}/${filename}`;
fs.access(path, async err => {
if (!err) {
res.send({
code: 0,
path: path.replace(__dirname, `http://127.0.0.1:${PORT}`)
})
}
await new Promise(resolve => {
setTimeout(() => {
resolve();
}, 100);
});
const readStream = fs.createReadStream(chunk.path);
const writeStream = fs.createWriteStream(path);
readStream.pipe(writeStream);
readStream.on('end', function() {
fs.unlinkSync(chunk.path);
res.send({
code: 0,
path: path.replace(__dirname, `http://127.0.0.1:${PORT}`)
});
})
})
});
-
2、合并多个切片文件
app.get('/merge',(req, res) => {
const { hash } = req.query;
const path = `${uploadDir}/${hash}`;
const fileList = fs.readdirSync(path);
let suffix = null;
fileList.sort((a, b) => {
const reg = /_(\d+)/;
return reg.exec(a)[1] - reg.exec(b)[1];
}).forEach(item => {
!suffix ? suffix = /\.([0-9a-zA-Z]+)$/.exec(item)[1] : null;
fs.appendFileSync(`${uploadDir}/${hash}.${suffix}`, fs.readFileSync(`${path}/${item}`));
fs.unlinkSync(`${path}/${item}`);
});
fs.rmdirSync(path);
res.send({
code: 0,
path: `http://127.0.0.1:${PORT}/upload/${hash}.${suffix}`
});
})