ElementPlus Upload组件使用compressorjs压缩图片上传

本文介绍了如何在Element-UI的el-upload组件中集成Compressor.js库,实现在移动端对大图片进行压缩,以减少文件大小、节省带宽和提升用户体验。详细阐述了图片压缩策略和组件的自定义上传逻辑。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

需求

Compressor.js 是一个用于在客户端(即在浏览器中)对图片进行压缩的 JavaScript 库。使用它有以下几个优点和意义:

  1. 减少文件大小: 图片通常是网页中占用大量带宽的资源之一。通过使用 Compressor.js 对图片进行压缩,可以显著减少图片文件的大小,从而减少页面加载时间,提高网页性能。

  2. 节省带宽: 在移动设备上访问网页时,特别是在使用移动数据连接(如4G或5G)时,大文件大小的图片会消耗大量的数据流量。通过压缩图片,可以节省用户的数据流量,降低用户的数据费用。

  3. 提高用户体验: 加载速度快的网页可以提供更好的用户体验。通过减少图片大小,可以加快网页的加载速度,使用户能够更快地访问和浏览网页内容。

  4. 支持移动端开发: 在移动端开发中,特别是在开发需要上传图片的应用时,通常需要在客户端对上传的图片进行压缩以减少上传时间和数据传输量。Compressor.js 提供了一个方便的方式来在移动设备上对图片进行压缩,使得开发人员能够轻松地实现这一功能。

  5. 保持图片质量: Compressor.js 在压缩图片的同时,尽可能地保持图片的质量,避免出现明显的视觉损失。尤其对于相机直拍的照片,有很好的加速作用,试想当服务器网络质量差,带宽只有数十K的时候,上传一张5M的高清图至少需要一分钟以上。而通过压缩操作后,5M照片可以压缩至200~300K且不损失质量,上传至需要几秒即可完成,体验立马上去一个档次。

而Element-UI的上传组件,默认就是使用FormData进行的直传,如果可以和压缩功能结合,当图片较大时自动处理。则在移动端上传直拍照片可以获得很好的体验效果。

集成方案

按照comppressor.js的文档(compressorjs - npm)。我们可以新建一个Compressor,在其success回调里进行上传。其入口和回调返回都是File结构,这样我们便可以处理文件流的压缩

针对element-ui的el-upload组件,有一个扩展点,即http-request可以传入一个UploadRequestHandler结构:

export declare type UploadRequestHandler = (options: UploadRequestOptions) => XMLHttpRequest | Promise<unknown>;

返回一个Promise进行对应的上传处理,这样我们即可绕过el-upload自带的上传逻辑,建立我们自己的上传及处理返回值逻辑

我们定义处理器如下:

// 文件上传切面,支持图片压缩和适配权限,默认激活策略是:图片大小大于300k(jpg)或500k(png),比例缩放到1334大小
export function optImgHttpRequest(options: UploadRequestOptions) {
  // el-upload的图片压缩适配
  const SIZE_LIMIT = 1334
  const JPG_KB_START = 300 * 1024
  const PNG_KB_START = 500 * 1024

  const thumbWidth = options.data['thumbWidth']
  const thumbHeight = options.data['thumbHeight']

  const uploadProcess = (resolve: Function, reject: Function, file: File) => {
    const formData = new FormData()
    formData.append(
      'file',
      file,
      options.file.name.toLowerCase().endsWith('.png') && options.file.size > 5000000
        ? options.file.name.replace(/(\.PNG|\.png)$/i, '.jpg') //超过5M的png会转化为jpg,文件名改变
        : file.name
    )
    if (thumbHeight) {
      formData.append('thumbHeight', thumbHeight.toString())
    }
    if (thumbWidth) {
      formData.append('thumbWidth', thumbWidth.toString())
    }
    return API.adminTools.upload
      .request({}, formData, {
        headers: {
          'Content-Type': 'multipart/form-data' // 覆盖类型为form-data上传
        }
      })
      .then((res: defs.FileInfo) => {
        resolve(res)
      })
      .catch((err) => {
        reject(err)
      })
  }

  if (
    // jpg和png要大于尺寸才开启压缩
    ((options.file.name.toLowerCase().endsWith('.jpg') || options.file.name.toLowerCase().endsWith('.jpeg')) &&
      options.file.size > JPG_KB_START) ||
    (options.file.name.toLowerCase().endsWith('.png') && options.file.size > PNG_KB_START)
  ) {
    return new Promise((resolve, reject) => {
      new Compressor(options.file, {
        quality: 0.8,
        maxWidth: SIZE_LIMIT,
        maxHeight: SIZE_LIMIT,
        success(result: File) {
          uploadProcess(resolve, reject, result)
        }
      })
    })
  } else {
    // 否则直接上传
    return new Promise((resolve, reject) => {
      uploadProcess(resolve, reject, options.file)
    })
  }
}

上传组件

这里是ccframe的一个input风格的上传组件,支持预览。按照定制的错误规则展示信息,以及当5M以上大小PNG自动更换后缀名(compressorjs默认5M以上PNG会压缩为JPG格式)等功能的支持

<!--
  CcFrame的Element Plus Upload的上传组件
  支持2种布局模式:
  1.默认的图片上传布局,只支持图片
  2.兼容Input风格的上传组件,支持预览,支持多种文件类型
  支持生成指定大小的缩略图(需要后端支持)缩放为contain模式
  @Author Jim 24/07/27
 -->
<template>
  <div class="inline-block leading-0 pt-10px pr-10px relative">
    <el-icon
      v-if="fileInfo.url && fileInfo.url.length > 0"
      :size="25"
      class="absolute! right-0 top-0 z-10 cursor-pointer"
      color="#71727A"
      @click="doDelete"
      ><CircleClose
    /></el-icon>
    <el-image
      v-if="props.shape === 'image'"
      :src="fileInfo.url"
      fit="scale-down"
      :preview-src-list="[fileInfo.url || '']"
      :preview-teleported="true"
      v-bind="$attrs"
      class="relative z-0"
    >
      <template #error>
        <div class="image-slot h-100% cursor-pointer" @click="dialogVisible = true">
          <el-icon
            :size="35"
            color="#71727A"
            class="absolute left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2"
            ><Plus
          /></el-icon>
        </div>
      </template>
    </el-image>
  </div>
  <el-input
    v-if="props.shape === 'input'"
    :placeholder="placeholder"
    v-model="fileInfo.filename"
    :size="size"
    :readonly="true"
    v-bind="$attrs"
  >
    <template #suffix>
      <div class="flex items-center" style="height: 36px">
        <svg-icon name="upload" class="toolIcon" @click="dialogVisible = true" title="上传" />
        <svg-icon name="preview" class="toolIcon" @click="showUploadFile" style="padding-left: 2px" title="预览" />
        <svg-icon name="delete" class="toolIcon delIcon" @click="doDelete" title="清除" />
      </div>
    </template>
  </el-input>
  <el-dialog
    title="文件上传"
    :append-to-body="true"
    v-model="dialogVisible"
    width="400px"
    :before-close="checkUploading"
  >
    <el-upload
      ref="upload"
      style="width: 360px"
      action="/admin/fileInf/upload"
      :accept="accept"
      :data="withExtraData"
      drag
      :limit="1"
      :on-error="showFail"
      :on-success="updateAndClose"
      :before-upload="beforeUpload"
      :http-request="optImgHttpRequest"
    >
      <i class="el-icon-upload" />
      <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
      <template #tip>
        <div class="el-upload__tip">上传文件不能超过10M</div>
      </template>
    </el-upload>
  </el-dialog>
  <transition name="fade">
    <div
      v-if="showPreview"
      class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-20px z-99999"
      @click="showPreview = false"
    >
      <img class="preview-img" :src="fileInfo.url" />
    </div>
  </transition>
</template>

<script lang="ts" setup>
import {
  watch,
  ref,
  onMounted,
  reactive,
  defineModel,
  withDefaults,
  defineProps,
  nextTick,
  getCurrentInstance
} from 'vue'
import { UPDATE_MODEL_EVENT, CHANGE_EVENT, INPUT_EVENT } from 'element-plus/es/constants/event.mjs'
import { UploadFile } from 'element-plus'
import { UploadAjaxError } from 'element-plus/es/components/upload/src/ajax'

import { tools, optImgHttpRequest } from '@/main'
import '@/services'
import { useUserStore } from '@/commons/store/modules/user'

const { userInfo } = useUserStore()

const props = withDefaults(
  defineProps<{
    modelValue: string | undefined // 双向绑定数据
    shape?: 'input' | 'image' // Input风格还是图片区块风格
    accept?: string // 选择器过滤的类型
    checkType?: string // 如果要检查图片,使用 'image/gif,image/jpeg,image/png'
    placeholder?: string // 占位提示信息,input风格需要
    thumbHeight?: number // 指定缩略图高度,不传则不生成缩略图(需要后端配合)
    thumbWidth?: number // 指定缩略图宽度,不传则不生成缩略图(需要后端配合)
    size?: 'default' | 'small' | 'large' // input风格尺寸
  }>(),
  {
    modelValue: undefined,
    shape: 'image',
    accept: '.gif,.jpg,.png,image/gif,image/jpeg,image/png', // 注意,默认是上传图片
    checkType: 'image/gif,image/jpeg,image/png', // 默认检查图片
    placeholder: '请选择文件上传',
    size: 'large'
  }
)

const withExtraData: Promise<Record<string, any>> = new Promise((resolve, reject) => {
  const extraData: Record<string, any> = {}
  if (props.thumbWidth) {
    extraData.thumbWidth = props.thumbWidth.toString()
  }
  if (props.thumbHeight) {
    extraData.thumbHeight = props.thumbHeight.toString()
  }
  resolve(extraData)
})

const emit = defineEmits([UPDATE_MODEL_EVENT, CHANGE_EVENT, INPUT_EVENT])

// dom
const upload = ref<any>()
// data
const fileInfo = reactive<defs.FileInfo>({
  url: undefined, // 回显图片地址
  filename: '', // 图片名称
  ext: '', // 扩展名类型
  path: '' //图片路径
})

const uploading = ref<boolean>(false)
const dialogVisible = ref<boolean>(false)
const showPreview = ref<boolean>(false)

watch(
  // modelValue重新赋值时,根据值解析path和filename、ext、url
  () => props.modelValue,
  (val) => {
    unpackVal(val)
  }
)

// methods
const unpackVal = (val?: string) => {
  // 从数据库读取时拆URL
  if (val) {
    // eslint-disable-next-line no-control-regex
    const extract = /((\w+\/)*)([^\0-\x1F\\/:*?"<>|]+\.([^.]+))$/.exec(val)
    if (extract) {
      fileInfo.filename = extract[3]
      fileInfo.ext = extract[4]
      fileInfo.path = extract[1]
      fileInfo.url = userInfo.fileDomain + fileInfo.path + fileInfo.filename
    }
  } else {
    // 清空
    fileInfo.filename = ''
    fileInfo.ext = ''
    fileInfo.url = ''
    fileInfo.path = ''
  }
}

const getFileUrl: () => string = () => {
  return fileInfo.url || ''
}

const beforeUpload: (file: File) => boolean = (file) => {
  if (props.checkType && !props.checkType.includes(file.type)) {
    tools.alert(`文件格式不正确 ${file.type}`)
    return false
  }
  const isLt10M = file.size / 1024 / 1024 < 10
  if (!isLt10M) {
    tools.alert('上传文件大小不能超过 10MB!')
  } else {
    uploading.value = true
  }
  return isLt10M
}

const checkUploading: (done: any) => void = (done) => {
  if (uploading.value) {
    tools
      .confirm('上传进度未结束,您确定要终止上传吗?')
      .then(() => {
        uploading.value = false
        upload.value.abort()
        done()
      })
      .catch(() => {
        // NOOP
      })
  } else {
    done()
  }
}

const updateAndClose: (fileInfo: defs.FileInfo, file: UploadFile, fileList: UploadFile[]) => void = (res) => {
  // 只有在整合optImgHttpRequest后,才会返回FileInfo
  uploading.value = false
  dialogVisible.value = false
  Object.assign(fileInfo, res)
  const fileVal = '' + fileInfo.path + fileInfo.filename
  upload.value.clearFiles() //手动清理队列

  emit(UPDATE_MODEL_EVENT, fileVal === '' ? undefined : fileVal)
  emit(CHANGE_EVENT, fileVal === '' ? undefined : fileVal)
}

const doDelete: () => void = () => {
  emit(UPDATE_MODEL_EVENT, undefined)
  emit(CHANGE_EVENT, undefined)
  emit(INPUT_EVENT, undefined)
}

const showUploadFile: () => void = () => {
  if (!props.modelValue) {
    tools.toast('您还未上传文件,无法预览', 'error')
  } else {
    if (['jpg', 'gif', 'png'].includes((fileInfo.ext || '').toLowerCase())) {
      // 图片展示
      showPreview.value = true
    } else {
      // 非图片下载
      const aTag = document.createElement('a')
      aTag.download = fileInfo.filename || '' // 下载的文件名
      aTag.href = fileInfo.url || ''
      aTag.click()
    }
  }
}

const showFail: (err: Error, file: UploadFile, fileList: UploadFile[]) => void = (err, file, fileList) => {
  let errorText = '网络请求失败'
  if (err instanceof UploadAjaxError) {
    // 需要验证,会不会走这里
    if (err.status) {
      switch (err.status) {
        case 500: {
          errorText = '上传失败' // 正常需要在组件限制文件大小,如果未限制会失败。默认最大的文件为10M
          break
        }
        case 504:
        case 404: {
          errorText = '上传URL错误'
          break
        }
      }
    }
  } else {
    errorText = err.message
  }
  tools.toast(errorText, 'error')
  uploading.value = false
}

onMounted(() => {
  unpackVal(props.modelValue)
})
</script>

<style scoped lang="scss">
.toolIcon {
  font-family: iconfont;
  user-select: none;
  width: 24px;
  height: 24px;
  line-height: 24px;
  cursor: pointer;
  margin-left: 7px;
  color: #409eff;
  :hover {
    color: #66b1ff;
  }
  :active {
    color: #3a8ee6;
  }
}

.delIcon {
  color: #f56c6c;
  :hover {
    color: #f78989;
  }
  :active {
    color: #dd6161;
  }
}

.x-screen-box {
  position: fixed;
  top: 70px;
  bottom: 70px;
  left: 70px;
  right: 70px;
  z-index: 99999;
}
.preview-img {
  object-fit: scale-down;
  width: 100%;
  height: 100%;
}
</style>

最终效果

点击上传按钮

上传完毕

点击预览按钮

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

FoxMale007

文章非V全文可读,觉得好请打赏

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

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

打赏作者

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

抵扣说明:

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

余额充值