# vue_file
**Repository Path**: k_2021/vue_file
## Basic Information
- **Project Name**: vue_file
- **Description**: 前端文件上传的各种方式
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 6
- **Forks**: 2
- **Created**: 2021-03-05
- **Last Updated**: 2023-03-14
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
阅读本文你将学会
* 自定义上传按钮样式
* 文件流方式上传文件
* 文件转换为`bs64`上传
* 大文件断点上传
## 一、文件上传的两套方案
* 1、基于文件流的方式上传
* 格式:`multipart/form-data`
* 数据格式:`form-data`
* 文件流信息:`file`
* 文件名字:`filename`
* 2、客户端将文件转换为`BASE64`上传到服务器端
## 二、使用文件流的方式上传文件
* 1、前端页面(不使用`elementui`自带的上传组件)

* 2、自定义上传按钮的原理
```javascript
选择上传文件
开始上传
```
一个原始的上传文件的我们设置为隐藏状态,另外设置一个美观的按钮来做点击上传按钮,当点击上传文件按钮的时候去触发原始上传文件按钮的点击事件,这时候就可以在电脑端打开选择文件面板了,然后监听原始按钮的`change`事件,根据选择的文件存储到`data`中,点击开始上传按钮才发送`ajax`请求
* 3、前端实现上传的代码片段
```javascript
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;
};
// 组装数据发送axios请求
formData.append('file', currentFile, currentFile.name);
// 根据后端上传文件携带其他的参数来写,不需要参数可以不写
formData.append('title', '测试文件');
// 设置请求头
const headers = {
// "Content-Type": "multipart/form-data",
}
// 发送ajax请求
axios.post('/upload1', formData, { headers }).then(res => {
this.currentFile = null;
console.log(res);
})
}
},
components: {
}
}
```
* 4、使用`express`后端接收上传文件
```javascript
const express = require('express')
const bodyParser = require('body-parser')
const multiparty = require('multiparty')
const PORT = 8888
const app = express()
// 处理post请求体数据
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,
})
})
})
}
// 基于form-data上传数据
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`
## 三、前端上传`base64`到后端
* 1、前端中定义将文件转换为`bs64`字符的方法
```javascript
// 将文件转换为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、文件上传
```javascript
// 开始上传
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 = {
// "Content-Type": "multipart/form-data",
}
// 发送ajax请求
axios.post('/upload2', postData, { headers }).then(res => {
this.currentFile = null;
console.log(res);
})
},
```
* 3、后端接收前端处理的数据并将转换为`Buffer`存储到服务器端
```javascript
const SparkMD5 = require('spark-md5');
const fs = require('fs');
// 处理当文件过大,bs64比较大的时候出现request entity too large错误
app.use(bodyParser.json({
limit: '50mb'
}));
...
// 使用base64上传文件
app.post('/upload2', async(req, res) => {
const {chunk, filename, title} = req.body;
console.log(title)
// 将前端传递过来的bs64转换为buffer
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`
## 四、前端大文件断点上传
大致原理就是将大文件分割成好几个部分(根据固定数量/固定大小方式),每个切片都有自己的数据和各自的名字,每一部分都发起一次`ajax`请求,将切片传递到服务器端。服务器端根据文件创建一个文件夹,用来存放大文件的切片,当客户端将全部切片传递到服务器端的时候,再发起一次请求告知服务器端,前端将数据全部传递完成了,服务器端接收到传递完成的通知的时候,将刚刚文件夹里面的文件全部合并成一个文件,最后将该文件夹删除。简短概括:**大文件-->拆成很多小文件-->发起很多ajax请求发送小文件-->服务器端接收小文件-->组装成大文件**
* 1、将大文件拆分成很多小文件来上传
```javascript
...
// 根据文件内容生成唯一的hash
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];
// 将文件切割为100份来上传
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;
// 发送ajax请求到服务器端
this.sendRequest();
},
```
* 2、根据文件切片发起`ajax`请求
```javascript
async sendRequest () {
// 根据多少切片来创建多少请求
let requestList = [];
// 设置请求头
const headers = {
// "Content-Type": "multipart/form-data",
}
this.partList.forEach((item, index) => {
const fn = () => {
let formData = new FormData();
formData.append('chunk', item.chunk);
formData.append('filename', item.filename);
// 发送ajax请求
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、全部切片上传完成后通知后端上传完成
```javascript
// 文件上传,需要后端合并文件
complete () {
axios.get('/merge', {
params: {
hash: this.hash,
}
}).then(res => {
console.log(res, '上传完成');
})
},
```
* 4、模拟暂停与开始
```javascript
// 暂停和开始
handleBtn () {
if (this.btn) {
//断点续传
this.abort = false;
this.btn = false;
this.sendRequest();
return;
}
//暂停上传
this.btn = true;
this.abort = true;
}
```
* 5、代码见`App3.vue`
## 五、大文件上传后端部分代码
* 1、接收文件切片
```javascript
// 切片上传
app.post('/upload3', async (req, res) => {
const {fields,files} = await handleMultiparty(req, res, true);
const [chunk] = files.chunk;
const [filename] = fields.filename;
// 获取上传文件的hash
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、合并多个切片文件
```javascript
// 大文件上传后
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}`
});
})
```