1. 前言
生成海报是小程序一项寻常普遍的低成本推广方式,在小程序中通过引导用户生成带有小程序二维码的海报发上票圈,来吸引更多的流量。
2. 需求分析
在与朋友圈类似的 带有文字描述 和 最多带有9张图片的 列表中,实现
1/ 使用云开发,生成二维码
2/ 点击图片,会根据图片的标签绘制对应模板的海报,
3/ 将生成的海报图片进行预览,并提供“保存到本地", 增加用户拒绝授权保存图片到本地相册后的处理
4/ 增加了一键保存的功能:即一键将某个人发的多张朋友圈图片,按照顺序绘制成海报图片保存到本地, 并实时显示绘制进度
3. 实现步骤
3.1 抽象封装canvas绘制组件
弱鸡的我一开始是drawImg, fillText, drawRect 一个接口一个接口分别去绘制图像,文字,矩形,但是扛不住需求变更的速度啊。在改了n个样式之后,卑微的我终于!!!决定把canvas绘制的逻辑抽取出来。
canvas绘制,当然离不开canvas页面标签,所以我们不如把它封装成一个组件 canvasdrawer,这个组件从外部接收一个Json,当接收到新的json时,开始绘制,json中每个item添加一个type,标志它是图像 / 文字 / 矩形,然后调用相应的封装好的绘制方法。
需要注意的是, 当绘制的图像是网路图片时,需要先拿到图片的临时路径才能来进行绘制。当海报参数中存在多张网络图片时,使用Promise.all 异步获取图片临时路径。
绘制完之后,调用 canvasToTempFile 生成临时图片链接,我们可以 triggerEvent 通知父组件该海报已绘制完成,并返回生成的海报的图片临时链接,用于实现预览或者本地保存
3.1.1 使用监听observer监听传入的json 是否改变,若改变了再进行绘制
properties: { // 传入的json painting: { type: Object, value: { view: [] }, // 监听传入的json 对象是否发生改变,决定是否需要重新绘制 observer(newVal, oldVal) { // 判断当前有没有在进行绘制的对象 if (!this.data.isPainting) { // 将对象转换成字符串再进行比较 if (JSON.stringify(newVal) !== JSON.stringify(oldVal)) { // 宽、高为必须的 if (newVal && newVal.width && newVal.height) { this.setData({ showCanvas: true, isPainting: true }) this.readyPigment() } } else { // 如果对象没有发生改变,但是模式又不是“same”模式的话,就出触发事件 if (newVal && newVal.mode !== 'same') { this.triggerEvent('getImage', { errMsg: 'canvasdrawer:samme params' }) } } } } }},
3.1.2 获取图片列表
// 获取图片列表getImagesInfo(views) { // 1.将获取到的每张图片信息推送到imagelist中 const imageList = [] for (let i = 0; i views.length; i++) { if (views[i].type === 'image') { imageList.push(this.getImageInfo(views[i].url)) } } // 2.临时图片下载,使用promise.all 异步下载,每8张一个异步task const loadTask = [] for (let i = 0; i Math.ceil(imageList.length / 8); i++) { loadTask.push(new Promise((resolve, reject) = { Promise.all(imageList.splice(i * 8, 8)).then(res = { resolve(res) }).catch(res = { reject(res) }) })) } // promise.all返回的res为多个promise返回的数组拼接成的数组,有所有的task返回 Promise.all(loadTask).then(res = { let tempFileList = [] for (let i = 0; i res.length; i++) { tempFileList = tempFileList.concat(res[i]) } this.setData({ tempFileList }) this.startPainting() })},
3.1.3 若是网络图片,先拿到图片的临时链接
// 获取图片信息getImageInfo(url) { return new Promise((resolve, reject) = { // 如果图片有缓存,则从缓存中读取 if (this.cache[url]) { resolve(this.cache[url]) } else { // 匹配图片链接地址是否匹配 const objExp = new RegExp(/^http(s)?://([w-]+.)+[w-]+(/[w- ./?%&=]*)?/) if (objExp.test(url)) { wx.getImageInfo({ src: url, complete: res = { if (res.errMsg === 'getImageInfo:ok') { this.cache[url] = res.path resolve(res.path) } else { this.triggerEvent('getImage', { errMsg: 'canvasdrawer:download fail' }) reject(new Error('getImageInfo fail')) } } }) } else { this.cache[url] = url resolve(url) } } })},
3.1.4 绘制完成后,使用wx.canvasToTempFilePath 将canvas转化成临时图片链接,并通知给父级组件
// 将图片保存到本地saveImageToLocal() { const { width, height } = this.data wx.canvasToTempFilePath({ x: 0, y: 0, width, height, canvasId: this.data.canvasId, complete: res = { if (res.errMsg === 'canvasToTempFilePath:ok') { this.setData({ showCanvas: false, isPainting: false, tempFileList: [] }) this.triggerEvent('getImage', { tempFilePath: res.tempFilePath, errMsg: 'canvasdrawer:ok' }) } else { this.triggerEvent('getImage', { errMsg: 'canvasdrawer:fail' }) } } }, this)}
3.2 获取海报参数
对一个有灵魂的海报来说 二维码 是必不可少的,其中,二维码的生成可通过两种方式实现:
1. 需要后台的协助:传输给后台相应的参数,由后台调官方接口去生成二维码,再返回给前端
2. 前端自给自足: 小程序官方增加了云开发,我们可以通过新增云函数中,在云函数中传入参数,生成二维码
注意:生成的二维码是buffer data,需要将其转化为图片链接
/* 获取云开发的小程序码 */const getQrCode = (path, scene, dataType) = { return new Promise((resolve, reject) = { let param = { page: path, scene: scene || '', width: 350 } if (!path) { reject('must have path') return } if (scene && scene.length 32) { reject('scene' length must less than 32') return } console.log('小程序码~云参数:', param) wx.cloud.callFunction({ name: 'getNewQrCode', data: param }).then(res = { let data if (res.result.errCode == 0) { if (dataType == 'base64') { data = `data:image/jpeg;base64,${wx.arrayBufferToBase64(res.result.buffer)}` } if (dataType == 'buffer') { data = res.result.buffer } if (data) { resolve(data) } else { reject('unknow data type') } } else { reject(res.result.errMsg) } }).catch(err = { reject(err) }) })}为了使小程序当前生命周期结束时,二维码仍然有效,我们可以调用小程序提供给我们的在用户存储目录写入文件的权限,将生成的二维码图片保存到用户存储目录中(wx.env.USER_DATA_PATH)
/* 图片临时缓存 */const localQrImageUrl = (buffer) = { return new Promise((resolve, reject) = { const TMP_IMG_NAME = 'TMP_QR_CODE_'; // 添加时间戳使之前生成的二维码图片不被覆盖 const TIME_STAMP = new Date().getTime().toString().substr(-8); const filePath = `${wx.env.USER_DATA_PATH}/${TMP_IMG_NAME}${TIME_STAMP}.jpg`; // const filePath = `${wx.env.USER_DATA_PATH}/${TMP_IMG_NAME}.jpg`; // console.log('临时存储路径:', filePath) wx.getFileSystemManager().writeFile({ filePath, data: buffer, encoding: 'binary', success() { // console.log('临时存储路径保存好了:', filePath) resolve(filePath) }, fail() { wx.getFileSystemManager().rmdir({ dirPath: `${wx.env.USER_DATA_PATH}/`, success() { wx.showModal({ title: "温馨提示", content: "本地内存不足,已删除该小程序清除本地缓存,请再次点击生成海报" }); reject(new Error("已清除本地缓存,请再次点击生成海报")); }, fail() { console.log("删除本地缓存失败"); // console.log('临时存储路径保存错了') reject(new Error("ERROR_BASE64SRC_WRITE")); } }); } }) })}
3.3 动态改变海报模板的某些特定参数
比如说多个商品都用到了同一个海报模板,即图片,文字和形状的大小,位置都是一致的,但是图片的链接地址url, 文字的内容text 可能不一样,这个时候我们可以通过一个reqParams 请求参数去动态改变模板;
我们在定义海报模板时,给需要变更内容的字段添加上一个标识。然后在 reqParams 请求参数中携带 { 标识 : 更改后的值},即可实现变更。
/** * 动态改变海报模板的参数 * 拼接示例:{width:, height:, desc:value} */changePosterParams() { let descList = Object.keys(this.reqParam); descList.forEach(desc={ let newTarget = this.reqParam[desc]; if(desc == 'width'){ this.poster['width'] = newTarget; }else if(desc == 'height'){ this.poster['height'] = newTarget; }else{ let target = this.getItemByKey(this.poster['views'], desc); if(target && target.url){ target.url = newTarget; } else if(target && target.content){ target.content = newTarget; } } })}/** * 在数组中获取到键desc的值为某一个值的对象 * 注意:需确保键值只能唯一,否则匹配到一个最后一个匹配到的项,即后面的会覆盖前面的 * @param {Array} arr * @param {String} keyValue */getItemByKey(arr,keyValue){ let targetItem=null; arr.forEach(item={ if (item['desc'] == keyValue){ targetItem=item; } }); return targetItem;}
3.4 保存图片到本地相册
当用户授权当然保存没问题,但是当用户拒绝授权怎么办嘞。
小程序现在升级了,再也不能静默再去获取授权了,需要由用户的点击操作才能调起设置页面,引导用户自己打开授权
需注意的是:当没有请求过授权时,是需要先去请求授权,设置页面才会出现该选项的(比如保存到本地相册的权限);
// 保存图片,有处理如果用户不授权的情况Poster.prototype.trySaveImg = function(imgUrl, successCallback) { const _this = this; wx.getSetting({ success(res){ console.log("用户当前开启的权限情况", res.authSetting); // 拒绝过授权,则打开设置页面(设置界面只会出现小程序已经向用户请求过的权限,所以需先向用户请求一次) if(res.authSetting['scope.writePhotosAlbum'] === false){ wx.openSetting({ success(res){ // 如果还是没有授权的话 if(!res.authSetting['scope.writePhotosAlbum']){ wx.showToast({ title: '未开启保存权限', image: '/images/common/err_tip_icon.png', duration: 2000 }) } } }); } // 没有请求过授权或者已授权 else{ if(successCallback){ successCallback(imgUrl); }else{ _this.saveImg(imgUrl); } } } })}当没有请求过授权或者已授权的情况下,就直接调起保存动作
// 保存图片 saveImg(imgUrl) { wx.saveImageToPhotosAlbum({ filePath: imgUrl, success(res) { wx.showToast({ title: '保存图片成功', image: '/images/common/icon_toast_success.jpg', duration: 2000 }) }, fail(err){ console.log(err) wx.showToast({ title: '未开启保存权限', image: '/images/common/err_tip_icon.png', duration: 2000 }) } })}
3.5 一键绘制多张海报
上面我在 3.1.4 步骤中提到生成成功后会通知父级组件,这里我们在父级组件中监听这个生成成功事件,并拿到生成的海报图片,可进行预览并保存在本地
// 当前海报绘制完成, 检测是否还有未完成绘制的海报eventGetImage(event){ const { tempFilePath, errMsg } = event.detail // 绘制成功 if (errMsg === 'canvasdrawer:ok') { this.setData({ shareImage: tempFilePath, isShowCanvas: false // 重新初始化canvas画报为false,等参数拼接完再去绘制下一张 }); this.saveImg(this.data.shareImage); // 保存该张海报图片 } // 当前海报绘制失败,增加失败图片序号到失败队列中,并绘制开始下一张 else{ console.log(errMsg); this.data.activeItemIndex++; this.data.failItemIndexList.push(this.data.activeItemIndex); this.startPainting(); }},检测还有没有未完成的绘制任务
// 检查是否还有绘制任务if(_this.data.activeItemIndex _this.data.totalItemsLen-1){ _this.data.activeItemIndex++; _this.startPainting();}// 绘制最后一张完成else{ _this.setData({ isDoingSaveAll: false }) wx.hideLoading(); // 判断有没有绘制失败的 if(_this.data.failItemIndexList.length 0){ let failItemIndexListString = _this.data.failItemIndexList.join(',') wx.showToast({ title: "第"+failItemIndexListString+"张下载失败", image: '/images/common/err_tip_icon.png', duration: 2000 }); }else{ wx.showToast({ title: "下载成功", image: '/images/common/icon_toast_success.jpg', duration: 2000 }); }}
4. 总结
github仓库完整代码
如果文章对你有帮助,麻烦点赞哦,一起走花路吧~














