前情概要:
最近业务里通过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)业务上这次是只能上传一张照片,如果是多个照片的情况该当如何处理?需要考虑什么?