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


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 相关文章推荐
原生javascript实现无间缝滚动示例
Jan 28 Javascript
纯js模拟div层弹性运动的方法
Jul 27 Javascript
JavaScript事件类型中UI事件详解
Jan 14 Javascript
Jquery针对tr td的一些实用操作方法(必看篇)
Oct 05 Javascript
JS类的定义与使用方法深入探索
Nov 26 Javascript
JS前端笔试题分析
Dec 19 Javascript
js调用刷新界面的几种方式
May 03 Javascript
JavaScript时间与时间戳的转换操作实例分析
Dec 07 Javascript
jQuery中DOM操作原则实例分析
Aug 01 jQuery
vue render函数动态加载img的src路径操作
Oct 26 Javascript
vue router-link 默认a标签去除下划线的实现
Nov 06 Javascript
vue实现滑动解锁功能
Mar 03 Vue.js
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/08/20 PHP
PHP的几个常用加密函数
2016/02/03 PHP
PHP实现创建微信自定义菜单的方法示例
2017/07/14 PHP
基于Laravel 多个中间件的执行顺序详解
2019/10/21 PHP
用javascript实现兼容IE7的类库 IE7_0_9.zip提供下载
2007/08/08 Javascript
学习ExtJS Panel常用方法
2009/10/07 Javascript
如何用js控制frame的隐藏或显示的解决办法
2013/03/20 Javascript
JS实现日期加减的方法
2013/11/29 Javascript
jquery中show()、hide()和toggle()用法实例
2015/01/15 Javascript
jQuery实现的动态伸缩导航菜单实例
2015/05/07 Javascript
原生javascript实现分享到朋友圈功能 支持ios和android
2016/05/11 Javascript
JavaScript代码里的判断小结
2016/08/22 Javascript
jquery插件锦集【推荐】
2016/12/16 Javascript
微信小程序开发图片拖拽实例详解
2017/05/05 Javascript
前端页面文件拖拽上传模块js代码示例
2017/05/19 Javascript
深入讲解xhr(XMLHttpRequest)/jsonp请求之abort
2017/07/26 Javascript
使用vue-cli脚手架工具搭建vue-webpack项目
2019/01/14 Javascript
详解JavaScript原型与原型链
2020/11/16 Javascript
python开发之基于thread线程搜索本地文件的方法
2015/11/11 Python
分析Python中解析构建数据知识
2018/01/20 Python
python 画出使用分类器得到的决策边界
2019/08/21 Python
Django数据库操作之save与update的使用
2020/04/01 Python
如何验证python安装成功
2020/07/06 Python
python和C++共享内存传输图像的示例
2020/10/27 Python
Miller Harris官网:英国小众香水品牌
2020/09/24 全球购物
汽车制造与装配专业自荐信范文
2014/01/02 职场文书
《草原的早晨》教学反思
2014/04/08 职场文书
禁止高声喧哗的标语
2014/06/11 职场文书
政法干警核心价值观心得体会
2014/09/11 职场文书
工程承包协议书范本
2014/09/29 职场文书
班子群众路线教育实践个人对照检查材料思想汇报
2014/09/30 职场文书
学生考试舞弊检讨书
2015/01/01 职场文书
董事长助理岗位职责
2015/02/11 职场文书
薪资证明范本
2015/06/19 职场文书
2015年暑期社会实践方案
2015/07/14 职场文书
导游词之千岛湖
2019/09/23 职场文书