图片上传

110 阅读7分钟

文件上传

核心逻辑

客户端 -------------->(网络协议http) --------------> 服务器

样式、交互逻辑、运算 消息格式、传输方式 存储、安全、访问控制

上传文件的第一步,也是最重要的一步,调试接口。因为文件上传一定是发起一个http请求,同时把文件捎带过去。

(常用的工具postman、apifox)

上传逻辑界面

分为三部分

  • 选取文件界面

    image-20231002141610594

        <div v-if="flag == 'select'" class="upload-select" @click="handleSelect">
          <YkIcon name="jiahao" color="black"></YkIcon>
          <input
            ref="selectFile"
            id="select"
            type="file"
            @change="selectFileChange"
          />
        </div>
    

    唯一一个重点就是,input file需要让其display:none,点击容器时,自己手动触发input的click事件

    function handleSelect() {
      const select = document.querySelector('#select') as HTMLInputElement
      select.click()
    }
    
  • 上传进度界面

    image-20231002141821308

    上传进度是最复杂的界面。

    • 首先需要实现一个进度条,可以使用css变量的方式来实现。代码如下:

      //通过css变量,用js动态控制伪元素的宽度
      const progressNumber = ref(0)
      const progLiner = ref<HTMLDivElement | null>(null)
      function setProgress(value: number) {
        let v = 0.6 * value + 'px'
        progLiner.value?.style.setProperty('--progress', v)
      }
      
        <div v-else-if="flag == 'progress'" class="upload-progress">
          <div class="progress-number">{{ progressNumber }}%</div>
          <div ref="progLiner" class="progress-liner"></div>
          <div class="cancel-btn" @click="cancelUpload">取消上传</div>
          <div class="mask"></div>
        </div>
    
    //css
        .progress-liner {
          width: 60px;
          height: 6px;
          border-radius: 6px;
          border: 1px solid #ccc;
          position: absolute;
          left: 50%;
          top: 50%;
          transform: translate(-50%, -50%);
          --progress: 20px;
    
          &::after {
            content: '';
            display: block;
            width: var(--progress);
            height: 6px;
            border-radius: 6px;
            background-color: rgb(48, 139, 219);
            position: absolute;
            left: 0;
            top: 0;
          }
    
    • 上传逻辑

      通过input的change事件读取到上传文件files,然后通过FileReader,将其转化为base64,实现图片预览的效果

      let cnacel=null
      function selectFileChange() {
        showArea('progress')
        let files = selectFile.value?.files
        console.log(files)
        if (files?.length == 0) {
          return
        }
        const file = files![0]
        //展示预览图
        const reader = new FileReader()
        //将文件转化为base64
        reader.readAsDataURL(file)
        reader.onload = function (e) {
          if (e.target && e.target.result) {
            previewImg.value!.src = e.target.result as string
          }
        }
        //进行上传
        cancel = uploadFile(
          file,
          (progress) => {
            progressNumber.value = progress
            setProgress(progress)
          },
          (res) => {
            console.log(res)
            showArea('result')
          }
        )
      }
      
      //模拟上传接口
      function uploadFile(
        file: File,
        onProgress: (n: number) => void,
        onFinish: (flag: string, file?: File) => void
      ) {
        //模拟请求
        let p = 0
        nextTick(() => {
          setProgress(0)
        })
        const timerId = setInterval(() => {
          p++
          onProgress(p)
          if (p == 100) {
            onFinish('服务器接收到了', file)
            clearInterval(timerId)
          }
        }, 20)
        //中断请求
        return function () {
          clearInterval(timerId)
        }
      }
      
  • 结果界面

    结果界面就很简单了,只需要判断中断请求函数是否生成,如果生成了就可以调用

    //cancel
    function cancelUpload() {
      //停止上传
      cancel && cancel()
      showArea('select')
      //注意:vue里不需要让对应的上传input.value清空,因为是通过v-if进行切换的
      //每次更改都会导致其重新创建。而纯css,只能通过display:none的方式进行显示隐藏,实际上还是同一个元素
    }
    

拖拽上传

  • 方式①:最简单的一种

    注意

    <input type='file'/>
    

    本身就支持拖拽,所以只需要让其宽高百分百,撑满容器,同时设置其不透明度为0,即可实现。

    这样甚至不需要前面那个点击外层容器,触发input的点击

  • 方式②:drag、drop

    首先,得让容器成为一个拖拽放置的目标容器(不理解的可先看下文 文件拖拽)

function ondragover(e: DragEvent) {
  e.preventDefault()
}
function ondrop(e: DragEvent) {
  e.preventDefault()
  let files = e.dataTransfer!.files
  //注意:直接其放入到input的file中,是无法直接触发onchange事件的,
  //需要自己再次手动触发
  selectFile.value!.files = files
  selectFileChange()
}

文件拖拽

drag、drop

  • dom元素想要拖拽,只需要给dom绑定一个draggrable属性,就可以让这个dom添加上拖拽特性。

  • 如果想要某个元素成为有效的存放容器,存放拖动元素,需要让其作为过程容器时,阻止拖拽默认事件作为结果容器时,也需要阻止默认事件

      const box = document.querySelector('.box')
      box.ondragenter = e => {
        e.preventDefault()
      }
      box.ondragover = e => {
        e.preventDefault()
      }
      box.ondrop = e => {
        e.preventDefault()
        console.log(e.dataTransfer.files)
      }

注意,img默认是可以拖拽的

<style>
  #box {
    width: 100px;
    height: 100px;
    background-color: red;
  }
</style>
​
<div id="box" draggable="true"></div>

拖拽元素的拖拽事件

h5对于可拖拽的元素,提供了下面三个事件用于拖拽监听

  • dragstart: 拖拽开始
  • drag: 拖拽中
  • dragend: 拖拽结束
      const box = document.querySelector('.box')
      box.addEventListener('dragstart', function (e) {
        console.log(e.dataTransfer)
      })
      box.addEventListener('drag', function (e) {})
      box.addEventListener('dragend', function (e) {
        console.log('拖拽结束')
      })

拖拽的事件对象dragEvent有以下几个属性:

image-20231001184921852

常用的几个

属性作用
target为拖拽中的元素的指向
clientX当前鼠标相对于浏览器窗口视口的左上角的值
clientY当前鼠标相对于浏览器窗口视口的左上角的值
dataTransferimage-20231001185215637
files:当前拖拽的文件
当一次拖拽开始发生的时候,会产生一个DataTransfer对象,这个DataTransfer对象于整个拖拽过程触发的事件共享。当这一次的拖拽行为结束之后,这个对象会被销毁

过程元素的拖拽范围事件

当有个可以拖拽的元素之后,元素在拖拽的过程中,会经过一些元素,这里给他取一个形象一点的名称:过程元素

image-20221001164233260.png

可以给这些过程元素绑定三个事件

  • dragenter: 拖拽元素进入该dom时触发
  • dragover: 拖拽元素在该dom范围内触发
  • dragleave: 拖拽元素离开该dom时触发
<div class="process">过程元素</div>

image-20210324124829819.png

const process = document.querySelector('.process')
process.addEventListener('dragenter', function(e){  console.log('拖拽元素进入')})
process.addEventListener('dragover', function(e){  console.log('拖拽元素移动')})
process.addEventListener('dragleave', function(e){  console.log('拖拽元素离开')}) 

需要注意的是,这三个事件中,并不能拿到dragStart中设置的DataTransfer对象数据。(并不是dragStart中的同一个dataTransfer)

      const box = document.querySelector('.box')
      const process = document.querySelector('.process')
      let t = null
      process.addEventListener('dragenter', function (e) {
        console.log(t == e.dataTransfer) //false
      })

      box.addEventListener('dragstart', function (e) {
        t = e.dataTransfer
      })

放置事件

拖拽元素松开的位置在目标元素中,会触发这个函数,在这个函数中可以获取到对应的dataTransfer

当有可以拖拽的元素之后,总是需要有一个用于放置的地方,这个地方取一个形象的名称叫做目标元素。可以给目标元素绑定一个事件

  • drop: 拖拽元素松开的位置在目标元素中,会触发这个函数【在这个函数中可以拿到在dragStart中对应的DataTransfer
  • 注意,想要触发drop事件,那么对应的drop容器需要注册drop事件,同时需要阻止dropover的默认行为,取消其作为过程元素,成为目标元素容器
  • **重点:drop事件中的 e.dataTransfer就是dragstart,drag.dragend中的 e.dataTransfer **(不是过程元素中dragover,dragenter,dargleave中的dataTransfer)

给目标元素绑定 drop 事件。

javascript复制代码const target = document.querySelector('.target')
target.addEventListener('drop', function (e) {  
  console.log('松开')
})

尝试拖拽之后,会发现,这个函数根本没有被触发,这是因为还需要给目标元素绑定一个事件

dragover为拖拽元素进入到目标元素(把它作为过程容器)的时候,触发的函数。然后在这个函数中,执行阻止默认行为,表示允许拖拽元素放置在此元素中

这样,对应的drop就会触发

javascript复制代码target.addEventListener('dragover', function (e) {  
  e.preventDefault()
})

根据第三点:

只有在drop中能拿到dragStart存放的DataTransfer,这个参数中,还有一个files字段,借助这个字段,能实现文件拖拽上传的功能。

//实现文件上传功能

      result.ondragover = e => {
        e.preventDefault()
      }
      result.ondrop = e => {
        e.preventDefault()  //阻止默认打开一个新的标签页
        console.log(e.dataTransfer.files)
      }

总结:

dragstartdrop中可以获取到相同的dataTransfer对象,因此一般移动元素就可以在start回调函数中在dataTransfer放置对应元素id,然后在drop的回调中对元素进行操作,比如将其放置到drop的e.target目标元素中。

    <div id="target" class="box" draggable="true"></div>
    <div class="process"></div>
    <div class="result"></div>
    <script>
      const box = document.querySelector('.box')
      box.ondragstart = function (e) {
        e.dataTransfer.effectAllowed = 'all'
        e.dataTransfer.setData('target', e.target.id)
      }
      const result = document.querySelector('.result')
      result.ondragover = function (e) {
        e.preventDefault()
      }
      result.ondrop = function (e) {
        e.preventDefault()
        let target = e.dataTransfer.getData('target')
        const moveEle = document.getElementById(target)
        const copyNode = moveEle.cloneNode()  //实现复制,拖拽直接appendChild即可
        const container = e.target
        container.appendChild(copyNode)
      }
    </script>

dropenter、dropover、dropend是过程元素触发,e.target也是对应的过程元素。

实现拖拽上传文件效果

只需要让该容器成为放置事件的放置元素即可

 result.ondragover = function (e) {
        e.preventDefault()
      }
      result.ondrop = function (e) {
        e.preventDefault()
        console.log(e.dataTransfer.files)
      }