开发者社区 > 博文 > 前端文件上传的几种交互造轮子
分享
  • 打开微信扫码分享

  • 点击前往QQ分享

  • 点击前往微博分享

  • 点击复制链接

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

  • jd****
  • 2023-06-20
  • IP归属:北京
  • 23240浏览

    背景

    前端文件上传本来是一个常规交互操作,没什么特殊性可言,但是最近在做文件上传,需要实现截图粘贴上传,去找了下有没有什么好用的组件,网上提供的方法有,但是没找完整的组件来支持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
    阅读量
    1129