- 使用vant的文件上传capture="camera" 无法直接调用摄像头
<van-uploader file-list="{{ fileList }}" bind:after-read="afterRead" capture="camera" accept="image"/> 华为荣耀8x点击是打开相册,第一个选项是拍摄照片。应该是直接调用摄像头,限制只能拍照上传。
2020-09-14 - Map组件中可否增加Canvas图层?
在地图上绘制热力图是种很常见的应用场景,或者是其他大量数据的覆盖物使用PolyLine、Circle等也会卡。因为在地图采用canvas的形式绘制数据是非常必要的。 需求是: 1.在缩放、拖动地图的情况下也使得canvas同步缩放、平移, 2.在缩放、拖动完地图后回调方法,使得开发者可以重新对canvas进行绘制。 请问将来会否将此功能加入Map组件
2020-01-21 - 关于在微信小程序使用WebglCanvas和ThreeJs开发的二三事(三)
更新了如何在微信小程序加载gltf模型并渲染,内容码在了csdn上有兴趣的小伙伴可以跳转浏览: https://blog.csdn.net/geshu12138/article/details/105862436
2020-06-11 - 微信小程序内嵌webview相关知识点整理
前言 随着微信小程序的广泛应用,越来越多的商家选择将营销阵营选择迁移到了小程序中,但受其小程序体积限制的影响,不能够完全满足商户的要求,应运而生的web-view组件很好的解决的这一问题。一方面内嵌web-view能够直接运行在小程序中,大大减少了商户的开发成本,另一方面能够实现小程序与h5的跳转,有良好的扩展性,方便商户多端间引流。 一、web-view内嵌h5与传统小程序对比 通过查找相关资料发现,内嵌web-view和微信小程序在离线能力、页面切换体验、二次渲染,操作响应和开发灵活性上有如下对比,在不同场景下可能性能上有些许出入,仅供参考。 [图片] 表格来源:https://segmentfault.com/a/1190000017752299 从上表中可以对比出 H5 相较于小程序的优缺点,开发者可以根据实际需要选择。此外,使用web-view还有以下优点: (1)己方账号(第三方)与小程序openId/UnionId的关联绑定,实现免登陆 比如你是某门户网站,需要识别自己小程序上的用户与网站用户的关系,可以通过三种方法绑定关系,公众号,小程序原生,小程序web-view内嵌跳转三种方法。 (2)内嵌H5的富文本,减少重复开发 比如你是门户网站,社区,以往有大量的新闻和帖子,里面带了各种css样式的富文本,音视频自定义的点击事件等,小程序的原生组件可能无法兼容,这时候直接内嵌这些H5新闻,将会大大降低开发成本。 (3)热更新,减少发布审核 内嵌H5可以减少频繁提交小程序审核,商户只需要重新发布h5的版本,就可以更新内嵌web-view的内容。 <script type="text/javascript" src='//res.wx.qq.com/open/js/jweixin-1.4.0.js'></script 3.4 web-view和小程序间通信 官方给出了两种通信方法。 1、postMessage 通信 在 H5 中需要先用 wx.miniProgram.postMessage 接口,把需要分享的信息,推送给小程序。 在用户点击了小程序后退、组件销毁、分享这些特殊事件之后,小程序页面通过 bindmessage 绑定的函数读取 post 信息。 2、设置 web-view 组件的 URL 通信 H5 跳转小程序: // h5中跳转小程序 navigateToWeixin() { wx.miniProgram.navigateTo({url: '/pages/shop/index'}); } // 小程序跳转h5--第一步 <view> <web-view src="{{url}}" ></web-view> </view> // 小程序跳转h5--第二步:在小程序的 js 文件中设置通过 URL 以问号传参的方式传入参数到 H5中 if(!option.page){ this.setData({ url: `${this.data.url}?${test}` }); } else { this.setData({ url: `${this.data.url}${option.page}?${test}` }); } 四、内嵌web-view调试解决方案 首先通过微信开发者工具打开,这里url本地调试可以跟局域网链接,开发者工具可以实时显示,如果跟的是线上链接,可能只能通过预览扫码。 [图片] 右键点击页面,左上角会出现调试,可以在右图的调试器中对h5进行调试。 [图片] 五、踩过的小坑 (1)H5唤醒一些小程序API有一定的延时,0.3~1秒; (2)请调用小程序专用的JSSDK,公众号中的JSSDK不通用; (3)web-view一定是撑满全屏的,自定义顶部菜单,悬浮的都没用; (4)web-view后边跟的h5链接,必须要url转义,否则会和小程序自身的符号定义冲突,可能识别不到?后的参数; (5)H5控制小程序的跳转路径必须是“/”开头,如 “/pages/xxx/xxx”,且路径必须在app.json里有,地址错误的话,有时不报错。 (6)postMessage的json必须是data开始,不然接收不到数据。 (7)bindmessage相关: 小程序后退:使用接口名 wx.miniProgram.navigateTo,wx.miniProgram.navigateBack,wx.miniProgram.switchTab 等切换小程序页面/场景的API时候都会触发。分享:这个就是当你点分享小程序的时候,会接受到H5之前发送的postMessage。组件销毁,web-view组件销毁,类似 wx.miniProgram.redirectTo 都会触发。(8)内嵌h5跳转小程序是有严重缓存的,比如:h5页面有计时器,跳转小程序后计时器依然会计时,可以通过在url后面加时间戳的方式解决。 还有小程序已经关闭,H5自带的背景音乐仍然在手机后台播放,可以通过浏览器标签页被隐藏或显示的时候会触发visibilitychange(页面可见性状态)事件解决。 六、总结 看到这里,相信大家对于内嵌web-view已经有了大致的了解,我们主要对内嵌web-view与传统小程序进行对比,分析了内嵌web-view的优缺点,其次对如何配置内嵌web-view进行了阐述,然后对实战中内嵌web-view如何调试进行了示例,最后对实战中可能踩坑和注意的小点进行了友情提示。所以如果本篇文章有帮助到你的话,小编会非常欣慰,欢迎点赞和转发呦~
2020-06-16 - 小程序埋点
数据统计作为⽬前⼀种常⽤的分析⽤户⾏为的⽅式,⼩程序端也是必不可少 的。⼩程序采取的曝光,点击数据埋点其实和h5原理是⼀样的。但是埋点作为⼀个和业务逻辑不相关的需求,我们如果在每⼀个点击事件,每⼀个⽣命周期加⼊各种埋点代码,则会⼲扰正常的业务逻辑,和使代码变的臃肿,提供下面方案来解决数据埋点 page = function(params) { let keys = params.keys() keys.forEach(v => { if (v === 'onLoad') { params[v] = function(options) { stat() //埋点代码 params[v].call(this, options) } } else if (v.includes('click')) { params[v] = funciton(event) { let data = event.dataset.config stat(data) // 点击埋点 param[v].call(this) } } }) } 这种思路不光适⽤于埋点,也可以⽤来作全局异常处理等场景 。
2020-06-09 - 小程序中通过CSS实现炫酷的动画效果
1.Animate.css简介Animate.css是一个可在您的Web项目中使用的即用型跨浏览器动画库。非常适合强调,首页,滑块和引导注意的提示。它是一个来自国外的 CSS3 动画库,它预设了抖动(shake)、闪烁(flash)、弹跳(bounce)、翻转(flip)、旋转(rotateIn/rotateOut)、淡入淡出(fadeIn/fadeOut)等多达 60 多种动画效果,几乎包含了所有常见的动画效果。虽然借助Animate.css 能够很方便、快速的制作 CSS3 动画效果,但还是建议看看Animate.css 的代码,也许你能从中学到一些东西。不论是在Web端和小程序内都可以正常使用,详细内容请到官方地址学习。 2.动画效果的实现在使用过程中,可以根据自己的喜好来改造css代码来达到你想要的效果,文中动图显示可能不是特别直观,建议自己写一遍代码,即利于学习,又能够直观的体会到动画效果。 1.发光的盒子 [图片] wxml代码: <view id="box">I am LetCode!</view> wxss代码: @keyframes animated-border { 0% { box-shadow: 0 0 0 0 rgba(255,255,255,0.4); } 100% { box-shadow: 0 0 0 20px rgba(255,255,255,0); } } #box { animation: animated-border 1.5s infinite; height: 100rpx; font-family: Arial; font-size: 18px; font-weight: bold; color: white; border: 2px solid; border-radius: 10px; margin: 100px 15px; line-height: 100rpx; text-align: center; } 2.文字的缩放效果 [图片] wxml代码: <view class="animate_zoomOutDown">关注公众号“Let编程”,有更多分享!</view> wxss代码: @keyframes zoomOutDown { 40% { opacity: 1; transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); } to { opacity: 0; transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 2000px, 0); animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); } } .animate_zoomOutDown { animation:2s linear 0s infinite alternate zoomOutDown; font-family: Arial; font-size: 18px; font-weight: bold; color: white; margin-top: 70px; text-align: center; margin-top: 15px; } 3.加载动画 [图片] wxml代码: <view class="load-container load"> <view class="loader"> </view> </view> <view class="txt">关注公众号“Let编程”,有更多分享!</view> wxss代码: .load-container { width: 240px; height: 240px; margin: 0 auto; position: relative; overflow: hidden; box-sizing: border-box; } .load .loader { color: #ffffff; font-size: 90px; text-indent: -9999em; overflow: hidden; width: 1em; height: 1em; border-radius: 50%; margin: 72px auto; position: relative; transform: translateZ(0); animation: load 1.7s infinite ease, round 1.7s infinite ease; } @keyframes load { 0% { box-shadow: 0 -0.83em 0 -0.4em, 0 -0.83em 0 -0.42em, 0 -0.83em 0 -0.44em, 0 -0.83em 0 -0.46em, 0 -0.83em 0 -0.477em;} 5%, 95% { box-shadow: 0 -0.83em 0 -0.4em, 0 -0.83em 0 -0.42em, 0 -0.83em 0 -0.44em, 0 -0.83em 0 -0.46em, 0 -0.83em 0 -0.477em;} 10%, 59% { box-shadow: 0 -0.83em 0 -0.4em, -0.087em -0.825em 0 -0.42em, -0.173em -0.812em 0 -0.44em, -0.256em -0.789em 0 -0.46em, -0.297em -0.775em 0 -0.477em;} 20% { box-shadow: 0 -0.83em 0 -0.4em, -0.338em -0.758em 0 -0.42em, -0.555em -0.617em 0 -0.44em, -0.671em -0.488em 0 -0.46em, -0.749em -0.34em 0 -0.477em;} 38% { box-shadow: 0 -0.83em 0 -0.4em, -0.377em -0.74em 0 -0.42em, -0.645em -0.522em 0 -0.44em, -0.775em -0.297em 0 -0.46em, -0.82em -0.09em 0 -0.477em;} 100% { box-shadow: 0 -0.83em 0 -0.4em, 0 -0.83em 0 -0.42em, 0 -0.83em 0 -0.44em, 0 -0.83em 0 -0.46em, 0 -0.83em 0 -0.477em;} } @keyframes round{ 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } 4.抖动的文字 [图片] wxml代码: <view class="shake-slow txt">关注公众号“Let编程”,有更多分享!</view> wxss代码: @keyframes shake-slow { 2% { transform: translate(6px, -2px) rotate(3.5deg); } 4% { transform: translate(5px, 8px) rotate(-0.5deg); } 6% { transform: translate(6px, -3px) rotate(-2.5deg); } 8% { transform: translate(4px, -2px) rotate(1.5deg); } 10% { transform: translate(-6px, 8px) rotate(-1.5deg); } 12% { transform: translate(-5px, 5px) rotate(1.5deg); } 14% { transform: translate(4px, 10px) rotate(3.5deg); } 16% { transform: translate(0px, 4px) rotate(1.5deg); } 18% { transform: translate(-1px, -6px) rotate(-0.5deg); } 20% { transform: translate(6px, -9px) rotate(2.5deg); } 22% { transform: translate(1px, -5px) rotate(-1.5deg); } 24% { transform: translate(-9px, 6px) rotate(-0.5deg); } 26% { transform: translate(8px, -2px) rotate(-1.5deg); } 28% { transform: translate(2px, -3px) rotate(-2.5deg); } 30% { transform: translate(9px, -7px) rotate(-0.5deg); } 32% { transform: translate(8px, -6px) rotate(-2.5deg); } 34% { transform: translate(-5px, 1px) rotate(3.5deg); } 36% { transform: translate(0px, -5px) rotate(2.5deg); } 38% { transform: translate(2px, 7px) rotate(-1.5deg); } 40% { transform: translate(6px, 3px) rotate(-1.5deg); } 42% { transform: translate(1px, -5px) rotate(-1.5deg); } 44% { transform: translate(10px, -4px) rotate(-0.5deg); } 46% { transform: translate(-2px, 2px) rotate(3.5deg); } 48% { transform: translate(3px, 4px) rotate(-0.5deg); } 50% { transform: translate(8px, 1px) rotate(-1.5deg); } 52% { transform: translate(7px, 4px) rotate(-1.5deg); } 54% { transform: translate(10px, 8px) rotate(-1.5deg); } 56% { transform: translate(-3px, 0px) rotate(-0.5deg); } 58% { transform: translate(0px, -1px) rotate(1.5deg); } 60% { transform: translate(6px, 9px) rotate(-1.5deg); } 62% { transform: translate(-9px, 8px) rotate(0.5deg); } 64% { transform: translate(-6px, 10px) rotate(0.5deg); } 66% { transform: translate(7px, 0px) rotate(0.5deg); } 68% { transform: translate(3px, 8px) rotate(-0.5deg); } 70% { transform: translate(-2px, -9px) rotate(1.5deg); } 72% { transform: translate(-6px, 2px) rotate(1.5deg); } 74% { transform: translate(-2px, 10px) rotate(-1.5deg); } 76% { transform: translate(2px, 8px) rotate(2.5deg); } 78% { transform: translate(6px, -2px) rotate(-0.5deg); } 80% { transform: translate(6px, 8px) rotate(0.5deg); } 82% { transform: translate(10px, 9px) rotate(3.5deg); } 84% { transform: translate(-3px, -1px) rotate(3.5deg); } 86% { transform: translate(1px, 8px) rotate(-2.5deg); } 88% { transform: translate(-5px, -9px) rotate(2.5deg); } 90% { transform: translate(2px, 8px) rotate(0.5deg); } 92% { transform: translate(0px, -1px) rotate(1.5deg); } 94% { transform: translate(-8px, -1px) rotate(0.5deg); } 96% { transform: translate(-3px, 8px) rotate(-1.5deg); } 98% { transform: translate(4px, 8px) rotate(0.5deg); } 0%, 100% { transform: translate(0, 0) rotate(0); } } .shake-slow{ animation:shake-slow 5s infinite ease-in-out; } 在实际开发过程中,远不止这些炫酷的动画效果,在互联网迅速的发展状态下,还需要更多的程序员来实现功能需求,因此本文只做简单的介绍,未完待续.....
2020-06-04 - 小程序日历组件推荐
小程序日历组件推荐 这几天在做民宿小程序,遇到一个需求就是 用户选择日期,来下单,由于民宿每天的价格都是变化的,所以要在日历上显示价格,找了很久,终于找到了 vant https://youzan.github.io/vant-weapp/#/calendar 1 [图片] 2 [图片] 3 具体使用方法如下所示 [图片] 4 具体小程序界面截图如下所示: 5 [图片] 6 7 现在说起来都是风轻云淡,但是找的过程又盲目,有着急,开发中,也遇到各种问题,好在,回过头去,vant calendar就是我要找的日历组件 8
2020-06-07 - 如何在小程序中快速实现环形进度条
在小程序开发过程中经常涉及到一些图表类需求,其中环形进度条比较属于比较常见的需求 [图片] [中间的文字部分需要自己实现,因为每个项目不同,本工具只实现进度条] 上图中,一方面我们我们需要实现动态计算弧度的进度条,还需要在进度条上加上渐变效果,如果每次都需要自己手写,那需要很多重复劳动,所以决定为为小程序生态圈贡献一份小小的力量,下面来介绍一下整个工具的实现思路,喜欢的给个star咯 https://github.com/lucaszhu2zgf/mp-progress 环形进度条由灰色底圈+渐变不确定圆弧+双色纽扣组成,首先先把页面结构写好: .canvas{ position: absolute; top: 0; left: 0; width: 400rpx; height: 400rpx; } 因为进度条需要盖在文字上面,所以采用了绝对定位。接下来先把灰色底圈给画上: const context = wx.createContext(); // 打底灰色曲线 context.beginPath(); context.arc(this.convert_length(200), this.convert_length(200), r, 0, 2*Math.PI); context.setLineWidth(12); context.setStrokeStyle('#f0f0f0'); context.stroke(); wx.drawCanvas({ canvasId: 'progress', actions: context.getActions() }); 效果如下: [图片] 接下来就要画绿色的进度条,渐变暂时先不考虑 // 圆弧角度 const deg = ((remain/total).toFixed(2))*2*Math.PI; // 画渐变曲线 context.beginPath(); // 由于外层大小是400,所以圆弧圆心坐标是200,200 context.arc(this.convert_length(200), this.convert_length(200), r, 0, deg); context.setLineWidth(12); context.setStrokeStyle('#56B37F'); context.stroke(); // 辅助函数,用于转换小程序中的rpx convert_length(length) { return Math.round(wx.getSystemInfoSync().windowWidth * length / 750); } [图片] 似乎完成了一大部分,先自测看看不是满圆的情况是啥样子,比如现在剩余车位是120个 [图片] 因为圆弧函数arc默认的起点在3点钟方向,而设计想要的圆弧的起点从12点钟方向开始,现在这样是没法达到预期效果。是不是可以使用css让canvas自己旋转-90deg就好了呢?于是我在上面的canvas样式中新增以下规则: .canvas{ transform: rotate(-90deg); } 但是在真机上并不起作用,于是我把新增的样式放到包裹canvas的外层元素上,发现外层元素已经旋转,可是圆弧还是从3点钟方向开始的,唯一能解释这个现象的是官方说:小程序中的canvas使用的是原生组件,所以这样设置css并不能达到我们想要的效果 [图片] 所以必须要在canvas画图的时候把坐标原点移动到弧形圆心,并且在画布内旋转-90deg [图片] // 更换原点 context.translate(this.convert_length(200), this.convert_length(200)); // arc原点默认为3点钟方向,需要调整到12点 context.rotate(-90 * Math.PI / 180); // 需要注意的是,原点变换之后圆弧arc原点也变成了0,0 真机预览效果达成预期 [图片] 接下来添加环形渐变效果,但是canvas原本提供的渐变类型只有两种: 1、LinearGradient线性渐变 [图片] 2、CircularGradient圆形渐变 [图片] 两种渐变中离设计效果最近的是线性渐变,至于为什么能够形成似乎是随圆形弧度增加而颜色变深的效果也只是控制坐标开始和结束的坐标位置罢了 const grd = context.createLinearGradient(0, 0, 100, 90); grd.addColorStop(0, '#56B37F'); grd.addColorStop(1, '#c0e674'); // 画渐变曲线 context.beginPath(); context.arc(0, 0, r, 0, deg); context.setLineWidth(12); context.setStrokeStyle(grd); context.stroke(); 来看一下真机预览效果: [图片] 非常棒,最后就剩下跟随进度条的纽扣效果了 [图片] 根据三角函数,已知三角形夹角根据公式radian = 2*Math.PI/360*deg,再利用cos和sin函数可以x、y,从而计算出纽扣在各部分半圆的坐标 const mathDeg = ((remain/total).toFixed(2))*360; // 计算弧度 let radian = ''; // 圆圈半径 const r = +this.convert_length(170); // 三角函数cos=y/r,sin=x/r,分别得到小点的x、y坐标 let x = 0; let y = 0; if (mathDeg <= 90) { // 求弧度 radian = 2*Math.PI/360*mathDeg; x = Math.round(Math.cos(radian)*r); y = Math.round(Math.sin(radian)*r); } else if (mathDeg > 90 && mathDeg <= 180) { // 求弧度 radian = 2*Math.PI/360*(180 - mathDeg); x = -Math.round(Math.cos(radian)*r); y = Math.round(Math.sin(radian)*r); } else if (mathDeg > 180 && mathDeg <= 270) { // 求弧度 radian = 2*Math.PI/360*(mathDeg - 180); x = -Math.round(Math.cos(radian)*r); y = -Math.round(Math.sin(radian)*r); } else{ // 求弧度 radian = 2*Math.PI/360*(360 - mathDeg); x = Math.round(Math.cos(radian)*r); y = -Math.round(Math.sin(radian)*r); } [图片] 有了纽扣的圆形坐标,最后一步就是按照设计绘制样式了 // 画纽扣 context.beginPath(); context.arc(x, y, this.convert_length(24), 0, 2 * Math.PI); context.setFillStyle('#ffffff'); context.setShadow(0, 0, this.convert_length(10), 'rgba(86,179,127,0.5)'); context.fill(); // 画绿点 context.beginPath(); context.arc(x, y, this.convert_length(12), 0, 2 * Math.PI); context.setFillStyle('#56B37F'); context.fill(); 来看一下最终效果 [图片] 最后我重新review了整个代码逻辑,并且已经将代码开源到https://github.com/lucaszhu2zgf/mp-progress,欢迎大家使用
2020-05-27 - 商户传入的appid参数不正确,请联系商户处理
-问题描述: 调用支付接口,后台请求支付接口成功,回执数据正常,回执数据返回至小程序端,报错“商户传入的appid参数不正确,请联系商户处理”,没有弹出输入密码页面!!! -问题出现场景: 小程序对同主体商户,能够支付成功,对应不同主体商户支付失败(商户平台已经绑定改小程序的APPID) - 当前 Bug 的表现(可附上截图) [图片] [图片] - 预期表现 [图片] -小程序端代码 [代码]wx.requestPayment({[代码][代码] [代码][代码]'timeStamp': res.data.timeStamp,[代码][代码] [代码][代码]'nonceStr': res.data.nonceStr,[代码][代码] [代码][代码]'package': res.data.package,[代码][代码] [代码][代码]'signType': res.data.signType,[代码][代码] [代码][代码]'paySign': res.data.paySign,[代码][代码] [代码][代码]'success': function (res) {[代码][代码] [代码][代码]wx.setStorageSync('ordertime', 60);[代码][代码] [代码][代码]wx.setStorageSync('clearsetInter','');[代码][代码] [代码][代码]wx.setStorageSync("orderid", "");[代码][代码] [代码][代码]wx.redirectTo({[代码][代码] [代码][代码]url: orderUrl[代码][代码] [代码][代码]})[代码][代码] [代码][代码]},[代码][代码] [代码][代码]'fail': function (res) {[代码][代码] [代码][代码]wx.redirectTo({[代码][代码] [代码][代码]url: '../../order/fail/fail?orderfail=fail&orderid=' + orderid[代码][代码] [代码][代码]})[代码][代码] [代码][代码]}[代码][代码] [代码][代码]})[代码]-请求参数: [代码]{nonce_str: [代码][代码]"prlNcOyt8XNXTDdI"[代码][代码], [代码][代码]package[代码][代码]: [代码][代码]"prepay_id=wx131821368388059bff1da1a91266347900"[代码][代码],…}[代码][代码]appid: [代码][代码]"wx64219b04c3af9d85"[代码][代码]mch_id: [代码][代码]"1515291591"[代码][代码]nonceStr: [代码][代码]"1125C6496504470BA95F99905D32E902"[代码][代码]nonce_str: [代码][代码]"prlNcOyt8XNXTDdI"[代码][代码]package[代码][代码]: [代码][代码]"prepay_id=wx131821368388059bff1da1a91266347900"[代码][代码]paySign: [代码][代码]"7061D742E95B91DD39E1B18731DDE8FC"[代码][代码]prepay_id: [代码][代码]"wx131821368388059bff1da1a91266347900"[代码][代码]result_code: [代码][代码]"SUCCESS"[代码][代码]return_code: [代码][代码]"SUCCESS"[代码][代码]return_msg: [代码][代码]"OK"[代码][代码]sign: [代码][代码]"B692060C049D89D585A254E5332C6C9F"[代码][代码]signType: [代码][代码]"MD5"[代码][代码]timeStamp: [代码][代码]"1560421295151"[代码][代码]trade_type: [代码][代码]"JSAPI"[代码]
2019-06-14 - 如何实现H5 APP 微信小程序互跳转
如何实现H5 APP 微信小程序互跳转?求大神指点
2019-04-28 - 小程序中使用three.js
小程序中使用three.js 目前小程序支持了webgl, 同时项目中有相关3D展示的需求,所以考虑将three.js移植到小程序中。 但是小程序里面没有浏览器相关的运行环境如 window,document等。要想在小程序中使用three.js需要使用相应的移植版本。https://github.com/yannliao/three.js实现了一个在 three.js 的基本移植版, 目前测试支持了 包含 BoxBufferGeometry, CircleBufferGeometry, ConeBufferGeometry, CylinderBufferGeometry, DodecahedronBufferGeometry 等基本模型,OrbitControl, GTLFLoader, OBJLoader等。 使用 下载https://github.com/yannliao/three.js项目中build目录下的three.weapp.min.js到小程序相应目录,如: [图片] 在index.wxml中加入canvas组件, 其中需要手动绑定相应的事件,用于手势控制。 [代码]<view style="height: 100%; width: 100%;" bindtouchstart="documentTouchStart" bindtouchmove="documentTouchMove" bindtouchend="documentTouchEnd" > <canvas type="webgl" id="c" style="width: 100%; height:100%;" bindtouchstart="touchStart" bindtouchmove="touchMove" bindtouchend="touchEnd" bindtouchcancel="touchCancel" bindlongtap="longTap" bindtap="tap"></canvas> </view> [代码] 在页面中引用three.js 和相应的Loader: [代码]import * as THREE from '../../libs/three.weapp.min.js' import { OrbitControls } from '../../jsm/loaders/OrbitControls' [代码] 在onLoad中获取canvas对象并注册到[代码]THREE.global[代码]中,[代码]THREE.global.registerCanvas[代码]可以传入id, 用于通过[代码]THREE.global.document.getElementById[代码]找到, 如果不传id默认使用canvas对象中的_canvasID, registerCanvas同时也会将该canvas选为当前使用canvas对象. 同时请在onUnload回调中注销canvas对象. 注意: THREE.global 中最多同时注册 5 个[代码]canvas[代码]对象, 并可以通过id找到. 注册的canvas对象, 会长驻内存, 如果不及时清理可能造成内存问题. [代码]THREE.global[代码]为[代码]three.js[代码] 的运行环境, 类似于浏览器中的window. [代码]Page({ data: { canvasId: '' }, onLoad: function () { wx.createSelectorQuery() .select('#c') .node() .exec((res) => { const canvas = THREE.global.registerCanvas(res[0].node) this.setData({ canvasId: canvas._canvasId }) // const canvas = THREE.global.registerCanvas('id_123', res[0].node) // canvas代码 }) }, onUnload: function () { THREE.global.unregisterCanvas(this.data.canvasId) // THREE.global.unregisterCanvas(res[0].node) // THREE.global.clearCanvas() }, [代码] 注册相关touch事件. 由于小程序架构原因, 需要手动绑定事件到THREE.global.canvas或者THREE.global.document上. 可以使用[代码]THREE.global.touchEventHandlerFactory('canvas', 'touchstart')[代码] 生成小程序的事件回调函数,触发默认canvas对象上的touch事件. [代码] { touchStart(e) { console.log('canvas', e) THREE.global.touchEventHandlerFactory('canvas', 'touchstart')(e) }, touchMove(e) { console.log('canvas', e) THREE.global.touchEventHandlerFactory('canvas', 'touchmove')(e) }, touchEnd(e) { console.log('canvas', e) THREE.global.touchEventHandlerFactory('canvas', 'touchend')(e) }, } [代码] 编写three.js代码, 小程序运行环境中没有requestAnimationFrame, 目前可以使用canvas.requestAnimationFrame之后会将requestAnimationFrame注入到THREE.global中. [代码]const camera = new THREE.PerspectiveCamera(70, canvas.width / canvas.height, 1, 1000); camera.position.z = 500; const scene = new THREE.Scene(); scene.background = new THREE.Color(0xAAAAAA); const renderer = new THREE.WebGLRenderer({ antialias: true }); const controls = new OrbitControls(camera, renderer.domElement); // controls.enableDamping = true; // controls.dampingFactor = 0.25; // controls.enableZoom = false; camera.position.set(200, 200, 500); controls.update(); const geometry = new THREE.BoxBufferGeometry(200, 200, 200); const texture = new THREE.TextureLoader().load('./pikachu.png'); const material = new THREE.MeshBasicMaterial({ map: texture }); // const material = new THREE.MeshBasicMaterial({ color: 0x44aa88 }); const mesh = new THREE.Mesh(geometry, material); scene.add(mesh); // renderer.setPixelRatio(wx.getSystemInfoSync().pixelRatio); // renderer.setSize(canvas.width, canvas.height); function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(canvas.width, canvas.height); } function render() { canvas.requestAnimationFrame(render); // mesh.rotation.x += 0.005; // mesh.rotation.y += 0.01; controls.update(); renderer.render(scene, camera); } render() [代码] 完整示例: index.js [代码]import * as THREE from '../../libs/three.weapp.min.js' import { OrbitControls } from '../../jsm/loaders/OrbitControls' Page({ data: {}, onLoad: function () { wx.createSelectorQuery() .select('#c') .node() .exec((res) => { const canvas = THREE.global.registerCanvas(res[0].node) this.setData({ canvasId: canvas._canvasId }) const camera = new THREE.PerspectiveCamera(70, canvas.width / canvas.height, 1, 1000); camera.position.z = 500; const scene = new THREE.Scene(); scene.background = new THREE.Color(0xAAAAAA); const renderer = new THREE.WebGLRenderer({ antialias: true }); const controls = new OrbitControls(camera, renderer.domElement); // controls.enableDamping = true; // controls.dampingFactor = 0.25; // controls.enableZoom = false; camera.position.set(200, 200, 500); controls.update(); const geometry = new THREE.BoxBufferGeometry(200, 200, 200); const texture = new THREE.TextureLoader().load('./pikachu.png'); const material = new THREE.MeshBasicMaterial({ map: texture }); // const material = new THREE.MeshBasicMaterial({ color: 0x44aa88 }); const mesh = new THREE.Mesh(geometry, material); scene.add(mesh); // renderer.setPixelRatio(wx.getSystemInfoSync().pixelRatio); // renderer.setSize(canvas.width, canvas.height); function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(canvas.width, canvas.height); } function render() { canvas.requestAnimationFrame(render); // mesh.rotation.x += 0.005; // mesh.rotation.y += 0.01; controls.update(); renderer.render(scene, camera); } render() }) }, onUnload: function () { THREE.global.unregisterCanvas(this.data.canvasId) }, touchStart(e) { console.log('canvas', e) THREE.global.touchEventHandlerFactory('canvas', 'touchstart')(e) }, touchMove(e) { console.log('canvas', e) THREE.global.touchEventHandlerFactory('canvas', 'touchmove')(e) }, touchEnd(e) { console.log('canvas', e) THREE.global.touchEventHandlerFactory('canvas', 'touchend')(e) }, touchCancel(e) { // console.log('canvas', e) }, longTap(e) { // console.log('canvas', e) }, tap(e) { // console.log('canvas', e) }, documentTouchStart(e) { // console.log('document',e) }, documentTouchMove(e) { // console.log('document',e) }, documentTouchEnd(e) { // console.log('document',e) }, }) [代码] index.wxml [代码]<view style="height: 100%; width: 100%;" bindtouchstart="documentTouchStart" bindtouchmove="documentTouchMove" bindtouchend="documentTouchEnd" > <canvas type="webgl" id="c" style="width: 100%; height:100%;" bindtouchstart="touchStart" bindtouchmove="touchMove" bindtouchend="touchEnd" bindtouchcancel="touchCancel" bindlongtap="longTap" bindtap="tap"></canvas> </view> [代码] 其他 全部示例在 https://github.com/yannliao/threejs-example three.js 库 https://github.com/yannliao/three.js loader 组件在 threejs-example 中的 jsm 目录中 欢迎提交PR和issue
2019-10-20 - 【笔记】小程序微信支付踩过的坑
最近一段时间一直在做跟微信支付相关的内容,遇到很多问题,说实话上周已经调试通过了一个微信支付流程,同样的配方,同样的味道,这次调通竟然花费了一周时间 总结下经常遇到的问题 1、签名失败, 这个问题真的折磨人 我的经历是这样的,证书和密钥都给我我配置好之后,支付一直报错,我找相关人员把密钥变更了,然后又试,开始报签名失败的问题。 试了一个晚上,也不知道是哪根筋接错了,又想用原先的密钥试试,结果成功了, 以上真是坎坷 附云开发一份在线文档 https://github.com/TencentCloudBase/mp-book/
2020-03-22 - 史上最详细一步一步介绍微信小程序微信支付功能开发
前言微信小程序开发微信支付, 相对于微信的其他功能,实话说相比之下好太多,可能是开发文档是微信支付这边撰写的缘故吧?猜的。所以微信支付在小程序中,虽然参数十分的多,环节特别的细致,但也算不上无从下手。上个项目实施了一次微信小程序支付功能的开发,趁记忆力尚可,赶紧记录一番本次环境平台为 小程序端(uniapp)| 原生一样,只介绍js部分 + egg.js端(node)流程基本都一致,只是语法上有许区别开发准备工欲善其事,必先利其器。开发之前我们需要先准备好哪些必须的开发前提或环境呢?资料:1. 审核并开通微信商户号: •微信支付页链接地址[1][图片] •等待审核, 审核通过后对小程序进行关联[图片] •小程序开启支付[图片] •小程序支付开通后[图片] •打开微信支付页,可以进行绑定查看[图片] •再到微信支付中去记录mch_id,和merchantkey(商户密码),记录merchantkey的方式如图:[图片][图片] •记录下merchantkey ,注意:merchantkey是敏感key,不可泄漏 2. 前面开发好拉取用户的授权,获取用户在当下小程序中的openid(重点必须) 3. 搭建好服务器接口调用, 记录下需要传递给微信服务器使用回调的服务器ip地址以及接口的url地址(提前准备好,可以使用postman做好测试)。(可以为本服务器也可以为另外服务器,主要作为回调) 4. 其他:微信官方文档要求 审核支付功能需要微信小程序已上线,但是当时我申请的时候小程序并未上线也过了,所以这一块我无法做出解释。另外,程序访问商户服务都是通过HTTPS,开发部署的时候需要安装HTTPS服务器 开发流程5. 先来看看官方微信支付给出的流程图[图片] •感觉有点懵逼 •我总结如下:[图片] 开始开发1.根据以上流程图,我们开始进行调用 •第一步 小程序发起支付的代码如下: async wxappay (openid, money) { return new Promise(async (resolve, reject) => { let Objct = { openid, //拉取授权获取到的openid money, //money必须是整数类型, 以RMB分为单位! body: 'xxx' } let temp = await wxappWxPay(obj) //进入到第一阶段, 预支付阶段 //后面的逻辑为第二阶段 }) } 注意,强烈推荐使用promise函数来实现,可以保证逻辑代码体在实现流程的一致性•第一步 后端node服务器接口获取支付第一部参数的代码如下: /** * 微信统一下单(微信支付)的接口数据(!!!!小程序专用付款方式) * @param {OBject} * 调用微信预支付接口(必填项) * @@排列顺序不可以错! * 1.appid * 2.body: 商品描述 * 3.mch_id: 支付申请配置的商户号 * 4.NonceStr: 随机字符串 * 5.notify_url: 微信付款后的回调地址 //后端egg的接口接收此地址来响应支付成功的回调 * 6.openid: * 7.out_trade_no: 订单号(32位) * 8.spbill_create_ip:后端调用API支付微信的ip地址 (支持32位和64位IP地址) * 9.total_fee: 支付金额 * 10. * /** * //生成微信支付的参数进行ASCII码从小到大排序 * @params: * body: 支付内容 * totalmoney: 支付金额 */ async getPrePayId(obj) { const { config, ctx } = this const { appid, merchantid, merchantkey } = config.wxapp //后台预先设置的appid,merchantid, merchantkey const { ip, notify_url } = config.payaddress const NonceStr = Math.random().toString(36).substr(2, 15) const orderid = uuid.v4().replace(/-/g, '') const { body, totalmoney, openid } = obj //预发起支付第一次签名 const uniorderParams = { appid, body, mch_id: merchantid, nonce_str: NonceStr, notify_url, openid, out_trade_no: orderid, spbill_create_ip: ip, total_fee: totalmoney, trade_type: 'JSAPI' } uniorderParams.sign = ctx.helper.getPreSign(uniorderParams, merchantkey) //根据上面的这个uniorderParams统一下单参数根据ASCii码从小到大排序,加上商户密钥做sign加密 let xml = ' ' + //重点, 必须使用xml格式来发送给微信服务器端 '' + uniorderParams.appid + ' ' + '' + uniorderParams.body + ' ' + '' + uniorderParams.mch_id + ' ' + '' + uniorderParams.nonce_str + ' ' + '' + uniorderParams.notify_url + '' + '' + uniorderParams.openid + ' ' + '' + uniorderParams.out_trade_no + '' + '' + uniorderParams.spbill_create_ip + ' ' + '' + uniorderParams.total_fee + ' ' + '' + uniorderParams.trade_type + ' ' + '' + uniorderParams.sign + ' ' + ''; const temp = await ctx.curl('https://api.mch.weixin.qq.com/pay/unifiedorder', { //统一下单的地址 method: 'POST', data: xml }) let result = {} if (temp.status == 200) { result = await ctx.helper.xmlToJson(temp.data.toString()) } /** * 获取预支付的sign签名 * 带字符串QUERY的URL的&拼接 */ getPreSign: (signParams, merchantkey) => { let keys = Object.keys(signParams).sort() let newArgs = {} keys.forEach( val => { if(signParams[val]) { newArgs[val] = signParams[val] } }) const string = queryString.stringify(newArgs) + '&key=' + merchantkey return crypto.createHash('md5').update(queryString.unescape(string), 'utf8').digest("hex").toUpperCase() } //二次签名... 1. 第一步中,如果 参数没问题,发送给微信服务器中会响应到一个prepay_id,而这个 prepay_id 就是预支付的code 2. 第二步,node服务器向微信服务器发起第二次签名,小程序端无感知 //二次签名 const paysign2 = { appId: result.appid, nonceStr: result.nonce_str, package: `prepay_id=${result.prepay_id}`, timeStamp: parseInt((Date.now() / 1000)).toString(), //注意:时间必须为秒 signType: 'MD5' } paysign2.paySign = ctx.helper.getPreSign(paysign2, merchantkey) const data = { paysign2, orderid } await ctx.model.Ordertable.create({ orderid, openid, money: totalmoney * 100, status: 0 }) //在这里我做了一个支付预处理落地到数据库的操作,当预支付 return data } 在这里我做了一个支付预处理落地到数据库的操作,当预支付通过,支付数据库插入一条status为0的待确认支付状态的数据1.第二步,第二次签名中的回调数据,一起通过接口返回给小程序端 async wxappay (openid, money) { return new Promise(async (resolve, reject) => { let Objct = { openid, //拉取授权获取到的openid money, //money必须是整数类型, 以RMB分为单位! body: 'xxx' } let temp = await wxappWxPay(obj) //进入到第一阶段, 预支付阶段 //前面的逻辑为第一阶段 //下面的逻辑为第二阶段 //第二次签名 let einfo = temp.data //小程序使用wx.requestPayment拉去支付逻辑 wx.requestPayment({ timeStamp: parseInt(einfo.paysign2.timeStamp).toString(), nonceStr: einfo.paysign2.nonceStr, package: einfo.paysign2.package, signType: 'MD5', paySign: einfo.paysign2.paySign, success: async res => { if (res) { let checkdata = { nonceStr: einfo.paysign2.nonceStr, out_trade_no: einfo.orderid, sign: einfo.paysign2.paySign } //下面是第四步,省略... } }, fail: res => { console.log('@', res) reject(res) } }) } }) } 注意, wx.requestPayment的回调success , 并不一定获取到正确结果,严谨的说。由于发起支付后,后端(node) 发送成功支付后,微信服务器会要求后端进行支付成功数据回调的响应。微信支付的官方说明如下:1.第三步 回调 notify_url填写注意事项•notify_url需要填写商户自己系统的真实地址,不能填写接口文档或demo上的示例地址。•notify_url必须是以https://或http://开头的完整全路径地址,并且确保url中的域名和IP是外网可以访问的,不能填写localhost、127.0.0.1、192.168.x.x等本地或内网IP。•notify_url不能携带参数。[图片]1.node服务器中回调地址代码如图: /** * 确认支付之后的订单 * 回调(微信)再次签名(响应支付成功的结果) * @param {Object} * 1.必须给微信一个响应。支fu的结果, * */ async wxPayNotify(xmldata) { const { ctx } = this let result = await ctx.helper.xmlToJson(xmldata) if (result) { let resxml = ' ' + '' + '' + '' + '' + '' + '' + ' ' if (result.result_code == 'SUCCESS') { await ctx.model.Ordertable.update({ status: 1, transactionid: result.transaction_id, money: result.total_fee }, { where: { orderid: result.out_trade_no, openid: result.openid } }) } return resxml } } > 在这里我上面落地存储数据的支付表,在收到微信的支付成功的回调后,将状态status :0 改为1 表示支付明确 6. 第四步 主动查询 > 由于微信的回调是异步,前端不可能等待微信的回调再来进行下一步逻辑处理,万一网络波动或者其他因素导致微信服务器的回调迟迟没有到我们的数据库中来呢?所以我们需要自己主动发起查询支付结果的API > 此API为:[微信查询支付接口](https://api.mch.weixin.qq.com/pay/orderquery) > 小程序端发起查询请求给后端,后端再向微信服务器调取查询结果: > node服务器的代码如下:(本次逻辑十分重要,关系我们支付的闭环) /** * 微信支付主动调取查询订单状态API */ async checkWxPayResult(obj) { const { ctx, config } = this const { appid, merchantid, merchantkey } = config.wxapp const { nonceStr, out_trade_no } = obj let Params = { appid, mch_id: merchantid, nonce_str: nonceStr, out_trade_no: out_trade_no } Params.sign = ctx.helper.getPreSign(Params, merchantkey) let xml = ''; const temp = await ctx.curl('https://api.mch.weixin.qq.com/pay/orderquery', { method: 'POST', data: xml }) let result = {} if (temp.status == 200) { result = await ctx.helper.xmlToJson(temp.data.toString()) return result } } 7. 将响应结果发送给小程序端 + 小程序端支付的完整逻辑就呈现出来了,代码如下: async wxappay (openid, money) { return new Promise(async (resolve, reject) => { let Objct = { openid, //拉取授权获取到的openid money, //money必须是整数类型, 以RMB分为单位! body: 'xxx' } let temp = await wxappWxPay(obj) //进入到第一阶段, 预支付阶段 //前面的逻辑为第一阶段 //下面的逻辑为第二阶段 //第二次签名 let einfo = temp.data //小程序使用wx.requestPayment拉去支付逻辑 wx.requestPayment({ timeStamp: parseInt(einfo.paysign2.timeStamp).toString(), nonceStr: einfo.paysign2.nonceStr, package: einfo.paysign2.package, signType: 'MD5', paySign: einfo.paysign2.paySign, success: async res => { if (res) { //虽然是res调取成功,但是我们并不需要这个参数的逻辑回调 let checkdata = { nonceStr: einfo.paysign2.nonceStr, out_trade_no: einfo.orderid, sign: einfo.paysign2.paySign } //下面是第四步,主动查询订单的支付情况 const result = await getWxappPayResult(checkdata) if (result.code == 200) { resolve(result.data) //最后,作为promise进行返回,此刻的支付是100%正确的。还可以参照落地的数据库表进行辅助对照 } } }, fail: res => { console.log('@', res) reject(res) } }) } }) } ``` 至此,小程序的支付就完成了。后续,此文仅仅是使用的node 作为后端, 实际上流程来说,JAVA,PHP等等语言来说,逻辑思路都基本一致的。[图片] 原创不易,喜欢的朋友麻烦点点关注!后续会写java版本的支付流程 ,敬请关注References[代码][1][代码] 微信支付页链接地址: https://pay.weixin.qq.com/
2020-04-29 - 微信支付接口报【签名错误】,看这一篇就够了
此文章致力解决在开发微信支付相关接口报【签名错误】,并不断升级更新 文章demo以’普通商户版’-‘JSAPI支付’作为案例(JSAPI支付文档) 先讲一下开发步骤和经验,文章后半部分讲排错经验 开发步骤 一 设置支付目录(文档链接) 支付目录,一定要设置实际支付页面的路径以 / 结尾,如果提示<当前页面URL未注册>,请检查自己实际支付页面的路径是否填写正确 发起支付的业务流程,我们做的操作应该是这样的:用户选择支付金额和其他参数–>用户点击支付–>前端向后台发起请求获取签名等参数–>后台调用统一下单接口,返回给前端需要的签名参数–>前端调用WeixinJSBridge.invoke–>用户填写密码–>支付成功–>微信发送通知给统一下单填写的回调方法。详细业务流程点我查看 二 后台调用统一下单接口(文档链接) 此接口参数非常多,第一次开发的时候,建议开发者仔仔细细对每个参数进行比对。遇到签名错误的同学,大部分人的原因是因为参数填写错误导致的 后台在给前端准备参数的时候,是要进行两次签名的:第一次是发送统一下单请求之前,对发送给微信的所有参数进行签名;第二次是微信返回预支付交易会话标识后,对传给前端的所有参数进行签名。 请注意,第一次签名和第二次签名的时候,参数是不一样的,第二次签名的时候,签名需要哪些参数呢?签名的参数是WeixinJSBridge.invoke需要用到的参数,和第一次签名需要的参数是不一样的! 对于参数package我第一次粗心大意,没有拼接字符串‘prepay_id=’希望大家也注意一些,前后台都需要拼接这个字符串‘prepay_id=’ 这是我刚刚花费10分钱获取的统一下单截图 [图片] 只有result_code和return_code都为SUCCESS的时候,说明调用成功,成功拿到预支付的id 三 前端获取参数后拉起微信支付(文档链接) [图片] 其实完整坐下来,微信支付就这么点东西,只是大家可能有些不熟悉,对于大家遇到的签名错误问题,绝大部分是参数没有认真进行参数比对,参数不能多,也不能少。如果还报错,建议从下面一些方式进行排查 排错经验 · 首先排查签名方法是否正确(签名效验工具),如果自己写的签名方法和工具展示出来的结果一模一样,说明你签名的工具方法写的没有问题,那么就剩下参数的问题了! · 然后进行参数比对,根据开发文档,进行比对,一个字母都不能差 · 第一次签名和第二次签名的APPID ,后台签名的i是小写,前端调用的是大写 · 后端第二次签名,参数package一定不要忘记拼接prepay_id · 请再三确认appid和mch_id是否正确,如果同时进行多个公众号支付开发,一定不要弄混 · 第二次签名参数timeStamp时间差距太大(你服务器时间要尽量准确,好像误差不能超过10分钟) · 中文参数错误,英文参数没有问题的,本文以MD5加密为例,请在加密的时候,指定编码格式为UTF-8 对于企业付款到零钱/银行卡 · 尝试在商户平台的账户信息中更改API密钥(账户设置-安全设置-API安全), 15分钟后生效 · 还是参数,参数,参数 终极杀器·缓存 作者开发语言是java,之前缓存无处不在,myeclipse(开发者工具)的缓存,本地编译缓存,服务器tomcat的缓存,如果你觉得我就是对的,什么都排查过了,没有问题,OK,建议清理缓存(先删除tomcat里边的项目,再添加然后重新编译项目;服务器tomcat缓存,清理tomcat文件夹下work-catalina文件夹的内容)。实在不行,重启本地电脑。重启服务器server。 请各位同学一定要先自行排查问题,如果还无法解决问题,或者你遇到过其他bug情况,欢迎留言,我会及时更新到文章,以便帮助更多人解决签名错误的问题。ღ( ´・ᴗ・` )比心 ------------------分割线------------------- 签名方式是否真的正确? ----------2019年12月2日更新,感谢斌斌反馈---------- 由于作者做支付是在2016年,辛辛苦苦整理了一份demo,一直沿用至今,忘记当时是否有官方sdk了,如果大家用的是官方sdkDemo,以上bug排查完,还是报签名错误,请检查签名方式,实际是MD5还是HMACSHA256。具体情况可以看这篇提问【JSAPI第二次签名到底什么机制?】
2021-03-18 - 我不想小程序能被搜索到,如何操作?
制作了一个公司内部使用的小程序,因为名称比较通俗,近期发现有非内部员工的用户通过微信搜索的方式找到了这个小程序。 我想在不修改小程序代码的情况下,我的这个小程序只能通过扫描小程序二维码的方式进入,是否可以操作,如何操作? 谢谢!
2020-04-26 - 背景音频无法监听停止事件?
https://developers.weixin.qq.com/s/SxzNUTm17Mgr 解决方案:使用已经停止维护的wx.onBackgroundAudioStop(function callback)可以监听到停止事件
2020-04-26 - 可以设置dialog区域外点无效吗?
需求是 dialog 弹窗只能通过用户点击“取消”按钮后隐藏,点击dialog区域外不可以隐藏
2020-04-26 - 小程序wx.getLocation 误差?
小程序wx.getLocation type gcj02 isHighAccuracy true 用真机调试 误差20米左右 还有其他的操作弥补误差么?
2019-12-30 - 2019-04-18
- wx.getUpdateManager is not a function
- 当前 Bug 的表现(可附上截图) - 提供一个最简复现 Demo [代码]checkForUpdate() {[代码][代码] [代码][代码]const updateManager = wx.getUpdateManager()[代码][代码] [代码][代码]updateManager.onCheckForUpdate([代码][代码]function[代码][代码](res) {[代码][代码] [代码][代码]// 请求完新版本信息的回调[代码][代码] [代码][代码]console.log(res.hasUpdate)[代码][代码] [代码][代码]})[代码][代码] [代码][代码]updateManager.onUpdateReady([代码][代码]function[代码][代码]() {[代码][代码] [代码][代码]wx.showModal({[代码][代码] [代码][代码]title: [代码][代码]'更新提示'[代码][代码],[代码][代码] [代码][代码]content: [代码][代码]'新版本已经准备好,是否重启应用?'[代码][代码],[代码][代码] [代码][代码]success: [代码][代码]function[代码][代码](res) {[代码][代码] [代码][代码]if[代码] [代码](res.confirm) {[代码][代码] [代码][代码]// 新的版本已经下载好,调用 applyUpdate 应用新版本并重启[代码][代码] [代码][代码]updateManager.applyUpdate()[代码][代码] [代码][代码]}[代码][代码] [代码][代码]},[代码][代码] [代码][代码]})[代码][代码] [代码][代码]})[代码][代码] [代码][代码]updateManager.onUpdateFailed([代码][代码]function[代码][代码]() {[代码][代码] [代码][代码]// 新版本下载失败[代码][代码] [代码][代码]})[代码][代码] [代码][代码]},[代码] 2019-05-166.5.72.0.522100.00%wx.getUpdateManager is not a function;at App checkForUpdate function
2019-05-16 - 现在有一个需求是下载pdf,但是我调用wx.saveFile之后,找不到
现在有一个需求是下载pdf,但是我调用wx.saveFile之后,找不到文件,那我这么下载功能不就废了么?我打印的下载路径是wxfile://store_41a34d910ff15a255d0f9f233ed75e42.pdf,这是一个pdf文件,请问有解决方案吗?
2019-07-30 - (4)获取用户信息
背景 我们发现大部分小程序都会使用 [代码]wx.getUserInfo[代码] 接口,来获取用户信息。原本设计这个接口时,我们希望开发者在真正需要用户信息的情况下才去调取这个接口,但很多开发者会直接调用这个接口,导致用户在使用小程序的时候产生困扰,归结起来有几点: 开发者在小程序首页直接调用 [代码]wx.getUserInfo[代码] 进行授权,弹框获取用户信息,会使得一部分用户点击“拒绝”按钮。 在开发者没有处理用户拒绝弹框的情况下,用户必须授权头像昵称等信息才能继续使用小程序,会导致某些用户放弃使用该小程序。 用户没有很好的方式重新授权,尽管我们增加了[代码]设置[代码]页面,可以让用户选择重新授权,但很多用户并不知道可以这么操作。 此外,我们发现开发者默认将 [代码]wx.login[代码] 和 [代码]wx.getUserInfo[代码] 绑定使用,这个是由于我们一开始的设计缺陷和实例代码导致的([代码]wx.getUserInfo[代码] 必须通过 [代码]wx.login[代码] 在后台生成 [代码]session_key[代码]后才能调用)。同时,我们收到开发者的反馈,希望用户进入小程序首页便能获取到用户的 [代码]unionId[代码],以便识别到用户是否以前关注了同主体公众号或使用过同主体的App 。 为了解决以上问题,针对获取用户信息我们更新了三个能力: 1.使用组件来获取用户信息 2.若用户满足一定条件,则可以用[代码]wx.login[代码] 获取到的[代码]code[代码]直接换到[代码]unionId[代码] 3.[代码]wx.getUserInfo[代码] 不需要依赖 [代码]wx.login[代码] 就能调用得到数据 获取用户信息组件介绍 [代码][代码] 组件变化: [代码]open-type [代码]属性增加 [代码]getUserInfo[代码] :用户点击时候会触发 [代码]bindgetuserinfo[代码] 事件。 新增事件 [代码]bindgetuserinfo[代码] :当 [代码]open-type[代码]为 [代码]getUserInfo[代码] 时,用户点击会触发。可以从事件返回参数的 [代码]detail[代码] 字段中获取到和 [代码]wx.getUserInfo[代码] 返回参数相同的数据。 示例: [代码]<button open-type="getUserInfo" bindgetuserinfo="userInfoHandler"> Click me button>[代码]和 [代码]wx.getUserInfo[代码] 不同之处在于: 1.API [代码]wx.getUserInfo[代码] 只会弹一次框,用户拒绝授权之后,再次调用将不会弹框; 2.组件 [代码][代码][代码][代码] 由于是用户主动触发,不受弹框次数限制,只要用户没有授权,都会再次弹框。 通过获取用户信息的组件,就可以解决用户再次授权的问题。 直接获取unionId开发者申请 [代码]userinfo[代码] 授权主要为了获取 [代码]unionid[代码],我们鼓励开发者在不骚扰用户的情况下合理获得[代码]unionid[代码],而仅在必要时才向用户弹窗申请使用昵称头像。为此,凡使用“获取用户信息组件”获取用户昵称头像的小程序,在满足以下全部条件时,将可以静默获得 [代码]unionid[代码]: 1.在微信开放平台下存在同主体的App、公众号、小程序。 2.用户关注了某个相同主体公众号,或曾经在某个相同主体App、公众号上进行过微信登录授权。 这样可让其他同主体的App、公众号、小程序的开发者快速获得已有用户的数据。 不依赖登录的用户信息获取某些工具类的轻量小程序不需要登录行为,但是也想获取用户信息,那么就可以在 [代码]wx.getUserInfo[代码] 的时候加一个参数 [代码]withCredentials: false[代码] 直接获取到用户信息,可以少一次网络请求。 这样可以在不给用户弹窗授权的情况下直接展示用户的信息。 最佳实践 1.调用 [代码]wx.login[代码] 获取 [代码]code[代码],然后从微信后端换取到 [代码]session_key[代码],用于解密 [代码]getUserInfo[代码]返回的敏感数据。 2.使用 [代码]wx.getSetting[代码] 获取用户的授权情况 1) 如果用户已经授权,直接调用 API [代码]wx.getUserInfo[代码] 获取用户最新的信息; 2) 用户未授权,在界面中显示一个按钮提示用户登入,当用户点击并授权后就获取到用户的最新信息。 3.获取到用户数据后可以进行展示或者发送给自己的后端。 One More Thing 除了获取用户方案介绍之外,再聊一聊很多初次接触微信小程序的开发者所不容易理解的一些概念: 1.关于OpenId和UnionId [代码]OpenId[代码] 是一个用户对于一个小程序/公众号的标识,开发者可以通过这个标识识别出用户。 [代码]UnionId[代码] 是一个用户对于同主体微信小程序/公众号/APP的标识,开发者需要在微信开放平台下绑定相同账号的主体。开发者可通过[代码]UnionId[代码],实现多个小程序、公众号、甚至APP 之间的数据互通了。 同一个用户的这两个 ID 对于同一个小程序来说是永久不变的,就算用户删了小程序,下次用户进入小程序,开发者依旧可以通过后台的记录标识出来。 2.关于 getUserInfo 和 login 很多开发者会把 [代码]login[代码] 和 [代码]getUserInfo[代码] 捆绑调用当成登录使用,其实 [代码]login[代码] 已经可以完成登录,[代码]getUserInfo[代码] 只是获取额外的用户信息。 在 [代码]login[代码] 获取到 [代码]code[代码] 后,会发送到开发者后端,开发者后端通过接口去微信后端换取到 [代码]openid[代码] 和[代码]sessionKey[代码](现在会将 [代码]unionid[代码] 也一并返回)后,把自定义登录态 [代码]3rd_session[代码]返回给前端,就已经完成登录行为了。而 [代码]login[代码] 行为是静默,不必授权的,用户不会察觉。 [代码]getUserInfo[代码] 只是为了提供更优质的服务而存在,比如展示头像昵称,判断性别,开发者可通过 [代码]unionId[代码] 和其他公众号上已有的用户画像结合来提供历史数据。因此开发者不必在用户刚刚进入小程序的时候就强制要求授权。 可以在官方的文档中看到 [代码]login[代码] 的最佳实践: [图片] Q & A Q1: 为什么 login 的时候不直接返回 openid,而是要用这么复杂的方式来经过后台好几层处理之后才能拿到? A: 为了防止坏人在网络链路上做手脚,所以小程序端请求开发者服务器的的请求都需要二次验证才是可信的。因为我们采取了小程序端只给 [代码]code[代码] ,由服务器端拿着 [代码]code[代码] 和 [代码]AppSecrect[代码] 去微信服务器请求的方式,才会给到开发者对应的[代码]openId[代码] 和用于加解密的 [代码]session_key。[代码] Q2: 既然用户的[代码]openId[代码] 是永远不变的,那么开发者可以使用[代码]openId[代码] 作为用户的登录态么? A: 不行,这是非常危险的行为。因为 [代码]openId[代码] 是不变的,如果有坏人拿着别人的 [代码]openId[代码] 来进行请求,那么就会出现冒充的情况。所以我们建议开发者可以自己在后台生成一个拥有有效期的 [代码]第三方session[代码] 来做登录态,用户每隔一段时间都需要进行更新以保障数据的安全性。 Q3: 是不是用户每次打开小程序都需要重新[代码]login[代码]? A: 不必,可以将登录态存入[代码]storage[代码]中,用户再次登录就可以拿[代码]storage[代码] 里的登录态做正常的业务请求,只有当登录态过期了之后才需要重新[代码]login[代码] 。这样子做一则可以减少用户等待时间,二则可以减少网络带宽。 目前微信的[代码]session_key[代码] 有效期是三天,所以建议开发者设置的登录态有效期要小于这个值。
2018-08-17 - 为什么不建议用openid作为登录凭证?
在开发小程序的过程中,登录是一个入口场景,基本每个开发者都会遇到, 那么在开发过程中,我们知道 既然openid是唯一的,那我为什么不能用openid作为凭证,还要麻烦的用个第三方session 其实我之前也一直不明白,今天看了下面这个例子,顿时豁然开朗 有可能造成数据越权。 比如今天我通过我的手机登录了微信,打开了小程序。但是明天有个朋友想用我的手机登一下微信。如果用openid作为登录凭证,登录小程序的时候检测到openid已经存在,所以不会再走登录过程,这样我的数据就让我的朋友看到了。所以还是要按照官方推荐的步骤来。 ### 20191224 https://developers.weixin.qq.com/community/develop/doc/0002a028214de86e94079941551800 小明同学很稀罕同桌小花,有天看到小花在某个微信公众号写日记,好巧,猥琐的小明看到并记住了小花的开屏密码。等课间小花同学出去时,将她手机开机并打开了那个公众号,进入了个人中心。 哎呀,时间不够看呀,于是选择了用浏览器打开看到了URL。 你说巧不巧,这个站竟然在URL里有个openid的传值,没有登陆鉴权。 小明用他无比迅捷的手速把url发给了自己的号,还不着痕迹地打扫了战场。 以后的日子里,小明时刻都能通过点击那个url翻看小花的日记,真是爽煞,发起了向女神攻心的神级技能。 ### 20200107 更新:下面这个文章有说明为什么不用openid作为登录态 https://developers.weixin.qq.com/community/develop/doc/000c2424654c40bd9c960e71e5b009 Q2: 既然用户的openId是永远不变的,那么开发者可以使用openId作为用户的登录态么? A:行,这是非常危险的行为。因为openId是不变的,如果有坏人拿着别人的openId来进行请求,那么就会出现冒充的情况。所以我们建议开发者可以自己在后台生成一个拥有有效期的第三方session来做登录态,用户每隔一段时间都需要进行更新以保障数据的安全性。
2020-01-07 - 响应式原理解析
Vue响应式 前言 数据响应式系统是Vue最显著的一个特性,我们通过Vue官方文档回顾一下。 数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。这使得状态管理非常简单直接,不过理解其工作原理同样重要,这样你可以避开一些常见的问题。 现在是时候深入一下了! 本文针对响应式系统的原理进行一个详细介绍。 响应式是什么 我们先来看一个例子 [代码]<div id="app"> <p>{{color}}</p> <button @click="changeColor">change color!</button> </div> new Vue({ el: '#app', data() { color: 'blue' }, methods: { changeColor() { this.color = 'yellow'; } } }) [代码] 当我们点击按钮的时候,视图的p标签文本就会从 [代码]blue[代码]改变成[代码]yellow[代码]。 Vue要完成这次更新,其实需要做两件事情: 监听数据[代码]color[代码]的变化。 当数据[代码]color[代码]更新变化时,自动通知依赖该数据的视图。 换成专业那么一点点点的名词就是利用[代码]数据劫持/数据代理[代码]去进行[代码]依赖收集[代码]、[代码]发布订阅模式[代码]。 我们只需要记住一句话:在getter中收集依赖,在setter中触发依赖 如何追踪侦测数据的变化 首先有个问题,如何侦测一个对象的变化? 目前来说,侦测对象变化有两种方法。大家都知道的! Object.defineProperty vue 2.x就是使用[代码]Object.defineProperty[代码]来数据响应式系统的。但是用此方法来来侦测变化会有很多缺陷。例如: Object.defineProperty无法监控到数组下标的变化,导致通过数组下标添加元素,不能实时响应; Object.defineProperty只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果,属性值是对象,还需要深度遍历。 … 本文也是利用Object.defineProperty来介绍响应式系统。 Proxy vue3就是通过proxy实现响应式系统的。而且在国庆期间已经发布pre-alpha版本。 相比旧的[代码]Object.defineProperty[代码], [代码]proxy[代码]可以代理整个对象,并且提供了多个[代码]traps[代码],可以实现诸多功能。此外,Proxy支持代理数组的变化等等 当然proxy也有一个致命的缺点,就是无法通过[代码]polyfill[代码]模拟,兼容性较差。 依赖收集的重要角色 Dep Watcher [代码]Dep[代码]、[代码]Watcher[代码]是数据响应式中两个比较重要的角色。 收集依赖的地方 Dep 因为在视图模板上可能有多处地方都引用同一个数据,所以要有一个地方去存放数据的依赖,这个地方就是Dep。 Dep主要维护一个依赖的数组,当我们利用render函数生成VNode的时候,会触发数据的getter,然后则会把依赖push到Dep的依赖数组中。 依赖是Watcher! 我们可以把[代码]Watcher[代码]理解成一个中介的角色,数据发生变化时,会触发数据的setter,然后通过遍历Dep中的依赖Watcher,然后Watcher通知额外的操作,额外的操作可能是更新视图、更新computed、更新watch等等… 原理实现 还是那句话:在getter中收集依赖,在setter中触发依赖。 下面我们看看代码: 代码有点长,下面会有步骤来讲解一次。 [代码]/* * 劫持数据的getter、setter */ function defineReactive(data, key, val) { // 1-1:把color数据变成响应式 const dep = new Dep(); Object.defineProperty(data, key, { enumerable: true, configurable: true, get() { // 3. 因为模板编译watcher访问到了color,从而触发get方法,触发了收集依赖的方法 dep.depend(); return val; }, set(newVal) { if (val === newVal) { return; } val = newVal; // 4-1. 假设我们通过 `this.color = 'yellow';`去更改`color`的值,就会触发set方法。 dep.notify(); } }); } /* * dep类,收集依赖,和触发依赖 */ class Dep { constructor() { this.subs = []; // 收集依赖的数组 } // 收集依赖 depend() { // 3-1. 通过外部的变量来添加到color的依赖中. if (window.target && !this.subs.includes(window.target)) { this.subs.push(window.target); } } // 通知依赖更新 notify() { // 4-2. 遍历 this.subs.forEach(watcher => { watcher.update(); }); } } /** * 数据与外部的中介 */ class Watcher { constructor(expr, cb) { this.cb = cb; this.expr = expr; // 2-1. 这里触发了get方法 this.value = this.get(); } get() { // 2-2. 这里把自己(watcher)赋值给了外部其中的一个变量 window.target = this; // 2-3. data[this.expr]触发了color的get const value = data[this.expr]; window.target = undefined; return value; } update() { this.value = this.get(); this.cb(this.value); } } [代码] 下面我们来走一次流程。 括号里面的1-1,2-2是对应代码的执行点。 1. 把数据变成响应式 利用[代码]defineReactive[代码]把color数据变成响应式(1-1),执行过这个方法后,我们调用[代码]console.log(this.color)[代码]的时候可以触发get方法。同理当我们[代码]this.color = 'yellow'[代码]。 注意:在[代码]Object.defineProperty[代码]上面初始化一个存放依赖的dep,这里其实是把dep作为数据[代码]color[代码]的一个私有变量,让get和set的方法可以访问到,也是我们经常说的闭包。 2. 编译模板创建watcher 假设我们现在编译模板遇到[代码]{{color}}[代码]。 Vue就会创建一个Watchter,伪代码如下: [代码]new Watcher('color', () => { // 当color发生变化的时候,会触发这里的方法。 }); [代码] 这里高能!! Watcher的构造函数里面调用了[代码]get()[代码]方法(2-2),把自己(watcher)赋值给了一个外部变量。 然后再触发get方法(2-3)。 3. get中收集依赖 因为模板编译watcher访问到了color,从而触发get方法,触发了收集依赖的方法。 进入到[代码]dep.depend[代码]方法中(3-1),这里因为在Watcher中把自己存到了外部变量中,所以在dep.depend方法中可以收集到依赖。 现在,依赖就被收集了。 4. 通过setter触发依赖 假设我们通过 [代码]this.color = 'yellow';[代码]去更改[代码]color[代码]的值,就会触发set方法,执行dep.notify(4-1)。 会遍历依赖数组,从而去触发Watcher的cb方法。 cb就是上面伪代码[代码]new Watcher[代码]的那个回调函数。 只要回调函数里面运行了操作dom方法,或者触发了diff算法更新dom,都可以把视图进行更新。 响应式简易流程大概就是这样了… 侦测数据变化的类型 其实数据监听变化有两种类型,一种是“推”(push),另一种是“拉”(pull)。 React和Angular中的变化侦测都属于“拉”,就是说在数据发生变化的时候,它不知道哪个数据变了,然后会发送一个信号给框架,框架收到信号后,会进行一个暴力比对来找出那些dom节点需要重新渲染。Angular中使用的是脏检查,在React使用虚拟dom的diff。 vue的数据监听属于“推”。当数据发生变化时,就知道哪个数据发生变化了,从而去更新有此数据依赖的视图。 因此,框架知道的数据更新信息越多,也就可以进行更细粒度的更新。比如,直接通过dom api操作dom。 相对“拉”的力度是最粗的。看到这里,是不是觉得vue的更新效率最快。 我们看看下面的例子 [代码]<template> <div>{{a}}</div> <div>{{b}}</div> </template> [代码] 这里我们看出模板只有a、b两个数据依赖,也就是说我们要创建两个闭包dep去存放两个watcher依赖,我们知道闭包的缺点就是内存泄露。如果有1000个数据依赖在模板上,每个数据所绑定的依赖就越多,依赖追踪在内存上的开销就会越大。 所以,从Vue.js2.0开始,它引入了虚拟dom,一个状态所绑定的依赖不再是具体的dom节点,而是一个组件,即一个组件一个Watcher。 这样状态变化后,会通知到组件,组件内部再使用虚拟 dom进行比对。这可以大大降低依赖数量,从而降低依赖追踪所消耗的内存。但并不是引入虚拟dom后,渲染速度变快了。准确的来说,应该是80%的场景下变得更快了,而剩下的20%反而变慢了。 个人觉得,鱼和熊掌不可兼得。 “推”是牺牲内存来换更新速度。 “拉”则是牺牲更新速度来获取内存。 总结 Vue响应式的灵魂:在getter中收集依赖,在setter中触发依赖。 [图片] 我们再看看图,回顾一下整个流程。 通过[代码]defineReactive[代码],遍历data里面的属性,把数据的getter/setter劫持,用来收集和触发依赖。 当外界通过Watcher读取数据时,会触发getter从而将Watcher添加到Dep依赖中。 当数据发生变化时,会触发setter,从而Dep会向依赖(Wachter)发送通知。 Watcher收到通知后,会向外界发送通知,变化通知到外界后可能接触视图更新,也有可能触发用户的某个回调函数等等。 参考文献:《深入浅出Vue.js》
2019-12-31 - 在scroll-view中使用sticky
.container{ width: 600rpx; height: 800rpx; } .a { position: sticky; position: -webkit-sticky; top: 0; width: 100%; height: 200rpx; background-color: black; } .b { width: 100%; height: 3000rpx; background-color: red; } <scroll-view class="container"> <view> <view class="a"></view> <view class="b"></view> </view> </scroll-view> sticky的元素在到达父元素的底部时会失效 scroll-view的高度为800rpx,但是scrollHeight为3200rpx,所以在scroll-view中嵌套一个view就能顺利定位
2019-12-18 - 如何实现圣诞节星星飘落效果
圣诞节快到啦~🎄🎄🎄🎄咱们也试着做做小程序版本的星星✨飘落效果吧~ 先来个效果图: [图片] 484听起来很叼,看起来也就那样。 来咱们上手开始撸 页面内容wxml,先简单切个图吧。 [代码]<view class="container"> <image src="https://qiniu-image.qtshe.com/merry_001.jpg" mode="widthFix"/> <image src="https://qiniu-image.qtshe.com/merry_02.jpg" alt="" mode="widthFix"/> <image src="https://qiniu-image.qtshe.com/merry_03.jpg" alt="" mode="widthFix"/> <image src="https://qiniu-image.qtshe.com/merry_04.jpg" alt="" mode="widthFix"/> <image src="https://qiniu-image.qtshe.com/merry_05.jpg" alt="" mode="widthFix"/> <image src="https://qiniu-image.qtshe.com/merry_06.jpg" alt="" mode="widthFix"/> <image src="https://qiniu-image.qtshe.com/merry_07.jpg" alt="" mode="widthFix"/> </view> <canvas canvas-id="myCanvas" /> [代码] 页面样式wxss,因为切片用的不太熟练,图片之间有个2rpx的空隙。 [代码].container { height: 100%; box-sizing: border-box; min-height: 100vh; } image { width: 100%; display: block; margin-top: -2rpx; //不会切图造的孽 } canvas { width: 100%; min-height:100vh; position: fixed; top: 0; z-index: 888; } [代码] 重点JS: [代码]//获取应用实例 const app = getApp() // 存储所有的星星 const stars = [] // 下落的加速度 const G = 0.01 const stime = 60 // 速度上限,避免速度过快 const SPEED_LIMIT_X = 1 const SPEED_LIMIT_Y = 1 const W = wx.getSystemInfoSync().windowWidth const H = wx.getSystemInfoSync().windowHeight var starImage = '' //星星素材 wx.getImageInfo({ src: 'https://qiniu-image.qtshe.com/WechatIMG470.png', success: (res)=> { starImage = res.path } }) Page({ onLoad() { this.setAudioPlay() }, onShow() { this.createStar() }, createStar() { let starCount = 350 //星星总的数量 let starNum = 0 //当前生成星星数 let deltaTime = 0 let ctx = wx.createCanvasContext('myCanvas') let requestAnimationFrame = (() => { return (callback) => { setTimeout(callback, 1000 / stime) } })() starLoop() function starLoop() { requestAnimationFrame(starLoop) ctx.clearRect(0, 0, W, H) deltaTime = 20 //每次增加的星星数量 starNum += deltaTime if (starNum > starCount) { stars.push( new Star(Math.random() * W, 0, Math.random() * 5 + 5) ); starNum %= starCount } stars.map((s, i) => { //重复绘制 s.update() s.draw() if (s.y >= H) { //大于屏幕高度的就从数组里去掉 stars.splice(i, 1) } }) ctx.draw() } function Star(x, y, radius) { this.x = x this.y = y this.sx = 0 this.sy = 0 this.deg = 0 this.radius = radius this.ax = Math.random() < 0.5 ? 0.005 : -0.005 } Star.prototype.update = function() { const deltaDeg = Math.random() * 0.6 + 0.2 this.sx += this.ax if (this.sx >= SPEED_LIMIT_X || this.sx <= -SPEED_LIMIT_X) { this.ax *= -1 } if (this.sy < SPEED_LIMIT_Y) { this.sy += G } this.deg += deltaDeg this.x += this.sx this.y += this.sy } Star.prototype.draw = function() { const radius = this.radius ctx.save() ctx.translate(this.x, this.y) ctx.rotate(this.deg * Math.PI / 180) ctx.drawImage(starImage, -radius, -radius * 1.8, radius * 2, radius * 2) ctx.restore() } }, setAudioPlay() { let adctx = wx.createInnerAudioContext() adctx.autoplay = true adctx.loop = true adctx.src = 'https://dn-qtshe.qbox.me/Jingle%20Bells.mp3' adctx.onPlay(() => { console.log('开始播放') adctx.play() }) } }) [代码] 以上只是简单实现了一个星星飘落的效果,预览的时候需要开启不校验合法域名哦~ 目前还有更优的h5版本,使用Three.js实现的,在小程序内使用Three.js对于我来说有点打脑壳,先把效果分享出来吧。 h5版本,手机看效果最佳 h5源码可直接右键查看:https://qiniu-web.qtshe.com/merryChrimas.html
2023-12-07 - ✨【进阶技能】小程序内wxParse读取的富文本中插入小程序商品链接,直接在文章中跳转商品详情,商品可插入文章任意位置
今天说一个进阶技能,怎么用wxParse读取的富文本中插入商品链接,首先你得懂得使用wxParse 和后台网页的富文本编辑器,这篇文章并不是基础讲解这俩个插件的使用方法,这里只作概述,如需详细了解,还请搜索这俩个插件的详细使用方法,弄懂之后再看本文,会比较容易理解 最终效果: 1.后台文本编辑器,添加商品链接效果: [图片] 2。小程序内文章中自动转为商品模块效果: [图片] *其中小程序的富文本页面的商品模块可以直接点击跳转到商品详情界面 实现流程: 一 插件简介及下载地址 1.wxParse:小程序中的HTML解析器,可以将HTML代码直接转换为小程序可以显示的代码,一般用于后台传入HTML富文本,小程序中直接显示文章 GitHub地址:https://github.com/icindy/wxParse 使用简介: [图片] 解压出来后,直接复制到小程序项目的根级目录, 在需要解析HTML的页面中加入代码 wxml:(除了目录地址,其他都是固定格式,不需要修改) import src="../../wxParse/wxParse.wxml" /> js:(这里的res.data.content就是我们后台传过来的HTML富文本代码,其他参数都不需要改) const WxParse = require('../../wxParse/wxParse.js'); onLoad: function (options) { WxParsewxParse('article', 'html', resdatacontent, that, 0); }, 2.Ueditor:网页的富文本编辑器插件,下载地址某度搜Ueditor官网就出来了,这种编辑器很多,关键不是TX的,就不发使用说明和下载地址了,怕给我文章封了,嘿嘿,自行查找吧 二 后台富文本编辑器修改 本文以Ueditor为例子,使用其他编辑器插件,可以看看实现原理 1.修改link插件文件:(说明:Ueditor有添加自定义按钮的方法,不过呢,因为我这里用的编辑器都是给小程序传的HTML编码,添加A标签的功能用不上,就直接修改了,有兴趣的可以额外添加一个按钮) [图片] 下面是完整代码,修改一下你的AJAX 调用接口地址后,直接复制,替换截图中的link.html文件就可以使用了 htmlhead title> scripttype"text/javascript"src"../internal.js"> --> tr td> 原title --> tr tdcolspan"2" labelfor"target"> ";// }else{// $G("msg").innerHTML = "";// } //这里的AJAX请求地址,需要改成你的接口地址 createRequest('你的AJAX接口地址.html?id='+$G('href').value.replace(/^\s+|\s+$/g, ''),getgoodinfo); //提示:这里的这个请求接口,用户用F12是可以看到的,所以调用的时候,一定要作加密认证,不要只传ID }; functionhrefStartWith(href,arr){ href = href.replace(/^\s+|\s+$/g, ''); forvar i=,ai;ai=arr[i++];){ if(href.indexOf(ai)==){ returntrue; } } returnfalse; } /获取ajax回传数据 unction getgoodinfo(){ if(http_request.readyState == ){ if(http_request.status==200){ if(http_request.responseText==){ alert("该ID商品不存在"); document.getElementById('title').value=; }else{ document.getElementById('text').value="商品链接:" + http_request.responseText.split(",")[]; //这里我是以 商品ID,商品名称,商品图标地址,商品价格 以‘,’英文逗号链接成的字符串,可以根据你的业务需要修改, document.getElementById('title').value=http_request.responseText; } }else{ alert("您请求的页面发生错误"); } } ar http_request = false; /生成ajax unction createRequest(url,func){ //Mozilla 等其他浏览器 if(window.XMLHttpRequest){ http_request = new XMLHttpRequest(); if(http_request.overrideMimeType){ http_request.overrideMimeType("text/xml"); } }elseif(windows.ActiveXObject){ try{ http_request = new ActiveXObject("Msxml2.XMLHTTP"); }catch(e){ try{ http_request = new ActiveXObject("Microsoft.XMLHTTP"); }catch(e){} } } if(!http_request){ alert("浏览器不支持访问"); returnfalse; } http_request.onreadystatechange = func; //发出http请求 http_request.open("GET",url,true); http_request.send(null); } 三 小程序端wxParse插件代码修改 1.修改html2json.js文件: [图片] 在该文件的146行添加如下代码:(这里我是以 商品ID,商品名称,商品图片地址,商品价格以‘,’英文逗号隔开组成字符串传过来的,可以根据你的业务需求更改识别方式) 如图: [图片] //将A标签的title转为商品信息 if (nodetag === 'a' && nodeattr.title!=) { //console.log(nodenode.attr.title) var title = nodeattr.title.split(",") nodeattr.title.split(",") nodegoodname = title[] nodegoodicon = title[] nodegoodprice = title[] } 2.修改wxParse.wxml文件: [图片] 在文件177行找到<!--a类型--> 进行修改,如图:[图片] 代码如下:(注释掉的是原本插件的代码,这里的商品模块CSS就自行设计编写吧,直接写到要调用wxParse的页面wxss文件中就行) blockwx:elif{{item.tag == 'a'}}viewbindtap"wxParseTagATap"class"wxParse-inline {{item.classStr}} wxParse-{{item.tag}}data-src{{item.attr.href}}style{{item.styleStr}}viewclass"goodbg row"imageclass"goodimg"src{{item.goodicon}}> 3.修改wxParse.js文件: 这个主要是修改一下点击跳转标签的路径,因为我这里是直接是把商品的ID传了过来,如果直接传了绝对路径这一步可以省略 [图片] [图片] 这里涂抹的地方换成你的小程序路径 到此,修改结束,就可以使用了! 后记 1.该方法可以改为跳转其他的页面路径也可以,稍微修改一下就行 2.后台的编辑器我没弄成可视化的商品跳转组件,这个如果有兴趣的同学搞出来了,给大家分享一下
2021-04-15 - 如何实现一个自定义数据版省市区二级、三级联动
社区可能有其他的方案了,但是再分享下吧,给有需要的童鞋。 效果图: [图片] 额,这个视频转GIF因为社区上传不了大图,所以剪了一部分,具体的效果还是直接工具打开代码片段预览吧~ 第一步:你的页面JSON引入该组件: [代码]{ "usingComponents": { "city-picker": "/components/cityPicker/index" } } [代码] 第二步:你的页面WXML引入该组件 [代码]<city-picker visible="{{visible}}" column="2" bind:close="handleClick" bind:confirm="handleConfirm" /> [代码] 第三步:你的页面JS调用 [代码]// 显示/隐藏picker选择器 handleClick() { this.setData( visible: !this.data.visible }) }, // 用户选择城市后 点击确定的返回值 handleConfirm(e) { const { detail: { provinceName = '', provinceId = '', cityName, cityId='', areaName = '', areaId = '' } = {} } = e this.setData({ cityId, cityName, areaId, areaName, provinceId, provinceName }) } [代码] 组件属性 属性 默认值 描述 visible false 是否显示picker选择器 column 3 显示几列,可选值:1,2,3 values [0, 0, 0] 必填,默认回填的省市区下标,可选择具体省市区后查看AppData的regionValue字段 close function 点击关闭picker弹窗 confirm function 点击选择器的确定返回值 confirm: 属性 默认值 描述 provinceName 北京市 省份名称 provinceId 110000 省份ID cityName 市辖区 城市名称 cityId 110100 城市ID areaName 东城区 区域名称 areaId 110000 区域Id 至于怎么获取你想默认城市的下标,可以滑动操作下选中省市区后,点击确定后查看appData里的regionValue的值。 以上就是一个自定义数据版本的省市区二级、三级联动啦,老规矩,结尾放代码片段。 https://developers.weixin.qq.com/s/F9k9cTmT7LAz
2022-07-20 - 微信小程序UI组件库合集
UI组件库合集,大家有遇到好的组件库,欢迎留言评论然后加入到文档里。 第一款: 官方WeUI组件库,地址 https://developers.weixin.qq.com/miniprogram/dev/extended/weui/ 预览码: [图片] 第二款: ColorUI:地址 https://github.com/weilanwl/ColorUI 预览码: [图片] 第三款: vantUI(又名:ZanUI):地址 https://youzan.github.io/vant-weapp/#/intro 预览码: [图片] 第四款: MinUI: 地址 https://meili.github.io/min/docs/minui/index.html 预览码: [图片] 第五款: iview-weapp:地址 https://weapp.iviewui.com/docs/guide/start 预览码: [图片] 第六款: WXRUI:暂无地址 预览码: [图片] 第七款: WuxUI:地址https://www.wuxui.com/#/introduce 预览码: [图片] 第八款: WussUI:地址 https://phonycode.github.io/wuss-weapp/quickstart.html 预览码: [图片] 第九款: TouchUI:地址 https://github.com/uileader/touchwx 预览码: [图片] 第十款: Hello UniApp: 地址 https://m3w.cn/uniapp 预览码: [图片] 第十一款: TaroUI:地址 https://taro-ui.jd.com/#/docs/introduction 预览码: [图片] 第十二款: Thor UI: 地址 https://thorui.cn/doc/ 预览码: [图片] 第十三款: GUI:https://github.com/Gensp/GUI 预览码: [图片] 第十四款: QyUI:暂无地址 预览码: [图片] 第十五款: WxaUI:暂无地址 预览码: [图片] 第十六款: kaiUI: github地址 https://github.com/Chaunjie/kai-ui 组件库文档:https://chaunjie.github.io/kui/dist/#/start 预览码: [图片] 第十七款: YsUI:暂无地址 预览码: [图片] 第十八款: BeeUI:git地址 http://ued.local.17173.com/gitlab/wxc/beeui.git 预览码: [图片] 第十九款: AntUI: 暂无地址 预览码: [图片] 第二十款: BleuUI:暂无地址 预览码: [图片] 第二十一款: uniydUI:暂无地址 预览码: [图片] 第二十二款: RovingUI:暂无地址 预览码: [图片] 第二十三款: DojayUI:暂无地址 预览码: [图片] 第二十四款: SkyUI:暂无地址 预览码: [图片] 第二十五款: YuUI:暂无地址 预览码: [图片] 第二十六款: wePyUI:暂无地址 预览码: [图片] 第二十七款: WXDUI:暂无地址 预览码: [图片] 第二十八款: XviewUI:暂无地址 预览码: [图片] 第二十九款: MinaUI:暂无地址 预览码: [图片] 第三十款: InyUI:暂无地址 预览码: [图片] 第三十一款: easyUI:地址 https://github.com/qq865738120/easyUI 预览码: [图片] 第三十二款 Kbone-UI: 地址 https://wechat-miniprogram.github.io/kboneui/ui/#/ 暂无预览码 第三十三款 VtuUi: 地址 https://github.com/jisida/VtuWeapp 预览码: [图片] 第三十四款 Lin-UI 地址:http://doc.mini.talelin.com/ 预览码: [图片] 第三十五款 GraceUI 地址: http://grace.hcoder.net/ 这个是收费的哦~ 预览码: [图片] 第三十六款 anna-remax-ui npm:https://www.npmjs.com/package/anna-remax-ui/v/1.0.12 anna-remax-ui 地址: https://annasearl.github.io/anna-remax-ui/components/general/button 预览码 [图片] 第三十七款 Olympus UI 地址:暂无 网易严选出品。 预览码 [图片] 第三十八款 AiYunXiaoUI 地址暂无 预览码 [图片] 第三十九款 visionUI npm:https://www.npmjs.com/package/vision-ui 预览码: [图片] 第四十款 AnimaUI(灵动UI) 地址:https://github.com/AnimaUI/wechat-miniprogram 预览码: [图片] 第四十一款 uView 地址:http://uviewui.com/components/quickstart.html 预览码: [图片] 第四十二款 firstUI 地址:https://www.firstui.cn/ 预览码: [图片]
2023-01-10 - 从CSS角度来做一个模态框
最近社区有文章提到了制作模态框,今天我们从CSS角度来做一个下图这种模态框。 [图片] 先上代码:https://developers.weixin.qq.com/s/fINXOVmC7wdn 模态框背后的背景是一个fixed定位,四个方向都为0的view,主要是怎么实现中间弹窗的垂直水平居中,这里提供两个方案。 1、定位 这种方案是将弹窗设置绝对定位后,左边(上边)距设为50%,就是弹窗左上角距父级左上角的距离正好是父级的宽度(高度)的50%,就是在正中间,同时使用[代码]transform[代码]属性,将弹窗向左(上)移动自身宽度(高度)的50%,来实现弹窗正好在水平(垂直)方向都能居中。 [代码]position: absolute; top: 50%; left: 50%; transform: translateX(-50%) translateY(-50%); [代码] 2、flex布局 这种方案更简单,只需要将父级设置为flex布局的同时设置[代码]justify-content[代码]和[代码]align-items[代码]都是[代码]center[代码]就可以了。 [代码]display: flex; justify-content: center; align-items: center; [代码] 彩蛋 模态框弹出后,在父级上加上[代码]catchtouchmove="ture"[代码]就可以阻止背后页面的上下滚动。
2019-12-27 - 小程序里使用es7的async await语法
我们做小程序开发时,有时候想让自己代码变得整洁,异步操作时避免回调地狱.我们会使用es6的promise. es7的async,await . promise在小程序和云开发的云函数里都可以使用. async和await只能在云开发的云函数里使用.我们在小程序的代码里直接使用,就会报如下错误. [图片] 这个报错就是告诉我们不能在小程序里直接使用es7的async和await语法.但是这么好的语法我们用起来确实显得代码整洁,逼格高. 那接下来我就教大家如何在小程序代码里使用es7的async和await语法. 一,下载facebook出的runtime.js类库 [图片] 其实这个问题,一些大厂已经给出了解决方案.如上图,我们只需要把facebook出的这个runtime.js类库下载下来,然后放到我们的小程序项目里. 下载链接:https://github.com/facebook/regenerator/blob/master/packages/regenerator-runtime/runtime.js github有时候下载比较慢,我也提前把这个类库下载好放我网盘里了. [图片] 下载链接:https://pan.baidu.com/s/19n5wmjIKK3PAPbcXBzWmQA 提取码:xxll 如果链接失效,可以在底部 留言,或者私信石头哥获取. 二,下载后,把runtime.js放到我们项目里 我这里把runtime.js放到我的utils目录下,如果你没有utils目录,可以新建. [图片] 三,代码里引入runtime.js类库 这里建议大家用 require语法引入. [图片] 这里需要注意的是.上图我们引入runtime.js时的变量名regeneratorRuntime必须和我这里一模一样.要不然就会引入不成功. 引入完后,在编译代码,可以看到控制台不再报我们一开始的错误 [图片] 四,简单使用async和await 首先要知道我们async和await是结合使用的. [图片] 上图是我简单写的一个定时器来模拟异步等待.只要我们这里成功的引入runtime.js类库,后面想使用async和await就方便很多了. 今天就讲到这里.想学习更多小程序相关的知识,请持续关注.下期见
2019-12-10 - 各种路由情况下使用 getCurrentPages 可能存在的陷阱
废话不多说,直接上微信API。 PageObject[] getCurrentPages()获取当前页面栈。数组中第一个元素为首页,最后一个元素为当前页面。 注意: 不要尝试修改页面栈,会导致路由以及页面状态错误。 不要在 [代码]App.onLaunch[代码] 的时候调用 [代码]getCurrentPages[代码],此时 [代码]page[代码] 还没有生成。 这是微信官方对 getCurrentPages 做的解析和使用注意点,相信大家都很熟悉了。文档中明确指出 不要在 [代码]App.onLaunch[代码] 的时候调用 [代码]getCurrentPages[代码],此时 [代码]page[代码] 还没有生成。 那么是不是只要我们不再 App.onLaunch 中使用,getCurrentPages 就会按照我们的预期来工作呢?其实并不尽然。 我在这里为了演示,写了两个页面 First Page 和 Second Page。First Page 可以跳转到 Second Page , Second Page 也可以回退到 First Page. [图片][图片][图片][图片] 并且在两个页面的 onShow 方法中调用了 [代码]getCurrentPages[代码] 方法。 onShow(){ let pages = getCurrentPages(); console.log('------------------- First Page onShow方法中获取栈内页面'); console.log(pages); }, 重新运行小程序,从 First Page 跳转到 Second Page ,观察控制台日志 [图片][图片] 可以看到在 First Page 的 onShow 方法中打印的包含一个对象H的数组, 在 Second Page 的 onShow 方法中打印出了包含两个对象H的数组,H 可以理解为Page实例对象的名称。这和我们预期的是一样的。 接下来我们举个特别点的栗子,怎么特别呢?这回我们的栗子没用糖炒,炒栗子的伙计把“路由”错当成糖了。那我们就来看看当栗子,啊...,不,应该是 [代码]getCurrentPages[代码] 遇到“路由”后会变成什么味儿呢? 首先我们来看看微信小程序API中都有哪些路由 [图片][图片] 没错就是这几个控制小程序页面跳转的全局函数。我们在这里尝试一下最常用的两个 navigateTo 和 navigateBack,其他的同理。 下面我们来看看 First Page 的跳转方法代码: /** * 跳转到 Second Page */ navigateToSecondPage() { wx.navigateTo({ url: '/other/other', }) let pages = getCurrentPages(); console.log('------------------- First Page navigateToSecondPage方法中获取栈内页面'); console.log(pages); }, 我们重新运行一下小程序后点击 First Page 中 跳转到Second Page的按钮,观察一下控制台的日志输出情况 [图片][图片] 哎哎哎......?怎么哪里好像不对呢?炒栗子的伙计说在点击按钮的时候我明明往锅里加了一个 Page 实例啊,为什么我放进去后没有马上看见呢?完了这下炒出来的栗子又变味了。 我们知道 navigateTo 方法肯定是向页面栈里又加入了一个新页面实例的,那么我们去 Second Page 中的 onLoad 方法中看看,有几个 Page 实例: [图片][图片] 哎呦,在Second Page 中明明就是两个,怎么在跳转执行后没有看到呢?伙计想是不是自己太心急了,于是他在加入第二个Page后并没有马上去看而是出去抽了根烟。 /** * 跳转到 Second Page */ navigateToSecondPage() { wx.navigateTo({ url: '/other/other', }) let timerId = setTimeout(function(){ let pages = getCurrentPages(); console.log('------------------- First Page navigateToSecondPage 1S later 方法中获取栈内页面'); console.log(pages); }, 1000); }, 抽完烟后回来发现又正常了 [图片][图片] 点击完跳转按钮1S后,第二个Page实例奇迹般地出现了。伙计有点兴奋啊。于是决定了以后碰到 [代码]getCurrentPages[代码] 和 路由就出去抽根烟,啊...... ,不不不,是等会再看。可是这并不是万全之策。我们都知道 setTimeOut 的运行机制,它是在当前任务栈内的任务执行完毕后再执行 setTimeOut 里任务。这个任务的执行开始时间是当前任务栈内所有的任务执行完成的耗时 + setTimeOut 的延迟时间,也就是说是一个不确定的时间,换个说法就是伙计出去抽根烟回来不一定能看见第二个Page实例。总感觉不是那么安全,那怎么办呢? 我们仔细阅读 navigateTo 的使用文档就会发现,它除了可以指定跳转路径(url)外,还可以添加三个回调方法。 [图片][图片] 那么现在我们根据使用文档来试着修改一下我们查看 page 实例的时间。我们等到跳转成功后就立马去查看。 /** * 跳转到 Second Page */ navigateToSecondPage() { wx.navigateTo({ url: '/other/other', success: function(){ let pages = getCurrentPages(); console.log('------------------- First Page navigateTo Success 方法中获取栈内页面'); console.log(pages); } }) }, 重新运行一下程序: [图片][图片] 好像这个success回调也不是很靠谱,还是得等一小会儿才能看见第二个Page实例。 这是 navigateTo ,下面我们来看看 navigateBack 。 navigateBack(){ let pages = getCurrentPages(); console.log('------------------- Second Page navigateBack 执行之前获取栈内页面'); console.log(pages); if(pages.length > 1){ wx.navigateBack({ delta: 1, success: function(){ console.log('-------------------Second Page navigateBack Success获取栈内页面'); let pages1 = getCurrentPages(); console.log(pages1); } }); } }, 我们清空控制台日志,然后点击 Second Page 的 返回 First Page 按钮 [图片][图片] 我们可以看到 wx.navigateBack 的 success 函数中可以拿到准备的页面堆栈数据。当然你也可以出去抽根烟等会回来再看数据。 所以我们根据上面的Demo可以得出两个小结论: 1 wx.navigateTo 执行之后只能通过延时的方法去获取准确的页面堆栈数据,具体延时多少看你炒栗子的经验喽。 2 wx.navigateBack 执行后可以通过延时和回调方法进行获取页面堆栈数据。 至于其他的路由的情况就不啰嗦那么多了,结论如下: 3 wx.switchTab 执行后可以通过延时和回调方法进行获取页面堆栈数据, 同 wx.navigateBack。 4 wx.reLaunch 执行之后只能通过延时的方法去获取准确的页面堆栈数据, 同 wx.navigateTo。 5 wx.redirectTo 没有办法在执行wx.redirectTo的页面内获取准确的页面堆栈数据。 怎么样,有没有感觉很 因吹丝汀 啊..... 好了,大概就这些了。 重要的事情说三遍: 尽量不要在执行完路由函数后立即调用 getCurrentPages 函数! 尽量不要在执行完路由函数后立即调用 getCurrentPages 函数! 尽量不要在执行完路由函数后立即调用 getCurrentPages 函数! 不然你会吃到怪味的栗子,希望可以帮到大家,如果哪些地方写的不对或不够准确,请大家批评指正。
2019-12-12 - 小程序webview组件,小程序和webview交互,小程序内联h5页面,小程序webview内网页实现微信支付
小程序支持webview以后,我们开发的好多h5页面,就可以直接在小程序里使用了,比如我们开发的微信商城,文章详情页,商品详情页,就可以开发一套,多处使用了。我们今天来讲一讲。在小程序的webview里实现微信支付功能。因为微信不允许在小程序的webview里直接调起微信支付。所以我们这节课就要涉及到小程序和webview的交互了。 老规矩先看效果。 因为这里涉及的东西比较多,录gif太多,没法上传,我就录制了一段视频出来。 https://v.qq.com/x/page/t0913iprnay.html 原理 先说下实现原理吧,实现原理就是我们在webview的h5页面里实现下单功能,然后点击支付按钮,我们点击支付按钮的时候会跳转到小程序页面,把订单号,订单总金额,传递到小程序里,然后小程序里使用订单号和订单金额去调起微信支付,实现付款,付款成功或者失败时都会有回调。我们再把对应的回调传递给webview,刷新webview里的订单和支付状态。 一,定义webview显示h5页面 关于webview的使用,我就不做讲解了,官方文档里写的很清楚,用起来也很简单。https://developers.weixin.qq.com/miniprogram/dev/component/web-view.html [图片] webview很简单,就是用一个webview组件,显示我们的网页。 二,定义h5页面 我这里启动一个本地服务器,用来展示一个简单的h5页面。 [图片] 上图是我在浏览器里显示的效果。 接下来我们在小程序的webview里显示这个页面,也很简单,只需要把我们的src定义为我们的本地网页链接就可以了。 [图片] 这里有一点需要注意 因为我们是本地链接,我们需要到开发者工具里把这一项给勾选。 [图片] 三,来看下h5页面代码 [代码]<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>小程序内嵌webview</title> <style> .btn { font-size: 70px; color: red; } </style> </head> <body> <h1>我是webview里的h5页面</h1> <a id="desc" class="btn" onclick="jumpPay()">点击支付</a> <script type="text/javascript" src="https://res.wx.qq.com/open/js/jweixin-1.3.2.js"></script> <script> console.log(location.href); let payOk = getQueryVariable("payOk"); console.log("payOk", payOk) if(payOk){//支付成功 document.getElementById('desc').innerText="支持成功" document.getElementById('desc').style.color="green" }else{ document.getElementById('desc').innerText="点击支付" } //获取url里携带的参数 function getQueryVariable(variable) { var query = window.location.search.substring(1); var vars = query.split("&"); for (var i = 0; i < vars.length; i++) { var pair = vars[i].split("="); if (pair[0] == variable) { return pair[1]; } } return (false); } function jumpPay() { let orderId = Date.now();//这里用当前时间戳做订单号(后面使用你自己真实的订单号) let money = 1;//订单总金额(单位分) let payData = {orderId: orderId, money: money}; let payDataStr = JSON.stringify(payData);//因为要吧参数传递给小程序,所以这里需要转为字符串 const url = `../wePay/wePay?payDataStr=${payDataStr}`; wx.miniProgram.navigateTo({ url: url }); // console.log("点击了去支付", url) console.log("点击了去支付") } </script> </body> </html> [代码] h5代码这里不做具体讲解,只简单说下。我们就是在点击支付按钮时,用当前时间戳做为订单号(因为订单号要保证唯一),然后传一个订单金额(单位分),这里节约起见,就传1分钱吧,花的是自己的钱,心疼。。。。 关键点说一下 1, 必须引入jweixin,才可以实现h5跳转小程序。 <script type=“text/javascript” src=“https://res.wx.qq.com/open/js/jweixin-1.3.2.js”></script> 2,跳转到小程序页面的方法 [代码]const url = `../wePay/wePay?payDataStr=${payDataStr}`; wx.miniProgram.navigateTo({ url: url }); [代码] 这里要和你小程序的页面保持一致。payDataStr是我们携带的参数 [图片] 四,小程序支付页 来看下我们的小程序支付页 [图片] 小程序支付页功能很简单,就是来接收我们h5传过订单号和订单金额。然后去调起微信支付,实现支付。支付成功和支付失败都有对应的回调。 [图片] 支付我们这里实用的小程序云开发来实现的支付,核心代码只有10行。由于支付不是本节的重点,所以这里不做具体讲解。感兴趣的同学可以去看我写的文章和我录的视频 小程序支付文章:https://www.jianshu.com/p/2b391df055a9 小程序支付视频:https://edu.csdn.net/course/play/25701/310742 下面把小程序接收参数和支付的完整代码贴出来给大家 [代码]Page({ //h5传过来的参数 onLoad: function(options) { console.log("webview传过来的参数", options) //字符串转对象 let payData = JSON.parse(options.payDataStr) console.log("orderId", payData.orderId) let that = this; wx.cloud.callFunction({ name: "pay", data: { orderId: payData.orderId, money: payData.money }, success(res) { console.log("获取成功", res) that.goPay(res.result); }, fail(err) { console.log("获取失败", err) } }) }, //微信支付 goPay(payData) { wx.requestPayment({ timeStamp: payData.timeStamp, nonceStr: payData.nonceStr, package: payData.package, signType: 'MD5', paySign: payData.paySign, success(res) { console.log("支付成功", res) //你可以在这里支付成功以后,再跳会webview页,并把支付成功状态传回去 wx.navigateTo({ url: '../webview/webview?payOk=true', }) }, fail(res) { console.log("支付失败", res) } }) } }) [代码] 代码里注释很清楚,这里有一点,就是我们支付成功后,需要告诉h5我们支付成功了,通知h5去刷新订单或者支付状态。 到这里我们就完整的实现了小程序webview展示h5页面,并且做到了h5和小程序的交互,实现了小程序webview的支付功能。 是不是很简单呢。 源码地址 1,关注“编程小石头”公号,回复“webview”即可获取源码 2,也可以到我github下载源码 https://github.com/qiushi123/xiaochengxu_demos [图片]
2019-08-15 - 计算器 源码
一、界面 1、主要界面:基本计算 [图片] 2、扩展界面:单位计算 [图片] 3、其它界面 房贷计算、正则表达式等,具体可扫码参考 [图片] 二、代码 1、index.js //加载第三方组件,处理四则运算,代替禁用的eval函数 var rpn = require("…/utils/rpn.js"); Page ( { data: { idb: “back”, idc: “clear”, idt: “toggle”, id9: “9”, id8: “8”, id7: “7”, id6: “6”, id5: “5”, id4: “4”, id3: “3”, id2: “2”, id1: “1”, id0: “0”, idadd: “+”, idj: “-”, idx: “×”, iddiv: “÷”, idd: “.”, ide: “=”, screenData: “0”, operaSymbo: { “+”: “+”, “-”: “-”, “×”: “*”, “÷”: “/”}, lastIsOperaSymbo: false, lastIsPoint: false, iconType: ‘waiting_circle’, iconColor: ‘white’, arr: [], logs: [] }, [代码]onLoad: function (options) { // 页面初始化 options为页面跳转所带来的参数 }, onReady: function () { // 页面渲染完成 }, onShow: function () { // 页面显示 }, onHide: function () { // 页面隐藏 }, onUnload: function () { // 页面关闭 }, /** * 用户点击右上角分享 */ onShareAppMessage: function () { }, //点击函数 clickBtn: function (event) { var id = event.target.id; if (id == this.data.idb) { //退格← var data = this.data.screenData.toString(); if (data == "0") { return; } data = data.substring(0, data.length - 1); if (data == "" || data == "-") { data = 0; } this.setData({ "screenData": data }); this.data.arr.pop(); } else if (id == this.data.idc) { //清屏C this.setData({ "screenData": "0" }); this.data.arr.length = 0; this.data.lastIsPoint = false; } else if (id == this.data.idt) { //正负号+/- var data = this.data.screenData; if (data == "0") { return; } var firstWord = data.charAt(0); if (firstWord == "-") { data = data.substr(1); this.data.arr.shift(); } else { data = "-" + data; this.data.arr.unshift("-"); } this.setData({ "screenData": data }); } else if (id == this.data.ide) { //等于= var data = this.data.screenData; if (data == "0") { return; } var lastWord = data.charAt(data.length); if (isNaN(lastWord)) { return; } console.log("parseFloat(data)" + parseFloat(data)); console.log("data" + data) if (parseFloat(data) == data) { return; } var log = data; var data = rpn.calCommonExp(data); // var num = ""; // var lastOperator = ""; // var arr = this.data.arr; // var optarr = []; // //遍历屏幕数组,获取操作数和操作符 // for (var i in arr) // { // if (isNaN(arr[i]) == false || arr[i] == this.data.idd || arr[i] == this.data.idt) // { // num += arr[i]; // } // else // { // lastOperator = arr[i]; // optarr.push(num); // optarr.push(arr[i]); // num = ""; // } // } // optarr.push(Number(num));//最后一个操作数 // var result = Number(optarr[0]) * 1.0; // console.log(result); // //循环操作数据,遇到符号就操作符号和下一个数 // for (var i = 1; i < optarr.length; i++) // { // if (isNaN(optarr[i])) // { // if (optarr[i] == this.data.idadd) // { // result += Number(optarr[i + 1]); // } // else if (optarr[i] == this.data.idj) // { // result -= Number(optarr[i + 1]); // } // else if (optarr[i] == this.data.idx) // { // result *= Number(optarr[i + 1]); // } // else if (optarr[i] == this.data.iddiv) // { // result /= Number(optarr[i + 1]); // } // } // } //存储历史记录 // this.data.logs.push(data + '=' + result); wx.setStorageSync("calclogs", this.data.logs); this.data.arr.length = 0; //this.data.arr.push(result); //this.setData({ "screenData": result + "" }); this.setData({ "screenData": data}); } else { if (this.data.operaSymbo[id]) { //如果是符号+-*/ this.data.lastIsPoint = false; //如果上一个输入字符是符号,或者屏幕是0,则再输入符号时不处理。 if (this.data.lastIsOperaSymbo || this.data.screenData == "0") { return; } } //对于不是符号的其它字符 var sd = this.data.screenData; var data; if(id == this.data.idd) { if (this.data.lastIsPoint) return; else { data = sd + id; this.data.lastIsPoint = true; } } else { if (sd === '0') { data = id; } else { data = sd + id; } } this.setData({ "screenData": data });//更新屏幕数据 this.data.arr.push(id); //更新本次输入的字符是否是运算符 if (this.data.operaSymbo[id]) { this.setData({ "lastIsOperaSymbo": true }); } else { this.setData({ "lastIsOperaSymbo": false }); } } }, [代码] } ) 2、index.wxml <view class=“content”> <view class=“layout-top”> <view class=“screen”> {{screenData}} </view> </view> <view class=“layout-bottom”> <view class=“btnGroup”> <view class=“item orange” bindtap=“clickBtn” id="{{idc}}">С</view> <view class=“item orange” bindtap=“clickBtn” id="{{idb}}">←</view> <view class=“item orange” bindtap=“clickBtn” id="{{idt}}">+/-</view> <view class=“item orange” bindtap=“clickBtn” id="{{idadd}}">+</view> </view> <view class=“btnGroup”> <view class=“item blue” bindtap=“clickBtn” id="{{id9}}">9</view> <view class=“item blue” bindtap=“clickBtn” id="{{id8}}">8</view> <view class=“item blue” bindtap=“clickBtn” id="{{id7}}">7</view> <view class=“item orange” bindtap=“clickBtn” id="{{idj}}">-</view> </view> <view class=“btnGroup”> <view class=“item blue” bindtap=“clickBtn” id="{{id6}}">6</view> <view class=“item blue” bindtap=“clickBtn” id="{{id5}}">5</view> <view class=“item blue” bindtap=“clickBtn” id="{{id4}}">4</view> <view class=“item orange” bindtap=“clickBtn” id="{{idx}}">×</view> </view> <view class=“btnGroup”> <view class=“item blue” bindtap=“clickBtn” id="{{id3}}">3</view> <view class=“item blue” bindtap=“clickBtn” id="{{id2}}">2</view> <view class=“item blue” bindtap=“clickBtn” id="{{id1}}">1</view> <view class=“item orange” bindtap=“clickBtn” id="{{iddiv}}">÷</view> </view> <view class=“btnGroup”> <view class=“item blue zero” bindtap=“clickBtn” id="{{id0}}">0</view> <view class=“item blue” bindtap=“clickBtn” id="{{idd}}">.</view> <view class=“item orange” bindtap=“clickBtn” id="{{ide}}">=</view> </view> </view> </view> 3、index.wcss /* //height: 90%;*/ .content { display: flex; flex-direction: column; align-items: center; background-color: #ccc; font-family: “Microsoft YaHei”; overflow-x: hidden; } .layout-top{ width: 100%; margin-bottom: 10rpx; } .layout-bottom{ width: 100%; } .screen { text-align: right; width: 100%; line-height: 200rpx; padding: 0 10rpx; font-weight: bold; font-size: 60px; box-sizing: border-box; border-top: 1px solid #fff; } .btnGroup { display: flex; flex-direction: row; flex: 1; width: 100%; height: 5rem; background-color: #fff; } .item { width:25%; display: flex; align-items: center; flex-direction: column; justify-content: center; margin-top: 1px; margin-right: 1px; } .item:active { background-color: #ff0000; } .zero{ width: 50%; } .orange { color: #fef4e9; background: #f78d1d; font-weight: bold; } .blue { color:#d9eef7; background-color: #0095cd; } .iconBtn{ display: flex; } .icon{ display: flex; align-items: center; width:100%; justify-content: center; } 三、完整工程下载链接 [](https://download.csdn.net/download/kevin_lp/11931364
2019-10-28 - 自定义可配置format的日期时间选择组件DateTimePicker
背景 由于在项目中遇到需要同时选择日期和时间的需求,所以自己写了一个可以动态配置format格式的时间选择器 效果图 [图片] wxml [代码]<view class="view-body"> <text class='item-key'>{{title}}<text style="color:red" wx:if="{{isRequired}}">*</text></text> <view class="item-value"> <picker mode="multiSelector" bindchange="bindPickerChange" bindcolumnchange="bindPickerColumnChange" value="{{multiIndex}}" range="{{multiArray}}"> <input disabled="{{true}}" value='{{value}}' name='{{name}}' /> <image class="img-arrow" src='/images/date_icon.png' /> </picker> </view> </view> [代码] js [代码]Component({ behaviors: ['wx://form-field'], properties: { title: { type: String }, name: { type: String }, isRequired: { type: Boolean }, format: { type: String } }, data: {}, lifetimes: { attached: function() { //当前时间 年月日 时分秒 const date = new Date() const curYear = date.getFullYear() const curMonth = date.getMonth() + 1 const curDay = date.getDate() const curHour = date.getHours() const curMinute = date.getMinutes() const curSecond = date.getSeconds() //记录默认年份 后面加载二月份天数 this.setData({ chooseYear: curYear }) //初始化时间选择轴 this.initColumn(curMonth) //不足两位的前面好补0 因为后面要获取在时间轴上的索引 时间轴初始化的时候都是两位 let showMonth = curMonth < 10 ? ('0' + curMonth) : curMonth let showDay = curDay < 10 ? ('0' + curDay) : curDay let showHour = curHour < 10 ? ('0' + curHour) : curHour let showMinute = curMinute < 10 ? ('0' + curMinute) : curMinute let showSecond = curSecond < 10 ? ('0' + curSecond) : curSecond //当前时间在picker列上面的索引 为了当打开时间选择轴时选中当前的时间 let indexYear = this.data.years.indexOf(curYear + '') let indexMonth = this.data.months.indexOf(showMonth + '') let indexDay = this.data.days.indexOf(showDay + '') let indexHour = this.data.hours.indexOf(showHour + '') let indexMinute = this.data.minutes.indexOf(showMinute + '') let indexSecond = this.data.seconds.indexOf(showSecond + '') let multiIndex = [] let multiArray = [] let value = '' let format = this.properties.format; if (format == 'yyyy-MM-dd') { multiIndex = [indexYear, indexMonth, indexDay] value = `${curYear}-${showMonth}-${showDay}` multiArray = [this.data.years, this.data.months, this.data.days] } if (format == 'HH:mm:ss') { multiIndex = [indexHour, indexMinute, indexSecond] value = `${showHour}:${showMinute}:${showSecond}` multiArray = [this.data.hours, this.data.minutes, this.data.seconds] } if (format == 'yyyy-MM-dd HH:mm') { multiIndex = [indexYear, indexMonth, indexDay, indexHour, indexMinute] value = `${curYear}-${showMonth}-${showDay} ${showHour}:${showMinute}` multiArray = [this.data.years, this.data.months, this.data.days, this.data.hours, this.data.minutes] } if (format == 'yyyy-MM-dd HH:mm:ss') { multiIndex = [indexYear, indexMonth, indexDay, indexHour, indexMinute, indexSecond] value = `${curYear}-${showMonth}-${showDay} ${showHour}:${showMinute}:${showSecond}` multiArray = [this.data.years, this.data.months, this.data.days, this.data.hours, this.data.minutes, this.data.seconds] } this.setData({ value, multiIndex, multiArray, curMonth, chooseYear: curYear, }) } }, /** * 组件的方法列表 */ methods: { //获取时间日期 bindPickerChange: function(e) { this.setData({ multiIndex: e.detail.value }) const index = this.data.multiIndex let format = this.properties.format var showTime = '' if (format == 'yyyy-MM-dd') { const year = this.data.multiArray[0][index[0]] const month = this.data.multiArray[1][index[1]] const day = this.data.multiArray[2][index[2]] showTime = `${year}-${month}-${day}` } if (format == 'HH:mm:ss') { const hour = this.data.multiArray[0][index[0]] const minute = this.data.multiArray[1][index[1]] const second = this.data.multiArray[2][index[2]] showTime = `${hour}:${minute}:${second}` } if (format == 'yyyy-MM-dd HH:mm') { const year = this.data.multiArray[0][index[0]] const month = this.data.multiArray[1][index[1]] const day = this.data.multiArray[2][index[2]] const hour = this.data.multiArray[3][index[3]] const minute = this.data.multiArray[4][index[4]] showTime = `${year}-${month}-${day} ${hour}:${minute}` } if (format == 'yyyy-MM-dd HH:mm:ss') { const year = this.data.multiArray[0][index[0]] const month = this.data.multiArray[1][index[1]] const day = this.data.multiArray[2][index[2]] const hour = this.data.multiArray[3][index[3]] const minute = this.data.multiArray[4][index[4]] const second = this.data.multiArray[5][index[5]] showTime = `${year}-${month}-${day} ${hour}:${minute}:${second}` } this.setData({ value: showTime }) this.triggerEvent('dateTimePicker', showTime) }, //初始化时间选择轴 initColumn(curMonth) { let years = [] let months = [] let days = [] let hours = [] let minutes = [] let seconds = [] for (let i = 1990; i <= 2099; i++) { years.push(i + '') } for (let i = 1; i <= 12; i++) { if (i < 10) { i = "0" + i; } months.push(i + '') } if (curMonth == 1 || curMonth == 3 || curMonth == 5 || curMonth == 7 || curMonth == 8 || curMonth == 10 || curMonth == 12) { for (let i = 1; i <= 31; i++) { if (i < 10) { i = "0" + i; } days.push(i + '') } } if (curMonth == 4 || curMonth == 6 || curMonth == 9 || curMonth == 11) { for (let i = 1; i <= 30; i++) { if (i < 10) { i = "0" + i; } days.push(i + '') } } if (curMonth == 2) { days=this.setFebDays() } for (let i = 0; i <= 23; i++) { if (i < 10) { i = "0" + i; } hours.push(i + '') } for (let i = 0; i <= 59; i++) { if (i < 10) { i = "0" + i; } minutes.push(i + '') } for (let i = 0; i <= 59; i++) { if (i < 10) { i = "0" + i; } seconds.push(i + '') } this.setData({ years, months, days, hours, minutes, seconds }) }, /** * 列改变时触发 */ bindPickerColumnChange: function(e) { //获取年份 用于计算改年的2月份为平年还是闰年 if (e.detail.column == 0 && this.properties.format != 'HH:mm:ss') { let chooseYear = this.data.multiArray[e.detail.column][e.detail.value]; this.setData({ chooseYear }) if (this.data.curMonth == '02' || this.data.chooseMonth == '02') { this.setFebDays() } } //当前第二为月份时需要初始化当月的天数 if (e.detail.column == 1 && this.properties.format != 'HH:mm:ss') { let num = parseInt(this.data.multiArray[e.detail.column][e.detail.value]); let temp = []; if (num == 1 || num == 3 || num == 5 || num == 7 || num == 8 || num == 10 || num == 12) { //31天的月份 for (let i = 1; i <= 31; i++) { if (i < 10) { i = "0" + i; } temp.push("" + i); } this.setData({ ['multiArray[2]']: temp }); } else if (num == 4 || num == 6 || num == 9 || num == 11) { //30天的月份 for (let i = 1; i <= 30; i++) { if (i < 10) { i = "0" + i; } temp.push("" + i); } this.setData({ ['multiArray[2]']: temp }); } else if (num == 2) { //2月份天数 this.setFebDays() } } let data = { multiArray: this.data.multiArray, multiIndex: this.data.multiIndex }; data.multiIndex[e.detail.column] = e.detail.value; this.setData(data); }, //计算二月份天数 setFebDays() { let year = parseInt(this.data.chooseYear); let temp = []; if (year % (year % 100 ? 4 : 400) ? false : true) { for (let i = 1; i <= 29; i++) { if (i < 10) { i = "0" + i; } temp.push("" + i); } this.setData({ ['multiArray[2]']: temp, chooseMonth: '02' }); } else { for (let i = 1; i <= 28; i++) { if (i < 10) { i = "0" + i; } temp.push("" + i); } this.setData({ ['multiArray[2]']: temp, chooseMonth: '02' }); } return temp; } }, }) [代码] wxss [代码].view-body { display: flex; align-items: center; padding: 4% 0%; border-bottom: 1px solid #eee; } .item-key { font-size: 30rpx; color: #666; width: 25%; } .item-value { flex-grow: 1; font-size: 31rpx; position: relative; margin-left: 3%; } .img-arrow { width: 45rpx; height: 45rpx; padding-right: 10rpx; position: absolute; top: 50%; transform: translateY(-50%); right: 5rpx; } [代码] 使用 在你页面中的json中添加引用,路径根据你的实际工程目录来写。 [代码]{ "usingComponents": { "DateTimePicker": "/components/datetimepicker/datetimepicker" } } [代码] wxml中添加 [代码]<DateTimePicker title='选择时间' isRequired='true' bind:dateTimePicker='onDateTimePicker' name='time' format='yyyy-MM-dd HH:mm:ss'/> [代码] 说明 title:表单组件的名称 isRequired:是否必填项 dateTimePicker:为选中确认回调 name:为表单点击 form 表单中 form-type 为 submit 的 button 组件时,会将表单组件中的 value 值进行提交,需要在表单组件中加上 name 来作为 key format:时间格式化参数 支持:‘yyyy-MM-dd HH:mm:ss’,‘HH:mm:ss’,‘yyyy-MM-dd HH:mm’,‘yyyy-MM-dd’ 四种 开发者可以根据自己的需求调整组件样式 最后代码片段如下: https://developers.weixin.qq.com/s/PzmDvcmi79fI
2020-02-17 - 小程序跳转页面加载优化
适应场景: 小程序页面跳转redirect/navigate/其它方式 分析: 从用户触发跳转行为到下一个页面onload生命周期函数内时间差会有500ms左右,如果在页面跳转之后进行onload函数内才开始去加载页面数据,那么这500ms左右的时间就浪费了。 改进: 在页面触发跳转行为的处理函数里结合promise预先加载下个页面的数据,并将promise对象缓存,此时页面跳转和加载数据同时进行,到了目标页面再取出缓存的promise对象进行判断和取数据操作。 效果: 跳转页面加载速度提高了600ms。 示例: 代码结构 [图片] pageManager.js [代码]// 写在utils里的公用方法 const pageList = {}; module.exports = { putData:function(pageName, data){ pageList[pageName] = data; }, getData:function(pageName){ return pageList[pageName]; } } [代码] util.js [代码]const myPromise = fn => obj => { return new Promise((resolve, reject) => { obj.complete = obj.success = (res) => { resolve(res); } obj.fail = (err) => { reject(err); } fn(obj); }) } module.exports = { myPromise : myPromise } [代码] index.js [代码]// 跳转页面 const {myPromise} = require('../../utils/util'); const pageManager = require('../../utils/pageManager'); page({ data: { }, onLoad:function(){ }, gotoPageA:function(){ const PromisePageA = myPromise(wx.request)({ url : '' }).then((res)=>{ return res.data; }) pageManager.putData('pageA',promisePageA); wx.navigateTo({ url: 'pages/pageA/pageA' }) } }) [代码] pageA.js [代码]// 被跳转页面 const util = require('../../utils/util.js'); const pageManager = require('../../utils/pageManager'); const {myPromise} = require('../../utils/util'); Page({ data:{ logs:[] }, onLoad: function(){ const promisePageA = pageManager.getData('pageA'); if(promisePageA){ const resData = promisePageA.then( function(data){ }, function(){ console.log("err"); } ) } } }) [代码]
2019-10-31 - 自定义下拉选择组件Select
背景 项目需求要实现类似html标签 <Select/>效果,所以写了一个类似效果的自定义组件。 效果图 [图片] wxml [代码]<view class="view-item"> <text class='item-key'>{{title}}<text style="color:red" wx:if="{{isRequired}}">*</text></text> <view class="view-select-container"> <view class='select-value' bindtap="selectToggle"> <input value="{{value}}" name='{{name}}' disabled="{{true}}" /> <image class='img-arrow' style="width:40rpx;height:40rpx" src='/images/drop_down.png' /> </view> <view class="view-options" wx:if="{{showOptions}}"> <cover-view class='option-item' wx:for="{{options}}" data-index="{{index}}" bindtap="selectItem">{{item[showkey]}}</cover-view> </view> <view class="view-out" wx:if="{{showOptions}}" bindtap="hideSelect"></view> </view> </view> [代码] js [代码]Component({ behaviors: ['wx://form-field'], //支持表单获取组件值 properties: { //组件的名称 title: { type: String }, //通过form获取组件的值 name: { type: String }, //下拉显示的数据集合 options: { type: Array }, //表单组件是否必填 isRequired: { type: Boolean }, //外部传递的动态变量 showkey: { type: String } }, data: { showOptions: false //组件默认的展开状态 }, /** * 组件的方法列表 */ lifetimes: { attached: function() { let key = this.properties.showkey this.setData({ value: this.properties.options[0][key] //默认选中第一个 }) }, }, methods: { selectToggle: function(e) { this.setData({ showOptions: !this.data.showOptions }) }, hideSelect: function(e) { this.setData({ showOptions: false }) }, selectItem: function(e) { let optionList = this.properties.options //外部传进来的数组对象 let nowIdx = e.currentTarget.dataset.index //当前点击的索引 let selectItem = optionList[nowIdx] //当前点击的内容 this.setData({ showOptions: false, value: selectItem[this.properties.showkey] }); let eventOption = {} // 触发事件的选项 this.triggerEvent("mySelectItem", selectItem) //组件选中回调 } } }) [代码] wxss [代码].view-item { display: flex; align-items: center; padding: 2% 0%; /* border-bottom: 1px solid #eee; */ } .item-key { font-size: 31rpx; color: #666; width: 25%; } .view-select-container { width: 75%; height: 80rpx; position: relative; } .select-value { width: 100%; height: 100%; display: flex; justify-content: space-between; align-items: center; box-sizing: border-box; border: 1px solid #eee; font-size: 31rpx; position: relative; } .img-arrow { width: 28rpx; height: 26rpx; padding-right: 10rpx; } .view-options { background-color: #fff; display: flex; width: 100%; z-index: 3; position: absolute; flex-direction: column; justify-content: center; align-items: center; } .view-out { position: fixed; z-index: 2; width: 100%; left: 0; top: 0; height: 100%; } .option-item { font-size: 28rpx; width: 100%; display: flex; justify-content: center; align-items: center; margin-top: -1px; text-align: center; box-sizing: border-box; border: 1px solid #eee; line-height: 80rpx; height: 80rpx; } input { padding-left: 3%; font-size: 30rpx; } [代码] 使用 json中引入自定义组件 [代码]{ "usingComponents": { "Select": "/components/select/select" } } [代码] js [代码]Page({ data: { optionArry: [{ "name": "香蕉", "id": "1" }, { "name": "苹果", "id": "2" }, { "name": "橘子", "id": "3" }, { "name": "雪梨", "id": "4" }], }, onLoad: function() {}, }) [代码] wxml中使用 [代码]<Select title="类别" options="{{optionArry}}" isRequired="true" bind:mySelectItem='onSelectItem' name='formkey' showkey='name' /> [代码] 总结:可以动态传递对象数组在组件中显示的属性名,类似picker的range-key;使用cover-view解决当组件展开时遮住原生组件时的点击击穿问题。 最后代码片段以供学习和参考: https://developers.weixin.qq.com/s/RwZLwImG7kcE
2019-11-08 - 关于小程序获取手机号解密失败的踩坑历程
小程序中获取微信手机号的前提条件这里贴一个链接,各位同学自己看 https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/getPhoneNumber.html 主要说说为什么会解密失败,主要有以下两点原因 微信服务器报错 你解密的姿势错了 对于第一点,比较少见,如果出现了,嗯 ,坐着等死就行了 解密失败一般是由于你的 sessionKey 失效导致的,而sessionKey 由 wx.login 这个api先获取code, 再由我们的后台拿这个code向微信服务器请求获得。code有效期五分钟。 上面的链接中有这样一句话 注意 在回调中调用 [代码]wx.login[代码] 登录,可能会刷新登录态。此时服务器使用 code 换取的 sessionKey 不是加密时使用的 sessionKey,导致解密失败。建议开发者提前进行 [代码]login[代码];或者在回调中先使用 [代码]checkSession[代码] 进行登录态检查,避免 [代码]login[代码] 刷新登录态。 这句话这么理解呢,打个比方,假设你第一次调用wx.login获取的为 sessionKey_1,在它的有效期内,你再去获取一个加密数据 encryptedData_1,这时候你用sessionKey_1去解密encryptedData_1就可以正常解密。 但是在获取encryptedData_1的时候会有个回调函数,如果你在这个回调函数里又调了一次wx.login,获取了 sessionKey_2,这时候登录态可能会被刷新,加密encryptedData_1时使用的sessionKey_1就会失效,再去解密encryptedData_1就会有很高的几率解密失败。至于为什么不会百分百刷新登录态,鬼知道这逻辑是怎么写的。 贴上我的部分代码(只贴获取手机号的部分,其他逻辑请根据自己产品业务酌情参考) [代码][代码] const app = getApp(); [代码][代码] [代码]Page({[代码] [代码] [代码][代码]data: {[代码][代码] [代码][代码]code: [代码][代码]""[代码][代码],[代码][代码] [代码][代码]},[代码] [代码] [代码][代码]// 获取手机号[代码][代码] [代码][代码]getPhoneAndLogin: [代码][代码]function[代码][代码](e){[代码][代码] [代码][代码]let that = [代码][代码]this[代码][代码];[代码][代码] [代码][代码]// console.log(e);[代码] [代码] [代码][代码]if[代码] [代码](e.detail.errMsg !== [代码][代码]"getPhoneNumber:ok"[代码][代码]){[代码][代码] [代码][代码]return[代码][代码];[代码][代码] [代码][代码]}[代码] [代码] [代码][代码]wx.showLoading({[代码][代码] [代码][代码]mask: [代码][代码]true[代码][代码],[代码][代码] [代码][代码]})[代码] [代码] [代码][代码]// 检查登录态是否过期[代码][代码] [代码][代码]wx.checkSession({[代码][代码] [代码][代码]success(res) {[代码][代码] [代码][代码]// session_key 未过期,并且在本生命周期一直有效[代码][代码] [代码][代码]// console.log(res);[代码] [代码] [代码][代码]wx.request({[代码][代码] [代码][代码]url: app.globalData.apiUrl + [代码][代码]'**********'[代码][代码],[代码][代码] [代码][代码]data: {[代码][代码] [代码][代码]code: that.data.code,[代码][代码] [代码][代码]encryptedData: e.detail.encryptedData,[代码][代码] [代码][代码]iv: e.detail.iv,[代码][代码] [代码][代码]},[代码][代码] [代码][代码]method: [代码][代码]"POST"[代码][代码],[代码][代码] [代码][代码]success: res => {[代码][代码] [代码][代码]// console.log(res);[代码] [代码] [代码][代码]},[代码][代码] [代码][代码]fail: res => {[代码][代码] [代码][代码] [代码][代码]wx.showToast({[代码][代码] [代码][代码]icon: [代码][代码]"none"[代码][代码],[代码][代码] [代码][代码]title: [代码][代码]'服务器繁忙'[代码][代码],[代码][代码] [代码][代码]})[代码][代码] [代码][代码]},[代码][代码] [代码][代码]complete: res => {[代码][代码] [代码][代码]wx.hideLoading();[代码][代码] [代码][代码]}[代码][代码] [代码][代码]})[代码][代码] [代码][代码]},[代码][代码] [代码][代码]fail(err) {[代码][代码] [代码][代码]// session_key 已经失效,需要重新执行登录流程[代码] [代码] [代码][代码]wx.login({[代码][代码] [代码][代码]success: res => {[代码][代码] [代码][代码]that.data.code = res.code[代码][代码] [代码][代码]}[代码][代码] [代码][代码]})[代码][代码] [代码][代码]}[代码][代码] [代码][代码]})[代码][代码] [代码][代码]},[代码][代码] [代码][代码] [代码][代码]/**[代码][代码] [代码][代码]* 生命周期函数--监听页面加载[代码][代码] [代码][代码]*/[代码][代码] [代码][代码]onLoad: [代码][代码]function[代码] [代码](options) {[代码][代码] [代码][代码]let that = [代码][代码]this[代码][代码];[代码][代码] [代码][代码]// console.log(options);[代码] [代码] [代码][代码]wx.login({[代码][代码] [代码][代码]success: res =>{[代码][代码] [代码][代码]that.data.code = res.code[代码][代码] [代码][代码]}[代码][代码] [代码][代码]})[代码][代码] [代码][代码]},[代码] [代码]})[代码] 以上,如果有问题欢迎各位大佬指正
2018-11-28 - 已解决。小程序获取手机号时,checkSession通过但是获取手机号解密失败
一开始我的处理方式是在页面直接用checkSession,我的session_key是在index.js登录的时候保存到storage,这里check回调的是“success”。 但是把此时storage里面的session_key结合授权按钮的参数去进行解密是失败的,需要在当前的Page再登陆一次才能成功。 不推荐把session_key存放在缓存。所以以上做法直接跳过。 最后参考了一个朋友的做法,在Page onLoad的时候执行一次wx.login(),然后拿到新的session_key,再用此时的新key去解密就通了。或者改为请求解密之前执行一次登录,据说出问题的概率还是很大 结尾补充:最后一种方法还有个问题要考虑,就是最好执行获取手机号之前再checkSession一下(尽管没啥用)。 问题源头,由于这个函数在校验session_key的时候,无论是过期的key还是新的key都是success,所以有了之后一些列的问题,session_key的状态没法把控 [代码]Page({ data: { currentSessionKey: null }, onLoad: function(options) { /* do something*/ const here = this; // 执行登录确保session_key在线 wx.login({ success(res) { if (res.code) { // call()是我自己基于wx.request封装的一个请求函数工具,这里通过后端发送登录请求获得openid const data = call(userLogin, { code: res.code }); data.then(obj => { if (!obj.error) { here.setData({ currentSessionKey: obj.result.session_key }) } }); } }, fail(error) { throw error; } }); }, // 点击按钮获取手机号权限并解析<button open-type="getPhoneNumber" bindgetphonenumber="getPhoneNumber" bindtap='doMyAction'>获取手机号</button> getPhoneNumber: function (e) { const { encryptedData, iv } = e.detail; const options = { encryptedData: encryptedData, iv: iv, sessionKey: this.data.currentSessionKey }; here.doGetPhone(options); }, doMyAction: function() { // 还可以做一些事情 }, doGetPhone: function (options) { const { sessionKey, encryptedData, iv } = options; const here = this; // 向服务器请求解密 wx.request({ // 这里是解密用的接口 url: 'https://xxx.com/python/decrypt', method: 'POST', data: { sessionKey: sessionKey, encryptedData: encryptedData, iv: iv }, success(res) { // 最终获取到用户数据,国家代号前缀、不带前缀的手机号。默认是不带前缀 const { countryCode, purePhoneNumber } = res.data; here.pageForward(countryCode, purePhoneNumber); }, fail(error) { console.log(error); here.pageForward(); } }) }, pageForward: function(countryCode, purePhoneNumber) { // 获取成功后我是跳转到另一个页面 wx.navigateTo({ url: `/pages/person/index?phone=${purePhoneNumber}` }) } }) [代码]
2020-09-15 - 微信小程序开发中如何把view上的内容绘制在画布上
微信小程序开发中如何把view上的内容绘制在画布上
2019-07-31 - web-view在Android机型上高度不正确的问题
机型:小米6、一加3t、华为meta9、小米mix2等Android机型; 微信版本:6.7.3; 触发条件:导航栏样式设置为自定义时(app.json中配置为"navigationStyle": "custom"); bug现象:Android机型中web-view整个页面的高度会被加上小程序的导航栏的高度,导致内嵌页面中底部一部分元素无法显示(页面中若有固定置底的元素会非常明显) 以下截图分别是开发者工具中的调试效果和真机中的实际效果(开发者工具中导航栏与内嵌页面是顶部是重合的,底部正常显示,但是到了真机中,页面的顶部会显示在导航栏下方,页面高度在iOS机型上是正常的,可以看到底部元素能正常显示,而Android机型上就不对了,根本看不到position:fixed的元素): [图片] ↑ 开发者工具中,iPhone6的效果 [图片] ↑ 开发者工具中nexus5的效果 [图片] ↑ 真机 Android机型中的效果 [图片] ↑ 真机iPhone7P中的效果 另外,看到有同学很早之前就提了这个bug了,不知道为啥一直没有被重视: 小程序全屏模式下webview高度被截断
2018-10-23 - 微信小程序swiper高度动态适配
ps:没有在[代码]swiper[代码]中添加[代码]scroll-view[代码]是为了可以使用页面的下拉刷新,最终方法直接跳到方案四。(含代码片段) 初始方案 [代码]swiper[代码]高度固定,[代码]swiper-item[代码]默认绝对定位且宽高100%,每个[代码]swiper-item[代码]中内容由固定高度的child组成,然后根据child数量动态计算[代码]swiper[代码]高度,初始方案(由于rpx针对屏幕宽度进行自适应,[代码]child_height[代码]使用[代码]rpx[代码]方便child正方形情况下自适应): [代码]swiper_height = child_height * child_num[代码]屏幕效果仅在宽度375的设备(ip6、ipⅩ)完美契合,其他设备都底部会出现多余空隙,并且在上拉加载过程中,随着内容增加,底部空隙也逐渐变大。 [图片] 方案二 开始以为是[代码]rpx[代码]适配显示问题,后通过文档中描述的WXSS 尺寸单位转化rpx为px([代码]child_height[代码]使用[代码]rpx[代码]): [代码]swiper_height = child_height * child_num * ( window_width / 750 )[代码]然后并无变化,我们可以看到[代码]child_height[代码]在不同宽度屏幕下,显示的宽高尺寸是不一样的([代码]px[代码]单位),那就尝试使用box在各个屏幕的实际高度进行计算[代码]swiper[代码]高度,box的高度可以单独在页面中增加一个固定标签,该标签样式和box宽高保持一致并且隐藏起来,然后在[代码]page[代码]的[代码]onload[代码]中通过wx.createSelectorQuery()获取标签实际高度[代码]baseItemHeight[代码]([代码]px[代码]单位): [代码]swiper_height = baseItemHeight * child_num[代码]结果显示原本的ip6、ipⅩ没有问题,另外宽带小于375的ip5上也ok,但是在大于375的设备上还是出现空隙,比如ip的plus系列。 方案三 之前的方案都无法计算出合适的[代码]swiper[代码]高度,那就换个思路,比如去计算空隙的高度。 [代码]swiper[代码]底部有一个load标签显示“加载更多”,该标签紧贴box其后,通过[代码]wx.createSelectorQuery()[代码]来获取[代码]bottom[代码],然而你会发现[代码]bottom[代码]是标签的[代码]height[代码]加[代码]top[代码]的和。计算底部空隙(暂时忽略“加载更多”标签高度): [代码]space_height = swiper_height - load_top[代码]刚计算完可以看到在静止状态下,计算出[代码]space_height[代码]拿去修改[代码]swiper_height[代码]显示空隙刚好被清掉了,但是接着就发现在动过程中获取到的[代码]bottom[代码]是不固定的,也就是说数值可能不准确导致[代码]space_height[代码]计算错误,显示效果达不到要求。 方案四 基于上述方案,[代码]swiper[代码]底部的load标签绝对定位[代码]bottom:0[代码],同时在[代码]swiper[代码]底部添加一个高度为0并且尾随内容box其后的标签(mark),然后获取这两个标签的top值之差: [代码]space_height = load_top - mark_top[代码][图片] 代码片段 这次获取到的空隙高度用于再计算[代码]swiper[代码]高度完美契合,美滋滋!!!
2018-05-23 - 瀑布图实现点击全屏预览效果
瀑布图如何实现点击全屏预览: // pages/map/index.js let col1H = 0; let col2H = 0; Page({ data: { images: [], col1: [], col2: [], }, onLoad: function (options) { wx.showToast({ title: "页面加载中...", icon: 'loading', duration: 2000, }); wx.getSystemInfo({ success: (res) => { let ww = res.windowWidth; let wh = res.windowHeight; let imgWidth = ww * 0.48; let scrollH = wh; this.setData({ scrollH: scrollH, imgWidth: imgWidth }); this.loadImages(); } }) }, onImageLoad: function (e) { let imageId = e.currentTarget.id; let oImgW = e.detail.width; //图片原始宽度 let oImgH = e.detail.height; //图片原始高度 let imgWidth = this.data.imgWidth; //图片设置的宽度 let scale = imgWidth / oImgW; //比例计算 let imgHeight = oImgH * scale; //自适应高度 let images = this.data.images; let imageObj = null; for (let i = 0; i < images.length; i++) { let img = images[i]; if (img.id === imageId) { imageObj = img; break; } } imageObj.height = imgHeight; let loadingCount = this.data.loadingCount - 1; let col1 = this.data.col1; let col2 = this.data.col2; if (col1H <= col2H) { col1H += imgHeight; col1.push(imageObj); } else { col2H += imgHeight; col2.push(imageObj); } let data = { loadingCount: loadingCount, col1: col1, col2: col2 }; if (!loadingCount) { data.images = []; } this.setData(data); }, loadImages: function () { let images = [ { pic: "https://7765-1", height: 0 }, { pic: "https://7765-2", height: 0}, { pic: "https://7765-3", height: 0}‘ { pic: "https://7765-4", height: 0}’ 】 let baseId = "img-" + (+new Date()); for (let i = 0; i < images.length; i++) { images[i].id = baseId + "-" + i; } this.setData({ loadingCount: images.length, images: images }); }, previewImg1: function(e) { //如何在这里实现点击全屏预览呢? this.loadImages(); var images = this.data.images; console.log(this.data.images); wx.previewImage({ urls: images, }) } })
2018-10-18 - Wxml2Canvas -- 快速生成小程序分享图通用方案
Wxml2Canvas库,可以将指定的wxml节点直接转换成canvas元素,并且保存成分享图,极大地提升了绘制分享图的效率。目前被应用于微信游戏圈、王者荣耀、刺激战场助手等小程序中。 github地址:https://github.com/wg-front/wxml2canvas 一、背景 随着小程序应用的日渐成熟,多处场景需要能够生成分享图便于用户进行二次传播,从而提升小程序的传播率以及加强品牌效应。 对于简单的分享图,比如固定大小的背景图加几行简短文字构成的分享小图,我们可以利用官方提供的canvas接口将元素直接绘制, 虽然繁琐了些,但能满足基本要求。 对于复杂的分享图,比如用户在微信游戏圈发表完话题后,需要将图文混排的富文本内容生成分享图,对于这种长度不定,内容动态变化的图片生成需求,直接利用官方的canvas接口绘制是十分困难的,包括但不限于文字换行、表情文字图片混排、文字加粗、子标题等元素都需要一一绘制。又如王者荣耀助手小程序,需要将十人对局的详细战绩绘制成分享图,包含英雄数据、装备、技能、对局结果等信息,要绘制100多张图片和大量的文字信息,如果依旧使用官方的接口一步一步绘制,对开发者来说简直就是一场噩梦。我们急需一种通用、高效的方式完成上述的工作。 在这样的背景下,wxml2cavnas诞生了,作为一种分享图绘制的通用方案,它不仅能快速的绘制简单的固定小图,还能直接将wxml元素真实地转换成canvas元素,并且适配各种机型。无论是复杂的图文混排的富文本内容,还是展现形式多样的战绩结果页,都可以利用wxml2cavnas完美地快速绘制并生成所期望的分享图片。 二、Wxml2Canvas介绍及示例 1. 介绍 Wxml2Cavnas库,是一个生成小程序分享图的通用方案,提供了两种绘制方式: 封装基础图形的绘制接口,包括矩形、圆形、线条、图片、圆角图片、纯文本等,使用时只需要声明元素类型并提供关键数据即可,不需要再关注canvas的具体绘制过程; wxml直接转换成canvas元素,使用时传入待绘制的wxml节点的class类名,并且声明绘制此节点的类型(图片、文字等),会自动读取此节点的computedStyle,利用这些数据完成元素的绘制。 2. 生成图示例 下面是两张极端复杂的分享图。 2.1 游戏圈话题 [图片] 点击查看完整长图 2.2.2 王者荣耀战绩 [图片] 点击查看完整大图 三、小程序的特性及局限 小程序提供了如下特性,可供我们便捷使用: measureText接口能直接测量出文本的宽度; SelectorQuery可以查询到节点对应的computedStyle。 利用第一条,我们在绘制超长文本时便于文本的省略或者换行,从而避免文字溢出。 利用第二条,我们可以根据class类名,直接拿到节点的样式,然后将style转换成canvas可识别的内容。 但是和html的canvas相比,小程序的canvas局限性很多。主要体现在如下几点: 不支持base64图片; 图片必须下载到本地后才能绘制到画布上; 图片域名需要在管理平台加入downFile安全域名; canvas属于原生组件,在移动端会置于最顶层; 通过SelectorQuery只能拿到节点的style,而无法获取文本节点的内容以及图片节点的链接。 针对以上问题,我们需要将base64图片转换jpg或png格式的图片,实现图片的统一下载逻辑,并且离屏绘制内容。针对第五条,好在SelectorQuery可以获取到节点的dataset属性,所以我们需要在待绘制的节点上显示地声明其类型(imgae、text等),并且显示地传入文本内容或图片链接,后文会有示例。 四、Wxml2Canvas使用方式 1. 初始化 首先在wxml中创建canvas节点,指定宽高: [代码] <canvas canvas-id="share" style="height: {{ height * zoom }}px; width: {{ width * zoom }}px;"> </canvas> [代码] 引入代码库,创建DrawImage实例,并传入如下参数: [代码] let DrawImage = require('./wxml2canvas/index.js'); let zoom = this.device.windowWidth / 375; let width = 375; let height = width * 3; let drawImage = new DrawImage({ element: 'share', // canvas节点的id, obj: this, // 在组件中使用时,需要传入当前组件的this width: width, // 宽高 height: height, background: '#161C3A', // 默认背景色 gradientBackground: { // 默认的渐变背景色,与background互斥 color: ['#17326b', '#340821'], line: [0, 0, 0, height] }, progress (percent) { // 绘制进度 }, finish (url) { // 画完后返回url }, error (res) { console.log(res); // 画失败的原因 } }); [代码] 所有的数字参数均以iphone6为基准,其中参数width和height决定了canvas画布的大小,规定值是在iphone6机型下的固定数值; zoom参数的作用是控制画布的缩放比例,如果要求画布自适应,则应传入 windowWidth / 375,windowWidth为手机屏幕的宽度。 2. 传入数据,生成图片 执行绘制操作: [代码] drawImage.draw(data, this); [代码] 执行绘制时需要传入数据data,数据的格式分为两种,下面展开介绍。 2.1 基础图形 第一种为基础的图形、图文绘制,直接使用官方提供接口,下面代码是一个基本的格式: [代码] let data = { list: [{ type: 'image', url: 'https://xxx', class: 'background_image', // delay: true, x: 0, y: 0, style: { width: width, height: width } }, { type: 'text', text: '文字', class: 'title', x: 0, y: 0, style: { fontSize: 14, lineHeight: 20, color: '#353535', fontFamily: 'PingFangSC-Regular' } }] } [代码] 如上,type声明了要元素的类型,有image、text、rect、line、circle、redius_image(圆角图)等,能满足绝大多数情况。 class类名指定了使用的样式,需要在style中写出,符合css样式规范。 delay参数用来异步绘制元素,会把此元素放在第二个循环中绘制。 x,y用来指定元素的起始坐标。 将css样式与元素分离的目的是便于管理与复用。 此种方式每个元素都相互独立,互不影响,能够满足自由度要求高的情况,可控性高。 2.2 wxml转换 第二种方式为指定wxml元素,自动获取,下面是示例: [代码] let data = { list: [{ type: 'wxml', class: '.panel .draw_canvas', limit: '.panel' x: 0, y: 0 }] } [代码] 如上,type声明为wxml时,会查找所有类名为draw_canvas的节点,并且加入到绘制队列中。 class传入的第一个类名限定了查询的范围,可以不传,第二个用来指定查找的节点,可以定义为任意不影响样式展现的通用类名。 limit属性用来限定相对位置,例如,一个文本的位置(left, top) = (50, 80), class为panel的节点的位置为(left, top) = (20, 40),则文本canvas上实际绘制的位置(x, y) = (50 - 20, 80 -40) = (30, 40)。如果不传入limit,则以实际的位置(x, y) = (50, 80)绘制。 由于小程序节点元素查询接口的局限,无法直接获取节点的文本内容和图片标签的src属性,也无法直接区分是文本还是图片,但是可以获取到dataset,所以我们需要在节点上显示地声明data-type来指明类型,再声明data-text传入文字或data-url传入图片链接。下面是个示例: [代码] <view class="panel"> <view class="panel__img draw_canvas" data-type="image" data-url="https://xxx"></view> <view class="panel__text draw_canvas" data-type="text" data-text="文字">文字</view> </view> [代码] 如上,会查询到两个节点符合条件,第一个为image图片,第二个为text文本,利用SelectorQuery查询它们的computedStyle,分别得到left、top、width、height等数据后,转换成canvas支持的格式,完成绘制。 除此之外,下面的示例功能更加丰富: [代码] <view class="panel"> <view class="panel__text draw_canvas" data-type="background-image" data-radius="1" data-shadow="" data-border="2px solid #000"></view> <view class="panel__text draw_canvas" data-type="text" data-background="#ffffff" data-padding="2 3 0 0" data-delay="1" data-left="10" data-top="10" data-maxlength="4" data-text="这是个文字">这是个文字</view> </view> [代码] 如上,第一个data-type为background-image,表示读取此节点的背景图片,因为可以通过computedStyle直接获取图片链接,所以不需要显示传入url。声明data-radius属性,表示要将此图绘成乘圆形图片。data-border属性表示要绘制图片的边框,虽然也可以通过computedStyle直接获取,但是为了避免非预期的结果,还是要声明传入,border格式应符合css标准。此外,图片的box-shadow等样式都会根据声明绘制出来。 第二个文本节点,声明了data-background,则会根据节点的位置属性给文字增加背景。 data-padding属性用来修正背景的位置和宽高。data-delay属性用来延迟绘制,可以根据值的大小,来控制元素的层级,data-left和data-top用来修正位置,支持负值。data-maxlength用来限制文本的最大长度,超长时会截取并追加’…’。 此外,data-type还有inline-text,inline-image等行内元素的绘制,其实现较为复杂,会在后文介绍。 五、Wxml2Canvas实现原理 1. 绘制流程 整个绘制流程如下: [图片] 因为小程序的限制,只能在画布上绘制本地图片,所以统一先对图片提前下载,然后再绘制,为了避免图片重复下载,内部维护一个图片列表,会对相同的图片链接去重,减少等待时间。 2. 基本图形的实现 基础图形的绘制比较简单,内部实现只是对基础能力的封装,使用者不用再关注canvas的绘制过程,只需要提供关键数据即可,下面是一个图片绘制的实现示例: [代码] function drawImage (item, style) { if(item.delay) { this.asyncList.push({item, style}); }else { if(item.y < 0) { item.y = this.height + item.y * zoom - style.height * zoom; }else { item.y = item.y * zoom; } if(item.x < 0) { item.x = this.width + item.x * zoom - style.width * zoom; }else { item.x = item.x * zoom; } ctx.drawImage(item.url, item.x, item.y, style.width * zoom, style.height * zoom); ctx.draw(true); } } [代码] 如上,x,y值坐标支持传入负值,表示从画布的底部和右侧计算位置。 3. Wxml转Canvas元素的实现 3.1 computedStyle的获取 首先需要获取wxml的样式,代码示例如下: [代码] query.selectAll(`${item.class}`).fields({ dataset: true, size: true, rect: true, computedStyle: ['width', 'height', ...] }, (res) => { self.drawWxml(res); }) [代码] 3.2 块级元素的绘制 对于声明为image、text的元素,默认为块级元素,它们的绘制都是独立进行的,不需要考虑其他的元素的影响,以wxml节点为圆形的image为例,下面是部分代码: [代码] if(sub.dataset.type === 'image') { let r = sub.width / 2; let x = sub.left + item.x * zoom; let y = sub.top + item.y * zoom; let leftFix = +sub.dataset.left || 0; let topFix = +sub.dataset.top || 0; let borderWidth = sub.borderWidth || 0; let borderColor = sub.borderColor; // 如果是圆形图片 if(sub.dataset.radius) { // 绘制圆形的border if(borderWidth) { ctx.beginPath() ctx.arc(x + r, y + r, r + borderWidth, 0, 2 * Math.PI) ctx.setStrokeStyle(borderColor) ctx.setLineWidth(borderWidth) ctx.stroke() ctx.closePath() } // 绘制圆形图片的阴影 if(sub.boxShadow !== 'none') { ctx.beginPath() ctx.arc(x + r, y + r, r + borderWidth, 0, 2 * Math.PI) ctx.setFillStyle(borderColor); setBoxShadow(sub.boxShadow); ctx.fill() ctx.closePath() } // 最后绘制圆形图片 ctx.save(); ctx.beginPath(); ctx.arc((x + r), (y + r) - limitTop, r, 0, 2 * Math.PI); ctx.clip(); ctx.drawImage(url, x + leftFix * zoom, y + topFix * zoom, sub.width, sub.height); ctx.closePath(); ctx.restore(); }else { // 常规图片 } } [代码] 如上,块级元素的绘制和基础图形的绘制差异不大,理解起来也很容易,不再多述。 3.3 令人头疼的行内元素的绘制 当wxml的data-type声明为inline-image或者inline-text时,我们认为是行内元素。行内元素的绘制是一个难点,因为元素之前存在关联,所以不得不考虑各种临界情况。下面展开细述。 3.3.1 纯文本换行 对于长度超过一行的行内元素,需要计算出合适的换行位置,下图所示的是两种临界情况: [图片] [图片] 如上图所示,第一种情况为最后一行只有一个文字,第二种情况最后一行的文字长度和宽度相同。虽然长度不同,但都可通过下面代码绘制: [代码] let lineNum = Math.ceil(measureWidth(text) / maxWidth); // 文字行数 let sinleLineLength = Math.floor(text.length / lineNume); // 向下取整,保证多于实际每行字数 let currentIndex = 0; // 记录文字的索引位置 for(let i = 0; i < lineNum; i++) { let offset = 0; // singleLineLength并不是精确的每行文字数,要校正 let endIndex = currentIndex + sinleLineLength + offset; let single = text.substring(currentIndex, endIndex); // 截取本行文字 let singleWidth = measureWidth(single); // 超长时,左移一位,直至正好 while(singleWidth > maxWidth) { offset--; endIndex = currentIndex + sinleLineLength + offset; single = text.substring(currentIndex, endIndex); singleWidth = measureWidth(single); } currentIndex = endIndex; ctx.fillText(single, item.x, item.y + i * style.lineHeight); } // 绘制剩余的 if(currentIndex < text.length) { let last = text.substring(currentIndex, text.length); ctx.fillText(last, item.x, item.y + lineNum * style.lineHeight); } [代码] 为了避免计算太多次,首先算出大致的行数,求出每行的文字数,然后移位索引下标,求出实际的每行的字数,再下移一行继续绘制,直到结束。 3.3.2 非换行的图文混排 [图片] 上图是一个包含表情图片和加粗文字的混排内容,当使用Wxml2Canvas查询元素时,会将第一行的内容分为五部分: 文本内容:这是段文字; 表情图片:发呆表情(非系统表情,image节点展现); 表情图片:发呆表情; 文本内容:这也; 加粗文本内容:是一段文字,这也是文字。 对于这种情况,执行查询computedStyle后,会返回相同的top值。我们把top值相同的元素聚合在一起,认为它们是同一行内容,事实也是如此。因为表情大小的差异以及其他影响,默认规定top值在±2的范围内都是同一行内容。然后将top值的聚合结果按照left的大小从左往右排列,再一一绘制,即可完美还原此种情况。 3.3.3 换行的图文混排 当混排内容出现了换行情况时,如下图所示: [图片] 此时的加粗内容占据了两行,当我们依旧根据top值归类时,却发现加粗文字的left值取的是第二行的left值。这就导致加粗文字和第一部分的文字的top值和left值相同,如果直接绘制,两部分会发生重叠。 为了避免这种尴尬的情况,我们可以利用加粗文字的height值与第一部分文字的height值比较,显然前者是后者的两倍,可以得知加粗部分出现了换行情况,直接将其放在同组top列表的最后位置。换行的部分根据lineHeight下移绘制,同时做记录。 最后一部分的文本内容也出现了换行情况,同样无法得到真正的起始left值,并且其top值与上一部分换行后的top值相同。此时应该将他的left值追加加粗换行部分的宽度,正好得到真正的left值,最后再绘制。 大多数的行内元素的展现形式都能以上述的逻辑完美还原。 六、总结 基于基础图形封装和wxml转换这两种绘制方式,可以满足绝大多数的场景,能够极大地减少工作量,而不需要再关注内部实现。在实际使用中,二者并非孤立存在,而更多的是一起使用。 [图片] 如上图所示,对于列表内容我们利用wxml读取绘制,对于下部的白色区域,不是wxml节点内容,我们可以使用基础图形绘制方式实现。二者的结合更加灵活高效。 目前Wxml2Canvas已经在公司内部开源,不久会放到github上,同时也在不断完善中,旨在实现更多的样式展现与提升稳定性和绘制速度。 如果有更好的建议与想法,请联系我。
2019-02-28 - 小程序canvas的那些事
背景 业务场景需要在小程序内生成活动的分享海报,图片中的某些数据需动态展示。可行的方案有️二: 服务端合成:直接返回给前端图片URL 客户端合成:客户端利用canvas绘制 在当前业务场景下,使用客户端合成会优于服务端合成,避免造成不必要的服务器CPU浪费。 下面主要谈谈**客户端(canvas)**合成的过程。 实现思路 小程序端发起请求,获取需动态展示的数据; 利用canvas绘制画布; 导出图片保存到相册。 小技巧&那些坑 理想很丰满,现实很骨感。 实现思路很简单,然而,在实现过程中,发现会趟一些坑,也有一些小技巧,遂记录下来,以供参考。 promise化 画布的绘制依赖系统信息(自适应和优化图片清晰度)和动态数据。故画布需要在所有前置条件都准备完成时,方可绘制。为了提高代码优雅度和维护性,建议用promise化,避免回调地狱(Callback Hell)。 [代码] let promise1 = new Promise((resolve, reject) => { this.getData(resolve, reject) }); let promise2 = new Promise((resolve, reject) => { this.getSystemInfo(resolve, reject) }); Promise.all([promise1, promise2]).then(() => { this.drawCanvas() }).catch(err => { console.log(err) }); [代码] 自适应 1、为了在各个机型下保持大小自适应,需要计算出缩放比: [代码] getSystemInfo(resolve, reject) { try { const res = wx.getSystemInfoSync() //缓存系统信息 systemInfo = res //这里视觉基于iPone6(375*667)设计,2x图视觉,可以填写750,1x图视觉,可以填写375 zoom = res.windowWidth / 750 * 1 resolve() } catch (e) { // Do something when catch error reject("获取机型失败") } } [代码] 2、绘制时进行按缩放比进行缩放,如: [代码]ctx.drawImage(imgUrl, x * zoom, y * zoom, w * zoom, h * zoom) [代码] 绘制网络图片 经测试,绘制CDN图片需要先将图片下载到本地,在进行绘制: [代码]wx.downloadFile({ url: imgUrl, success: res => { if (res.statusCode === 200) { ctx.drawImage(res.tempFilePath, 326 * zoom, 176 * zoom, 14 * zoom, 14 * zoom) } } }) [代码] 绘制base64图片 因为业务上某些原因,依赖的图片数据,后端只能以base64格式返回给前端,而小程序在真机上无法直接绘制(开发工具OK)。 解决思路(存在兼容性问题,fileManager**基础库 1.9.9 **开始支持): 1、调用fileManager.writeFile存储base64到本地; 2、绘制本地图片。 实现代码如下: [代码]// 先获得一个文件实例 fileManager = wx.getFileSystemManager() // 把图片base64格式转存到本地,用于canvas绘制 fileManager.writeFile({ filePath: `${wx.env.USER_DATA_PATH}/qrcode.png`, data: self.data.qrcode, encoding: 'base64', success: () => { //此处需先调用wx.getImageInfo,方可绘制成功 wx.getImageInfo({ src: `${wx.env.USER_DATA_PATH}/qrcode.png`, success: () => { //绘制二维码 ctx.drawImage(`${wx.env.USER_DATA_PATH}/qrcode.png`, 207 * zoom, 313 * zoom, 148 * zoom, 148 * zoom) ctx.draw() } }) } }) [代码] 保存到本地相册 wx.saveImageToPhotosAlbum这个API需用户授权,故开发者需做好拒绝授权的兼容。此处实现对拒绝授权的场景进行引导。 [代码]canvas2Img(e) { wx.getSetting({ success(res) { if (res.authSetting['scope.writePhotosAlbum'] === undefined) { //呼起授权界面 wx.authorize({ scope: 'scope.writePhotosAlbum', success() { save() } }) } else if (res.authSetting['scope.writePhotosAlbum'] === false) { //引导拒绝过授权的用户授权 wx.showModal({ title: '温馨提示', content: '需要您授权保存到相册的权限', success: res => { if (res.confirm) { wx.openSetting({ success(res) { if (res.authSetting['scope.writePhotosAlbum']) { save() } } }) } } }) } else { save() } } }) function save() { wx.canvasToTempFilePath({ x: 0, y: 0, width: 562*zoom, height: 792*zoom, destWidth: 562*zoom*systemInfo.pixelRatio, destHeight: 792*zoom*systemInfo.pixelRatio, fileType: 'png', quality: 1, canvasId: 'shareImg', success: res => { wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath, success: () => { wx.showModal({ content: '保存成功', showCancel: false, confirmText: '确认' }) }, fail: res => { if (res.errMsg !== 'saveImageToPhotosAlbum:fail cancel') { wx.showModal({ content: '保存到相册失败', showCancel: false }) } } }) }, fail: () => { wx.showModal({ content: '保存到相册失败', showCancel: false }) } }) } [代码] 导出清晰图片 wx.canvasToTempFilePath有destWidth(输出的图片的宽度)和destHeight(输出的图片的高度)属性。若此处以canvas的宽高去填写的话,在高像素手机下,导出的图片会模糊。 原因:destWidth和destHeight单位是物理像素(pixel),canvas绘制的时候用的是逻辑像素(物理像素=逻辑像素 * density),所以这里如果只是使用canvas中的width和height(逻辑像素)作为输出图片的长宽的话,生成的图片width和height实际上是缩放了到canvas的 1 / density大小了,所以就显得比较模糊了。 这里应该乘以设备像素比,实现如下: [代码]wx.canvasToTempFilePath({ x: 0, y: 0, width: 562*zoom, height: 792*zoom, destWidth: 562*zoom*systemInfo.pixelRatio, destHeight: 792*zoom*systemInfo.pixelRatio, fileType: 'png', quality: 1, canvasId: 'shareImg' )} [代码] 特殊字体的绘制 研究发现,目前小程序canvas无法支持设置特殊字体,而业务生成的海报,又期望以特殊字体去呈现,最终取了个折中方案——保留数字部分的特殊样式。 实现方式为:把0-9这10个数字单独切图,用ctx.drawImage API,以图片形式去绘制。 [代码]drawNum(num, x, y, w, h) { return new Promise(function (resolve, reject) { //这里存储0-9的图片CDN链接 let numMap = [] wx.downloadFile({ url: numMap[num], success: res => { if (res.statusCode === 200) { ctx.drawImage(res.tempFilePath, x * zoom, y * zoom, w * zoom, h * zoom) resolve() } }, fail: () => { reject() } }) }) } [代码] 安卓机型图片绘制锯齿化问题 测试发现,同样的绘制方案,在安卓下,调用ctx.drawImage方法,图片会出现锯齿问题。测试还发现,原像素越高,锯齿化程度降低(但业务上使用太大像素的素材也不合理),这里需要客户端底层进行优化,目前没有找到合适的解决方案。 总结 个人觉得,目前小程序canvas就底层能力上相比web还有一些不足。所以应注意两点: 提前从业务出发,考虑当前实现的可行性,以便采取更优方案(如特殊字体,像素要求等); 若绘制canvas导出图片是个高频场景,可参考html2canvas进行封装,以便提高效能(SelectorQuery节点查询需1.9.90以上)。 ps:之前有想过利用web-view方式,在传统网页去绘制,然后通过web-view和小程序的通信来实现的方式。时间原因,并未尝试,感兴趣同学可以尝试下。
2019-03-07 - canvas绘图怎么匹配多个手机型号 使用rpx
let canvas = wx.createCanvasContext("shareCanvas"); //绘制背景 canvas.drawImage(res[1].path, 0, 0, 301, 420); canvas.save(); //绘制二维码 canvas.drawImage(res[0].path, 108, 262, 85, 85); //绘制文字 canvas.font = "normal bold 20px sans-serif"; canvas.fillStyle = "#fff"; canvas.setTextAlign("center"); canvas.fillText("您的好友“" + this.wechat + "”", 150, 40); canvas.fillText("送你20元红包", 150, 65); canvas.draw();
2019-01-04 - canvas画图随记
最近画了一张分享图,在此记录一下遇到的问题及解决方法。 画布尺寸自适应 微信小程序尺寸为rpx,会自适应各种机型,但canvas的方法参数默认为px,所以需要对画布上的每一项参数乘以(画布宽度/设备屏幕宽度),将rpx换算成px,达到尺寸自适应的目的,所以将此系数设置为全局变量。代码如下: [代码]var app = getApp(); const device = wx.getSystemInfoSync(); const width = device.windowWidth;//设备屏幕宽度 const xs = width / 375; [代码] 调用: [代码]createCard: function() { var context = wx.createCanvasContext('myCanvas'); context.fillText('内容', 100 * xs , 100 * xs) } [代码] 长文本换行 由于fillText只能画一行,但很多情况下是需要将长文本自动换行展示的,这个时候则需要对文本进行处理。 方法:遍历该文本,计算出每一字宽度之和,当该宽度大于文本最大宽度时绘制当前截取部分,并将绘制高度加上行高,宽度置0,重新计算并绘制下一行。当只剩最后一字时,绘制剩余部分。 缺陷:当文本内有换行符时,绘制会换行,但当前计算宽度不会增加,导致格式混乱。所以需要在计算宽度之和前判断该字符是否为换行符,若果是,则绘制当前部分,开始下一行的计算。 完善:如果需要知道绘制文本的总高度,设置初始文本高度为0,在绘制一行时加上行高则可。代码如下: [代码] /** * context:当前画布对象 * text:文本内容 * leftWidth:文本左上角x坐标 * initHeight:文本左上角y坐标 * canvasWidth:一行文本最大宽度 */ drawText: function(context, text, leftWidth, initHeight, canvasWidth) { var lineWidth = 0; //文本宽度 var textHeight = 0; //文本总高度 var lastSubStrIndex = 0; //每次开始截取的字符串的索引 for (let i = 0; i < text.length; i++) { if (text[i] == "\n") { //如遇换行 context.fillText(text.substring(lastSubStrIndex, i), leftWidth, initHeight, canvasWidth); //绘制截取部分 initHeight += 17.5 * xs; //17.5为字体高度 lineWidth = 0; lastSubStrIndex = i + 1; //截取字符串时跳过换行符 textHeight += 17.5 * xs; } else { lineWidth += context.measureText(text[i]).width; //计算每个字的宽度之和 if (lineWidth > canvasWidth) { context.fillText(text.substring(lastSubStrIndex, i), leftWidth, initHeight, canvasWidth); initHeight += 17.5 * xs; lineWidth = 0; lastSubStrIndex = i; textHeight += 17.5 * xs; } } if (i == text.length - 1) { //绘制剩余部分 context.fillText(text.substring(lastSubStrIndex, i + 1), leftWidth, initHeight, canvasWidth); textHeight += 17.5 * xs; } } return textHeight; }, [代码] 调用: [代码] var text = '新建项目选择小程序项目,选择代码存放的硬盘路径,填入刚刚申请到的小程序的 AppID,给你的项目起一个好听的名字,最后,勾选 "创建 QuickStart 项目" (注意: 你要选择一个空的目录才会有这个选项),点击确定,你就得到了你的第一个小程序了,点击顶部菜单编译就可以在微信开发者工具中预览你的第一个小程序。'; context.setFontSize(15 * xs) that.drawText(context, text, 30 * xs, 100 * xs, 320 * xs) [代码] 高度自适应 如碰到画布高度需要根据内容高度不同而不同,或者某元素与可变化高度的元素固定距离的情况,则需要计算出可变化元素高度,再根据该高度进行计算其他高度。例如: [图片] 微信图标始终距离文本30px,而该文本高度可变,所以图标的左上角y轴坐标=文本y轴坐标+文本高度+下边距,代码如下: [代码]var textHeight = that.drawText(context, text, 30 * xs 100 * xs, 320 * xs) context.drawImage('/images/wx.png', 68 * xs, (100 + 30) * xs + textHeight, 80 * xs, 80 * xs) [代码] 注意:因为计算文本高度的方法里已经乘过系数,所以这里不需要乘。宽度自适应同理。 绘制圆角矩形框 由于没有绘制圆角矩形的方法,所以需要将圆角矩形分开绘制。 方法:将四个圆角当成四分之一圆绘制,然后分别画四条边,坐标如下图所示。 [图片] 代码: [代码] /** * context:当前画布对象 * x:圆角矩形左上角x坐标 * y:圆角矩形左上角y坐标 * w:宽度 * h:高度 * r:border-radius * color:填充颜色 */ roundRect(ctx, x, y, w, h, r, color) { ctx.beginPath() // 左上角 ctx.arc(x + r, y + r, r, Math.PI, Math.PI * 1.5) // 上边框 ctx.moveTo(x + r, y) ctx.lineTo(x + w - r, y) ctx.lineTo(x + w, y + r) // 右上角 ctx.arc(x + w - r, y + r, r, Math.PI * 1.5, Math.PI * 2) // 右边框 ctx.lineTo(x + w, y + h - r) ctx.lineTo(x + w - r, y + h) // 右下角 ctx.arc(x + w - r, y + h - r, r, 0, Math.PI * 0.5) // 下边框 ctx.lineTo(x + r, y + h) ctx.lineTo(x, y + h - r) // 左下角 ctx.arc(x + r, y + h - r, r, Math.PI * 0.5, Math.PI) // 左边框 ctx.lineTo(x, y + r) ctx.lineTo(x + r, y) //填充颜色 ctx.setFillStyle(color); ctx.fill() ctx.closePath() } [代码] 调用: [代码]that.roundRect(context, 15 * xs, 60 * xs, 350* xs, 200 * xs, 14 * xs, '#ffffff') [代码] 文本加粗 官方文档里有说到font的使用规则与css语法一致,有几个需要注意的地方,否则可能会导致设置无效。 [图片] 调用: [代码] context.font = "normal bold 27px sans-serif"; context.setFontSize(27 * xs) context.fillText('加粗字体', 100 * xs , 145 * xs) [代码] 效果: [图片] 注意:在真机上若没有写第一个normal参数,则不能成功设置。 字体大小可以在下面重新赋值。 如果没有效果可以注意console有没有如下图所示 设置无效的警告,原因很大可能是因为参数写的不对。 [图片] 圆形头像绘制 方法:在画布上剪切一个圆,然后在圆上画头像,最后恢复即可。有一个需要注意的地方,drawImage方法只能绘制本地图片,如果需要绘制网络图片需下载完成之后再画。代码如下: [代码] context.save() context.beginPath() context.arc(77 / 2 * xs + 150 * xs, 77 / 2 * xs + 73 * xs, 77 / 2 * xs, 0, Math.PI * 2, false) context.clip() var headimg = '/images/headimg.jpg' context.drawImage(headimg, 150 * xs, 73 * xs, 77 * xs, 77 * xs) context.restore() context.draw(); [代码] 遇到的问题:当图片为长方形时,强行将图片压缩为正方形会导致头像变形。 解决办法:image组件里,参数mode有一个值为aspectFill,即保持纵横比缩放图片,只保证图片的短边能完全显示出来,我们参考这种思路来截取图片。 [图片] 这里以宽比高长的图为例。如上图所示,圆为头像显示位置,线为中线,矩形框为一张宽大于高的图片。矩形左上角即为画图时的左上角坐标。截部分如图所示,得到图片宽高后,短边固定为头像尺寸,长边根据短边缩放比计算得到。图片宽=原图宽 /(头像高 / 原图高)。左上角的x轴坐标为:中线x坐标 - 图片宽 / 2。代码如下所示: [代码] context.save() context.beginPath() context.arc(77 / 2 * xs + 150 * xs, 77 / 2 * xs + 73 * xs, 77 / 2 * xs, 0, Math.PI * 2, false) context.clip() var headimg = '/images/headimg.jpg'; //头像路径 var headimgHeight = 0; var headimgWidth = 0; wx.getImageInfo({ src: headimg, success(res) { headimgHeight = res.height; //原图高度 headimgWidth = res.width; //原图宽度 //当宽 > 高时 if (headimgWidth > headimgHeight) { var width = headimgWidth / (headimgHeight / (77 * xs)); //图片宽度 var x = (150 + 77 / 2) * xs - width / 2; //x轴坐标 context.drawImage(headimg, x, 73 * xs, width, 77 * xs) } else { //当高>=宽时 var height = headimgHeight / (headimgWidth / (77 * xs)); //图片高度 var h = (73 + 77 / 2) * xs - height / 2; //y轴坐标 context.drawImage(headimg, 150 * xs, h, 77 * xs, height) context.restore() context.draw(); } } }) [代码] 注意:这里得到的图片宽高已经是px为单位,所以不乘系数。
2019-03-18 - 几行代码实现小程序云开发提现功能
先看效果: [图片] 纯云开发实现,下面说使用步骤: 一:开通商户的企业付款到领取功能 说明地址: https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_1 使用条件 1、商户号(或同主体其他非服务商商户号)已入驻90日 2、截止今日回推30天,商户号(或同主体其他非服务商商户号)连续不间断保持有交易 使用条件是第一难,第二难在下面这里 [图片] 在网上找了很多,感觉是云开发这里的一个不完善地方,如果不填ip,会报这种错 [代码]{"errorCode":1,"errorMessage":"user code exception caught","stackTrace":"NO_AUTH"} [代码] [代码]<xml> <return_code><![CDATA[SUCCESS]]></return_code> <return_msg><![CDATA[此IP地址不允许调用接口,如有需要请登录微信支付商户平台更改配置]]></return_msg> <mch_appid><![CDATA[wx383426ad9ffe1111]]></mch_appid> <mchid><![CDATA[1536511111]]></mchid> <result_code><![CDATA[FAIL]]></result_code> <err_code><![CDATA[NO_AUTH]]></err_code> <err_code_des><![CDATA[此IP地址不允许调用接口,如有需要请登录微信支付商户平台更改配置]]></err_code_des> </xml> [代码] 云开发没有ip这个概念,所以这里有些无从下手,不过这里我采用了个替代方案,参考了社区帖子: https://developers.weixin.qq.com/community/develop/doc/00088cff3a40d87d80f7267b65b800 之后我也亲自验证了,基本上就是这几个,当然肯定不够,但是可以自己在逻辑上进行处理,ip以下: [代码]172.81.207.12 172.81.212.74 172.81.236.99 172.81.235.12 172.81.245.51 212.64.65.131 212.64.84.22 212.64.85.35 212.64.85.139 212.64.87.134 [代码] 接着,可以动手了 二、云开发部分 1、设置云存储 证书配置地址: [图片] 下载后有三个文件,我们只需要p12结尾的那个 [图片] 然后,将这个apiclient_cert.p12文件上传到你的云存储 [图片] 这里处理完了,我们只需要一个东西,就是fileID也就是常说的云存储ID(上图红框内容) 2、配置云函数 新建云函数ref云函数 [图片] 代码如下: [代码]const config = { appid: 'wx383426ad9ffe1111', //小程序Appid envName: 'zf-shcud', // 小程序云开发环境ID mchid: '1111111111', //商户号 partnerKey: '1111111111111111111111', //此处填服务商密钥 pfx: '', //证书初始化 fileID: 'cloud://zf-shcud.11111111111111111/apiclient_cert.p12' //证书云存储id }; const cloud = require('wx-server-sdk') cloud.init({ env: config.envName }) const db = cloud.database(); const tenpay = require('tenpay'); //支付核心模块 exports.main = async(event, context) => { //首先获取证书文件 const res = await cloud.downloadFile({ fileID: config.fileID, }) config.pfx = res.fileContent let pay = new tenpay(config,true) let result = await pay.transfers({ //这部分参数含义参考https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_2 partner_trade_no: 'bookreflect' + Date.now() + event.num, openid: event.userinfo._openid, check_name: 'NO_CHECK', amount: parseInt(event.num) * 100, desc: '二手书小程序提现', }); if (result.result_code == 'SUCCESS') { //如果提现成功后的操作 //以下是进行余额计算 let re=await db.collection('user').doc(event.userinfo._id).update({ data: { parse: event.userinfo.parse - parseInt(event.num) } }); return re } } [代码] 需安装的依赖:wx-server-sdk、tenpay 这里只是实现了简单原始的提现操作,关于提现后,比如防止重复提交,提现限额这些,在开源二手书商城上有完整流程,地址: https://github.com/xuhuai66/used-book-pro 这种办法,不是每次都能成功提现,小概率遇到ip未在白名单情况,还是希望,云开发团队能尽快出一个更好的解决方案吧
2019-09-21 - 数组去重常用方法
function DuplicateRemovalArray_1(arr) { var resultArr; resultArr = arr.filter(function (item, index, array) { return array.indexOf(item) == index; }); return resultArr; } function DuplicateRemovalArray_2(arr) { /** * Accumulator (acc) (累计器) Current Value (cur) (当前值) Current Index (idx) (当前索引) Source Array (src) (源数组) * */ let result = arr.sort().reduce((accumulator, currentValue, currentIndex, array) => { if (accumulator.length === 0 || accumulator[accumulator.length - 1] !== currentValue) { accumulator.push(currentValue); } return accumulator; }, []); return result; } function DuplicateRemovalArray_3(arr) { let mapArray = Array.from(new Set(arr)); return mapArray; } function DuplicateRemovalArray_4(arr) { let mapArray = [...new Set(arr)] return mapArray; } let p1 = DuplicateRemovalArray_1([1,2,2,3]); let p2 = DuplicateRemovalArray_2([3,2,6,6,6,3]); let p3 = DuplicateRemovalArray_3([1,1,0,1,5]); let p4 = DuplicateRemovalArray_4([1,1,0,1,5]); console.log(p1); console.log(p2); console.log(p3); console.log(p4); let Channel = [ { "key": "其他", "Count": 1638, "Ratio": "2.939300%" }, { "key": "其他", "Count": 23057, "Ratio": "41.374900%" }, { "key": "平面", "Count": 26674, "Ratio": "47.865400%" }, { "key": "工程", "Count": 32, "Ratio": "0.057400%" }, ]; var obj = {}; let arr = Channel.reduce(function (item, next) { obj[next.key] ? '' : obj[next.key] = true && item.push(next); return item; }, []); console.log(arr); // 对象去重 输出 [ 1, 2, 3 ] [ 2, 3, 6 ] [ 1, 0, 5 ] [ 1, 0, 5 ] [ { key: '其他', Count: 1638, Ratio: '2.939300%' }, { key: '平面', Count: 26674, Ratio: '47.865400%' }, { key: '工程', Count: 32, Ratio: '0.057400%' } ] function uniqueArray(list) { list.sort() // [1,101,2,20,21,211,3,1] 注意排序 101 跟 2 const size = list.length let slowP = 0 for( let fastP = 0; fastP < size; fastP++) { if(list[fastP] != list[slowP]) { slowP++ list[slowP] = list[fastP] } } // console.log("slowP", slowP, list) return list.slice(0, slowP + 1) } let t = uniqueArray([1,101,2,20,21,211,3,1]) t=> [ 1, 101, 2, 20, 21, 211, 3 ]
2022-11-09 - canvas生成海报图、分享
首先话不多说元素样式走起来 canvas宽高由js变量动态定义 html <canvas class=‘canvas’ canvas-id=“shareCanvas” style=“width:{{canvasWidth}}px;height:{{canvasHeight}}px”></canvas> css .canvas{ margin: 0 auto; letter-spacing: 2rpx; //画布上文字间距 我实在不知道canvas api怎么控制字间距 margin-top: 10%; } 然后绘制canvas代码 canvasImg:function(){ [代码]var that = this; wx.showLoading({ title: '图片正在生成' }); //拿到用户名称和用户头像 name ,img var name = app.header.userInfo.nickName; var img = app.header.userInfo.avatarUrl.replace("http:", "https:"); //请求后台接口拿到小程序码临时url //这里是我们后台根据我传递的页面路径和标识获取对应小程序的小程序码 返回一个图片临时缓存的url wx.request({ url: app.data.proxy + '/miniprogram/getUnlimited', data: { type: app.data.pdd, page:'pages/index/index' }, success(res) { var image = res.data.result; //拿到临时url 使用getImageInfo缓存到本地 wx.getImageInfo({ src:image, success(res){ //liload 小程序码本地缓存地址 var liload = res; //获取用户设备宽高 wx.getImageInfo({ src: img, success(res) { var width,height; wx.getSystemInfo({ success(res) { width = res.screenWidth * 0.6; height = Math.round(width * 1066 / 600); that.setData({ canvasWidth: width, canvasHeight: height }); } }); var x = width/750; //设置相对canvas自适应根元素大小 const ctx = wx.createCanvasContext('shareCanvas'); ctx.drawImage('../img/pinduoduo.jpg', 0, 0, 500*x, 812*x); ctx.save(); ctx.setTextAlign('center'); // 文字居中 ctx.setFillStyle('#fff'); // 文字颜色:黑色 ctx.setFontSize(16); // 文字字号 ctx.fillText(name, 250*x, 250*x); //名字 ctx.setFontSize(14); // 文字字号 ctx.fillText('邀请你使用【拼拼宝盒】', 250*x, 300*x); ctx.save(); //圆形头像框 ctx.setStrokeStyle('rgba(0,0,0,.2)'); ctx.arc(250 * x, 140 * x, 60 * x, 0, 2 * Math.PI); ctx.setStrokeStyle('rgba(0,0,0,.2)'); ctx.arc(250 * x, 460 * x, 120 * x, 0, 2 * Math.PI); ctx.fill('#fff'); //小程序码 ctx.clip(); ctx.drawImage(liload.path, 150*x, 360*x, 200*x, 200*x); //头像 ctx.clip(); ctx.drawImage(res.path, 190*x, 80*x, 120*x, 120*x); ctx.save(); ctx.stroke(); ctx.draw(); wx.hideLoading(); } }); } }) } }); [代码] } 绘制完成 [图片] 保存事件 这里首先用getSetting检测用户有没有开启相册权限 有的话直接保存 没有的话弹到权限页面让用户授权 btnTap:function () { [代码]var that = this; wx.showLoading({ title: '正在保存', mask: true, }); wx.getSetting({ success(res) { if (res.authSetting['scope.writePhotosAlbum']) { that.saveImg(); } else if (res.authSetting['scope.writePhotosAlbum'] === undefined) { wx.authorize({ scope: 'scope.writePhotosAlbum', success() { that.saveImg(); }, fail() { wx.showToast({ title: '您没有授权,无法保存到相册', icon: 'none' }) } }) } else { wx.openSetting({ success(res) { if (res.authSetting['scope.writePhotosAlbum']) { that.saveImg(); } else { wx.showToast({ title: '您没有授权,无法保存到相册', icon: 'none' }) } } }) } } }) [代码] } 用户有授权调用保存图片API 保存图片到手机 saveImg:function(){ [代码] wx.canvasToTempFilePath({ canvasId: 'shareCanvas', success: function (res) { wx.hideLoading(); var tempFilePath = res.tempFilePath; wx.saveImageToPhotosAlbum({ filePath: tempFilePath, success(res) { wx.showModal({ content: '图片已保存到相册,赶紧晒一下吧~', showCancel: false, confirmText: '好的', confirmColor: '#333', success: function (res) { if (res.confirm) { var arr = []; arr.push(tempFilePath); //保存后全屏显示 wx.previewImage({ urls: arr, current: arr }); } }, fail: function (res) { } }) }, fail: function (res) { wx.showToast({ title: '没有相册权限', icon: 'none', duration: 2000 }); } }) } }); [代码] } 好了 就这么多了 第一次发帖 有不足之处望各路大佬见谅、指出不胜感激 附代码片段:https://developers.weixin.qq.com/s/8Z8oXbm17ojh
2020-07-28 - 小程序之日历签到积分
[图片] 该示例为纯手写代码,暂无插件,不多说直接上代码 我们的实现思路: JS部分 1、获取当前年月 const date = new Date(); cur_year = date.getFullYear(); cur_month = date.getMonth() + 1; const weeks_ch = [‘日’, ‘一’, ‘二’, ‘三’, ‘四’, ‘五’, ‘六’]; this.setData({ cur_year, cur_month, weeks_ch, }) 2、获取当月共多少天 getThisMonthDays: function (year, month) { return new Date(year, month, 0).getDate() }, 3、获取当月第一天星期几 getFirstDayOfWeek: function (year, month) { return new Date(Date.UTC(year, month - 1, 1)).getDay(); }, 4、计算当月1号前空了几个格子,把它填充在days数组的前面 calculateEmptyGrids: function (year, month) { var that = this; //计算每个月时要清零 that.setData({ days: [] }); const firstDayOfWeek = this.getFirstDayOfWeek(year, month); if (firstDayOfWeek > 0) { for (let i = 0; i < firstDayOfWeek; i++) { var obj = { date: null, isSign: false } that.data.days.push(obj); } this.setData({ days: that.data.days }); //清空 } else { this.setData({ days: [] }); } }, 5、绘制当月天数占的格子,并把它放到days数组中 calculateDays: function (year, month, sign) { var that = this; var isSign; const thisMonthDays = this.getThisMonthDays(year, month); for (var i = 1; i <= thisMonthDays; i++) { var obj = { date: i, isSign: ‘’ } for (var j = 0; j < sign.length; j++) { if (i == parseInt(sign[j].create_time.substr(8, 2))) { obj.isSign = true; break; } } that.data.days.push(obj); } this.setData({ days: that.data.days }); }, 6、切换控制年月,上一个月,下一个月 handleCalendar: function (e) { const handle = e.currentTarget.dataset.handle; const cur_year = this.data.cur_year; const cur_month = this.data.cur_month; if (handle === ‘prev’) { let newMonth = cur_month - 1; let newYear = cur_year; if (newMonth < 1) { newYear = cur_year - 1; newMonth = 12; } this.signRecord(newYear, newMonth); this.setData({ cur_year: newYear, cur_month: newMonth, imgType: ‘cnext.png’ }) } else { if (cur_month + 1 > month) { this.setData({ imgType: ‘next.png’ }) } else { let newMonth = cur_month + 1; let newYear = cur_year; if (newMonth > 12) { newYear = cur_year + 1; newMonth = 1; } this.signRecord(newYear, newMonth); if (cur_month + 1 == month) { this.setData({ cur_year: newYear, cur_month: newMonth, imgType: ‘next.png’ }) } else { this.setData({ cur_year: newYear, cur_month: newMonth, imgType: ‘cnext.png’ }) } } } }, wxml部分: <view class=‘all’> <view class=“bar”> <!-- 上一个月 --> <view class=“previous” bindtap=“handleCalendar” data-handle=“prev”> <image src=‘https://www.***.com/weChatImg/pre.png’></image> </view> <!-- 显示年月 --> <view class=“date”>{{cur_year || “–”}} / {{filter.fill(cur_month) || “–”}}</view> <!-- 下一个月 --> <view class=“next” bindtap=“handleCalendar” data-handle=“next”> <image src=‘https://www.***.com/weChatImg/{{imgType}}’></image> </view> </view> <view class=‘xxian’> <image src=‘weChatImg/huan.png’></image> <image src=‘weChatImg/huan.png’></image> </view> <!-- 显示星期 --> <view class=“week”> <view wx:for="{{weeks_ch}}" wx:key="{{index}}" data-idx="{{index}}">{{item}}</view> </view> <view class=‘days’> <!-- 列 --> <view class=“columns” wx:for="{{days.length/7}}" wx:for-index=“i” wx:key=“i”> <view wx:for="{{days}}" wx:for-index=“j” wx:key=“j”> <!-- 行 --> <view class=“rows” wx:if="{{j/7 == i}}"> <view class=“rows” wx:for="{{7}}" wx:for-index=“k” wx:key=“k”> <!-- 每个月份的空的单元格 --> <view class=‘cell’ wx:if="{{days[j+k].date == null}}"> <text decode="{{true}}"> </text> </view> <!-- 每个月份的有数字的单元格 --> <view class=‘cell’ wx:else> <!-- 当前日期已签到 --> <view wx:if="{{days[j+k].isSign == true}}" style=‘color:#acacac’ class=‘cell’> {{days[j+k].date}} <image src=‘https://www.***.com/weChatImg/sgin.png’></image> </view> <!-- 当前日期未签到 --> <view wx:else> <text>{{days[j+k].date}}</text> </view> </view> </view> </view> </view> </view> </view> </view> 相信大家通过以上思路,再结合自己的需求应该可以自己做出符合自己心目中的日历插件或者签到
2019-09-11 - 筛选分类,阻止底层页面穿透滚动
采用movable-area,movable-view,catchtouchmove实现筛选时,阻止底层页面穿透滚动,根据之前的项目需求,做了相应的简化: movable-area,movable-view实现弹窗内筛选项的滚动 catchtouchmove阻止页面滚动 https://developers.weixin.qq.com/s/xUPdx7mX75b8
2019-09-04 - 个人微信小程序开发如何接入支付功能?
最近很多朋友问个人小程序有没有办法现实支付功能? 在微信小程序里,主体为个人时,是无法发现支付的。 所以我们要实现支付功能,我们需要借助第三方的支付功能。下面我来分享一下实现方法。 步骤一:在小蜜蜂支付平台开通支付功能。目前支持个人申请 步骤二:跳转至“收银台小助手” 完成收款 以下是实现代码: [代码]var shop_no = "20191024308978649d";//门店号,小蜜蜂服务平台分配 var outTradeNo = shop_no + Date.parse(new Date()); wx.navigateToMiniProgram({ appId: 'wx113f2407af0ed4da', path: 'pages/pay/index', extraData: { "shop_no": shop_no, "secret": "wD7Ct61VJePUad1KyA2Iz5EYKLO2b8",//支付秘钥,小蜜蜂服务平台分配 "totalAmount": 0.1*100, //金额单位:分 "outTradeNo": outTradeNo }, envVersion: 'release', success(res) { // 打开成功 } }) [代码] 小蜜蜂支付平台官方文档:https://www.bestsmartbee.com/index/paydoc
2019-06-04 - 【开箱即用】分享几个好看的波浪动画css效果!
以下代码不一定都是本人原创,很多都是借鉴参考的(模仿是第一生产力嘛),有些已忘记出处了。以下分享给大家,供学习参考!欢迎收藏补充,说不定哪天你就用上了! 一、第一种效果 [图片] [代码]//index.wxml <view class="zr"> <view class='user_box'> <view class='userInfo'> <open-data type="userAvatarUrl"></open-data> </view> <view class='userInfo_name'> <open-data type="userNickName"></open-data> , 欢迎您 </view> </view> <view class="water"> <view class="water-c"> <view class="water-1"> </view> <view class="water-2"> </view> </view> </view> </view> //index.wxss .zr { color: white; background: #4cb4e7; /*#0396FF*/ width: 100%; height: 100px; position: relative; } .water { position: absolute; left: 0; bottom: -10px; height: 30px; width: 100%; z-index: 1; } .water-c { position: relative; } .water-1 { background: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjYwMHB4IiBoZWlnaHQ9IjYwcHgiIHZpZXdCb3g9IjAgMCA2MDAgNjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPgogICAgPCEtLSBHZW5lcmF0b3I6IFNrZXRjaCAzLjQgKDE1NTc1KSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT53YXRlci0xPC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGRlZnM+PC9kZWZzPgogICAgPGcgaWQ9IuaIkSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc2tldGNoOnR5cGU9Ik1TUGFnZSI+CiAgICAgICAgPGcgaWQ9Ii0iIHNrZXRjaDp0eXBlPSJNU0FydGJvYXJkR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMjEuMDAwMDAwLCAtMTMzLjAwMDAwMCkiIGZpbGwtb3BhY2l0eT0iMC4zIiBmaWxsPSIjRkZGRkZGIj4KICAgICAgICAgICAgPGcgaWQ9IndhdGVyLTEiIHNrZXRjaDp0eXBlPSJNU0xheWVyR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEyMS4wMDAwMDAsIDEzMy4wMDAwMDApIj4KICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik0wLDcuNjk4NTczOTUgTDQuNjcwNzE5NjJlLTE1LDYwIEw2MDAsNjAgTDYwMCw3LjM1MjMwNDYxIEM2MDAsNy4zNTIzMDQ2MSA0MzIuNzIxMDUyLDI0LjEwNjUxMzggMjkwLjQ4NDA0LDcuMzU2NzQxODcgQzE0OC4yNDcwMjcsLTkuMzkzMDMwMDggMCw3LjY5ODU3Mzk1IDAsNy42OTg1NzM5NSBaIiBpZD0iUGF0aC0xIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIj48L3BhdGg+CiAgICAgICAgICAgIDwvZz4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPg==") repeat-x; background-size: 600px; -webkit-animation: wave-animation-1 3.5s infinite linear; animation: wave-animation-1 3.5s infinite linear; } .water-2 { top: 5px; background: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjYwMHB4IiBoZWlnaHQ9IjYwcHgiIHZpZXdCb3g9IjAgMCA2MDAgNjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPgogICAgPCEtLSBHZW5lcmF0b3I6IFNrZXRjaCAzLjQgKDE1NTc1KSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT53YXRlci0yPC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGRlZnM+PC9kZWZzPgogICAgPGcgaWQ9IuaIkSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc2tldGNoOnR5cGU9Ik1TUGFnZSI+CiAgICAgICAgPGcgaWQ9Ii0iIHNrZXRjaDp0eXBlPSJNU0FydGJvYXJkR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMjEuMDAwMDAwLCAtMjQ2LjAwMDAwMCkiIGZpbGw9IiNGRkZGRkYiPgogICAgICAgICAgICA8ZyBpZD0id2F0ZXItMiIgc2tldGNoOnR5cGU9Ik1TTGF5ZXJHcm91cCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTIxLjAwMDAwMCwgMjQ2LjAwMDAwMCkiPgogICAgICAgICAgICAgICAgPHBhdGggZD0iTTAsNy42OTg1NzM5NSBMNC42NzA3MTk2MmUtMTUsNjAgTDYwMCw2MCBMNjAwLDcuMzUyMzA0NjEgQzYwMCw3LjM1MjMwNDYxIDQzMi43MjEwNTIsMjQuMTA2NTEzOCAyOTAuNDg0MDQsNy4zNTY3NDE4NyBDMTQ4LjI0NzAyNywtOS4zOTMwMzAwOCAwLDcuNjk4NTczOTUgMCw3LjY5ODU3Mzk1IFoiIGlkPSJQYXRoLTIiIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDMwMC4wMDAwMDAsIDMwLjAwMDAwMCkgc2NhbGUoLTEsIDEpIHRyYW5zbGF0ZSgtMzAwLjAwMDAwMCwgLTMwLjAwMDAwMCkgIj48L3BhdGg+CiAgICAgICAgICAgIDwvZz4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPg==") repeat-x; background-size: 600px; -webkit-animation: wave-animation-2 6s infinite linear; animation: wave-animation-2 6s infinite linear; } .water-1, .water-2 { position: absolute; width: 100%; height: 60px; } .back-white { background: #fff; } @keyframes wave-animation-1 { 0% { background-position: 0 top; } 100% { background-position: 600px top; } } @keyframes wave-animation-2 { 0% { background-position: 0 top; } 100% { background-position: 600px top; } } .user_box { display: flex; z-index: 10000 !important; opacity: 0; /* 透明度*/ animation: love 1.5s ease-in-out; animation-fill-mode: forwards; } .userInfo_name { flex: 1; vertical-align: middle; width: 100%; margin-left: 5%; margin-top: 5%; font-size: 42rpx; } .userInfo { flex: 1; width: 100%; border-radius: 50%; overflow: hidden; max-height: 50px; max-width: 50px; margin-left: 5%; margin-top: 5%; border: 2px solid #fff; } [代码] 二、第二种效果 [图片] [代码]//index.wxml <view class="waveWrapper waveAnimation"> <view class="waveWrapperInner bgTop"> <view class="wave waveTop" style="background-image: url('https://s2.ax1x.com/2019/09/26/um8g7n.png')"></view> </view> <view class="waveWrapperInner bgMiddle"> <view class="wave waveMiddle" style="background-image: url('https://s2.ax1x.com/2019/09/26/umGZ38.png')"></view> </view> <view class="waveWrapperInner bgBottom"> <view class="wave waveBottom" style="background-image: url('https://s2.ax1x.com/2019/09/26/umGuuQ.png')"></view> </view> </view> //index.wxss .waveWrapper { overflow: hidden; position: absolute; left: 0; right: 0; height: 300px; top: 0; margin: auto; } .waveWrapperInner { position: absolute; width: 100%; overflow: hidden; height: 100%; bottom: -1px; background-image: linear-gradient(to top, #86377b 20%, #27273c 80%); } .bgTop { z-index: 15; opacity: 0.5; } .bgMiddle { z-index: 10; opacity: 0.75; } .bgBottom { z-index: 5; } .wave { position: absolute; left: 0; width: 500%; height: 100%; background-repeat: repeat no-repeat; background-position: 0 bottom; transform-origin: center bottom; } .waveTop { background-size: 50% 100px; } .waveAnimation .waveTop { animation: move-wave 3s; -webkit-animation: move-wave 3s; -webkit-animation-delay: 1s; animation-delay: 1s; } .waveMiddle { background-size: 50% 120px; } .waveAnimation .waveMiddle { animation: move_wave 10s linear infinite; } .waveBottom { background-size: 50% 100px; } .waveAnimation .waveBottom { animation: move_wave 15s linear infinite; } @keyframes move_wave { 0% { transform: translateX(0) translateZ(0) scaleY(1) } 50% { transform: translateX(-25%) translateZ(0) scaleY(0.55) } 100% { transform: translateX(-50%) translateZ(0) scaleY(1) } } [代码] 三、第三种效果 [图片] [代码]//index.wxml <view class="container"> <image class="title" src="https://ftp.bmp.ovh/imgs/2019/09/74bada9c4143786a.png"></image> <view class="content"> <view class="hd" style="transform:rotateZ({{angle}}deg);"> <image class="logo" src="https://ftp.bmp.ovh/imgs/2019/09/d31b8fcf19ee48dc.png"></image> <image class="wave" src="wave.png" mode="aspectFill"></image> <image class="wave wave-bg" src="wave.png" mode="aspectFill"></image> </view> <view class="bd" style="height: 100rpx;"> </view> </view> </view> //index.wxss image{ max-width:none; } .container { background: #7acfa6; align-items: stretch; padding: 0; height: 100%; overflow: hidden; } .content{ flex: 1; display: flex; position: relative; z-index: 10; flex-direction: column; align-items: stretch; justify-content: center; width: 100%; height: 100%; padding-bottom: 450rpx; background: -webkit-gradient(linear, left top, left bottom, from(rgba(244,244,244,0)), color-stop(0.1, #f4f4f4), to(#f4f4f4)); opacity: 0; transform: translate3d(0,100%,0); animation: rise 3s cubic-bezier(0.19, 1, 0.22, 1) .25s forwards; } @keyframes rise{ 0% {opacity: 0;transform: translate3d(0,100%,0);} 50% {opacity: 1;} 100% {opacity: 1;transform: translate3d(0,450rpx,0);} } .title{ position: absolute; top: 30rpx; left: 50%; width: 600rpx; height: 200rpx; margin-left: -300rpx; opacity: 0; animation: show 2.5s cubic-bezier(0.19, 1, 0.22, 1) .5s forwards; } @keyframes show{ 0% {opacity: 0;} 100% {opacity: .95;} } .hd { position: absolute; top: 0; left: 50%; width: 1000rpx; margin-left: -500rpx; height: 200rpx; transition: all .35s ease; } .logo { position: absolute; z-index: 2; left: 50%; bottom: 200rpx; width: 160rpx; height: 160rpx; margin-left: -80rpx; border-radius: 160rpx; animation: sway 10s ease-in-out infinite; opacity: .95; } @keyframes sway{ 0% {transform: translate3d(0,20rpx,0) rotate(-15deg); } 17% {transform: translate3d(0,0rpx,0) rotate(25deg); } 34% {transform: translate3d(0,-20rpx,0) rotate(-20deg); } 50% {transform: translate3d(0,-10rpx,0) rotate(15deg); } 67% {transform: translate3d(0,10rpx,0) rotate(-25deg); } 84% {transform: translate3d(0,15rpx,0) rotate(15deg); } 100% {transform: translate3d(0,20rpx,0) rotate(-15deg); } } .wave { position: absolute; z-index: 3; right: 0; bottom: 0; opacity: 0.725; height: 260rpx; width: 2250rpx; animation: wave 10s linear infinite; } .wave-bg { z-index: 1; animation: wave-bg 10.25s linear infinite; } @keyframes wave{ from {transform: translate3d(125rpx,0,0);} to {transform: translate3d(1125rpx,0,0);} } @keyframes wave-bg{ from {transform: translate3d(375rpx,0,0);} to {transform: translate3d(1375rpx,0,0);} } .bd { position: relative; flex: 1; display: flex; flex-direction: column; align-items: stretch; animation: bd-rise 2s cubic-bezier(0.23,1,0.32,1) .75s forwards; opacity: 0; } @keyframes bd-rise{ from {opacity: 0; transform: translate3d(0,60rpx,0); } to {opacity: 1; transform: translate3d(0,0,0); } } [代码] wave.png(可下载到本地) [图片] 在这个基础上,再加上js的代码,即可实现根据手机倾向,水波晃动的效果 wx.onAccelerometerChange(function callback) 监听加速度数据事件。 [图片] [代码]//index.js Page({ onReady: function () { var _this = this; wx.onAccelerometerChange(function (res) { var angle = -(res.x * 30).toFixed(1); if (angle > 14) { angle = 14; } else if (angle < -14) { angle = -14; } if (_this.data.angle !== angle) { _this.setData({ angle: angle }); } }); }, }); [代码] 四、第四种效果 [图片] [代码]//index.wxml <view class='page__bd'> <view class="bg-img padding-tb-xl" style="background-image:url('http://wx4.sinaimg.cn/mw690/006UdlVNgy1g2v2t1ih8jj31hc0p0qej.jpg');background-size:cover;"> <view class="cu-bar"> <view class="content text-bold text-white"> 悦拍屋 </view> </view> </view> <view class="shadow-blur"> <image src="https://raw.githubusercontent.com/weilanwl/ColorUI/master/demo/images/wave.gif" mode="scaleToFill" class="gif-black response" style="height:100rpx;margin-top:-100rpx;"></image> </view> </view> //index.wxss @import "colorui.wxss"; .gif-black { display: block; border: none; mix-blend-mode: screen; } [代码] 本效果需要引入ColorUI组件库
2019-09-26 - 浅谈微信抽奖小程序!
说抽奖小程序之前,先整体了解下抽奖这一核心。从以下六方面分析 抽奖心理:抽奖在想什么? 抽奖假象:越多越多? 抽奖玩法:抽奖本质,能不能解决? 抽奖变现:或许天上真的会掉馅饼,但还是钱好 抽奖机会:还有没有必要做一个抽奖小程序? 抽奖意义:我们有一个锦鲤梦,游戏人生 抽奖是一个利他利己的行为,一来增加自己的中奖率,二来传播也为他人制造中奖机会。通过人与人之间的信任关系,间接为发起人活动背书,随之品牌营销,低成本的流量曝光。 哪里有福利,哪里有免费,哪里就有流量! 谁都可以参与,谁都可以发起。 一、抽奖心理你是否懂参与者,他们分别的诉求是什么? ▍抽奖者:我能中?有多少人参与了,什么时候开奖,及时通知开奖结果,活动真实性?简单易懂,奖品吸引力。 ▍发起者:怎么玩?才能更好的推广自己/公众号增粉/品牌服务/产品特性等,用户精不精准? 先明确身份(普通玩家,品牌方,运营,线下商家等) 再次挖掘需求(打品牌广告,提升销售?),工具是否支持 设置门槛,扩大影响力,ROI 上手教程(需求场景解决方案,与产品匹配) [图片] 二、抽奖假象▍抽奖越多越好? 听说现在有几百款抽奖小程序了,生存状况如何?先说出3个,有何差异? ▍玩法越多越好? 看到复杂的玩法,一方面难受,一方面欣慰 难受,大概率不会去玩 欣慰,营销玩法还挺丰富的,需求与利益结合 ▍功能越多越好? 如何把用户“困”在小程序,是对“美好生活”的向往? 后来才明白一句“你说你,想要逃,偏偏注定要……” 化简为繁,一蹴而就,谁在可歌可泣! 三、抽奖玩法简单来说,大的方向也只需要解决这三步。 提供奖品 开奖条件 发放奖品 [图片] ▍提供奖品: 实物,红包,知识付费等。 ▍开奖条件: 默认到达时间自动开奖,其次按人数,即开即中。 弱化开奖条件,因为一般都是按时间开奖。为用户提供最佳的解决方法,而非,先熟悉整个游戏规则,展示全部条件,全部玩法等。 ▍发奖方式: 发起者联系:让中奖人填写地址,电话,微信号等,适合快递实物,或添加中奖人再说明。 中奖人联系:留下发起人的手机,微信,二维码等,适合发放优惠券,红包等虚拟物品。 延伸: 单独自定义给中奖者一句话 一般平台自带文案,如果发起者还能单独设置一句,相当于多了一次互动营销机会,凸显人格化。 延长中奖人填写中奖信息时间 比如遇到节假日,参与人中奖人太多等。 ▍分享按钮: 是否允许参与者分享本次抽奖活动。之前「抽奖助手」把这个功能放在了“更多功能”里面,默认不开启,有的小程序选择了默认开启,我更倾向于前者。 在心理上,用户并不一定需要他人随意分享,他的赞助奖品。 在操作上,手动开启代表用户默认同意,手动关闭代表防止止损。 所以,应该把选择选择权交给用户,而不是替用户选择。让用户的利益与平台背后的利益一起成长,也多了裂变小程序的条件,多赢。在用户的关系链分享,垂直密集,越是喜欢抽奖的人,越会分享给抽奖的人,相互吸引。 有前置这个功能的必要。 ▍其他玩法: 线下活动/线上直播/年会抽奖玩法: 突出:公司,logo,主题,倒序抽奖 担心:繁琐,手忙脚乱,冷场,多人操作 在线互动——投屏,弹幕,背景音乐,活跃氛围 后台规则——简单,倒序(手动逐个开奖),重抽 延伸:摇一摇功能(摇越快,中奖率越高) 上滑抽奖:提示上拉,看下个奖品(固定栏展示:不断陷入) 参与条件:如性别,地区,(实名),达到一定抽奖次数,输入指定口令参与等 头像玩法: 列表:头像,昵称,码,中奖概率,排名(突出榜单,中奖概率) 宫格:头像,更多(一般都是这种,突出活动受欢迎) ▍小插曲:关于抽奖背景图片 如果平台能针对节假日提供2-3张背景图,不是更有助于发起活动吗?在每次小程序迭代上新,去旧图片。 这个小设计,花费不大,又让用户觉得产品很用心。 [图片] 四、抽奖变现自有广告:上首页(类似每日福利,自助福利) 增值服务:付费版(去除广告,突破参与人数限制,公号仅粉丝参与等) 电商合作:合作电商(购买商城东西当抽奖礼物),第三方电商优惠券导购等 互选广告:保证小程序内广告的调性,视觉统一性,比如开学期某当的的图书满减活动,做用户尽可能有兴趣的广告,增加了广告主的曝光/跳转/转化,成为平台的案例级,再次吸引广告主。位置:banner,开奖后短文案广告,商城广告等 官方广告:如先看视频后抽奖等 五、抽奖机会说抽奖就想到……那其他抽奖小程序还有机会吗? 头部小程序在教育你,我(品牌)等于抽奖(品类),稳固自己的地位,防守 做不了头部,后来者怎么办? 占据特性 垂直聚焦 开创品类 微商,公众号主,电商,专属游戏周边,线下门店等成了多款抽奖小程序的突破口。 不同人群的喜好,行为,用户社交关系链的特征不一样,复制之前的成功模式到自己的行业,有一定的借鉴意义。 谁能比较优雅、灵活地解决掉对应场景存在的问题,谁能满足差异化需求,打消参与顾虑,自然流量/广告主纷涌而至。 以下举3个例子 01. 点赞抽奖 公众号粉丝互动:点赞就能参与,抽红包 行业KOL & 节点效应:或许你喜欢的公号发起了一个活动,因为他的背书,而积极参与。如何帮助公众号主完成与粉丝的互动,做成案例,由此扩散,吸引一个又一个的公众号主,重合度引爆。这种推广方式,好过工具方主动介绍。 [图片] 02.锦鲤圈 京东平台导流:店铺抽奖 与 用户跳转到“京东购物”小程序店铺提升中奖率 通过锦鲤奖+人气奖,双福利邀好友,关注店铺,浏览该店铺商品,提升中奖率,多入口跳转到京东店铺。(方法很京东,也有小程序通过试玩应用,签到等任务来提升抽奖币,限定消耗抽奖币参与,换购等电商) [图片] 03.华硕+ 品牌宣传,拉新会员,导流门店 成为会员无门槛抽奖。导流到线下门店(线下门店优惠券),门店自助发布活动管理等 [图片] 不同的产品决定了其走向,有的是平台主导权,有的擅长调性,研究人性,有的擅长运营增长,有的正利用不公平,谁是分享王福利就向你倾斜。 抽奖再怎么玩法多样,用户有他自己的判断,同化的用户留下来! 六、抽奖意义如果说抽奖有什么社会意义,解决供需双方之间的需求。在诞生的那一刻,给我们带来短期或长期的价值,降低推广成本,短时间获取用户,尽管用户用完就跑…… 不管我们做什么,要提供给用户价值和好玩的东西,做对小程序本身有价值的事情。 感谢围观小程序测评系列《抽奖小程序》,作为小程序测评师,希望给大家带来一些启发,一起推送小程序更好地发展。
2019-09-04 - 模块化——加速小程序开发
阅读基础:有小程序项目经验,有查阅官方文档习惯的小伙伴 随着公司小程序项目日益繁多,仅仅靠着官方提供的框架、组件、API,已经远远不能满足项目高效迭代的要求了,于是我们组内萌生了对小程序进行模块化的想法。 实际项目中我们对小程序模块化已经涉及各个模块,我总结一下,从三个方向跟大家分享我们不一样的模块化思路:[代码]Page+[代码],[代码]basePage[代码],[代码]适配层[代码]。 Page+:赋予页面更多的功能 [代码]Page()[代码]作为页面的入口,我们可以通过对其入参对象的封装实现:生命周期的改造、全局状态管理和新增页面功能。 官方删除了小程序分享回调 complete,一起来尝试将其恢复吧。一般我们的逻辑是这样的: [代码]// pages/index/index.js Page({ // 数据初始化 data: { shareFlag: false, //页面是否处于分享中 shareComplete: false //分享回调事件 }, // onShow 生命周期 onShow: function () { const { shareFlag, shareComplete } = this.data if( shareFlag ){ this.data.shareFlag = false //变量不涉及页面渲染,不使用 setData shareComplete && shareComplete() } }, // 分享事件 onShareAppMessage: function () { let shareInfo = { title: '分享测试标题', path: '', complete: function () { console.log('页面分享成功啦~') } } this.data.shareFlag = true this.data.shareComplete = typeof (shareInfo.complete) == 'function' ? shareInfo.complete : false return shareInfo } }) [代码] 在单页面内实现分享回调这样操作是可行的,如果多页面、多项目都要实现该功能,重复拷贝代码,则显格外得繁琐。 我们来将这个功能抽离封装一下吧。 [代码]// pages/index/index.js import PagePlus from './pagePlus.js' PagePlus({ // 分享事件 onShareAppMessage: function () { return { title: '分享测试标题', path: '', complete: function () { console.log('页面分享成功啦~') } } } }) [代码] [代码]// pages/index/pagePlus.js const PagePlus = (pageObj) => { const _onShow = pageObj.onShow, _onShareAppMessage = pageObj.onShareAppMessage, _data = { shareStatus: false, //页面是否处于分享中 shareComplete: false //分享回调事件 } Object.assign(_data, pageObj.data) delete pageObj.data pageObj.onShow = function () { typeof _onShow == 'function' && _onShow.apply(this) const { shareStatus, shareComplete } = this.data if (shareStatus) { this.data.shareStatus = false //变量不涉及页面渲染,不使用 setData shareComplete && shareComplete() } } pageObj.onShareAppMessage = function () { const shareInfo = typeof _onShareAppMessage == 'function' && _onShareAppMessage.apply(this) this.data.shareStatus = true shareInfo && (this.data.shareComplete = shareInfo.complete) return shareInfo } Page({ data: _data, ...pageObj }) } export default PagePlus [代码] 我们来增加一个新的生命周期回调——[代码]onReshow[代码](页面非首次显示回调,常用于详情页操作影响上一页列表数据的场景)。 [代码]// pages/index/index.js import PagePlus from './pagePlus.js' PagePlus({ // 监听页面非首次显示 onReshow: function(){ console.log('onReshow lifeCallBack') }, onShareAppMessage: function () { return { title: '分享测试标题', path: '', complete: function () { console.log('页面分享成功啦~') } } } }) [代码] [代码]// pages/index/pagePlus.js class BasePage{ data = { pagePlus: { shareStatus: false, //页面是否处于分享中 shareComplete: false, //分享回调事件 firstEnter: true //第一次进入页面 } } methods = { onShow: this.onShow, onShareAppMessage: this.onShareAppMessage, onReshow: this.onReshow } onShow(){ const { shareStatus, shareComplete, firstEnter } = this.data.pagePlus if (firstEnter) { this.data.pagePlus.firstEnter = false } else { this.onReshow() } if (shareStatus) { this.data.pagePlus.shareStatus = false shareComplete && shareComplete() } } onShareAppMessage(shareInfo){ this.data.pagePlus.shareStatus = true shareInfo && (this.data.pagePlus.shareComplete = shareInfo.complete) } } const PagePlus = (pageObj) => { const basePage = new BasePage() for (var i in basePage.methods) { basePage.methods[i] = (() => { const key = i const _temFn = basePage.methods[key] return function () { if (key == 'onShareAppMessage') { const shareInfo = typeof pageObj[key] == 'function' && pageObj[key].apply(this, arguments) _temFn.apply(this, [shareInfo]) return shareInfo } typeof pageObj[key] == 'function' && pageObj[key].apply(this, arguments) typeof _temFn == 'function' && _temFn.apply(this, arguments) } })() } Object.assign(basePage.data, pageObj.data) delete pageObj.data Page({ data: basePage.data, ...pageObj, ...basePage.methods }) } export default PagePlus [代码] 自此,我们修改了原生的生命周期回调和增加了新的生命周期回调。当然我们还能为 Page+ 赋予更多的功能,例如: [代码]页面刷新[代码]:下拉自动刷新当前页。 [代码]定时器自动清除[代码]:离开页面时,自动清除页面执行的定时器。 [代码]全局状态管理[代码]:页面间数据共享,相关数据关联的组件即时渲染更新。 相关的代码实现,大家可以自己思考一下怎么实现;我的实现细节,如果大家感兴趣的话就在下方给我留言吧,你们的回复是我更新的动力哦。 basePage:公共 Component 管理器 小程序页面彼此独立,使用 Component 都需要各自引用,为了实现页面公共 Component 的统一管理,这个时候就可以引入 basePage 的概念:以 basePage 作为父组件,其他公共 Component 作为子组件,页面通过 basePage 对公共 Component 进行管理。 实现原理 1、定义一个 Component ,作为 basePage 。 2、每个页面统一引用 basePage ,且规定页面的元素都需要写到 <basePage/> 标签内部 。 3、通过 basePage 引用页面公共的 Component ,并进行业务逻辑编辑。 实现细节 实际使用过程中,我发现有两个问题: 1、Page 和 basePage 通信是非常频繁的,需要通过 WXML 数据绑定和 triggerEvent 触发事件,略显麻烦。 2、setTimeout、webSocket 等后台进程,可能触发[代码]非当前显示页面[代码]的渲染更新,而绝大部分情况,我们只需要[代码]当前显示页面[代码]的渲染更新。 针对这两种场景的优化,我们可以把当前显示页面的 basePage 实例对象赋值到 global 的某个具体变量;每当 Page 触发 show 生命周期回调的时候,我们就对这个变量赋值的实例对象进行更新,这样我们就可以通过 global 的变量直接操作当前显示页面的 basePage 了。 部分代码示例 [代码]{ "文件路径": "pages/index/index.json", "usingComponents": { "basePage": "../../components/basePage/index" } } [代码] [代码]<!--pages/index/index.wxml--> <basePage> <!-- 页面元素 --> </basePage> [代码] [代码]// components/basePage/index.js Component({ /** * Component 所在页面的生命周期函数 */ pageLifetimes: { show: function () { global.basePage = this }, hide: function () { global.basePage = null } } }) [代码] [代码]{ "文件路径": "components/basePage/index.json", "说明": "在此处统一引入页面公共的 Component", "component": true, "usingComponents": {} } [代码] [代码]<!--components/basePage/index.wxml--> <slot /> [代码] 适配层:让代码适应更多的场景 如果你的项目对代码后续维护、迭代和可移植性有较高需求,或者需要多项目并行,这个时候通过适配层去调用各个功能模块就显得尤为重要。适配层方面我做的还是比较粗糙的,如果有建议欢迎指出。 适配层的时机 项目不是 bugfix 级别的迭代,都有适配层设计的必要。 如果是[代码]新项目[代码],心底不认为自己是“咸鱼”而是代码的“亲爹”,[代码]适配层完全可以作为标配[代码]去实现;这就是展现自己对代码全局观的时候了,把自己对代码的理解都用适配层去诠释吧。 如果是[代码]旧项目迭代[代码],在项目排期允许的情况下,尽可能理解原代码的基本实现细节;对比新的项目是要束手束脚一些,适配层的设计要在[代码]尽可能少改变原有代码[代码]的情况下进行;如果排期比紧急,适配层的完整实现[代码]可以在几个版本迭代中逐步实现[代码]。 模块设计必须高内聚低耦合 如果功能模块的设计过于松散、耦合复杂,这就意味着适配层将需要做各种兼容,这和适配层设计的初衷背道而驰,不做也罢。 配置文件 如果你的代码有移植性要求,为这些不同环境准备对应的配置文件吧,配置文件可以通过自制脚手架实现,也可以粗暴地手动替换,在保证尽可能不出错的情况下实现即可。 功能模块的入口 所有整合的功能模块都需要通过适配层进行调用,适配层就是你的“王之财宝”。 规范 && 文档 适配层是从代码的全局考虑,如果是项目是分工完成,项目的开发人员都需要遵守适配层规范进行代码开发;文档我一直都认为都是非常必要的,但还是经常会懈怠,没有进行完整的文档编写,但我基本会在所有项目成员都能理解适配层的情况下,进行简单的口头说明。 因为开心说一些废话 一次需求迭代中,几乎涉及手头上的所有小程序项目;刚好就在需求前的半个月,我们小组完成了对所有项目模块化改造;虽然需求来得很急,我们还是很完美的实现了。毕竟[代码]模块化之前,每个项目的改造都是独立的工作量;模块化之后,就只有适配层迭代的工作量了[代码]。不过真是辛苦了测试小伙伴,因为对所有项目进行模块化改造,意味着测试小伙伴对所有项目进行回归测试,感谢测试小伙伴,比心! 这篇文章,对 Page+ 的具体实现展示比较详细,感觉对 basePage 和适配层讲的都比较偏概念。毕竟这部分内容都和业务逻辑联系比较紧密,很难抽象深入讲解。刚好还有假期还有一段时间,如果自己还有时间就再写一篇关于最近项目的模块化剖析吧,哈哈。
2019-10-09 - 微信小程序生成图片和图片保存
生成图片这个功能需要使用 Canvas ,先将要保存的图片使用 Canvas 画出来,然后调用相关方法保存到手机上。文中使用或是未使用的有关小程序中 Canvas 的 api 可以在 小程序Canvas相关Api 这里查看。 为了使用方便,我将这个需求的实现做成了一个组件,便于项目的其他地方复用,也遇到了一些因为使用自定义组件带来的问题 <!–more–> 要实现的效果图 [图片] 实现思路 相关的代码比较长,而且大部分为调用 Canvas 的 api 代码,整体贴出无必要。本文中只描述思路,具体代码看 小程序生成图片的微信代码片段 。 代码中的几个方法 [代码]drawRoundedRect[代码] :用来绘制圆角矩形,此处不详述它的画图原理 [代码]point[代码] :为方法 1 服务的,一看就懂 [代码]downFile[代码] :对微信的下载方法进行了一层简单封装,传入 url ,返回一个 Promise [代码]save[代码] : 保存图片的相关逻辑 [代码]doAuth[代码] :当调用 [代码]wx.saveImageToPhotosAlbum[代码] 方法保存图片时,如果没有保存图片的权限会保存失败,此时需要让用户重新授权 [代码]computedPercent[代码] :一个快捷的计算比例的方法,传入从设计图上量出来的像素数即可, [代码]oldWidth[代码] 是设计图上的 Canvas 区域宽度 [代码]initData[代码] :数据初始化,获取设备相关信息,将网络图下载到本地 [代码]writeCanvas[代码] : 主要画图逻辑,调用此方法时保证所需数据已处理完毕,开始画图 数据初始化 需求中需要显示用户头像和小程序码,小程序码后面是要挂参数的,可以简单理解为要挂个用户参数在后面,类似这样 [代码]?uid=2233[代码] ,这就是组件中要传入的 scene 的值,然后根据这个参数,在开始画图之前,先调用接口从服务端那里获取图片的链接,再利用微信的 [代码]wx.downloadFile[代码] 方法将图片下载到本地,在 Canvas 中使用本地路径,用户头像和小程序码都下载好了之后就可以开始画图了,否则的话,提示网络错误。 为了演示方便,本文中的网络图都换成了本地图片 画图 由于是在组件中,所以获取 Canvas 上下文的时候要传入 [代码]this[代码] ,根据设计图,从里向外依次往 Canvas 上叠加就是了,从设计图上量出的像素调用方法来获取比例,画完之后调用 Canvas 的 [代码]draw[代码] 方法技术绘画,并且将 loading 状态取消 保存图片 保存图片的时候,要注意保存的图片的宽高都乘一下设备的像素比,防止出来的图片太小了,保存图片需要相关权限,若用户为授权,要弹窗让他授权之后再进行保存操作 遇到的问题 以下部分问题有时效性,请大家理性看待。 canvas 结束绘画要调用 [代码]ctx.draw()[代码] 方法,不调用的话是什么都不会显示的 [代码]wx.createCanvasContext(string canvasId, Object this)[代码] ,在自定义组件下,当前组件实例的 [代码]this[代码] ,表示在这个自定义组件下查找拥有 [代码]canvas-id[代码] 的 [代码]<canvas>[代码] ,如果省略则不在任何自定义组件内查找。简而言之就是在页面级调用这个方法的时候,第二个参数可以不传,会默认传入 this ;但是在自定义组件中调用这个方法的话,要传入 this 部分手机上保存的图片分辨率太小,导致图片上的字看不清。最后使用了一个百分比的计算,所有的的坐标或者大小都是根据屏幕宽高和设计图宽高比例计算出来了,这样可以保证即使在不同手机上或者在不同的容器中,都是有恰当比例的;并且保存图片的时候,要注意保存的图片的宽高都乘一下设备的像素比 Canvas 中不能放网络图,所以网络图需要先下载到本地之后在使用 如果用到了下载网络图片的话,别忘记设置微信的 [代码]downloadFile[代码] 合法域名 结语 对于大部分同学来说,自己项目的需求和我这里实现的可能是有些出入的,所以本文只是起到一个例子的作用,可以帮助你快速上手这个模块的开发
2023-08-14 - 一些精品小程序项目分享(原代码)
小程序有着微信近十亿的用户作为基础,加上公众号、小程序码、微信群转发等多种传播途径,非常容易就获取到直接的用户,跟地推等传统获客方式相比,它的成本是非常低的,低成本高转换的获客,对品牌营销帮助非常大。 给大家分享一下精品小程序的demo,代码仅供学习。 这些我是平时用来学习的demo,看看别人写的代码和自己有什么不一样,多方思考后受益良多 地址:http://t.cn/ExJMe95 [图片][图片] 地址:http://t.cn/ExJMe95 地址:http://t.cn/ExJMe95 地址:http://t.cn/ExJMe95
2019-10-10 - 关于微信小程序canvas的各种坑?
[自力更生自给自足的解决了这些问题,遇到同样问题的朋友可以参考参考] 1.用drawImage画线上图片在真机上显示不出来,模拟器上却可以显示(需要说明的是:drawImage画显示图片是需要获取到图片后才能开始画,并且获取的线上图片地址需要是https。)但是这些我都照做了可是真机上还是无法显示,为什么?我也试过下载到本地后画也没有用。 解决方法: 首先 ,我发现用canvas绘制线上图片时,必须先下载到本地,而且线上图片的地址必须是在配置的安全域名下,我遇到绘制不出的原因在于:没有等待图片完全下载好就绘制了,所以这里要考虑绘图顺序,可以用image的bindload事件或者downloadTask.onProgressUpdate来监听图片加载过程。 2.小程序的canvas没有裁剪的api,请问如何用canvas将图片画成圆形? 解决方法: 这个问题我是通过制作一张和头像图片一样大的中间有个圆形镂空(中间透明)的正方形图片绘制在头像上,在视觉上给头像做出圆形的效果。 3.`ctx.drawImage`绘制的画布,使用`ctx.clearRect`清除不了。 解决方法: 这个问题我没有解决。 4.模拟器上有个bug就是在画了图片后再画文字,文字会被覆盖,但是去真机上查看是没有问题的,文字可以正常显示。 解决方法: 这个问题是模拟器的bug。 5.为了让canvas不在页面显示,将canvas用view标签包起来后,给view设置了overflow=hidden和opacity=0的属性,是可以成功将canvas隐藏,但是在真机上测试时,一旦在这个隐藏的canvas上绘制图片,canvas又显示在屏幕上了。模拟器上是不会显示的。 解决方法: 由于canvas是原生的组件所以在模拟器上可以被隐藏,但是在真机上一直置于最上层,所以在真机上canvas一旦被绘制就一定会显示。我想了一个奇怪的方法,我在canvas的外层套了一个宽高正好一屏的view标签,然后将view的背景设置为黑色,再让canvas定位到屏幕的中间。这样看起来像是进入了图片预览。然后短暂延迟后通过 wx.canvasToTempFilePath生成图片后再调wx.previewImage。同时再用wx:if把canvas给销毁,用hidden把view给隐藏,页面每次进入的时候再还原初始值。以上是我根据我自己的需求想的折中办法,有相同情况的同学可以参考。(其实我想实现的最最效果是腾讯投票生成朋友圈二维码的那种,他们的canvas就没有显示在页面上,所以我猜想他们可能是在服务器端进行渲染后再传图给前端的) 6.canvas文字不能换行的问题 解决方法: 这个问题我是通过字符串截取的思路做的,固定每行的字数,为了美观用了ctx.setTextAlign('center')让每行字都水平居中对其。 ps:希望小程序官方能统一回答下这些问题,这几个问题中有些问题一直都有很多人问,可是没有一个好的回答,希望官方能有个好的解答谢谢啦!
2017-08-30 - 小程序码在部分手机无法识别?
[图片] 比如上面手机的图片 在ios10.2 红米7 安卓9 等机型下面 无法识别小程序码
2019-10-11