前端文件上传的几种交互造轮子

  • 前端文件上传的几种交互造轮子已关闭评论
  • 218 次浏览
  • A+
所属分类:Web前端
摘要

前端文件上传本来是一个常规交互操作,没什么特殊性可言,但是最近在做文件上传,需要实现截图粘贴上传,去找了下有没有什么好用的组件,网上提供的方法有,但是没找完整的组件来支持cv上传,经过了解发现可以用剪贴板功能让自己的cv实现文件上传,于是自己就整合了目前几种文件上传的交互方式,码了一个支持cv的vue3文件上传组件(造个轮子)。


背景

前端文件上传本来是一个常规交互操作,没什么特殊性可言,但是最近在做文件上传,需要实现截图粘贴上传,去找了下有没有什么好用的组件,网上提供的方法有,但是没找完整的组件来支持cv上传,经过了解发现可以用剪贴板功能让自己的cv实现文件上传,于是自己就整合了目前几种文件上传的交互方式,码了一个支持cv的vue3文件上传组件(造个轮子)。

介绍

作为一个完整的组件内容还是挺多的,这里主要介绍下上传交互中一些主要功能,包括上传的几种交互方式,

上传进度的获取,上传类型的限制,默认上传请求和自定义上传请求。

以下代码都是非完整代码,大家用于参考实现过程,可以通过以下代码修改来完成自己想要的交互功能。

几种交互

1,点击选择上传

点击选择是最常见的上传交互,之前原生上传控件,样式修改比较麻烦,为了修改上传样式,我们可以把该控件设置隐藏,用其他元素通过从click交互, 来触发该文件选择控件。在选择文件控件上绑定onchange事件,该控件在change后获取到文件,然后调用上传方法,实现如下:

<div class="uploader-content" @click="handleClick">      <input ref="inputRef"            class="uploader-target"            :name="name" :multiple="multiple"            :accept="accept" type="file"           @change="handleChange" /> </div> <script setup>     const inputRef = shallowRef(null)     const handleClick = () => {         inputRef.value.value = ''         inputRef.value?.click()     }     const handleChange = (e) => {         const files = e.target.files         if (!files) return         // 获取到文件后调用附件上传方法         uploadFiles(files)     } </script> <style  lang='less' scoped>     .uploader-target {         display: none;     } </style> 

2,拖动上传

拖拽文件上传,首先在页面上建立一个拖放区域,在拖放区域上绑定拖放事件,监听拖放事件drop内容中datTransfer中是包含files,如果存在files,获取files然后调用上传附件方法。

拖放区域可以通过事件dragover来检查拖放文件是否进入拖放区域来设置拖放区域悬浮样式,通过dragleave来检查离开拖放区取消悬浮样式。

进行交互提示

实现如下:

<div class="uploader-drag" v-if="props.uploadMode == 'drag'" :class="['dragger', dragover ? 'dragover' : '']" @drop.prevent="onDrop" @dragover.prevent="onDragover"      @dragleave.prevent="dragover = false">      <div class="dragicon-box">          <span>+</span>      </div>   </div> <script setup> const dragover = ref(false) const onDrop = (e) => {         const files = Array.from(e.dataTransfer?.files)         dragover.value = false         uploadFiles(files);     } const onDragover = () => {         dragover.value = true     } </script> 

3,复制上传(复制检测区域设置)

复制上传的交互步骤

•将文件保存到剪贴板: 执行键盘快捷键或者使用鼠标复制

•将鼠标移动到可粘贴区: 判断是否移动到可粘贴区,来确定是否在执行粘贴后上传,否则整个页面都会作为粘贴区,

•执行粘贴操作:执行键盘粘贴快捷键(ctrl+v)

粘贴区绑定paste事件,在触发paste事件前将鼠标移到粘贴区,复制会被检查不在粘贴区,阻止上传操作,实现如下:

<div class="uploader-paste"       v-if="props.uploadMode == 'paste'"       :class="['dragger', dragover ? '' : '']"       @mouseover.stop="clipboardover = true"      @mouseleave.stop="clipboardover = false"      @drop.prevent="onDrop"       @dragover.prevent="onDragover"      @dragleave.prevent="dragover = false"      @paste="pasteFun"  >      <!--默认插槽内容-->      <template v-if="$slots.default == null">          <div class="dragicon-box">              <span>+</span>          </div>      </template>      <slot />  </div> <script setup>   const  clipboardover = ref(false)   const pasteFun = (e) => {       if(!clipboardover.value) return       const clipboardFile = e.clipboardData.files;       uploadFiles(clipboardFile)  } </script> 

上传模式

根据以上三种交互,大家可自由组合上传形式,比如点击和拖拽,拖拽和粘贴组合等等,我这边目前按点击,拖拽,粘贴叠加组合,设置为:

•点击上传,click

•拖拽上传 drag(包括点击上传和拖拽上传)

•粘贴上传 paste (包括点击,拖拽和复制上传)

通过传参 uploadeMode设置 (click, drag, paste)

组件设置:

<div class="uploader-content" @click="handleClick">     <input          ref="inputRef"          class="uploader-target"          :name="name"          :multiple="multiple"          :accept="props.accept"          type="file"         @change="handleChange"          v-if="props.uploadMode != 'click'"     />    <!-- click -->    <div class="uploader-click" v-if="props.uploadMode == 'click'">         <slot />         <input              ref="inputRef"              class="uploader-target"              :name="name"              :multiple="multiple"              :accept="accept"              type="file"             @change="handleChange"              @click.stop />     </div>     <!-- drag -->     <div class="uploader-drag"          v-if="props.uploadMode == 'drag'"          :class="['dragger', dragover ? 'dragover' : '']"          @drop.prevent="onDrop"          @dragover.prevent="onDragover"         @dragleave.prevent="dragover = false">          <template v-if="$slots.default == null">              <div class="dragicon-box">                  <span>+</span>               </div>           </template>           <slot />      </div>      <!-- copy -->      <div class="uploader-paste"            v-if="props.uploadMode == 'paste'"            :class="['dragger', dragover ? '' : '']"            @mouseover.stop="clipboardover = true"           @mouseleave.stop="clipboardover = false"           @drop.prevent="onDrop"            @dragover.prevent="onDragover"           @dragleave.prevent="dragover = false"           @paste="pasteFun"        >           <template v-if="$slots.default == null">               <div class="dragicon-box">                  <span>+</span>                </div>           </template>           <slot />         </div>     </div> </template> 

组件应用

<Upload action="https://jsonplaceholder.typicode.com/posts/" uploadMode="click">     <div>点击上传</div> </Upload> <script lang="ts">     import Upload from '@/components/uploader'; </script> 

文件限制

文件限制包括是否多文件上传限制multiple, 上传数量limit限制,上传类型accept限制,这些设置参考了element-plus上传组件,在其基础上做了简化。实现如下

multiple 和 accept 首先需要在点击控件上绑定,以便于在点击选择上传时就能够过滤对应文件,拖拽上传和粘贴上传,无法通过input[type=file] 组件控制需要在上传方法中判断过滤,(以粘贴上传为例)

组件实现

<div class="uploader-content" @click="handleClick">         <input ref="inputRef"                 class="uploader-target"                 :name="name" :multiple="multiple" :accept="props.accept" type="file"                 @change="handleChange" v-if="props.uploadMode != 'click'" @click.stop />          <div class="uploader-paste" v-if="props.uploadMode == 'paste'" :class="['dragger', dragover ? '' : '']"              @mouseover.stop="clipboardover = true"             @mouseleave.stop="clipboardover = false"             @drop.prevent="onDrop"              @dragover.prevent="onDragover"             @dragleave.prevent="dragover = false"             @paste="pasteFun"             >             <template v-if="$slots.default == null">                 <div class="dragicon-box">                     <span>+</span>                 </div>             </template>             <slot />         </div>     </div> <script setup>     import { shallowRef, ref } from 'vue';     const inputRef = shallowRef(null)     // 上传文件     const uploadFiles = (files) => {         if (files.length === 0) return         const { limit, multiple, accept } = props         // 是否多文件限制,主要用于拖拽和粘贴上传中         if (!multiple) {             files = Array.from(files).slice(0, 1)         }         // 文件数量         if (limit && files.length > limit) {             /*具体大家需要的逻辑可自行定义*/             return         }         // 文件类型限制         if (accept) {             files = filesFiltered(Array.from(files), accept)         }         //在文件符合条件后执行上传方法     }     // 文件过滤     const filesFiltered = (files, accept) => {         return files.filter((file) => {             const { type, name } = file             const extension = name.includes('.') ? `.${name.split('.').pop()}` : ''             const baseType = type.replace(//.*$/, '')             return accept                 .split(',')                 .map((type) => type.trim())                 .filter((type) => type)                 .some((acceptedType) => {                     if (acceptedType.startsWith('.')) {                         return extension === acceptedType                     }                     if (//*$/.test(acceptedType)) {                         return baseType === acceptedType.replace(//*$/, '')                     }                     if (/^[^/]+/[^/]+$/.test(acceptedType)) {                          type === acceptedType                     }                     return false              })         })     }  </script> 

上传进度设置

获取文件上传进度,使用ajax中的progress 事件监听机制,回传数据loaded进度,和ttotal进行计算,获取到计算的百分比通过process插槽线上在界面上。

具体实现如下:

组件实现

文件限制后执行组件上传,默认情况下走内置的上传方法,如果做了自定义,上传进度也需要自己实现(自己实现过程可以参考内置方法中的实现)

// 上传方法调用 ajaxUpload({...props, file}) // 上传方法实现 ajaxUpload = (options) => { const xhr = new XMLHttpRequest()     const action = option.action     console.log(xhr, xhr.upload)     if (xhr.upload) {     // 建立progress监听       xhr.upload.addEventListener('progress', (evt:any) => {         const progressEvt = evt         progressEvt.percent = evt.total > 0 ? (evt.loaded / evt.total) * 100 : 0         // 回传进度数据         option.onProgress(progressEvt)       })     } } 

同样文件上传成功,异常等方法也可以通过监听load并且判断 xhr.status 来实现,

xhr.addEventListener('load', () => {       if (xhr.status < 200 || xhr.status >= 300) {         return option.onError(getError(action, option, xhr))       }       option.onSuccess(getBody(xhr)) }) 

组件使用

•配置获取进度数据回调函数 onProgress

•配置接收回传的进度数据进行赋值

•配置进度条插槽显示进度数据

<Upload action="https://jsonplaceholder.typicode.com/posts/" :limit="3" uploadMode="click" :onProgress="progress">    <div class="button">点击上传</div>    <template v-slot:progress>        <!-自定义的进度条样式,大家可以根据自己的想象,自行设置进度条样式-->        <div class="progress-box">           <div class="progress">              <span class="line" :style="{'width': progressval + '%'}"></span>            </div>            <span class="val">{{progressval}} %</span>         </div>    </template> </Upload> <script setup> import {ref} from 'vue' import Upload from '@/components/uploader'; const progressval = ref(0) const progress = (evt)=>{       progressval.value = evt.percent.toFixed(2) }, // 上传成功 const uploadSucess = (e)=>{       console.log('sucess', e) } // 上传异常 const uploadError= (e)=> {    console.log('sucess', e) } </script> 

自定义上传请求

默认情况下,不需要自定义上传请求,组件内置了上传请求,如果个人有需求可以自定义上传请求,子定义上传请求,是在文件限制流程后,检查是否有自定义请求方法,如果存在就将文件传入自定义请求方法。

组件实现:

// 上传文件 const uploadFiles = (files) => {     if (files.length === 0) return     const { limit, multiple, accept, httpRequest } = props     // 是否多文件限制,主要用于拖拽和粘贴上传中     if (!multiple) {        files = Array.from(files).slice(0, 1)     }     // 文件数量     if (limit && files.length > limit) {        /*具体大家需要的逻辑可自行定义*/        return     }     // 文件类型限制     if (accept) {        files = filesFiltered(Array.from(files), accept)     }     //在文件符合条件后执行上传方法     // 自定义上传方法调用     if(httpRequest) {        return httpRequest(files)     }  } 

组件应用:

注意点: 通过自定义上传方法实现时,在原来组件上的属性action无效

<Upload :limit="3" uploadMode="click" :onProgress="progress" :onSuccess="uploadSucess" :onError="uploadError" :httpRequest="httpRequest">     <div class="button">点击上传</div>     <template v-slot:progress>        <div class="progress-box">           <div class="progress">               <span class="line" :style="{'width': progressval + '%'}"></span>            </div>            <span class="val">{{progressval}} %</span>        </div>     </template>  </Upload> <script setup>    const httpRequest = (files)=> {       // 获取到文件 ,自定已上传方法    } </script> 

总结

通过以上可以实现一个支持多种交互方式的文件上传组件,同时也将element-plus中文件上传的流程做了一个学习,因为该组件的实现过程就是参考了element-plus的实现,在element-plus上传的基础上添加了粘贴上传交互, 该组件的实现重在交互方式,各个样式风格通过插槽自定义。

作者:京东物流 刘海鼎

来源:京东云开发者社区