最近作了移动端生成图片而且上传的需求,踩了很多坑,这里记录一下。因为本次使用canvas主要功能集中在绘制网络图片以及生成/上传图片,所以本文多为和图片相关的记录。css
最开始使用canvas直接加载网络图片的时候,忘记考虑图片加载的问题了,所以直接上手写image.src = xxx
接着就是ctx.drawImage
,最后发现网络图片根本没有被绘制上去,这才想起来图片必需要先加载完以后才能使用ctx.drawImage
去绘制,不然图片没有加载完,canvas
直接绘制一张空的图片。webpack
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
属性才能够跨域访问图片。git
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确实执行了这两张图片的渲染逻辑。github
查了查资料发现canvas只能按照渲染的前后顺序来展现绘图的层级关系,没法手动指定层级,所以咱们想要在背景图上方绘制图片的话,必需要等到背景图绘制完毕以后才能继续执行其余的逻辑。web
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%
的东西让我很头疼。typescript
尝试了各类办法都失败以后,上网查了查资料才发现原来context
还有保存和还原方法,再配合clip
裁切就能够完成一个圆形图片了!canvas
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
在头像图片的位置绘制一个圆形,而后裁切掉多余的部分,接着绘制头像,最后再恢复画布状态便可。后端
以上步骤就进行完以后,我测试了一下绘制图片而且上传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...
}
复制代码
这样测试了一下全部图片均可以正常被加载出来了~
上面问题解决了以后,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