Vue图片压缩方案

在 Vue 项目中处理用户上传的大图片时,通过前端压缩减小图片体积是优化上传体验的有效方式。以下为具体实现方案及相关说明:

一、图片压缩方案对比

不同压缩方案在体积缩减、兼容性、耗时等方面各有特点,具体如下:

方案 体积缩减 兼容性 耗时 备注
Canvas 缩放 60-85% 99% 中 本文主方案
WebP encode 额外-30% 92% 高 需服务端配合回退 JPG
WebAssembly 70-90% 92% 高 mozJPEG/avif-js,包大
服务端压缩 0% 100% 低 前端仍要先传原图

二、核心实现:Canvas 缩放压缩

利用 HTML5 的 Canvas API,通过调整图片尺寸和质量实现压缩,是兼容性和效果平衡较好的方案。

1. 压缩工具函数

核心思路是利用 HTML5 的 Canvas API 对图片进行压缩,主要通过调整图片尺寸和质量来实现。

javascript 复制代码
/**
 * 图片压缩工具
 * @param file 原始图片文件
 * @param options 压缩选项
 * @returns 压缩后的图片文件
 */
export const compressImage = async (
  file: File,
  options: {
    maxWidth?: number;    // 最大宽度
    maxHeight?: number;   // 最大高度
    quality?: number;     // 图片质量 0-1
  } = {}
): Promise<File | null> => {
  // 默认配置
  const {
    maxWidth = 1200,
    maxHeight = 1200,
    quality = 0.8
  } = options;

  return new Promise((resolve) => {
    // 创建图片对象
    const img = new Image();
    const objectUrl = URL.createObjectURL(file);
    img.src = objectUrl;

    img.onload = () => {
      URL.revokeObjectURL(objectUrl);

      // 计算压缩后的尺寸(保持比例)
      let width = img.width;
      let height = img.height;

      // 如果图片尺寸超过最大限制,按比例缩小
      if (width > maxWidth || height > maxHeight) {
        const ratio = Math.min(maxWidth / width, maxHeight / height);
        width = width * ratio;
        height = height * ratio;
      }

      // 创建Canvas元素
      const canvas = document.createElement('canvas');
      canvas.width = width;
      canvas.height = height;

      // 绘制图片到Canvas
      const ctx = canvas.getContext('2d');
      if (!ctx) {
        resolve(null);
        return;
      }
      
      ctx.drawImage(img, 0, 0, width, height);

      // 将Canvas内容转换为Blob
      canvas.toBlob(
        (blob) => {
          if (!blob) {
            resolve(null);
            return;
          }

          // 转换为File对象
          const compressedFile = new File(
            [blob],
            file.name,
            { type: blob.type, lastModified: Date.now() }
          );
          resolve(compressedFile);
        },
        file.type || 'image/jpeg',
        quality
      );
    };

    img.onerror = () => {
      URL.revokeObjectURL(objectUrl);
      resolve(null);
    };
  });
};
2. Vue3 组件中使用示例
html 复制代码
<template>
  <div class="image-upload">
    <input
      type="file"
      accept="image/*"
      @change="handleFileChange"
      class="file-input"
    />
    
    <div v-if="previewUrl" class="preview">
      <img :src="previewUrl" alt="预览图" class="preview-img" />
      <p>原始大小: {{ originalSize }}KB</p>
      <p v-if="compressedSize">压缩后大小: {{ compressedSize }}KB</p>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { compressImage } from '@/utils/imageCompressor';

const previewUrl = ref('');
const originalSize = ref(0);
const compressedSize = ref(0);

const handleFileChange = async (e: Event) => {
  const input = e.target as HTMLInputElement;
  if (!input.files || input.files.length === 0) return;

  const file = input.files[0];
  
  // 显示原始文件大小
  originalSize.value = Math.round(file.size / 1024);
  
  // 如果文件小于500KB,不压缩直接使用
  if (file.size < 500 * 1024) {
    previewUrl.value = URL.createObjectURL(file);
    compressedSize.value = originalSize.value;
    return;
  }

  // 压缩图片
  const compressedFile = await compressImage(file, {
    maxWidth: 1000,
    maxHeight: 1000,
    quality: 0.7
  });

  if (compressedFile) {
    previewUrl.value = URL.createObjectURL(compressedFile);
    compressedSize.value = Math.round(compressedFile.size / 1024);
    
    // 这里可以将compressedFile上传到服务器
    // await uploadToServer(compressedFile);
  }
};
</script>

<style scoped>
.image-upload {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.file-input {
  padding: 0.5rem;
  border: 1px solid #ccc;
  border-radius: 4px;
}

.preview {
  margin-top: 1rem;
}

.preview-img {
  max-width: 100%;
  border-radius: 4px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
</style>

三、服务端二次校验(Node 示例)

javascript 复制代码
router.post('/upload', multer().single('file'), async (req, res) => {
  const { buffer, mimetype, size } = req.file;
  if (size > 1024 * 1024) return res.status(413).send('too big');
  if (!['image/jpeg', 'image/webp'].includes(mimetype)) return res.status(400).send('bad type');

  // 计算 SSIM 或 Magic Number 防止"伪造压缩"
  const metadata = await sharp(buffer).metadata();
  if (metadata.width! > 1600) return res.status(400).send('width overflow');

  // 真正落盘
  await putToOSS(buffer, mimetype);
  res.json({ url });
});

四、注意事项

  1. 压缩会损失一定画质,需根据业务场景平衡画质与文件大小;
  2. 对于几十 MB 的超大图片,可考虑分阶段压缩(先大幅缩小尺寸,再调整质量);
  3. 服务端必须进行二次校验,防止前端绕过压缩逻辑上传超大文件。
相关推荐
LucianaiB10 小时前
【元宝搜索开发者征文大赛】AI时代,学得慢就不用学?用元宝搜索打造一个教程保质期,确保每一个教程符合你的现状
前端·人工智能·ai编程
excel10 小时前
JavaScript 对象池:减少高频对象创建与 GC 压力
前端
陈随易11 小时前
纯 Go 写的 JS 引擎,跑分超过了 C 语言的 QuickJS
前端·后端·程序员
子兮曰13 小时前
Chrome 权限元素实战:144 与 151 的渐进增强写法
前端·javascript·chrome
IT_陈寒13 小时前
Redis连接池泄漏那次,我差点把生产环境搞崩了
前端·人工智能·后端
强尼银手13 小时前
我花一天做了个格斗游戏原型,判定全对,然后被「美术」劝退了
前端·javascript
石小石Orz13 小时前
AI 正在蚕食我们的编码能力
前端·后端·算法
陈随易14 小时前
专注飞机 FocusFlight,两个人把番茄钟做成了一趟航班
前端·后端·程序员
总要学点什么15 小时前
第98篇:三维特效-材质线效果——给一条线换十种材质,虚线的图案只填了一半
开发语言·前端·javascript·ecmascript·gis·webgl·材质
10share15 小时前
基于大模型的 i18n 方案:把中文原文当 Key,轻松搞定多语言维护
前端·vue.js