最近做了移动端生成图片并且上传的需求,踩了不少坑,这里记录一下。由于本次使用canvas主要功能集中在绘制网络图片以及生成/上传图片,因此本文多为和图片相关的记录。
绘图部分
onload
最开始使用canvas直接加载网络图片的时候,忘记考虑图片加载的问题了,因此直接上手写image.src = xxx
接着就是ctx.drawImage
,最后发现网络图片根本没有被绘制上去,这才想起来图片必须要先加载完之后才能使用ctx.drawImage
去绘制,否则图片没有加载完,canvas
直接绘制一张空的图片。
export class Canvas {
// code here...
// 加载图片
addImage(src: string) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
resolve(img);
}
img.src = src;
});
}
// code here...
}
复制代码
图片跨域问题
修改完毕之后,本来以为这次代码能跑起来了,却发现由于访问了CDN图片地址,Image默认不支持访问跨域资源,必须要手动指定crossOrigin
属性才可以跨域访问图片。
export class Canvas {
// code here...
// 加载图片
addImage(src: string) {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
resolve(img);
}
img.src = src;
});
}
// code here...
}
复制代码
渲染的层级关系
图片加载出来之后,本来感觉问题已经解决了,但是发现我本来应该渲染的三张图片,最后只出来了一张背景图,另外两个图都不见了,但是在代码里面打印日志是可以看到canvas确实执行了这两张图片的渲染逻辑。
查了查资料发现canvas只能按照渲染的先后顺序来展示绘图的层级关系,无法手动指定层级,因此我们想要在背景图上方绘制图片的话,必须要等到背景图绘制完毕之后才能继续执行其他的逻辑。
export class Canvas {
canvas: HTMLCanvasElement;
ctx: CanvasRenderingContext2D;
constructor() {
// other code
this.canvas = document.createElement('canvas');
this.ctx = this.canvas.getContext('2d')!;
// other code
}
// code here...
addImage(src: string) {
// ...
}
drawImage() {
const bg = 'https://xxx';
this.addImage(bg).then((img: ImageBitmap) => {
this.ctx.drawImage(img, 0, 0, img.width, img.height);
// 在这之后才能继续绘制其他图片
this.addImage(xxx);
});
}
}
复制代码
绘制圆形图片
所有绘制都结束之后,产品突然过来跟我说,希望在最后面加上一行用户信息的区域,包括用户头像和用户名,本来以为是很简单的工作,按照上面的逻辑再绘制一张图片和一段文字即可,后来发现用户头像的图片都是方形的,但是产品希望要一张圆形头像图片,在css
中只需要很简单一行border-radius: 50%
的东西让我很头疼。
尝试了各种办法都失败之后,上网查了查资料才发现原来context
还有保存和还原方法,再配合clip
裁切就可以完成一个圆形图片了!
export class Canvas {
constructor() {
// ...
}
addImage() {
// ...
}
drawImage() {
// ...
}
drawUserInfo() {
// other code
const src = 'https://xxx';
this.addImage(src).then((img: ImageBitmap) => {
this.ctx.save();
this.ctx.arc(x, y, r, 0, Math.PI * 2);
this.ctx.clip();
this.ctx.drawImage(img, x, y, img.width, img.height);
this.ctx.restore();
});
}
}
复制代码
这样先保存当前画布的状态,然后通过arc
在头像图片的位置绘制一个圆形,然后裁切掉多余的部分,接着绘制头像,最后再恢复画布状态即可。
优化部分
3x图
以上步骤就进行完之后,我测试了一下绘制图片并且上传CDN的功能,一切正常!
然后满心欢喜的展示这张图片的时候,发现在手机上展示出来的实在太模糊了,甚至连文字都看不清楚。
这时我才意识到我们平时用的图片都是3x或者2x图,现在我按照UI稿的360px
宽度绘制的这张图片只是1x图,在我们高分辨率的手机上展示出来就会很模糊,因此为了让图片不模糊,我也需要将图片变为3x图。
因此将绘图时候所有的宽高及其他数组全部都×3.
this.width = 360 * 3;
this.height = 500 * 3;
this.ctx.drawImage(bg, 0, 0, this.width * 3, this.height * 3);
// 其他改动同理
复制代码
这样改动之后,展示出来的图片就非常清晰了!
缓存图片导致绘制卡主
由于需要加载多张图片,因此我这里需要监听所有图片都加载并且绘制成功之后,才能执行最终的canvas.toBlob()
逻辑并且上传图片。
export class Canvas {
constructor() {
// ...
this.loadedImageNumber = 0;
}
// code here
imageOnLoaded() {
this.loadedImageNumber++;
if(this.laodedImageNumber >= 3) {
// upload image
}
}
}
复制代码
结果pm和qa同学测试的时候,经常发现上传过程卡住了,一直处于loading状态,后来经过无数次尝试和排查问题之后,发现因为前面有些图片已经加载过一次了,这里的图片有可能从浏览器的缓存里面获取了,因此根本不会执行onload
函数,这样就导致this.loadedImageNumber
一直达不成大于等于3的条件,所以就卡在loading状态了。
解决办法是完善一下addImage
函数,在监听onload
的同时判断一下img的状态,如果是complete
的话也执行一遍回调逻辑,顺便也加了一下关于onerror
的处理。
export class Canvas {
// code here...
// 加载图片
addImage(src: string) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
resolve(img);
}
img.onerror = () => {
// error callback
reject();
}
img.src = src;
if(img.complete) {
resolve(img);
}
});
}
// code here...
}
复制代码
这样测试了一下所有图片都可以正常被加载出来了~
polyfill
上面问题解决了之后,QA同学有反馈有一些低端手机依然卡在loading状态,我本来还以为是图片绘制依然有问题,然后我借过手机来调试了一下,发现并不是卡在图片绘制过程,而是canvas.toBlob()
的时候报错了,于是后面的逻辑都卡住了。
export class Canvas {
// code here
imageOnLoaded() {
this.loadedImageNumber++;
if(this.laodedImageNumber >= 3) {
this.canvas.toBlob(blob => {
// 真正的上传函数
this.uploadImage(blob);
},
'image/jpeg',
1.0
)
}
}
// code here
}
复制代码
再次上网查了查资料,发现需要打polyfill才行。
github地址:JavaScript-Canvas-to-Blob
网上很多使用介绍的文章,或者直接看github官网的readme也很容易看懂。
if(__BROWSER__) {
// import canvas toblob polyfill
require('blueimp-canvas-to-blob');
}
export class Canvas {
// ...
}
复制代码
__BROWSER__
是webpack.DefinePlugin
定义的客户端渲染环境。
这下感觉应该万事大吉了。
优化图片大小
然后就又被QA同学找了。。
QA同学反馈说图片上传太慢了,弱网情况下要loading很久才会结束,或者甚至直接到后端接口返回超时了也还没有结束图片上传。
我抓包看了看图片上传的接口,发现确实有点慢,因为生成的图片体积太大了,足足有2.6M多
问了一下同事,原来是最初图片模糊的时候,我想要提高图片质量,改成3x图的同时又在toBlob()
的时候指定图片质量是1.0,所以导致了图片体积过大。
把这里的图片质量指定为0.8左右之后体积一下就降下来了,并且图片质量其实并没有改变多少。
export class Canvas {
// code here
imageOnLoaded() {
this.loadedImageNumber++;
if(this.laodedImageNumber >= 3) {
this.canvas.toBlob(blob => {
// 真正的上传函数
this.uploadImage(blob);
},
'image/jpeg',
0.8
)
}
}
// code here
}
复制代码
到这里这个功能总算是完成了 OwO