微信小程序生成海报分享朋友圈的实现方法


Posted in Javascript onMay 06, 2019

项目需求写完有一段时间了,但是还是想回过来总结一下,一是对项目的回顾优化等,二是对坑的地方做个记录,避免以后遇到类似的问题。

需求

利用微信强大的社交能力通过小程序达到裂变的目的,拉取新用户。

生成的海报如下

微信小程序生成海报分享朋友圈的实现方法

需求分析

1、利用小程序官方提供的api可以直接分享转发到微信群打开小程序

2、利用小程序生成海报保存图片到相册分享到朋友圈,用户长按识别二维码关注公众号或者打开小程序来达到裂变的目的

实现方案

一、分析如何实现

相信大家应该都会有类似的迷惑,就是如何按照产品设计的那样绘制成海报,其实当时我也是不知道如何下手,认真想了下得通过canvas绘制成图片,这样用户保存这个图片到相册,就可以分享到朋友圈了。但是要绘制的图片上面不仅有文字还有数字、图片、二维码等且都是活的,这个要怎么动态生成呢。认真想了下,需要一点一点的将文字和数字,背景图绘制到画布上去,这样通过api最终合成一个图片导出到手机相册中。

二、需要解决的问题

1、二维码的动态获取和绘制(包括如何生成小程序二维码、公众号二维码、打开网页二维码)

2、背景图如何绘制,获取图片信息

3、将绘制完成的图片保存到本地相册

4、处理用户是否取消授权保存到相册

三、实现步骤

这里我具体写下围绕上面所提出的问题,描述大概实现的过程

①首先创建canvas画布,我把画布定位设成负的,是为了不让它显示在页面上,是因为我尝试把canvas通过判断条件动态的显示和隐藏,在绘制的时候会出现问题,所以采用了这种方法,这里还有一定要设置画布的大小。

<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)
  }
  })
 }
 },

总结

以上所述是小编给大家介绍的微信小程序生成海报分享朋友圈的实现方法,希望对大家有所帮助,如果大家有任何疑问欢迎给我留言,小编会及时回复大家的!

Javascript 相关文章推荐
jQuery+JSON+jPlayer实现QQ空间音乐查询功能示例
Jun 17 Javascript
深入理解JQuery keyUp和keyDown的区别
Dec 12 Javascript
js创建元素(节点)示例
Jan 02 Javascript
jQuery多项选项卡的实现思路附样式及代码
Jun 03 Javascript
Node.js与PHP、Python的字符处理性能对比
Jul 06 Javascript
利用python分析access日志的方法
Oct 26 Javascript
js使用Replace结合正则替换重复出现的字符串功能示例
Dec 27 Javascript
用angular实现多选按钮的全选与反选实例代码
May 23 Javascript
React-Native中props具体使用详解
Sep 04 Javascript
原生js中ajax访问的实例详解
Sep 19 Javascript
详解使用create-react-app添加css modules、sasss和antd
Jul 31 Javascript
开发Node CLI构建微信小程序脚手架的示例
Mar 27 Javascript
vue项目打包后上传至GitHub并实现github-pages的预览
May 06 #Javascript
Vue实现购物车的全选、单选、显示商品价格代码实例
May 06 #Javascript
JavaScript中关于base64的一些事
May 06 #Javascript
JavaScript中工厂函数与构造函数示例详解
May 06 #Javascript
微信小程序登录数据解密及状态维持实例详解
May 06 #Javascript
一文了解Vue中的nextTick
May 06 #Javascript
angular 服务随记小结
May 06 #Javascript
You might like
超级好用的一个php上传图片类(随机名,缩略图,加水印)
2010/06/30 PHP
php define的第二个参数使用方法
2013/11/04 PHP
PHP生成RSS文件类实例
2014/12/05 PHP
PHP分页初探 一个最简单的PHP分页代码的简单实现
2016/06/21 PHP
JS延迟加载(setTimeout) JS最后加载
2010/07/15 Javascript
JQuery获取文本框中字符长度的代码
2011/09/29 Javascript
jQuery获取和设置表单元素的方法
2014/02/14 Javascript
JavaScript获取某年某月的最后一天附截图
2014/06/23 Javascript
javascript结合Canvas 实现简易的圆形时钟
2015/03/11 Javascript
Javascript中的return作用及javascript return关键字用法详解
2015/11/05 Javascript
创建一个类Person的简单实例
2016/05/17 Javascript
BootStrap Table 分页后重新搜索问题的解决办法
2016/08/08 Javascript
js HTML5多媒体影音播放
2016/10/17 Javascript
值得学习的bootstrap fileinput文件上传工具
2016/11/08 Javascript
bootstrap配合Masonry插件实现瀑布式布局
2017/01/18 Javascript
老生常谈js中0到底是 true 还是 false
2017/03/08 Javascript
React-Native使用Mobx实现购物车功能
2017/09/14 Javascript
将jquery.qqFace.js表情转换成微信的字符码
2017/12/01 jQuery
原生JS实现的碰撞检测功能示例
2018/05/18 Javascript
详解搭建es6+devServer简单开发环境
2018/09/25 Javascript
vue项目打包之后背景样式丢失的解决方案
2019/01/17 Javascript
vue自定义组件(通过Vue.use()来使用)即install的用法说明
2020/08/11 Javascript
Python素数检测的方法
2015/05/11 Python
Python的dict字典结构操作方法学习笔记
2016/05/07 Python
PyCharm安装第三方库如Requests的图文教程
2018/05/18 Python
pytorch:torch.mm()和torch.matmul()的使用
2019/12/27 Python
在Python中使用K-Means聚类和PCA主成分分析进行图像压缩
2020/04/10 Python
Python 实现一个简单的web服务器
2021/01/03 Python
简述Html5 IphoneX 适配方法
2018/02/08 HTML / CSS
外企测试工程师面试题
2015/02/01 面试题
敬老文明号事迹材料
2014/01/16 职场文书
自荐信格式简述
2014/01/25 职场文书
爱心捐款倡议书范文
2014/05/12 职场文书
年会邀请函范文
2015/01/30 职场文书
python flask框架快速入门
2021/05/14 Python
MySql中的json_extract函数处理json字段详情
2022/06/05 MySQL