Debug-022-el-upload照片上传-整体实现回顾

前情概要:

        最近业务里通过el-upload实现一个上传图片的功能,有一些小小的心得分享给各位。一方面是review一下,毕竟实现了很多细小的功能,还有这么多属性、方法(钩子)和碰到的问题,感觉小有成就。另一方面,这个上传图片的功能比较少用,但是确实多数系统都有可能去做的一项功能,时间一久,很多钩子和属性非常容易遗忘,所以这里备份可以便于将来二次利用,温习一下,很多地方又可以直接去CV,也算是为以后提升效率考虑吧(虽然现在把这些在回顾一遍写下来真的超级麻烦,但还是应该有一些匠心精神吧,共勉共勉)。

先分享效果吧:(由于涉及到业务,这里还是以截图的方式吧,先不使用视频)

图1:初始效果

图2:有图片的效果,可以放大、删除、替换

   

html结构(vue3 +elementUI-plus写法)

          <el-upload
            ref="imgREF"    //绑定ref,准备拿实例
            v-model:file-list="imgList" //图片的
            action="#"    //详见下面注释
            accept=".jpg, .jpeg, .png" //接受上传的图片类型
            :limit="1"                //限制上传图片个数
            list-type="picture-card"  
            :auto-upload="false"
            :on-change="handleChangeImg"    //图片改变时的钩子  这里用于图片上传
            :on-exceed="handleExceed"       //图片超出个数限制时的钩子函数
          >
            <el-icon><plus /></el-icon>
            <template #file="{ file }">   //插槽  file是这个图片的信息
              <div style="width: 100%;">
                <el-image class="el-upload-list__item-thumbnail" :src="file.url" fit="contain" />
                <div class="el-upload-list__item-actions flex">
                  <div
                    class="el-upload-list__item-preview"
                    @click="handleImgEnlarge(file)"
                  >
                    <!-- <el-icon><zoom-in /></el-icon> -->
                    <!-- 放大 -->
                    <svg-icon style="width: 22px;height: 22px;" name="device_img_enlarge" />
                  </div>
                  <div
                    class="el-upload-list__item-preview"
                    @click="handleImgDelete(file)"
                  >
                    <!-- 删除 -->
                    <svg-icon style="width: 22px;height: 22px;" name="device_img_delete" />
                  </div>
                  <div
                    class="el-upload-list__item-preview"
                    @click="handleImgReplace(file)"
                  >
                    <!-- 替换 -->
                    <svg-icon style="width: 22px;height: 22px;" name="device_img_replace" />
                  </div>
                </div>
              </div>
            </template>
          </el-upload>


// //elementUI中的upload默认的提交行为是通过actin属性中输入的url链接,提交到指定的服务器上,
但是这种url提交图片的方式,在实际的项目环境中往往是不可取的,我们的服务器会拦截所有的请求,
进行权限控制,密钥检查,请求头分析等安全行为控制。写在这里的url无法实现定义请求参数之类的,
就更不能进行后面的操作了。如果你说放行这个请求不就行了嘛,确实,放行不做任何操作是可以,
真被人攻击了,到时候定责的时候就麻烦了。所以最恰当的方式,就是自定义图片的上传行为。

一、上传图片

代码如下:

const imgREF = ref<UploadInstance>()   //el-upload的实例
const showViewer = ref(false)    // 放大图片<el-image-viewer>的显示与否
const imgList = ref<any>([]) // 图片列表,用于存放用户上传的图片
const urlList = ref<UploadUserFile[]>([]) // 放大查看列表

// 上传图片-文件状态改变   用于判断图片类型和大小
function handleChangeImg(uploadFile:any) {
  console.log('handleChangeImg', uploadFile, imgList.value)
  form.imgRaw = uploadFile.raw
  const uuid = uuidv4()   //给上传到OSS上的文件加后缀id区分相同文件
  // 这里需要encodeURIComponent,不然中文图片在详情回显会报错,可能是加密原因导致
  if (uploadFile.raw!.type === 'image/png')form.imageName = `${encodeURIComponent(uploadFile.name.slice(0, -4))}-${uuid}${uploadFile.name.slice(-4)}`
  if (uploadFile.raw!.type === 'image/jpeg')form.imageName = `${encodeURIComponent(uploadFile.name.slice(0, -5))}-${uuid}${uploadFile.name.slice(-5)}`
  const FILE_TYPES = [
    'image/png',
    'image/jpeg',
  ]
  const rawFile = uploadFile.raw!
  if (!FILE_TYPES.includes(rawFile.type)) {
    imgREF.value!.clearFiles()
    return $message.error('请上传jpg、png、jepg格式文件')
  } else if (rawFile.size / 1024 / 1024 > 5) {
    imgREF.value!.clearFiles()
    return $message.error('图片不超过5MB')
  } else {
    imgList.value = [uploadFile]
  }

  console.log('finally', imgList.value)
}

(1) 打印uploadFile

        这一点对我来说很重要,我需要根据打印中得到的文件信息去判断文件的大小,类型是否正确,对于新手来讲还是十分必要的,在你没把握的情况下。

图3 

上传图片打印的uploadFile里有全部的文件信息。 真正的文件是其中的raw,但是这里我这里整体存下来,因为之后会使用uploadFile的url字段

(2)判断类型和大小

根据uploadFile中的raw中的type字段和size字段就可以判断大小。

图片类型:需求上需要我们上传什么类型,我们就先准备好一个变量存储在一个数组,上传文件的时候拿type中的类型和数组中我们允许的类型去比对即可,不对就抛错。

图片大小:这里大小单位存的应该是字节,需求上需要限制图片大小不超过5MB,所以先除以1024换算成KB,再除以1024换算成MB,小于5MB即可,否则抛错。

需要注意的是:这里是需要调用upload实例上的方法imgREF.value!.clearFiles()去清理掉上传的文件,因为这个上传文件的动作不会被阻止,就算没有使用:on-change="handleChangeImg"这个方法,文件同样会被上传。哪怕你校验了文件的格式,需要代码手动清理掉,打印一下imgREF.value实例看一下:直接调用即可。

图4

满足所有校验之后,最后把uploadFile放入imgList数组中。这一步就完成了

二、放大图片

(1)页面部分el-upload中使用插槽

        这块页面布局可以参考element-plus官网上的关于el-upload的自定义缩略图这一部分案例。饿了么基于 Vue 2.x 和Vue3.x版本的写法会有一点不同,但我觉的对诸位问题不大。稍作调整即可。
       

(2)逻辑代码

  <el-image-viewer
    v-if="showViewer"
    :url-list="urlList"
    @close="closeViewer"
  />



// 放大查看图片
function handleImgEnlarge(file:any) {
  urlList.value = [imgList.value[0].url]
  console.log('handlePictureCardPreview', imgList.value)
  showViewer.value = true
}



function closeViewer() {
  console.log('closeViewer')
  showViewer.value = false
}

 如果只是实现el-image的图片放大预览,那么在el-image的话是有这个属性在的,即:preview-src-list,只要配了这个属性就可以开启图片预览功能,它的值是图片链接的数组。但是这里不行,因为放大的逻辑是在“放大镜”按钮上。好在百度一下找到了另一种实现方式,就是使用

 <el-image-viewer>标签,但是在官网上没有实例去给我们参考,藏得位置也比较深。

图5 

        百度参考了一下前人的经验和代码,写了一个确实是没问题的。几个比较重要的特性就是预览显示的时机,关闭还有数据绑定,这几个文档上就有。

        饿了么做的预览模式还是很不错的,效果如下。支持放大,缩小,一比一还原,旋转,还有一点居然可以实现拖拽移动。这是我优先考虑使用el-image(el-image-viewer)的原因。而在饿了么官网上el-upload那一节“自定义缩略图”示例中,是使用el-dialog弹窗完成的放大功能,显然没有image的预览模式好。

图6

下图是官网的放大方式:用的是dialog,显然没有上一种好使。

图7

三、删除图片

// 删除图片
function handleImgDelete(file:any) {
  urlList.value = []
  imgREF.value!.clearFiles()
}

       

 图8

        这里通过ref拿到el-upload的实例之后使用上面的clearFiles()方法即可。这里还需要注意的是清除放大展示图片的的urlList数组,因为我们需求上是只能上传一个图片,这里不删除,那么放大功能会保留之前的图片。

四、替换图片

        这个功能我印象里是花了一点时间的。建议各位先看“十二、钩子函数的使用”。

先上代码:

// 当超出限制时,执行的钩子函数  这里用以替换照片的功能
function handleExceed(files:any) {
  console.log('handleExceed', files)
  const FILE_TYPES = [
    'image/png',
    'image/jpeg',
  ]
  const rawFile = files[0]!
  // 替换之前需要校验照片格式,但是这里先不删除原照片
  if (!FILE_TYPES.includes(rawFile.type)) {
    // imgREF.value!.clearFiles()
    return $message.error('请上传jpg、png、jepg格式文件')
  } else if (rawFile.size / 1024 / 1024 > 5) {
    // imgREF.value!.clearFiles()
    return $message.error('图片不超过5MB')
  } else {
    // 满足前两个条件,则删除原照片,上传新照片
    imgREF.value!.clearFiles()
    const file = files[0] as UploadRawFile
    file.uid = genFileId()
    imgREF.value!.handleStart(file)
    urlList.value = [rawFile]
    console.log('finallyhandleExceed', imgList.value)
  }
}

// 替换图片
function handleImgReplace(file:any) {
  onUploadImgLocal(file)
}

// 手动调取图片本地上传入口
function onUploadImgLocal(row:any) {
  console.log('importBillExcel', row)
  const input = document.createElement('input')
  input.type = 'file'
  input.accept = '.jpeg, .png,  .jpg' // 限制选择的文件类型为 .jpg, .png,  .jpg
  input.style.display = 'none'
  document.body.appendChild(input)
  input.click()
  input.onchange = (e:any) => {
    const file = e.target.files[0] // 获取文件对象
    console.log('eeeeee', e, file)
    handleExceed([file])
  }
}

       上面就是执行替换的方法。其实是不需要onUploadImgLocal唤起上传文件的功能的,但是需求上限制上传图片为1张,上传完毕的时候,必须要把默认上传图片的那个样式隐藏掉,就是最上方初始效果的那种图。此时,页面没有入口再次唤起上传文件,所以有了onUploadImgLocal这个方法。

        当执行完上传文件的动作后,代码上我再次执行handleExceed方法(注意这里的传参格式),这里的file是第二次需要上传的文件,在handleExceed中也同样需要对文件二次校验一遍。满足所有打条件后,先删掉上一张图片,再去执行handleStart(官网上你可以看看el-upload的示例:“覆盖前一个文件”)这个动作,这个也是官网提供的以方法的形式手动上传文件(图片),将第二张图片上传给el-upload。至此就会完成替换功能。

五、上传图片至OSS

阿里云对象存储服务(Object Storage Service,简称OSS)为您提供基于网络的数据 存取服务

        我们项目中的方式是将图片或者文件上传到OSS上,获取到链接之后,传递给后端,详情页回显也是通过调用oss方法根据URL路径去回显的。

当然前提是已经在阿里OSS上配好了项目路径(这个我们这是交给后端),如果你想知道具体方式,可以参考下面两篇博文:

手把手教你使用阿里云对象存储 OSS服务上传文件-保姆级!_阿里云oss上传-CSDN博客

前端、后端上传文件到OSS,简明记录_前端上传文件到oss-CSDN博客

如果你们已经有了一个oss存放目录,那么我们可以参考oss上的示例方法,我是参考这里的示例去上传文件的

如何使用Browser.js SDK简单上传文件_对象存储(OSS)-阿里云帮助中心

// 首先要npm install ali-oss
//初始化
const client = new OSS({
        // yourRegion填写Bucket所在地域。以华东1(杭州)为例,yourRegion填写为oss-cn-hangzhou。
        region: "yourRegion",
        // 从STS服务获取的临时访问密钥(AccessKey ID和AccessKey Secret)。
        accessKeyId: "yourAccessKeyId",
        accessKeySecret: "yourAccessKeySecret",
        // 从STS服务获取的安全令牌(SecurityToken)。
        stsToken: "yourSecurityToken",
        // 填写Bucket名称。
        bucket: "examplebucket",
});

//accessKeyId和accessKeySecret写你们项目中申请好的,bucket也需要使用OSS中我们自定义的项目地址名称


// OSS
const result = await client.put(`/device/install/imge_${form.imageName}`, form.imgRaw)
console.log('oss', result)

图9

打印成功上传oss后返回的result。 这里面的URL字段是关键,需要存储至后端,这是回显图片的关键,由于我之前保存给后端使用的是name,会有一些问题,比如需要处理中文名称的问题

六、详情回显图片

        一般新增过后,系统会有“详情页”去回显上传过的图片信息。这里我原来也是调用oss上提供的方法去回显。

Browser.js SDK图片处理_对象存储(OSS)-阿里云帮助中心

中最后我提到过。如果存给后端的是返回响应中的url,在回显时可以直接使用这个url链接,将其传给子组件中的el-image组件中即可回显照片。

七、上传图片大小 类型个数限制

        这个参考“一、上传图片”中的步骤(2),其实只要你能拿到文件的file,就可以拿到大小,类型,就可以做这些限制。个数限制是用的el-upload中的limit属性。

八、上传图片的默认入口隐藏

一个css样式即可实现:关键是通过F12找到图1的位置,然后把它disable:none掉。

并且这里需要配合监听,考虑一下隐藏的时机:上传过一张照片就隐藏,没有照片才显示。

// 这里监听imgList图片的个数,看是否隐藏上传图片的默认入口
watchEffect(() => {
  const elements = document.querySelectorAll('.el-upload--picture-card')
  if (imgList.value.length) {
    // 遍历所有匹配的元素并设置 display 为 none
    elements.forEach((element) => {
      element.style.display = 'none'
    })
  } else {
    elements.forEach((element) => {
      element.style.display = ''
    })
  }
})

九、操作el-upload的实例

el-upload实例上的方法倒不是很多,我们用到了其中两个。不像el-table和el-tree可能有十几个。

十一、中文名称图片时使用encodeURIComponent()方法对中文名称编码进行处理

这里讲两个bug:

(1)在最初实现上传图片的功能时,我取oss响应的name,如果有中文名称的图片时,我发现回显的时候URL中会出现乱码,怀疑是公司加密的原因,因此使用了encodeURIComponent()去转义。但是如果直接使用oss响应的URL字段,名称会被自动转义,不必使用此方法。

(2)每次在上传照片的时候(on-change中)需要给照片的name添加唯一标识,这里使用uuid去拼接。不然如果两次上传oss的图片不一样,但是名称类型一致,后一项会覆盖前一项,导致第一次的照片也变成第二次上传的照片了。

  const uuid = uuidv4()
  // 存一份图片名称
  form.imageName = `${uuid}-${uploadFile.name}`

十二、钩子函数的使用

 官方文档中:

        还是有很多钩子的,我曾经都试过,可能是我的使用方式或者是时机不对,有一些钩子没有使用成功。这里还是着重介绍用到的钩子吧:

on-change:这个基本上是必用的钩子,这里能拿到上传的文件或者图片的全部信息去进行校验工作。

        当你上传了文件,成功时就会执行这个方法,也是在这个钩子中执行的“七、上传图片大小 类型个数限制”主要功能。

on-exceed:当超出限制时,执行的钩子函数。

        如果上传的文件超出了你在el-upload中设置的limit,那么会触发这个钩子。正好可以利用这个钩子执行替换功能。(官网上你可以看看el-upload的示例:“覆盖前一个文件”,用的就是这个钩子)详细的替换功能可以再回到“四、钩子函数的使用

十三、手动调用图片本地上传入口

// 手动调取图片本地上传入口
function onUploadImgLocal(row:any) {
  console.log('importBillExcel', row)
  const input = document.createElement('input')
  input.type = 'file'
  input.accept = '.jpeg, .png,  .jpg' // 限制选择的文件类型为 .jpg, .png,  .jpg
  input.style.display = 'none'
  document.body.appendChild(input)
  input.click()
  input.onchange = (e:any) => {
    const file = e.target.files[0] // 获取文件对象
    console.log('eeeeee', e, file)
    handleExceed([file])
  }
}

TODO:这一块之后我再补充一下技术盲区(在我的另一篇博文中)。

十四、新增中没有上传图片时,详情中el-image使用占位符给空状态图片站位

 详情页面,没有上传图片的时候的占位符

        <!-- 安装照片 -->
        <el-image
          v-if="item.type === 'img'"
          style="width: 120px; height: 120px;"
          :src="imgUrl"
          :zoom-rate="1.2"
          :max-scale="7"
          :min-scale="0.2"
          :preview-src-list="[imgUrl]"
          fit="cover"
        >
          <!-- 没有上传图片或者报错时的占位符 -->
          <template #error>
            <div class="image-slot">
              <div style="width: 120px; height: 120px;background: #efefef;">
                <div style="padding-top: 40px;margin-left: 15px;">
                  暂未上传照片
                </div>
              </div>
            </div>
          </template>
        </el-image>

十五、需要注意的小点

(1)on-change和on-exceed中的形参不同,前者是file,后者是[file]
(2)vue2和vue3的饿了吗UI可能有一些写法不同,需要注意
(3)handleStart指令其实是会触发on-change钩子的,所以在on-exceed去校验这一步应该还是可以优化的
(4)image-viewer 的使用
(5)变量的更新需要注意
    const imgList = ref<any>([]) // 图片列表
    const urlList = ref<UploadUserFile[]>([]) // 放大查看列表
    const imgREF = ref<UploadInstance>()   //方便拿el-upload的实例

(6)业务上这次是只能上传一张照片,如果是多个照片的情况该当如何处理?需要考虑什么?

        

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码农小白-RMS

谢谢老板

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值