likes
comments
collection
share

Vue3 项目, 使用 vue-simple-uploader 实现分块/片上传分块上传: 顾名思义, 就是把一个需要上

作者站长头像
站长
· 阅读数 10

前言

分块上传: 顾名思义, 就是把一个需要上传的大文件, 分成若干份小块, 一块一块的传输给服务器, 然后再合并成一个大文件的过程。

分块上传的优点: 巴拉巴拉巴拉小魔仙。。。

需求背景:

最近做了一个项目需要对接甲方的上传, 我们上传文件的最大才限制了 50M, 却发现超过 30M 的文件上传会超时、失败。

得到对方技术的回复是: 超过30M 的文件上传需要对接他们的分片上传功能, 没听错没写错也没看错,不是30G 也不是 300M, 仅仅是 30M。由于【专网】的限制,沟通成本和对接成本非常高,也不和他们扯那么多,怎么要求就怎么来吧。

我没做过分块上传的功能,网上大概搜了下方案, 后面看到 vue-simple-uploader, 一个基于 simple-uploader.js 的 Vue 上传组件, 感觉能实现我需要的功能, 文档有中文的, 用起来比较方便。

先看完成的效果

官方提供的效果

Vue3 项目, 使用 vue-simple-uploader 实现分块/片上传分块上传: 顾名思义, 就是把一个需要上

我完成的效果

Vue3 项目, 使用 vue-simple-uploader 实现分块/片上传分块上传: 顾名思义, 就是把一个需要上

流程梳理

第一步: 通过上传组件的 @file-added 方法拿到文件信息, 调用接口【文件分块上传_初始化】拿到这次上传任务的{上传ID}

第二步: 将{分块标识, 上传ID}通过调用接口【分块上传】, 开始分块上传

第三步: 分块上传完成后, 上传组件的 @file-success 方法中调用接口【分块上传_合并】,拿到上传完成的{文件ID}, 上传至此完成

动手使用

安装 vue-simple-uploader

需要加上 @next 才是 Vue3 的版本, 否则下载的是 Vue2 的版本

npm install vue-simple-uploader@next --save

main.js

import { createApp } from 'vue';
import App from './App.vue';
import uploader from 'vue-simple-uploader';
import 'vue-simple-uploader/dist/style.css';
// ...

createApp(App).use(uploader).mount('#app');

页面使用, 详细说明查看代码中的注释

<template>
  <uploader
    :options="options"
    :auto-start="false"
    class="uploader-container"
    @file-added="onFileAdded"
    @file-success="onFileSuccess"
  >
    <uploader-unsupport>您的浏览器不支持上传组件</uploader-unsupport>
    <uploader-drop>
      <uploader-btn :attrs="attrs">选择文件上传</uploader-btn>
    </uploader-drop>
    <uploader-list>
      <!-- 我自定义了文件列表, 如果没有这个需求, <uploader-list>内部留空即可 -->
      <template #default="props">
        <!-- 原已上传的文件列表: 将原来已经上传的文件展示出来 -->
        <div v-for="file in uploadFileList" :key="file.fileId" :file="file" class="file-item">
          <div class="file-progress success" style="width: 100%"></div>
          <SimpleUploaderIcon :file="file" />
          <div class="file-title van-ellipsis">{{ file.name }}</div>
          <div class="file-status">已上传</div>
          <van-button size="mini" type="danger" @click="onDelete(uploadFileList, file)">删除文件</van-button>
        </div>
        <!-- 新上传的文件列表: 本次通过组件上传的操作区域 -->
        <uploader-file v-for="file in props.fileList" :key="file.fileId" :file="file" :list="true">
          <template #default="fileProps">
            <div class="file-item">
              <div class="file-progress" :class="{ success: fileProps.status === 'success', error: fileProps.status === 'error' }" :style="{ width: fileProps.progressStyle.progress }"></div>
              <SimpleUploaderIcon :file="fileProps" />
              <div class="file-title van-ellipsis">{{ file.name }}</div>
              <div v-show="fileProps.status === 'success'" class="file-status">上传成功</div>
              <div v-show="fileProps.status === 'error'" class="file-status">上传失败</div>
              <div v-show="fileProps.status === 'uploading'" class="file-status">正在上传({{ fileProps.progressStyle.progress }})</div>
              <van-button v-show="fileProps.status === 'success'" type="danger" @click="onDelete(props.fileList, file)">删除文件</van-button>
              <van-button v-show="fileProps.status === 'error'" type="warning" @click="onCancel(file)">移除文件</van-button>
              <van-button v-show="fileProps.status === 'uploading'" type="danger" @click="onCancel">取消上传</van-button>
            </div>
          </template>
        </uploader-file>
      </template>
    </uploader-list>
  </uploader>
</template>

<script setup>
import SimpleUploaderIcon from './SimpleUploaderIcon.vue'; // 一个小组件: 根据文件类型展示不同的图标, 不重要
import { uploadInit, uploadComplete } from '@/api/attachment';
import { showFailToast, showSuccessToast, showToast } from 'vant';

// 原来已上传过的文件列表
const uploadFileList = ref([
  {
    fileId: '23fdklk23nm23lmnl32kkl',
    extension: 'png',
    name: '20240505.png',
  },
]);

/**
 * 上传组件配置
 */
const options = {
  target: '//localhost:3000/upload', // 目标上传 URL
  // target: '/api/upload', // 目标上传 URL
  headers: { Authorization: '授权token' }, // 接口的定义, 根据实际情况而定
  chunkSize: 5 * 1024 * 1024, // 分块大小
  forceChunkSize: true, // 是否强制所有的块都是小于等于 chunkSize 的值。默认是 false。
  fileParameterName: 'file', // 上传文件时文件的参数名,默认file
  maxChunkRetries: 3, // 最大自动失败重试上传次数
  testChunks: false, // 是否开启服务器分片校验
  // 组件上传会默认携带很多参数, 可以通过query和processParams进行修改
  // 额外的请求参数
  query: (file) => {
    return {
      ...file.params,
    };
  },
  // 处理请求参数, 将参数名字修改成接口需要的
  processParams: (params) => {
    params.f_PartNumber = params.chunkNumber; // 分块标识(1,2,3,4...)
    return params;
  },
};
// 限制上传的文件类型
const attrs = {
  accept: '.png,.jpg,.txt,.pdf,.ppt,.pptx,.doc,docx,.xls,xlsx,.ofd',
};

/**
 * 这个事件一般用作文件校验,如果说返回了 false, 那么这个文件就会被忽略,不会添加到文件上传列表中
 * 我模版中禁止了自动上传(:auto-start="false"), 在这里校验文件和拿到上传{上传ID}后需要调用继续上传
 */
const onFileAdded = async (file) => {
  const maxSize = 50 * 1024 * 1024;
  if (file.size > maxSize) {
    showFailToast({ message: '最大支持上传 50MB 的文件', icon: 'warning-o' });
    file.cancel(); // 取消上传且从文件列表中移除。
    return false;
  }
  // 先设置为暂停状态,才能继续上传
  file.pause();
  // 文件分块上传_初始化,拿到上传{上传ID}
  const { data } = await uploadInit({ fileName: file.name });
  if (data.code === 200 && data.data) {
    file.params = { f_UploadId: data.data.f_UploadId };
    // 从 uploader 实例调用上传方法.upload() 方法会报错, 我不知道为什么, 故而使用 resume()。
    // 继续上传
    file.resume();
  } else {
    file.cancel(); // 拦截器会报错误信息, 我这里直接取消上传且从文件列表中移除。
  }
};

/**
 * 一个文件上传成功事件,
 * 第一个参数 rootFile 就是成功上传的文件所属的根 Uploader.File 对象,它应该包含或者等于成功上传文件;
 * 第二个参数 file 就是当前成功的 Uploader.File 对象本身;
 * 第三个参数就是 d 就是服务端响应内容,永远都是字符串!!!!!!!!
 * 第四个参数 chunk 就是 Uploader.Chunk 实例,它就是该文件的最后一个块实例,如果你想得到请求响应码的话,chunk.xhr.status 就是。
 */
const onFileSuccess = async (rootFile, file, d) => {
  const res = JSON.parse(d); // 转成json
  if (res.code === 200) {
    // 分块上传_合并(分块上传完毕后调用)
    const { data } = await uploadComplete({ f_UploadId: file.params.f_UploadId });
    if (data.code === 200 && data.data) {
      file.fileId = data.data.f_FileId; // 提供给后续删除操作
      showSuccessToast('上传成功, 附件已保存到报销明细中');
    } else {
      onCancel(file);
    }
  } else {
    // 分块上传不走拦截器, 所以这里写报错提示
    showToast({ message: '上传失败, 请重试', icon: 'cross' });
    onCancel(file);
  }
};

const onDelete = (uploadListData, file) => {
  // 确认提示
  // 调用删除接口
  // 成功后删除列表数据
  const findIndex = uploadListData.findIndex((v) => v.fileId === file.fileId);
  if (findIndex !== -1) {
    uploadListData.splice(findIndex, 1);
  }
};

/**
 * 取消本次上传任务
 */
const onCancel = async (file) => {
  // 调用取消上传接口
  // 取消上传且从文件列表中移除。
  file.cancel();
};
</script>

// css 不重要

其他说明

  1. 官方提供有分块上传接口, 按照说明运行即可: github.com/simple-uplo…
  2. 上方模版中 props fileList file fileProps 的数据属性比较多, 不再介绍, 可以直接在页面输出来看
  3. 没有做断点续传的功能, 上传失败的时候我就调用取消上传的接口将本次上传任务取消
  4. 上传异常/失败的时候也调用了任务取消让用户重新上传
  5. 分块上传的时候会提交默认参数, 可通过给 options 配置 queryprocessParams 修改或添加参数, 我添加的参数如划线所示

Vue3 项目, 使用 vue-simple-uploader 实现分块/片上传分块上传: 顾名思义, 就是把一个需要上

(完结)

需求简单, 代码就简单, 复杂的业务看下方的文档吧

参考

  1. 官方文档: github.com/simple-uplo…
  2. 官方文档: github.com/simple-uplo…
  3. 基于vue-simple-uploader封装文件分片上传、秒传及断点续传的全局上传插件
  4. vue-simple-uploader 常见问题整理
转载自:https://juejin.cn/post/7376465327359557670
评论
请登录