图片上传功能实现与前端性能优化实战经验
我的写法,亲测靠谱
图片上传这事儿,看起来简单,但真做起来能踩出八百个坑。我刚开始做项目时,直接用 <input type="file"> 拿到文件就往后台扔,结果用户一传个 10MB 的原图,页面直接卡死,服务器还爆了。后来才明白,前端得把活儿干利索点,不能光靠后端兜底。
我现在处理图片上传,核心就三点:**限制大小、压缩处理、预览反馈**。下面是我现在用的代码,基本覆盖了大部分场景:
function handleImageUpload(file) {
// 1. 基础校验
if (!file.type.startsWith('image/')) {
alert('请上传图片文件');
return;
}
if (file.size > 5 * 1024 * 1024) { // 5MB
alert('图片不能超过5MB');
return;
}
// 2. 生成预览
const previewUrl = URL.createObjectURL(file);
showPreview(previewUrl); // 自定义函数,显示预览
// 3. 压缩(可选)
compressImage(file, { quality: 0.8 })
.then(compressedFile => {
uploadToServer(compressedFile);
})
.catch(() => {
// 压缩失败就用原图
uploadToServer(file);
});
}
这个流程跑下来,用户体验稳很多。重点是:先校验再处理,失败有兜底。压缩不是必须的,但对移动端用户特别友好——很多人手机拍的照片动不动就 6-7MB,传起来慢还费流量。
这几种错误写法,别再踩坑了
我见过太多人在这几个地方翻车,自己也栽过跟头,列出来避雷:
- 直接读取 file 内容而不校验类型:用户可能传个 .exe 文件,虽然 input 限制了 accept,但手动改文件后缀就能绕过。一定要用
file.type判断。 - 用 FileReader 读成 base64 后直接发给后端:base64 体积比原文件大 33%,而且后端还得转回二进制,纯属浪费带宽。应该用
FormData直接 append File 对象。 - 预览图不 revokeObjectURL:每次
URL.createObjectURL()都会占用内存,不释放的话,用户多传几次页面就卡了。记得在组件销毁或下次上传前调用URL.revokeObjectURL(url)。 - 压缩时忽略方向信息(EXIF):iOS 拍的照片经常旋转 90 度,因为 EXIF 里存了 orientation。用 canvas 压缩时如果不处理,图片就歪了。这点特别坑,我折腾了两天才发现。
举个反面例子,这种写法千万别用:
// ❌ 错误示范:没校验、没压缩、内存泄漏
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.src = e.target.result;
document.body.appendChild(img); // 没 revoke,内存爆炸
};
reader.readAsDataURL(file);
fetch('/upload', {
method: 'POST',
body: JSON.stringify({ data: e.target.result }) // 用 base64,体积膨胀
});
实际项目中的坑
除了基础逻辑,真实项目里还有不少细节要注意:
1. 移动端相册选择 vs 拍照:在 iOS 上,<input accept="image/*" capture> 调起的是相机,但有些安卓机反而会弹出相册。如果业务需要强制拍照,得用原生 App 或 Cordova 插件,纯 H5 很难统一。
2. 多图上传的并发控制:用户一次选 20 张图,你总不能同时发 20 个请求。我一般用 Promise.allSettled + 限流,比如最多同时传 3 张:
async function uploadMultiple(files) {
const limit = 3;
const results = [];
for (let i = 0; i < files.length; i += limit) {
const chunk = files.slice(i, i + limit);
const promises = chunk.map(file => uploadSingle(file));
const chunkResults = await Promise.allSettled(promises);
results.push(...chunkResults);
}
return results;
}
3. 上传进度反馈:大文件上传没进度条,用户以为卡死了。用 xhr 的 onprogress 事件很简单,但 fetch 原生不支持,得用 xhr 封装或者找第三方库。我图省事直接用 axios,它封装好了:
const formData = new FormData();
formData.append('file', file);
axios.post('/upload', formData, {
onUploadProgress: (progressEvent) => {
const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total);
updateProgress(percent); // 更新 UI
}
});
4. 服务端返回的图片 URL 要加时间戳:不然用户换头像后,浏览器可能缓存旧图。比如返回 https://jztheme.com/avatar.jpg?t=1717020000,前端直接用就行,不用自己拼。
压缩方案怎么选
很多人一上来就用 canvas 压缩,但其实要看场景:
- 如果是头像、商品图这类对质量要求不高的,canvas 压缩足够,代码也简单。
- 但如果是设计稿、高清摄影图,canvas 压缩会丢色,这时候不如直接传原图,让后端处理(比如用 Sharp 库)。
我现在的 canvas 压缩函数,处理了 EXIF 方向问题,亲测有效:
function compressImage(file, options = {}) {
const { quality = 0.8, maxWidth = 1920 } = options;
return new Promise((resolve) => {
const img = new Image();
img.src = URL.createObjectURL(file);
img.onload = () => {
URL.revokeObjectURL(img.src);
// 计算新尺寸(保持比例)
let { width, height } = img;
if (width > maxWidth) {
height = (height * maxWidth) / width;
width = maxWidth;
}
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, width, height);
canvas.toBlob(
(blob) => {
const compressedFile = new File([blob], file.name, { type: 'image/jpeg' });
resolve(compressedFile);
},
'image/jpeg',
quality
);
};
});
}
注意这里没处理 EXIF,因为现代浏览器 canvas 绘制时会自动纠正方向(Chrome 60+、Safari 13.1+)。如果要兼容老浏览器,得用 exif-js 库读取 orientation 再旋转 canvas,但我觉得没必要——现在谁还用那么老的浏览器?
结尾碎碎念
图片上传看着是小功能,但要做好得考虑一堆边界情况。我现在的方案也不是完美的,比如压缩后文件名会变成 blob,但后端一般也不 care。关键是别让用户卡住、别让服务器崩掉、别传错文件,这三点做到,基本就过关了。
以上是我踩坑后的总结,希望对你有帮助。有更好的方案欢迎评论区交流,比如你们怎么处理 WebP 格式?或者有没有更轻量的压缩库?

暂无评论