Vue3 + ElementPlus 前端实现分片上传

目录

1. 什么是分片上传

2. 上传组件模板

3. 上传组件逻辑

3.1 基本思路

3.2 选择上传文件

3.3 校验文件是否合法

3.4 文件加密

3.5 合并文件

3.6 文件切片上传

4. 参考文章

4.1 文章链接

4.2 参考文章提到的注意事项

4.2.1 nginx 上传大小限制

4.2.2 大文件下载


1. 什么是分片上传

将 一个文件 切割为 一系列特定大小的 数据片段,将这些 数据片段 分别上传到服务端;

全部上传完成后,再由服务端将这些 数据片段 合并成为一个完整的资源;

上传过程中,由于外部因素(比如网络波动)导致上传中断,下次上传时会保留该文件的上传进度(断点续传);

2. 上传组件模板

包含三部分:

  • 上传组件,使用 el-upload
  • 进度条组件,使用 el-progress
  • 上传完成状态组件,使用 el-input 自定义
 <el-form-item label="上传附件" prop="uploadFile">
   <el-upload
       v-if="!editForm.inlineAppVersionModel.fileName"
       class="upload-demo"
       drag
       :show-file-list="false"
       :action="APP_MANAGEMENT.uploadFile"
        // 根据项目的接口传递参数
       :data="{
         applicationId: applicationId,
         applicationVersion: applicationVersion,
         bucketName: 'app'
       }"
       // 覆盖默认的http请求
       :http-request="handleFileUpload"
   >
     <el-icon class="el-icon--upload">
       <upload-filled />
     </el-icon>

     <div v-if="!progress" class="el-upload__text">
       Drop file here or <em>click to upload</em>
     </div>

     // 进度条
     <el-progress
        v-else
        :text-inside="true"
        :stroke-width="24"
        :percentage="progress"
        status="success"
       />
   </el-upload>

   // 上传成功之后隐藏上传文件组件
   <div v-else style="display: flex;">
     <el-input v-model="editForm.inlineAppVersionModel.fileName" readonly>
     </el-input>
     <div style="display: flex;">
       <el-button 
           type="primary" 
           :icon="Download" 
           size="small" 
           @click="handleFileDownload" 
          />
       <el-button type="primary" :icon="Delete" size="small" @click="handleFileDel" />
     </div>
   </div>
 </el-form-item>

3. 上传组件逻辑

3.1 基本思路

使用 el-upload 选择文件

选择成功的 回调函数 可以读取文件信息,用于前端校验文件的合法性

前端校验文件合法后,将文件进行切片

通过 请求轮询 把切片传递给后端

3.2 选择上传文件

在这一步,可以获得文件信息

根据文件信息,对文件进行合法性校验

校验成功后,调用文件切片方法

/**
 * @description: 选择上传文件
 * @param file el-upload 返回的参数
 */
const handleFileUpload = async (file: any) => {
  console.log('el-upload 返回的参数 === ', file.file);

  // 如果文件合法,则进行分片上传
  if (await checkMirrorFile(file)) {
    // 文件信息
    const files = file.file;
    // 从 0 开始的切片
    const shardIndex = 0;
    // 调用 文件切片 方法
    uploadFileSilce(files, shardIndex);
  // 文件非法,则进行提示
  } else {
    ElMessage.error('请检查文件是否合法!');
  }
};

3.3 校验文件是否合法

校验文件格式

校验文件大小

调用接口,校验磁盘剩余空间大小

/**
 * @description: 校验文件合法性
 */
const checkMirrorFile = async (file) => {
    // 校验文件格式,支持.zip/.tar
    const fileType = file.file.name.split('.')
    if (fileType[fileType.length - 1] !== 'zip' && fileType[fileType.length - 1] !== 'tar') {
        ElMessage.warning('文件格式错误,仅支持 .zip/.tar')
        return false
    }

    // 校验文件大小
    const fileSize = file.file.size;
    // 文件大小是否超出 2G
    if (fileSize > 2 * 1024 * 1024 * 1024) {
        ElMessage.warning('上传文件大小不能超过 2G')
        return false
    }

    // 调用接口校验文件合法性,比如判断磁盘空间大小是否足够
    const res = await checkMirrorFileApi()
    if (res.code !== 200) {
        ElMessage.warning('暂时无法查看磁盘可用空间,请重试')
        return false
    }
    // 查看磁盘容量大小
    if (res.data.diskDevInfos && res.data.diskDevInfos.length > 0) {
        let saveSize = 0
        res.data.diskDevInfos.forEach(i => {
            // 磁盘空间赋值
            if (i.devName === '/dev/mapper/centos-root') {
                // 返回值为GB,转为字节B
                saveSize = i.free * 1024 * 1024 * 1024
            }
        })
        // 上传的文件大小没有超出磁盘可用空间
        if (fileSize < saveSize) {
            return true
        } else {
            ElMessage.warning('文件大小超出磁盘可用空间容量')
            return false
        }
    } else {
        ElMessage.warning('文件大小超出磁盘可用空间容量')
        return false
    }
}

3.4 文件加密

此处文件上传用 MD5 进行加密,需要安装依赖 spark-md5

npm i spark-md5

/**
 * @description: 文件加密处理
 */
const getMD5 = (file: any): Promise<string> => new Promise((resolve, reject) => {
  const spark = new SparkMD5.ArrayBuffer();
  // 获取文件二进制数据
  const fileReader = new FileReader();
  fileReader.readAsArrayBuffer(file); // file 就是获取到的文件
  // 异步执行函数
  fileReader.addEventListener('load', (e: any) => {
    spark.append(e.target.result);
    const md5: string = spark.end();
    resolve(md5);
  });
  fileReader.addEventListener('error', (e) => {
    reject(e);
  });
});

3.5 合并文件

通过接口合并上传文件,接口需要的参数:

  • 文件名
  • 文件唯一 hash 值

接口合并完成后,前端展示已上传的文件名称

/**
 * @description: 合并文件
 * @param name 文件名
 * @param hash 文件唯一 hash 值
 * @return 命名名称
 */
const composeFile = async (name: string, hash: string) => {
  console.log('开始文件合并');
  const res = await uploadFileMerge({
    applicationId: props.applicationId,
    applicationVersion: props.applicationVersion,
    bucketName: 'app',
    fileName: name,
    hash,
  });
  console.log('后端接口合并文件 ===', res);
  if (res.status === 200 && res.data.code) {
    // 合并成功后,调整已上传的文件名称
    state.editForm.inlineAppVersionModel.fileName = name;
  }
};

3.6 文件切片上传

接口轮询 —— 每次携带一个文件切片给后端;后端接受到切片 并 返回成功状态码后,再进行下一次切片上传

/**
 * @description: 分片函数
 * @param file 文件
 * @param shardIndex 分片数量
 */
const uploadFileSilce = async (file: File, shardIndex: number) => {
      // 文件名
      const { name } = file;
      // 文件大小
      const { size } = file;
      // 分片大小
      const shardSize = 1024 * 1024 * 5;
      // 文件加密
      const hash: string = await getMD5(file);
      // 分片总数
      const shardTotal = Math.ceil(size / shardSize);

      // 如果 当前分片索引 大于 总分片数
      if (shardIndex >= shardTotal) {
        isAlive.value = false;
        progress.value = 100;
        // 合并文件
        composeFile(name, hash);
        return;
      }

      // 文件开始结束的位置
      const start = shardIndex * shardSize;
      const end = Math.min(start + shardSize, size);
      // 开始切割
      const packet = file.slice(start, end);
      
      // 拼接请求参数
      const formData = new FormData();
      formData.append('file', packet);
      formData.append('applicationId', props.applicationId);
      formData.append('applicationVersion', props.applicationVersion);
      formData.append('bucketName', 'app');
      formData.append('hash', hash);
      formData.append('shardSize', shardSize as unknown as string);
      formData.append('seq', shardIndex as unknown as string);

      // 如果 当前分片索引 小于 总分片数
      if (shardIndex < shardTotal) {
        // 进度条保留两位小数展示
        progress.value = Number(((shardIndex / shardTotal) * 100).toFixed(2)) * 1;
        // 调用文件上传接口
        const res = await uploadFile(formData);
        if (res.status !== 200) {
          ElMessage.error('上传失败');
          progress.value = 0;
          return;
        }
        if (res.status === 200 && res.data.code === 200) {
          // 这里为所有切片上传成功后进行的操作
          console.log('上传成功');
        }
        // eslint-disable-next-line no-param-reassign
        shardIndex++;
        // 递归调用 分片函数
        uploadFileSilce(file, shardIndex);
      }
    };

4. 参考文章

4.1 文章链接

前端大文件上传和下载(分片上传)_BreenCL的博客-CSDN博客_前端分片上传前端大文件上传(分片上传)一、问题日常业务中难免出现前端需要向后端传输大型文件的情况,这时单次的请求不能满足传输大文件的需求,就需要用到分片上传业务需求为:用户可以上传小于20G的镜像文件,并进显示当前上传进度前端:vue3.x+Element Plus组件+axios二、解决解决思路简单为前端选择文件后读取到文件的基本信息,包括:文件的大小、文件格式等信息,用于前端校验,校验完成后将文件进行切片并通过请求轮询把切片传递给后端Vue的元素代码如下,主要借助el-upload组件:&lhttps://blog.csdn.net/baoyin0822/article/details/123922628

4.2 参考文章提到的注意事项

4.2.1 nginx 上传大小限制

nginx 默认上传大小为 1MB,若超过 1MB,则需要修改 nginx 配置 解除上传限制

4.2.2 大文件下载

/**
 * @description: 动态创建 a 标签,实现大文件下载
 */
const downloadMirror = async (item) => {
  let t = {
    id: item.id,
  }
  const res = await downloadMirrorApi(t)
  if (res.headers["content-disposition"]) {
    let temp = res.headers["content-disposition"].split(";")[1].split("filename=")[1]
    let fileName = decodeURIComponent(temp)
    // 通过创建a标签实现文件下载
    let link = document.createElement('a')
    link.download = fileName
    link.style.display = 'none'
    link.href = res.data.msg
    document.body.appendChild(link)
    link.click()
    document.body.removeChild(link)
  } else {
    ElMessage({
      message: '该文件不存在',
      type: 'warning',
    })
  }
}

  • 4
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
好的,以下是使用Vue3和Element Plus实现手机端侧边菜单栏的步骤: 1. 安装Vue3和Element Plus 首先,你需要安装Vue3和Element Plus。你可以通过npm或yarn安装它们。具体安装命令如下: ``` npm install vue@next npm install element-plus@next ``` 或者 ``` yarn add vue@next yarn add element-plus@next ``` 2. 创建侧边菜单组件 接下来,你需要创建一个侧边菜单组件。你可以使用Element Plus的Menu组件来实现。以下是一个基本的侧边菜单组件示例: ``` <template> <div class="sidebar-menu"> <el-menu :default-active="$route.path" class="el-menu-vertical-demo" router> <el-menu-item index="/"> <i class="el-icon-menu"></i> <span>首页</span> </el-menu-item> <el-menu-item index="/about"> <i class="el-icon-info"></i> <span>关于我们</span> </el-menu-item> <el-menu-item index="/contact"> <i class="el-icon-phone"></i> <span>联系我们</span> </el-menu-item> </el-menu> </div> </template> <script> import { defineComponent } from 'vue' export default defineComponent({ name: 'SidebarMenu', }) </script> <style> .sidebar-menu { height: 100%; background-color: #f0f2f5; padding: 20px; } </style> ``` 在这个示例中,我们使用了Vue Router来处理路由。我们还使用了Element Plus的Menu组件来创建菜单项。 3. 在App组件中引入侧边菜单组件 接下来,你需要在App组件中引入侧边菜单组件。以下是一个基本的App组件示例: ``` <template> <div class="app"> <el-container> <el-aside width="200px"> <sidebar-menu></sidebar-menu> </el-aside> <el-container> <router-view></router-view> </el-container> </el-container> </div> </template> <script> import { defineComponent } from 'vue' import SidebarMenu from './components/SidebarMenu.vue' export default defineComponent({ name: 'App', components: { SidebarMenu, }, }) </script> <style> .app { height: 100%; } </style> ``` 在这个示例中,我们使用了Element Plus的Container和Aside组件来创建一个侧边菜单栏和一个主内容区域。我们还引入了我们之前创建的侧边菜单组件。 4. 完成 现在,你已经创建好了一个基本的手机端侧边菜单栏。你可以根据你的需求自定义样式和菜单项。 希望这可以帮助到你!
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Lyrelion

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值