阅读本文你将学会
- 自定义上传按钮样式
- 文件流方式上传文件
- 文件转换为
bs64
上传 - 大文件断点上传
一、文件上传的两套方案
- 1、基于文件流的方式上传
- 格式:
multipart/form-data
- 数据格式:
form-data
- 文件流信息:
file
- 文件名字:
filename
- 文件流信息:
- 格式:
- 2、客户端将文件转换为
BASE64
上传到服务器端
二、使用文件流的方式上传文件
-
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; }; // 组装数据发送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
后端接收上传文件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
字符的方法// 将文件转换为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 = { // "Content-Type": "multipart/form-data", } // 发送ajax请求 axios.post('/upload2', postData, { headers }).then(res => { this.currentFile = null; console.log(res); }) },
-
3、后端接收前端处理的数据并将转换为
Buffer
存储到服务器端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、将大文件拆分成很多小文件来上传
... // 根据文件内容生成唯一的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
请求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、全部切片上传完成后通知后端上传完成
// 文件上传,需要后端合并文件 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; // 获取上传文件的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、合并多个切片文件
// 大文件上传后 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}` }); })
六、源码地址
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!