最近作了移動端生成圖片而且上傳的需求,踩了很多坑,這裏記錄一下。因爲本次使用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