使用VUE或者React框架加载HTML大图时极容易导致前端页面卡死报out of memory错误,报出这个错误还好,如果没有报错浏览器可能就无法操作了。
前端压缩
为保证前端页面正常渲染html大图,需要在html加载前压缩图片。前端解决这个问题有两种方案,一个是上传前压缩,一个是上传后压缩。上传前可以保证当前图片无论在哪里显示大小已经被压缩过,无需再去管理。上传后压缩可以保证服务器端拥有高清原图,显示前需要做一次压缩。
无论那种压缩,压缩方式基本一致
1.获取图片尺寸
2.计算压缩比率
3.通过canvas.toDataUrl 参数转化压缩图片
4.将base64字符转转成文件或者直接显示
上传前压缩
获取到本地文件后,通过FileReader将本地文件写入到内存中
// 获取文件图片的宽高 function getImageWHFromFile(file, fn, err) { try { // 读取上传之后的文件对象 const imageReader = new FileReader() // 将图片的内存url地址添加到FileReader中 imageReader.readAsDataURL(file) // 当图片完全加载到FileReader对象中后,触发下面的事件 imageReader.addEventListener('loadend', function(e) { // 获取上传到本机内存中的图片的url地址 const imageSrc = e.target.result // 调用计算图片大小的方法 calculateImageSize(imageSrc).then(function({ image, width, height }) { // 得到图片的宽和高 fn(image, width, height) }).catch(e => { err() }) }) // 可以做一些其他状态监听 } catch (e) { err(file) } }
计算内存中图片的宽高
// 根据图片地址获取图片的宽和高 function calculateImageSize(src) { return new Promise(function(resolve, reject) { const image = new Image() image.addEventListener('load', function(e) { resolve({ image: image, width: e.target.width, height: e.target.height, }) }) image.addEventListener('error', function(e) { reject(Error('获取尺寸失败')) }) // 将图片的url地址添加到图片地址中 image.setAttribute('crossOrigin', 'Anonymous') image.src = src }) }
得到图片宽高后就可以计算压缩比率,通过图片真水的宽高和期望的宽高计算压缩率,基本上可以得到合适的图片尺寸
// 计算压缩比率 function getRatio(oriW, oriH, width, height) { const proW = (oriW / width).toFixed(5) const proH = (oriH / height).toFixed(5) return proW < proH ? proW : proH }
得到压缩比率就可以执行压缩,压缩使用canvas实现
// 缩放img对象中的图片 function compressImageToBase64(image, quality) { const canvas = document.createElement('canvas') const context = canvas.getContext('2d') const imageWidth = image.width * quality const imageHeight = image.height * quality canvas.width = imageWidth canvas.height = imageHeight context.drawImage(image, 0, 0, imageWidth, imageHeight) return canvas.toDataURL('image/jpeg', quality) }
压缩完得到图片的base64(DataURL)数据,可以用这个数据直接输出到img标签显示,或者继续加工生成图片文件
将base64转成blob对象
// DataURL转Blob对象 function dataURLToBlob(dataurl) { const arr = dataurl.split(',') const mime = arr[0].match(/:(.*?);/)[1] const bstr = atob(arr[1]) let n = bstr.length const u8arr = new Uint8Array(n) while (n--) { u8arr[n] = bstr.charCodeAt(n) } return new Blob([u8arr], { type: mime }) }
将Blob转成File,调用接口用FormData的形式上传文件
// 对图片进行压缩并返回文件格式 function compressToFile(image, quality) { const blob = dataURLToBlob(compressImageToBase64(image, quality)) return new File([blob], new Date().getTime() + '.jpg') }
到此,就得到了一个压缩后的文件对象
上传后压缩
上传后压缩的意思是,服务器返回一个图片url后,如果这个url指向的图片尺寸很大,比如:10M,这时候在浏览器中显示这个图片会导致页面卡死。我的解决方案是压缩后再显示,压缩方式大概与上传前压缩类似,从calculateImageSize方法开始计算图片尺寸,然后进行压缩,得到base64数据后回调显示这个图片。这里存在一个问题:压缩开始后chrome的控制台无法打开。原因则是image加载图片后会在控制台的Network中输出base64格式的日志,这个日志占用了太大的内存。
服务器压缩
服务器压缩是最好的且能满足产品对大图要求的解决方案。前端上传原图,服务器压缩后生成两个地址,一个缩略图一个原图,缩略图供前端显示,原图供下载。+
附全部代码供参考
/** * @param {待压缩图片文件} file * @param {异步回调} fn * @param {压缩期望尺寸} size [width ,height] */ const fileCompressToFile = function(file, fn, size) { const _size = getComperssSize(size) if (_size === null) { fn(file) return } // 开启后执行压缩 getImageWHFromFile(file, (image, width, height) => { const pro = getRatio(_size[0], _size[1], width, height) if (pro >= 1) { fn(file) return } // 开始压缩 fn(compressToFile(image, pro)) }, () => { fn(file) }) } // 通过url压缩图片,压缩成功返回base64 const urlCompressToBase64 = function(url, fn, size) { const _size = getComperssSize(size) if (_size === null) { fn(url) return } calculateImageSize(url).then(function({ image, width, height }) { const pro = getRatio(_size[0], _size[1], width, height) if (pro >= 1) { fn(url) return } // 开始压缩 fn(compressImageToBase64(image, pro)) }).catch(e => { console.error(e) fn(url) }) } // 格式化压缩尺寸 function getComperssSize(size) { if (!size) { return null } const defaultSize = 500 if (Array.isArray(size)) { return [size[0] || defaultSize, size[1] || defaultSize] } const pf = parseFloat(size) return [pf || defaultSize, pf || defaultSize] } // 计算压缩比率 function getRatio(oriW, oriH, width, height) { const proW = (oriW / width).toFixed(5) const proH = (oriH / height).toFixed(5) return proW < proH ? proW : proH } // 缩放img对象中的图片 function compressImageToBase64(image, quality) { const canvas = document.createElement('canvas') const context = canvas.getContext('2d') const imageWidth = image.width * quality const imageHeight = image.height * quality canvas.width = imageWidth canvas.height = imageHeight context.drawImage(image, 0, 0, imageWidth, imageHeight) return canvas.toDataURL('image/jpeg', quality) } // 对图片进行压缩并返回文件格式 function compressToFile(image, quality) { const blob = dataURLToBlob(compressImageToBase64(image, quality)) return new File([blob], new Date().getTime() + '.jpg') } // DataURL转Blob对象 function dataURLToBlob(dataurl) { const arr = dataurl.split(',') const mime = arr[0].match(/:(.*?);/)[1] const bstr = atob(arr[1]) let n = bstr.length const u8arr = new Uint8Array(n) while (n--) { u8arr[n] = bstr.charCodeAt(n) } return new Blob([u8arr], { type: mime }) } // 获取文件图片的宽高 function getImageWHFromFile(file, fn, err) { try { // 读取上传之后的文件对象 const imageReader = new FileReader() // 将图片的内存url地址添加到FileReader中 imageReader.readAsDataURL(file) // 当图片完全加载到FileReader对象中后,触发下面的事件 imageReader.addEventListener('loadend', function(e) { // 获取上传到本机内存中的图片的url地址 const imageSrc = e.target.result // 调用计算图片大小的方法 calculateImageSize(imageSrc).then(function({ image, width, height }) { // 得到图片的宽和高 fn(image, width, height) }).catch(e => { err() }) }) // 可以做一些其他状态监听 } catch (e) { err(file) } } // 根据图片地址获取图片的宽和高 function calculateImageSize(src) { return new Promise(function(resolve, reject) { const image = new Image() image.addEventListener('load', function(e) { resolve({ image: image, width: e.target.width, height: e.target.height, }) }) image.addEventListener('error', function(e) { reject(Error('获取尺寸失败')) }) // 将图片的url地址添加到图片地址中 image.setAttribute('crossOrigin', 'Anonymous') image.src = src }) } export { fileCompressToFile, urlCompressToBase64, }