写点什么

前端文件上传的几种交互造轮子 | 京东云技术团队

  • 2023-06-26
    北京
  • 本文字数:6206 字

    阅读完需:约 20 分钟

前端文件上传的几种交互造轮子 | 京东云技术团队

背景

前端文件上传本来是一个常规交互操作,没什么特殊性可言,但是最近在做文件上传,需要实现截图粘贴上传,去找了下有没有什么好用的组件,网上提供的方法有,但是没找完整的组件来支持 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 上传的基础上添加了粘贴上传交互, 该组件的实现重在交互方式,各个样式风格通过插槽自定义。


作者:京东物流 刘海鼎

来源:京东云开发者社区

发布于: 刚刚阅读数: 3
用户头像

拥抱技术,与开发者携手创造未来! 2018-11-20 加入

我们将持续为人工智能、大数据、云计算、物联网等相关领域的开发者,提供技术干货、行业技术内容、技术落地实践等文章内容。京东云开发者社区官方网站【https://developer.jdcloud.com/】,欢迎大家来玩

评论

发布
暂无评论
前端文件上传的几种交互造轮子 | 京东云技术团队_前端_京东科技开发者_InfoQ写作社区