利用es2024新特性,图片压缩及上传

📅 2026/7/5 8:47:53
利用es2024新特性,图片压缩及上传
背景在微信内置的浏览器上传图片时因考虑到手机端的图片较大一般在5M以上甚至更大达到10M以上手机端的浏览器对es最新语法的支持、网速、cpu处理速度及内存大小等限制我们需要对图片进行大小判断、图片在保证品质的前提等比例压缩大小然后进行上传大致分为三步1、在h5移动端利用h5新语法的特性来写2、利用es新特性同时要兼容微信内置浏览器的内核无论基于谷歌chrome或苹果的safiry,要兼容低版本的浏览器3、后端无论是python\php\java\net等接收图片并保存到服务器const MAX_SIZE 5 * 1024 * 1024; const MAX_WIDTH 2560; const MAX_HEIGHT 2560; const fileInput document.querySelector(#file); const uploadBtn document.querySelector(#uploadBtn); const preview document.querySelector(#preview); const logEl document.querySelector(#log); const imageUrlInput document.querySelector(#imageUrl); let finalBlob null; let finalFileName ; const log (...args) { logEl.textContent args.join( ) \n; }; const formatSize bytes { if (bytes 1024) return ${bytes} B; if (bytes 1024 * 1024) return ${(bytes / 1024).toFixed(2)} KB; return ${(bytes / 1024 / 1024).toFixed(2)} MB; }; const isJpgOrPng file { return [image/jpeg, image/png].includes(file.type); }; const supportWebp async () { const canvas document.createElement(canvas); canvas.width 1; canvas.height 1; return new Promise(resolve { canvas.toBlob(blob { resolve(blob?.type image/webp); }, image/webp, 0.8); }); }; const loadImage file { return new Promise((resolve, reject) { const url URL.createObjectURL(file); const img new Image(); img.onload () { URL.revokeObjectURL(url); resolve(img); }; img.onerror () { URL.revokeObjectURL(url); reject(new Error(图片读取失败)); }; img.src url; }); }; const calcTargetSize (width, height) { let targetWidth width; let targetHeight height; if (targetWidth MAX_WIDTH || targetHeight MAX_HEIGHT) { const ratio Math.min(MAX_WIDTH / targetWidth, MAX_HEIGHT / targetHeight); targetWidth Math.round(targetWidth * ratio); targetHeight Math.round(targetHeight * ratio); } return { width: targetWidth, height: targetHeight, }; }; const canvasToBlob (canvas, type image/webp, quality 0.82) { return new Promise((resolve, reject) { canvas.toBlob(blob { if (!blob) { reject(new Error(图片压缩失败)); return; } resolve(blob); }, type, quality); }); }; const imageToCanvas img { const { width, height } calcTargetSize(img.naturalWidth, img.naturalHeight); const canvas document.createElement(canvas); canvas.width width; canvas.height height; const ctx canvas.getContext(2d, { alpha: false, desynchronized: true, }); ctx.fillStyle #fff; ctx.fillRect(0, 0, width, height); ctx.drawImage(img, 0, 0, width, height); return canvas; }; const compressToWebpUnder5M async file { if (!isJpgOrPng(file)) { throw new Error(只允许上传 JPG 或 PNG 图片); } const webpOk await supportWebp(); if (!webpOk) { throw new Error(当前浏览器不支持 WebP 转换请升级微信或系统浏览器); } const img await loadImage(file); let canvas imageToCanvas(img); let quality 0.86; let blob await canvasToBlob(canvas, image/webp, quality); log(原图大小, formatSize(file.size)); log(首次 WebP, formatSize(blob.size)); if (blob.size MAX_SIZE) { return blob; } /** * 先降低质量 */ let minQuality 0.45; let maxQuality 0.86; for (let i 0; i 8; i) { quality (minQuality maxQuality) / 2; const tempBlob await canvasToBlob(canvas, image/webp, quality); if (tempBlob.size MAX_SIZE) { maxQuality quality; } else { blob tempBlob; minQuality quality; } } if (blob.size MAX_SIZE) { log(压缩后大小, formatSize(blob.size)); return blob; } /** * 如果降质量还超过 5MB再逐步缩小分辨率 */ let scale 0.9; while (blob.size MAX_SIZE scale 0.4) { const oldCanvas canvas; const newCanvas document.createElement(canvas); newCanvas.width Math.round(oldCanvas.width * scale); newCanvas.height Math.round(oldCanvas.height * scale); const ctx newCanvas.getContext(2d, { alpha: false, desynchronized: true, }); ctx.fillStyle #fff; ctx.fillRect(0, 0, newCanvas.width, newCanvas.height); ctx.drawImage(oldCanvas, 0, 0, newCanvas.width, newCanvas.height); canvas newCanvas; blob await canvasToBlob(canvas, image/webp, 0.72); log(缩放 ${Math.round(scale * 100)}% 后, formatSize(blob.size)); scale - 0.1; } if (blob.size MAX_SIZE) { throw new Error(图片过大压缩后仍超过 5MB请换一张图片); } log(最终大小, formatSize(blob.size)); return blob; }; fileInput.addEventListener(change, async event { logEl.textContent ; finalBlob null; finalFileName ; const [file] event.target.files; if (!file) { return; } try { log(正在处理图片...); const blob await compressToWebpUnder5M(file); finalBlob blob; finalFileName ${crypto.randomUUID()}.webp; const previewUrl URL.createObjectURL(blob); preview.src previewUrl; preview.style.display block; log(图片已转换为 WebP); log(待上传文件名, finalFileName); log(待上传大小, formatSize(blob.size)); } catch (error) { log(错误, error.message); fileInput.value ; } }); uploadBtn.addEventListener(click, async () { if (!finalBlob) { log(请先选择图片); return; } if (finalBlob.size MAX_SIZE) { log(图片超过 5MB禁止上传); return; } const formData new FormData(); formData.append(image, finalBlob, finalFileName); uploadBtn.disabled true; uploadBtn.textContent 上传中...; try { const response await fetch(/api/index/upload/, { method: POST, body: formData, credentials: same-origin, }); const result await response.json(); if (!response.ok || result.code ! 0) { throw new Error(result.msg || 上传失败); } imageUrlInput.value result.data.url; log(上传成功, result.data.url); } catch (error) { log(上传失败, error.message); } finally { uploadBtn.disabled false; uploadBtn.textContent 上传; } });!doctype html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title图片上传 WebP 压缩/title style body { font-family: Arial, sans-serif; padding: 16px; } #preview { max-width: 100%; margin-top: 16px; display: none; border-radius: 8px; } #log { margin-top: 16px; white-space: pre-wrap; background: #f6f6f6; padding: 12px; border-radius: 6px; font-size: 14px; } button { margin-top: 12px; padding: 10px 16px; border: 0; border-radius: 6px; background: #07c160; color: #fff; font-size: 16px; } /style /head body h3图片上传/h3 input idfile typefile acceptimage/jpeg,image/png,image/jpg br button iduploadBtn上传/button input idimageUrl typehidden value img idpreview alt预览图 pre idlog/pre script typemodule src__API__/js/upload-webp.js/script /body /html