最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 关于文件上传的那点事

    正文概述 掘金(水痕001)   2021-03-05   452

    阅读本文你将学会

    • 自定义上传按钮样式
    • 文件流方式上传文件
    • 文件转换为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介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元