在 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 });
});
四、注意事项
- 压缩会损失一定画质,需根据业务场景平衡画质与文件大小;
- 对于几十 MB 的超大图片,可考虑分阶段压缩(先大幅缩小尺寸,再调整质量);
- 服务端必须进行二次校验,防止前端绕过压缩逻辑上传超大文件。