項目需求寫完有一段時間了,可是仍是想回過來總結一下,一是對項目的回顧優化等,二是對坑的地方作個記錄,避免之後遇到相似的問題。html
利用微信強大的社交能力經過小程序達到裂變的目的,拉取新用戶。
生成的海報以下前端
一、利用小程序官方提供的api能夠直接分享轉發到微信羣打開小程序
二、利用小程序生成海報保存圖片到相冊分享到朋友圈,用戶長按識別二維碼關注公衆號或者打開小程序來達到裂變的目的canvas
相信你們應該都會有相似的迷惑,就是如何按照產品設計的那樣繪製成海報,其實當時我也是不知道如何下手,認真想了下得經過canvas繪製成圖片,這樣用戶保存這個圖片到相冊,就能夠分享到朋友圈了。可是要繪製的圖片上面不只有文字還有數字、圖片、二維碼等且都是活的,這個要怎麼動態生成呢。認真想了下,須要一點一點的將文字和數字,背景圖繪製到畫布上去,這樣經過api最終合成一個圖片導出到手機相冊中。小程序
一、二維碼的動態獲取和繪製(包括如何生成小程序二維碼、公衆號二維碼、打開網頁二維碼)
二、背景圖如何繪製,獲取圖片信息
三、將繪製完成的圖片保存到本地相冊
四、處理用戶是否取消受權保存到相冊後端
這裏我具體寫下圍繞上面所提出的問題,描述大概實現的過程api
①首先建立canvas畫布,我把畫布定位設成負的,是爲了避免讓它顯示在頁面上,是由於我嘗試把canvas經過判斷條件動態的顯示和隱藏,在繪製的時候會出現問題,因此採用了這種方法,這裏還有必定要設置畫布的大小。promise
<canvas canvas-id="myCanvas" style="width: 690px;height:1085px;position: fixed;top: -10000px;"></canvas>
②建立好畫布以後,先繪製背景圖,由於背景圖我是放在本地,因此獲取 <canvas> 組件 canvas-id 屬性,經過createCanvasContext建立canvas的繪圖上下文 CanvasContext 對象。使用drawImage繪製圖像到畫布,第一個參數是圖片的本地地址,後面兩個參數是圖像相對畫布左上角位置的x軸和y軸,最後兩個參數是設置圖像的寬高。微信
const ctx = wx.createCanvasContext('myCanvas') ctx.drawImage('/img/study/shareimg.png', 0, 0, 690, 1085)
③建立好背景圖後,在背景圖上繪製頭像,文字和數字。經過getImageInfo獲取頭像的信息,這裏須要注意下在獲取的網絡圖片要先配置download域名才能生效,具體在小程序後臺設置裏配置。網絡
獲取頭像地址,首先量取頭像在畫布中的大小,和x軸Y軸的座標,這裏的result[0]是我用promise封裝返回的一個圖片地址微信開發
let headImg = new Promise(function (resolve) { wx.getImageInfo({ src: `${app.globalData.baseUrl2}${that.data.currentChildren.headImg}`, success: function (res) { resolve(res.path) }, fail: function (err) { console.log(err) wx.showToast({ title: '網絡錯誤請重試', icon: 'loading' }) } }) }) let avatarurl_width = 60, //繪製的頭像寬度 avatarurl_heigth = 60, //繪製的頭像高度 avatarurl_x = 28, //繪製的頭像在畫布上的位置 avatarurl_y = 36; //繪製的頭像在畫布上的位置 ctx.save(); // 先保存狀態 已便於畫完圓再用 ctx.beginPath(); //開始繪製 //先畫個圓 前兩個參數肯定了圓心 (x,y) 座標 第三個參數是圓的半徑 四參數是繪圖方向 默認是false,即順時針 ctx.arc(avatarurl_width / 2 + avatarurl_x, avatarurl_heigth / 2 + avatarurl_y, avatarurl_width / 2, 0, Math.PI * 2, false); ctx.clip(); //畫了圓 再剪切 原始畫布中剪切任意形狀和尺寸。一旦剪切了某個區域,則全部以後的繪圖都會被限制在被剪切的區域內 ctx.drawImage(result[0], avatarurl_x, avatarurl_y, avatarurl_width, avatarurl_heigth); // 推動去圖片
這裏舉個例子說下如何繪製文字,好比我要繪製以下這個「字」,須要動態獲取前面字數的總寬度,這樣才能設置「字」的x軸座標,這裏我原本是想經過 measureText來測量字體的寬度,可是在iOS端第一次獲取的寬度值不對,關於這個問題,我還在微信開發者社區提了 bug,因此我想用另外一個方法來實現,就是先獲取正常狀況下一個字的寬度值,而後乘以總字數就得到了總寬度,親試是能夠的。
let allReading = 97 / 6 / app.globalData.ratio * wordNumber.toString().length + 325; ctx.font = 'normal normal 30px sans-serif'; ctx.setFillStyle('#ffffff') ctx.fillText('字', allReading, 150);
④繪製公衆號二維碼,和獲取頭像是同樣的,也是先經過接口返回圖片網絡地址,而後再經過getImageInfo獲取公衆號二維碼圖片信息
⑤如何繪製小程序碼,具體官網文檔也給出生成無限小程序碼接口,經過生成的小程序能夠打開任意一個小程序頁面,而且二維碼永久有效,具體調用哪一個小程序二維碼接口有不一樣的應用場景,具體能夠看下官方文檔怎麼說的,也就是說前端經過傳遞參數調取後端接口返回的小程序碼,而後繪製在畫布上(和上面寫的繪製頭像和公衆號二維碼同樣的)
ctx.drawImage('小程序碼的本地地址', x軸, Y軸, 寬, 高)
⑥最終繪製完把canvas畫布轉成圖片並返回圖片地址
wx.canvasToTempFilePath({ canvasId: 'myCanvas', success: function (res) { canvasToTempFilePath = res.tempFilePath // 返回的圖片地址保存到一個全局變量裏 that.setData({ showShareImg: true }) wx.showToast({ title: '繪製成功', }) }, fail: function () { wx.showToast({ title: '繪製失敗', }) }, complete: function () { wx.hideLoading() wx.hideToast() } })
⑦保存到系統相冊;先判斷用戶是否開啓用戶受權相冊,處理不一樣狀況下的結果。好比用戶若是按照正常邏輯受權是沒問題的,可是有的用戶若是點擊了取消受權該如何處理,若是不處理會出現必定的問題。因此當用戶點擊取消受權以後,來個彈框提示,當它再次點擊的時候,主動跳到設置引導用戶去開啓受權,從而達到保存到相冊分享朋友圈的目的。
// 獲取用戶是否開啓用戶受權相冊 if (!openStatus) { wx.openSetting({ success: (result) => { if (result) { if (result.authSetting["scope.writePhotosAlbum"] === true) { openStatus = true; wx.saveImageToPhotosAlbum({ filePath: canvasToTempFilePath, success() { that.setData({ showShareImg: false }) wx.showToast({ title: '圖片保存成功,快去分享到朋友圈吧~', icon: 'none', duration: 2000 }) }, fail() { wx.showToast({ title: '保存失敗', icon: 'none' }) } }) } } }, fail: () => { }, complete: () => { } }); } else { wx.getSetting({ success(res) { // 若是沒有則獲取受權 if (!res.authSetting['scope.writePhotosAlbum']) { wx.authorize({ scope: 'scope.writePhotosAlbum', success() { openStatus = true wx.saveImageToPhotosAlbum({ filePath: canvasToTempFilePath, success() { that.setData({ showShareImg: false }) wx.showToast({ title: '圖片保存成功,快去分享到朋友圈吧~', icon: 'none', duration: 2000 }) }, fail() { wx.showToast({ title: '保存失敗', icon: 'none' }) } }) }, fail() { // 若是用戶拒絕過或沒有受權,則再次打開受權窗口 openStatus = false console.log('請設置容許訪問相冊') wx.showToast({ title: '請設置容許訪問相冊', icon: 'none' }) } }) } else { // 有則直接保存 openStatus = true wx.saveImageToPhotosAlbum({ filePath: canvasToTempFilePath, success() { that.setData({ showShareImg: false }) wx.showToast({ title: '圖片保存成功,快去分享到朋友圈吧~', icon: 'none', duration: 2000 }) }, fail() { wx.showToast({ title: '保存失敗', icon: 'none' }) } }) } }, fail(err) { console.log(err) } }) }
至此全部的步驟都已實現,在繪製的時候會遇到一些異步請求後臺返回的數據,因此我用promise和async和await進行了封裝,確保導出的圖片信息是完整的。在繪製的過程確實遇到一些坑的地方。好比初開始導出的圖片比例大小不對,還有用measureText測量文字寬度不對,屢次繪製(可能受網絡緣由)有時導出的圖片上的文字顏色會有偏差等。若是你也遇到一些比較坑的地方能夠一塊兒探討下作個記錄,下面附下完整的代碼
import regeneratorRuntime from '../../utils/runtime.js' // 引入模塊 const app = getApp(), api = require('../../service/http.js'); var ctx = null, // 建立canvas對象 canvasToTempFilePath = null, // 保存最終生成的導出的圖片地址 openStatus = true; // 聲明一個全局變量判斷是否受權保存到相冊 // 獲取微信公衆號二維碼 getCode: function () { return new Promise(function (resolve, reject) { api.fetch('/wechat/open/getQRCodeNormal', 'GET').then(res => { console.log(res, '獲取微信公衆號二維碼') if (res.code == 200) { console.log(res.content, 'codeUrl') resolve(res.content) } }).catch(err => { console.log(err) }) }) }, // 生成海報 async createCanvasImage() { let that = this; // 點擊生成海報數據埋點 that.setData({ generateId: '點擊生成海報' }) if (!ctx) { let codeUrl = await that.getCode() wx.showLoading({ title: '繪製中...' }) let code = new Promise(function (resolve) { wx.getImageInfo({ src: codeUrl, success: function (res) { resolve(res.path) }, fail: function (err) { console.log(err) wx.showToast({ title: '網絡錯誤請重試', icon: 'loading' }) } }) }) let headImg = new Promise(function (resolve) { wx.getImageInfo({ src: `${app.globalData.baseUrl2}${that.data.currentChildren.headImg}`, success: function (res) { resolve(res.path) }, fail: function (err) { console.log(err) wx.showToast({ title: '網絡錯誤請重試', icon: 'loading' }) } }) }) Promise.all([headImg, code]).then(function (result) { const ctx = wx.createCanvasContext('myCanvas') console.log(ctx, app.globalData.ratio, 'ctx') let canvasWidthPx = 690 * app.globalData.ratio, canvasHeightPx = 1085 * app.globalData.ratio, avatarurl_width = 60, //繪製的頭像寬度 avatarurl_heigth = 60, //繪製的頭像高度 avatarurl_x = 28, //繪製的頭像在畫布上的位置 avatarurl_y = 36, //繪製的頭像在畫布上的位置 codeurl_width = 80, //繪製的二維碼寬度 codeurl_heigth = 80, //繪製的二維碼高度 codeurl_x = 588, //繪製的二維碼在畫布上的位置 codeurl_y = 984, //繪製的二維碼在畫布上的位置 wordNumber = that.data.wordNumber, // 獲取總閱讀字數 // nameWidth = ctx.measureText(that.data.wordNumber).width, // 獲取總閱讀字數的寬度 // allReading = ((nameWidth + 375) - 325) * 2 + 380; // allReading = nameWidth / app.globalData.ratio + 325; allReading = 97 / 6 / app.globalData.ratio * wordNumber.toString().length + 325; console.log(wordNumber, wordNumber.toString().length, allReading, '獲取總閱讀字數的寬度') ctx.drawImage('/img/study/shareimg.png', 0, 0, 690, 1085) ctx.save(); // 先保存狀態 已便於畫完圓再用 ctx.beginPath(); //開始繪製 //先畫個圓 前兩個參數肯定了圓心 (x,y) 座標 第三個參數是圓的半徑 四參數是繪圖方向 默認是false,即順時針 ctx.arc(avatarurl_width / 2 + avatarurl_x, avatarurl_heigth / 2 + avatarurl_y, avatarurl_width / 2, 0, Math.PI * 2, false); ctx.clip(); //畫了圓 再剪切 原始畫布中剪切任意形狀和尺寸。一旦剪切了某個區域,則全部以後的繪圖都會被限制在被剪切的區域內 ctx.drawImage(result[0], avatarurl_x, avatarurl_y, avatarurl_width, avatarurl_heigth); // 推動去圖片 ctx.restore(); //恢復以前保存的繪圖上下文狀態 能夠繼續繪製 ctx.setFillStyle('#ffffff'); // 文字顏色 ctx.setFontSize(28); // 文字字號 ctx.fillText(that.data.currentChildren.name, 103, 78); // 繪製文字 ctx.font = 'normal bold 44px sans-serif'; ctx.setFillStyle('#ffffff'); // 文字顏色 ctx.fillText(wordNumber, 325, 153); // 繪製文字 ctx.font = 'normal normal 30px sans-serif'; ctx.setFillStyle('#ffffff') ctx.fillText('字', allReading, 150); ctx.font = 'normal normal 24px sans-serif'; ctx.setFillStyle('#ffffff'); // 文字顏色 ctx.fillText('戰勝了全國', 26, 190); // 繪製文字 ctx.font = 'normal normal 24px sans-serif'; ctx.setFillStyle('#faed15'); // 文字顏色 ctx.fillText(that.data.percent, 154, 190); // 繪製孩子百分比 ctx.font = 'normal normal 24px sans-serif'; ctx.setFillStyle('#ffffff'); // 文字顏色 ctx.fillText('的小朋友', 205, 190); // 繪製孩子百分比 ctx.font = 'normal bold 32px sans-serif'; ctx.setFillStyle('#333333'); // 文字顏色 ctx.fillText(that.data.singIn, 50, 290); // 簽到天數 ctx.fillText(that.data.reading, 280, 290); // 閱讀時長 ctx.fillText(that.data.reading, 508, 290); // 聽書時長 // 書籍閱讀結構 ctx.font = 'normal normal 28px sans-serif'; ctx.setFillStyle('#ffffff'); // 文字顏色 ctx.fillText(that.data.bookInfo[0].count, 260, 510); ctx.fillText(that.data.bookInfo[1].count, 420, 532); ctx.fillText(that.data.bookInfo[2].count, 520, 594); ctx.fillText(that.data.bookInfo[3].count, 515, 710); ctx.fillText(that.data.bookInfo[4].count, 492, 828); ctx.fillText(that.data.bookInfo[5].count, 348, 858); ctx.fillText(that.data.bookInfo[6].count, 212, 828); ctx.fillText(that.data.bookInfo[7].count, 148, 726); ctx.fillText(that.data.bookInfo[8].count, 158, 600); ctx.font = 'normal normal 18px sans-serif'; ctx.setFillStyle('#ffffff'); // 文字顏色 ctx.fillText(that.data.bookInfo[0].name, 232, 530); ctx.fillText(that.data.bookInfo[1].name, 394, 552); ctx.fillText(that.data.bookInfo[2].name, 496, 614); ctx.fillText(that.data.bookInfo[3].name, 490, 730); ctx.fillText(that.data.bookInfo[4].name, 466, 850); ctx.fillText(that.data.bookInfo[5].name, 323, 878); ctx.fillText(that.data.bookInfo[6].name, 184, 850); ctx.fillText(that.data.bookInfo[7].name, 117, 746); ctx.fillText(that.data.bookInfo[8].name, 130, 621); ctx.drawImage(result[1], codeurl_x, codeurl_y, codeurl_width, codeurl_heigth); // 繪製頭像 ctx.draw(false, function () { // canvas畫布轉成圖片並返回圖片地址 wx.canvasToTempFilePath({ canvasId: 'myCanvas', success: function (res) { canvasToTempFilePath = res.tempFilePath that.setData({ showShareImg: true }) console.log(res.tempFilePath, 'canvasToTempFilePath') wx.showToast({ title: '繪製成功', }) }, fail: function () { wx.showToast({ title: '繪製失敗', }) }, complete: function () { wx.hideLoading() wx.hideToast() } }) }) }) } }, // 保存到系統相冊 saveShareImg: function () { let that = this; // 數據埋點點擊保存學情海報 that.setData({ saveId: '保存學情海報' }) // 獲取用戶是否開啓用戶受權相冊 if (!openStatus) { wx.openSetting({ success: (result) => { if (result) { if (result.authSetting["scope.writePhotosAlbum"] === true) { openStatus = true; wx.saveImageToPhotosAlbum({ filePath: canvasToTempFilePath, success() { that.setData({ showShareImg: false }) wx.showToast({ title: '圖片保存成功,快去分享到朋友圈吧~', icon: 'none', duration: 2000 }) }, fail() { wx.showToast({ title: '保存失敗', icon: 'none' }) } }) } } }, fail: () => { }, complete: () => { } }); } else { wx.getSetting({ success(res) { // 若是沒有則獲取受權 if (!res.authSetting['scope.writePhotosAlbum']) { wx.authorize({ scope: 'scope.writePhotosAlbum', success() { openStatus = true wx.saveImageToPhotosAlbum({ filePath: canvasToTempFilePath, success() { that.setData({ showShareImg: false }) wx.showToast({ title: '圖片保存成功,快去分享到朋友圈吧~', icon: 'none', duration: 2000 }) }, fail() { wx.showToast({ title: '保存失敗', icon: 'none' }) } }) }, fail() { // 若是用戶拒絕過或沒有受權,則再次打開受權窗口 openStatus = false console.log('請設置容許訪問相冊') wx.showToast({ title: '請設置容許訪問相冊', icon: 'none' }) } }) } else { // 有則直接保存 openStatus = true wx.saveImageToPhotosAlbum({ filePath: canvasToTempFilePath, success() { that.setData({ showShareImg: false }) wx.showToast({ title: '圖片保存成功,快去分享到朋友圈吧~', icon: 'none', duration: 2000 }) }, fail() { wx.showToast({ title: '保存失敗', icon: 'none' }) } }) } }, fail(err) { console.log(err) } }) } },