date:
updated:

使用uniapp在微信小程序中压缩图片的几种方法及其优缺点


问题产生的背景

需要用微信小程序收集用户的单据, 需要把图片转换为base64发送到服务器保存, 而且base64大小限制在512kb以下

方法一 通过chooseImage压缩图片

使用原生api, 在用户选择图片的过程中压缩, 通过去掉’original’选项强制用户选择压缩的图片, 而不允许选择原图

1
2
3
4
5
6
wx.chooseImage({
sizeType: ['compressed'],
success: res => {
// ...
}
})

使用uniapp封装的api, uniapp为大部分api封装了promise, 通过then返回一个数组, 分别为错误对象和原始返回数据

1
2
3
4
5
6
uni.chooseImage({
sizeType: ['compressed'],
}).then(data => {
let [err, res] = data
// ...
})

这种方式虽然可以大大减少图片的体积到可用的范围, 常规图片可以压缩到70kb左右, 但图片严重失真, 用户的单据中的文字都无法看清, 不满足需求

方法二 使用canvas压缩图片

在web端经常使用canvas来对图片进行裁剪, 压缩和转换, 但在小程序上有较多的限制, 渲染速度也会降低

template

1
2
3
4
<canvas canvas-id='canvas' 
id="canvas"
:style="{width: canvasWidth + 'px', height: canvasHeight + 'px'}"
></canvas>
  • canvas-id 用于js获取canvas对象
  • style 通过内联样式动态改变canvas的宽高
  • 在canvas外层罩一个view, 定位到屏幕外, 这样渲染的时候就不会被用户看到了

javascript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
data() {
return {
tmpFileList: [] // 保存图片临时链接
}
}
methods: {
// 封装转换base64图片的方法
filePathToBase64(filePath) {
let base64 = wx.getFileSystemManager().readFileSync(filePath, 'base64')
let beforeStr = 'data:image/jpeg;base64,'
return beforeStr + base64
},
async chooseImage() {
let size // 当前图片大小
let tmpFile // 压缩后的图片临时链接

// 选择图片
await uni.chooseImage({
count: 1,
sizeType: ['original'], // 只允许上传原图
}).then(e => {
e = e[1] // 解开 uniapp 的封装
this.tmpFileList = this.tmpFileList.concat(e.tempFilePaths)
tmpFile = e.tempFilePaths[0]
size = this.filePathToBase64(this.tmpFileList[0]).length
})

// 获取图片长宽, 并改变canvas的大小
await uni.getImageInfo({
src: this.tmpFileList[0],
}).then(res => {
this.canvasWidth = res[1].width
this.canvasHeight = res[1].height
})

// 把图片渲染到canvas
const ctx = uni.createCanvasContext('canvas', this)
ctx.drawImage(this.tmpFileList[0], 0, 0, this.canvasWidth, this.canvasHeight)
await new Promise(resolve => {
ctx.draw(false, () => resolve())
}) // 尽管在这里等待了回调, 在手机上还是不一定渲染完, 直接进行下一步有可能得到图片的左上角

// 等待渲染完成, 循环压缩图片指导符合尺寸要求
setTimeout(async () => {
let compressTime = 0
const compressQualitys = [
0.5, 0.4, 0.3, 0.2, 0.1, 0.05, 0.03, 0.02, 0.01, 0.005
]
while(size > 512000 && compressTime < 9) {
await wx.canvasToTempFilePath({
canvasId: 'canvas',
quality: compressQualitys[compressTime],
fileType: 'jpg',
}, this)
.then(res => {
console.log(res.tempFilePath)
tmpFile = res.tempFilePath
size = this.filePathToBase64(res.tempFilePath).length
})
compressTime++
}
this.tmpFileList.push(tmpFile)
}, 500)
}
}

然而这个方法还是存在问题

  • 循环压缩非常消耗手机性能, 如果压缩10次的话要花费大量的时间
  • 即使压缩了10次, 太大的图片还是不能符合大小要求, 而且图片会严重失真

方法三 通过compressImage压缩图片

小程序为我们提供了compressImage的api来压缩图片,其性能比canvas渲染压缩更快,用户等待时间少

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
async function compressImage(imagePath, targetSize=512) {
let currentBase64 = filePathToBase64(imagePath)
let repeatTime = 0 // 重复压缩次数
let compressedPath = imagePath // 压缩后的图片文件路径
while (currentBase64.length / 1024 > targetSize) {
await uni.compressImage({
src: imagePath,
quality: 50 - 5 * repeatTime,
})
.then(res => {
let [err, data] = res
currentBase64 = filePathToBase64(data.tempFilePath)
compressedPath = data.tempFilePath
console.log(`压缩第${repeatTime + 1}次, 压缩质量为${50 - 5 * repeatTime}%,压缩后大小为: ${currentBase64.length}`)
})
if(repeatTime >= 9) {
console.log('循环次数过多, 强制停止压缩')
break
}
repeatTime++
}
console.log(`压缩完成, 当前大小为${currentBase64.length}`)
return {imagePath: compressedPath, base64: currentBase64}
}

这个方法的缺点是,在压缩的过程中会保存临时图片到本地相册,如果压缩了10次就会保存10张临时图片,用户会在相册中看到一大堆重复的图片,严重影响用户体验

但在用户手机上保存临时图片不加限制也不作清理,我觉得这个更多的是微信的锅

总结

在小程序上进行前端的图片压缩有各方面的限制和缺点,不要期待小程序对图片压缩的能力,这种事情还是交给后端吧


← Prev 在uniapp中完善vue的响应式 | 重新认识git【九】改变历史提交 Next →