- 小程序怎么接入国外地图
用小程序原生的map国内地点能得到很好显示,但是国外的地点显示不出来。请问大神都是怎么解决的 。 我看其他的小程序好像只有携程旅游攻略有显示国外的地图,其他的好像没有找到。但是携程旅游攻略的地图一会显示bing,一会显示腾讯的。搞了很久没有搞出来,希望有人指点,谢谢
2017-06-13 - 创建试用小程序的接口的openid如何获取?
https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/Register_Mini_Programs/beta_mp/fastregister.html 调用创建的时候怎么获取openid?要先关注公众号?怎么知道当前用户的openid
2021-03-08 - 小程序分包加载
背景音: - 随着开发功能越来越多, 小程序初次加载速度实在是吃力. 万般煎熬之迹, 只听晴天霹雳一声巨响, 腾讯放了一个大招, 针对小程序做了文件大小限制做了升级. 分包也就此诞生 环境要求: - 微信 6.6 客户端,1.7.3 及以上基础库开始支持,请更新至最新客户端版本,开发者工具请使用 1.01.1712150 及以上版本 在下左木子, 接下来给大家讲解如何运用分包. 废话不多说, 咱看图! 保你看完明明白白: [图片] [图片] [图片]
2019-07-25 - 微信小程序用户授权弹窗,获取用户信息。用户拒绝授权时,引导用户去重新授权
我们在开发小程序时,如果想获取用户信息,就需要获取用的授权,如果用户误点了拒绝授权,我们怎么样去正确的引导用户重新授权呢。今天就来给大家讲讲如果正确的引导用户授权。 老规矩,先看效果图 [图片] 从上图可以看出,我们在用户点击拒绝授权时,我们会弹出一个提示框,提示用户去设置页重新授权,当用户去授权页重新授权以后,我们再回到首页,点击获取用户信息时,就可以成功的获取到用户信息了。 如下图蓝色框里,就是我们成功的获取的用户信息。 [图片] 一,我们获取用户信息的时候需要用户授权 我们点击获取用户信息时,通常会弹出如下提示框,如果用户点击了取消,就再也没有办法通过点击授权按钮获取用户信息了。 [图片] 所以接下来我们要做的就是在用户拒绝了授权时,引导用户去设置页重新授权。 把获取用户授权的代码先贴给大家 [代码]<button open-type="getUserInfo" bindgetuserinfo="getUserInfo"> 授权获取头像昵称 </button> [代码] 二,检测用户是否授权 我们在用户点击了上面定义的button按钮后,做权限检测。代码如下。 [代码] getUserInfo: function(e) { let that = this; // console.log(e) // 获取用户信息 wx.getSetting({ success(res) { // console.log("res", res) if (res.authSetting['scope.userInfo']) { console.log("已授权=====") // 已经授权,可以直接调用 getUserInfo 获取头像昵称 wx.getUserInfo({ success(res) { console.log("获取用户信息成功", res) that.setData({ name: res.userInfo.nickName }) }, fail(res) { console.log("获取用户信息失败", res) } }) } else { console.log("未授权=====") that.showSettingToast("请授权") } } }) }, [代码] 给大家简单解析下。 wx.getSetting :用来获取用户授权列表 if (res.authSetting[‘scope.userInfo’]) 代码用户授权成功,如果用户没有授权,就代表授权失败。 在授权失败时,我们调用that.showSettingToast()方法 三,showSettingToast方法如下 [代码] // 打开权限设置页提示框 showSettingToast: function(e) { wx.showModal({ title: '提示!', confirmText: '去设置', showCancel: false, content: e, success: function(res) { if (res.confirm) { wx.navigateTo({ url: '../setting/setting', }) } } }) } [代码] 这方法做的就是引导用户去设置页。 四,我们的设置页 [图片] 我们的设置页其实很简单,只有上图这么一段代码。 [图片] 五,去系统设置页。 我们上面第四步的button按钮,点击以后,就会去系统设置页。 [图片] 可以看到系统设置页,有一个开关,当用户点击开关时,就可以重新授权啦。 [图片] 重新授权成功以后,我们回到首页,就可以成功的获取到用户信息了。 [图片] 到这里我们就成功的实现了引导用户授权的功能了。 把index.wxml和index.js代码贴出来给大家 index.wxml [代码]<!--index.wxml--> <button open-type="getUserInfo" bindgetuserinfo="getUserInfo"> 授权获取头像昵称 </button> <text>{{name}}</text> [代码] index.js [代码]//index.js Page({ getUserInfo: function(e) { let that = this; // console.log(e) // 获取用户信息 wx.getSetting({ success(res) { // console.log("res", res) if (res.authSetting['scope.userInfo']) { console.log("已授权=====") // 已经授权,可以直接调用 getUserInfo 获取头像昵称 wx.getUserInfo({ success(res) { console.log("获取用户信息成功", res) that.setData({ name: res.userInfo.nickName }) }, fail(res) { console.log("获取用户信息失败", res) } }) } else { console.log("未授权=====") that.showSettingToast("请授权") } } }) }, // 打开权限设置页提示框 showSettingToast: function(e) { wx.showModal({ title: '提示!', confirmText: '去设置', showCancel: false, content: e, success: function(res) { if (res.confirm) { wx.navigateTo({ url: '../setting/setting', }) } } }) }, }) [代码] 有任何关于编程的问题都可以留言或者私信我,我看到后会及时解答 编程小石头,码农一枚,非著名全栈开发人员。分享自己的一些经验,学习心得,希望后来人少走弯路,少填坑。 有任何关于小程序的问题可以加我微信:2501902696(备注小程序) 视频讲解: https://edu.csdn.net/course/detail/9531
2019-07-26 - 小程序实现列表拖拽排序
小程序列表拖拽排序 [图片] wxml [代码]<view class='listbox'> <view class='list kelong' hidden='{{!showkelong}}' style='top:{{kelong.top}}px'> <view class='index'>?</view> <image src='{{kelong.xt}}' class='xt'></image> <view class='info'> <view class="name">{{kelong.name}}</view> <view class='sub-name'>{{kelong.subname}}</view> </view> <image src='/images/sl_36.png' class='more'></image> </view> <view class='list' wx:for="{{optionList}}" wx:key=""> <view class='index'>{{index+1}}</view> <image src='{{item.xt}}' class='xt'></image> <view class='info'> <view class="name">{{item.name}}</view> <view class='sub-name'>{{item.subname}}</view> </view> <image src='/images/sl_36.png' class='more'></image> <view class='moreiconpl' data-index='{{index}}' catchtouchstart='dragStart' catchtouchmove='dragMove' catchtouchend='dragEnd'></view> </view> </view> [代码] wxss [代码].map-list .list { position: relative; height: 120rpx; } .map-list .list::after { content: ''; width: 660rpx; height: 2rpx; background-color: #eee; position: absolute; right: 0; bottom: 0; } .map-list .list .xt { display: block; width: 95rpx; height: 77rpx; position: absolute; left: 93rpx; top: 20rpx; } .map-list .list .more { display: block; width: 48rpx; height: 38rpx; position: absolute; right: 30rpx; top: 40rpx; } .map-list .list .info { display: block; width: 380rpx; height: 80rpx; position: absolute; left: 220rpx; top: 20rpx; font-size: 30rpx; } .map-list .list .info .sub-name { font-size: 28rpx; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #646567; } .map-list .list .index { color: #e4463b; font-size: 32rpx; font-weight: bold; position: absolute; left: 35rpx; top: 40rpx; } [代码] js [代码]data:{ kelong: { top: 0, xt: '', name: '', subname: '' }, replace: { xt: '', name: '', subname: '' }, }, dragStart: function(e) { var that = this var kelong = that.data.kelong var i = e.currentTarget.dataset.index kelong.xt = this.data.optionList[i].xt kelong.name = this.data.optionList[i].name kelong.subname = this.data.optionList[i].subname var query = wx.createSelectorQuery(); //选择id query.select('.listbox').boundingClientRect(function(rect) { // console.log(rect.top) kelong.top = e.changedTouches[0].clientY - rect.top - 30 that.setData({ kelong: kelong, showkelong: true }) }).exec(); }, dragMove: function(e) { var that = this var i = e.currentTarget.dataset.index var query = wx.createSelectorQuery(); var kelong = that.data.kelong var listnum = that.data.optionList.length var optionList = that.data.optionList query.select('.listbox').boundingClientRect(function(rect) { kelong.top = e.changedTouches[0].clientY - rect.top - 30 if(kelong.top < -60) { kelong.top = -60 } else if (kelong.top > rect.height) { kelong.top = rect.height - 60 } that.setData({ kelong: kelong, }) }).exec(); }, dragEnd: function(e) { var that = this var i = e.currentTarget.dataset.index var query = wx.createSelectorQuery(); var kelong = that.data.kelong var listnum = that.data.optionList.length var optionList = that.data.optionList query.select('.listbox').boundingClientRect(function (rect) { kelong.top = e.changedTouches[0].clientY - rect.top - 30 if(kelong.top<-20){ wx.showModal({ title: '删除提示', content: '确定要删除此条记录?', confirmColor:'#e4463b' }) } var target = parseInt(kelong.top / 60) var replace = that.data.replace if (target >= 0) { replace.xt = optionList[target].xt replace.name = optionList[target].name replace.subname = optionList[target].subname optionList[target].xt = optionList[i].xt optionList[target].name = optionList[i].name optionList[target].subname = optionList[i].subname optionList[i].xt = replace.xt optionList[i].name = replace.name optionList[i].subname = replace.subname } that.setData({ optionList: optionList, showkelong:false }) }).exec(); }, [代码]
2019-07-28 - 小程序图文编辑器页面制作
[图片] wxml [代码]<view class='ceng' hidden='{{!showceng}}' catchtouchmove='true'></view> <!-- editTxt-titt --> <view class='edit-txt' hidden='{{!showedittitt}}' catchtouchmove='true'> <textarea placeholder='请编辑文字' value='{{edittitt}}' bindconfirm="editedtitt" maxlength="300" /> </view> <!-- editTxt-content --> <view class='edit-txt' hidden='{{!showeditcontent}}' catchtouchmove='true'> <textarea placeholder='请编辑文字' value='{{editcontent}}' bindconfirm="editedcontent" maxlength="300" /> </view> <!-- reditTxt-content 标题输入,首编辑输入,重新编辑输入是分别使用三个不同的编辑框实现--> <view class='edit-txt' hidden='{{!showreditcontent}}' catchtouchmove='true'> <textarea placeholder='请编辑文字' value='{{reditcontent}}' bindconfirm="reditedcontent" maxlength="300" /> </view> <view class='k-bai'> <view class='txt titt' data-name='titt' bindtap='towrite'> {{titt}} <image src='/images/delet.png' class='delet-icon' data-name='titt' catchtap='ondelettitt' wx:if='{{titt!="请输入标题文字"}}'></image> </view> </view> <view class='content k-bai' wx:for="{{content}}" wx:key="" data-leixing='{{item.leixing}}' data-index='{{index}}' bindtap='redit'> <view class='txt' wx:if="{{item.leixing=='txt'}}">{{item.neirong}}</view> <image src='{{item.url}}' class='image' mode="widthFix" wx:if="{{item.leixing=='img'}}"></image> <image src='/images/delet.png' class='delet-icon' data-index='{{index}}' catchtap='ondelet' wx:if="{{content[index]!=''}}"></image> </view> <view class='add'> <image src='/images/add_06.png' bindtap='addtxt'></image> <image src='/images/add_03.png' bindtap='addimg'></image> </view> <view class='save-btn' bindtap='onsave'>上传</view> [代码] js [代码]// pages/edit/edit.js Page({ /** * 页面的初始数据 */ data: { titt: '请输入标题文字', showceng: false, showedittitt: false, showeditcontent: false, showreditcontent: false, edittitt: '', editcontent: '', reditcontent: '', target: '', content: [{ leixing: 'txt', neirong: '我爱你' }, { leixing: 'img', url: 'http://wechatpx.oss-cn-beijing.aliyuncs.com/card1_03.png' } ], }, /** * 生命周期函数--监听页面加载 */ towrite: function(e) { var that = this var target = e.currentTarget.dataset.name that.setData({ showceng: true, showedittitt: true, target: target, edittitt: '' }) }, editedtitt: function(e) { var that = this var target = that.target that.setData({ titt: e.detail.value, showceng: false, showedittitt: false, [target]: e.detail.value }) }, ondelettitt: function(e) { var that = this wx.showModal({ title: '重置标题', content: '您确定要重置标题吗?', success(res) { if (res.confirm) { that.setData({ titt: '请输入标题文字' }) } else if (res.cancel) { } }, confirmColor: '#5677fc' }) }, ondelet: function(e) { var that = this var index = e.currentTarget.dataset.index var content = that.data.content wx.showModal({ title: '删除提示', content: '您确定要删除这段编辑吗?', success(res) { if (res.confirm) { content.splice(index, 1) that.setData({ content: content }) } else if (res.cancel) {} }, confirmColor: '#5677fc' }) }, addtxt: function() { var that = this var content = that.data.content that.setData({ editcontent:'', showceng: true, showeditcontent: true }) }, editedcontent: function(e) { var that = this var input = new Object input.leixing = 'txt' input.neirong = e.detail.value var content = that.data.content content.push(input) that.setData({ content:content, showceng: false, showeditcontent: false }) }, redit:function(e){ var that = this var index = e.currentTarget.dataset.index var leixing = e.currentTarget.dataset.leixing var target = that.data.target if(leixing=='txt'){ target = "content["+index+"].neirong" that.setData({ reditcontent:'', showceng: true, showreditcontent: true, target: target }) }else if(leixing=='img'){ wx.chooseImage({ count: 1, sizeType: ['original', 'compressed'], sourceType: ['album', 'camera'], success(res) { const tempFilePaths = res.tempFilePaths target = "content[" + index + "].url" that.setData({ [target]: tempFilePaths }) } }) } }, reditedcontent: function (e) { var that = this var target = that.data.target that.setData({ [target]: e.detail.value, showceng: false, showreditcontent: false, }) }, addimg:function(){ var that = this wx.chooseImage({ count: 1, sizeType: ['original', 'compressed'], sourceType: ['album', 'camera'], success(res) { const tempFilePaths = res.tempFilePaths var input = new Object input.leixing = 'img' input.url = tempFilePaths var content = that.data.content content.push(input) that.setData({ content: content, }) } }) }, onsave:function(){ wx.showToast({ title: '上传成功!', }) }, onLoad: function(options) { }, /** * 生命周期函数--监听页面初次渲染完成 */ onReady: function() { }, /** * 生命周期函数--监听页面显示 */ onShow: function() { }, /** * 生命周期函数--监听页面隐藏 */ onHide: function() { }, /** * 生命周期函数--监听页面卸载 */ onUnload: function() { }, /** * 页面相关事件处理函数--监听用户下拉动作 */ onPullDownRefresh: function() { }, /** * 页面上拉触底事件的处理函数 */ onReachBottom: function() { }, /** * 用户点击右上角分享 */ onShareAppMessage: function() { } }) [代码] wxss [代码]/* pages/edit/edit.wxss */ page { background-color: #f1f1f1; font-size: 28rpx; padding-bottom: 150rpx; } .k-bai { background-color: #fff; padding: 30rpx; color: #666; } .txt { padding: 30rpx; border: 1rpx solid #eee; margin-top: 20rpx; position: relative; } .delet-icon { display: block; width: 50rpx; height: 50rpx; border-radius: 50rpx; position: absolute; right: -20rpx; top: -20rpx; z-index: 10; } .ceng { width: 750rpx; height: 1334rpx; position: fixed; left: 0; top: 0; z-index: 50; background-color: rgba(0, 0, 0, 0.3); } .edit-txt { width: 660rpx; background-color: #fff; padding: 30rpx; margin: 0 auto; position: fixed; left: 50%; margin-left: -360rpx; top: 220rpx; z-index: 60; } .edit-txt .save-btn { display: block; width: 450rpx; height: 80rpx; border-radius: 80rpx; background-color: #5677fc; color: #fff; text-align: center; line-height: 80rpx; position: absolute; left: 50%; margin-left: -225rpx; bottom: -40rpx; box-shadow: 0 0 10rpx rgba(0, 0, 0, 0.2); } .edit-txt textarea { width: 660rpx; height: 500rpx; } .add { position: relative; padding: 20rpx 0; width: 200rpx; margin: 0 auto; height: 70rpx; background-color: #fff; border-radius: 0 0 20rpx 20rpx; box-shadow: 0 7rpx 5rpx rgba(0, 0, 0, 0.1); } .add image { display: inline-block; width: 70rpx; height: 70rpx; } .add image:first-child { margin-right: 20rpx; margin-left: 20rpx; } .content { margin-top: 30rpx; position: relative; } .content .delet-icon { right: 12rpx; top: 25rpx; } .content .image { display: block; padding: 30rpx; border: 1rpx solid #eee; margin-top: 20rpx; position: relative; width: 630rpx; } .save-btn { width: 690rpx; height: 90rpx; text-align: center; line-height: 90rpx; background-color: #5677fc; color: #fff; font-size: 34rpx; border-radius: 90rpx; position: fixed; left: 50%; margin-left: -345rpx; bottom: 30rpx; z-index: 100; } [代码]
2019-07-29 - 微信小程序注册、登录小功能都在这
微信小程序实现注册、登录页面的小功能整理,希望对大家有帮助。 1. 正则验证手机号码 [代码]var[代码] [代码]mobile = that.data.phone;[代码][代码] [代码][代码]var[代码] [代码]myreg = /^(((13[0-9]{1})|(15[0-9]{1})|(18[0-9]{1})|(17[0-9]{1}))+\d{8})$/;[代码][代码] [代码][代码]if[代码] [代码](!myreg.test(mobile)) {[代码][代码] [代码][代码]wx.showToast({[代码][代码] [代码][代码]title: [代码][代码]'手机号有误!'[代码][代码],[代码][代码] [代码][代码]icon: [代码][代码]'success'[代码][代码],[代码][代码] [代码][代码]duration: 1500[代码][代码] [代码][代码]})[代码][代码] [代码][代码]return[代码] [代码];[代码][代码] [代码][代码]}[代码][代码] [代码][代码]wx.showToast({[代码][代码] [代码][代码]title: [代码][代码]'手机号正确!'[代码][代码],[代码][代码] [代码][代码]icon: [代码][代码]'success'[代码][代码],[代码][代码] [代码][代码]duration: 1500[代码][代码] [代码][代码]})[代码]2. 60秒倒计时 [图片] 发送短信验证码后会有60秒的倒计时功能。 网上有很多这种插件,很方便 比如: http://smsow.zhenzikj.com/doc/sdk.html [图片] 使用方法1.引入插件countdown.js [代码]var[代码] [代码]CountDown = require([代码][代码]'../../utils/countdown.js'[代码][代码]);[代码] 2.在 onLoad 周期初始化 [代码]onLoad: [代码][代码]function[代码] [代码]() {[代码][代码] [代码][代码]this[代码][代码].countdown = [代码][代码]new[代码] [代码]CountDown([代码][代码]this[代码][代码]);[代码][代码]}[代码] 3. 在获取验证码的按钮上增加captchaDisabled、captchaTxt 分别用于控制倒计时过程中是否可以点击、倒计时秒数提示 [代码]<button class=[代码][代码]'codeBtn'[代码] [代码]bindtap=[代码][代码]'getSmsCaptcha'[代码] [代码]disabled=[代码][代码]'{{captchaDisabled}}'[代码][代码]>{{captchaTxt}}</button>[代码] 4. 调用start方法触发倒计时 [代码]getSmsCaptcha(e) {[代码][代码] [代码][代码]this[代码][代码].countdown.start();[代码][代码]}[代码] 3. 发送短信验证码 小编使用的是榛子云短信(http://smsow.zhenzikj.com/doc/sdk.html)的发送验证码短信。 目前提供了普通版和云函数版,建议下载云函数版的。两个版本中都提供了对验证码的支持,你无需生成验证码,SDK已经帮你都弄好了。 如何使用 1)配置域名 在微信公众平台-小程序管理中配置域名https://smsdeveloper.zhenzikj.com,如下图: [图片] 2) 引入sdk [代码]var[代码] [代码]zhenzisms = require([代码][代码]'../../utils/zhenzisms.js'[代码][代码]);[代码] 3)初始化 [代码]zhenzisms.client.init([代码][代码]'https://sms_developer.zhenzikj.com'[代码][代码], [代码][代码]'你的榛子云appId'[代码][代码], [代码][代码]'你的榛子云appSecret'[代码][代码]);[代码] 4) 发送验证码短信 [代码]zhenzisms.client.sendCode([代码][代码]function[代码] [代码](res) {[代码][代码] [代码][代码]wx.showToast({[代码][代码] [代码][代码]title: res.data.data,[代码][代码] [代码][代码]icon: [代码][代码]'none'[代码][代码],[代码][代码] [代码][代码]duration: 2000[代码][代码] [代码][代码]})[代码][代码] [代码][代码]}, that.data.phone, [代码][代码]'验证码为:{code}'[代码][代码], [代码][代码]''[代码][代码], 60 * 5, 4);[代码] 参数1:请求后的用于接收返回结果的回调函数 参数number:接收者手机号码 参数3:短信模板,其中{code}为验证码占位符,会自动替换 参数messageId:该条信息的唯一标识,可用于查询 参数seconds:验证码有效期,单位是秒 参数length:验证码长度,比如4位或6位 返回结果是json格式的字符串, code: 发送状态,0为成功。非0为发送失败,可从data中查看错误信息 当然,你也可以使用云函数版的,请参考文档: http://smsow.zhenzikj.com/doc/weixinmp_yun_sdk_doc2.html
2019-07-29 - 小程序将小程序码与图片结合生成海报分享朋友圈
样例参考(瑞幸咖啡小程序) [图片][图片][图片] 需求分析 服务器端会返回不确认的图片资源到前端 前端将返回的每张图片都要贴上小程序码 将贴上小程序码的图片使用 swiper 组件轮播 用户点击保存时,将图片保存至相册。 至于点击保存如何保存至相册(wx.saveImageToPhotosAlbuml 了解一下,注意一下授权问题即可) 碰到的问题 canvas为原生组件, 而原生组件的层级是最高的,所以页面中的其他组件无论设置 z-index 为多少,都无法盖在原生组件上。 [代码]采用方法: 通过定位,将其移除到不在可视范围,如iphone6 .canvas { position: relative; left: -375px; } [代码] 绘制多张图片时, 一个canvas 标签只能对应画一张图片 (目前我测试的是这样,如有其它方法,欢迎评论交流) [代码]采用方法: 1. html端循环要生成的图片张数,对应循环出多个 canvas 组件,分别设置不同的 canvasId 区分 2. 封装一个绘制海报的函数,返回一个Promise对象,用于后面绘制完所有的图片,统一赋值渲染,避免多次触发数据更新 3. js端 循环执行一次 绘制海豹函数,存于一个数组列表 4. 使用 Promise.all 函数,统一绘制完毕将生产图片路径赋值 html: <canvas v-for="u in m.urlList" :key="u" :canvas-id="'poster'+index" class="canvas" style="width:100%; height:100%;"> </canvas> <swiper :indicator-dots="true" :circular="true" :autoplay="true" indicator-color="rgba(255,255,255,.2)" indicator-active-color="#fff" class="swiper"> <swiper-item v-for="f in m.filePaths" :key="index"> <image :src="f"> </swiper-item> </swiper> js: // 数据 (mpvue 开发) function data(){ return { m : { urlLIst: [ // 图片资源 '/static/poster0.jpg', '/static/poster1.jpg' ], filePaths: [], // 生成图片(贴上小程序码) } } } try { let res = wx.getSystemInfoSync(); // 同步获取系统信息 let w = res.windowWidth; // 手机可用区域宽度 let h = res.windowHeight; // 手机可用区域高度 let codeUrl = '/static/code.jpg'; // 小程序码 let drawList = []; // 用于保存绘制海报图Promise对象 m.urlList.forEach( (u, i)=> { // 传入canvas组件ID,图片路径(测试使用的是本地路径) drawList.push(drawPoster('poster'+i, u, codeUrl)); }) // 统一更新数据 Promise.all(drawList).then((valuse)=>{ m.filePaths = valuse; }); // 封装绘制图片函数 function drawPoster(canvasId, bgUrl, codeUrl){ return new Promise( (resolve, reject) => { // 创建画布实例 let ctx = wx.createCanvasContext(canvasId); // 绘制背景图: 图片路径,x坐标,y坐标,宽,高 ctx.drawImage(bgUrl, 0, 0, w, h); // 绘制小程序码 ctx.drawImage(codeUrl, w-120, h-120, 100, 100); // 绘制 ctx.draw(false, ()=>{ // 该通过函数将canvas绘制导出为图片 wx.canvasToTempFilePath({ x: 0, y: 0, width: w, height: h, canvasId: canvasId, success(res){ resolve(res.tempFilePath); } }); }); } }catch(e){ // 自己封装了一成 wx.$toast(e); } [代码] 最终demo效果图 [图片][图片] 在社区中暂未看到多张海报实现的方案,如果有更好的实现方案,欢迎交流
2019-07-30 - 全自动、全面化收集 formid 思路,开启脑洞时刻
formid 基础知识 formid 是啥 小程序给用户发消息的唯一途径,所以前端要尽量多收集 formid 保存到服务端; formid 怎么收集 小程序要求必须用户提交表单才能得到formid; form 增加 report-submit 属性,并通过 button submit,前端 formSubmit 事件就能获得 formid ,如下方代码所示: [代码]<form bindsubmit="formSubmit" report-submit class="layout"> <button formType="submit" class="button" hover-class="none"> 。。。 </button> </form> [代码] formid 组件封装(初期思路) 我们将form与button 封装成组件(fomids),通过slot,可以显示任意元素,模拟form submit; 把页面上需要点击的元素用fomids包起来,fomids 将收集的 formid 放入cache,然后通过网络请求将 fomid 传给服务端; 大数小程序都是这么做的,这样做也没有问题,就是略微麻烦,form 与button 组合的样式有时不太好控制 formid 组件封装(进阶思路) 此思路是上面思路的延申,我们把 formids 这个组件做成layout级,把整个页面包在里面,利用事件自动冒泡的特性,只要有点击事件,就能收集到 formid 新建组件layout [代码]<!--components/layout.wxml--> <form bindsubmit="formSubmit" report-submit class="layout"> <button formType="submit" class="button" hover-class="none"> <view class="fixed"><slot></slot></view> </button> </form> [代码] [代码]/* components/layout.wxss */ .layout { display: inline-block; padding-left: 0; padding-right: 0; box-sizing: border-box; font-size: inherit; text-align: left; text-decoration: none; line-height: inherit; -webkit-tap-highlight-color: transparent; color: inherit; width: 100%; position: relative; } .layout .button{ display: inline-block; padding-left: 0; padding-right: 0; box-sizing: border-box; font-size: inherit; text-align: left; text-decoration: none; line-height: inherit; -webkit-tap-highlight-color: #000; color: inherit; width: 100%; position: relative; } .layout .button .fixed{ position:relative; z-index: 9999; width: 100%; } .layout .button:before,.layout .button:after{ border-width: 0; } [代码] [代码]// components/layout.js Component({ methods:{ formSubmit: function(e) { console.log('layout.formids',e.detail.formId) if("the formId is a mock one"!=e.detail.formId){ let formids=wx.getStorageSync('formids') || []; formids.push(e.detail.formId); formids=[...new Set(formids)]; wx.setStorage({key:'formids',data:formids}); } }, }, }) [代码] 将 layout 添加为全局组件 app.json 中增加 [代码]"usingComponents":{ "layout":"/components/layout" }, [代码] 在页面wxml中使用 [代码]<layout> <view class="pages">...</view> </layout> [代码] 怎么将 formid 提给服务端 首先你必须将wx.requxest 进行封装为myRequxest,页面上都是用myRequxest 进行网络请求; myRequxest 中 header 增加formids,从cache中获得放到header中;并删除formid缓存;(需要服务端从header中获得formids并存起来) [代码]function myRequxest(....){ let formids = wx.getStorageSync('formids'); if(formids){ wx.removeStorage({key:'formids'}); } let openid='';//用户openid wx.request({ ... header: { formids:formids.toString(), openid:openid, }, ... }) } [代码] 延申思考 从 写 form 收集 -> 封装 formids 组件 -> 封装 layout(formids) 组件,跳出思维固化,可以做更多可能; 希望这个思路能给你一些触发
2019-07-31 - 小程序服务商开发者,搭建一个mvp第三方平台必备的开发功能模块构成
我们在搭建小程序第三方平台过程发现官方提供的有很多接口,但不是每个接口都需要开发者用代码去实现。比如代码包管理的删除接口,就可以通过登录open.weixin.qq.com里,在列表看着更加详细的代码包版本介绍通过官方提供的界面就可以删除。即如果不是特殊需要,此接口可以不做开发。 那么我们搭建一个小程序服务商第三方平台需要哪些必要的功能模块呢? 必备功能模块概况 首先用一张图总概括下搭建一个mvp小程序服务商第三方平台必备的开发功能模块 [图片] 第三方平台权限集 先用一张图表明第三方平台账号在授权后可用的权限集的部分。 [图片] 只有平台先申请某一个权限,经过审核且全网发布后,旗下小程序才能在授权时选择某权限。有以下等 A>=B, B>=C;也就是第三方平台即使有每个微信提供的每个权限集,通过管理员授权后也不一定有全部权权限。原因: 1 单一授权被授权给其他第三方平台账号,或管理员授权时选择不把某一个权限授权给平台。比如开发和代码管理权限集 2 旗下小程序自身主体资质性质自带,不能拥有某个api权限。案例参考3 3 最终授权成功的权限集里,每个权限下都有多个api,所以在用某个api前需要看是由有权限。比如开发和代码管理权限集下,setwebviewdomain这个api不支持个人主体资质的appid调用 4 授权成功后,用token就可以用第三方平台提供的api代调用实现业务,或代调用小程序开发文档的api(标志参数需要access_token)里的接口,来实现产品开发。 授权及回调域名验证 ####### 说明 ####### 发起域名必须要在合法登记的域名名单内,且用户授权同意后的跳转url必须要在资料登记。体现在第三方平台创建时的以下两项配置中。 ① 授权发起页域名 ② 授权事件接收URL [图片] 授权回调URL源码 [代码] public void ProcessRequest(HttpContext context) { Request = context.Request; Response = context.Response; string signature = Request.QueryString["signature"]; string msg_signature = Request.QueryString["msg_signature"]; string timestamp = Request.QueryString["timestamp"]; string nonce = Request.QueryString["nonce"]; #region Response body // 微信并不会因为返回失败就再发送一次消息,相反如果不返回success, 微信会延迟推送 string ResMsg = "success"; if (Request.HttpMethod == "POST") { using (Stream stream = Request.InputStream) { Byte[] postBytes = new Byte[stream.Length]; stream.Read(postBytes, 0, (Int32)stream.Length); string postString = Encoding.UTF8.GetString(postBytes); if (postString.Contains("<AppId>")) { XElement xdoc = XElement.Parse(postString); CurAppID = xdoc.Element("AppId").Value.Trim(); LoadAppInfo(CurAppID); string postStringXmlSrc = string.Empty; Tencent.WXBizMsgCrypt wxcpt = new Tencent.WXBizMsgCrypt(CurAppToken, CurAppEncodingAESKey, CurAppID); int ret = wxcpt.DecryptMsg(msg_signature, timestamp, nonce, postString, ref postStringXmlSrc); if (ret == 0) { xdoc = XElement.Parse(postStringXmlSrc); if (xdoc != null) { string InfoType = xdoc.Element("InfoType").Value.Trim(); if (InfoType == "component_verify_ticket") { string componentVerifyTicket = xdoc.Element("ComponentVerifyTicket").Value.Trim(); WeixinDataHelper.UpdateComponentVerifyTicket(CurAppID, componentVerifyTicket); } else if (InfoType == "unauthorized") { string authorizedAppId = xdoc.Element("AuthorizerAppid").Value.Trim(); WeixinDataHelper.Unauthorized(CurAppID, authorizedAppId); } else { // 微信平台上填写的授权URL 目前就支持这两种 InfoType } } } } } } else if (Request.HttpMethod == "GET") { ResMsg = Request.QueryString["echostr"]; } ResponseEnd(ResMsg); #endregion } [代码] 各种票据有效性维护机制 1 授权后得到授权码(authorization_code ) [代码]需要用授权码去调用接口换取令牌,并保存。 [代码] 2 获取令牌和刷新令牌 [代码]用授权码获得令牌authorizer_access_token和刷新令牌authorizer_refresh_token。 需要保存令牌和刷新令牌。 [代码] 3 刷新令牌authorizer_access_token [代码]用刷新令牌定期去微信网关拉取令牌,维持令牌的有效性,保证后期代实现接口时令牌有效性。约1h左右的时间去刷新一次令牌。刷新令牌服务需要有重试机制,因为瞬时网络原因会返回失败,需要重试。 [代码] 消息与事件处理平台 1 第三方平台component_verify_ticke更新 [代码]平台审核通过后,每隔10分钟定时推送一次component_verify_ticket,开发者需要保存在数据库。再授权场景获取预授权码时需要用到这个有效的ticket。 [代码] 2 授权状态变更(成功,变更,取消) 3 代码审核通知消息 4 注意: [代码]① 这里的消息时加密的需要先解密。 ② 有开发者反馈说不知道返回信息时旗下哪个appid,这里补充下,appid是再请求头的request参数里直接返回的。 ③ 消息与通知解密部分代码 [代码] [代码] public void ProcessRequest(HttpContext context) { HttpRequest Request = context.Request; HttpResponse Response = context.Response; // 所属的已授权公众号的appid string AppID = Request.QueryString["AppID"]; string reqSignature = Request.QueryString["signature"]; string reqMsgSignature = Request.QueryString["msg_signature"]; string reqTimestamp = Request.QueryString["timestamp"]; string reqNonce = Request.QueryString["nonce"]; if (string.IsNullOrEmpty(AppID) || !WeixinHelper.ValidateWeixinInterface(reqSignature, WeixinResources.ComponentAppToken, reqTimestamp, reqNonce)) { ResponseEnd(Response, string.Empty,AppID); return; } if (AppID == WeixinResources.AutoTestAppID) { string _tmsg = new WeixinAutoTestHandler(Request, Response, WeixinResources.ComponentAppID).GetMsg(); ResponseEnd(Response, _tmsg,AppID); return; } #region Response body string ResMsg = string.Empty; if (Request.HttpMethod == "POST") { using (Stream stream = Request.InputStream) { Byte[] postBytes = new Byte[stream.Length]; stream.Read(postBytes, 0, (Int32)stream.Length); string postString = Encoding.UTF8.GetString(postBytes); string Msg = string.Empty; // 解密 Tencent.WXBizMsgCrypt wxcpt = new Tencent.WXBizMsgCrypt(WeixinResources.ComponentAppToken, WeixinResources.ComponentAppEncodingAESKey, WeixinResources.ComponentAppID); int ret = 0; ret = wxcpt.DecryptMsg(reqMsgSignature, reqTimestamp, reqNonce, postString, ref Msg); if (ret != 0) { ResponseEnd(Response, string.Empty,AppID); return; } // 生成响应消息 string resMsg = WeixinHelper.ReturnMessageAsThirdPlatform(AppID, Msg); // 加密消息 string EncryptMsg = string.Empty; ret = wxcpt.EncryptMsg(resMsg, reqTimestamp, reqNonce, ref EncryptMsg); if (ret != 0) { EncryptMsg = string.Empty; } ResMsg = EncryptMsg; } } else if (Request.HttpMethod == "GET") { ResMsg = Request.QueryString["echostr"]; } ResponseEnd(Response, ResMsg,AppID); #endregion } [代码] [代码]public static string ReturnMessageAsThirdPlatform(string AppID, string requestMsg) { string responseContent = string.Empty; XmlDocument xmlDoc = new XmlDocument(); xmlDoc.LoadXml(requestMsg); XmlNode MsgType = xmlDoc.SelectSingleNode("/xml/MsgType"); if (MsgType != null) { switch (MsgType.InnerText) { case "event": responseContent = EventHandleAsThirdPlatform(AppID, xmlDoc); //事件处理 break; case "text": case "image": case "voice": case "video": case "shortvideo": case "location": responseContent = TextHandleAsThirdPlatform(AppID, xmlDoc); //消息处理 break; default: break; } } return responseContent; } [代码] 代小程序域名设置机制 1 小程序服务器域名 为授权小程序设置服务器域名 requestdomain request合法域名 wsrequestdomain socket合法域名 uploaddomain uploadFile合法域名 downloaddomain downloadFile合法域名 2 小程序业务域名 为授权小程序提供业务域名 web-view的合法域名 第三方平台先登记后,在给旗下小程序授权 旗下小程序可以使用配置域名的子域名作为业务域名 代小程序代码包管理机制 1 提交代码包 详细关于代码包管理参考https://developers.weixin.qq.com/community/develop/article/doc/000622ad764e48a45419e25b151813 ①ext.json的配置项更改体现在代码包配置json数据里。ext_json字段对应ext.json里的字段,这里template_id关联第三方平台》小程序模板管理里的模板ID。 ② 那么要提交小程序的appid呢?还是通过ext_json字段来配置。ext_jsonjson字符串里的extAppid就是第三方平台账号旗下授权,本次要提交代码的小程序appid,通过上传代码时候的来指定extAppid就可以给一个小程序代提交代码。 [图片] [图片] 2 提交审核 3 收到审核通知审核通过后提交上线 4 查询最近一次提交审核进度 5 回退到上一个版本 第三方代码模板管理 1 创建小程序账号,授权给平台账号,并绑定到第三方平台账号的开发小程序列表 2 用开发小程序appid创建项目的开发代码包模板 3 用开发小程序appid提交代码到草稿箱 4从草稿箱添加到模板库 附加总结稿 [图片]
2020-11-05 - [打怪升级]小程序评论回复和发帖功能实战(二)
[图片] 这次分享下“发帖功能”,这个功能其实风险蛮大的,特别是对一些敏感言论的控制,如果没有做好可能导致小程序被封,所以除了必要的人工审核和巡查以外,我们需要一些微信安全监测API的帮忙,在AI加持下,现在很多大公司对内容和图片的效率大大提高了。 [图片] 这个DEMO仅是一个流程示例,由于涉及到云函数和“真”敏感图,这里就有文字图代替。 [图片] 发帖的功能只要理清思路,其实并不复杂,利用机器AI做内容审查是关键,直接关系到小程序的整体安全。 用户选择手机图库或拍照 [代码]let tempImg = 0; //代表已选择的图片 wx.chooseImage({ count: 3 - tempImg.length, //选择不超过3张照片,去掉当前已经选择的照片 sizeType: ['original', 'compressed'], //获取原图或者压缩图 sourceType: ['album', 'camera'], //获取图片来源 图库、拍照 success(res) { // tempFilePath可以作为img标签的src属性显示图片 let tempFilePaths = res.tempFilePaths; console.log(tempFilePaths); //举例:这里可以size 来判断图片是否大于 1MB,方便后面内容检查 if (res.tempFiles[0] && res.tempFiles[0].size > 1024 * 1024) { console.log("图片大于1MB啦") } } }) [代码] 这里用到的方法是chooseImage,它可以设置让用户选择手机图片库和拍照获得,需要注意的是考虑到后面要用微信自带API做图片安全检查,图片大小不能超过1MB,所以需要设置sizeType为compressed。 内容检查(重点) 由于内容安全对于小程序运营至关重要,稍有不慎就容易导致小程序被封,所以在这块的校验除了常规人工检查外,我们还可以用到微信的内容安全API。 为什么用微信官方提供的API? 主要有二点:有一定的免费额度,基于企鹅大厂的专业AI检查。 1、云函数+云调用 [图片] 目录结构 [代码]├─checkContent │ config.json //云调用的权限配置 │ index.js //云服务器node 入口文件 │ package-lock.json │ package.json // NPM包依赖 │ ... [代码] 为什么要强调这个? 因为本人一开始在用云函数+云调用的时候,经常会出现各种不明BUG,很多都是因为目录里面少传文件,或者少配置。 云函数内容: [代码]const cloud = require('wx-server-sdk'); cloud.init(); exports.main = async (event, context) => { console.log(event.txt); const { value, txt } = event; try { let msgR = false; let imageR = false; //检查 文字内容是否违规 if (txt) { msgR = await cloud.openapi.security.msgSecCheck({ content: txt }) } //检查 图片内容是否违规 if (value) { imageR = await cloud.openapi.security.imgSecCheck({ media: { header: { 'Content-Type': 'application/octet-stream' }, contentType: 'image/png', value: Buffer.from(value) } }) } return { msgR, //内容检查返回值 imageR //图片检查返回值 }; } catch (err) { // 错误处理 // err.errCode !== 0 return err } } [代码] 这里主要用到security.msgSecCheck和security.imgSecCheck这2个微信开放云调用方法(需开发者工具版本 >= 1.02.1904090),以往我们还要在服务器上单独写个方法,现在变得十分的方便,直接在云函数中调用即可。 这里需要重点说2个点 图片security.imgSecCheck 方法只能接收buffer,所以需要把temp的临时图片转化为buffer的形式传过去,我们这里用到 getFileSystemManager 的方法。 如果目录文件中没有config.json,需要自己建一个,并且做一个授权的配置。 [代码]{ "permissions": { "openapi": [ "security.msgSecCheck", "security.imgSecCheck" ] } } [代码] 2、检查文字内容安全 [代码]wx.cloud.callFunction({ name: 'checkContent', data: { txt: "乐于分享,一起进步" }, success(_res) { console.log(_res) }, fail(_res) { console.log(_res) } }) //返回值参考 { "errMsg": "cloud.callFunction:ok", "result": { "msgR": { "errMsg": "openapi.security.msgSecCheck:ok", "errCode": 0 }, "imageR": false }, "requestID": "77952319-b2b4-11e9-bdc8-525400192d0e" } [代码] 应用场景举例: 用户个人资料违规文字检测; 媒体新闻类用户发表文章,评论内容检测; 游戏类用户编辑上传的素材(如答题类小游戏用户上传的问题及答案)检测等。 频率限制:单个 appId 调用上限为 4000 次/分钟,2,000,000 次/天* 通过wx.cloud.callFunction的方法调用checkContent的云函数,检查一段文本是否含有违法违规内容。 3、检查图片内容安全 [代码]//获取 temp临时图片文件的 buffer wx.getFileSystemManager().readFile({ filePath: tempImg[0], //这里做示例,所以就选取第一张图片 success: buffer => { console.log(buffer.data) //这里是 云函数调用方法 wx.cloud.callFunction({ name: 'checkContent', data: { value: buffer.data }, success(json) { console.log(json.result.imageR) if (json.result.imageR.errCode == 87014) { wx.showToast({ title: '图片含有违法违规内容', icon: 'none' }); console.log("bad") } else { //图片正常 } } }) } }) //返回值参考 { "errMsg": "cloud.callFunction:ok", "result": { "msgR": false, "imageR": { "errMsg": "openapi.security.imgSecCheck:ok", "errCode": 0 } }, "requestID": "c126353c2d-b40b-11e9-81c4d-525400235f2a" } [代码] 应用场景举例: 图片智能鉴黄:涉及拍照的工具类应用(如美拍,识图类应用)用户拍照上传检测;电商类商品上架图片检测;媒体类用户文章里的图片检测等; 敏感人脸识别:用户头像;媒体类用户文章里的图片检测;社交类用户上传的图片检测等。 频率限制:单个 appId 调用上限为 2000 次/分钟,200,000 次/天*(图片大小限制:1M) 这里先要用 getFileSystemManager() 获取临时图片的buffer(这个是重点),然后再通过wx.cloud.callFunction的方法调用 checkContent的云函数中security.imgSecCheck的方法,校验一张图片是否含有违法违规内容。 一开始本人调试的时候,也遇到无法上传的问题,必须通过文件管理(getFileSystemManager)获取buffer后才能上传检查图片,耗费了本人不少debugger时间。 完整代码 原本想做个实际的demo(代码片段)分享给大家打开参考的,但是云函数必须是一个已注册的APPID,无奈只能贴代码。 这里主要还是提供一个整体思路,希望能帮助大家减少开发成本,更好的解决问题和完成任务 ^_^ html部分: [代码]<!-- pages/post /index.wxml --> <view class="wrap"> <view class="title"> <input placeholder="智酷方程式,乐于分享" maxlength="30" bindinput="getTitle"/> </view> <view class="content"> <textarea auto-focus="true" maxlength="200" bindinput="textareaCtrl" placeholder-style="color:#999;" placeholder="关注公众号,一起学习,一起进步" /> <view class='fontNum'>{{content.length}}/200</view> </view> <view class="chooseImg"> <block wx:for="{{tempImg}}" wx:for-item="item" wx:key="ids" wx:for-index="index"> <view class="chooseImgBox"> <image src="{{item}}" /> <view data-index="{{index}}" catch:tap="removeImg" class="removeImg"></view> </view> </block> <!-- 判断图片 大于等于3张的时候 取消 更多 --> <block wx:if="{{tempImg.length < 3}}"> <view class="chooseImgBoxMore" catch:tap="choosePhoto"> <view class="arrow"></view> </view> </block> </view> <view class='submit' catch:tap="submitPost"> <view class='blue'>提交</view> <view>取消</view> </view> </view> [代码] JS部分: [代码]Page({ /** * 页面的初始数据 */ data: { titleDetail: "", //帖子title内容 content: "", //发帖内容 tempImg: [], //选择图片的缩略图,临时地址 }, /** * 生命周期函数--监听页面加载 */ onLoad: function (options) { wx.cloud.init(); }, /** * 生命周期函数--监听页面显示 */ onShow: function () { }, /** * 检测输入字数 * @param {object} e */ textareaCtrl: function (e) { if (e.detail.value) { this.setData({ content: e.detail.value }) } else { this.setData({ content: "" }) } }, /** * 选择图片 */ choosePhoto() { let self = this; let tempImg = self.data.tempImg; if (tempImg.length > 2) { return; } wx.chooseImage({ count: 3 - tempImg.length, //选择不超过3张照片,去掉当前已经选择的照片 sizeType: ['original', 'compressed'], sourceType: ['album', 'camera'], success(res) { console.log(res); // tempFilePath可以作为img标签的src属性显示图片 let tempFilePaths = res.tempFilePaths; tempImg = tempImg.concat(tempFilePaths); console.log(tempImg); self.setData({ tempImg }) wx.getFileSystemManager().readFile({ filePath: tempImg[0], success: buffer => { console.log(buffer.data) wx.cloud.callFunction({ name: 'checkContent', data: { value: buffer.data }, success(json) { console.log(JSON.stringify(json)) console.log(json.result.imageR) if (json.result.imageR.errCode == 87014) { wx.showToast({ title: '图片含有违法违规内容', icon: 'none' }); console.log("bad") } else { //图片正常 } } }) } }) }, fail: err => { console.log(err) } }) }, /** * 删除照片 */ removeImg(e) { let self = this; let index = e.currentTarget.dataset.index; console.log(e); let tempImg = self.data.tempImg; tempImg.splice(index, 1); self.setData({ tempImg }) }, /** * 发贴 */ submitPost(e) { let { titleDetail, content } = this.data; wx.cloud.callFunction({ name: 'checkContent', data: { txt: content }, success(_res) { console.log(JSON.stringify(_res)) wx.navigateTo({ url: "/pages/postimg/result" }) }, fail(_res) { console.log(_res) } }) } }) [代码] 往期回顾: [打怪升级]小程序评论回复和发贴组件实战(一) [填坑手册]小程序Canvas生成海报(一) [拆弹时刻]小程序Canvas生成海报(二) [填坑手册]小程序目录结构和component组件使用心得
2021-09-13 - 微信小程序搜索又一波红利来了
无意中在小程序后台「微信搜一搜」发现这个,应该是即将到来的功能吧—— [图片] 「该功能开通后,小程序能够主动推送内容到微信搜一搜,推送并审核通过的内容能够被用户在微信搜一搜搜索到。」 什么意思呢?就是说以前是小程序页面被动被收录,即将可以主动发布内容出去,然后在「微信搜一搜」被搜到,是不是有点订阅号的赶脚? 具体怎么用还不知道,拭目以待吧~ 贴一个我们的小程序「医启查」,大家可以查询医院和医生的执业注册信息,以及患者社区、健康科普,我们也给用户发放福利,积分可以免费报销医药费,或者兑换健康礼品,大家可以体验一下~ [图片]
2019-08-01 - 小程序热力图组件
小程序热力图 基于小程序canvas画布+原生map组件实现 项目地址 https://github.com/rover95/wxapp-heatmap 配置参数 [代码]/* points: [{ //热力点数组 longitude: 103, //经度 latitude: 30, //纬度 ratio: 0.5, //热力点覆盖范围半径系数 opacity: 0.9 //热力点最大透明度,控制热力点权重 }], longitude: 110, //地图中心点经度 latitude: 30, //地图中心点纬度 mapScale: 7, //地图缩放,同原生map range: 50, //热力点基础覆盖范围,单位px */ [代码] WXML [代码]<!-- <heatMap>组件父节点必须定义宽高,<heatMap>组件将填充满父节点 --> <view style="width:100vw;height:200px" wx:if="{{points.length>0}}"> <heatMap points="{{points}}" longitude="{{longitude}}" latitude="{{latitude}}" mapScale="{{mapScale}}" range="{{range}}"></heatMap> </view> [代码] 开发工具上渲染可能会出现画面撕裂,真机上显示正常 [图片] 小程序画布性能孱弱,热力图需要像素级操作,全屏或渲染大尺寸canvas会有卡顿,有待微信优化 “避免设置过大的宽高,在安卓下会有crash的问题”
2019-08-02 - 微信小程序云开发连接mysql数据库,小程序云函数操作mysql数据库
小程序云开发的功能是越来越强大了,现在小程序云开发可以直接借助云函数来链接mysql数据,操作mysql数据库了,今天就来给大家讲一讲如何使用小程序云开发的云函数来操作mysql数据库。 首先要明确一点,就是小程序云开发的云函数是基于node.js的,所以我们使用node.js的mysql2模块可以直接来链接并操作mysql数据库,所以我们现在要做的就是怎么样在云函数里使用mysql2模块,并且借助这个模块类库来实现mysql数据库的链接。 老规矩,先看效果图 [图片] 我们这里要做的就是在云函数里链接mysql数据库,并返回链接的mysql数据库的版本号。mysql数据库都能成功链接了,后面对mysql的增删改查操作也就是小意思了。所以我们这里先成功的链接mysql数据库才是最重要的。 一,创建小程序并引入云开发 这里我不在做讲解,我之前有讲过小程序云开发的初始化创建,也有录视频讲解,不懂的同学可以移步去看下,云开发项目的创建视频 https://edu.csdn.net/course/play/9604/284440 这里有3点需要注意的 1,一定要在app.js里做云开发环境的初始化 [图片] 2,在project.config.json里配置云函数的目录 [图片] 3,一定要用自己注册的小程序的appid [图片] 二,创建云函数,名字就叫mysql吧。 在我们的cloud,右键创建云函数 [图片] 三,安装mysql2模块依赖 1,右键我们的mysql云函数,点击在终端中打开 [图片] 2,在终端中输入 npm install mysql2 [图片] 需要你电脑安装npm,如果没有安装,请自行百度,网上很多npm的安装教程的。 [图片] 等待我们的mysql2安装成功 四,编写mysql云函数链接mysql数据库 [图片] 完整的代码给大家贴出来 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') //引入mysql操作模块 const mysql = require('mysql2/promise') cloud.init() // 云函数入口函数 exports.main = async(event, context) => { //链接mysql数据库的test库,这里你可以链接你mysql中的任意库 try { const connection = await mysql.createConnection({ host: "你的服务器ip", database: "操作那个数据库", user: "mysql使用后名", password: "mysql密码" }) const [rows, fields] = await connection.execute('SELECT version();') return rows; } catch (err) { console.log("链接错误", err) return err } } [代码] 记得把上面的host,database,user,password 替换成你自己的。 五,上传并部署云函数 [图片] 部署成功 [图片] 这里有一点需要注意,就是你不能用云函数链接你本地mysql数据库,因为上传云函数以后,是上传到里微信服务器,没有办法调用到你本地mysql到,除非你设置下本地mysql可以被外界访问,或者使用你自己服务器上的mysql数据库。 [图片] 这样就可以成功的使用微信小程序链接我们的mysql数据库了。 到这里我们点用自己定义的mysql云函数,就可以成功的链接我们的mysql数据库了。 [图片] 是不是很简单。 更多关于云开发的知识,可以翻看我之前的文章,也可以看我录制的视频讲解 视频讲解 https://edu.csdn.net/course/detail/9604 有任何关于小程序的问题都可以加石头哥微信2501902696(备注小程序) 我们下一节给大家讲解使用小程序云开发实现邮件的发送功能。敬请期待。
2019-08-03 - 新能力解读:页面间通信接口
在 2019 年 7 月 2 日的小程序基础库版本更新 v2.7.3 中,小程序新增了一个页面间通讯的接口,帮助我们的小程序完成不同页面间数据同步的功能。 页面间通信接口能干嘛? 在 v2.7.3 之前,小程序不同页面间的大批量数据传递主要有两种: 借助诸如 Mobx 、Redux 等工具,来实现不同页面间的数据传递。 借助小程序提供的 storage ,来完成在不同页面间数据的同步。 前者需要引入一些第三方工具库,从而提升了整个应用的大小,同时,引入的工具也带来了学习生本。而后者则是基于小程序提供的存储,先将数据存入存储,再到另外一个页面去读取,如果数据涉及到了多个页面,则可能会导致数据的紊乱。 新的页面间通信接口则直接解决了上述的两个问题,你可以直接使用 API 在两个页面之间传递数据,再也无需担心数据的紊乱。 新增的页面间通信接口应当如何使用? 关于页面间通信接口的使用非常简单。 这里,我们假设存在 A 和 B 两个页面,其中 A 是首页,B是详情页。 A 向 B 传递数据 如果你需要从首页向详情页传递数据,则可以这样操作。 在页面 A 执行代码 [代码]wx.navigateTo({ url: 'test?id=1' success: function(res) { // 通过eventChannel向被打开页面传送数据 res.eventChannel.emit('acceptDataFromOpenerPage', { data: 'test' }) } }) [代码] 这样,当 A 跳转到 B 时,就会出发 B 当中定义的 acceptDataFromOpenerPage,并将后续的数据传递过去。 在 B 中,你可以在 onLoad 去定义 eventChannel 的相关方法 [代码]Page({ onLoad: function(option){ // 监听acceptDataFromOpenerPage事件,获取上一页面通过eventChannel传送到当前页面的数据 let eventChannel = this.getOpenerEventChannel(); eventChannel.on('acceptDataFromOpenerPage', function(data) { console.log(data) }) } }) [代码] B 向 A 传递数据 如果需要被打开的页面向打开的页面传递数据,则可以使用如下代码: 在 A 中的跳转时,加入 events 的定义,定义你自己的函数,以及对应的处理函数。 [代码]wx.navigateTo({ url: 'test?id=1', events: { someEvent: function(data) { console.log(data) } }, }) [代码] 然后在 B 中,调用如下代码来发信息 [代码]Page({ onLoad: function(option){ const eventChannel = this.getOpenerEventChannel() eventChannel.emit('someEvent', {data: 'test'}); } }) [代码] 这样,就可以在 B 页面将数据传回到 A 页面了。 页面间通信接口使用注意事项? 在使用页面间通信接口时需要注意两点: 该功能从基础库 2.7.3 开始支持,低版本需做兼容处理。
2019-09-21 - 小程序电商的7种粉丝裂变方法!
电商小程序是很容易裂变和吸粉的,这也是很多商家开发电商小程序的主要原因。目前的电商小程序分为哪些类型,有什么裂变的方式呢?下面给大家总结一下小程序电商裂变的方法。 [图片] 电商类小程序目前主要分为3种: 第一种:内容电商变现 基于公众号资源开通小程序,围绕公众号用户进行商品销售变现。这一类商业模式在使用时会更直观的发现一些问题,作为能够打通微信公众号和小程序的用户行为分析产品,能够直观发现哪些公众号用户进入了小程序,并且能够定位到具体的人。 第二种:电商企业开展新渠道 本身在发展电商业务的企业开始尝试小程序渠道的销售效果,但难点在于缺少电商平台的引流机制,通过微信生态内的渠道推广流量比较贵,而且用户不精准。 第三种:线下门店发展线上通路 很多线下门店也开展了小程序,甚至包括餐饮项目,线下门店发展小程序渠道主要作用在于提供线上线下的流量枢纽,线下流量转到线上持续盈利,线上流量转到线下拓展客源。而无论哪种,对小程序运营者来说,流量都是当下需要解决的重要问题。公众号内容电商流量总有干涸的时候,电商小程序需要学会自我造血。 电商小程序的7种裂变方式: 1.拼团送福利 拼团的典型代表是拼多多,一人发起拼团邀请好友一起购买获得低价,很多社区团购在用这种思路做营销。而通过预售、抽奖、发起人免单等变化形式又衍生出抽奖团、预售团、分销团等不同形式; 2.砍价 砍价也是很常见的裂变方法,一人发起砍价,邀请好友帮忙降低价格; 3.分销 分销是利用用户的社交关系,让参与者不是享受到优惠,而是能够赚到佣金; 4.推荐有礼 比较典型的像瑞幸咖啡的邀请新用户可免费得一杯咖啡的活动形式,在设计活动时需要注意让推荐者与被推荐者都能获得福利; 5.拆红包 拆红包的活动套路是发给用户1个红包,需要有一定用户帮助他拆才能得到; 6.限时折扣 限时折扣裂变的意义在于用户在购买后生成一张抢购海报,邀请其他用户参与就可以得到佣金等其他福利; 7.分享发红包 类似于外卖平台的下单后发红包功能 以上,是我们总结的关于电商小程序裂变的7种常见玩法,在具体设计上,需要考虑奖品的诱惑力、用户付出的成本、用户参与环节等相关因素。 最后分享一下我收集的小程序: https://www.sucaihuo.com/source/0-0-266-0-0-0
2019-08-04 - 从0到1开发一个小程序cli脚手架(二) --版本发布/管理篇
接上文 《从0到1开发一个小程序cli脚手架(一)–创建页面/组件模版篇》 github地址:https://github.com/jinxuanzheng01/xdk-cli 觉得有用的朋友帮忙给项目一个star,谢谢 上文大家应该大致学会了怎么搭建一个cli脚手架,包括实现了一个快速生成启动模版的功能,本质上作为脚手架应该可以做更多的事情,本篇文章会实现一些新的功能,例如:自动发布体验版,版本号控制,环境变量控制 痛点 不知道大家有没有一天发多次版本或者一天给多个小程序发版的经历,按照微信正常的发布流程,需要: 修改版本号/版本描述 修改发布环境 点击微信开发者工具上传体验版 提交审核 确认环境/版本 点击发布 其中所有的1,2步为手工修改config文件,第5步是确认手工修改config文件的正确性,毕竟人总会犯错,作者表示就干过线下环境发布到测试环境的事情,而且这是在做了第5步的情况下,很遗憾没有仔细核对 为了不再次发生同样的事情导致引咎辞职,那么有没有更好的方法呢 ?自然是有的,既然人不可靠,那么直接把它流程化自然就可以了 准备工作 最好阅读了上篇文章《从0到1开发一个小程序cli脚手架(一)–创建页面/组件模版篇》,并搭建了一个简单的demo 需要了解微信小程序提供的一些cli能力, 点击这里 Let‘ go 后续的很多流程上的实操代码为了缩短篇幅会以伪代码的形式来进行描述,强烈推荐先阅读上文,如果需要更详细的实操代码请去github仓库查看 实际效果图:[图片] 梳理流程 识别命令行 询问问题,拿到版本号和版本描述 调用微信提供的cli能力,进行体验版上传 是不是发现非常简单,事实也是如此,整个功能做下来也就60行代码 ~ 目录结构 项目结构分为入口文件,配置文件 [代码]- lib - publish-weapp.js - index.js - config.js - node_modules - package.json [代码] config.js用来记录一些基础常量和默认项的配置,例如项目路径,执行路径等 [代码]module.exports = { // 根目录 root: __dirname, // 执行命令目录路径 dir_root: process.cwd(), // 小程序项目路径 entry: './', // 项目编译输出文件夹 output: './', } [代码] 识别命令行 在入口文件(index.js)处利用第三方库 commander 识别命令行参数,同时作为路由进行任务分发, [代码]#!/usr/bin/env node const version = require('./package').version; // 版本号 /* = package import -------------------------------------------------------------- */ const program = require('commander'); // 命令行解析 /* = task events -------------------------------------------------------------- */ const publishWeApp = require('./lib/publish-weapp'); // 发布体验版 program .command('publish') .description('发布微信小程序体验版') .action((cmd, options) => publishWeApp(); /* = main entrance -------------------------------------------------------------- */ program.parse(process.argv) [代码] 创建交互命令 接下来是一个QA环节,这时候需要根据自己的实际需要安排自己需要的命令,可以回想一下要解决的问题: 修改版本号/版本描述 修改发布环境 根据cli自动上传体验版 大概得出这样一个队列: [代码]function getQuestion({version, versionDesc} = {}) { return [ // 确定是否发布正式版 { type: 'confirm', name: 'isRelease', message: '是否为正式发布版本?', default: true }, // 设置版本号 { type: 'list', name: 'version', message: `设置上传的版本号 (当前版本号: ${version}):`, default: 1, choices: getVersionChoices(version), filter(opts) { if (opts === 'no change') { return version; } return opts.split(': ')[1]; }, when(answer) { return !!answer.isRelease } }, // 设置上传描述 { type: 'input', name: 'versionDesc', message: `写一个简单的介绍来描述这个版本的改动过:`, default: versionDesc }, ] } [代码] 最后获得的json对象,是这个样子: [代码]{isRelease: true, version: '1.0.1', versionDesc: '这是一个体验版'} [代码] 确定是否发布正式版 主要是因为体验版并非完全是发行版,公司内部测试的时候也是需要发布测试用体验版的,但是又涉及只有正式版本修改本地版本文件,那么就只能多此一问作为区分了 设置版本号 / 设置版本信息 [图片] 设置上述如图的 体验版上的版本号和描述信息,这里有个case是只有第一个问题选择发布正式版才会让你设置版本号,测试版本使用默认版本号0.0.0,这也是区分体验版的一种方式 [代码] when(answer) { return !!answer.isRelease } [代码] 这里只设置了三个问题:是否发布正式版,设置版本号,设置上传描述,当然你也可以设置自己想做的其他事情 上传体验版 翻了下小程序的文档,大概犹如下几个关键点: 找到cli工具 小程序cli并非全局安装,需要自己去索引路径,命令行工具所在位置:macOS: [代码]<安装路径>/Contents/MacOS/cli[代码] Windows: [代码]<安装路径>/cli.bat[代码] mac的 安装路径 如果是默认安装的话,是/Applications/wechatwebdevtools.app/, 外加cli的位置是: /Applications/wechatwebdevtools.app/Contents/Resources/app.nw/bin/cli windows 的话作者表示没有这个环境,只能大家自己探索了 拼凑上传命令 官方文档给了非常详细的描述: [图片] [代码]# 上传路径 /Users/username/demo 下的项目,指定版本号为 1.0.0,版本备注为 initial release cli -u 1.0.0@/Users/username/demo --upload-desc 'initial release' # 上传并将代码包大小等信息存入 /Users/username/info.json cli -u 1.0.0@/Users/username/demo --upload-desc 'initial release' --upload-info-output /Users/username/info.json [代码] 编写上传逻辑 基本流程: 获取cli -> 获取当前版本配置 -> 问题队列(获取上传信息)-> 执行上传(cli命令)-> 修改本地版本文件 -> 成功提示 [代码]// ./lib/publish-weapp.js 文件 module.exports = async function(userConf) { // cli路径 const cli = `/Applications/wechatwebdevtools.app/Contents/Resources/app.nw/bin/cli`; // 版本配置文件路径 const versionConfPath = Config.dir_root + '/xdk.version.json'; // 获取版本配置 const versionConf = require(versionConfPath); // 开始执行问题队列 anser case: {isRelease: true, version: '1.0.1', versionDesc: '这是一个体验版'} let answer = await inquirer.prompt(getQuestion(versionConf)); versionConf.version = answer.version || '0.0.0'; versionConf.versionDesc = answer.versionDesc; //上传体验版 let res = spawn.sync(cli, ['-u', `${versionConf.version}@${Config.output}`, '--upload-desc', versionConf.versionDesc], { stdio: 'inherit' }); if (res.status !== 0) process.exit(1); // 修改本地版本文件 (当为发行版时) !!answer.isRelease && await rewriteLocalVersionFile(versionConfPath, versionConf); // success tips Log.success(`上传体验版成功, 登录微信公众平台 https://mp.weixin.qq.com 获取体验版二维码`); } // 修改本地版本文件 function rewriteLocalVersionFile(filepath, versionConf) { return new Promise((resolve, reject) => { fs.writeFile(filepath, jsonFormat(versionConf), err => { if(err){ Log.error(err); process.exit(1); }else { resolve(); } }) }) } [代码] 注意这里需要在项目根目录下创建一个xdk.version.json的版本文件,详细配置说明见仓库 大致是这样的结构: [代码]// xdk.version.json { "version": "0.12.2", "versionDesc": "12.2版本" } [代码] 到这里基本就完成了上传的所有步骤,可以当前项目下键入[代码]xdk-cli publish[代码]查看一下程序是否正常运行,注意上传出错记得检查是否处于登录状态 扩展:关于版本号自增 可以看到在版本号环节使用的是list类型,而非input类型,这是因为手写版本号有写错的风险, 还是让我做选择题吧 emmm… 最终效果: [图片] 就不在这里展开了,可以下, 搜索[代码]getVersionChoices[代码]函数: https://github.com/jinxuanzheng01/xdk-cli/blob/master/lib/publish-weapp.js 解决打包工具的问题 你会发现是运行cli命令就直接发布了,那么用gulp,webpack等工具项目因为需要上传dist目录而非src的原因,需要先进行打包再进行发布: gulp -> xdk-cli publish ,将一个动作拆成了两个 遗忘了一个怎么办?纠结了 不知道大家对微信开发者工具的上传钩子还有没有印象,要实现的大概就是这么个东西, 一个上传前预处理的钩子 [图片] 这个实现起来也很简单,首先给用户的配置文件(xdk.config.js)开一个可配置项: [代码]// xdk.config.js { // 发布钩子 publishHook: { async before(answer) { this.spawnSync('gulp'); return Promise.resolve(); }, async after(answer) { this.log('发布后的钩子执行了~'); return Promise.resolve(); } } } [代码] 在publish-weapp.js中去识别钩子即可: [代码] // 前置钩子函数 await userConf.publishHook.before.call(originPrototype, answer); //上传体验版 let res = spawn.sync(cli, ['-u', `${versionConf.version}@${Config.output}`, '--upload-desc', versionConf.versionDesc], { stdio: 'inherit' }); // 后置钩子函数 await userConf.publishHook.after.call(originPrototype, answer); [代码] 当然,你需要判断下钩子是否存在,类型判断,包括需要返回给用户配置里一些基本的方法和属性,例如:日志输出,命令行执行等 我这边以gulp为例,可以看到在publsih前先执行[代码]gulp[代码],后进行发布,最后log出一行提示信息 完全符合预期 解决环境变量切换问题 解决了自动上传的问题,接下来就要解决环境变量切换的问题了,这里还要借用下刚才写的钩子函数: [代码]{ // 发布钩子 publishHook: { async before(answer) { this.spawnSync('gulp', [`--env=${answer.isRelease ? 'online' : 'stage'}`]); return Promise.resolve(); }, async after(answer) { this.log.success('发布后的钩子执行了~'); return Promise.resolve(); } } } [代码] 以gulp为例,根据之前的回答的结果answer.isRelease,判断是否为使用正式环境 如果你没有使用编译工具,也可以提出一个env的config文件,使用fs模块直接重写该环境变量 下面是一串伪代码: [代码]目录结构 - app (小程序项目) - page - app.json ... - env.js - xdk.config.js - xdk.version.js - envConf.js // envConf.js module.exports = { ['online']: { appHost1: 'https://app.xxxxxxxxx.com', appHost2: 'https://app.xxxxxxxxx.com', }, ['stage']: { appHost1: 'https://stage.xxxxxxxxx.com', appHost2: 'https://stage.xxxxxxxxx.com', } }; // xdk.config.js publishHook: { async before(answer) { let config = require('envConf.js')[answer.isRelease ? 'online' : 'stage']; fs.writeFile('./app/env.js', jsonFormat(jsonConf), (err) => { if(err){ this.log.error('写入失败') }else { this.log.success('写入成功); } }); return Promise.resolve(); } [代码] 打包工具的问题还有环境变量的问题,我都没有写在cli里面,是因为不同项目之间对环境变量的写法,格式都不尽相同,具体要怎么做还是要留给开发者自己来确定吧,这样看起来更灵活一些 最后 总之利用脚手架解决了从发布到上线的一连串问题,使得不再担心因为切换环境导致线上bug,也不再担心写版本号写错的问题,确认线上环境这个环境也变成了一个非强需求的事情 整个上线流程也只需要:xdk-cli publish -> 提交审核即可,而且整个代码也并不复杂,publish-weapp.js整个文件算上注释也就60行代码,何乐不为呢? 注:下篇会讲一下如何做自定义指令,帮助小伙伴可以更自由的适配不同的项目~
2019-08-05 - 小程序内用户帐号登录规范调整和优化建议
为更好地保护用户隐私信息,优化用户体验,平台将会对小程序内的帐号登录功能进行规范。本公告所称“帐号登录功能”是指开发者在小程序内提供帐号登录功能,包括但不限于进行的手机号登录,getuserinfo形式登录、邮箱登录等形式。具体规范要求如下: 1.服务范围开放的小程序 对于用户注册流程是对外开放、无需验证特定范围用户,且注册后即可提供线上服务的小程序,不得在用户清楚知悉、了解小程序的功能之前,要求用户进行帐号登录。 包括但不限于打开小程序后立即跳转提示登录或打开小程序后立即强制弹窗要求登录,都属于违反上述要求的情况; 以下反面示例,在用户打开小程序后立刻弹出授权登录页; [图片] 建议修改为如下正面示例形式:在体验小程序功能后,用户主动点击登录按钮后触发登录流程,且为用户提供暂不登录选项。 [图片] 2.服务范围特定的小程序 对于客观上服务范围特定、未完全开放用户注册,需通过更多方式完成身份验证后才能提供服务的小程序,可以直接引导用户进行帐号登录。例如为学校系统、员工系统、社保卡信息系统等提供服务的小程序; 下图案例为正面示例:校友管理系统,符合规范要求。 [图片] 3.仅提供注册功能小程序 对于线上仅提供注册功能,其他服务均需以其他方式提供的小程序,可在说明要求使用帐号登录功能的原因后,引导用户进行帐号注册或帐号登录。如ETC注册申请、信用卡申请; 如下反面示例,用户在进入时未获取任何信息,首页直接强制弹框要求登录注册ETC,这是不符合规范的。 [图片] 建议修改为如下正面示例所示形式:允许在首页说明注册功能后,提供登录或注册按钮供用户主动选择点击登录。 [图片] 4.提供可取消或拒绝登录选项 任何小程序调用帐号登录功能,应当为用户清晰提供可取消或拒绝的选项按钮,不得以任何方式强制用户进行帐号登录。 如下图所示反面示例,到需要登录环节直接跳转登录页面,用户只能选择点击登录或退出小程序,这不符合登录规范要求。 [图片] 建议修改为下图正面示例形式,在需帐号登录的环节,为用户主动点击登录,并提供可取消按钮,不强制登录。 [图片] 针对以上登录规范要求,平台希望开发者们能相应地调整小程序的帐号登录功能。如未满足登录规范要求,从2019年9月1日开始,平台将会在后续的代码审核环节进行规则提示和修改要求反馈。
2019-07-20