图片上传功能在日常的开发中并不少见,但是图片的体积过大会增大服务器压力,用户体验感也不好,本文将基于canvas实现图片的压缩。全文无尿点,可以放心观看。
前言✨
之前和室友做了一个Vue3的蛋糕售卖和后台管理的系统,后台服务涉及到了管理员图片的上传,最开始采用base64直接上传,服务器响应时间太长(之前没经验),后来可以选择直接传输file类型的文件,但是管理员上传的文件大小不受控制,文件较大的时候,服务器响应时间也比较久。摸爬滚打的参考了很多的文章之后,也踩了不少的坑,于此记录下来,分享技术
的同时。也算是自己的一个小总结
。
前置知识✨
主要简单介绍一下后续所使用到的对象/方法,尽量用最简单最浅显的语言来讲清楚这部分知识,可放心食用。
1.FileReader对象 MDN-FileReader
通过FileReader对象可以读取文件/缓冲区内容
2.canvas MDN-canvas
可以理解为一个标签,可以通过这个标签上的属性来绘制图片并且可以实现压缩效果。
- canvas.getContext('2d'): 获得渲染上下文和它的2d绘画功能,返回ctx对象
- ctx.drawImage(img,x,y,width,height): 绘制图像 | x,y对应的是坐标轴,width、height绘制的图片大小
- canvas.toDataUrl(type, encoderOptions): 返回一个包含图片展示的 data URI
var canvas = document.getElementById("canvas"); var dataURL = canvas.toDataURL(); console.log(dataURL); // 注意返回结果中的逗号(伏笔) // "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNby // blAAAADElEQVQImWNgoBMAAABpAAFEI8ARAAAAAElFTkSuQmCC"
type: 指定图片的类型 | encoderOptions(可选) 指定图片体积的压缩比(0~1)默认为0.92
本文的重点就是基于encoderOptions进行压缩图片
3.atob MDN-atob
对经过 base-64 编码的字符串进行解码。
let encodedData = window.btoa("Hello, world"); // 编码 SGVsbG8sIHdvcmxk let decodedData = window.atob(encodedData); // 解码 Hello, world
4.ArrayBuffer MDN-ArrayBuffer
ArrayBuffer
对象代表储存二进制数据的一段内存
const buf = new ArrayBuffer(8);
上面代码生成了一段 8 字节的内存区域,每个字节的值默认都是 0。(看不太懂,不着急👇接着往下看)
5.File 构造函数 MDN-File
var myFile = new File(bits, name[, options]);
- bits: 一个包含
ArrayBuffer
,ArrayBufferView
,Blob
,或者DOMString
对象的Array
— 或者任何这些对象的组合。这是 UTF-8 编码的文件内容。 - name: 表示文件名称,或者文件路径。
- options: 选项对象,包含文件的可选属性。
正文✨
1. 搭建一个基于koa的后台服务|接受图片并且返回 图片的url地址
如果不了解koa的童鞋,可以讲这段代码拷贝,安装一下对应的依赖启动服务即可。
- koa-router 后端路由服务
- koa-static 开启静态服务
- koa-body 读取file文件
const path = require('path') const Koa = require('koa') const KoaStatic = require('koa-static') const Router = require('koa-router') const koaBody = require('koa-body') const router = new Router() const app = new Koa() app.use(koaBody({ multipart: true,// 支持请求中body携带文件类型的数据 formidable: { uploadDir: path.join(__dirname, '/public/uploads'),//设置图片保存地址 keepExtensions: true } })) // 接受图片保存在public文件夹下,开启静态web服务 app.use(KoaStatic(path.join(__dirname, '/public'))) // 上传图片中间件 function uploadImg (ctx) { const file = ctx.request.files.file const basename = path.basename(file.path) // 根据图片的绝对地址获取图片名称: 例如 /d/aa/c.js -> c.js ctx.body = { url: `${ctx.origin}/uploads/${basename}` } } // cors 跨域解决方案 app.use(async (ctx, next) => { ctx.set('Access-Control-Allow-Origin', '*'); if (ctx.method == 'OPTIONS') { ctx.body = 200; } else { await next(); } }) router.post('/uploads', uploadImg) app.use(router.routes()) app.listen(9001, () => console.log('服务启动成功')) // http://localhost:9001/uploads
目录结构:
2.上传图片,返回图片的url地址之后渲染到页面中(模拟一次正常的网络交互)
<input type="file" accept="image/*" name="file"> <button onclick="submit">提交表单</button> <img id="img" src="" alt="">
First make it work,then make it fast
我们先让流程跑通
const input = document.querySelector('input') const img = document.getElementById('img') const btn = document.querySelector('button') const formData = new FormData() function reqUploadImg() { fetch('http://localhost:9001/uploads', { method: 'post', body: formData }).then(res => res.json()) .then(res => { console.log(res) img.src = res.url }) } input.onchange = function (e) { const file = e.target.files[0] formData.append('file', file) }
上面的代码需要注意几个内容(正常交互不必在意)
- 开启liveServer
- 提交表单文件FormData
添加表单内容为 append
方法(我找了好久。。。用set方法一直为空) - 不能讲Service文件夹和前台代码放在一个根目录下,否则每次提交会刷新,原因是liveServer的监视,如下 可以看到,每次提交之后因为koa后台服务保存了图片之后,进而被liveServer监视,导致浏览器错以为代码发生改变而自动刷新了
展示效果:
3.优化代码
写代码之前先捋清楚我们的思路
- 1.首先我们需要读取图片
- 2.读取完文件之后通过canvas压缩生成一个新的图片
- 3.再通过buffer重新转化为图片 先看看效果,打印两次文件的size大小,从158kb到52kb,体积变为原来的1/3不到
修改后的代码(后面会讲base64转化原理)
input.onchange = function (e) { const file = e.target.files[0] // console.log(file) compressPic(file).then(resultFile => { formData.append('file', resultFile) }) } function compressPic(file, encoderOtp = 0.2) { return new Promise(resolve => { // 1. 通过FileReader读取文件 const reader = new FileReader() let res = reader.readAsDataURL(file) reader.onload = (event) => { // 2. 读取完毕之后获取图片的base64(上文伏笔),并创建新图片 const { result: src } = event.target const image = new Image() image.src = src image.onload = () => { // 3.图片加载完之后通过canvas压缩图片 const canvas = document.createElement('canvas') canvas.width = image.width canvas.height = image.height // 3.1 绘制canvas const ctx = canvas.getContext('2d') ctx.drawImage(image, 0, 0, image.width, image.height) // 3.2 返回图片url地址,并且进行压缩 const canvasURL = canvas.toDataURL(file.type, encoderOtp) const buffer = atob(canvasURL.split(',')[1]) // 3.3 bufferArray 无符号位字节数组 相当于在内存中开辟length长度的字节空间 let length = buffer.length const bufferArray = new Uint8Array(length) // 3.4 给新开辟的bufferArray赋值 while (length--) { bufferArray[length] = buffer.charCodeAt(length) } // 3.5将压缩后的文件通过resolve返回出去 const resultFile = new File([bufferArray], file.name, { type: file.type }) console.log(resultFile) resolve(resultFile) } } }) }
4.测试结果
我们再测试其他的几张图片试一试
文件格式 | 原体积 | encoder0.2 | encoder0.3 | encoder0.4 | encoder0.5 | 默认值0.92 |
jng | 158kb | 51kb | 66kb | 75kb | 87kb | 190kb |
gif | 490kb | 55kb | 72kb | 85kb | 99kb | 410kb |
png | 118kb | 141kb | 141kb | 141kb | 141kb | 141kb |
other | 76kb | 38kb | 47kb | 50kb | 60kb | 100kb |
结论 -----这部分结论可以参考 :# 前端图片最优化压缩方案
- encoder系数在0.2-0.5之间可以有效的压缩图片体积
- encoder默认值体积反而会变大
- png格式文件,和encdoer系数无关,甚至体积
不减反增
这里就展示了0.2~0.5的数据 低于0.2会出现图片压缩过度,质量下降。
左图为encoder0.1 右图为0.2,当encoder为0.1的时候,仔细看看还是可以看出丢失的精度。
5.发现问题
注意看第三行测试用例,为什么无论encoder是什么值,压缩后的体积都是一样呢? 🤔🤔
看来这其中一定哪个环节出了问题,继续测试发现只要是png格式
的文件就会出现这样的情况,其他文件即便是gif也会进行压缩
我们的结论一定正确吗?
再来看看这张png格式文件
再试试体积很小的svg文件
经过多组测试发现,使用canvas压缩体积的时候,文件体积过小反而会出现越压缩体积越大的情况
6.解决小体积图片压缩问题
那这样就很好解决了,设置一个体积阀门,小于这个体积的图片我们不做限制。毕竟我们的初心就是为了压缩大体积文件
在上述代码的基础上添加 minSize,当小于300kb的时候不进行canvas压缩
这里其实也可以通过Promise.all实现多文件的压缩。这里就不演示了
base64转换原理
说出来也挺巧的,在写这篇文章的时候恰好看到大佬山月
在朋友圈发的base64转换工具,感觉很棒!这里我们就借助这个工具来和大家讲讲base64的转换原理。地址:base64转换工具
转换流程:
- 将数字/字母转化为 Asc编码、汉字对应16位两字节的方式
- 将AsCii编码转化为base64编码。编码的方式为:
- 每
3*8bit
的字节转换为4*6bit
的字节,剩下的两位用 00 补齐,所以Base64 编码后的数据比编码之前大 1/3 - 每不足三个字节,自动补全,结果采用
=
占位 如👇输入字母a 转化为YQ==
总结✨
- 基于canvas可以实现对大图片的体积压缩 | 甚至可以实现指定
drawImage
图片宽高,进一步缩小图片体积 - 图片体积过小大概小于 150kb以下,会出现越压缩体积越小的情况 (这部分没有太多数据支撑,可能会存在一定误差)
- 基于
binary-to-text
算法,可以将二进制数据转化为64格式,但是体积也会增大1/3