平时项目开发中,获取本地图片并压缩上传是一个很常见的需求,最典型的就是修改用户头像功能,今天就来封装一个可以到处通过的组件。

首先分析需求,要达到什么效果呢?

  • 点击后打开文件选择器,选择文件
  • 对获取的图片文件压缩
  • 前端能够预览获取到的图片
  • 将压缩后的图片上传给服务器

分析下来,大致就是上面的几点,那么再深入分析一下,这个图片预览上传的功能可能很多地方都需要用,并不只限于修改头像这一个地方或者这一个项目,能否将这个组件封装下,让它能够适应各种情况且不需要改动,也就是仅仅只作为一个功能组件,而不带有任何 UI 性质。
所以,上面的需求其实应该压缩下,

  • 获取图片并压缩
  • 预览图片并上传

获取图片并压缩,这个功能比较独立且可以不涉及到 UI,应该把它单独封装成一个公共的组件以达到通过的目的。
而预览并上传,这个就涉及到 Ui 了,并且要上传的话需要调用服务器接口,具体到不同的业务场景和项目,这接口肯定是不一样的,所以这二个点并不适合封装成通用的组件,需要根据不同的业务场景和项目进行相应的变动。

分析到这里思路就清晰了,下面开始撸码,首先来看第一个,获取并压缩图片。不啰嗦,直接上代码,必要的地方都有注释,一看就明白

<!--拍照,选择图片组件-->

<template>
  <div @click.stop = "addPic" ref = "upload">
    <input type = "file"
           accept = "image/jpg,image/png,image/jpeg,image/gif"
           @change = "onFileChange"
           style = "display: none">
  </div>
</template>

<script>
  import { Indicator } from 'mint-ui'//加载状态动画
  import lrz from 'lrz'//压缩库,需要通过 npm 安装下

  export default {
    name: 'UploadImg',
    data () {
      return {
        imgUrl: '',
      }
    },
    methods: {
      addPic () {
        let els = this.$refs.upload.querySelectorAll('input[type=file]')
        els[0].click()
        return false
      },
      onFileChange (e) {
        //获取图片文件
        let files = e.target.files || e.dataTransfer.files
        if (!files.length) return
        this.createImage(files, e)
      },
      createImage (file, e) {
        this.imgUrl = ''
        Indicator.open()

        lrz(file[0], {width: 480}).then((rst) => {
          this.imgUrl = rst.base64
          //在将图片压缩成功后,将信息传出去
          this.$emit('imgUrl', this.imgUrl)
        }).always(() => {
          //每次压缩完之后要将input内容清空,不然会出现传出去的内容不对的bug
          e.target.value = null
          this.imgUrl = ''
          Indicator.close()
        })
      },
    },
  }
</script>

通过代码可以看到,这个组件的内容很简单,就一个隐藏的 input ,用来获取图片文件,获取到文件后进行压缩,然后将压缩后的数据 emit 发送出去。
这里有一点要注意:压缩之后的图片是 base64 格式而不是 file 类型,base64 实际上就是一段字符串,有很多站长工具都可以做转换,这里不多说,不知道的自己查查。所以最后上传给服务器的是一段 base64 的字符串而不是 file 文件,这点要跟后端沟通好。如果后端不配合,一定要你传 file 文件,那就只有再另找办法转文件或者直接拿文件上传了,,,

在将压缩后的数据发送出去后,我们就需要对数据进行处理了,也就是预览并上传。这二个功能因为根据业务和项目的不同肯定会有变化,所以就没法作为一个公共的通用组件来设计,
那么可以在一个父组件里面写这二个功能,把获取并压缩图片的组件作为它的子组件。在父组件中触发子组件的功能,父组件中拿到子组件传过来的图片数据显示出来并上传给服务器就行了,代码如下:

<template>
  <div>
    <div class = "container">
      <div class = "flex bb avatarLine" @click.stop = "changeAvatar">
        <p>头像</p>
        <img v-if = "newAvatar" :src = "newAvatar" alt = "">
        <img v-else src = "../../assets/img/img-avatar-default.png" alt = "">
        <UploadImg v-on:imgUrl = "handleImg" ref = "upload" style = "display: none"></UploadImg>
      </div>
  </div>
</template>

<script>
  import UploadImg from '@/components/UploadImg'

  export default {
    name: 'setting',
    components: {
      UploadImg,
    },
    data () {
      return {
        newAvatar: '',
      }
    },
    methods: {
      changeAvatar () {
        //触发子组件获取图片的事件
        this.$refs.upload.addPic()
      },
      handleImg (imgUrl) {
        //拿到子组件中传过来的数据并上传
        this.newAvatar = imgUrl
        this.api.post(this, '/user/avatar', {avatar: this.newAvatar}).then((data) => {
          if (data && data.user) {
            this.$store.commit('updateUserInfo', data.user)
          }
        })
      },
  }
</script>

UploadImg 组件可以存在多个,当有多个 UploadImg 组件的时候,它们的 ref ,触发和接收事件的方法名需要是不同的名字,这点需要注意,如果名字都是一样的话会发生数据错乱的 bug

Logo

前往低代码交流专区

更多推荐