- 小程序实现看一看视频滑动切换
最终效果 由于很多人不知道看一看还有个视频功能,所以这里先让大家看下我们最终要完成的效果。 它的入口在 发现 -> 看一看 -> 精选 -> 随便找个视频点进去即可。 [图片] 初步想法 由效果可以看出,其实就是需要监听视频的滚动,当超出可视区范围多少px,就切换到下一个视频。 要实现这个功能,大多数人的想法都是:监听scroll 事件后,在调用目标元素的getBoundingClientRect()方法,得到它对应于视图的坐标,再判断是否在可视区域之内,然后切换视频。 缺点 但是这样做的缺点是:调用目标元素的getBoundingClientRect 是会触发重排的,尤其是元素一多起来,调用所有元素的getBoundingClientRect得到信息在进行判断所以很容易造成性能问题。而且这种切换计算的逻辑会写的非常复杂,可以自行脑补一下。 所以我们要换个思维,不要通过监听scoll事件去计算目标元素距离顶部或者底部距离。而应该是直接监听当前目标元素是否还在可视区域内。当离开可视区域的时候,切换到下一个。 整理完大致思路之后,终于要开搞了。 [图片] 实现 前面已经分析了要通过监听当前目标元素是否还在可视区域内来做切换的动作,那么有什么API是可以用来做这件事的呢? 答案是: IntersectionObserver API 这个API是用来观察目标元素与指定元素交集的变化。当交集 < 0 的时候,说明不在指定元素区域内。当目标元素进入或者退出指定元素的时候,会执行相应的回调函数。所以我们可以通过这个API注册一个回调函数用于切换视频。 在小程序里,同样提供了这个API,是IntersectionObserver。 有了这个API,就可以开始干活了。由于我们这个区域是一个滚动区域,所以我用了scoll-view。 index.wxml 文件 [代码]<scroll-view> <view wx:for="{{ videos }}" wx:for-index="idx" wx:for-item="videoItem"> <!-- <view class="{{ currentPlayVideoIndex === idx ? 'active test' : 'test'}}" data-index="{{ idx }}" id="{{ videoItem.video_id }}"> {{ idx }}dddd</view> --> <span class="{{ currentPlayVideoIndex === idx ? 'active' : ''}}">{{ idx }}ddddddd </span> <video id="{{ videoItem.video_id }}" data-index="{{ idx }}" preload src="http://wxsnsdy.tc.qq.com/105/20210/snsdyvideodownload?filekey=30280201010421301f0201690402534804102ca905ce620b1241b726bc41dcff44e00204012882540400&bizid=1023&hy=SH&fileparam=302c020101042530230204136ffd93020457e3c4ff02024ef202031e8d7f02030f42400204045a320a0201000400" class='video-item' muted controls> </video> </view> </scroll-view> [代码] index.wxss [代码].video-item { height: 450px; } .test { width: 100%; height: 450px; border: 1px solid red; padding: 30px; } .active { color: pink; } [代码] [代码]Page({ /** * 页面的初始数据 */ data: { videos: [{ video_id: 'mpVideo0', url: 'http://wxsnsdy.tc.qq.com/105/20210/snsdyvideodownload?filekey=30280201010421301f0201690402534804102ca905ce620b1241b726bc41dcff44e00204012882540400&bizid=1023&hy=SH&fileparam=302c020101042530230204136ffd93020457e3c4ff02024ef202031e8d7f02030f42400204045a320a0201000400', }, { video_id: 'mpVideo1', url: 'http://mpvideo.qpic.cn/tjg_2394158861_50000_01730a9db3924ffa98201662d51615ed.f10002.mp4?dis_k=23639703f249e3c59cf674369cfcac86&dis_t=1562297977', }, { video_id: 'mpVideo2', url: 'http://mpvideo.qpic.cn/tjg_2394158861_50000_01730a9db3924ffa98201662d51615ed.f10002.mp4?dis_k=23639703f249e3c59cf674369cfcac86&dis_t=1562297977', }, { video_id: 'mpVideo3', url: 'http://mpvideo.qpic.cn/tjg_2394158861_50000_01730a9db3924ffa98201662d51615ed.f10002.mp4?dis_k=23639703f249e3c59cf674369cfcac86&dis_t=1562297977', }, { video_id: 'mpVideo4', url: 'http://mpvideo.qpic.cn/tjg_2394158861_50000_01730a9db3924ffa98201662d51615ed.f10002.mp4?dis_k=23639703f249e3c59cf674369cfcac86&dis_t=1562297977', }, { video_id: 'mpVideo5', url: 'http://mpvideo.qpic.cn/tjg_2394158861_50000_01730a9db3924ffa98201662d51615ed.f10002.mp4?dis_k=23639703f249e3c59cf674369cfcac86&dis_t=1562297977', }, { video_id: 'mpVideo6', url: 'http://mpvideo.qpic.cn/tjg_2394158861_50000_01730a9db3924ffa98201662d51615ed.f10002.mp4?dis_k=23639703f249e3c59cf674369cfcac86&dis_t=1562297977', }, { video_id: 'mpVideo7', url: 'http://mpvideo.qpic.cn/tjg_2394158861_50000_01730a9db3924ffa98201662d51615ed.f10002.mp4?dis_k=23639703f249e3c59cf674369cfcac86&dis_t=1562297977', }], currentPlayVideoIndex: 0, isActive: true }, /** * 生命周期函数--监听页面加载 */ onLoad: function (options) { // onLoad 的时候立刻调用handleVideoScroll // 对视频进行监听 this.handleVideoScroll(); // cgi请求,用于获取videos 的数据,由于是demo演示,我直接写死了videos... }, /** * 页面上拉触底事件的处理函数 */ onReachBottom: function () { }, controlVideos: function (res) { console.log('调用controvideos', res); }, handleVideoScroll: function () { const currentId = this.data.videos[this.data.currentPlayVideoIndex].video_id; // 关键代码 // relativeToViewport 这里指定对比的就是viewport,viewport的意思就是document中的可视区域 this.observerObj = wx.createIntersectionObserver().relativeToViewport(); console.log('listen ' + currentId); // 监听目标视频跟viewport相交区域的变化 this.observerObj.observe(`#${currentId}`, this.controlVideos); } }) [代码] copy上面的代码进入小程序,你就会看到这样一个界面。 [图片] 其中外面的一圈表示的是viewPort,里面一层就是我们现在正在监听的视频,我用右上角的粉色字体来标记了,它的回调函数是controlVideos。当目标视频进入或者退出viewport的时候,controlVideos就会执行。 onLoad的时候执行了handleVideoScroll,这时候开始对目标视频进行监听,此时目标元素在viewport内,所以会调用controVideos,打印出相关信息。 [图片] 其他的字段先不说,其中的intersectionRatio表示了他们相交的比例。其中1表示完全在viewport内,0表示不在viewport内。 如果我持续去滚动第一个视频,直到它看不到了,就会看到控制台打印出 [图片] 这时候的intersectionRatio = 0,代表已经不在viewport内了,所以我们就可以将currentPlayIndex 切换到下一个了。 切换代码如下: [代码] controlVideos: function (res) { const { currentPlayVideoIndex } = this.data; console.log('当前currentIndex', currentPlayVideoIndex) const currentId = this.data.videos[currentPlayVideoIndex].video_id; if (res && res.intersectionRatio > 0) { // 视频在可视区域内,播放视频 wx.createVideoContext(currentId).play(); console.log("play" + currentPlayVideoIndex) } else { // 需要切换视频的时候,将当前视频暂停播放 // 并且通过handleVideoScroll 来播放下一个视频 wx.createVideoContext(currentId).pause(); // 切换到下一个视频 this.setData({ 'currentPlayVideoIndex': currentPlayVideoIndex + 1 }, () => { // 注意切换完成之后,还需要在调用handleVideoScroll 来对下一个视频进行绑定 this.handleVideoScroll(); }); } }, [代码] 到这一步,应该就可以看到这样的向下切换的效果了。 [图片] 但是,我们现在只是做下向下滚动的切换。那么向上的呢?要做向上滑动的切换,首先要知道视频是在向下还是向上滑动。这里有个字段可以帮助我们识别:boundingClientRect 。 它表示的是目标元素相对与viewport的节点信息。当视频向上滚动的时候,它距离viewport的top值为负,向下滚动的时候,为正值。 [图片] 有了这个字段,我们就可以通过判断向上还是向下的滚动,来切换视频了。 [代码] controlVideos: function (res) { const { currentPlayVideoIndex } = this.data; console.log('当前currentIndex', currentPlayVideoIndex) const currentId = this.data.videos[currentPlayVideoIndex].video_id; if (res && res.intersectionRatio > 0) { // 视频在可视区域内,播放视频 wx.createVideoContext(currentId).play(); console.log("play" + currentPlayVideoIndex) } else { // 需要切换视频的时候,将当前视频暂停播放,并且通过handleVideoScroll 来播放下一个视频 wx.createVideoContext(currentId).pause(); // 当top < 0的时候,说明是在向上滑动,这时候currentPlayVideoIndex 需要加1 if (res.boundingClientRect.top < 0) { if (currentPlayVideoIndex < this.data.videos.length - 1) { this.setData({ 'currentPlayVideoIndex': currentPlayVideoIndex + 1 }, () => { // 同时解绑第一个视频,保证同一个时间只监听一个视频 this.observerObj.disconnect(); this.handleVideoScroll(); }); } } else { // 当top > 0的时候,说明是在向下滑动,这时候currentPlayVideoIndex 需要减1 if (currentPlayVideoIndex - 1 < 0) { return; } this.setData({ 'currentPlayVideoIndex': this.data.currentPlayVideoIndex - 1 }, () => { this.observerObj.disconnect(); this.handleVideoScroll(); }) } }, [代码] 但是我们的产品在体验的过程中,会提出并不是完全看不见了才去切换,可能想要还剩个150px就切换了,所以我这里要对viewport调整一下 [代码] this.observerObj = wx.createIntersectionObserver().relativeToViewport({ top: -300, bottom: -300 }); [代码] 完成之后,你就可以缓缓的滑动你的视频,实现视频切换的效果了。可以看到当视频差不多被遮住不到一半,就开始切换了。 [图片] 总结 整个过程其实就是好好利用了IntersectionObserver这个API而已。当然现在只是一个非常简单的实现,性能问题,以及快读滑动的情况都无法应对,我们下一篇在接着~。
2019-08-15 - js 控制checkbox 选中
<form bindsubmit="formSubmit"> <checkbox-group name="itemId"> <view class="tp-vote-content" wx:if="{{voteStatus == '0'}}" wx:for="{{itemList}}" bindtap='selectCheckBox'> <checkbox value="{{item.itemId}}" /> <text class="tp-vote-name">{{item.itemId}}.{{item.name}}</text> </view> <view class="tp-vote-complay"> 表演单位:{{item.detail}}</view> </view> </checkbox-group> </form> 如上所示的代码,checkbox 的可点范围太小 ,想在外层的view 添加点击事件,控制checkbox 的选中取消。该怎么操作,求给个思路 谢谢了
2017-09-11 - 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 - 多个input框怎么输入完跳到下一个输入框
[图片] 我一开始用判断输入框是否有内容,有的话就让下一个输入框获取焦点 可以达到这种效果,但是在真机上运行效果就不是很好,到一下个输入框的时候键盘就会被重新唤起,用户体验不是很好,有大神帮我解决下嘛?或者给点思路
2018-07-25 - 求助!wx.downloadFile下载文件的问题
- 需求的场景描述(希望解决的问题) @卢霄霄 大佬前辈们求助 需求是这样的,我有一个页面是有个用户列表,里面有大约20到30个人,他们有的有头像,有的没头像,然后外派公司的后端返的我是object的文件,他让我下载用临时路径显示,我下到一定数就不再走wx.downloadFile了,我们公司以前都是返的url路径所以这类问题我没遇到过,我的猜想是页面有最大下载限制数貌似,想请教下各位大佬原因是不是有限制 - 希望提供的能力 解决思路
2019-01-15 - 【技巧】swiper仿tab切换
大家好,上次给大家分享了swiper多图片的解决方案:https://developers.weixin.qq.com/community/develop/doc/000068ff25ccf0bae4e76eab156c04 今天再给大家分享一个关于swiper的小技巧,利用swiper仿tab切换。 相信大家在app或浏览器上阅读新闻时,比如今日头条,会有这样一个场景,左右滑动的时候可以切换不同栏目,体验非常好,但是小程序好像没有提供相关组件,如果想实现这种效果该怎么做呢今天就给大家介绍一下在小程序里是怎么实现的。 首先先看下效果 [图片] 实现原理很简单,利用小程序swiper再配合scroll-view就能实现,不过这里面有几点需要注意一下: 1.scroll-view一定要给一个高度,不然会有问题; 2.切换的时候只显示当前的swiper-item里的内容,其它swiper-item里的内容可以先隐藏掉,这是因为如果你的swiper-item里的图片太多的话可能会造成页面回收,因为新闻列表大多是图文列表,而tab经常是不止两个的,可能是7、8个或更多,如果每个tab都显示的话到时上拉加载页面会非常庞大,所以这里我建议不用显示的内容先隐藏,记住是swiper-item里的内容不是swiper-item,到时切换回来时再重新渲染,如果你要保存滚动的位置还要做其它的一些处理,这里就不仔细讲解了; 3.这里适用的是整个页面都是tab切换的,如果只是在页面的某处实现tab切换,还要考虑高度的问题,加载数据的时候根据数据个数长度来计算高度,每次加载数据都要计算高度,切换到不同的tab也是,这部分比较麻烦,因为要计算,不过并不难,只要 计算正确的话是没有问题的; 大概就是这样,基本实现思路,大家可以根据这个思路去拓展,在上面加上自己的功能,over! 代码片段:https://developers.weixin.qq.com/s/89OO1smX736d 系甘先,得闲饮茶
2019-02-26 - [拆弹时刻]小程序canvas生成海报(二)--优化方案
[图片] 海报生成速度缓慢问题的优化 微信头像在app.js中预先加载缓存 多图片异步加载 流程中断处理 二次授权失败的处理 请求或者下载图片失败处理 保存图片可被压缩 海报生成速度缓慢问题的优化 原因分析: 主要的时间消耗在于getImageInfo网络请求获取头像和下载图片获得临时地址的过程,可以看到海报中有3张图片(微信头像、主图、动态二维码(对应不同新闻的ID))需要下载,接下来主要就是对这3张图的优化 微信头像在app.js中预先加载缓存 [代码]//app.js //可以在app.js中使用小程序默认的全局变量,将头像在加载的时候预先缓存 App({ onLaunch: function () { // 获取用户信息 wx.getSetting({ success: res => { if (res.authSetting['scope.userInfo']) { // 已经授权,可以直接调用 getUserInfo 获取头像昵称,不会弹框 wx.getUserInfo({ success: res => { this.globalData.userInfo = res.userInfo; //从返回值中获取微信头像地址 let WxHeader = res.userInfo.avatarUrl; wx.getImageInfo({ src: WxHeader,//下载微信头像获得临时地址 success: res => { //将头像缓存在全局变量里 this.globalData.avatarUrlTempPath = res.path; }, fail: res => { //失败回调 } }); } }) } } }) }, globalData: { userInfo: null, //如果用户没有授权,无法在加载小程序的时候获取头像,就使用默认头像 avatarUrlTempPath: "./images/defaultHeader.jpg" } }) [代码] 大致思路是: 加载App.js的时候 ==> getSetting(判断是否授权) ==> getUserInfo(获取头像) ==> getImageInfo(生成临时地址) 将需要的网络请求在加载小程序的时候就异步完成,提前将临时地址缓存在全局变量globalData中,这样当用户进入新闻页面,点击生成海报的时候就不需要在请求微信头像,缩短了不少时间。 注意: 如果用户一开始没有微信授权,生成海报时又必须要用户头像不能使用默认的话,那就只能老老实实走之前的流程了。 多图片异步加载 [代码]let num = 0; //下载图片计数器,假设一共三张图片 //下载图片1 wx.getImageInfo({ src: image_1, success: function (res) { //判断是否是最后一张图 if (num >= 2) { console.log("图片全部下载完毕,可以绘制海报") } else { //如果不是最后一张图则+1,继续 num++; } }, fail: function (res) { //失败回调 } }); //下载图片2 wx.getImageInfo({ src: image_2, success: function (res) { //判断是否是最后一张图 if (num >= 2) { console.log("图片全部下载完毕,可以绘制海报") } else { //如果不是最后一张图则+1,继续 num++; } } }); ...... [代码] 这里智库君一开始是使用promise的同步办法,但是发现3张图片阻塞严重,如果一张图片下载过慢,就会影响整个海报生成时间,所以可以改为添加计数器判断的异步方法。 当海报生成需要多张图片的时候,完全可以异步的方式加载他们,通过计数器判断是否是最后一张。 流程中断处理 [图片] 从图中可以看出,整个海报生成过程有二次授权:用户信息授权获取头像和保存相册授权,非常可能因为用户的误点或者拒绝而导致流程中断。 主要分为二种情况: 需要的图片没有拿到,我们可以采取使用默认图片的方式替代。 保存相册授权被拒绝,我们可以提示用户“截图保存”,由于当前版本6.7.2+的**wx.openSetting()**被限制(无法直接被调用),如果必须要相册权限,我们可以通过showModal触发。 API/组件名称 终端类型 微信版本 触发方法 openSetting 6.7.2 2.3.0 showModal [代码]// 关于 openSetting 的调用方法 wx.showModal({ title: '相册权限', content: '需要你提供保存相册权限', success: function (res) { if (res.confirm) { wx.openSetting({ success(settingdata) { console.log(settingdata) if (settingdata.authSetting['scope.writePhotosAlbum']) { console.log('获取 相册 权限成功,给出再次点击图片保存到相册的提示。'); } else { console.log('获取 相册 权限失败,给出不给权限就无法正常使用的提示') } } }) } } }) //获取相册权限的流程处理 wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath, //canvasToTempFilePath API生成的临时地址 success: function (data) { console.log("提示图片保存成功"); }, fail: function (err) { console.log(err); if (err.errMsg === "saveImageToPhotosAlbum:fail auth deny") { console.log("当初用户拒绝,再次发起授权") //调用上面说到的方法 wx.openSetting } else { console.log("提示:请截屏保存分享"); } }, complete(res) { console.log(res); } }) [代码] [图片] 保存图片可被压缩 小程序官方提供了一个API可以设置用户保存图片的质量,仅针对JPG。目前不完全确定:压缩会不会导致额外的性能开销而延长保存时间,自己测试下来 100%、80%、60% 保存时间上没有明显区别。 属性 默认值 说明 最低版本 quality 1.0 图片的质量,取值范围为 (0, 1] 1.7.0 [代码]wx.canvasToTempFilePath({ fileType: 'jpg', canvasId: 'canvasId', quality:0.8, //设置JPG保存质量 80% success: res => { }, fail:res => { } }, this) [代码] 官方文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/wx.canvasToTempFilePath.html?search-key=canvasToTempFilePath [图片] [代码片段]Canvas生成海报实战demo demo的微信路径:https://developers.weixin.qq.com/s/Q74OU3m57c9x demo的ID:Q74OU3m57c9x 如果你装了IDE工具,可以直接访问上面的demo路径 通过代码片段将demo的ID输入进去也可添加: [图片] [图片] 如果智酷君的分享能够帮助到你,或者想持续获得最新的全栈攻略 可以搜索公众号 Geek_Club 或者 智酷方程式 扫描二维码关注公众号哟👇👇👇 [图片]
2019-06-11 - 小程序拖动问题
现在小程序有个组件叫做movable-view,是挺好用的,很方便。但是,如果想要利用这个组件在商城里做出像客服按钮在整个屏幕上任意拖动时,就必须要求movable-area是占着整个屏幕的,而且层数必须在其他容器之上,那么这样子就会导致其他容器的点击事件无法触发。问题来了,有思路解决这种必须要按拖动按钮且不能遮挡住其他容器的方法吗?还是说不用官方组件有其他拖动脚本?
2017-11-01 - http上传云文件图片跨域的另一种解决方案
vue做个后台管理,用微信提供的http操作云储存上传文件会出现跨域问题,我只想搭个前端就能直接上传到云文件,不想再搭个后台中转。 换个思路,云函数是可以直接请求的,云函数里是可以直接上传图片到云文件的 由上可得 可以把图片数据传到云函数在云函数中上传图片 然而 测试发现云函数调用时有最大数据限制,大概十几kb就报错了,这肯定时不行的 但是 数据库新增是可以直接操作的, 来一波骚操作,先把图片数据转换成base64的字符串上传到云数据库,把_id传给云函数,让云函数从数据库中取出图片的数据,转换成 [代码]new Buffer(base64Data, 'base64') [代码] 哈哈哈哈哈哈哈哈哈哈哈,完美上传图片 然而事情并没有那么简单~~ 上传到云数据库时也有数据大小限制的 经过测试 base64字符串在500000字符长度是可以的(大概400kb) 在600000字符长度就会报错, 所以我们还要对上传的图片数据做分批上传 以上测试都没有错误截图了,只有已经可以完美上传的代码(请勿完全复制,根据你项目实际情况修改) [代码] export function add(token, datastr) { const params = { "env": envid, "query": "db.collection(\"testadd\").add({data:"+ datastr + "})", } // console.log("==params==" + JSON.stringify(params)) var url = 'api/tcb/databaseadd?access_token=' + token; return request({ url: url, method: 'POST', data: params }) } addObj({ commit },param) { return new Promise((resolve, reject) => { var accessToken=state.token if(accessToken==null){ accessToken=getToken() } add(accessToken,JSON.stringify(param)).then(response => { // // console.log('上传数据返回结果',response) resolve(response) if(response.errcode == 0){ // if(response.data[0] != null){ // resolve(JSON.parse(response.data[0])) // }else{ // resolve(null) // } } }).catch(error => { reject(error) }) }) }, //这是html中的 <input type="file" @change="handleChange" ref="fileInput1" accept="image/*"> //input上传文件的回调 handleChange(info) { const file = this.$refs.fileInput1.files[0] const fr = new FileReader() var self = this fr.onload = (e) => { try { console.log('file', fr) // self.imgData = fr.result self.imgData = {type:"Buffer", data: fr.result} } catch (error) { self.$message.error(`${file.name} 打开失败`); } } fr.readAsDataURL(file) info.target.value= "" }, //点击确定上传按钮回调 onModalOk(){ var imgData = self.imgData.data var splitCount = 500000 //长度大于一定时就要分断上传 // if(imgData.length > splitCount){ // imgData = imgData.substring(0, splitCount) // } var count = Math.ceil(imgData.length / splitCount) //要上传的次数 var hasAdd = 0 var idList = [] for (let index = 0; index < count; index++) { const subData = imgData.substring(splitCount * index, splitCount * (index + 1)); self.$store .dispatch("user/addObj", {name: 'testName.png', fileStream: subData, index: index}) .then(response => { hasAdd += 1 idList.push(response.id_list[0]) console.log('当前hasAdd', idList, hasAdd, count) if(hasAdd == count){ self.$store .dispatch("user/updateImgObj", {id: idList}) .then(response2 => { self.listLoading = false console.log('关闭返回的结果2', response2) }).catch(error => { self.listLoading = false self.$message.error('上传失败'); }) } }).catch(error => { self.listLoading = false self.$message.error('上传失败'); }) } } [代码] 然后是云函数中的代码: [代码] // 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init() const db = cloud.database() const _ = db.command // 云函数入口函数 exports.main = async (event, context) => { const wxContext = cloud.getWXContext() var arr = [] for (var i = 0; i < event.id.length; i++){ var data = await db.collection("testadd").doc(event.id[i]).get() arr.push(data.data) } arr.sort(function(a, b){ return a.index - b.index }) var base64Data = '' for (var i = 0; i < arr.length; i++){ base64Data += arr[i].fileStream } // return data base64Data = base64Data.replace(/^data:image\/\w+;base64,/, ""); var result = await cloud.uploadFile({ cloudPath: arr[0].name, fileContent: new Buffer(base64Data, 'base64') //{ type: 'Buffer', data: data.fileStream}, }) // var result = {} // result.data = base64Data' //上传完成后删除数据库中的数据 for (var i = 0; i < event.id.length; i++) { await db.collection("testadd").doc(event.id[i]).remove() } return result } [代码] 研究了一天,上传图片这块微信的文档里只是简单说了上传格式,却没有例子,一次次尝试才测试出什么样的格式存到云函数中是有效的图片,分享给广大有需求的程序员,减少需要研究花费的时间
2019-12-06 - 主动发送推送
自从小程序出现之后,公司就决定暂时不用app,而改为使用小程序来开发产品,所有功能模块都可以在小程序上实现,除了通知体系,现在小程序还是无法主动向用户发送通知,这个让我们的整个产品,不管是从产品设计还是产品架构方面,都缺失了很重要的一块。不知道小程序现在是否有提供基本的通知体系架构思路?
2018-09-14 - 小程序绘图篇:如何正确地给一个渐变背景按钮绘制阴影
最近在开发一个小程序项目中遇到了一个小的问题。那就是需要给一个背景是渐变色的按钮添加一层阴影,下图中红色线标记的地方。 [图片] 目的是为了让这个按钮更有立体感。刚开始做的时候觉得没有什么难度,在开发工具上也能够很好的展示;但是到了真机测试的时候,才发现原来给按钮绘制阴影的方法在真机上显示不出来。于是就有了下面我寻找解决方案的过程,最终我顺利的解决了这个问题。我把我解决这个问题的思路记录下来,方便我日后回顾,也希望能够跟大家一起交流,我们一起进步。 当我在手机上发现绘制的按钮没有阴影的时候,我就知道接下来我将可能要付出几个小时的时间去解决这个问题。我也做好了心理准备,于是就踏上了寻找解决方案的道路。 第一步:搜索问题 当我遇到这个问题的时候,我的第一反应就是先上网搜索一下看看有没有同学也遇到过这种问题。于是我就搜索小程序canvas绘图与按钮阴影相关的一些问题和资料。我的确找到了一些相关的文章和问题,但是我发现这些资料要么跟自己遇到的问题不太符合,要么搜到的问题还没有回复。 我提取了这些文章和问题中有用的点,得出了下面的结论: 小程序canvas可以绘制圆角阴影 小程序绘制阴影的API有新旧两个版本 没有说明渐变色背景的按钮是否可以直接添加阴影 第二步:分析对比 先来看一下我现在的代码是这样的: [代码]// ... 此处省去部分代码 // 绘制渐变色 _ctxc.beginPath() _ctxc.arc((SHARE_TO_CHAT_CANVAS_WIDTH - w) / 2, btn_h + r, r, 0.5 * Math.PI, 1.5 * Math.PI) // 绘制右边圆弧 _ctxc.arc((SHARE_TO_CHAT_CANVAS_WIDTH + w) / 2, btn_h + r, r, -0.5 * Math.PI, 0.5 * Math.PI) // 闭合路径 _ctxc.closePath() // 添加渐变色 const grd = _ctxc.createLinearGradient((SHARE_TO_CHAT_CANVAS_WIDTH - w) / 2 - r, btn_h, (SHARE_TO_CHAT_CANVAS_WIDTH + w) / 2 + r, btn_h) grd.addColorStop(0, first_color) grd.addColorStop(1, second_color) _ctxc.setFillStyle(grd) // 设置阴影 _ctxc.shadowOffsetX = 2 _ctxc.shadowOffsetY = 2 _ctxc.shadowColor = '#43454c' _ctxc.shadowBlur = 5 // 填充渐变色 _ctxc.fill() // 取消阴影 _ctxc.shadowOffsetX = 0 _ctxc.shadowOffsetY = 0 _ctxc.shadowColor = '#ffffff' _ctxc.shadowBlur = 0 // 绘制中间的文案 _ctxc.font = `normal normal bold 36px sans-serif` _ctxc.fillStyle = '#ffffff' _ctxc.setTextBaseline('middle') _ctxc.setTextAlign('center') _ctxc.fillText(`立即打卡`, SHARE_TO_CHAT_CANVAS_WIDTH / 2, btn_h + r) // 绘制 _ctxc.draw() [代码] 现在的代码在模拟器上可以完美的绘制出我想要的结果,如下图 [图片] 但是在真机上不行。我们可以看到在开发工具上是有阴影效果的,但是在真机上的效果却是没有阴影的。如下图 [图片] 所以我要做的就是逐步排查,要依次确定下面几个问题的答案: 问题一:看一下官方文档给出的代码在真机上能不能够显示出来阴影。 官方文档给出的代码如下: [代码]const ctx = wx.createCanvasContext('myCanvas') ctx.setFillStyle('red') ctx.setShadow(10, 50, 50, 'blue') ctx.fillRect(10, 10, 150, 75) ctx.draw() [代码] 我们直接测试一下在编辑器和手机上的效果,编辑器上的效果如下: [图片] 模拟器上是可以显示效果的,真机上的效果如下: [图片] 也是可以显示的,这说明在真机上的确是可以绘制出来阴影的。 但是我同时也注意到,这个实例的代码使用的是老的API。 [图片] 所以我再次尝试把老的API替换成新的API进行测试 代码修改如下: [代码] // ...省略部分代码 const ctx = wx.createCanvasContext('share-to-chat') ctx.setFillStyle('red') // ctx.setShadow(10, 50, 50, 'blue') // 使用新的API ctx.shadowOffsetX = 10 ctx.shadowOffsetY = 50 ctx.shadowColor = 'blue' ctx.shadowBlur = 50 ctx.fillRect(10, 10, 150, 75) ctx.draw() [代码] 这次修改之后,在编辑器上绘图没有发生什么变化。但是,在真机上却显示不出来了,只有一个红色的矩形,如下图所示: [图片] 所以到这里我们可以知道,使用新的绘制阴影的API在我这台真机上是绘制不出来阴影的。那我们就先使用老的API进行阴影的绘制,到这里这个问题已经有了一点进展了,我们继续进行排查。 问题二:是否能够绘制出来圆角的阴影 我们上面的测试绘制的阴影是矩形的阴影,如果我们绘制的是一个圆弧呢?还能绘制出来阴影吗?我们接着向下走。这次我们先绘制一个圆形,然后再给这个圆添加一个阴影。那么代码如下: [代码]// ...省略部分代码 const ctx = wx.createCanvasContext('share-to-chat') ctx.arc(100, 75, 50, 0, 2 * Math.PI) ctx.setFillStyle('red') ctx.setShadow(10, 50, 50, 'blue') // ctx.shadowOffsetX = 10 // ctx.shadowOffsetY = 50 // ctx.shadowColor = 'blue' // ctx.shadowBlur = 50 ctx.fill() ctx.draw() [代码] 这次在编辑器上的效果如下: [图片] 在手机上的效果如下: [图片] 也是可以的,那么按道理来说,如果我们使用老版本绘制阴影的API的话,我们绘制的这个圆形按钮也应该能够显示出来阴影。来我们来试一试吧。 修改最初的代码如下: [代码] // ...省略部分代码 _ctxc.setFillStyle(grd) // 设置阴影 // _ctxc.shadowOffsetX = 2 // _ctxc.shadowOffsetY = 2 // _ctxc.shadowColor = '#43454c' // _ctxc.shadowBlur = 5 // 使用老的API _ctxc.setShadow(2, 2, 5, '#43454c') // 填充渐变色 _ctxc.fill() // ...省略部分代码 [代码] 在编辑器上还是可以正常绘制阴影的,但是在手机上还是不能够显示阴影。 那么问题出现在哪里呢?这时候就要找能够绘制出来阴影和不能够绘制出来阴影这两种情况我们改变了什么。 问题3:在绘制渐变色背景按钮的同时是否可以绘制阴影 想到这里,我就猜想会不会是因为我绘制的圆形按钮使用的是渐变色背景的原因,导致我们没有办法绘制出来阴影呢?还是要实践一下,继续修改代码。这次我不使用渐变色背景,而改为使用纯色背景来测试一下。修改代码如下: [代码]// ...省略部分代码 // 添加渐变色 // const grd = _ctxc.createLinearGradient((SHARE_TO_CHAT_CANVAS_WIDTH - w) / 2 - r, btn_h, (SHARE_TO_CHAT_CANVAS_WIDTH + w) / 2 + r, btn_h) // grd.addColorStop(0, first_color) // grd.addColorStop(1, second_color) // _ctxc.setFillStyle(grd) // 使用纯色背景 _ctxc.setFillStyle('#4DAEFE') // 设置阴影 // _ctxc.shadowOffsetX = 2 // _ctxc.shadowOffsetY = 2 // _ctxc.shadowColor = '#43454c' // _ctxc.shadowBlur = 5 // 使用老的API _ctxc.setShadow(2, 2, 5, '#43454c') // 填充渐变色 _ctxc.fill() // ...省略部分代码 [代码] 这次在编辑器的显示如下图: [图片] 是有阴影的,那么在手机上效果如何呢?我们看一下: [图片] 很棒,这次终于在真机上也绘制出来的阴影了;距离我们成功也不远啦。 那么我们接下来要解决的问题就是,如何能够把阴影和渐变色背景都添加上去,这一次我们鱼和熊掌都想得到。 那怎么办呢,办法总比问题多,我们只需要在绘制好阴影的基础上再绘制一次渐变的背景就可以了。 就是先绘制一个纯色的带有阴影的按钮,然后在同样的位置我们再次绘制一个有渐变背景色的按钮,最后绘制文字就好啦。真是机智如我呀。最终的代码如下: [代码]// ...省略部分代码 // 开始绘制 _ctxc.beginPath() // 绘制左边圆弧 _ctxc.arc((SHARE_TO_CHAT_CANVAS_WIDTH - w) / 2, btn_h + r, r, 0.5 * Math.PI, 1.5 * Math.PI) // 绘制右边圆弧 _ctxc.arc((SHARE_TO_CHAT_CANVAS_WIDTH + w) / 2, btn_h + r, r, -0.5 * Math.PI, 0.5 * Math.PI) // 绘制中间的渐变矩形 _ctxc.closePath() _ctxc.setFillStyle('#000') // 设置阴影 // 新版本添加阴影 _ctxc.shadowOffsetX = 5 _ctxc.shadowOffsetY = 5 _ctxc.shadowColor = '#a5a6a7' _ctxc.shadowBlur = 5 // 旧版本添加阴影 _ctxc.setShadow(5, 5, 5, '#a5a6a7') _ctxc.fill() // 重置取消阴影 // 新版本重置阴影 _ctxc.shadowOffsetX = 0 _ctxc.shadowOffsetY = 0 _ctxc.shadowColor = '#ffffff' _ctxc.shadowBlur = 0 // 旧版本重置阴影 _ctxc.setShadow(0, 0, 0, '#ffffff') // 绘制渐变色 _ctxc.beginPath() _ctxc.arc((SHARE_TO_CHAT_CANVAS_WIDTH - w) / 2, btn_h + r, r, 0.5 * Math.PI, 1.5 * Math.PI) // 绘制右边圆弧 _ctxc.arc((SHARE_TO_CHAT_CANVAS_WIDTH + w) / 2, btn_h + r, r, -0.5 * Math.PI, 0.5 * Math.PI) // 关闭路径 _ctxc.closePath() const grd = _ctxc.createLinearGradient((SHARE_TO_CHAT_CANVAS_WIDTH - w) / 2 - r, btn_h, (SHARE_TO_CHAT_CANVAS_WIDTH + w) / 2 + r, btn_h) grd.addColorStop(0, first_color) grd.addColorStop(1, second_color) _ctxc.setFillStyle(grd) _ctxc.fill() // 立即打卡的文案字体大小 _ctxc.font = `normal normal bold 40px sans-serif` _ctxc.fillStyle = '#ffffff' _ctxc.setTextBaseline('middle') _ctxc.setTextAlign('center') _ctxc.fillText(`立即打卡`, SHARE_TO_CHAT_CANVAS_WIDTH / 2, btn_h + r) // 绘制 _ctxc.draw() // ...省略部分代码 [代码] 让我们来看一下在手机上的效果吧: [图片] 这一次终于完美地解决了这个问题,开心。 还有,一些同学可能注意到,我上面的代码在添加阴影的时候新版本和老版本的API我都使用了,主要是因为我能够测试的手机比较少,不知道在别的品牌的手机上表现如何。所以为了保险起见,我两种方法都使用了。 还有一点需要注意的是,我本次实践的环境是:基础调试库 2.3.0,手机型号 iPhone 8 最终这个带阴影的有渐变背景色的按钮就完成啦。 第三步:总结与思考 关于小程序绘图这次遇到的问题我们可以总结一下:遇到在编辑器和真机表现不一样的情况,我们首先应该先上网查一下看看小伙伴们有没有遇到类似的问题,如果人家已经有比较好的解决方案,我们就可以参考别人的解决方案去解决我们自己遇到的问题,这样会比较省时省力。如果没有的话,我们就需要自己去逐步排查问题,不断地缩小出现问题的范围,最终定位出出现问题的地方;然后再去想办法解决。 其实,如果想达到这样的效果,我们不一定非得通过代码绘制去实现这么一个功能;也可以直接让设计师把按钮这一部分导出一张小的图片来,我们直接把这个包含阴影的按钮图片绘制在图上就好了。如果我们这个按钮是不经常发生变化的话,那么直接把包含阴影按钮的图片绘制在画布上也许是挺好的一个解决方法。所以,我们首先要考虑的是我们的代码想要达到什么样的效果,那么达到这样的效果有哪些途径,各有什么优缺点。然后我们选择一个最佳的方案去开发就可以啦。 这篇文章到这里就结束啦,希望我的分享能够帮助到一些遇到同样绘图问题的小伙伴。最后一点私心,推荐一下我们开发的这一款小程序主线程。 [图片] 一个可以帮助大家规划时间和任务,并且可以组队学习找伙伴一起打卡的小程序,如果你觉得的不错的话,记得帮我们分享一下哟。
2019-10-28 - 【请教】小程序自由选中多行文本
请教个问题,类似于手机长嗯文字后会出现左右两个选择光标可以自由选中文字,麻烦看到的开发者们留下一点自己的思路,非常感谢 [图片],
2018-07-06 - video组件无法播放服务器视频
经过千辛万苦,终于把手机mp4视频上传服务器做好了,不过遇到一个尴尬的情况,在小程序的开发工具中,可以正常的利用video播放上传的视频,不过用手机播放的时候,就一直转圈圈,等了半天也没有反应,文件不大,不到2MB,注:安卓可以正常使用,IOS一直转圈。 由于当前域名还没有注册完成,所以是使用IP地址作为视频文件路径的,是不是由于此处而导致?其他我真的是没有思路了。。各位有没有遇到过类似的情况?对了,我用的IIS。
2018-06-28 - 可以在微信中打开一个网页,然后通过网页打开微信小程序吗?
由于公司有个需求,需要使用同一个二维码即可关注公众号也可通过该二维码打开微信小程序,我的思路是通过web的方式进行实现,在web上判断是否关注公众号,如何没关注的打开公众号关注界面,如果关注了则打开微信小程序,不知道该方法是否可行?目前打开公众号关注界面是可以的,但是不知道如何通过web打开微信小程序界面
2017-08-09 - Animation动画的问题
- 当前 Bug 的表现(可附上截图) translate:[图片]width:[图片] bottom:[图片] - 预期表现 你小程序既然都已经有专属单位rpx了,并且animation动画的width,定位bottom都可以支持rpx,为什么你translate就不行,搞特殊化嘛?啊?就不能好好的统一下嘛?好你既然tanslate不支持rpx,但是你又不让cover-view标签支持width和bottom是几个意思?我换个思路给cover-view设置动画,偏偏就是 width 和bottom这些支持rpx单位的没有效果,这不是搞事情吗? 算我⚽⚽你们了,能不能让translate也允许传入自定义单位长度值? - 复现路径 - 提供一个最简复现 Demo
2018-11-02 - 点击删除小图标,把对应图片删了,怎么做
[图片] 我的想法是:在得到图片路径后,都存进数组,然后的是图片的显示,iamge的src在数组里面去取,点击删除的时候,得到对应删除按钮的下标,通过这个删除下标去删除数组里的对应下标。实现应该可以。 现在遇到的问题:删除小图标的下标由那里来,怎么才能传到删除方法里? 有哪位,实现过吗?请给我点思路,甚是感谢,如有小程序-【小黄车ofo】的过路大神,你一定会做,因为我看到【小黄车ofo】发布动态页有这个功能,而且实现得挺好。
2017-05-16 - 如何自动化生成带小程序码的海报
我看到小程序"社群之都"里已经做到这个功能了. 操作是:进小程序=>首页正在报名活动=>邀请函 就能生成一份带当前活动信息和小程序码的海报. 并且长按海报可以出现操作栏,可以保存图片和识别图中二维码. 我不是很有思路,感觉这种自动生成海报通过python来做可能容易点. 小程序中图片是不会长按出现操作栏的,通过调用预览图片api则有这个效果. 有没有大大知道,这是怎么实现的?
2018-03-06 - 微信支付
想请教下 当用户出示收付款码时商家用扫码枪扫描能获取用户的openid,从而进行判断用户是否是会员,如果是会员就自动进行折扣,这种思路行吗
2018-09-27 - wx.uploadFile图片上传问题
- 需求的场景描述(希望解决的问题) 小程序上传图片的时候都是上传一张图片调用一次接口???现在我的工作思路是先上传存储图片,然后返回路径,然后在加载另一个函数实现入库操作。 郁闷的是现在上传一张图片调用一次接口并返回路径,多图上传时无法拼接路径字符串 - 希望提供的能力 各位大神你们多图上传也是上传一张图片调用一次接口??? 我后台是用的php 求大神指点一下解决方案 [图片]后台[图片]
2019-03-29 - 小程序如何追加节点或把字符串解析成节点
关键词搜索一个列表,需要关键词高亮,我的思路是把字符串中的关键词替换成类似: "<text class='light-keyword'>" + word + "</text>" 这样会被解析成字符串,如何解析成一个节点
2017-12-26 - (17)分享功能调整背后的故事
有时候我们使用一个小程序会遇到以下情形: 我们打开一个小程序,就看见提示“分享到5个群,可以获得一张20元的优惠券”,吸引我们去无脑分享到不同的群里; 打开某个小游戏,提示我“一定要分享到xx个群,才能继续玩游戏”; …… 而我们在群里打开这类小程序,仍然是提示我分享的信息,这类功能无疑打断了我们对小程序/小游戏正常的功能使用。 我们收到了很多用户对这类小程序/小游戏的抱怨。这类分享并非是用户主动自发的,而是受到了某类利益的诱惑,或是被迫分享。这样的内容充斥在群里、小程序里,对用户造成了骚扰,是对分享功能的滥用。 在原来的分享接口中,用户发起分享动作之后,可以通过 success 、fail、complete等回调来判断用户是否完成了最后的分享动作。通过这个能力,开发者是可以将产品交互在分享这个能力上做得比较自然和顺畅。但却被上述情形的小程序滥用。在我们权衡了分享功能带来的利弊后,我们打算回收这个能力。调整为:我们将不再支持分享回调参数 success 、fail 、complete 。即开发者无法判断用户最终是否完成了分享动作,也无法获取到分享成功后的回调参数shareTicket 。 接下来将与大家介绍此次分享功能调整后,小程序的调整建议。 对应小程序调整建议 此次调整可能影响到两种分享功能的用法。 第一种:通过判断用户最终是否有分享来做分支逻辑的小程序。 例如,通过判断 success 回调触发,来判断用户是否分享出去了,进而给奖励,如果用户没有分享出去则不给奖励。这类功能是我们平台不倡导的,后续将没有办法实现。 如果是需要在分享完成后变更当前页面的状态,可以适当调整交互方案。例如过去赠送代金券后显示“等待领取”等应用场景,可以改成在分享后继续保留“赠送”按钮,但提示用户一个代金券只能被一人领取,重复赠送无效。 第二种:获取用户分享之后的 shareTicket ,换取群唯一标识 openGId ,进而显示对应群的相关信息的小程序。 例如,部分小程序实现了群内的排行信息,通过分享小程序到某个群里,可以查看该群内成员的排行榜。 此次调整后,用户分享完成后无法立刻显示该群的排行榜信息,但仍可在用户从群消息点击进入小程序时显示该群的排行榜信息。 因此建议适当修改产品流程,在用户分享小程序之时,提示用户可进入群内查看群排行等信息。避免调整策略生效之后带来的交互不完整影响。 调整覆盖范围提示 近期新提交的版本中将会受到此策略的影响。 除此之外,调整策略在即将发布的基础库版本 2.3.0 生效,该基础库版本对应本月即将发布的微信客户端版本(暂定版本号 6.7.2)。即:近期提交审核的小程序版本,在基础库版本 2.3.0 以下的环境中仍不受此策略影响,仅在基础库版本 2.3.0 以上的环境受影响。 开发者需要注意,近期提交审核的版本都需要考虑兼容上述调整带来的影响,请各位开发者及时调整分享能力。
2018-08-17 - 怎么从下级页面获取 app.js中设置的数据
新手小白求职 现在就是模拟用户数据 ,因为好几个页面要用到这些数据 所以想把这些放到app.js 中 然后其余页面从app.js 中获取 怎么才能获取到 或者是我的思路是不是有问题
2017-05-27 - 底部下拉菜单栏(action-sheet)怎么跳转啊
- [图片] insert() { wx.showActionSheet({ itemList: ['单选题', '多选题','填空题'], itemColor: '#007aff', success(res) { console.log(res.tapIndex); if (res.tapIndex === 0) { } else if (res.tapIndex === 1) { } } }) }, - 就是这个底部下拉菜单栏,想弄个跳转,但是我好想没什么思路,可以帮忙解决下吗
2018-05-25 - canvas绘制图片报错
[图片] 前端用canvas绘制图片 昨天还好好的 今天测试了一下 发现不能绘制海报了 报了不知名的错误 ready这个变量没有出现在JS当中 设计思路 先下载图片 然后把图片保存在本地 通过返回的路径 去绘制图片 大佬路过 稍微看一下 能说明一下最好了
2018-09-07 - scroll滚动,聊天下拉加载分页
可以定位到指定ID,但是页面会一跳一跳 会闪 xml [图片] JS [图片] 请问有没有哪位哥们做过聊天页面的下拉加载分页,或者有更好的办法和思路分享一下!!!
2017-09-27 - 需要登录才能使用的APP
有没有什么好的解决思路 本意是在onLaunch里判断授权登录 如果登录了就直接跳转进主页,但是判断登录的是异步查询服务器端的 此时主页已经开始渲染了 是不是应该创建一个类似于欢迎页的入口页面 等待App判断完登录状态这样做? 或者有没有什么好的解决思路
2017-06-28 - 微信小程序抽奖判断IP抽奖过一次就不能在抽奖
现在开发一个抽奖的活动,页面功能已经做好了现在就是不知道怎么判断抽奖的人不能重复抽奖或者是一天只能抽奖一次,想只用前端来实现因为本人只是前端不会后台,希望各位大神给点教程和思路谢谢
2018-12-25 - 调用获取用户access_token接口/sns/oauth2/access_t
目前公司想把两个小程序下的用户群打通,需要获取unionid,现在思路有两个: 一:1.使用/sns/jscode2session?获取openid。(此处使用encryptedData和iv解密不出unionid,只能解密出openid) 2.使用/cgi-bin/token?获取access_token。(这个接口好像只需要appid和secret,想请问一下各位,这个token能否用在下面接口来获取用户信息?) 3.使用/cgi-bin/user/info?获取用户信息。(此处就会报错40001 token过期或不是最新) 二:1.使用/sns/oauth2/access_token?(appid,secret,code)来获取用户access_token和openid。(此处报错48001,api未授权) 2.使用/sns/userinfo?(access_token,openid)来获取unionid 请各位兄弟走过路过帮个忙,顺便请官方人员好好解释一下。
2019-07-05 - 怎么才能让小程序根据不同身份的用户进入不同的页面
公司有需求 所有用户首次进入是相同页面进行登录操作 之后用户再次进入要不同身份的用户进入不同的页面 要怎么做 完全没有思路
2018-05-04 - security.imgSecCheck接口返回-404012错误
security.imgSecCheck <'errCode': -404012, 'errMsg': 'cloud.callFunction:fail error while waiting for the result; at cloud.callFunction api;> 模拟器上没有任何问题,我的小米手机体验版就是上面这个错误,一直过去不。。。网上查了下,才知道这个问题18年就有人反应过,至今都没有解决吗? 烦请大神指点解决办法。。。多谢了啊 [图片] -------------------------------------------------------- 找到解决办法了。思路主要参照下面大神的解决办法: https://developers.weixin.qq.com/community/develop/article/doc/00062c5c7a8ec834dc692913156013 用户传上来的图,一般是通过canvas的uni.canvasToTempFilePath方法存到本地,然后调用API审核,但是一般都会报404012的错误;原因个人认为是canvasToTempFilePath导出的图片尺寸过大所致。 解决办法:套用两层uni.canvasToTempFilePath方法!!! 第一层uni.canvasToTempFilePath方法,将destWidth和destHeight按用户图片的实际比例缩小,我缩小到120*180,用户相册的原图接近5M,导出后只有60多k;成功后将res.tempFilePath提交给security.imgSecCheck去审核(我试过很多遍,能正常审核);而在该方法的success下面继续嵌套uni.canvasToTempFilePath方法导出小程序真正要去使用的图片;
2020-01-10 - 关于图片验证码问题
我想请教一个问题 我的小程序需要使用图片验证码 我之前的思路是 将后台生成的验证码 aes加密后生成cookie返回前台. 然后前台 输入验证码后 cookie一起带过来 我在解密 比较. 可是这个简单的操作 放在微信小程序里面怎么实现呢? 看了好多文档都不明白.
2018-03-14 - 小程序有没有能把页面转成图片的方法
在公众号网页上通过dom-to-image库可以将某个dom内容转为图片,然后下载下来。搜索了好久,不知道小程序有什么思路可以将页面转为图片,下载到本地
2018-01-22 - 充电桩
有做过充电桩项目微信小程序的吗?高德百度地图上都查不到这种充电桩位置,老板说通过电桩上的GPS模块和数据芯片及云服务器得到数据,可行吗?求思路
2018-10-25 - 批量倒计时怎么处理?
[图片] 如图所示,这样的批量倒计时,有没有人做过? 想的是用 wxs 去处理数据,但是wxs中不支持 settimeout setinterval, 如果频繁的去调用setData 渲染,性能上会不会吃不消? 有么有人有思路的? 谢谢各位, 祝各位工作顺利~
2018-08-09 - 图片的选中
有这样一个需求:图片来源于数据库,一个图片列表,可上拉加载更多,已实现。问题来了,用户选中某张图片,当前图片为选中,再次点击为取消选中,两个状态通过两张icon的切换来表现。每张图片的左上角对应有两张icon小图标【选中icon/未选中icon】。请问一下,这个过程如何实现? [图片] 我的思路是:wx:if里的值要是可以跟随渲染下标index,也就是能跟随图片一起渲染,这样当触发事件时,方法里是可以得到对应下标的,就可以通过这个下标来设置对应icon的状态true/false,这样wx:if就能很好的的控制对应图片左上角的两张icon图标。但这个wx:if里的值动态生成是个问题,不知道怎么实现,望大神给思路,谢谢! [图片] 有些小伙伴说,给icon图标和对应的图片绑定一个ID,这个显然没必要,icon和图片通过渲染下标index是一一对应的。 希望得到各位的解决思路,感谢!
2017-06-26 - 小程序导出数据到excel表,借助云开发后台实现excel数据的保存
我们在做小程序开发的过程中,可能会有这样的需求,就是把我们云数据库里的数据批量导出到excel表里。如果直接在小程序里写是实现不了的,所以我们要借助小程序的云开发功能了。这里需要用到云函数,云存储和云数据库。可以说通过这一个例子,把我们微信小程序云开发相关的知识都用到了。 老规矩,先看效果图 [图片] 上图就是我们保存用户数据到excel生成的excel文件。 实现思路 1,创建云函数 2,在云函数里读取云数据库里的数据 3,安装node-xlsx类库(node类库) 4,把云数据库里读取到的数据存到excel里 5,把excel存到云存储里并返回对应的云文件地址 6,通过云文件地址下载excel文件 一,创建excel云函数 关于云函数的创建,我这里不多说了。如果你连云函数的创建都不知道,建议你去小程序云开发官方文档去看看。或者看下我录制的云开发入门的视频:https://edu.csdn.net/course/detail/9604 创建云函数时有两点需要注意的,给大家说下 1,一定要把app.js里的环境id换成你自己的 [图片] 2,你的云函数目录要选择你对应的云开发环境(通常这里默认选中的) 不过你这里的云开发环境要和你app.js里的保持一致 [图片] 二,读取云数据库里的数据 我们第一步创建好云函数以后,可以先在云函数里读取我们的云数据库里的数据。 1,先看下我们云数据库里的数据 [图片] 2,编写云函数,读取云数据库里的数据(一定要记得部署云函数) [图片] 3,成功读取到数据 [图片] 把读取user数据表的完整代码给大家贴出来。 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init({ env: "test-vsbkm" }) // 云函数入口函数 exports.main = async(event, context) => { return await cloud.database().collection('users').get(); } [代码] 三,安装生成excel文件的类库 node-xlsx 通过上面第二步可以看到我们已经成功的拿到需要保存到excel的源数据,我们接下来要做的就是把数据保存到excel 1,安装node-xlsx类库 [图片] 这一步需要我们事先安装node,因为我们要用到npm命令,通过命令行 [代码]npm install node-xlsx [代码] [图片] 可以看出我们安装完成以后,多了一个package-lock.json的文件 [图片] 四,编写把数据保存到excel的代码, 下图是我们的核心代码 [图片] 这里的数据是我们查询的users表的数据,然后通过下面代码遍历数组,然后存入excel。这里需要注意我们的id,name,weixin要和users表里的对应。 [代码] for (let key in userdata) { let arr = []; arr.push(userdata[key].id); arr.push(userdata[key].name); arr.push(userdata[key].weixin); alldata.push(arr) } [代码] 还有下面这段代码,是把excel保存到云存储用的 [代码] //4,把excel文件保存到云存储里 return await cloud.uploadFile({ cloudPath: dataCVS, fileContent: buffer, //excel二进制文件 }) [代码] 下面把完整的excel里的index.js代码贴给大家,记得把云开发环境id换成你自己的。 [代码]const cloud = require('wx-server-sdk') //这里最好也初始化一下你的云开发环境 cloud.init({ env: "test-vsbkm" }) //操作excel用的类库 const xlsx = require('node-xlsx'); // 云函数入口函数 exports.main = async(event, context) => { try { let {userdata} = event //1,定义excel表格名 let dataCVS = 'test.xlsx' //2,定义存储数据的 let alldata = []; let row = ['id', '姓名', '微信号']; //表属性 alldata.push(row); for (let key in userdata) { let arr = []; arr.push(userdata[key].id); arr.push(userdata[key].name); arr.push(userdata[key].weixin); alldata.push(arr) } //3,把数据保存到excel里 var buffer = await xlsx.build([{ name: "mySheetName", data: alldata }]); //4,把excel文件保存到云存储里 return await cloud.uploadFile({ cloudPath: dataCVS, fileContent: buffer, //excel二进制文件 }) } catch (e) { console.error(e) return e } } [代码] 五,把excel存到云存储里并返回对应的云文件地址 我们上面已经成功的把数据存到excel里,并把excel文件存到云存储里。可以看下效果。 [图片] 我们这个时候,就可以通过上图的下载地址下载excel文件了。 [图片] 我们打开下载的excel [图片] 其实到这里就差不多实现了基本的把数据保存到excel里的功能了,但是我们要下载excel,总不能每次都去云开发后台吧。所以我们接下来要动态的获取这个下载地址。 六,获取云文件地址下载excel文件 [图片] 通过上图我们可以看出,我们获取下载链接需要用到一个fileID,而这个fileID在我们保存excel到云存储时,有返回,如下图。我们把fileID传给我们获取下载链接的方法即可。 [图片] 1,我们获取到了下载链接,接下来就要把下载链接显示到页面 [图片] 2,代码显示到页面以后,我们就要复制这个链接,方便用户粘贴到浏览器或者微信去下载 [图片] 下面把我这个页面的完整代码贴给大家 [代码]Page({ onLoad: function(options) { let that = this; //读取users表数据 wx.cloud.callFunction({ name: "getUsers", success(res) { console.log("读取成功", res.result.data) that.savaExcel(res.result.data) }, fail(res) { console.log("读取失败", res) } }) }, //把数据保存到excel里,并把excel保存到云存储 savaExcel(userdata) { let that = this wx.cloud.callFunction({ name: "excel", data: { userdata: userdata }, success(res) { console.log("保存成功", res) that.getFileUrl(res.result.fileID) }, fail(res) { console.log("保存失败", res) } }) }, //获取云存储文件下载地址,这个地址有效期一天 getFileUrl(fileID) { let that = this; wx.cloud.getTempFileURL({ fileList: [fileID], success: res => { // get temp file URL console.log("文件下载链接", res.fileList[0].tempFileURL) that.setData({ fileUrl: res.fileList[0].tempFileURL }) }, fail: err => { // handle error } }) }, //复制excel文件下载链接 copyFileUrl() { let that=this wx.setClipboardData({ data: that.data.fileUrl, success(res) { wx.getClipboardData({ success(res) { console.log("复制成功",res.data) // data } }) } }) } }) [代码] 给大家说下上面代码的步骤。 1,下通过getUsers云函数去云数据库获取数据 2,把获取到的数据通过excel云函数把数据保存到excel,然后把excel保存的云存储。 3,获取云存储里的文件下载链接 4,复制下载链接,到浏览器里下载excel文件。 到这里我们就完整的实现了把数据保存到excel的功能了。 文章有点长,知识点有点多,但是大家把这个搞会以后,就可以完整的学习小程序云开发的:云函数,云数据库,云存储了。可以说这是一个综合的案例。 有什么不懂的地方,或者有疑问的地方,请在文章底部留言,我看到都会及时解答的。后面我还会出一系列关于云开发的文章,敬请关注。
2019-09-07 - 大家好,关于WXML变量里面套变量
是这样的, 我们这里要做一个页面嵌入多个倒计时,而这个倒计时的个数是动态变化的。也就是说倒计时的个数不确定,按照原本单个的思路是在页面里放入一个 倒计时: {{clock}} 再递归 [图片] 能够实现。 但现在需要多个而且不确定个数的情况下,想把里面的 clock也变成动态的,结果就需要写成 {{clock1}} {{clock2}} {{clock3}}。。。而顺着思路下去就想这样处理 {{ {{value}} }},但不符合微信小程序的规范,不知道大家有什么处理的方法。
2017-10-30 - 如何统计各个员工的小程序的推广效果?
公司每个员工掌管几个微信公众号,通过哪种方式可以查看各个员工的小程序推广效果? 提供思路即可。谢谢
2017-11-30 - 关于小程序初始化
我的小程序需要实现,扫码后,动态识别需要加载的内容,也就是在小程序打开的时候,就要得到一个动态内容的标识,这个有没有实现思路
2017-03-18 - 【高校开发者】简单分享下
简单分享下 写这篇文章主要是想跟刚迈入编程这条水泥路的小程序新手开发人员一起交流交流的,因为咱也只是个开发小程序的菜鸟人员,技术帖咱是写不来了,就只能跟大家唠唠嗑了。 1. 开篇 接触微信小程序也有一年多了,在这一年多的时间里也写过五、六个小程序,最初接触小程序是因为我们想通过小程序,来推广运营公众号和扩展公众号功能。 之后的开发就完全是因为好玩和兴趣了吧! 好玩??? 怎么玩? 大一大二咱大家刚接触算法编程,或许对算法编程是这样的: [图片] [图片] [图片] [图片] 而且经常会听到学这到底干什么用的呀! 好烦,这周数据结构又学了几个算法,二叉树?图?那行吧,咱就用这些个数据结构写个小程序吧! 又或者我们玩了一些好玩的小程序,那行吧,咱也试着用我们学过的算法知识在小程序上也写一个吧? 即使是些老掉牙的游戏,贪吃蛇、扫雷、连连看消消乐、成语接龙等等这些小游戏,实现后你还能在这些个游戏基础上发挥你的脑洞,进行创作改编。或许还能让你编个爆款小程序出来也说不定,是吧。 这些小游戏,小程序内部或多或少都用到了传说中的算法。 之所以选择用小程序来实现,还有一个最主要的原因就是小程序能够很轻易、很低调的就让我们秀一波。 小程序在真机上运行测试很容易,一个二维码就搞定,而且开发的小程序,不仅仅只是让我们本专业同学能够测测玩玩,还能够让更多非本专业的,好比你对象呀同学呀爸爸妈妈呀爷爷奶奶呀哥哥姐姐弟弟妹妹姑姑婶婶阿姨婆婆呀等等等等(只要他们有微信就行)都能够一起体验我们写的这些个小游戏呀。(o(∩_∩)o ) 所以在小程序开发过程中不仅学到了知识,还让我们见到了知识,玩到了知识,还能从中时不时的得到些个赞赏鼓励。你说这样子学习,你学不好才怪! 小程序还可以是一个表白神器,花点心思写一个专属 Ta 的独一无二的小程序,只给你的 Ta 开放个权限,再送给 Ta 比你花多少钱买个什么礼物都珍贵,你说我说的在理不!所以,你说小程序开发好不好,爱情学习双丰收呀。 2. 案例 好吧,上面废话有点多,下面先放几个自己做的小程序聊聊,素材什么的都是自己花心思画的(丑不丑无所谓,自己看着舒服就行),而且写一款完完全全凭自己感觉设计出来的小程序会让自己更有成就感。 2.1 反义词消消乐 反义词消消乐这个就是去年参加微信小程序应用开发赛做的一个小程序,为了增加游戏趣味性也增加了多人对战的功能。 反义词的逻辑匹配算法其实很简单,后台字词数据表,表中每条数据存储正、反两词,因此拥有共同的id值(int类型),前端获取到数据后只需将每条数据进行简单分割处理,可以让正意思词的标记值等于+id,反意思词的标记值等于-id,这样我们就能通过两标记值相加为0,来判断字词是否匹配成功。 游戏内的所有逻辑都交给小程序来判断运转,后端只是用来实现数据的存取,中转和传输。多人中前后端是使用 WebSocket 通道进行实时双向通信的,确保游戏内所有消息能够快速同步。逻辑如下: [图片] 简单解释下上图,当用户点击匹配后,发送信号与后台建立websocket连接,连接成功后传回连接的websocket的id值即sid,前端打包用户数据,sid,用户状态,用户转态就是此时是否已经退出游戏,发送给后端,后端保存信息,放入用户匹配池中,当准备就绪后,传回前端,前端发送匹配请求,后端再次开始为用户分配房间号,之后判断实时监听判断分配的房间是否满足两人,满足即可开始游戏逻辑。当游戏开始后,前端与后端的处理逻辑为如下所示: [图片] 上图是表示游戏匹配过程中的逻辑,其中打包小程序端发送的游戏信息其实就是用户点击反义词数据的状态信息,有助于后台判断分数加减等情况。服务器端的定向广播游戏信息,就是将处理好的分数等情况广播发送给定向房间内的用户。 反义词小程序整体流程大致是这么实现,而分配玩家的逻辑可以是按随机,按先到先服务,又或者按指定条件(如胜率分段等)将玩家分配到一起。 [图片] [图片] 2.2 困住小星星 困住小星星这个小程序,其玩法是模仿日本游戏设计师TaroIto2007年制作的“黑猫”(ChatNoir)游戏,而我在将其成功围住之后放了一些彩蛋,具体内容嘛就不说了 (✿◡‿◡),你们可以自己更改主题后随意发挥。 玩法就是游戏初始化墙的个数大概是9-13个之间。玩家需自己想办法点一个圈,目的就是要将其围住,不让她从边界跑掉即可。 小星星的寻路机制算法用到了最小路径和最大通路的算法,在每次寻路前先计算出各个非墙点的最小路径步数,以及最大通路步数,当最小路径步数相同时,就用最大通路步数来进行比较,最终找到最优路径,所以要想困住其实还是有点难的。 因为游戏中的地图是9*9,因此在最短路径算法中,其中点是(4,4),要计算到各边界的最短路径问题,可以从外往里开始计算。比如最外围,因为是边界,所以其路径长度为0,之后每往里一层,其路径长度就加1,直到到达主角位置。在不存在墙的情况下如下示: [图片] 最小路径: 其实要想计算出这个图的路径,我们不难发现,计算某个点的路径时(例如上图标红处),我们只需遍历找出每个点周围(即左、左上、右上、右、右下、左下)路径最小的值并加1,即是最终的最短路径长度。 如何实现?首先我们规定图中81个点初始path值可设为 -100,障碍物的path值可设为 100,边界path值可设为 0,主角位置可设为 999。之后再分别从四个大方向,即左上、右上、右下、左下这四个方向进行反复计算。 如何计算,以左上为例,左上计算即表示从左到右,从上到下,对各个点进行遍历。在遍历各点的同时,判断该点是否为障碍物,是则直接返回path的值为100,若该点为边界,则直接返回path的值为0。 如果都不是,则找出该点周围的6个点,并分别遍历周围的这6个点,判断这6个点的路径值,找出周围6个点路径的最小值min,min初值设为100。 如果筛选出来周围的点中存在没有计算过的点,即存在小于0的值,那需要等以后来计算他们的值,因此先不参与比较判断。而周围点的path值满足大于0的条件时,与min值比较,选出其中的最小值并赋给min,遍历完6个点后。当min值满足小于100的条件时,那么说明该点周围的最小值已经计算出来,直接将该点周围最小值路径值加1即为该点path值。 上面说的有点绕,直接上代码: [代码]calPath: function(location) { //计算路径 var row = location.row var col = location.col if (this.data.map_location[row][col] == 1) { //墙直接返回100 location.path = 100 return location.path } if (row == 0 || col == 0 || row == 8 || col == 8) { //边界直接返回0 location.path = 0 return location.path } var sixDir = this.calSixDir(location) //寻找周围的六个点 //遍历周围6个点,找出最小值 var min = 100 for (var i = 0; i < 6; i++) { if (sixDir[i].path >= 0) { // 存在点没有值,那需要等下次再来计算其值 var tmp = sixDir[i].path if (min > tmp) { min = tmp } } } if (min < 100) { location.path = min + 1 } else { location.path += 1 } return location.path }, [代码] 以此类推,从右上、右下、左下这三个方向进行计算,也都是相同的逻辑操作。 最大通路: 最大通路其实与最短路径的计算差不多,只是最大通路我们计算的是该点周围能走的个数,即寻找出该点周围6个点后,统计一下该点周围非墙的个数即可。因此计算时只需两个for循环遍历一遍图中所有点并分别计算一下即可。 [图片] [图片] 2.3 fly 拖鞋、摩斯密码 这个名字嘛fly 拖鞋,就是我随便取的。当时画素材的时候,就是随笔画了画涂了涂,看着有点像拖鞋就叫了 fly 拖鞋,这个游戏其实是用小游戏写的,游戏内容其实就跟fly bird一样。 开发这个游戏的过程中其实没用到什么算法,也没什么好说的。但用到了些物理知识,就是高中咱都学过的上抛与自由落体的公式,说这个就是想你知道写游戏的时候其实很经常用到物理知识。 这个摩斯密码,就是因为当时跟那谁谁谁聊起过摩斯密码,感觉那滴滴哒哒的也很神奇,而且又凑巧那天看到了个摩斯密码表,于是就照着那个表写了这个翻译器。所以用小程序写个小工具自己用还是很方便的。我觉得吧小程序是最适合自己写工具的,方便,而且干什么都行,就看你怎么用了。 [图片] [图片] 上面说那么多其实就想让学弟学妹们知道小程序可以这么玩。把我们所学的知识用可视化的形式体现出来,这可能比单纯的学习课本上的知识更有趣。 3.聊聊 小程序开发起来不会太难,在这个过程中其实也没有遇到太多大问题,遇到的bug无非就两种,要么语法错误,要么逻辑错误。 其实当遇到一个问题的时候,最好的解决办法就是官方文档了。这个真不是废话,很多时候遇到的bug其实就是我们对api的不熟悉,错用,用错而已,再不是,那就是咱自己代码逻辑错误了吧。 3.1 开发 开发个小程序的过程无非就这五个过程:问题—>分析—>设计—> 编码—>优化。 本人开发的心路历程首先就是自己要明白到底要开发个什么东西出来,之后便是去官方文档中简单查询下这些api,看看我们设计实现的这些个功能到底可不可行能不能实现。实现不了的,就换个思路看看有什么可替代的方案。 之后就是得想清楚,具体的实现步骤,实现逻辑,并分配好前后端的功能逻辑,之后再简单搭建小程序的整体框架,架构。我写的小程序其实大部分的实现逻辑都是在前端实现,后端我只是单纯的用来做些数据存取、中转等功能。 当然现在小程序的云开发,大大方便了我们,直接可以将数据放到云开发数据库上,用官方的原话讲就是一个既可在小程序前端操作,也能在云函数中读写的 JSON 数据库,而且这也大大简化了后台服务器的环境构建,运营维护等。可以减少很大的工作量。 最后再是动手编码实现我们的想法,将小程序的基础功能逻辑基本实现后,再慢慢的对其进行优化,完善。 其实我最喜欢就是完善和优化的这个过程,它能够让我很惬意的去设计、体验,将我们自己的小程序从粗糙的demo到精致的成品这个过程,是一件很有成就感的事。作出一个美美哒,精致的小程序,你就会连吃饭学习睡觉都感觉很舒畅。 3.2 收尾 最后总结就是,在开发过程中,可以先分析好功能逻辑,再开发个拥有主要功能的demo出来(先不管页面什么的设计有多难看),之后我们在这个demo的基础上一版版的优化修改,一步步迭代,最终成型。是的,小程序的开发就是这么容易,这么简单,这么好学,这么有趣。 还有就是要开发设计一个小程序其实不需要太多太复杂的功能,开发一个小程序的真谛就是简洁! 不仅仅是界面简洁,操作简洁,其实最主要的就是功能简洁,这或许也就是小程序设计的初衷吧。 最后还有就是遇到问题咱们首先确定一下是不是语法错误,再看看是不是逻辑错误,再不是,可以去文档上看看找找原因,文档还没有就去开发者社区里搜搜,开发者社区还没有的话可以先在这里面提个问题,最后咱们再去百度google,查查看,如果还没有解决方案的话。 那没办法了,只能开大招了—— 小黄鸭调试法! [图片] 在很久很久以前,有一个传说,传说中有一位程序大师,他随身携带了一只小黄鸭,每当在调试代码的时候会在桌上放上这只小黄鸭,然后详细地向鸭子解释每行代码…没了,这个传说就这么短,这就是传说中的小黄鸭调试发。 为什么呢?维基百科是这么解释的,小黄鸭调试法,又称橡皮鸭调试法,或者黄鸭除虫法,是在软件工程中使用的代码调试的一种方法,方法就是在程序调试,测试,除错过程中,操作人向小黄鸭耐心的解释每一行程序的作用,以此来激发灵感与发现矛盾 说白了就是回归带自己代码上,然后自言自语一翻。 你还别说,这个真管用,说出来更容易帮自己捋清楚思路,这个方法真的是屡试不爽,一直试一直爽! 要是你有幸能够把问题解决好了,最好可以记录一下踩过的坑那是最好的了,如果你再善良一点那就来开发者社区中分享一波吧。不管问题多简单,你要知道比你懂的少的人还是有很多的,他们那些新新手可能更需要的是咱们这些菜鸟新手的帮助吧。 最后再说一句话: [图片]
2019-05-27 - 微信小程序收集数据
最近用微信小程序做了一个form,想当问卷来用,但是不知道该怎么把用户填写的数据收集到本地,请大神提示一下思路,非常感谢!
2018-01-10 - 输入框 input 怎么动态控制是否可可以编辑?
小程序输入框 input 怎么动态控制是否可可以编辑?我的思路是,从后台取值,然后就是点击修改按钮,才可以修改。disabled='true'和disabled='false',都不可编辑。改怎么做呢?
2018-09-21 - 切换商品分类scrollview不回到顶部
切换商品分类,先设置goodsList = [ ],然后再请求数据重新填充goodsList, 数据没问题,就是scrollview不回到顶部而是到上一次滚动的位置。问一下大家遇到过没有,求个思路 发现问题:给scrollview设置了hidden导致的
2019-03-27 - 如何调用第三方接口数据
了解完前面的那些内容,动手能力强的,基本上就可以完成很多类别的小程序了。 今天这篇文章进入一个新的领域,开始进入网络开发,前面的内容都是不需要有服务器或后台的概念的。因为所有功能都在小程序端就解决了,数据也只是使用的缓存。 如果要实现一些比较复杂的小程序,服务器的使用肯定是绕不开的一个环节,在微信小程序中,通常有如下 3 种方式,进行后台服务端的开发。 真正意义上的第三方,使用非自己,非微信官方的接口; 使用微信小程序的云开发能力; 自己搭建服务器,给小程序提供服务端接口; 它们的开发难度是 1 > 2 > 3,1 和 3 在小程序端的开发并无差异,只是 3 可能需要全栈开发能力,才能驾驭。 微信官方提供的网络 API 文档,有如下 4 类:请求数据(request),下载文件(downloadFile),上传文件(uploadFile),WebSocket。 今天这篇文章,就来介绍一下,如何在小程序中使用 request 获取第三方的数据? 开发实战 我们要使用的是易源接口提供的数据 API — 历史上的今天。 首先在项目目录中,新建 request 目录,并新建 Page index,页面代码如下: [代码]<block wx:for="{{ lists }}" wx:key="{{ index }}"> <view class="list"> <view class="title">{{ item.title }}</view> <view class="desc">{{ item.year }}年{{ item.month }}月{{ item.day }}日</view> <view class="access"></view> </view> </block> [代码] 很简单,就是一个列表样式,传入 lists 值,用来展示「历史上的今天」的事件。 接下来就是请求第三方的 API 数据了,先看一下易源的接口文档:历史上的今天-易源接口文档,大致内容如下图: [图片] 接口返回格式为 json,请求数据需要 4 个参数,这是为了校验,需要我们注册账户才可以获取。 这里重点是要知道,返回数据的 json 结构,如下: [代码]{ "showapi_res_code": 0, "showapi_res_error": "", "showapi_res_body": { "ret_code": 0, "list": [ { "title": "世界卫生组织宣布已经成功控制SARS", "month": 7, "img": "http://img.showapi.com/201107/5/099368663.jpg", "year": "2003", "day": 5 } ] } } [代码] 接下来,我们就可以编写 js 代码了,打开 [代码]request/index.js[代码] 文件,编写请求函数: [代码]const { showapi_appid, showapi_sign } = require('./self.config.js'); const request_data = function(callback) { wx.request({ url: 'https://route.showapi.com/119-42', data: { showapi_appid, showapi_sign }, header: { 'content-type': 'application/json' }, success: res => { var showapiData = res.data.showapi_res_body.list; callback(showapiData); } }) } [代码] 通过 [代码]wx.request[代码] API 就可以进行网络请求,根据 [代码]json[代码] 的结构,可以准确拿到 list 信息。注意一点:这里使用 [代码]callback[代码] 返回数据,思考一下为什么?另外,上述代码中 [代码]showapi_appid[代码],[代码]showapi_sign[代码] 参数需要替换成自己的。 最后页面事件函数中,调用该方法即可,代码如下: [代码]onLoad: function (options) { wx.showLoading({ title: '加载中' }) request_data(res => { this.setData({ lists: res }); wx.hideLoading(); }); }, [代码] 最终页面显示如下图所示: [图片] **需要注意的是:**线上版本一定要在小程序公众平台进行域名信息配置,开发工具中,在此处查看信息。 [图片] 开发测试阶段,可以勾选「不校验」选项,方便在开发工具中进行调试。 [图片] 总结 本篇文章介绍了微信小程序使用服务器(也就是后台开发)的三种方式,分别是:使用第三方数据接口、使用云开发以及自建服务器。 另外,以实战形式介绍了 [代码]wx.request[代码] 的用法,并完成了「历史上的今天」的小工具。 留一个思考题:由于类似的第三方数据接口,都是付费服务,这个接口虽然免费,但是限制调用次数,如何通过改进代码最低限度调用接口?有两个思路: 借助缓存功能,每天每个用户只需要 1 次请求; 借助数据库功能,不管多少用户,每天只需要 1 次请求; 更多文章:https://github.com/pengloo53/miniprogram-articles
2019-09-10 - 常见小程序优化方案总结
一、首次启动性能优化 1、首次打开一个小程序,用户一般会观察到如下图所示的三种状态 [图片] 这张图中的三种状态对应的都是什么呢?小程序启动时,微信会为小程序展示一个固定的启动界面,界面内包含小程序的图标、名称和加载提示图标。 此时,微信会在背后完成几项工作:下载小程序代码包、加载小程序代码包、初始化小程序首页。下载到的小程序代码包不是小程序的源代码,而是编译、压缩、打包之后的代码包。 2、小程序加载的顺序 微信会在小程序启动前为小程序准备好通用的运行环境。这个运行环境包括几个供小程序使用的线程,并在其中完成小程序基础库的初始化,预先执行通用逻辑,尽可能做好小程序的启动准备。这样可以显著减少小程序的启动时间。 [图片] 通过这张图可以对比发现,小程序首次启动的 第一张图是资源准备(代码包下载);第二张图是业务代码的注入以及落地页首次渲染;第三张图是落地页数据请求时的loading态(部分小程序存在)。 3、优化方案 控制包大小:上传代码时要先进行压缩、静态图片资源除小的icon外其余放到cdn、无用代码清除; 分包加载:根据业务场景,将用户访问率高的页面放在主包里,将访问率低的页面放入子包里,按需加载; 分包预加载:在进入小程序某个页面时,由框架自动预下载可能需要的分包,提升进入后续分包页面时的启动速度。对于独立分包,也可以预下载主包。分包预下载 官方文档链接 独立分包技术:区别于子包,和主包之间是无关的,在功能比较独立的子包里,使用户只需下载分包资源;独立分包 官方文档链接 二、渲染性能优化 1、数据渲染优化 双线程下的界面渲染,小程序的逻辑层和渲染层是分开的两个线程。在渲染层,宿主环境会把WXML转化成对应的JS对象,在逻辑层发生数据变更的时候,我们需要通过宿主环境提供的setData方法把数据从逻辑层传递到渲染层,再经过对比前后差异,把差异应用在原来的Dom树上,渲染出正确的UI界面。 [图片] 页面初始化的时间大致由页面初始数据通信时间和初始渲染时间两部分构成。其中,数据通信的时间指数据从逻辑层开始组织数据到视图层完全接收完毕的时间,数据量小于64KB时总时长可以控制在30ms内。传输时间与数据量大体上呈现正相关关系,传输过大的数据将使这一时间显著增加。因而减少传输数据量是降低数据传输时间的有效方式。 [图片] 在数据传输时,逻辑层会执行一次JSON.stringify来去除掉setData数据中不可传输的部分,之后将数据发送给视图层。同时,逻辑层还会将setData所设置的数据字段与data合并,使开发者可以用this.data读取到变更后的数据。因此,为了提升数据更新的性能,可以参考如下方法: 1.不要过于频繁调用setData,应考虑将多次setData合并成一次setData调用; 2.数据通信的性能与数据量正相关,因而如果有一些数据字段不在界面中展示且数据结构比较复杂或包含长字符串,则不应使用setData来设置这些数据; 3.与界面渲染无关的数据最好不要设置在data中,可以考虑设置在page对象的其他字段下; 4.勿在后台页面去setData; 5.建议创建一个检测data大小的方法,如果超过64K可以打印报警日志提醒开发者; 2、长列表优化方案 无限下拉加载后会大数据量展现导致的性能问题,一个常见的方法在诸多C端都有使用,一句话说就是"只渲染所需的元素"。虚拟列表是按需显示思路的一种实现,即虚拟列表是一种根据滚动容器元素的可视区域来渲染长列表数据中某一个部分数据的技术。简而言之,虚拟列表指的就是「可视区域渲染」的列表。有三个概念需要了解一下: 滚动容器元素:一般情况下,滚动容器元素是 window 对象。然而,我们可以通过布局的方式,在某个页面中任意指定一个或者多个滚动容器元素。只要某个元素能在内部产生横向或者纵向的滚动,那这个元素就是滚动容器元素考虑每个列表项只是渲染一些纯文本。在本文中,只讨论元素的纵向滚动。 可滚动区域:滚动容器元素的内部内容区域。假设有 100 条数据,每个列表项的高度是 50,那么可滚动的区域的高度就是 100 * 50。可滚动区域当前的具体高度值一般可以通过(滚动容器)元素的 scrollHeight 属性获取。用户可以通过滚动来改变列表在可视区域的显示部分。 可视区域:滚动容器元素的视觉可见区域。如果容器元素是 window 对象,可视区域就是浏览器的视口大小(即视觉视口);如果容器元素是某个 div 元素,其高度是 300,右侧有纵向滚动条可以滚动,那么视觉可见的区域就是可视区域。 实现虚拟列表就是在处理用户滚动时,要改变列表在可视区域的渲染部分,其具体步骤如下: 计算当前可见区域起始数据的 startIndex 计算当前可见区域结束数据的 endIndex 计算当前可见区域的数据,并渲染到页面中 计算 startIndex 对应的数据在整个列表中的偏移位置 startOffset,并设置到列表上 计算 endIndex 对应的数据相对于可滚动区域最底部的偏移位置 endOffset,并设置到列表上 [图片] 虚拟列表的实现原理可以参考这篇文章:浅说虚拟列表的实现原理 3、长列表局部渲染技巧 在一个列表中,有n条数据,采用上拉加载更多的方式。假如这个时候想对其中某一个数据进行点赞操作,还能及时看到点赞的效果,可以采用setData全局刷新,点赞完成之后,重新获取数据,再次进行全局重新渲染,这样做的优点是:方便,快捷!缺点是:用户体验极其不好,当用户刷量100多条数据后,重新渲染量大会出现空白期(没有渲染过来)。 优化步骤: 1.将点赞的[代码]id[代码]传过去,知道点的是那一条数据, 将点赞的[代码]id[代码]传过去,知道点的是那一条数据 <view wx:if="{{!item.status}}" class=“btn” data-id="{{index}}" bindtap=“couponTap”>立即领取</view> 2.重新获取数据,查找相对应id的那条数据的下标([代码]index[代码]是不会改变的) 3.用setData进行局部刷新 this.setData({ list[index] : newList[index] }) 4、用户事件优化 视图层将事件反馈给逻辑层时,同样需要一个通信过程,通信的方向是从视图层到逻辑层。因为这个通信过程是异步的,会产生一定的延迟,延迟时间同样与传输的数据量正相关,数据量小于64KB时在30ms内。降低延迟时间的方法主要有两个。 1.去掉不必要的事件绑定(WXML中的bind和catch),从而减少通信的数据量和次数; 2.事件绑定时需要传输target和currentTarget的dataset,因而不要在节点的data前缀属性中放置过大的数据。 三、生命周期优化 1、异步请求,页面渲染需要的数据最好在onLoad时异步请求数据,不要在onReady时请求;非页面渲染需要的数据,尽量放在onReady生命周期去调用; 2、定时器、事件监听、播放组件、音视频组件等,在页面转入后台(onHide)或者销毁(onUnload)时应该中止掉; 四、图片静态资源预加载 在日常小程序的开发中,有很多的大图片是放置于cdn上的,在需要进行展示的时候,如果没有预加载有可能出现图片展示的不及时,造成不好的体验,所以如下方式实现了图片预加载的功能,可以封装成组件的形式。 实现思路是将图片添加进页面中,设置不可见,然后加载图片,实现一个预加载的功能。 1、添加模版文件: img-loader.wxml <template name=“img-loader”> <image mode=“aspectFill” wx:for="{{ imgLoadList }}" wx:key="*this" src="{{ item }}" data-src="{{ item }}" bindload="_imgOnLoad" binderror="_imgOnLoadError" style=“width:0;height:0;opacity:0” /> </template> 2、添加js文件:img-loader.js /** 图片预加载组件 */ class ImgLoader { /** 初始化方法,在页面的 onLoad 方法中调用,传入 Page 对象及图片加载完成的默认回调 */ constructor(pageContext, defaultCallback) { this.page = pageContext this.defaultCallback = defaultCallback || function () { } this.callbacks = {} this.imgInfo = {} [代码]this.page.data.imgLoadList = [] //下载队列 this.page._imgOnLoad = this._imgOnLoad.bind(this) this.page._imgOnLoadError = this._imgOnLoadError.bind(this) [代码] } /** 加载图片 @param {String} src 图片地址 @param {Function} callback 加载完成后的回调(可选),第一个参数个错误信息,第二个为图片信息 */ load(src, callback) { if (!src) return; [代码]let list = this.page.data.imgLoadList, imgInfo = this.imgInfo[src] if (callback) this.callbacks[src] = callback //已经加载成功过的,直接回调 if (imgInfo) { this._runCallback(null, { src: src, width: imgInfo.width, height: imgInfo.height }) //新的未在下载队列中的 } else if (list.indexOf(src) == -1) { list.push(src) this.page.setData({ 'imgLoadList': list }) } [代码] } _imgOnLoad(ev) { let src = ev.currentTarget.dataset.src, width = ev.detail.width, height = ev.detail.height [代码]//记录已下载图片的尺寸信息 this.imgInfo[src] = { width, height } this._removeFromLoadList(src) this._runCallback(null, { src, width, height }) [代码] } _imgOnLoadError(ev) { let src = ev.currentTarget.dataset.src this._removeFromLoadList(src) this._runCallback(‘Loading failed’, { src }) } //将图片从下载队列中移除 _removeFromLoadList(src) { let list = this.page.data.imgLoadList list.splice(list.indexOf(src), 1) this.page.setData({ ‘imgLoadList’: list }) } //执行回调 _runCallback(err, data) { let callback = this.callbacks[data.src] || this.defaultCallback callback(err, data) delete this.callbacks[data.src] } } module.exports = ImgLoader 3、在需要使用预加载功能的xxx.wxml页面中加入模版文件和使用代码: <import src="…/…/templates/img-loader.wxml"/> <template is=“img-loader” data="{{ imgLoadList }}"></template> 4、在需要使用预加载功能页面的xxx.js文件中引入文件和使用代码: import ImgLoader from ‘…/…/templates/img-loader.js’; let images = [ ‘http://cdn.weimob.com/saas/activity/bargain/images/arms/shoulie.png’, ‘http://cdn.weimob.com/saas/activity/bargain/images/arms/shandian.png’, ‘http://cdn.weimob.com/saas/activity/bargain/images/arms/fengbao.png’ ] //初始化图片预加载组件,并指定统一的加载完成回调 this.imgLoader = new ImgLoader(this, this.imageOnLoad.bind(this)); images.forEach(item => { this.imgLoader.load(item) }) 备注:如有错误请帮忙指出;如有侵权,请联系我们删除,谢谢!
2019-09-03 - 如何做一个可以选择开始日期结束日期的选择器
有什么思路啊 网页里有那种可以点两次选择的组件 微信小程序里面没这样的都是滚轮 这样我要选择开始结束日期谁做过类似的啊
2017-08-08 - 登录逻辑处理问题求解?
目前在开发小程序,遇到一个问题,希望大家能帮忙提供下解决思路。 问题描述: 我在onLaunch中执行了以下操作: wx.getStorageSync('token'); //此token是后台接口根据openid和sessionKey生成的 若token存在,则调用wx.checkSession判断session是否失效 若session失效,则调用wx.login获取code,然后用code从后台换取新的token缓存下来 现在小程序打开的首页需要在请求头中包含这个token,后台接口才能验证通过,并返回数据,但是因为wx.checkSession、wx.login、wx.request三个接口都是异步的,所以首页的接口调用时,可能还没有拿到这个token,因为所有的接口都需要这个token,所以我想在app.js中实现获取这个token的逻辑。 请问如何保证拿到这个token之后再去调用其他接口?
2019-07-31 - 想知道联网对战游戏是怎么实现的,转发邀请就可以直接一起开始游戏
看过别人的微信小游戏可以邀请好友对战,想知道是怎么实现的,api中好像没看到转发里面有什么特殊的东西支持好友对戏,所以感觉很神奇,有大神可以给个思路吗
2018-12-03 - 云开发获取appid 和 appsecret
我想使用模板消息发送消息,需要我获得 access_token, 获取这个又需要 appid 和 appsecret ,我用了wx.getAccountInfoSync() 获取到了appid ,但appsecret 怎么获取?或者说 发送模板消息大家有什么更好的其他的思路吗?
2018-12-10 - 基于小程序·云开发构建高考查分小程序丨实战
2019高考报名人数达到了 1031 万的新高,作为一名三年前参考高考的准程序猿,赶在高考前,加班加点从零开始做了一款高考查分小程序,算是一名老学长送给学弟学妹们的高考礼。上线仅 1 个月,用户数就突破了 1k,关于小程序的介绍就不多说了,可以去搜【历年高考分数线查询】体验,今天主要谈谈技术原理和实现细节。 [图片] 数据来源 小程序后台共收录近 30w 条数据,包含 2008-2017 年所有重点高校的各个批次的文理分科录取分数线以及 2008-2018 所有采用新课标一卷、新课标二卷、新课标三卷以及部分自主命题省份的从提前批到高职专科批的录取分数线,勉强称得上内容翔实。 [图片] 所有数据均采集自各大院校和各高考相关网站,由于数据量巨大,为提高速度,使用了 concurrent.futures (需要 Python3.5+) 模块里的 ThreadPoolExecutor 来构建线程池来并发执行多任务。 数据库采用的是 PgSQL,一款号称世界上最强大的开源数据库产品,所有数据均存在新建的 gaokao 数据库中,其下有两个表,university(院校的录取分)和 province(省份的批次线) university 表说明 字段 解释 name 院校名称 stu_loc 生源地 stu_wl 文理科 pc 录取批次 year 年份 score 录取平均分 province 表说明 字段 解释 year 年份 stu_loc 考生所在地 stu_wl 文理科 pc 批次 control 本批次最低控制线 30w 的数据量,多个站点,并发爬取,数据冲突是不可避免地,在执行插入之前,首先过滤掉残缺不全的数据,比如在插入 university 表时某条数据缺少 pc 字段,那么这条记录就应该被舍弃,最严重的是数据重复,我采用的解决办法是:先查询待插入的数据是否已经存在, university 表的主码是(name,stu,stu_wl,pc,year),因为现实约束一个院校只能在一个年份在一个类别一个批次只能有一个录取平均分,如果不存在,才执行最后的插入,并 commit 提交事务。 后台搭建 在 30w 条数据拿到后,我打算后台采用 Flask+PgSQL 的模式实现,甚至在后台在阿里云服务器部署好,小程序端在开发者工具联调通过之后,小程序上线遇到到一个大麻烦,因为小程序要求线上运行不能通过 ip 地址访问后台,必须通过备案的域名访问,域名购买一个倒不麻烦,只是域名备案比较耗时间,需要一周多时间,而当时距离高考也就不到 5 天,在手足无措之时,无意间看到小程序云开发,关于小程序云开发,官网的介绍是: 开发者可以使用云开发开发微信小程序、小游戏,无需搭建服务器,即可使用云端能力。 云开发为开发者提供完整的原生云端支持和微信服务支持,弱化后端和运维概念,无需搭建服务器,使用平台提供的 API 进行核心业务开发,即可实现快速上线和迭代,同时这一能力,同开发者已经使用的云服务相互兼容,并不互斥。 也就是说,只要把数据导入小程序自带的后台,就能通过小程序平台的 API 访问到这些数据,以前了解过第三方的 LeanCloud云 和 Bomb 云,没想到小程序现在集成了这些功能,不得不佩服一下腾讯。 也就是,接下来的后台的工作是主要是导入数据,查询小程序后台可知,后台支持导入 json 或者 csv 格式的数据。于是我就写了个脚本,把数据从本地数据库导出到 json 文件中: [代码]import psycopg2 import json # 连接 pgsql 数据库,为保证隐私,密码已隐藏 conn = psycopg2.connect(database="gaokao", user="postgres", password="*******", host="127.0.0.1", port="5432") cur = conn.cursor() cur.execute('select stu_loc,year,stu_wl,pc,control from province') result = [] query_res = cur.fetchall() for i in query_res: item = {} item['stu_loc'] = i[0] item['year'] = i[1] item['wl'] = i[2] item['pc'] = i[3] item['score'] = i[4] result.append(item) # indent=2 控制 json 格式的缩进 # ensure_ascii 控制中文的正常显示 with open("province.json", 'w', encoding="utf-8") as f: f.write(json.dumps(result, indent=2, ensure_ascii=False)) [代码] 这里还有有个坑需要说明一下,小程序后台要求的 json 格式和我们平常意义上的 json 格式还有点区别,首先,json 的所有内容不能被 [ 和 ] 包括起来,而且每个被 {} 所包括得数据项之间不能有逗号。 [图片] 选用 notepad++ 打开原来的 json 文件,使用替换功能就能解决,把 [ 和 ] 替换成空格,把 },替换成 } 即可。 修改之后,在小程序后台通过导入该 json 文件,后台搭建就基本完成了。 小程序端编写 关于小程序端的编写,我主要谈谈两点经验,第一是页面的编写,比如下面这个界面。 [图片] 最开始想实现这样的效果,完全没有思路,最后在从自定义模态弹窗那得到了思路,一开始地区院校这个下拉框对应的布局是隐藏的,在 wxml 文件中通过 hidden=true 控制,一点击 地区/院校 下拉框,就把 hidden 置为 false,如果开始有其他下拉框对应的布局的 hidden 属性是 false 的话,同时要把这些布局的 hidden 属性置为 true 来隐藏其他布局,当然,这里的 true or false 需要在 js 里通过 setData() 动态修改,把修改后的数据从数据层渲染到视图层。 第二是关于小程序云开发的原生 Bug,查询后台时一次只能最多查询到 20 条数据,要实现一次得到所有匹配的结果,需要解决两个问题,第一个问题很自然而然就能想到,第一次查到 20 条数据后,第二次跳过前 20 条再取 20 条,第三次跳过前 40 条再取 20 条,以此类推;还有一个更为致命的问题,查询后台的 API 获取结果的回调函数的 异步 的,也就是说,为了保证获得完整数据,第二次查询需要写在第一次查询的回调里,第三次查询需要写在第二次查询的回调里,而且你还不能显式地知道要查询多少次,需要写多少层这样的嵌套,以及烦人的同名变量覆盖问题,这就是所谓的 异步地狱。为了解决这个问题,需要我们编写代码把这个异步方法转成同步的,具体做法是: 先在所要添加功能的js页面中导入 runtime.js 文件,同时把runtime.js文件放入相应文件夹 ; const regeneratorRuntime = require("…/runtime"); runtime.js 下载地址:https://github.com/inspurer/CampusPunchcard/blob/master/runtime.js 同时模仿下例代码完成业务逻辑: [代码]// 查询可能较慢,最好加入加载动画 wx.showLoading({ title: '加载中', }) const countResult = await db.collection('province').where({ stu_loc: name, pc: pici, }).count() const total = countResult.total //计算需分几次取 const batchTimes = Math.ceil(total / MAX_LIMIT) // 承载所有读操作的 promise 的数组 //初次循环获取云端数据库的分次数的promise数组 for (let i = 0; i < batchTimes; i++) { const promise = await db.collection('province').where({ stu_loc: name, pc: pici, }).skip(i * MAX_LIMIT).limit(MAX_LIMIT).get() //二次循环根据获取的promise数组的数据长度获取全部数据push到newResult数组中 for (let j = 0; j < promise.data.length; j++) { var item = {}; item.code = i * MAX_LIMIT + j; item.name = promise.data[j].stu_loc; item.year = promise.data[j].year; item.wl = promise.data[j].wl; item.pc = promise.data[j].pc; item.score = promise.data[j].score; console.table(promise.data) newResult.push(item) } } if (newResult.length != 0) { that.setData({ hasdataFlag: true, resultData: newResult }) } else { that.setData({ hasdataFlag: false, resultData: newResult }) } // 隐藏加载动画 wx.hideLoading() [代码] 以上就是我本次开发的一些心得体会,欢迎批评指正。 源码地址: https://github.com/TencentCloudBase/Good-practice-tutorial-recommended 联系我们 更多云开发使用技巧及 Serverless 行业动态,扫码关注我们~ [图片]
2019-09-06 - 在云开发批量下载中为什么不能插入数组数据?
var that = this var arr = [] type.get().then(res=>{ var list = res.data for(var i in list){ for(var j in list[i].menu){ wx.cloud.downloadFile({ fileID:list[i].menu[j].image }).then(res=>{ // arr.push(res.tempFilePath) list[i].menu[j].image = res.tempFilePath // arr可以存入所有的临时路径,但是为什么不能往list插入临时路径? that.setData({ list:list }) }) } } }) 思路:将数组数据中的图片路径,通过云开发下载api,从云数据库路径(cloud://xxx)修改为临时路径 问题:临时路径可以push进arr数组,却不能插入list数组。请问应该怎么处理?大佬请赐教。。。
2019-12-29 - 如果 用户第一次选择 拒绝 ,但是在有些业务情况下必须要“允许”情况下
问题是怎么样实现第二次调起“微信授权”窗口,看到别的小程序实现了,求给个思路
2017-10-02 - 小程序如何实现页面跳转到微信聊天
需求场景如下: [图片] 每天需要把员工的计件工资,以文字形式通过微信发给员工, 如何通过“发送员工”按钮直接进入微信,选择联系人,将文字粘贴发给员工?现在的思路是将“发给员工”按钮作为小程序分享按钮,通过留言将相关文字粘贴发给员工,如下: [图片] 需求是,有没有什么方法,不需要分享小程序,而直接有小程序进入微信,选择联系人进行发送。因为每天要发送很多员工,故希望有个更快捷的发送方式。 望社区大神解答,在下不胜感激!~
2019-06-18 - <web-view>组件小程序能放流量主代码吗
- 需求的场景描述(希望解决的问题) 求助,<web-view>内嵌h5做的一个小程序,目前已经能开通小程序流量主了,请问可以把流量主代码添加到<web-view>小程序内吗? - 希望提供的能力 如果可以烦请给个思路哈 如果不支持,希望能改进下
2019-04-12 - 生成二维码海报 带参数
比如上传一个产品,二维码是1个,是不是要后台生成保存数据库 然后 前端扫码识别有多个场景 是不是要带参数 有后台生成二维码 然后 前端 是 动态画出海报的 demo 或者思路吗
2019-02-13 - 如何生成二维码(不是小程序的二维码)
不是扫描打开小程序的二维码。是 一串字符串生成二维码。没有思路,怎么找都不知道。求助求助
2018-11-15 - 实现一个高亮的代码编辑框
实现效果 [图片] 实现思路 说到富文本编辑,首先想到的自然是 [代码]editor[代码] 组件了,然而 [代码]editor[代码] 组件设置 [代码]html[代码] 的方法只有 [代码]setContents[代码],但是这个方法是用来初始化内容的,每次设置都会使得光标变到开头,如果用这个方法,每输一个字符光标都会跳到开头,无法使用。 因此设想了一种新的方案,将编辑和显示分开;底层放置一个 [代码]rich-text[代码] 用于显示高亮后的代码,上层放置一个编辑器,将颜色设置为透明,字体和大小与底层一致;当编辑器输入字符时,通过高亮处理后显示在底层的 [代码]rich-text[代码] 上;这样就实现了一个高亮的代码编辑框 编辑器选择 小程序中一共有 3 种输入框,[代码]input[代码]、[代码]textarea[代码] 和 [代码]editor[代码],其中 [代码]input[代码] 只能输入单行文本,并不适合此场景;[代码]textarea[代码] 可以编辑多行文本,本是个不错的方案,然而一方面 [代码]textarea[代码] 是原生组件,会受到一些限制,另一方面,似乎在真机上给 [代码]textarea[代码] 设置字体无法生效,用默认的字体又有点丑;因此最终还是选用了 [代码]editor[代码] 高亮方案 这里选择了轻量且强大的 prismjs 代码实现 [代码]rich-text[代码] 通过 [代码]absolute[代码] 布局固定在 [代码]editor[代码] 下方,[代码]editor[代码] 被设置成透明颜色(除光标外) [代码]<view class="editor-view"> <rich-text class="highlight" nodes="{{code}}" /> <editor id="editor" class="editor" placeholder="请输入 html" bindinput="input" /> </view> [代码] 每输入一个字符,在 [代码]js[代码] 中进行高亮处理后在 [代码]rich-text[代码] 中显示 [代码]const Prism = require("./prism.js"); Page({ input(e) { // markdown 则改为 Prism.highlight(e.detail.text, Prism.languages.markdown, "markdown") this.setData({ code: "<pre style=\"color:#ccc;font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;\">" + Prism.highlight(e.detail.text, Prism.languages.html, "html") + "</pre>" }) } }) [代码] 应用场景 markdown 编辑器 [代码]markdown[代码] 编辑器作为一种简单的富文本编辑器,通过这样的方式实现就可以比较美观 富文本编辑完成后进行微调 在 [代码]editor[代码] 中编辑完成后,可以通过编辑 [代码]html[代码] 进行样式的微调 性能 在模拟器中是非常流畅的,在真机上稍有延缓,个人觉得是可以接受的(可以适当限制内容长度,过长的内容不进行高亮处理) 立即体验 代码片段
2020-02-21 - 控件上展示一段只读文字,然后点击修改可以编辑这段文字,怎么实现
控件上展示一段只读文字,然后点击修改可以编辑这段文字,怎么实现,textarea,text好像都没有只读模式,editor 有只读和编辑模式,但我要从数据库读一段文字显示在editor 一直实现不了,麻烦大神给指点下思路
2019-06-23 - 云函数获取用户手机号吗?
小程序通过云函数获得用户手机号码? 思路解析, [图片] 了解了小程序的加密方式,我们就可以自己去解密我们需要的信息。如:最困住我们的用户手机号码? 官方是有案例的,想更多学习可以给与参考,但是估计要多看几遍,有node基础的就比较好理解一些。 [图片] 下面是官网地址: https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/signature.html#method-cloud 下面开始说我自己的方法; 1.首先构建云函数,需要两个云函数,一个用来解密,session_key,一个用来解密加密手机号码; [代码]//云函数:getSession; // 云函数入口文件 const cloud = require('wx-server-sdk') //npm install request-promise 通过终端下载npm install wx-server-sdk , const rp = require('request-promise'); cloud.init() // 云函数入口函数 exports.main = async (event, context) => { const _JSCODE = event.code const AccessToken_options = { method: 'GET', url: 'https://api.weixin.qq.com/sns/jscode2session', qs: { appid: '', //你的小程序appid; secret: '', //你的秘钥 grant_type: 'authorization_code', js_code: _JSCODE }, json: true }; const resultValue = await rp(AccessToken_options); return { resultValue } } [代码] 下载好需要的两个包,就可以对云函数初始化,执行npm init 有个起名字的环节,用过node的都知道,默认index.js,有个选择 [代码]package name: (gettoken) index.js version: (1.0.0) description: git repository: keywords: author: license: (ISC) About to write to D:\projects\zy_face_id_wxs\server\getToken\package.json: { "name": "index.js", "version": "1.0.0", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "request-promise": "^4.2.4", "wx-server-sdk": "^0.8.1" }, "devDependencies": {}, "description": "" } Is this OK? (yes) yes [代码] 这是初始化,终端的代码; 右键点击上传并部署到云端; 下面是客户端的代码; [代码] getPhoneNumber(e) { if (!e.detail.errMsg || e.detail.errMsg != "getPhoneNumber:ok") { wx.showModal({ content: '不能获取手机号码', showCancel: false }) return; } wx.showLoading({ title: '获取手机号中...', }) console.log(e) wx.login({ success(res) { if (res.code) { console.log(res.code) console.log(e.detail.iv) console.log(e.detail.encryptedData) wx.cloud.callFunction({ name: 'getSession', //调用云函数获取session_key; data: { code: res.code, }, success: res => { wx.hideLoading() // console.log(res.result.resultValue) var data = res.result.resultValue console.log(data) console.log(data.session_key) //获取到了session_key的值; }, fail: error => { console.log(error) } }) } } }) }, [代码] 2.构建第二个云函数 GetWX; [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') // const requestpromise = require('request-promise'); var WXBizDataCrypt = require('./RdWXBizDataCrypt') // 用于手机号解密 cloud.init() exports.main = async (event, context) => { const session_key = event.session_key //appid写入你自己的appid,session_key 用第一个云函数的返回值; const pc = new WXBizDataCrypt(appid, session_key ) // -解密第一步 const data = pc.decryptData(event.encryptedData, event.iv) // 解密第二步 return { data } } [代码] 这个同样执行上面的步骤,npm install wx-server-sdk 和初始化npm init; 重点来了,解密所需要的js文件。 [图片]` 这两个文件已经上传到我的网盘里面,需要的请下载; https://pan.baidu.com/s/1VrS1gX_Bw3dKaZkQnNzy2A 提取码:cxnh; 格式和图片的保持一致,并上传到云端。 3.开始前端调用了; 代码如下; [代码]getPhoneNumber(e) { if (!e.detail.errMsg || e.detail.errMsg != "getPhoneNumber:ok") { wx.showModal({ content: '不能获取手机号码', showCancel: false }) return; } wx.showLoading({ title: '获取手机号中...', }) console.log(e) wx.login({ success(res) { if (res.code) { console.log(res.code) console.log(e.detail.iv) console.log(e.detail.encryptedData) wx.cloud.callFunction({ name: 'getSession', //调用云函数获取session_key; data: { code: res.code, }, success: res => { wx.hideLoading() var data = res.result.resultValue console.log(data) console.log(data.session_key) //获取到了session_key的值; const session_key=data.session_key wx.cloud.callFunction({ name:'getWX', //解析秘文,获得手机号码; data:{ session_key: session_key, encryptedData: e.detail.encryptedData, iv: e.detail.iv, }, success:res=>{ console.log(res) }, fail:err=>{ console.log(err) } }) }, fail: error => { console.log(error) } }) } } }) }, [代码] 4.输出的结果: [图片] 遇到问题了在私信问我,一一回答。
2019-07-17 - 小程序radio组件,当选项文本为多行时,点选按钮变形的问题。
小程序radio组件,当选项文本为多行时,点选按钮变形的问题,如下图: [图片] 【解决思路】 1、固定点选按钮的尺寸。 2、把点选按钮和选项文本单独分开成2列,只是在页面布局上并排显示,且按钮和文本同时共享同一个事件处理函数。 以上只是思路,正在尝试实践。如果有高手看到,希望能有更好的解决方案。
2019-01-19 - 怎么保持背景音乐离开小程序依然播放
使用wx.wx.playBackgroundAudio进行背景音乐播放,显示在聊天顶部这个如何实现,我看到好多音乐类小程序都会自动显示在聊天顶部,当离开小程序后,背景音乐还能播放;请问各位大神解说一下思路~~~~~
2018-07-02 - super 码农 copy 2
最近有不少同学不会写某个功能,在社区里发帖要代码的,还请大家先思考一下,发帖子的时候请带上自己的思路,而不是直接要代码,多思考对自己有帮助。 所谓没吃过猪肉,也得要见过猪跑啊。虽然今年猪肉特别贵。 言归正传,刚接触开发的同学,先学会百度,绝大部分碰到的问题或者要开发的功能,别人都碰到过,也做过。 比如服务号消息配置,大家碰到的token验证失败,access_token访问超限这些,github、码云上面都有封装好的代码,开箱即用。初次接触服务号开发,坑会很多,很浪费时间。 开发越久,头发越少,且行且珍惜。 [图片] 示例代码链接:https://developers.weixin.qq.com/s/zlh6FBmb7qbb 参考项目:https://github.com/Binaryify/vue-tetris ps: 大自然的搬运工。 我于杀戮之中盛放,一如黎明中的花朵。
2019-09-23 - 做完一个小程序,总结出一个入门开发课程
根据自己从零开始做「字节加工厂」这个小程序的经验,我计划写一个《微信小程序开发实战》的课程,当前已经完成了**「入门篇」**,介绍微信小程序开发,从 0 到 1 这个过程。 虽然说是入门篇,但是涉及的内容还是挺多的,也并不基础。有些内容涉及到 Web 前端的知识点,还有些内容涉及到 Node 知识点,例如「使用云函数开发」这篇,示例代码中还涉及到了 Node 中 https API 的使用。 技术相关的内容,基础大抵都是类似的。看似形形色色的不同形态,不同语言,不同架构,其实只是应用层面的使用方法不同而已。 最重要的还是基本功,越是基础的东西,越难习得,当然它的价值也最高。 这仅仅指的是技术层面,技术层面虽然很难,但还不是最难的,最难的是思路,也就是「产品能力」。 很多时候,我们学会了很多技术,却不知道用来做什么。 这个课程,我也没有办法去解决这个问题。 能够解决的是,帮助你将官方文档窜起来,从实际问题出发,去解决问题。文档终归是文档,它只是罗列出使用说明,我希望通过这个课程,带你学会实际解决问题的能力。 按照我的学习路径,《入门篇》我总结了 12 篇文章,如下目录: 了解小程序的页面逻辑 从写一个完整的页面开始 使用 Map API,完成一个页面交互 使用 Storage API,实现数据持久化保存 使用 Canvas API,做一个分享卡片 页面传参的几种方式 学会使用第三方 NPM 扩展包 使用 request API,调用第三方接口数据 使用云函数开发,绕过设置合法域名信息 学会云函数的本地测试以及云端测试 聊一聊小程序的服务端开发 学会使用云开发数据库能力 根据我的经验,假如你真的学会了上述文章中提到的知识点,微信小程序开发肯定是入门了,如果再深入一点,可能往「全栈开发」也踏入了半只脚。 当然,这个入门篇并非适合所有「新人」,它是根据我的学习路径而成,我本身是具备 Web 前端 以及 Node 开发经验的。 所以,如果你正好也有类似的开发经验,那么,这个入门篇的内容,对于你而言,可能要容易得多了,至少也能帮你节省一些时间,少走一些弯路。 写完《入门篇》之后,又花了点时间,整理成了 PDF 电子书,欢迎加入我的免费知识星球「字节加工厂」,获取电子书。 [图片] 写完「入门篇」,后面计划开始整理「效率篇」,文章将会同步更新在 GitHub 上,仓库地址:https://github.com/pengloo53/miniprogram-articles 欢迎 star
2019-10-08 - 小程序能不能制作gif格式的图
小程序能不能制作gif格式的图,如何实现一组图片生成gif格式,和gif格式拆分成图片组,给个制作思路
2019-01-09 - 教你解决showLoading 和 showToast显示异常的问题
问题描述 当wx.showLoading 和 wx.showToast 混合使用时,showLoading和showToast会相互覆盖对方,调用hideLoading时也会将toast内容进行隐藏。 触发场景 当我们给一个网络请求增加Loading态时,如果同时存在多个请求(A和B),如果A请求失败需要将错误信息以Toast形式展示,B请求完成后又调用了wx.hideLoading来结束Loading态,此时Toast也会立即消失,不符合展示一段时间后再隐藏的预期。 解决思路 这个问题的出现,其实是因为小程序将Toast和Loading放到同一层渲染引起的,而且缺乏一个优先级判断,也没有提供Toast、Loading是否正在显示的接口供业务侧判断。所以实现的方案是我们自己实现这套逻辑,可以使用Object.defineProperty方法重新定义原生API,业务使用方式不需要任何修改。 代码参考 [代码]// 注意此代码应该在调用原生api之前执行 let isShowLoading = false; let isShowToast = false; const { showLoading, hideLoading, showToast, hideToast } = wx; Object.defineProperty(wx, 'showLoading', { configurable: true, // 是否可以配置 enumerable: true, // 是否可迭代 writable: true, // 是否可重写 value(...param) { if (isShowToast) { // Toast优先级更高 return; } isShowLoading = true; console.log('--------showLoading--------') return showLoading.apply(this, param); // 原样移交函数参数和this } }); Object.defineProperty(wx, 'hideLoading', { configurable: true, // 是否可以配置 enumerable: true, // 是否可迭代 writable: true, // 是否可重写 value(...param) { if (isShowToast) { // Toast优先级更高 return; } isShowLoading = false; console.log('--------hideLoading--------') return hideLoading.apply(this, param); // 原样移交函数参数和this } }); Object.defineProperty(wx, 'showToast', { configurable: true, // 是否可以配置 enumerable: true, // 是否可迭代 writable: true, // 是否可重写 value(...param) { if (isShowLoading) { // Toast优先级更高 wx.hideLoading(); } isShowToast = true; console.error('--------showToast--------') return showToast.apply(this, param); // 原样移交函数参数和this } }); Object.defineProperty(wx, 'hideToast', { configurable: true, // 是否可以配置 enumerable: true, // 是否可迭代 writable: true, // 是否可重写 value(...param) { isShowToast = false; console.error('--------hideToast--------') return hideToast.apply(this, param); // 原样移交函数参数和this } }); [代码] 调整后展示逻辑为: 优先级:Toast>Loading,如果Toast正在显示,调用showLoading、hideLoading将无效 调用showToast时,如果Loading正在显示,则先调用 wx.hideLoading 隐藏Loading
2019-10-30 - ios系统版本为9.3.5不支持原生js的Object.values()方法
手机系统版本:ios 9.3.5 微信版本:7.0.4 问题描述:不支持JS的Object.values()。 解决办法的思路(还未尝试): 1、引进第三方的js库,比如underscore,放弃js的原生写法。 2、用到js原生方法的地方加上判断,如果不支持 那么抛出异常 或者 自己写一些工具函数来替代。 疑惑点: 微信的版本为当前的最新版(问题发布时间为:2019-05-31),那么按理来说,最新版本的微信应该会为我们检测系统版本等一系列的支持。 大家有什么思路吗,我还挺喜欢JS的原生写法的,有没有思路往原生写法上靠的?
2019-05-31 - css实现打勾操作
参考博客上一位大佬的实现思路,使用css实现动效打勾。 思路真是个好东西,而我却没有! 详见以下代码片段: https://developers.weixin.qq.com/s/0lyq6Gmn7M68 昨天下午接到个需求,希望成功后,能够出现个动效打勾的弹出层提醒用户成功,想着用canvas实现,奈何canvas用的贼菜,只能实现静态的,动效的死活搞不出来。想着google以下canvas实现动效打勾,却发现个css实现的博客,感觉发现了新大陆,甚是欢喜。 不得不说这个思路太赞!我恐怕的想两天才会想到。特来记录下,提醒自己想问题思路宽一点!
2019-03-07 - [填坑手册]小程序Canvas生成海报(一)--完整流程
[图片] 海报生成示例 最近智酷君在做[小程序]canvas生成海报的项目中遇到一些棘手的问题,在网上查阅了各种资料,也踩扁了各种坑,智酷君希望把这些“填坑”经验整理一下分享出来,避免后来的兄弟重复“掉坑”。 [图片] 原型图 这是一个大致的原型图,下面来看下如何制作这个海报,以及整体的思路。 [图片] 海报生成流程 [代码片段]Canvas生成海报实战demo demo的微信路径:https://developers.weixin.qq.com/s/Q74OU3m57c9x demo的ID:Q74OU3m57c9x 如果你装了IDE工具,可以直接访问上面的demo路径 通过代码片段将demo的ID输入进去也可添加: [图片] [图片] 下面分享下主要的代码内容和“填坑现场”: 一、添加字体 https://developers.weixin.qq.com/miniprogram/dev/api/canvas/font.html [代码]canvasContext.font = value //示例 ctx.font = `normal bold 20px sans-serif`//设置字体大小,默认10 ctx.setTextAlign('left'); ctx.setTextBaseline("top"); ctx.fillText("《智酷方程式》专注研究和分享前端技术", 50, 15, 250)//绘制文本 [代码] 符合 CSS font 语法的 DOMString 字符串,至少需要提供字体大小和字体族名。默认值为 10px sans-serif 文字过长在canvas下换行问题处理(最多两行,超过“…”代替) [代码]ctx.setTextAlign('left'); ctx.setFillStyle('#000');//文字颜色:默认黑色 ctx.font = `normal bold 18px sans-serif`//设置字体大小,默认10 let canvasTitleArray = canvasTitle.split(""); let firstTitle = ""; //第一行字 let secondTitle = ""; //第二行字 for (let i = 0; i < canvasTitleArray.length; i++) { let element = canvasTitleArray[i]; let firstWidth = ctx.measureText(firstTitle).width; //console.log(ctx.measureText(firstTitle).width); if (firstWidth > 260) { let secondWidth = ctx.measureText(secondTitle).width; //第二行字数超过,变为... if (secondWidth > 260) { secondTitle += "..."; break; } else { secondTitle += element; } } else { firstTitle += element; } } //第一行文字 ctx.fillText(firstTitle, 20, 278, 280)//绘制文本 //第二行问题 if (secondTitle) { ctx.fillText(secondTitle, 20, 300, 280)//绘制文本 } [代码] 通过 ctx.measureText 这个方法可以判断文字的宽度,然后进行切割。 (一行字允许宽度为280时,判断需要写小点,比如260) 二、获取临时地址并设置图片 [代码]let mainImg = "https://demo.com/url.jpg"; wx.getImageInfo({ src: mainImg,//服务器返回的图片地址 success: function (res) { //处理图片纵横比例过大或者过小的问题!!! let h = res.height; let w = res.width; let setHeight = 280, //默认源图截取的区域 setWidth = 220; //默认源图截取的区域 if (w / h > 1.5) { setHeight = h; setWidth = parseInt(280 / 220 * h); } else if (w / h < 1) { setWidth = w; setHeight = parseInt(220 / 280 * w); } else { setHeight = h; setWidth = w; }; console.log(setWidth, setHeight) ctx.drawImage(res.path, 0, 0, setWidth, setHeight, 20, 50, 280, 220); ctx.draw(true); }, fail: function (res) { //失败回调 } }); [代码] 在开发过程中如果封面图无法按照约定的比例(280x220)给到: 那么我们就需要处理默认封面图过大或者过小的问题,大致思路是:代码中通过比较纵横比(280/220=1.27)正比例放大或者缩小原图,然后从左上切割,竟可能保证过高的图是宽度100%,过宽的图是高度100%。 在canvas中draw图片,必须是一个(相对)本地路径,我们可以通过将图片保存在本地后生成的临时路径。 微信官方提供两个API: wx.downloadFile(OBJECT)和wx.getImageInfo(OBJECT)。都需先配置download域名才能生效。 三、裁切“圆形”头像画图 [代码]ctx.save(); //保存画图板 ctx.beginPath()//开始创建一个路径 ctx.arc(35, 25, 15, 0, 2 * Math.PI, false)//画一个圆形裁剪区域 ctx.clip()//裁剪 ctx.closePath(); ctx.drawImage(headImageLocal, 20, 10, 30, 30); ctx.draw(true); ctx.restore()//恢复之前保存的绘图上下文 [代码] 使用图形上下文的不带参数的clip()方法来实现Canvas的图像裁剪功能。该方法使用路径来对Canvas话不设置一个裁剪区域。因此,必须先创建好路径。创建完整后,调用clip()方法来设置裁剪区域。 需要注意的是裁剪是对画布进行的,裁切后的画布不能恢复到原来的大小,也就是说画布是越切越小的,要想保证最后仍然能在canvas最初定义的大小下绘图需要注意save()和restore()。画布是先裁切完了再进行绘图。并不一定非要是图片,路径也可以放进去~ 小程序 canvas 裁切BUG [代码]ctx.setFillStyle("#fff"); ctx.fillRect(0, 0, 320, 500); //第一个填充矩形 wx.downloadFile({ url: headUri, success(res) { ctx.beginPath() ctx.arc(50, 50, 25, 0, 2 * Math.PI) ctx.clip() ctx.drawImage(res.tempFilePath, 25, 25); //第二个填充图片 ctx.draw() ctx.restore() ctx.setFillStyle("#fff"); ctx.fillRect(0, 0, 320, 500); ctx.draw(true) ctx.restore() } }) [代码] clip裁切这个功能,如果有超过一张图片/背景叠加,则裁切效果失效。 错误参考:http://html51.com/info-38753-1/ 四、将canvas导出成虚拟地址 [代码]wx.canvasToTempFilePath({ fileType: 'jpg', canvasId: 'customCanvas', success: (res) => { console.log(res.tempFilePath) //为canvas的虚拟地址 } }) res: { errMsg: "canvasToTempFilePath:ok", tempFilePath: "http://tmp/wx02935bb29080a7b4.o6zAJswFAuZuKQ5NZfPr….cGnD1a02PlVC0b3284be3a41d08986c2477579a5fd8e.jpg" } [代码] 这里需要把canvas里面的内容,导出成一个临时地址才能保存在相册,比如: http://tmp/wx02935bb29080a7b4.o6zAJswFAuZuKQ5NZfPr5UfJVR4k.cGnD1a02PlVC0b3284be3a41d08986c2477579a5fd8e.jpg 五、询问并获取访问手机本地相册权限 [代码]wx.getSetting({ success(res) { console.log(res) if (!res.authSetting['scope.writePhotosAlbum']) { //判断权限 wx.authorize({ //获取权限 scope: 'scope.writePhotosAlbum', success() { console.log('授权成功') //转化路径 self.saveImg(); } }) } else { self.saveImg(); } } }) [代码] 判断是否有访问相册的权限,如果没有,则请求权限。 六、保存到用户手机本地相册 [代码]wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath, success: function (data) { wx.showToast({ title: '保存到系统相册成功', icon: 'success', duration: 2000 }) }, fail: function (err) { console.log(err); if (err.errMsg === "saveImageToPhotosAlbum:fail auth deny") { console.log("当初用户拒绝,再次发起授权") wx.openSetting({ success(settingdata) { console.log(settingdata) if (settingdata.authSetting['scope.writePhotosAlbum']) { console.log('获取权限成功,给出再次点击图片保存到相册的提示。') } else { console.log('获取权限失败,给出不给权限就无法正常使用的提示') } } }) } else { wx.showToast({ title: '保存失败', icon: 'none' }); } }, complete(res) { console.log(res); } }) [代码] 保存到本地需要一定的时间,需要加一个loading的状态。 七、关于组件中引用canvas [代码]let ctx = wx.createCanvasContext('posterCanvas',this); //需要加this [代码] 在components中canvas无法选中的问题: 在components自定义组件下,当前组件实例的this,表示在这个自定义组件下查找拥有 canvas-id 的 <canvas> ,如果省略则不在任何自定义组件内查找。
2021-09-13 - 【圣诞节】给你的头像加个圣诞帽吧
圣诞节快来了,来给你的头像加个帽子吧 看着大伙都在弄这个,我自己也来试一哈,我分别用了两种方式来实现,一种是普通的方式,一种是wxs方式 普通方式 效果图如下: [图片] 思路 获取头像 选择素材 缩放,移动,旋转素材 生成canvas 生成图片,保存图片 实现方式 [图片] 首先是获取头像,这个不用说,大家应该都会的。 选择素材这里我准备了三张圣诞帽的素材,这个网上有很多,可以自己找下,然后我还做了一个选择手机相册的功能,如果你自己有素材的话也可以直接选择这个功能。 缩放,移动,旋转素材都是通过触摸函数去实现的,这里是先将布局做好,然后在标签上面绑定各个触摸事件,通过返回的值在标签的style里设置实现各个效果。 调整好了之后点击保存头像会获取所有参数并将头像画出来,再通过 [代码]wx.canvasToTempFilePath()[代码] 将canvas生成图片最后通过 [代码]wx.saveImageToPhotosAlbum()[代码] 保存图片。 主要代码 主要的函数就是下面这几个,代码片段我会放在文末,没有什么比较难的地方,就是要注意下计算的时候不要算错就行。 [图片] 需要注意的点 由于素材的大小可能会有不同,所以在重新选择素材的时候高度要重新设置一下,这里我用了一个方法来重置高度,主要是每次重新选择素材的时候就用 [代码]wx.getImageInfo()[代码] 这个api去获取图片素材的宽高,再计算出宽高比。 [图片] wxs实现方式 实现方式 思路跟普通方式是一样的,不同的是这里将绑定事件通过 [代码]wxs[代码] 去实现,直接设置标签的参数而不通过逻辑层去处理,在性能上会比较好一点,不过这种实现方式在进行旋转的时候最后生成的图片会有不准,后面会说到。 参数的获取是通过在标签上设置style,然后点击保存的时候用 [代码]wx.createSelectorQuery()[代码] 获取各个参数的 [图片] [图片] 获取旋转的值 由于 [代码]wx.createSelectorQuery()[代码] 并不能获取到 [代码]rotate[代码] 这个参数,所以我是通过下面这种方式来拿到旋转的值的,将旋转值以宽度的形式赋值给 [代码].vo-ro[代码] [图片] [图片] 但是我发现旋转之后生成的图片不是正确的,原因是旋转之后通过 [代码]wx.createSelectorQuery()[代码] 拿到的宽高并不是图片大小的宽高,而是旋转之后的宽高,按理来说不应该是这样的,即使通过样式旋转,它的宽高应该保持不变才对,这样就造成了参数上的错误,所以画出来的图片是不准确的。 因为加了旋转之后画出来的图片会不准确,暂时想不出别的方法,我把旋转的按钮先注释掉了,只支持缩放跟拖拽。 总结 两种方式,wxs性能要更好,但是效果没第一种的好,看你要哪种了,最后祝大家圣诞节快乐,祝你生活愉快 https://developers.weixin.qq.com/s/Cizd1RmY7qdg
2019-12-25 - 小程序不能够动态加载图片?
需求: 由于同一类下的不同产品,希望通过wx:for 循环加载显示所有产品,而不同类型产品图片各不相同。 目前具体做法: 写一个按照类型取图片的js方法:如下: function activityImg(type) { var statusJson = {}; var x = ""; switch (type) { case 1: x = "ssq.jpg"; break; case 2: x = "dlt.jpg"; break; } statusJson.x = x; return statusJson; } 在对应的wxml文件引入该文件: <wxs src='../../utils/stateUtils.wxs' module='product /> 最有在需要动态显示地方这样使用: [图片] 这个样子,动态显示图片思路如上!不知道为什么这样,不能够显示图片! 请问: 1、这种方案要如何优化 才能正确显示图片? 2、如果这方思路不能显示图片,请问 这种需求下,要求动态显示图片的,采用什么方式可以显示图片? 谢谢
2019-11-30 - 一键完成小程序国际化
随着小程序使用的人数增长,小程序管理后台陆续收到非中文母语的用户要求支持英文的请求。小程序一开始是直接在程序里使用中文字符串的方式,要做国际化只能把这些硬编码中文的地方全部替换为i18n调用。写了个脚本跑了下,发现小程序里涉及到需要转换硬编码的地方有2000+处。。。手动修改不太科学,于是写了个名为mina-i18n的工具,用于小程序的国际化转换,有兴趣的同学可以用自己的小程序项目实验一下: npm install -g mina-i18n mina-i18n /path/to/origin/mina/project /path/to/i18n/mina/project工具主要包含对JS和WXML两种文件的处理,JS使用babel处理,WXML使用htmlparser库处理,json和css由于没法使用函数调用,这个需要用户自行处理了。 JS处理: JS文件的处理思路:找到中文字符串,将其改为以中文字符串为参数的i18n函数调用。我们的目标是一键转换后可以直接运行,而替换的行为发生在任何一个文件里,所以我们需要一个全局可访问的函数。在小程序里,有两个可以全局访问的变量,一个是getApp(),一个是wx,为了代码的简洁性,我们决定在wx上挂载一个L函数作为全局i18n函数。即: "中文" 转换为 wx._t("中文")直接使用正则表达式匹配是无法做到的符合语法的替换,这里使用babel完成保留程序上下文的替换,替换逻辑写在babel 的插件里的 StringLiteral 的回调里(所有JS程序里解析到的字符串都会进入这个回调),代码如下: const visitor = { StringLiteral(path) { const parentPath = path.parent // 每个中文字符串只处理一次,识别到已处理过就退出,不然会死循环 if (t.isCallExpression(parentPath)) { const callee = parentPath.callee if (t.isMemberExpression(callee)) { const { object, property } = callee if ( t.isIdentifier(object) && object.name === MINA_I18N_JS_FUNCTION_CALLEE && property.name === MINA_I18N_FUNCTION_NAME ) { return } } } const reg = /\s*[\u4E00-\u9FA5]+\s*/g const stringValue = path.node.value if (reg.test(stringValue)) { path.replaceWith(t.CallExpression( t.MemberExpression( t.identifier(MINA_I18N_JS_FUNCTION_CALLEE), t.identifier(MINA_I18N_FUNCTION_NAME) ), [t.stringLiteral(stringValue)] )) } } } WXML处理: 对WXML的处理思路与JS类似,就是找出WXML文件里的中文文本,替换为以文本为参数的i18n函数调用。在小程序里使用函数调用需要用到 WXS语言 ,这个语言无法使用小程序API,只有极少数的类库,反正你当成是个纯运算逻辑的WXML语法助手就行。因为WXS语言没法从外部获取数据,或者WXML里的i18n函数调用只能是类似 i18nFunc("中文","en") 这种形式,就是说具体要转为哪种语言,必须明确地告知WXS的函数。一般用户的语言都是从系统信息中获取,或者用户手动选择后存在服务端,这个语言的变量信息我们在JS里可以获取,而且这个变量每个页面都会用到,而WXML里是没法访问到getApp()和wx这些全局变量的,我们只能在每个页面的Page函数的data变量里加上这个语言变量Lang,才能在每个WXML里使用类似 i18nFunc("中文", Lang) 的形式来做文本替换。处理代码如下: const visitor = { ObjectProperty(path, state) { if (path.node.key.name === 'data') { const parentPath = path.parentPath || {} const parentParentPath = parentPath.parentPath || {} const node = parentParentPath.node if ( t.isObjectExpression(parentPath) && t.isCallExpression(parentParentPath) && node && node.callee && t.isIdentifier(node.callee) && node.callee.name === 'Page' ) { // 变量插入只处理一次,识别到已处理过就退出,不然会死循环 if ( t.isCallExpression(path.node.value) && t.isObjectExpression(path.node.value.arguments[0]) && path.node.value.arguments[0].properties[0].key.name === MINA_I18N_LANG_NAME ) { return } path.replaceWith( t.objectProperty( path.node.key, t.CallExpression( t.MemberExpression( t.identifier('Object'), t.identifier('assign') ), [ t.objectExpression([ t.objectProperty( t.identifier(MINA_I18N_LANG_NAME), t.CallExpression( t.MemberExpression( t.identifier(MINA_I18N_JS_FUNCTION_CALLEE), t.identifier(MINA_I18N_GETLANG_FUNCTION_NAME) ), [] ) ) ]), path.node.value ] ) ) ) } } } } 有了可在WXML里运行的 i18n函数,接下来就是对WXML源文件里的中文文本进行替换。WXML的转换处理要比JS复杂一些,处理JS的时候babel帮我们做好了语法树的解析、遍历和代码生成过程,我们只要实现语法树访问的回调就行,WXML就没有这么好用的工具了,所以需要自己做多点工作。WXML的解析用了 htmlparser2 ,这个库会返回给你一棵DOM树,按着根节点就能遍历整棵树。不过在实际解析生成WXML的时候,发现这个库有个地方没法满足,就是它解析出来的节点属性无法区分开是为空还是 boolean attributes ,也就是说<view hidden></view>和<view hidden=""></view>解析出来的结果都是一样的{hidden: ""},这其实有很大的歧义,因为在小程序里<view hidden></view>会表示这个view的hidden=true,而<view hidden=""></view>这会被解释为hidden=false,因此按照这个解析结果生成的话,就发现原来小程序里应该隐藏的view都被显示了。。。搜了下其他有其他人给作者提过这个问题的 issue ,我看作者的意思大概是这个库不是用来处理序列化或者生成代码这类事的,在github搜了下好像这个库又确实是JS里HTML解析库里排名第一的,于是就自己fork出来 一份,加了个选项来做boolean attributes 的区分。 在做WXML处理的时候,我犯了个错误,就是把处理WXML跟处理HTML这两件事等同起来了,可能之前做过一些HTML相关的处理工作,所以写着写着就很顺地按着原来用正则表达式处理的方式去做了,结果发现在处理节点属性为{{xxx}}的动态属性时总是有edge case处理不到。中间隔了两天去做其他需求后再回来一想,这{{xxx}}里的xxx就是JS代码啊,直接用babel不就完事了吗?困在原来处理HTML的思路里调试了大半天浪费时间。正确的处理WXML的方式就是用htmlparser解析静态文本,解析到的{{xxx}}语法交给babel处理,最后再重新合成WXML代码。这里面还有属性的单双引号处理、uincode与汉字的一些处理等细节,具体就不展开了,代码里都有: function buildWXML(root) { if (Array.isArray(root)) { let xmlString = '' root.forEach(node => { xmlString += buildWXML(node) }) return xmlString } else if (root.type === 'text') { const text = root.data return processMinaTemplateText(text) } else if (root.type === 'tag') { const tagName = root.name.replace('wx-', '') let tagString = `<${tagName}` const attr = root.attribs || {} Object.keys(attr).forEach(key => { if (attr[key] === null) { tagString += ` ${key}` } else { const attrValue = processMinaTemplateText(attr[key], { isAttrValue: true }) tagString += ` ${key}="${attrValue}"` } }) const children = root.children || [] if (isSelfCloseTag(tagName) && children.length === 0) { tagString += '/>' } else { tagString += '>' children.forEach(node => { tagString += buildWXML(node) }) tagString += `</${tagName}>` } return tagString } else if (root.type === 'comment') { return `<!--${root.data}-->` } else { return '' } } function processMinaTemplateText(text, options = {}) { const textArray = text.split(/({{[^}]*}})/g) let returnText = '' textArray.forEach(item => { if (item.startsWith('{{') && item.endsWith('}}')) { let expression = item.substring(2, item.length - 2) returnText += '{{' + processMinaTemplateExpression(expression) + '}}' } else { returnText += processPlainText(item) } }) return returnText } function processMinaTemplateExpression(code) { const visitor = { StringLiteral(path) { const parentPath = path.parent if (t.isCallExpression(parentPath)) { const callee = parentPath.callee if (t.isIdentifier(callee) && callee.name === MINA_I18N_FUNCTION_NAME) { return } } const reg = /\s*[\u4E00-\u9FA5]+\s*/g const stringValue = path.node.value if (reg.test(stringValue)) { path.replaceWith(t.CallExpression( t.identifier(MINA_I18N_FUNCTION_NAME), [t.stringLiteral(stringValue), t.identifier(MINA_I18N_LANG_NAME)] )) createI18NData(stringValue) } } } try { const result = babel.transform(code, { plugins: [ { visitor } ], generatorOpts: { quotes: 'single', compact: false } }) const i18nScriptContent = transformText(toSingleQuotes(result.code)) return i18nScriptContent.replace(/[\r\n]+$/g, '').replace(/^;/g, '').replace(/;$/g, '') } catch (e) { console.log(`parser expression : ${code}, error: ${JSON.stringify(e)}`) return code } } 翻译:一开始做这个工具的目的,就是要做到一键转换后就可以在微信开发者工具里跑起来。经过上面两步的处理,我们已经把项目里出现过的所有中文字符串都提取并替换,接下来我们就要实现 i18n 翻译函数。 最简单的是简体和繁体的转换,因为已经有 opencc 这个优秀的开源库可以处理 。 const OpenCC = require('opencc') const opencc = new OpenCC() const hant_text = opencc.convertSync(text) 中英文的翻译没法离线,只能找线上服务。在找了google,有道,金山,必应,百度等翻译服务后,发现微软的必应是最方便的,不需要申请token或者去对抗防爬虫,翻译效果也不错,非常适合用在一个可以无条件使用的工具里,接口调用非常简洁: request.post({ url: 'https://cn.bing.com/ttranslatev3', form: { fromLang: 'zh-Hans', to: 'en', text: text }, json: true, timeout: 2000 }).then(res => { return res[0].translations[0].text }) 绝大多数中文项目的i18n做到繁体+英文就够了,外贸项目等特殊情况的话,改一下调用bing API的语言参数就行,bing支持多种语言翻译。 至此,工具也算基本完成了,使用了自己小程序和github上找的几个开源小程序项目做测试,基本都是一键转换后就能直接在开发者工具上跑的,真机预览也没出问题。有兴趣的同学可以在自己项目的小程序上跑跑看。我看了下日常使用的各个小程序,可以说几乎100%都是没有提供切换语言选项的,万一以后有需求的话,希望这个工具可以方便到大家。 对源码有兴趣的同学可以到 github 看一下,如果转换的小程序跑起来有错误的话也欢迎提issue,我会尽快解决的。
2019-08-13 - 复杂瀑布流长列表页踩坑记录,内存不足问题【1】
这篇文章主要是解决小程序无限滚动瀑布流页面引起的ios内存不足,自动退出问题 问题回顾:我们有一个列表展示页,是无限瀑布流式的,展示的元素我们封装成了单个组件,暂且叫它[代码]Item组件[代码]。这个瀑布流包含若干个Item组件,并且这个Item组件也比较复杂,包含各种展示样式(根据不同类型,大概有9种吧,反正渲染节点很多),在进行滑动的过程中,item大概加载30-40个以后,就会造成小程序内存不足而退出,蓝瘦香菇… 点击此处查看二期 解决思路: 将超出屏幕一定部分的列表内的组件进行不渲染的处理(也就是用wx:if卸载掉组件),当到达渲染临界点时再开始渲染;保证每次少量的数据展示。 我们的项目中是保持15条Item,我们是每次分页请求5条,按照前5条,中间5条和后5条来划分,如果不在这个范围,则用一个等高度的骨架代替,并且卸载这些组件 实现方式 使用曝光监听,当一个Item曝光时,记录Item高度,并放到数组里面,作为骨架的填充高度,如果已经记录了高度,则不再重复记录;曝光时向外传递一个当前渲染范围的中心值(比如当前Item所属页码,或者当前Item索引),以此进行处理; 这里有一点要注意,如果你的列表item组件比较复杂,需要在ready的时候将记录的高度设置为item最小高度,不然组件重新装载时会有一定的渲染时间,在临界点会造成跳屏【此处已经通过骨架组件解决,可以忽略,只是作为踩坑记录】 此时优化点 为避免频繁setData和渲染,做了防抖函数,时间是600ms 此时缺点 滑动特别快时,会出现白屏,是因为曝光监听是在组件里面,而超快速滚动时,组件没有装载进来,也无法进行曝光监听,所以无法触发,这里考虑用骨架组件进行二次监听曝光 优化迭代 将骨架组件作为外壳套在Item外面(用[代码]slot[代码]),并对骨架进行监听曝光,可以解决上面缺点 给骨架组件做一个常规骨架屏样式,而不是纯白色,看起来更优雅 最后,还是尽量减少节点数,优化代码
2019-12-05 - 开源的垃圾分类小程序
1.第一阶段了解 一开始了解小程序不知道是在某年某月的新闻中看见的,感觉这是一个流量入口,自己仅仅是了解,没有真的动手去学习开发,主要是工作时期比较多一直没有时间。现在想想是真的来迟了,好在是我没有忘记 2. 第二阶段学习 学习的时候感觉和vue 有一点点相似的感觉。 花了一段时间看的开发文档,感觉文档清晰,思路简洁。很多人喜欢看视频,我到是不是很喜欢。因为看视频的速度特别慢。只要文档足够细基本看完就差不多了,当然了论坛我也是常来的那种,看看大家都出问题在什么地方,一些坑的问题只要是开发程序就会有。哈哈 3. 第三阶段开发 开发都是照着例子,比如在学习云开发的时候,默认的那个项目就完全把主要的云开发内容演示了。我基本都是照壶画瓢。 4. 沉入其中 现在做项目之前我首先想想要不要开发app.公司现在我首先说服领导优先开发小程序。公司的其他可以改的我基本都说服大家优先小程序。我们公司点饭系统就被我改为小程序版本(内部订餐)。 5. 我的开源小程序 代码本身不太具有多少价值,程序员么开源精神,哈哈哈哈哈 地址github Garbage
2020-05-09 - 请问下怎样修改动态列表的值
[图片]点击加号三个都加 点击减号三个都减。请问怎么才能定位到某一行加或减呢?给个大概的思路就行 现在能取到一行的唯一标识 但是不知道怎么修改,求大神解答
2019-07-04 - 文本显示如何实现如下效果?
[图片] 当文档超出限定的行数时在末尾显示“...更多”,“更多”可点击。 利用text组件只做到了显示..., 有会的大神提供下思路哈。
2019-08-22 - 微信小程序生成图片和图片保存
生成图片这个功能需要使用 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 - 封装请求时要获取token,怎么在拿到token后才做请求?
自己封装了request请求,通过storage拿token值,但是封装的文件加载特别快,当时还没有拿到token,导致后期所有请求都失败,需要重新刷页面才能获取token,有思路可以帮忙说一下,感谢
2019-08-28 - 怎么处理评论展示表情包呢?
在显示文章的评论信息微信小程序怎么显示表情包呢?微信小程序自己好像不支持表情包的样子!如果要展示表情包要怎么处理呢?求大神给个思路呢!
2019-07-04 - 小程序点击显示与隐藏如何实现?
[图片] 我想实在上图中现点击上述其中一个标题,仅展示当前点击的标题与内容,再次点击当前标题,所有标题显示(如上图),不知道有没有提供一个思路呀
2019-07-31 - 关于云数库使用的问题?数据库中100条数据,第一次调用20条数据,下拉再次调用2
关于云数库使用的问题?数据库中100条数据,第一次调用20条数据,下拉再次调用20条数据,以后每次下拉调用20调数据。 希望能提供一个解决思路
2019-09-08 - 求UI实现思路
如图的布局为左右滑动布局,中间是一个卡片,左右两侧露出相邻的卡片的一部分,左滑或者右滑可以直接将上/下个卡片定位到屏幕中央。我尝试了用scrollview和swiper来实现,都不是很理想,请问有什么比较好的思路来做呢,谢谢 [图片]
2017-08-15 - 视频是在wifi下播放 4G不播放是否能实现?
wx.getNetworkType 想到 在进度监听的同时 调用这个放来来获取当前的网络环境 进行做到只在wifi下播放 不知道这个思路大家觉得可行不..
2019-09-19 - input组件处于聚焦时,修改value导致触发bindinput的解决方案
在安卓机型下,input组件处于聚焦时,修改value会导致触发bindinput 解决思路如下(不是具体代码) // 定义一个loading变量 var loading = false // 修改value时 changeValue() { loading = true //... } // 触发bindinput时,限制其执行 bindInput() { if (loading) return //... } // 触发bindfocus时,重置loading bindfocus() { loading = false }
2019-12-27 - 小程序如何实现文字过多截断并且添加全文按钮?
[图片]想实现微博的这种功能,但是没啥思路,请教各路大神了,跪谢!!!!!
2019-07-05 - 首屏页预加载数据的实现思路是什么呐?
首屏页预加载数据,第一次进入小程序显示一张图片,数据加载完 隐藏该图片
2019-01-22 - 微信小程序swiper控件卡死的解决方法
微信小程序swiper控件,在使用过程中会偶尔出现卡死现象(不能再左右滑动),跟踪一下,归结原因可能是swiper组件内部问题,当无法响应用户快速翻动动作时,当前页变量current无法变更为正确页码索引,而是被置为0,所以,解决这个问题的思路如下: [代码]swiperchange: function (event) { if (event.detail.source == "touch") { //防止swiper控件卡死 if (this.data.current == 0 && this.data.preIndex>1 ) {//卡死时,重置current为正确索引 this.setData({ current: this.data.preIndex }); } else {//正常轮转时,记录正确页码索引 this.setData({ preIndex: this.data.current }); } } } [代码]
2020-02-16 - 自定义弹框怎么实现下面这种图片放置效果?求助
[图片] 上面的是效果图,弹框内的图片能够显示在弹框外 [图片] 然后如果图片一旦超出弹出框的话就会这样 [图片] 这是弹框代码 哪位大佬能给一个大概的方法或者思路,非常感谢
2020-01-10 - 小程序的getElementsById,就像一把梭
使用selectComponent可以抓取自定义组件实例对象,但在层层嵌套结构的业务场景中,id的设置繁复,js/wxml开发界面频繁的切换,查找、维护、调整的开发工作很是让人抓狂啊 好想封装一个getElementsById方法给小程序,像在web开发中那样能够方便的获取页面元素。在父子子子子级间轻松调用,好想念jquery开发的一把梭时代! 实现如下需求: 任何绑定id的自定义组件都能够方便抓取实例对象(任何嵌套层级均可调用) 通过数据配置 思路 实现不难,我们可以将所有自定义组件在create生命周期方法时将[代码]this[代码]挂载到一个全局变量中,[代码]detached[代码]生命周期时销毁该实例(不然爆内存) 实现 准备一个全局变量 [代码]app._elements = {} [代码] 挂载/销毁方法 一个全局的挂载、销毁方法,方便将实例对象注册、注销在app._elements上 [代码]app.mount = function(id, context){ app._elements[id] = context } app.unmount = function(id){ app._elements[id] = null } [代码] getElementsById 定义全局[代码]getElementsById[代码]在Page中能够方便调用 [代码]app.getElementsById = function(id){ let res = app._elements[id] if (!res) { // 兼容selectComponent return wx.selectComponent('#'+id) || wx.selectComponent('.'+id) } return res } [代码] 自定义组件 ui-component组件 [代码]const app = getApp() Component({ options: { multipleSlots: true, // 在组件定义时的选项中启用多slot支持 addGlobalClass: true }, properties: { dataSource: { type: Object, }, }, data: {}, behaviors: [], lifetimes: { created: function() { }, attached: function() { this.id = this.data.dataSource.$$id // 专用$$id来指定唯一名称 }, ready: function() { app.mount(this.id, this) }, detached: function(){ app.unmount(this.id) } }, methods: { active(clsName){ /* do something */ } }) [代码] 应用 下面开始在Page中使用[代码]getElementsById[代码]来抓取自定义组件实例 wxml [代码]<ui-component dataSource="{{config}}" /> [代码] js [代码]Page({ data: { config: { $$id: 'component-id', title: 'some text' } }, onReady(){ // 我们应该在onReady中来调用,onLoad时,页面组件结构并没有渲染完成 const $ele = app.getElementsById('component-id') $ele.active('.active') } }) [代码] 至此,基本思路已经实现,现在即兼容了selectComponent方法,又简化了写模板id的麻烦。不知道大家有没有了解小程序组件是可以递归嵌套自己的(模板不能递归嵌套)。因此聪明的你应该可以想到通过数据嵌套去实现组件嵌套,进而实现结构嵌套,这样我们就能够实现很复杂的页面结构,当然小程序目前建议是结构应该在30层左右,然并卵,反正它能够正常显示,哈哈 github地址:https://github.com/webkixi/aotoo-xquery 小程序demo演示,下列小程序基于xquery的个人开发,公司的就不放了 xquery [图片] saui [图片] 嘟嘟倒计时 [图片]
2019-12-21 - Canvas图片生成文字样式失效
Canvas图片生成文字样式失效(有时正常有时不正常),正常截图如下: [图片] 错误截图如下(文字:越买越赚钱): [图片] 代码如下:[图片]
2019-06-13 - 手机端启动小程序时,提示 Error: can't find module
首先这是我报错的代码: [代码]import {isVoiceRecordUseLatestVersion} from [代码][代码]"../../templates/chat-input/chat-input"[代码][代码];[代码][图片] 当前 Bug 的表现: 在安卓端启动小程序时,提示 “Error: can't find module : ../../templates/chat-input/chat-input” 电脑端启动无任何报错,使用真机调式启动也不会有任何报错 错误信息: [图片] 预期表现: 不应报错 求解!完全想不到思路,求提供思路!
2019-04-18 - 在线答题小程序交互设计整理四
在线答题小程序交互设计整理四 ### 本文概述 今天体验的小程序名字是:国务院客户端 [图片] 最近我会体验市面上不同在线答题类小程序,每个小程序仅仅截图,不做分析,每篇文章整理一个小程序,为我的小程序提供设计上的思路 在线答题小程序主要有三个细节: 1. 答题环境 2. 得分环境 3. 错题记录环节 这三部分是文章的重点。 ### 小程序截图 [图片] [图片] [图片] [图片] [图片] [图片] [图片] ### 本文总结 整体说这种做一题就强弹窗提示给答案的交互非常不友好,但是禁不住这个UI实在是漂亮,是个加分项,不愧是国务院官方小程序答题结果页这个分享也是很值得借鉴的一点
2019-12-19 - 在小程序内如何打开第三方链接?
我们都知道小程序内如果需要打开第三方链接,如打开 https://github.com/ 需要在开发设置里配置才可以打开,但是对于第三方网站就无能为力 [图片] 看到的常见方法是 提供用户复制链接功能,提示用户用手机浏览器打开 [图片] 现在有另一种思路是,因为是自己的小程序所以通过 客服组件 用户点击 客服组件时调用小程序copy文字方法,用户在客服聊天里直接粘贴发送,点击就可查看。 [图片] 具体体验小伙伴们可以体验 小程序,欢迎有更好的意见一起交流哦~ [图片]
2020-02-19 - 数据库的设计思路
- 需求的场景描述(希望解决的问题) - 希望提供的能力 假设有这么个场景:实现点赞功能,现在有一张用户表,一张文章表,如何判断某个用户是否已点赞?我目前的想法是在用户表中增加一个数组字段(用于存放已点赞的文章id),然后点赞时查询数组,判断是否包含此次点赞的文章,若包含则取消点赞(数组中删除这篇文章),否则点赞成功(添加到数组),但我感觉这种方式不太好,我觉得数据库的字段是不是应该尽量避免操作数组?有木有更好的方法呢? 还有收藏功能也是,是否要在用户表加一个数组字段用于存放已收藏的文章id? 如果有更好的解决方案,求推荐,求解答,顺便,还有评论功能,创建一张评论表,字段(内容,发送者,发送对象,是否是楼中楼, 时间),大神们的评论模块是怎么设计数据库的呢?
2019-04-02 - 小程序如何进行自动连接蓝牙设备?
小程序可不可以把上一次连接的蓝牙设备保留下来 ,下一次打开蓝牙就可以自动连接,有没有人知道怎么实现,可以说一下具体思路吗
2019-10-31 - 有什么方法给云开发数据库的某个数据设置上限 ?
比如让每个用户每天最多获取1000金币 这个思路!
2019-09-21 - 获取小程序码base64后无法显示,请给出方法或思路,别让大家在这里瞎折腾好吗?
[代码]wx.request({ [代码][代码]//获取小程序码[代码][代码] [代码][代码]url: [代码][代码]"https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token="[代码] [代码]+ getApp().globalData.access_token, //获取小程序码[代码][代码] [代码][代码]header: {[代码][代码] [代码][代码]'content-type'[代码][代码]: [代码][代码]'application/json'[代码][代码] [代码][代码]},[代码][代码] [代码][代码]data:{[代码][代码] [代码][代码]scene: json.content,[代码][代码] [代码][代码]page:[代码][代码]"pages/news/detial"[代码][代码],[代码][代码] [代码][代码]width:430[代码][代码] [代码][代码]},[代码][代码] [代码][代码]method:[代码][代码]"POST"[代码][代码],[代码][代码] [代码][代码]success: [代码][代码]function[代码] [代码](res) {[代码][代码] [代码][代码]console.log(res);[代码][代码] [代码][代码]var[代码] [代码]json = res.data;[代码][代码] [代码][代码]console.log([代码][代码]"json.length="[代码] [代码]+ json.length);[代码][代码] [代码][代码]console.log([代码][代码]"content length="[代码] [代码]+ res.header[[代码][代码]'Content-Length'[代码][代码]]);[代码][代码] [代码][代码]var[代码] [代码]a =[代码][代码]new[代码] [代码]Uint8Array(json.length)[代码][代码] [代码][代码]for[代码][代码]([代码][代码]var[代码] [代码]i=0;i<json.length;++i){[代码][代码] [代码][代码]a[i]=json.charCodeAt(i);[代码][代码] [代码][代码]}[代码][代码] [代码][代码]var[代码] [代码]b = wx.arrayBufferToBase64(a);[代码][代码] [代码][代码]console.log(b.substring(0,100))[代码][代码] [代码][代码]that.setData({ img: b })[代码][代码] [代码][代码]wx.hideNavigationBarLoading();[代码][代码] [代码][代码]}[代码][代码]})[代码]console.log返回的结果: {data: "����", header: {…}, statusCode: 200, errMsg: "request:ok"} json.length=91729 content length=96702 /f39/QAQSkZJRgABAQAAAQABAAD9/QBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB 可以看出: res.data是字符串格式的图片数据,然而转换成base64后图片并不能显示出来, 请问微信的技术人员,该如何显示潘慧的小程序码图片呢? 如果现在显示不出来,能不能说一声,避免大家浪费数据在这里瞎测试, 如果能显示,请给出方法或思路,别让大家在这里瞎折腾好吗????
2018-06-07 - 微信小程序音视频与WebRTC互通的技术思路和实践
概述 本文介绍了微信小程序视音视频和WebRTC的技术特征、差异等,并针对两者的技术差异分享和总结了微信小程序视音视频和WebRTC互通的实现思路以及技术方案。 关于作者 rexchang(常青):腾讯视频云终端技术总监,2008 年毕业加入腾讯,一直从事客户端研发相关工作,先后参与过 PC QQ、手机QQ、QQ物联 等产品项目,目前在腾讯视频云团队负责音视频终端解决方案的优化和落地工作。 分别介绍一下小程序音视频和WebRTC 小程序音视频是什么? 2017年腾讯视频云团队跟微信团队联合,将视频云 SDK 跟微信小程序整合在一起,并通过 <live-pusher> 和 <live-player> 两个标签的形式开放内部的功能。通过这两个标签,开发者可以实现在线直播、低延时监控、双人视频通话以及多人视频会议等功能 [图片] WebRTC又是什么? WebRTC(Web Real-Time Communication),是一个支持网页浏览器进行实时语音对话或视频对话的技术,是谷歌收购 GIPS 公司而获得的一项技术,在 Chrome 浏览器上无需安装插件,通过 javascript 就可以编写实时音视频通话程序。 小程序音视频和WebRTC的区别在哪里? 如果您跟我一样是一个实用主义者,那我就简单从实用主义角度说一下我的结论:小程序搞定了手机,WebRTC拿下了PC。 如果你对技术原理比较感兴趣,那我们就可以从多个技术的角度去列举两者的区别,下面是一张详细对比的表格: [图片] (区别一):内部原理 小程序音视频是将腾讯视频云的 liteavsdk 嵌入到微信内部实现的,然后通过 <live-pusher> 和 <live-player> 两个标签将 SDK 内部的音视频能力开放出来。所以小程序的标签起到了开发者 API 的作用,而内部的 SDK 则是真正用来实现音视频功能。 WebRTC 由谷歌收购 GIPS 得来(这里不得不提一下,我加入腾讯时所在的第一个团队就是 QQ 团队,当时 QQ 的音视频还是购买的 GIPS 公司的产品,不过由于技术支持不能满足需求,后来我们就转为自研路线了)。所以其技术被完整的保留并且加入到了 Google 的 Chrome 浏览器内核当中。而且最近苹果也已经开始在 Safari 浏览器中支持 WebRTC 的相关能力。 (区别二):传输协议 小程序音视频在直播场景使用了 RTMP 推流协议以及 FLV 播放协议,这两种协议都已经有多年的沉淀而且在互联网上的资料也是汗牛充栋。 小程序音视频在视频通话场景则使用了经过 UDP 改造的 RTMP 协议,相比于普通 RTMP 有更强的抗弱网能力和更低的卡顿率。 小程序音视频内核 LiteAVSDK 的抗弱网演示 WebRTC的底层则是使用RTP和RTCP两种数据协议,其中RTP主要用于音视频数据传输,而 RTCP 则用于传输控制。 (区别三):移动端碎片化问题 小程序音视频由于是微信统一实现的,而且微信团队每个版本都尽量要求功能对齐,否则宁可不上,所以在碎片化问题上基本不存在。 相比之下,WebRTC在这里则要尴尬的多,一方面Android系统的碎片化本身让WebRTC的具体表现呈现“百花齐放”的景象,同时,iOS 目前的内嵌WebView(也就是在微信等APP里打开的各种内嵌网页)不支持WebRTC也还是个很麻烦的问题。 (区别四):未来扩展性 小程序音视频跟随微信的版本发布,有什么问题一般是当前代码流修正,然后跟随下一个版本发布,所以一般一个功能点(比如给 pusher 加一个美颜的功能)或者一个问题点(比如不支持手势放大)从确立到最终实现(或解决)仅需要一个月的时间,而且微信APP新版本的覆盖速度也确实挺快。 相比之下,WebRTC则不是一个团队或者一家公司的问题了,因为它现在已经走标准路线,所以每一个新特性都是先确定标准,然后再推动浏览器厂商(包括苹果)进行跟随。 (区别五): 桌面浏览器 在前面几个问题的分析上,我的观点都倾向于小程序音视频。确实,在目前国内的移动领域里,谷歌和苹果都不能一家说了算,真正说了算的还是微信。 但是在桌面浏览器这个部分,Chrome目前在PC浏览器市场上留到地位的存在决定了 WebRTC 的优势就很大了,开发者可以在不安装插件的情况下就可以实现自己想要的功能。 所以,实现同 Chrome 浏览器的音视频互通,成为了小程序音视频的一个必不可少的能力特性。 互联互通 小程序音视频和WebRTC支架并非零和博弈,双方都有自己的优势和不足,实现两者的互通就能实现 1 + 1 > 2 的效果。 PC 端 用户可以使用 Chrome 浏览器直接使用音视频能力,免去安装桌面应用程序的痛苦。 移动端 用户可以使用微信小程序直接使用音视频能力,减少安装App的等待时间。 两者结合,可以将原本局限于小应用场景下的音视频能力扩展到各行各业中。 当然,要实现互联互通,并不是特别容易,首先,我们需要对 WebRTC 协议本身有一个全面的了解: 剖析WebRTC 就像结婚一样,既然你决定要选择另一个人作为人生下半辈子的伴侣,那你肯定会先深入地了解一下TA这个人,比如性格,脾气,爱好等各个方面。 同样,我们要想很好的将小程序音视频和WebRTC打通,那也必须要多了解一下WebRTC,对其知根知底,方能和平相处。 WebRTC 的设计思路是open的 WebRTC 的接口设计一开始就尽可能把内部细节更多的暴露出来,而不是简单封装一套傻瓜式的接口。这种方案的好处是二次开发的灵活度比较高,比如您可以发现 WebRTC 的 API 可以灵活到操作很多连接细节。 但任何事情都有另一面,WebRTC的学习成本并不低,虽然Google做了很多浅显易懂的PPT来教你怎么 Getting Start,但真要完整的学进去,还是需要静下心来,慢慢啃下去。 WebRTC 有多种后台接入方案 说WebRTC喜欢迁就比人,也是一种比喻,WebRTC所支持的后台架构非常多(比如 Mixer, Mesh,Router),而且谷歌认为这些后台实现方案并不需要给出什么限制和标准,因此也就没有提供统一的后台解决方案。 这种开放式的设计思路非常好,但副作用就是实现成本高。在真刀真枪的项目落地时,没有踩坑经验的开发者就很容易被这种技术门槛挡在门外。尤其是想要将 WebRTC 真正应用到企业级解决方案中,面对录制和存档的刚性需求,就需要花费大量时间进行定制开发。 互通方案 了解到 WebRTC 的这些特点后,我们的互通方案也就比较清晰了: 首先,小程序音视频的特点是接口简单,快速上手,这是小程序的优势;而这一点恰恰是WebRTC的劣势,所以我们没有必要在小程序端为WebRTC暴露十几组接口函数,而是继续采用小程序音视频的<live-pusher> 和 <live-player> 标签来解决问题。 其次,WebRTC 的后台没有官方实现,那就意味着这里有很大的发挥空间,腾讯视频云就可以实现一套WebRTC后台并将其同小程序音视频所使用RTMP后台进行打通。简单来说,腾讯视频云要在小程序音视频和WebRTC之间充当红娘(更确切的说,应该是翻译员)的角色。 但是看过《新闻联播》里国家领导人之间谈话镜头的人都知道,这种翻译是会影响交流速度的。小程序音视频和WebRTC之间互通,中间引入一个翻译员,是不是通讯延时也就增加了? 其实不会,因为小程序音视频和WebRTC的视频编码标准在常规应用场景中是一致的,都是H.264标准,只是音频格式不同而已。这就意味着,翻译员要做的事情很少,两边基本都能听懂对方在说什么,所以延时不会增加多少。 协议握手 下图所展示的就是腾讯视频云在小程序音视频和WebRTC互通问题上所采取的方案: [图片] (1)首先,微信端的小程序通过腾讯视频云SDK将音视频流推送到腾讯云 RTMP 服务器。 (2)其次,腾讯云 RTMP 服务器的会对音视频数据进行初步的转化处理,然后透传给腾讯视频云的实时音视频后台集群。 (3)再次,实时音视频后台会再次将数据交给一个叫做 WebRTC-Proxy 的模块,就在这里, WebRTC-Proxy 要将来自小程序音视频的音视频数据翻译成 WebRTC 理解的“语言”。 (4)最后,在PC上的Chrome浏览器,就可以通过浏览器内置的WebRTC模块跟 WebRTC-Proxy 通讯,进而看到小程序端的视频影像。 (5)上面的四个过程倒过来,就可以实现双向视频通话;而将腾讯视频云作为星型结构的中心节点,多个端(不管是小程序还是Chrome浏览器)都接入进来,那就可以形成多人音视频解决方案。 打通房间逻辑 仅仅完成了音视频数据在小程序和WebRTC之间的握手还远远不够,因为在一次成功的音视频通话背后,不仅仅是把一端的音视频数据传递到另一端这么简单,还有状态的同步和成员间的状态协同。 比如多人视频通话中,涉及到呼叫和接通的流程,其中一方如果挂断了,其他人要收到挂断的通知。同时,如果有新的参与者加入,那么其他人也要收到相应的通知。WebRTC 中有很多组件,比如 RTCPeerConnection 就负责处理网络连接中各种密密麻麻的逻辑细节。但是 WebRTC 的接口中引入的新名词非常多,对于初学者来说还是有一定的门槛,为了简化这里的逻辑,我们引入一个叫做“房间”的概念。 所谓房间(Room),就是把同时参与视频通话的各方圈在一起的一个东西。比如双人通话中,通话中的两个人 A 和 B 就可以认为在一个房间中。再比如在多人通话中,通话中的五个人(A B C D E)也可以认为是在一个房间里。 有了房间的概念,那我们就可以对刚才说的状态协同用两个简单的动作描述一下:如果有一个人加入了视频通话,那么就可以理解为他/她已经进房(EnterRoom)了;如果有一个退出了视频通话,那么就可以理解为他/她已经离开房间(LeaveRoom)了。而房间的门板上始终写着:“目前在房间里有哪几个人”。 有了房间的概念,我们就可以将小程序的两个简单的<live-pusher> 和 <live-player>标签,同 WebRTC 那一套复杂的 API 进行功能上的对齐,我们甚至不需要修改我们在第一版中定义的接口,就可以达成这个目标: <live-pusher> 标签:代表房间中的“我”。 <live-player> 标签:代表房间中的“其他人”。 内部逻辑细节 [图片] (1)<live-pusher> 的 url 接口不再传递 rtmp:// 协议的推流地址,而是传递 room:// 协议的推流地址。 (2)<live-pusher> 标签在 start 成功之后,就相当于成功进入一个 room,之后,您可以通过 onPushEvent (PUSH_EVT_ROOM_USERLIST = 1020) 事件,收到房间里还有那些人的信息。在视频通话期间,房间内各个成员的进进出出,也都会通过这个事件通知给您的小程序代码。 (3)ROOM_USERLIST 里每一项都是一个二元组(如果是 1v1 的视频通话,ROOM_USERLIST 里只会有一个人): userid 和 playurl。 userid 代表是哪个用户, playurl 则是这个用户远程画面的播放地址。您要做的只是使用 <live-player> 标签播放这些远程画面的图像和声音而已。 (4)在 WebRTC 这一端,您可以参考我们的 webrtc API,这套 API 相对于 WebRTC 原生的 API,更适合初学者使用。 呃… 您可能会说:“你这也叫简单呀,我感觉还是要写几十行代码,能不能真的做到像一个标签一样简单呢?” 好吧,其实上面四步是我们第一个版本的接入流程,就在我们昨晚这套方案之后,小程序团队刚好推出了自定义组件的机制,于是,我们有了更好的接入方案。 能不能更简单? 如果您希望一天内就打通 webrtc 和 小程序音视频 的互通,那么我推荐您不要从零开始,因为那会耗费您太多时间去踩坑和 bugfix,推荐您直接使用我们封装好的 <webrtc-room> ,这套方案既可以帮助您完成快速接入,又能满足一定的定制需求。 另外,不要忘记在微信=>发现=>小程序=>腾讯云视频云,体验一下腾讯云官方 Demo 中的 WebRTC 互通效果哦。 [图片] <webrtc-room> 功能说明 <webrtc-room> 标签是基于 <live-pusher> 和 <live-player> 实现的用于 WebRTC 互通的自定义组件。用于实现跟 Chrome 和 App SDK 之间的视频通话功能。 版本要求 微信 6.6.6 版本开始支持。 Demo体验 (1) Chrome: 用谷歌浏览器打开 体验页面。 (2) 微信端:发现=>小程序=>搜索“腾讯视频云”,点击 视频通话 页卡,输入相同的房间号。 对接资料 源码地址 源码说明 小程序端源码 Github Chrome端源码 Github 属性定义 属性 类型 默认值 说明 template String ‘float’ 必要,标识组件使用的界面模版。 demo中内置 bigsmall,float,grid三种布局 sdkAppID String 必要,开通实时音视频服务创建应用后分配的 sdkAppID userID String 必要,用户 ID userSig String 必要,身份签名,相当于登录密码的作用 roomID Number 必要,房间号 beauty Number 0 可选, 美颜指数,取值 0 - 9,数值越大效果越明显 whiteness String 0 可选, 美白指数,取值 0 - 9,数值越大效果越明显 muted Boolean false 可选,true 静音 false 不静音 debug Boolean false 可选,true 打印推流 debug 信息 fales 不打印推流 debug 信息 bindRoomEvent Function 必要,监听 <webrtc-room> 组件返回的事件 enableIM Boolean false 可选,是否启用IM bindIMEvent Function 当IM开启时必要,监听 IM 返回的事件 aspect String 9:16 可选, 宽高比3:4, 9:16 minBitrate String 200 可选,最小码率,该数值决定了画面最差的清晰度表现 maxBitrate String 400 可选,最大码率,该数值决定了画面最好的清晰度表现 autoplay Boolean false 可选,进入房间后是否自动显示远程画面 enableCamera Boolean true 可选,开启\关闭摄像头 pureAudioPushMode Number 可选,纯音频推流模式 recordId Number 可选,自动录制时业务自定义id enableCamera Boolean true 是否开启摄像头 smallViewLeft String ‘1vw’ 小窗口距离大画面左边的距离,只在template设置为bigsmall有效 smallViewTop String ‘1vw’ 小窗口距离大画面顶部的距离,只在template设置为bigsmall有效 smallViewWidth String ‘30vw’ 小窗口宽度,只在template设置为bigsmall有效 smallViewHeight String ‘40vw’ 小窗口高度,只在template设置为bigsmall有效 waitingImg String 当微信切到后台时的垫片图片 loadingImg String 画面loading图片 操作接口 <webrtc-room> 组件包含如下操作接口,您需要先通过 selectComponent 获取 <webrtc-room> 标签的引用,之后就可以进行相应的操作了。 函数名 说明 start() 启动 pause() 暂停 resume() 恢复 stop() 停止 switchCamera() 切换摄像头 sendC2CTextMsg(receiveUser, msg, succ, fail) 发送C2C文本消息 sendC2CCustomMsg(receiveUser, msgObj, succ, fail) 发送C2C自定义消息 sendGroupTextMsg(msg, succ, fail) 发送群组文本消息 sendGroupCustomMsg(msgObj, succ, fail) 发送群组自定义消息 [代码]var webrtcroom = this.selectComponent("#webrtcroomid") webrtcroom.pause(); [代码] 事件通知 <webrtc-room> 标签通过 onRoomEvent 返回内部事件,通过 onIMEvent返回 IM 消息事件,事件参数格式如下: [代码]"detail": { "tag": "事件tag标识,具有唯一性", "code": "事件代码", "detail": "对应事件的详细参数" } [代码] 如何快速接入? 上面说了很多细节的技术原理和内部细节,如果您想要快速尝试一下,建议您阅读下面三篇文章就可以了。 一分钟跑通demo 我们准备了一个简单上手的小程序音视频Demo,输入房间号即可开始通话,这篇文章主要介绍如何把Demo快速地 run 起来。 一分钟集成组件 这篇文章主要介绍如何快速地将 <webrtc-room> 组件集成到您的小程序工程项目中。 快速调通基本功能 这篇文档就主要介绍如何二次开发了,它介绍了<webrtc-room> 中主要 API 的使用。 总结 本篇文章主要介绍了小程序音视频和Chrome浏览中重要的WebRTC技术的互通方案,希望能对您的项目开发有所帮助,期待您的反馈。
2019-02-20