- 小白的小程序二维码canvas绘制之旅--node.js
要做一个分享到朋友圈的功能,但是微信没有相关的接口,只能通过将相关页面生成小程序码,然后生成图片,让用户保存到相册,再分享到朋友圈。于是,经过三天的折腾终于搞出来了,虽然不知道是不是正确的方法,还是分享出来,给同样没有经验的童鞋作参考。 首先是遇到的一些坑: 1. 开始的时候直接在前端调用接口,但是发现 api.weixin.qq.com 添加到请求域名,因此只能从后端获取。 2. 后端的请求我不会,T^T,这个确实是我自己太菜了,我用的 node.js,搜索发现使用 var request = require('request') 就可以了。 3. 后端请求的时候,因为这是异步操作,导致微信接口还没有返回结果,我这边接口就已经返回小程序端了,这里需要 promise 搭配 await 来实现进程阻塞。 4. 获取到token之后,使用生成二维码的接口 getwxacodeunlimit,得到返回值是一堆乱码,我以为这是正常的数据,于是开始想办法处理这些数据。网上搜到的是用 pipe 和 fs 存到服务器,在从前端把图片从服务器下载下来,在canvas画,但是我用的腾讯云小程序方案,根本不知道图片存到了哪里,也不知道下载链接。所以我就尝试将乱码发送到前端,用 arrayBufferToBase64 处理,发现根本没有输出。。。最后折腾了好久,发现是最开始获取的时候,接受数据的编码格式不对,在 request 的请求参数 opts 里添加一句 :encoding: null ,就可以正确接受,结果是一个buffer变量,里面是多维数字数组。 5. 得到数据之后,我以为只要能在前端显示,就能在canvas里画了,于是就把buffer发送到前端,然后用arrayBufferToBase64 处理,再加上 "data:image/png;base64," 头,就可以作为 image 标签的 src 参数进行显示了,然而,这种 base64 图片,在开发工具上可以画在 canvas里,真机上却不行。官方不支持。。。。所以饶了一圈,又得回到原点。最后还是通过在后端,将得到的buffer上传到cos对象存储,再将存储地址返回到前端,前端通过wx.downloadFile将图片下载下来,再画到 canvas 上。 然后是一些要注意的点: 1. 真机获取二维码只能通过后端实现。 2. canvas画图是异步操作,需要延时后执行 canvasToTempFilePath 3. 除了B接口,另外两个是有个数限制的,要小心调用,用完就没了。 4. token调用也是有次数限制的,需要做处理。 5. canvas大小需要自己手动根据屏幕尺寸进行调整。 6. 我用的wafer 2 ,node.js后端,cos存储 7. 生成的二维码进入的是已经发布的版本,我的是已经有一个发布的版本的。 最后是代码(水平有限): 后端,放在server/controllers/getqrcode.js: [代码]const { mysql } = require([代码][代码]'../qcloud'[代码][代码])[代码][代码]//腾讯云对象存储接口[代码][代码]const COS = require([代码][代码]'cos-nodejs-sdk-v5'[代码][代码])[代码][代码]// 获取基础配置[代码][代码]const configs = require([代码][代码]'../config'[代码][代码])[代码][代码]//请求包[代码][代码]var[代码] [代码]request = require([代码][代码]'request'[代码][代码])[代码] [代码]// token有效时间[代码][代码]var[代码] [代码]period = 7200000; [代码][代码]//小程序开发信息[代码][代码]const APPID = [代码][代码]'wxxxxxxxxxxxxxxxxxx'[代码][代码]const APPSECRET = [代码][代码]'b3dxxxxxxxxxxxxxxxxxxxxxx'[代码] [代码]module.exports = async ctx => {[代码][代码] [代码][代码]//小程序传入的参数,path是页面路径放到page里,pptid是参数,放到scene里的[代码][代码] [代码][代码]const pptid = ctx.query.pptid[代码][代码] [代码][代码]const path = ctx.query.path[代码][代码] [代码][代码]let result = [代码][代码]''[代码][代码] [代码][代码]let msg = [代码][代码]'init'[代码][代码] [代码][代码]let id = 1[代码][代码] [代码][代码]let token =[代码][代码]'none'[代码][代码] [代码][代码]var[代码] [代码]urlpath = [代码][代码]'none'[代码][代码] [代码][代码]console.log([代码][代码]'getqrcode\r\n'[代码][代码])[代码][代码] [代码][代码]const getAccessToken = [代码][代码]function[代码] [代码]() {[代码][代码] [代码][代码]//获取token[代码][代码] [代码][代码]var[代码] [代码]opts = {[代码][代码] [代码][代码]method: [代码][代码]'GET'[代码][代码],[代码][代码] [代码][代码]url: [代码][代码]'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid='[代码] [代码]+ APPID + [代码][代码]'&secret='[代码] [代码]+ APPSECRET,[代码][代码] [代码][代码]header: {[代码][代码] [代码][代码]'content-type'[代码][代码]: [代码][代码]'application/json'[代码] [代码]// 默认值[代码][代码] [代码][代码]}[代码][代码] [代码][代码]}[代码][代码] [代码][代码]console.log([代码][代码]'do get AccessToken!\r\n'[代码][代码])[代码][代码] [代码][代码]return[代码] [代码]new[代码] [代码]Promise((resolve, reject) => {[代码][代码] [代码][代码]request.get(opts, [代码][代码]function[代码] [代码](err, response, body) {[代码][代码] [代码][代码]if[代码] [代码](!err && response.statusCode == 200) {[代码][代码] [代码][代码]if[代码] [代码](body !== [代码][代码]'null'[代码][代码]) {[代码][代码] [代码][代码]results = body;[代码][代码] [代码][代码]code = JSON.parse(results);[代码][代码] [代码][代码]if[代码] [代码](code.access_token != [代码][代码]null[代码][代码]) {[代码][代码] [代码][代码]let accessToken = code.access_token[代码][代码] [代码][代码]resolve(accessToken)[代码][代码] [代码][代码]} [代码][代码]else[代码] [代码]{[代码][代码] [代码][代码]msg = [代码][代码]'joberror'[代码][代码] [代码][代码]console.log([代码][代码]'job error:'[代码] [代码]+ results);[代码][代码] [代码][代码]reject(results)[代码][代码] [代码][代码]}[代码][代码] [代码][代码]} [代码][代码]else[代码] [代码]{[代码][代码] [代码][代码]msg = [代码][代码]'getfail'[代码][代码] [代码][代码]reject(msg)[代码][代码] [代码][代码]}[代码][代码] [代码][代码]} [代码][代码]else[代码] [代码]{[代码][代码] [代码][代码]msg = [代码][代码]'netfail'[代码][代码] [代码][代码]reject(err)[代码][代码] [代码][代码]}[代码][代码] [代码][代码]});[代码][代码] [代码][代码]})[代码][代码] [代码][代码]};[代码][代码] [代码][代码]const getqrc = [代码][代码]function[代码] [代码](token) {[代码][代码] [代码][代码]//请求二维码的参数 [代码][代码] [代码][代码]var[代码] [代码]postData = {[代码][代码] [代码][代码]page: path,[代码][代码] [代码][代码]width: 430,[代码][代码] [代码][代码]scene: pptid[代码][代码] [代码][代码]} [代码][代码] [代码][代码]var[代码] [代码]opts = {[代码][代码] [代码][代码]method: [代码][代码]'POST'[代码][代码],[代码][代码] [代码][代码]url: [代码][代码]'https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token='[代码] [代码]+ token,[代码][代码] [代码][代码]body: JSON.stringify(postData),[代码][代码] [代码][代码]encoding:[代码][代码]null[代码][代码],[代码][代码] [代码][代码]// responseType:"arraybuffer",//这个好像没用[代码][代码] [代码][代码]}[代码][代码] [代码][代码]console.log([代码][代码]'do get QRcode!\r\n'[代码][代码])[代码][代码] [代码][代码]return[代码] [代码]new[代码] [代码]Promise((resolve, reject) => { [代码][代码] [代码][代码]request(opts, [代码][代码]function[代码] [代码](err, response, body) {[代码][代码] [代码][代码]if[代码] [代码](!err && response.statusCode == 200) {[代码][代码] [代码][代码]if[代码] [代码](body !== [代码][代码]'null'[代码][代码]) {[代码][代码] [代码][代码]result = body[代码][代码] [代码][代码]// console.log(body)[代码][代码] [代码][代码]msg = [代码][代码]'qrdone'[代码][代码] [代码][代码]resolve(body)[代码][代码] [代码][代码]} [代码][代码]else[代码] [代码]{[代码][代码] [代码][代码]msg = [代码][代码]'qrgetfail'[代码][代码] [代码][代码]reject(msg)[代码][代码] [代码][代码]}[代码][代码] [代码][代码]} [代码][代码]else[代码] [代码]{[代码][代码] [代码][代码]msg = [代码][代码]'qrnetfail'[代码][代码] [代码][代码]reject(err)[代码][代码] [代码][代码]}[代码][代码] [代码][代码]})[代码][代码]//.pipe(fs.createWriteStream('./temp2code.jpg'))[代码] [代码] [代码][代码]})[代码][代码] [代码][代码]};[代码][代码] [代码][代码]const uploadimg=[代码][代码]function[代码][代码](body){[代码][代码] [代码][代码]//上传到cos[代码][代码] [代码][代码]//body就是上一步获取到的body[代码][代码] [代码][代码]//存到cos后自动就生成图片了 [代码][代码] [代码][代码]var[代码] [代码]cos = [代码][代码]new[代码] [代码]COS({[代码][代码] [代码][代码]// 必选参数[代码][代码] [代码][代码]SecretId: configs.cos.SecretId,[代码][代码] [代码][代码]SecretKey: configs.cos.SecretKey,[代码][代码] [代码][代码]// 可选参数[代码][代码] [代码][代码]FileParallelLimit: 3, [代码][代码]// 控制文件上传并发数[代码][代码] [代码][代码]ChunkParallelLimit: 8, [代码][代码]// 控制单个文件下分片上传并发数,在同园区上传可以设置较大的并发数[代码][代码] [代码][代码]ChunkSize: 1024 * 1024, [代码][代码]// 控制分片大小,单位 B,在同园区上传可以设置较大的分片大小[代码][代码] [代码][代码]});[代码][代码] [代码][代码]return[代码] [代码]new[代码] [代码]Promise((resolve, reject) => { [代码][代码] [代码][代码]cos.putObject({[代码][代码] [代码][代码]Bucket: configs.cos.Bucket, [代码][代码]// Bucket 格式:test-1250000000[代码][代码] [代码][代码]Region: configs.cos.region,[代码][代码] [代码][代码]Key: [代码][代码]'qrcode-'[代码][代码]+pptid+[代码][代码]'.jpg'[代码][代码], [代码][代码]/* 给文件起个名字 */[代码][代码] [代码][代码]Body: body, [代码][代码]/* 必须 */[代码][代码] [代码][代码]}, [代码][代码]function[代码] [代码](err, data) {[代码][代码] [代码][代码]if[代码] [代码](err) {[代码][代码] [代码][代码]reject(err)[代码][代码] [代码][代码]console.log(err);[代码][代码] [代码][代码]} [代码][代码]else[代码] [代码]{[代码][代码] [代码][代码]resolve(data)[代码][代码] [代码][代码]// console.log(data);[代码][代码] [代码][代码]}[代码][代码] [代码][代码]});[代码][代码] [代码][代码]})[代码][代码] [代码][代码]}[代码][代码] [代码][代码]//查询数据库中的token是否需要更新[代码][代码] [代码][代码]let res = await mysql([代码][代码]"systemSet"[代码][代码]).where({ id }).first()[代码][代码] [代码][代码]console.log([代码][代码]'sql updated on:'[代码] [代码]+ res.updatetime + [代码][代码]'\r\n'[代码][代码])[代码][代码] [代码][代码]var[代码] [代码]timestamp = Date.parse([代码][代码]new[代码] [代码]Date());[代码][代码] [代码][代码]if[代码] [代码](timestamp - Date.parse(res.updatetime) < period){[代码][代码] [代码][代码]//不需要更新[代码][代码] [代码][代码]msg=[代码][代码]'good'[代码][代码] [代码][代码]token= res.accesstoken[代码][代码] [代码][代码]}[代码][代码]else[代码][代码]{[代码][代码] [代码][代码]//token太久了,需要更新[代码][代码] [代码][代码]try[代码] [代码]{[代码][代码] [代码][代码]const t = await getAccessToken()[代码][代码] [代码][代码]token = t[代码][代码] [代码][代码]//更新数据库中的token[代码][代码] [代码][代码]await mysql([代码][代码]"systemSet"[代码][代码]).update({[代码][代码] [代码][代码]accesstoken: token[代码][代码] [代码][代码]}).where({ id })[代码][代码] [代码][代码]msg = [代码][代码]'update'[代码][代码] [代码][代码]console.log(t.substring(0, 5) + [代码][代码]'...done!\r\n'[代码][代码])[代码][代码] [代码][代码]} [代码][代码]catch[代码] [代码](err) {[代码][代码] [代码][代码]console.log(err);[代码][代码] [代码][代码]}[代码][代码] [代码][代码]} [代码][代码] [代码][代码]//获取二维码数据[代码][代码] [代码][代码]if[代码] [代码](msg == [代码][代码]'good'[代码] [代码]|| msg == [代码][代码]'update'[代码][代码]) {[代码][代码] [代码][代码]try[代码] [代码]{[代码][代码] [代码][代码]var[代码] [代码]q = await getqrc(token)[代码][代码] [代码][代码]// console.log(q)[代码][代码] [代码][代码]try[代码] [代码]{[代码][代码] [代码][代码]// 存储到cos,并返回地址[代码][代码] [代码][代码]var[代码] [代码]p = await uploadimg(q)[代码][代码] [代码][代码]urlpath = [代码][代码]'https://'[代码] [代码]+ p.Location[代码][代码] [代码][代码]// console.log(p)[代码][代码] [代码][代码]} [代码][代码]catch[代码] [代码](err) {[代码][代码] [代码][代码]console.log(err)[代码][代码] [代码][代码]}[代码][代码] [代码][代码]} [代码][代码]catch[代码] [代码](err) {[代码][代码] [代码][代码]var[代码] [代码]q = err[代码][代码] [代码][代码]console.log(err);[代码][代码] [代码][代码]}[代码][代码] [代码][代码]}[代码][代码]else[代码][代码]{[代码][代码] [代码][代码]var[代码] [代码]q = [代码][代码]'notoken'[代码][代码] [代码][代码]}[代码][代码] [代码][代码]//将地址返回到前端[代码][代码] [代码][代码]ctx.state.data = urlpath[代码][代码] [代码][代码]console.log([代码][代码]'finish\r\n'[代码][代码])[代码][代码]}[代码]小程序端代码(只贴获取二维码图片了): [代码]getqrcode:[代码][代码]function[代码][代码](){[代码][代码] [代码][代码]let that = [代码][代码]this[代码][代码] [代码][代码]wx.showLoading({[代码][代码] [代码][代码]title: [代码][代码]'图片正在生成中...'[代码][代码] [代码][代码]})[代码][代码] [代码][代码]qcloud.request({[代码][代码] [代码][代码]url: `${config.service.host}/weapp/getqrcode`,[代码][代码] [代码][代码]login: [代码][代码]false[代码][代码],[代码][代码] [代码][代码]data: { [代码][代码] [代码][代码]pptid: that.data.pptid,[代码][代码] [代码][代码]path:[代码][代码]'pages/detail/detail'[代码][代码] [代码][代码]},[代码][代码] [代码][代码]success(result) {[代码][代码] [代码][代码]//成功获取到了地址[代码][代码] [代码][代码]console.log([代码][代码]'Success get qrcode:'[代码][代码],result)[代码][代码] [代码][代码]let res = result.data.data[代码][代码] [代码][代码]// var base64 = wx.arrayBufferToBase64(res.data); [代码][代码] [代码][代码]// console.log(res)[代码][代码] [代码][代码]// that.setData({ imgurlqr: "data:image/png;base64," + base64 })[代码][代码] [代码][代码]//从cos下载图片[代码][代码] [代码][代码]wx.downloadFile({[代码][代码] [代码][代码]url: res,[代码][代码] [代码][代码]success: [代码][代码]function[代码] [代码](res0) {[代码][代码] [代码][代码]//获取到临时地址,进行canvas的绘制[代码][代码] [代码][代码]that.drawCanv(res0.tempFilePath)[代码][代码] [代码][代码]},[代码][代码] [代码][代码]fail: [代码][代码]function[代码] [代码](err) {[代码][代码] [代码][代码]console.log(err)[代码][代码] [代码][代码]}[代码][代码] [代码][代码]})[代码][代码] [代码][代码]},[代码][代码] [代码][代码]fail(error) {[代码][代码] [代码][代码]util.showModel([代码][代码]'请求失败'[代码][代码], error);[代码][代码] [代码][代码]console.log([代码][代码]'request fail'[代码][代码], error);[代码][代码] [代码][代码]}[代码][代码] [代码][代码]})[代码][代码] [代码][代码]},[代码]基本就这些了吧。心累。先这样用用看吧,不知道会不会有其他问题=^=
2018-09-22 - 支持图片和文字的移动、旋转、缩放、双指缩放,保存编辑状态并生成预览图
在做一个制作表情包的小程序,什么也不说先上菊花码 [图片] 先看一下第一个版本,low到不行,简单的一些制作功能 [图片] 接下来做有个更加人性化的制作功能,接下来就有了 支持图片和文字的移动、旋转、缩放、双指缩放,保存编辑状态并生成预览图 [图片] 其实参考了 https://github.com/goolhanrry/Weapp-Demo-LemonJournal 这位兄弟的,我在基础上加入双指缩放功能, 就在touch事件中判断是否双指操作就可以了 [代码]if[代码] [代码](e.touches.length == 2) {[代码][代码] [代码][代码]// 隐藏移除按钮[代码][代码] [代码][代码]this[代码][代码].setData({[代码][代码] [代码][代码]hideRemove: [代码][代码]true[代码][代码] [代码][代码]})[代码][代码] [代码][代码]var[代码] [代码]that = [代码][代码]this[代码][代码];[代码][代码] [代码][代码]var[代码] [代码]xMove = e.touches[1].clientX - e.touches[0].clientX;[代码][代码] [代码][代码]var[代码] [代码]yMove = e.touches[1].clientY - e.touches[0].clientY;[代码][代码] [代码][代码]var[代码] [代码]distance = Math.sqrt(xMove * xMove + yMove * yMove);[代码] [代码] [代码][代码]that.data.newdistance = distance; [代码][代码]//第二次就可以计算它们的差值了 [代码][代码] [代码][代码]that.data.diffdistance = that.data.newdistance - that.data.olddistance; [代码][代码]//两次差值[代码][代码] [代码][代码]that.data.olddistance = that.data.newdistance; [代码][代码]//计算之后更新比例 [代码][代码] [代码][代码]var[代码] [代码]res_scale = that.data.scale + 0.005 * that.data.diffdistance;[代码] [代码] [代码][代码]if[代码] [代码](res_scale > 4) { [代码][代码]//放大的最大倍数[代码][代码] [代码][代码]return[代码][代码];[代码][代码] [代码][代码]} [代码][代码]else[代码] [代码]if[代码] [代码](res_scale < 1) { [代码][代码]//缩小不能小于当前[代码][代码] [代码][代码]return[代码][代码];[代码][代码] [代码][代码]}[代码][代码] [代码][代码]that.setData({[代码][代码] [代码][代码]scale: res_scale,[代码][代码] [代码][代码]})[代码] [代码]} [代码][代码]else[代码] [代码]{[代码] [代码] [代码][代码]// 拖动组件则所有控件同时移动[代码][代码] [代码][代码]this[代码][代码].setData({[代码][代码] [代码][代码]stickerCenterX: [代码][代码]this[代码][代码].data.stickerCenterX + diff_x,[代码][代码] [代码][代码]stickerCenterY: [代码][代码]this[代码][代码].data.stickerCenterY + diff_y,[代码][代码] [代码][代码]removeCenterX: [代码][代码]this[代码][代码].data.removeCenterX + diff_x,[代码][代码] [代码][代码]removeCenterY: [代码][代码]this[代码][代码].data.removeCenterY + diff_y,[代码][代码] [代码][代码]handleCenterX: [代码][代码]this[代码][代码].data.handleCenterX + diff_x,[代码][代码] [代码][代码]handleCenterY: [代码][代码]this[代码][代码].data.handleCenterY + diff_y[代码][代码] [代码][代码]})[代码][代码]}[代码]
2018-09-21