vue项目性能优化

用户上传文件的时候,如果文件过大,那么上传可能就会很耗时。而且一旦上传的过程中发生了网络中断,那上传就前功尽弃了。为了提高用户的体验,我们可以选择断点续传,也就是把文件切分成小块后,挨个上传。这样即使中间上传中断,但下次再上传时,只上传缺失的那些部分就可以了。可以看到,断点上传虽然在性能上,会造成网络请求变多的问题,但也极大地提高了用户上传的体验。


一、上传文件有两套方案

1、基于文件流(form-data) element-ui上传组件默认是基于文件流的
2、客户端把文件转化为BASE64,再传给后台

1、基于文件流方案代码

用element-ui提供基于文件流的上传方案:

<template>
  <div id="app">
    <!-- 
      action:存放的是文件上传到服务器的接口地址
    -->
    <el-upload
      drag
      action="/single1"
      :show-file-list="false"
      :on-success="handleSuccess"
      :before-upload="beforeUpload"
    >
      <i class="el-icon-upload"></i>
      <div class="el-upload__text">
        将文件拖到此处,或
        <em>点击上传</em>
      </div>
    </el-upload>

    <!-- IMG -->
    <div class="uploadImg" v-if="img">
      <img :src="img" alt />
    </div>
  </div>
</template>

<script>
/*
 * 默认上传
 *   格式:multipart/form-data
 *   数据格式:form-data
 *      file 文件流信息
 *      filename 文件名字
 *   上传成功后获取服务器返回信息,通知on-success回调函数执行
 * 内部封装了ajax
 */
export default {
  name: "App",
  data() {
    return {
      img: null,
    };
  },
  methods: {
    handleSuccess(result) {
      if (result.code == 0) {
        this.img = result.path;
      }
    },
    beforeUpload(file) {
      // 格式校验
      let { type, size } = file;

      if (!/(png|gif|jpeg|jpg)/i.test(type)) {
        this.$message("文件合适不正确~~");
        return false;
      }

      if (size > 200 * 1024 * 1024) {
        this.$message("文件过大,请上传小于200MB的文件~~");
        return false;
      }

      return true;
    },
  },
};
</script>

2、基于BASE64的上传方案

<template>
  <div id="app">
    <el-upload drag action :auto-upload="false" :show-file-list="false" :on-change="changeFile">
      <i class="el-icon-upload"></i>
      <div class="el-upload__text">
        将文件拖到此处,或
        <em>点击上传</em>
      </div>
    </el-upload>

    <!-- IMG -->
    <div class="uploadImg" v-show="img">
      <img :src="img" alt />
    </div>
  </div>
</template>

<script>
import { fileParse } from "./assets/utils";
import axios from "axios";
import qs from "qs";

export default {
  name: "App",
  data() {
    return {
      img: null,
    };
  },
  methods: {
    async changeFile(file) {
      if (!file) return;
      file = file.raw;
      // 继续做格式校验
      /*
       * 把上传的文件先进行解析(FileReader)
       * 把其转换base64编码格式
       * 自己基于axios把信息传递给服务器
       * ...
       */
      let result = await fileParse(file, "base64");
      result = await axios.post(
        "/single2",
        qs.stringify({
          chunk: encodeURIComponent(result),
          filename: file.name,
        }),
        {
          headers: {
            "Content-Type": "application/x-www-form-urlencoded",
          },
        }
      );
      result = result.data;
      if (result.code == 0) {
        this.img = result.path;
      }
    },
  },
};
</script>

二、如何实现断点续传?

1、拿到文件,对文件进行切片,有两个方式,一种时固定数量,另一种时固定大小。
2、用SparkMD5库对每一个分片进行命名(服务器接口后,会对相同hash的文件进行合并)
3、发请求传文件,可以有串行和并行两种方式。这里使用串行,一个一个发,方便点击暂停上传的时候取消发送。
在这里插入图片描述

4、可以拿一个数组保存待发的文件,上传成功的文件可以从数组里面删除。这样,当再次点击继续发送的时候,就不需要重复发送了。
在这里插入图片描述
5、等全部文件发完了,再发一个请求告诉服务器文件发完了

dom部分:

<div id="app">
    <el-upload drag action :auto-upload="false" :show-file-list="false" :on-change="changeFile">
      <i class="el-icon-upload"></i>
      <div class="el-upload__text">
        将文件拖到此处,或
        <em>点击上传</em>
      </div>
    </el-upload>

    <!-- PROGRESS -->
    <div class="progress">
      <span>上传进度:{{total|totalText}}%</span>
      <el-link type="primary" v-if="total>0 && total<100" @click="handleBtn">{{btn|btnText}}</el-link>
    </div>

    <!-- VIDEO -->
    <div class="uploadImg" v-if="video">
      <video :src="video" controls />
    </div>
  </div>

文件上传时调用的方法:

   async changeFile(file) {
        if (!file) return;
        console.log('file', file)
        file = file.raw;
        // 解析成buffer数据
        // 切片处理,把文件切成多个部分(固定数量/固定大小)
        // 每一个切片都有自己的部分数据和自己的名字
        let buffer = await fileParse(file, "buffer"),
        spark = new SparkMD5.ArrayBuffer(),
        suffix;
        spark.append(buffer);
        let hash = spark.end();
        suffix = /\.([0-9a-zA-Z]+)$/i.exec(file.name)[1];
        console.log('suffix', suffix)
        // 创建100个切片
        let partList = [],
        partsize = file.size / 100,
        cur = 0;
        for(let i = 0; i < 100; i++) {
            let item = {
                chunk: file.slice(cur, cur + partsize),
                filename: `${hash}_${i}.${suffix}`,       
            };
            cur += partsize;
            partList.push(item)
        }
        this.partList = partList;
        this.hash = hash;
        this.sendRequest();
    },

发送请求的方法:

async sendRequest() {
        // 根据100个切片创建100个请求(集合)
        let requestList = [];
        this.partList.forEach((item, index) => {
            // 每一个函数都是发送一个切片的请求
            let fn = () => {
                let formData = new FormData();
                formData.append("chunk", item.chunk);
                formData.append("filename", item.filename);
                return axios.post('/single3', formData, {
                    headers: { "Content-Type": "multipart/form-data" },
                }).then(res => {
                    res = res.data;
                    if (res.code == 0) {
                        this.total += 1;
                        this.partList.splice(index, 1);
                    }
                })
            }
            requestList.push(fn);
        })
        let complete = async () => {
            let result = await axios.get("/merge", {
            params: {
                hash: this.hash,
            },
            });
            result = result.data;
            if (result.code == 0) {
            this.video = result.path;
            }
        };
        let i = 0;
        let send = async() => {
            // 都发完了
            if (this.abort) return;
            if (i >= requestList.length) {
                complete();
                return;
            }
            await requestList[i]();
            i++;
            send();
        }
        send();
    },

处理切换按钮的逻辑:

	handleBtn() {
        if (this.btn) {
            // 断点续传
            this.btn = false;
            this.abort = false;
            this.sendRequest();
            return;
        }
        // 暂停上传
        this.btn = true;
        this.abort = true;
    }

总结

虽然文件断点续传的功能要浪费额外的性能,造成网络请求变多的问题,但是这提高了用户体验。
视频资源
项目代码

Logo

前往低代码交流专区

更多推荐