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


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 相关文章推荐
angularJS中$apply()方法详解
Jan 07 Javascript
javascript html5实现表单验证
Mar 01 Javascript
JavaScript驾驭网页-DOM
Mar 24 Javascript
jQuery实现图片向左向右切换效果的简单实例
May 18 Javascript
jQuery插件Easyui设置datagrid的pageNumber导致两次请求问题的解决方法
Aug 06 Javascript
jquery操作ID带有变量的节点实例
Dec 07 Javascript
使用jQuery,Angular实现登录界面验证码详解
Apr 27 jQuery
angularjs实现搜索的关键字在正文中高亮出来
Jun 13 Javascript
jquery图片放大镜效果
Jun 23 jQuery
微信小程序实现下载进度条的方法
Dec 08 Javascript
小程序扫描普通链接二维码跳转小程序指定界面方法
May 07 Javascript
node.js使用zlib模块进行数据压缩和解压操作示例
Feb 12 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
thinkPHP实现瀑布流的方法
2014/11/29 PHP
PHP生成静态HTML页面最简单方法示例
2015/04/09 PHP
Zend Framework基于Command命令行建立ZF项目的方法
2017/02/18 PHP
Laravel 自动生成验证的实例讲解:login / logout
2019/10/14 PHP
jQuery代码优化 选择符篇
2011/11/01 Javascript
jQuery操作 input type=checkbox的实现代码
2012/06/14 Javascript
使用js修改客户端注册表的方法
2013/08/09 Javascript
js 动态为textbox添加下拉框数据源的方法
2014/04/24 Javascript
javascript实现table选中的行以指定颜色高亮显示的方法
2015/05/13 Javascript
无需 Flash 使用 jQuery 复制文字到剪贴板
2016/04/26 Javascript
在微信、支付宝、百度钱包实现点击返回按钮关闭当前页面和窗口的方法
2016/08/05 Javascript
js获取元素的标签名实现方法
2016/10/08 Javascript
详解使用grunt完成requirejs的合并压缩和js文件的版本控制
2017/03/02 Javascript
javascript遍历json对象的key和任意js对象属性实例
2017/03/09 Javascript
vue.js利用Object.defineProperty实现双向绑定
2017/03/09 Javascript
Angular 4.x 动态创建表单实例
2017/04/25 Javascript
Node.js 使用流实现读写同步边读边写功能
2017/09/11 Javascript
微信小程序swiper组件用法实例分析【附源码下载】
2017/12/07 Javascript
原生JS实现的雪花飘落动画效果
2018/05/03 Javascript
每天学点Vue源码之vm.$mount挂载函数
2019/03/11 Javascript
如何利用Node.js与JSON搭建简单的动态服务器
2020/06/16 Javascript
vue+canvas实现拼图小游戏
2020/09/18 Javascript
Vue中ref和$refs的介绍以及使用方法示例
2021/01/11 Vue.js
初学Python实用技巧两则
2014/08/29 Python
python框架django基础指南
2016/09/08 Python
对Python使用mfcc的两种方式详解
2019/01/09 Python
Python中url标签使用知识点总结
2020/01/16 Python
详解Python多线程下的list
2020/07/03 Python
如何将anaconda安装配置的mmdetection环境离线拷贝到另一台电脑
2020/10/15 Python
详解使用python爬取抖音app视频(appium可以操控手机)
2021/01/26 Python
Vector, ArrayList, HashTable, HashMap哪些是线程安全的,哪些不是
2015/10/12 面试题
浙大毕业生自荐信
2014/01/26 职场文书
廉洁自律承诺书
2014/03/27 职场文书
公司更名通知函
2015/04/24 职场文书
学长教您写论文:经验总结
2019/07/09 职场文书
利用python做数据拟合详情
2021/11/17 Python