- 自定义导航栏所有机型的适配方案
写在前面的话 大家看到这个文章时一定会感觉这是在炒剩饭,社区中已经有那么多分享自定义导航适配的文章了,为什么我还要再写一个呢? 主要原因就是,社区中大部分的适配方案中给出的大小是不精确的,并不能完美适配各种场景。 社区中大部分文章给到的值是 iOS -> 44px , Android -> 48px 思路 正常来讲,iOS和Android下的胶囊按钮的位置以及大小都是相同且不变的,我们可以通过胶囊按钮的位置和大小再配合 wx.getSystemInfo 或者 wx.getSystemInfoSync 中得到的 [代码]statusBarHeight[代码] 来计算出导航栏的位置和大小。 小程序提供了一个获取菜单按钮(右上角胶囊按钮)的布局位置信息的API,可以通过这个API获取到胶囊按钮的位置信息,但是经过实际测试,这个接口目前存在BUG,得到的值经常是错误的(通过特殊手段可以偶尔拿到正确的值),这个接口目前是无法使用的,等待官方修复吧。 下面是我经过实际测试得到的准确数据: 真机和开发者工具模拟器上的胶囊按钮不一样 [代码]# iOS top 4px right 7px width 87px height 32px # Android top 8px right 10px width 95px height 32px # 开发者工具模拟器(iOS) top 6px right 10px width 87px height 32px # 开发者工具模拟器(Android) top 8px right 10px width 87px height 32px [代码] [代码]top[代码] 的值是从 [代码]statusBarHeight[代码] 作为原点开始计算的。 使用上面数据中胶囊按钮的高度加 [代码]top[代码] * 2 上再加上 [代码]statusBarHeight[代码] 的高度就可以得到整个导航栏的高度了。 为什么 [代码]top[代码] * 2 ?因为胶囊按钮是垂直居中在 title 那一栏中的,上下都要有边距。 扩展 通过胶囊按钮的 [代码]right[代码] 可以准确的算出自定义导航的 [代码]左边距[代码]。 通过胶囊按钮的 [代码]right[代码] + [代码]width[代码] 可以准确的算出自定义导航的 [代码]右边距[代码] 。 通过 wx.getSystemInfo 或者 wx.getSystemInfoSync 中得到的 [代码]windowWidth[代码] - 胶囊按钮的 [代码]right[代码] + [代码]width[代码] 可以准确的算出自定义导航的 [代码]width[代码] 。 再扩展 wx.getSystemInfo 或者 wx.getSystemInfoSync 中得到的 [代码]statusBarHeight[代码] 每个机型都不一样,刘海屏得到的数据也是准确的。 如果是自定义整个页面,iPhone X系列的刘海屏,底部要留 [代码]68px[代码] ,不要问我为什么! 代码片段 https://developers.weixin.qq.com/s/Q79g6kmo7w5J
2019-02-25 - 小程序构建骨架屏的探索
首屏 一般情况下,在首屏数据未拿到之前,为了提升用户的体验,会在页面上展示一个loading的图层,类似下面这个 [图片] 其中除了菊花图以外网上还流传这各种各样的loading动画,在PC端上几乎要统一江湖了,不过最近在移动端上面看到不同于菊花图的加载方式,就是这篇文章需要分享的Skeleton Screen,中文称之为"骨架屏" 概念 A skeleton screen is essentially a blank version of a page into which information is gradually loaded. 在H5中,骨架屏其实已经不是什么新奇的概念了,网上也有各种方案生成对应的骨架屏,包括我们经常使用的知乎、饿了么、美团等APP都有应用骨架屏这个概念 图片来源网络,侵删 [图片] 方案 先从H5生成骨架屏方案开始说起,总的来说H5生成骨架屏的方案有2种 完全靠手写HTML和CSS方式给每个页面定制一套骨架屏 利用预渲染的方式生成静态骨架屏 第一套方案,毫无疑问是最简单最直白的方式,缺点也很明显,假如页面布局有修改的话,那么除了修改业务代码之外还需要额外修改骨架屏,增加了维护的成本。 第二套方案,一定程度上改善了第一套方案带来的维护成本增加的缺点,主要还是使用工具预渲染页面,获取到DOM节点和样式,保留页面结构,覆盖样式,生成灰色块盖在原有文本、图片或者是canvas等节点上面,最后将生成的HTML和CSS打包出来,就是一个带有骨架屏的页面。最后再利用webpack工具将生成的骨架屏插入到HTML里面,详细的话可以看看饿了么的分享,这里就不多描述了。 调研了下H5生成骨架屏的方案,对于小程序生成骨架屏的方案也有了一个大致的想法,主要有2个难点需要实现 预渲染 获取节点 预渲染 再说回饿了么提供的骨架屏的方案,使用 puppeteer 渲染页面(或者使用服务端渲染,vue或者react都有提供相应的方案),拿到DOM节点和样式,这里有一点需要注意的是,页面的渲染是需要初始化的数据,数据的来源可以是初始化的data(vue)或者mock数据,当然小程序是无法直接使用 puppeteer 来做预渲染(有另外的方案可以实现),需要利用小程序初始化的 data + template 渲染之后得到一个初始化结构作为骨架屏的结构 [代码]//index.js Page({ data: { motto: 'Hello World', userInfo: { avatarUrl: 'https://wx.qlogo.cn/mmopen/vi_32/SYiaiba5faeraYBoQCWdsBX4hSjFKiawzhIpnXjejDtjmiaFqMqhIlRBqR7IVdbKE51npeF6X1cXxtDQD2bzehgqMA/132', nickName: 'jay' }, lists: [ 'aslkdnoakjbsnfkajbfk', 'qwrwfhbfdvndgndghndeghsdfh', 'qweqwtefhfhgmjfgjdfghaefdhsdfgdfh', ], showSkeleton: true }, onLoad: function () { const that = this; setTimeout(() => { that.setData({ showSkeleton: false }) }, 3000) } }) //index.wxml <view class="container"> <view class="userinfo"> <block> <image class="userinfo-avatar skeleton-radius" src="{{userInfo.avatarUrl}}" mode="cover"></image> <text class="userinfo-nickname skeleton-rect">{{userInfo.nickName}}</text> </block> </view> <view style="margin: 20px 0"> <view wx:for="{{lists}}" class="lists"> <icon type="success" size="20" class="list skeleton-radius"/> <text class="skeleton-rect">{{item}}</text> </view> </view> <view class="usermotto"> <text class="user-motto skeleton-rect">{{motto}}</text> </view> <view style="margin-top: 200px;"> aaaaaaaaaaa </view> </view> [代码] 有了上面的 data + template 之后,就有了一个初始化的页面结构,接下来就需要拿到节点信息 节点 小程序基础库1.4.0之后小程序基础库提供了一组新的API,可用于获取节点信息,具体API戳这里。 跟H5方式一样,根据class或者id获取节点信息,不同的是只能获取到当前的节点信息,无法获取到其父或者子节点信息,所以只能手动给需要渲染骨架屏的节点添加相应的class或者id [代码]<view class="container"> <view class="userinfo"> <block> <image class="userinfo-avatar skeleton-radius" src="{{userInfo.avatarUrl}}" mode="cover"></image> <text class="userinfo-nickname skeleton-rect">{{userInfo.nickName}}</text> </block> </view> <view style="margin: 20px 0"> <view wx:for="{{lists}}" class="lists"> <icon type="success" size="20" class="list skeleton-radius"/> <text class="skeleton-rect">{{item}}</text> </view> </view> <view class="usermotto"> <text class="user-motto skeleton-rect">{{motto}}</text> </view> <view style="margin-top: 200px;"> aaaaaaaaaaa </view> </view> [代码] 约定2个特殊的class作为获取节点信息的标记[代码]skeleton-rect[代码]和[代码]skeleton-radius[代码],在页面中获取相应的[代码]top[代码]、[代码]left[代码]、[代码]width[代码]、[代码]height[代码]进行骨架屏的绘制 结果 [图片] 具体的调用方式和源码,请看 github ,最后求start 总结 上文有说到小程序也可以使用 page-skeleton-webpack-plugin 方式一样生成骨架屏,最重要的一点就是需要将小程序跑在chrome上面,后面的流程就一样了,至于怎么将小程序跑在chrome上面呢?可以利用 wept ,缺点就是目前作者已经停止维护这个工具了,不支持新版小程序的API。 说回来我这个生成骨架屏的方案,其实跟 page-skeleton-webpack-plugin 有点相似,不同的是,page-skeleton-webpack-plugin 采用离线渲染的方式生成静态骨架屏插入路由中,而我采用运行时先渲染页面默认结构,然后根据默认结构再绘制骨架屏。从性能角度出发确实不如 page-skeleton-webpack-plugin,但是也差不了多少了,主要还是小程序并没有提供类似服务端渲染的方案。目前从使用上来讲,还是有点小麻烦,需要默认数据撑开页面结构,需要给相应的节点添加class,后面有时间再研究下有没有更好的方案吧~~~
2019-02-20 - 小程序开发常见问题(一)
1、域名必须是HTTPS 小程序后台配置的域名,有服务器域名、业务域名、消息推送域名、普通二维码域名,前三者必须是HTTPS域名,普通二维码域名可以是HTTP域名 2、input组件placeholder字体颜色 写在placeholder-class里面的color并不生效,需要写在placeholder-style里面就可以了 3、wx.navigateTo跳转不生效? 带有tabbar的页面,必须使用wx.switchTab进行跳转 4、tabbar在切换时页面数据无法刷新 tabbar的实现可能是显示和隐藏view,所以,不会一直调用page.onLoad()方法,可以尝试把代码逻辑写在page.onShow()里面,或者在onTabItemTap方法中处理 5、如何获取shareTickets(可以解密微信群ID) 获取shareTickets需要在app.onLaunch或者app.onShow里面才能获取到,而不是page.onShow,请一定要注意。 注:建议在app.onShow里面去获取,app.onLaunch不是一直会执行 6、getPhoneNumber获取手机号 目前该接口针对非个人开发者,且完成了认证的小程序开放。个人开发者是没办法调用这个API的 7、wx.previewImage图片预览 预览的图片URL必须是HTTPS开头,不能是本地图片 8、wx.playVoice音频播放 必须保证音频文件已经在本地,比如在wx.startRecord后,可以获取到本地临时的tempPath。或者提前调用wx.downloadFile来下载资源文件,然后再播放 9、API老版本兼容 可以用wx.canIUse或者wx.getSystemInfoSync来获取version和SDKversion进行判断,老版本给出相应提示即可 10、获取系统信息 wx.getSystemInfo,可得到系统语言、屏幕宽高、微信版本号、操作系统、设备像素比、客户端甚础库版本等信息 11、如何去掉自定义button灰色的圆角边框 主要是button的伪元素设置了样式,去掉即可: button::after{ display: none;} 12、回到页面顶部 回到页面顶部,有两种方式: 1、使用scroll-view设置为纵向滚动,然后设置scroll-top值; 2、使用wx.pageScrollTo方法,此方法是1.4.0开始支持,所以要做低版本兼容; 13、textarea是APP的原生组件,层级最高 这是个大坑,在有textarea的页面,不要做弹出框设计,建议在输入大段文本时,单独成一个新页面。 14、image组件底部有间隙 image组件默认底部会有间隙,可以设置为块元素(display: block),也可以设置vertical-align: top; 15、一段文字如何换行 小程序中唯一可以实现换行的标签组件是text 注:text中不支持<br>,只能使用\n进行换行 16、设置最外层标签的margin-bottom在IOS下不生效 margin-bottom在安卓和开发工具里面都正常,就是在IOS下不起效,建议改成padding-bottom 17、小程序中canvas的图片不支持base64格式 base64格式图片,在开发工具里面可以正常显示,真机上没有显示。建议修改成带https开头的url形式 18、删除体验版,仍然有缓存? 开发过程中,可能会遇到,删除小程序的体验版,但是缓存依然存在。建议把开发版和线上版都删除,因为小程序缓存是共用的。 19、开发者工具无法复制、粘贴代码 开发者工具中,经常会遇到无法复制、粘贴、搜索代码,有时还会出现,在A文件输入,内部跑到了B文件中,大写的尴尬。遇到这种情况,建议重启微信开发者工作。 20、小程序告警群 小程序后台可以查看所有错误信息,但是,为了方便第一时间了解错误报警,建议使用官方“客户端告警群”,如下图。告警的阀值,可以自己设置。 [图片] [图片] [图片]
2023-12-25 - 小程序开发常见问题(二)
1、wx.setStorageSync和wx.getStorageSync报错问题 为什么说这个问题,是因为这个API确实会报错,并且调用越频繁,报错会越多,先看一下截图: [图片] 所以,怀疑微信官方API也有出错的机率,这里没有根治的办法,只能做一些缓解报错次数的办法:减少调用频次,不要在公用方法里面去频繁调用set和get本地缓存;添加try catch,出错之后,可以再调用一次或多次,减少报错的可能性~ **2、picker下拉列表为什么获取不到长度 ** [图片] 如上图,如果是设置了key的数组,会发现,此时的array的length是0,这可能是小程序的一个bug,length只能自己处理了~ 3、如何获取音频文件的长度 如果调用的是新API,wx.createInnerAudioContext可以直接获取duration;如果是低版本,调用wx.startRecord方法时,只能自己写个计数器来处理duration了。 在部分机型上会有个蛋痛的问题,自己写个定时器,和微信内部录音的时间对不上,当录音600秒(十分钟)时,会相关1~3秒。并且在部分安卓手机上,InnerAudioContext.stop()不会自动调用,需要手动去调用stop 4、如何获取微信群名称? 小程序中是没办法直接获取到微信群名称,只有一种方法获取open-gid,然后再通过open-data组件来显示群名称: <open-data type=“groupName” open-gid=“xxxxxx”></open-data> open-gid的获取方法: 用户把小程序分享到微信群,会在分享成功后返回shareTickets(因为可以分享到多个群,所以这里是一个数组); 如果用户是从群内点击的小程序卡片,会在小程序的app.onshow里面获取了shareTicket。拿到shareTicket后,再到服务端解密,就可以拿到open-gid~ 5、小程序最多支持多少个节点? 小程序可以理解为,被微信包装了一层的H5,页面会有最大节点,建议不要在页面做无限翻页,或者超大数据渲染,这些都可能导致小程序崩溃(如果内存不够时,微信优先杀掉小程序)。 [图片] 节点数过多时,就直接报错了:invokeWebviewMethod 数据传输长度为 1233778 已经超过最大长度 1048576 1048576是个神奇的数字,大家可以自行百度~ 待续…
2019-01-29 - 微信小程序开发常见问题(三)
一、获取formId 相信使用过小程序的同学,多少都收到过小程序的通过消息,如下: [图片] [图片] 这类通知消息,是和好友消息一样展示在微信的聊天列表中,所以,点击率还是比较高的。想实现这种小程序的模板消息,就必须要获取用户的formid才可以(如何发消息,请仔细查阅小程序官方文档) 我们来说一下如何获取formId: a、必须通过form组件提交才能获取到formId; b、给form组件设置report-submit="true"属性; c、给form组件添加bindsubmit事件绑定,携带 form 中的数据触发 submit 事件,event.detail = {value : {‘name’: ‘value’} , formId: ‘’}; d、必须用户手动触发提交表单,不能JS模拟提交,所以,页面上必须要有提交按钮; 看一下示例代码: <form report-submit=‘true’ bindsubmit=‘userSubmit’> <button class=‘button’ bindtap=‘copy’ form-type=‘submit’>复制</button> </form> 以上示例就可以在userSubmit里获取到formId了: userSubmit: function (e) { console.log(e.detail.formId); }, 需要注意一点,开发工具里面是没办法查看到真实的formId的,会是这样一句提示"the formId is a mock one",提交给服务端就可以拿到了~ 最暴力的方式:整个页面最外层套一个button,点击页面任何地方,都可以获取到formId 二、区分转发的是群聊还是好友 这个其实就是场景值的判断,先看一张图: [图片] 上图可以看出,从好友聊天窗口和群聊窗口点击小程序卡片后,场景值是不一样的,分别是1007和1008,所以,我们可以在app的onLuanch或者onShow方法中去获取到scene值,这样就能知道用户是通过哪种方式进入小程序的~ 之前分享成功,可以获取群ID,这种方式已经被微信官方禁掉了。可以看这篇文章: 三、有哪些开源的小程序框架 wepy wepy是最早推出的一款小程序框架,基于vue进行封装,作者龚澄是腾讯的工程师,早期集累了很多习惯使用vue进行开发的小程序人员。github地址:https://github.com/Tencent/wepy mpvue mpvue也是一个使用 Vue.js 开发小程序的前端框架,美团点评下的一个部门开发的,有胡成全带队开发。github地址:https://github.com/Meituan-Dianping/mpvue taro Taro 是由京东 - 凹凸实验室打造的一套遵循 React 语法规范的多端统一开发框架。统于有一款使用React语法开发小程序的框架了,github地址:https://github.com/NervJS/taro 以上三个框架,都有自己的官方交流群,大家可以添加对应的小助手,然后会自动拉你进群。
2019-01-31 - 微信小程序开发常见问题(四)
一、判断小程序版本号 小程序的API是不断更新的,你可能使用某个API时,文档里会说明,此API在1.x.x版本开始支持,需要自己做兼容处理。 如果你使用小程序版本号做兼容,就必须了解小程序的基础库版本号规则,在这里介绍一下。 小程序基础库版本号使用 semver 规范,格式为 Major.Minor.Patch,Major、Minor、Patch 均为整数,1.9.901、2.44.322、10.32.44 都是符合 semver 风格的版本号。 以下是官方提供的兼容代码: [图片] 二、设置仅发起者可转发 有些场景,需要仅发起者可转发,参与者不能转发。比如老师上课点名签到,老师如果把签到小程序分享到群内,只希望现场的同学可以正常签到,即使其他同学想分享,也没有权限。 方法一:禁止显示分享按钮 小程序中有个API,wx.hideShareMenu,如下: [图片] 在page.js中,正常写入onShareAppMessage,然后判断用户是否为发起者,如果是发起者,调用wx.showShareMenu,如果不是发起者,调用wx.hideShareMenu。 方法二:单独做一个分享后的页面 这个办法比较low,但也能实现。思路是在onShareAppMessage里面的写入一个默认path,打开之后就是一个提示界面。如果是发起者,就把path修改成正常的路径~ [图片] 三、swiper禁止手动滑动 最简单的方式,在swiper上面加一个透明的蒙层~ 四、使用switchTab跳转后页面不刷新的问题 方法一:通过getCurrentPages获取获取当前的页面栈,调用对应的方法 [图片] switchTab成功跳转后调用success,此时可以拿到跳转后页面的page对象,从而调用页面onLoad方法重载页面; 方法二:把还tab的页面,代码逻辑放在onShow里面 [图片] 五、e.target和e.currentTarget的区别 简单的说,e.currentTarge是指注册了事件监听器的对象,e.target是指对象里的子对象,实际触发这个事件的对象。 [图片] 小程序开发过程中,如果通过event未获取到值时,或许是你自己用错了。 六、图片设置为圆角,会快速从方形闪烁一下 解决方法:给父元素设置圆角,或者给图片添加透明边框 七、input中使用手写输入法的坑 之前总有“报名工具”的小程序用户反馈,说是报名内容都输入完整了,但是保存不完整。 经排查,发现所有使用手写输入法的用户,都会遇到这现象。排查代码发现绑定的input事件,用户如果不点击手写输入法上的确认键,就不会触发bindinput。 这种情况下,有两种解决方式: 1、改用event.detail.value的形式来获取form表单数据; 2、再添加一个bindblur事件,保证事件能够正常执行; 因报名工具小程序中,有用户自定义字段,所以不确定用户会添加多少个字段,使用event获取的话,需要给每个input添加一个name属性,相对比较费劲,我改用了第二种方案。 [图片] 八、IOS下用户授权后,头像和昵称显示问题 这个问题其实是图片src是一个data中的变量,然后这个变量又发生了变化。但是在IOS设备上,就是没办法显示更改后的图片。(或者编辑图文投票时,也会有这现象) 解决办法:通过wx:if和wx:else判断,展示不同的image组件 [图片] 九、一键退出(隐藏)小程序 首先要说,这个需求不合理,右上角有退出按钮,但既然有同学问这问题,我也自己折腾了一下,基本可以实现一键退出。 先在每个page中添加隐藏page的方法: [图片] 有退出button的页面,对应的JS添加方法: [图片] 虽然可以实现,但也很low,其实就是隐藏了所有的page而已~
2019-01-31 - getCurrentPages()的用法
getCurrentPages()的用法 getCurrentPages()是个好东西,今天来说说他的用法。 先看看官方文档: [路由 · 小程序]:https://developers.weixin.qq.com/miniprogram/dev/framework/app-service/route.html [图片] getCurrentPages() 函数用于获取当前页面栈的实例,以数组形式按栈的顺序给出,第一个元素为首页,最后一个元素为当前页面。 简单说,就是可以获取到当前小程序的页面栈 那么,获取到页面栈,有什么用处呢? 1、判断页面栈是否超过10级,超过10级,将不能打开新页面(主要是不用用navigateTo方式打开)。 2、可以修改某个页面栈的data数据,或者方法。 这里给大家分享一个实际应用场景:仅发起者可分享。 有些投票、通知、抽奖、签到等,发起者会在私密的圈子内进行,比如,仅会员群才能参与的抽奖、公司内部的通知公告、班级内部的投票等。发起者是不希望别人分享出去,那么小程序里面要怎么做? 说到分享,在小程序内,应该是想到onShareAppMessage这个方法。只要page.js中有这个方法,不管你是否在内部写了代码逻辑,小程序默认就是可以分享的,如果没有这个方法,小程序右上角的“…”就不会出现“转发”的选项。 问题是,是否允许分享,一般都是小程序内的一个开关设置项,可以看下图: [图片] 用户在加载内容时,需要先从服务端获取到这个开关状态,再决定是否出现“转发”的选项。 此时,我们默认不给page添加onShareAppMessage方法,这样,你转发出去的小程序卡片,别人将无法通过长按进行分享(群聊无法长按分享,私聊还是个坑,看下图)。 [图片] [图片] 然后再动态设置当前page的onShareAppMessage方法,用this,或者getCurrentPages()都能解决,看下图: [图片] 目前私聊的卡片,长按依然可以转发,似乎不是很完美,但是,功能基本实现了。 如果想让私聊卡片的转发无效,你也可以变通一下,比如做个限群成员可见功能,即使私聊卡片被转发,可以判断小程序场景值,不展示内容即可~ 1、用wx.getLaunchOptionsSync() 获取小程序启动时的参数: [图片] 2、判断群聊和私聊的场景值: [图片] 3、如果是微信私聊中打开,给用户提示即可~ [图片] 欢迎各位一起讨论技术问题:mianhuabingbei
2019-02-20 - 【优化】解决swiper渲染很多图片时的卡顿
相信各位在开发的时候应该有遇到这样一个场景,比如商品的图片浏览,有时图片的浏览会很大,多的时候达几百张或上千张,这样就需要swiper里需要很多swiper-item,如此一来渲染的时候就会很消耗性能,渲染时会有一大段的空白时间,有时还会造成卡顿,体验非常差,下面给大家介绍一下我的解决方案。 首先是wxml结构: [图片] js: [图片] [图片] 主要是利用current属性,swiper里面只放3个swiper-item,要显示的图片放在第二,第一和第三放的是加载的动画背景,步骤如下: 将请求到的数据存入一个数组picListAll内,这里不需要setData,只需要在data外面定义一个变量就行了,以减少渲染性能; 把要显示的图片路径赋值给picUrl; 切换的时候根据bindchange获取current属性,当current改变时判断当前图片在picListAll的index,根据index拿到图片再赋值给picUrl; 主要实现步骤就是以上3 步,比较简单,要注意的是当切换到第一张和最后一张的时候要判断一下,把loding动画去掉,请求的时候还可以传入index参数以显示不同的图片,方便从前一页点击图片进入到此页面时能定位到该图片,例子里我是自己mock数据的,只是为了展示,如果你有服务器的话可以弄几百张看看效果,对比直接渲染和用以上方式渲染的差异。当然,这只是我的解决方案,如果各位有更好的方案欢迎一起讨论,一起进步。 完整代码:https://github.com/HaveYuan/swiper
2019-02-20 - 小程序登陆注册功能的简单实现(有图有源码)
有了云开发我们不仅可以方便的获取到用户的openid,还可以通过云开发的数据库来存储用户信息,进而实现小程序用户的登陆与注册功能。 本节知识点 1,云开发的使用 2,云函数的使用 3,云数据库的使用 4,用户登陆 5,用户注册 涉及到三个页面 1,登陆页面 [图片] 2,注册页面 [图片] 3,登陆成功 [图片] 下面来看具体的代码实现 一,注册页面实现 1,register.wxml [代码]<!--pages/register/register.wxml--> <input class='input' placeholder='请输入用户名' bindinput='inputName'></input> <input class='input' placeholder='请输入密码' bindinput='inputPassword'></input> <input class='input' placeholder='请输入电话' bindinput='inputPhone'></input> <input class='input' placeholder='请输入地址' bindinput='inputAddress'></input> <button class='button' type='primary' bindtap='register'>注册</button> [代码] 2,register.js 需要注意的是我们注册时需要使用到云开发数据库,在使用云开发数据库之前还需要初始化云开发,代码里都有注释 [代码]// pages/register/register.js let app = getApp(); // 获取数据库引用 const db = wx.cloud.database(); const userListDB = db.collection('userlist'); let name = null; let password = null; let phone = null; let address = null; Page({ /** * 页面的初始数据 */ data: { }, //输入用户名 inputName(evnet) { name = evnet.detail.value; }, //输入密码 inputPassword(evnet) { password = evnet.detail.value; }, //输入手机号 inputPhone(evnet) { phone = evnet.detail.value; }, //输入地址 inputAddress(evnet) { address = evnet.detail.value; }, //注册 register() { let that = this; if (!app.checkNamePassword(name, password)) { return; } if (!app.checkPhoneAddress(phone, address)) { return; } //查询用户是否已经注册 userListDB.where({ _openid: app.globalData.openid // 填入当前用户 openid }).get({ success: function(res) { let userInfos = res.data; console.log(res.data) if (userInfos && userInfos.length > 0) { let user = userInfos[0]; if (user && user.name) { wx.showModal({ title: '提示', content: '您已注册,确定要更新账号密码吗?', success: function(res) { if (res.confirm) { console.log('用户点击确定') that.saveuserinfo(); } } }) } } else { that.saveuserinfo(); } } }) }, saveuserinfo() { let that = this; userListDB.doc('_openid').set({ data: { name: name, password: password, phone: phone, address: address } }).then(res => { app.showTips('注册成功'); }) }, }) [代码] 3,在app.js里初始化云开发 下面的prod-8aa9a5就是我们云开发的环境id [代码]//app.js App({ onLaunch: function() { //云开发初始化 wx.cloud.init({ env: 'prod-8aa9a5', traceUser: true }) } }) [代码] [图片] 4,注册成功后,我们在云开发控制台的数据库里就可以看到注册信息了。 [图片] 二,注册成功后,就要实现登陆功能了 我们这里的登陆功能需要用到第一步注册时的用户名和密码,也就是上图数据库里的name和password字段 1,登陆页面实现代码 login.wxml [代码]<!--pages/login/login.wxml--> <input class='input' placeholder='请输入用户名' bindinput='inputName'></input> <input class='input' placeholder='请输入密码' bindinput='inputPassword'></input> <button class='button' type='primary' bindtap='login'>登陆</button> <button class='button' type='primary' bindtap='register'>去注册</button> [代码] 2,登陆功能实现 [代码]// pages/login/login.js let app = getApp(); // 获取数据库引用 const db = wx.cloud.database(); const userListDB = db.collection('userlist'); let name = null; let password = null; Page({ data: { }, //输入用户名 inputName(evnet) { console.log(evnet.detail.value) name = evnet.detail.value; }, //输入密码 inputPassword(evnet) { password = evnet.detail.value; }, //登陆 login() { let that = this; if (!app.checkNamePassword(name, password)) { return; } //登陆获取用户信息 userListDB.where({ _openid: app.globalData.openid }).get({ success: function(res) { let userInfos = res.data; console.log(res.data) if (userInfos && userInfos.length > 0) { let user = userInfos[0]; if (user.name !== name) { app.showTips('用户名不匹配'); } else if (user.password !== password) { app.showTips('密码不匹配'); } else { app.showTips('登陆成功'); let jsonStr=JSON.stringify(user); wx.navigateTo({ url: '../index/index?jsonStr=' + jsonStr, }) } } else { app.showTips('用户不存在'); } } }) }, register() { wx.navigateTo({ url: '../register/register', }) }, }) [代码] 3,登陆成功后显示用户信息 电话号码是胡乱填的,想联系我可以__加我微信2501902696__ [图片] 这样就可以实现小程序的登陆与注册了。 想要完整源码或者有小程序相关的问题,可以加我微信2501902696(备注小程序)
2019-02-21 - 微信小程序弹出用户授权弹窗,微信小程序引导用户授权,获取位置经纬度
我们在开发小程序时,有些操作必须让用户授权。比如我们获取用户位置,需要用户授权位置信息。授权操作我们需要给用户弹窗提示,在用户禁用某些权限时,又要引导用户去设置页开启相应权限。我们这里就以获取经纬度为例,来带大家学会友好的引导用户授权。 老规矩,先看效果图 [图片] 一,我们使用位置信息,就需要授权 [代码] //校验位置权限是否打开 checkLocation() { let that = this; //选择位置,需要用户授权 wx.getSetting({ success(res) { if (!res.authSetting['scope.userLocation']) { wx.authorize({ scope: 'scope.userLocation', success() { wx.showToast({ //这里提示失败原因 title: '授权成功!', duration: 1500 }) }, fail() { that.showSettingToast('需要授权位置信息'); } }) } } }) }, [代码] [图片] 首先检验用户是否授权位置信息的权限“scope.userLocation”,如果有授权,我们就可以直接去获取用户的位置经纬度了。如果没有授权,我们就弹窗引导用户去设置页。去设置页的方法如下 [代码] // 打开权限设置页提示框 showSettingToast: function (e) { wx.showModal({ title: '提示!', confirmText: '去设置', showCancel: false, content: e, success: function (res) { if (res.confirm) { wx.navigateTo({ url: '../setting/setting', }) } } }) }, [代码] [图片] 由于去设置页,需要用户手动触发,这里我们就用一个setting.wxml页作为过过渡页。 [图片] 我们这个过渡页的按钮,用户点击后就会去真正的授权页了。 [图片] 当用户开启地理位置授权后。我们再点击获取位置,就可以获取到用户当前的经纬度了。 [图片] 完整代码如下 [代码]//index.js Page({ getLocation() { this.checkLocation(); let that = this; wx.chooseLocation({ success: function(res) { var latitude = res.latitude var longitude = res.longitude; that.setData({ address: "经纬度:" + longitude + ", " + latitude, }) } }); }, //校验位置权限是否打开 checkLocation() { let that = this; //选择位置,需要用户授权 wx.getSetting({ success(res) { if (!res.authSetting['scope.userLocation']) { wx.authorize({ scope: 'scope.userLocation', success() { wx.showToast({ //这里提示失败原因 title: '授权成功!', duration: 1500 }) }, fail() { that.showSettingToast('需要授权位置信息'); } }) } } }) }, // 打开权限设置页提示框 showSettingToast: function (e) { wx.showModal({ title: '提示!', confirmText: '去设置', showCancel: false, content: e, success: function (res) { if (res.confirm) { wx.navigateTo({ url: '../setting/setting', }) } } }) }, }) [代码] 还有我们授权必须再app.json里注册相关权限。如我的app.json如下 [代码]{ "pages": [ "pages/index/index", "pages/setting/setting" ], "window": { "backgroundTextStyle": "light", "navigationBarBackgroundColor": "#fff", "navigationBarTitleText": "WeChat", "navigationBarTextStyle": "black" }, "permission": { "scope.userLocation": { "desc": "你的位置信息将用于小程序位置接口的效果展示" } } } [代码] 到此我们就实现了小程序引导授权的全部功能,并且可以获取到用户的位置经纬度了。是不是很简单。 如果有关于小程序的问题,可以加我微信2501902696(备注小程序)
2019-02-21 - 微信小程序银行卡号的识别
整体流程图如下: [图片] 1、小程序调起摄像头 <camera device-position=“back” flash=“off” binderror=“error” mode=“normal” style=“width: 100%; height: 300px;”></camera> <button type=“primary” bindtap=“takePhoto”>拍照</button> 2、拍摄照片并把照片转化为base64编码 拍摄的照片路径为临时路径,所以需要转化为base64编码 [代码]takePhoto() { const me = this; const ctx = wx.createCameraContext() ctx.takePhoto({ quality: 'normal', success: (res) => { const fs = wx.getFileSystemManager(); fs.readFile({ filePath: res.tempImagePath, encoding: 'base64', success(res) { } }) } }) }, [代码] 3、图片传送给第三方云平台ocr 接口(这里是以百度云举例) 1、获取第三方云平台的token [代码] const token = await identificationCard.getToken({ query: { grant_type: 'client_credentials', client_id: '自己申请的账号', client_secret: '自己申请的密钥' } }) [代码] 2、第三方云平台解析拍摄的银行卡 [代码]identificationCard.getCardDetail返回的card就是相应图片中银行卡信息 const card = await identificationCard.getCardDetail(token.data.access_token, { query: { image: base64 }, method: "POST" }) [代码] 4、完整代码 wxml <camera device-position=“back” flash=“off” binderror=“error” mode=“normal” style=“width: 100%; height: 300px;”></camera> <button type=“primary” bindtap=“takePhoto”>拍照</button> [代码]api const identificationCard = { getToken: (params) => wxRequest(params, 'https://aip.baidubce.com/oauth/2.0/token'), getCardDetail: (query, params)=> wxRequest(params, 'https://aip.baidubce.com/rest/2.0/ocr/v1/bankcard?access_token='+query), } js async getCardDetail(base64){ const token = await identificationCard.getToken({ query: { grant_type: 'client_credentials', client_id: '自己申请的账号', client_secret: '自己申请的密钥' } }) const card = await identificationCard.getCardDetail(token.data.access_token, { query: { image: base64 }, method: "POST" }) } takePhoto() { const me = this; const ctx = wx.createCameraContext() ctx.takePhoto({ quality: 'normal', success: (res) => { const fs = wx.getFileSystemManager(); fs.readFile({ filePath: res.tempImagePath, encoding: 'base64', success(res) { me.getCardDetail(res.data) } }) } }) } ```[代码]
2019-02-21 - 小程序性能和体验优化方法
[图片] 小程序应避免出现任何 JavaScript 异常 出现 JavaScript 异常可能导致小程序的交互无法进行下去,我们应当追求零异常,保证小程序的高鲁棒性和高可用性 小程序所有请求应响应正常 请求失败可能导致小程序的交互无法进行下去,应当保证所有请求都能成功 所有请求的耗时不应太久 请求的耗时太长会让用户一直等待甚至离开,应当优化好服务器处理时间、减小回包大小,让请求快速响应 避免短时间内发起太多的图片请求 短时间内发起太多图片请求会触发浏览器并行加载的限制,可能导致图片加载慢,用户一直处理等待。应该合理控制数量,可考虑使用雪碧图技术或在屏幕外的图片使用懒加载 避免短时间内发起太多的请求 短时间内发起太多请求会触发小程序并行请求数量的限制,同时太多请求也可能导致加载慢等问题,应合理控制请求数量,甚至做请求的合并等 避免 setData 的数据过大 setData工作原理 小程序的视图层目前使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境。在架构上,WebView 和 JavascriptCore 都是独立的模块,并不具备数据直接共享的通道。当前,视图层和逻辑层的数据传输,实际上通过两边提供的 evaluateJavascript 所实现。即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 JS 脚本,再通过执行 JS 脚本的形式传递到两边独立环境。 而 evaluateJavascript 的执行会受很多方面的影响,数据到达视图层并不是实时的。 由于小程序运行逻辑线程与渲染线程之上,setData的调用会把数据从逻辑层传到渲染层,数据太大会增加通信时间 常见的 setData 操作错误 频繁的去 setData Android 下用户在滑动时会感觉到卡顿,操作反馈延迟严重,因为 JS 线程一直在编译执行渲染,未能及时将用户操作事件传递到逻辑层,逻辑层亦无法及时将操作处理结果及时传递到视图层 染有出现延时,由于 WebView 的 JS 线程一直处于忙碌状态,逻辑层到页面层的通信耗时上升,视图层收到的数据消息时距离发出时间已经过去了几百毫秒,渲染的结果并不实时 每次 setData 都传递大量新数据 由setData的底层实现可知,数据传输实际是一次 evaluateJavascript 脚本过程,当数据量过大时会增加脚本的编译执行时间,占用 WebView JS 线程 后台态页面进行 setData 当页面进入后台态(用户不可见),不应该继续去进行setData,后台态页面的渲染用户是无法感受的,另外后台态页面去setData也会抢占前台页面的执行 避免 setData 的调用过于频繁 setData接口的调用涉及逻辑层与渲染层间的线程通过,通信过于频繁可能导致处理队列阻塞,界面渲染不及时而导致卡顿,应避免无用的频繁调用 避免将未绑定在 WXML 的变量传入 setData setData操作会引起框架处理一些渲染界面相关的工作,一个未绑定的变量意味着与界面渲染无关,传入setData会造成不必要的性能消耗 合理设置可点击元素的响应区域大小 我们应该合理地设置好可点击元素的响应区域大小,如果过小会导致用户很难点中,体验很差 避免渲染界面的耗时过长 渲染界面的耗时过长会让用户觉得卡顿,体验较差,出现这一情况时,需要校验下是否同时渲染的区域太大 避免执行脚本的耗时过长 执行脚本的耗时过长会让用户觉得卡顿,体验较差,出现这一情况时,需要确认并优化脚本的逻辑 对网络请求做必要的缓存以避免多余的请求 发起网络请求总会让用户等待,可能造成不好的体验,应尽量避免多余的请求,比如对同样的请求进行缓存 wxss 覆盖率较高,较少或没有引入未被使用的样式 按需引入 wxss 资源,如果小程序中存在大量未使用的样式,会增加小程序包体积大小,从而在一定程度上影响加载速度 文字颜色与背景色搭配较好,适宜的颜色对比度更方便用户阅读 文字颜色与背景色需要搭配得当,适宜的颜色对比度可以让用户更好地阅读,提升小程序的用户体验 所有资源请求都建议使用 HTTPS 使用 HTTPS,可以让你的小程序更加安全,而 HTTP 是明文传输的,存在可能被篡改内容的风险 不使用废弃接口 使用即将废弃或已废弃接口,可能导致小程序运行不正常。一般而言,接口不会立即去掉,但保险起见,建议不要使用,避免后续小程序突然运行异常 避免过大的 WXML 节点数目 建议一个页面使用少于 1000 个 WXML 节点,节点树深度少于 30 层,子节点数不大于 60 个。一个太大的 WXML 节点树会增加内存的使用,样式重排时间也会更长 避免将不可能被访问到的页面打包在小程序包里 小程序的包大小会影响加载时间,应该尽量控制包体积大小,避免将不会被使用的文件打包进去 及时回收定时器 定时器是全局的,并不是跟页面绑定的,当页面因后退被销毁时,定时器应注意手动回收 避免使用 css ‘:active’ 伪类来实现点击态 使用 css ‘:active’ 伪类来实现点击态,很容易触发,并且滚动或滑动时点击态不会消失,体验较差 建议使用小程序内置组件的 ‘hover-*’ 属性来实现 滚动区域可开启惯性滚动以增强体验 惯性滚动会使滚动比较顺畅,在安卓下默认有惯性滚动,而在 iOS 下需要额外设置 [代码]-webkit-overflow-scrolling: touch[代码] 的样式
2019-03-15 - 小程序原生高颜值组件库--ColorUI
[图片] 简介 ColorUI是一个Css类的UI组件库!不是一个Js框架。相比于同类小程序组件库,ColorUI更注重于视觉交互! 浏览GitHub:https://github.com/weilanwl/ColorUI [图片] 如何使用? 先下载源码包 → Github 引入到我的小程序 将 /demo/ 下的 colorui.wxss 和 icon.wxss 复制到小程序的根目录下 在 app.wxss 引入两个文件 [代码]@import "icon.wxss"; @import "colorui.wxss"; [代码] 使用模板全新开发 复制 /template/ 文件夹并重命名为你的项目,微信开发者工具导入为小程序就可以使用ColorUI了 体验沉浸式导航 [图片] App.js 获取系统参数并写入全局参数。 [代码]//App.js App({ onLaunch: function() { wx.getSystemInfo({ success: e => { this.globalData.StatusBar = e.statusBarHeight; let custom = wx.getMenuButtonBoundingClientRect(); this.globalData.Custom = custom; this.globalData.CustomBar = custom.bottom + custom.top - e.statusBarHeight; } }) } }) [代码] Page.js 页面配置获取全局参数。 [代码]//Page.js const app = getApp() Page({ data: { StatusBar: app.globalData.StatusBar, CustomBar: app.globalData.CustomBar, Custom: app.globalData.Custom } }) [代码] Page.wxml 页面构造导航。更多导航样式请下载Demo查阅 操作条组件。 [代码]<view class="cu-custom" style="height:{{CustomBar}}px;"> <view class="cu-bar fixed bg-gradual-pink" style="height:{{CustomBar}}px;padding-top:{{StatusBar}}px;"> <navigator class='action border-custom' open-type="navigateBack" delta="1" hover-class="none" style='width:{{Custom.width}}px;height:{{Custom.height}}px;margin-left:calc(750rpx - {{Custom.right}}px)'> <text class='icon-back'></text> <text class='icon-homefill'></text> </navigator> <view class='content' style='top:{{StatusBar}}px;'>操作条</view> </view> </view> [代码] 自定义系统Tabbar [图片] 按照官方 自定义 tabBar 配置好Tabbar (开发工具和版本库请使用最新版)。 使用ColorUI配置Tabbar只需要更改 Wxml 页的内容即可。 更多Tabbar样式请下载Demo查阅 操作条组件。 /custom-tab-bar/index.wxml [代码] <view class="cu-bar tabbar bg-white shadow"> <view class="action" wx:for="{{list}}" wx:key="index" data-path="{{item.pagePath}}" data-index="{{index}}" bindtap="switchTab"> <view class='icon-cu-image'> <image src='{{selected === index ? item.selectedIconPath : item.iconPath}}' class='{{selected === index ? "animation" : "animation"}}'></image> </view> <view class='{{selected === index ? "text-green" : "text-gray"}}'>{{item.text}}</view> </view> </view> [代码] 作者叨叨 ColorUI是一个高度自定义的Css样式库,包含了开发常用的元素和组件,元素组件之间也能相互嵌套使用。我也会不定期更新一些扩展到源码。 其实大家都在催我写文档,但这个库源码就在这,所见即所得,粘贴复制就可以得到你想要的页面。当然,文档我还是要写的,也希望大家多多提意见。 现在前端的开发方向基本都是奔着Js方向的,布局和样式大家讨论的有点少。以后我会在开发者社区多聊一聊关于开发中的布局和样式。 [图片] 感谢阅读。
2019-02-26 - 「分享」高性能双列瀑布流极简实现(附示例)❤️
前言 在日常开发过程中,经常会有双列瀑布流场景的需求出现,如商品列表、文章列表等,本文将简单介绍这种情景下如何高效、精准的实现双列瀑布流场景,支持刷新、加载更多等,实现效果如下。 [图片] [图片] 开发思路 瀑布流视图有一种参差的美感,常规列表布局如 flex wrap 等由于存在行高度限制,无法让第二行的 item 对齐上一行最矮处,因此,瀑布流布局时采用双列 scrollview 的 flex 布局。 参差布局的实现,采用代码计算左右两列的高度,然后对左右两列总高度进行比较,新加入的 item 总是排在总高度较小的那列后面。 计算时可以尽可能的缓存高度,例如左右两列高度在每次计算时都缓存起来,有新的 item 加入列表时直接增加左右两列高度即可,不需要重新从头计算。 index.js [代码]const tplWidth = (750 - 24 - 8) / 2; const tplHeight = 595; // plWidth * 1.66 newPhotos.forEach(photo => { const { height, width } = photo let photoHeight = tplWidth if (height > width) { photoHeight = tplHeight photo.display = 'long' } else { photo.display = 'short' } if (leftHeight < rightHeight) { leftList.push(photo) leftHeight += photoHeight } else { rightList.push(photo) rightHeight += photoHeight } }) [代码] index.wxml [代码]<!-- list --> <view class='list'> <!-- left --> <view class='left-list'> <block wx:for="{{leftList}}" wx:key="{{item._id}}"> <cell photo="{{item}}" bindclick='onCellClicked' /> </block> </view> <!-- right --> <view class='right-list'> <block wx:for="{{rightList}}" wx:key="{{item._id}}"> <cell photo="{{item}}" bindclick='onCellClicked' /> </block> </view> </view> [代码] index.css [代码].list { display: flex; flex: 1; position: relative; flex-direction: row; justify-content: space-between; padding-left: 12rpx; padding-right: 12rpx; padding-top: 8rpx; } .left-list { display: flex; position: relative; flex-direction: column; width: 359rpx; } .right-list { display: flex; position: relative; flex-direction: column; width: 359rpx; } [代码]
2019-02-27 - 小程序实现大转盘,九宫格抽奖,带跑马灯效果
基本实现功能 1,小程序仿天猫超市大转盘 2,九宫格转盘抽奖 3,积分抽奖 4,抽到的积分随机生成 5,抽奖结果可以同步到服务器(小程序云开发后台) 老规矩先看效果图 [图片] 简单说一下实现原理. 我们借助js的定时器,来执行一个加法。比如我们设置一个上限300,每过一定时间执行一次,然后我们再做一个随机数,这个随机数不停的++,直到总数大于300.就代表抽奖结束。核心代码如下。 [代码] //开始抽奖 startGame: function() { if (this.data.isRunning) return this.setData({ isRunning: true }) var _this = this; var indexSelect = 0 var i = 0; var timer = setInterval(function() { indexSelect++; let randomNum = Math.floor(Math.random() * 10) * 10; //可均衡获取0到90的随机整数 i += randomNum; if (i > 300) { //去除循环 clearInterval(timer) //获奖提示 let jifen = 1; let selectNum = _this.data.indexSelect console.log("选号:" + selectNum ); if (selectNum===0) { jifen = 2; } else if (selectNum === 1) { jifen = 3; } else if (selectNum === 2) { jifen = 4; } else if (selectNum === 3) { jifen = 5; } else if(selectNum === 4) { jifen = 6; } else if(selectNum === 5) { jifen = 8; } else if (selectNum === 6) { jifen = 10; } wx.showModal({ title: '恭喜您', content: '获得了' + jifen + "积分", showCancel: false, //去掉取消按钮 success: function(res) { if (res.confirm) { _this.setData({ isRunning: false }) } } }) } indexSelect = indexSelect % 8; _this.setData({ indexSelect: indexSelect }) }, (200 + i)) } [代码] 完整源码可以加我微信,如果有关于小程序的问题,可以加我微信2501902696(备注小程序)
2019-03-05 - 微信小程序开发常见问题汇总
1、域名必须是https 非https的域名不被微信小程序允许。 2、input组件placeholder字体颜色 卸载placeholder-class里面的color并不生效,需要写在placeholder-style里面就可以了。 3、wx.navigateTo无法跳转到带tabbar的页面 带有tabbar的页面,必须使用wx.switchTab进行跳转。 4、tabbar在切换时页面数据无法刷新 tabbar的实现可能是显示和隐藏view,所以,不会一直调用page.onLoad()方法,可以尝试把代码逻辑写在page.onShow()里面。 5、如何获取shareTickets 获取shareTickets需要在app.onLaunch或者app.onShow里面才能获取到,而不是page.onShow,请一定要注意。 注:建议在app.onShow里面去获取,app.onLaunch不是一直会执行。 6、getPhoneNumber获取手机号 目前该接口针对非个人开发者,且完成了认证的小程序开放。个人开发者是没办法调用这个API的。 7、wx.previewImage图片预览 预览图片URL必须是https开头,不能是本地图片。 8、wx.playVoice音频播放 必须保证音频文件已经在本地,比如在wx.startRecord后,可以获取到filePath。或者提前调用wx.downloadFile来下载资源文件,然后再播放。 9、API老版本兼容 可以用wx.canIUse或者wx.getSystemInfoSync来进行判断,老版本给出相应提示即可。 10、获取系统信息 wx.getSystemInfo,可得到系统语言、屏幕宽高、微信版本号、操作系统、设备像素比、客户端甚础库版本等信息。 11、如何去掉自定义button灰色的圆角边框 主要是button的伪元素设置了样式,去掉即可:button::after{ display: none;}。 12、回到页面顶部 主要是button的伪元素设置了样式,去掉即可:button::after{ display: none;}。 13、input textarea是APP的原生组件,z-index层级最高 有做过搜索框的同学,可能会遇到iOS下面,设置icon的z-index后,依然无法显示。建议做显示隐藏效果:点击之前是一个view,点击之后隐藏view,显示input。 14、小程序如何冷启动 小程序的机制,是在退出五分钟内进入,就会显示的是退出前的页面,如果你希望进入小程序都相当于冷启动的方式,直接进入主页面。你可以在page的onUnload里面里面set一个值,然后在app的onShow的时候判断这个值,然后决定是否跳到首页。 15、一段文字如何换行 小程序中唯一可以实现换行的标签组件是text。 注:text中不支持<br>,只能使用n进行换行。 16、设置最外层标签的margin-bottom在iOS下不生效 margin-bottom在安卓和开发工具里面都正常,就是在iOS下不起效,建议改成padding-bottom。 17、小程序中canvas的图片不支持base64格式 base64格式图片,在开发工具里面可以正常显示,真机上没有显示。建议修改成带https开头的url形式。 微信小程序开发视频教程分享:https://www.sucaihuo.com/video/222-0-0
2019-03-05 - 小程序中使用防抖函数
这几天看了很多关于防抖函数的博客,我是在微信小程序中使用,在此总结一下关于防抖函数的知识。 为什么需要防抖函数? 防抖函数适用的是【有大量重复操作】的场景,比如列表渲染之后对每一项进行操作。 函数代码: [代码]var timer; debounce: function (func, wait) { return () => { clearTimeout(timer); timer = setTimeout(func, wait); }; }, [代码] 参数: func:需要防抖的函数; wait:number类型,setTimeout的时间参数; 代码分析: 命名一个叫做debounce的函数,参数有两个(func,wait),return一个函数,内容为清除计时器,然后设置计时器,计时器的意思是:在wait时间后执行func。 清除计时器是整个函数的核心,因为防抖需要不停地清除计时器,最后在计时器结束后触发func来达到效果。 防抖函数的调用方法 example: [代码]this.debounce(this.函数名,3000)() [代码] 在使用这个函数的时候我遇到了一些问题: 因为微信小程序中很多地方都需要使用this.setData,如果对于this指向的理解不深入的话,很容易出现以下情况: 1:this==undefined; 2:Error:data is not defined; 等等一些列关于this的问题。 解决方法: [代码]this.debounce(this.函数名.bind(this),3000)() [代码] 使用bind(this)把this指向传到函数内部就解决了。
2019-03-19 - setData 学问多
为什么不能频繁 setData 先科普下 setData 做的事情: 在数据传输时,逻辑层会执行一次 JSON.stringify 来去除掉 setData 数据中不可传输的部分,之后将数据发送给视图层。同时,逻辑层还会将 setData 所设置的数据字段与 data 合并,使开发者可以用 this.data 读取到变更后的数据。 因此频繁调用,视图会一直更新,阻塞用户交互,引发性能问题。 但频繁调用是常见开发场景,能不能频繁调用的同时,视图延迟更新呢? 参考 Vue,我们能知道,Vue 每次赋值操作并不会直接更新视图,而是缓存到一个数据更新队列中,异步更新,再触发渲染,此时多次赋值,也只会渲染一次。 [代码]let newState = null; let timeout = null; const asyncSetData = ({ vm, newData, }) => { newState = { ...newState, ...newData, }; clearTimeout(timeout); timeout = setTimeout(() => { vm.setData({ ...newState, }); newState = null }, 0); }; [代码] 由于异步代码会在同步代码之后执行,因此,当你多次使用 asyncSetData 设置 newState 时,newState 都会被缓存起来,并异步 setData 一次 但同时,这个方案也会带来一个新的问题,同步代码会阻塞页面的渲染。 同步代码会阻塞页面的渲染的问题其实在浏览器中也存在,但在小程序中,由于是逻辑、视图双线程架构,因此逻辑并不会阻塞视图渲染,这是小程序的优点,但在这套方案将会丢失这个优点。 鱼与熊掌不可兼得也! 对于信息流页面,数据过多怎么办 单次设置的数据不能超过 1024kB,请尽量避免一次设置过多的数据 通常,我们拉取到分页的数据 newList,添加到数组里,一般是这么写: [代码]this.setData({ list: this.data.list.concat(newList) }) [代码] 随着分页次数的增加,list 会逐渐增大,当超过 1024 kb 时,程序会报 [代码]exceed max data size[代码] 错误。 为了避免这个问题,我们可以直接修改 list 的某项数据,而不是对整个 list 重新赋值: [代码]let length = this.data.list.length; let newData = newList.reduce((acc, v, i)=>{ acc[`list[${length+i}]`] = v; return acc; }, {}); this.setData(newData); [代码] 这看着似乎还有点繁琐,为了简化操作,我们可以把 list 的数据结构从一维数组改为二维数组:[代码]list = [newList, newList][代码], 每次分页,可以直接将整个 newList 赋值到 list 作为一个子数组,此时赋值方式为: [代码]let length = this.data.list.length; this.setData({ [`list[${length}]`]: newList }); [代码] 同时,模板也需要相应改成二重循环: [代码]<block wx:for="{{list}}" wx:for-item="listItem" wx:key="{{listItem}}"> <child wx:for="{{listItem}}" wx:key="{{item}}"></child> </block> [代码] 下拉加载,让我们一夜回到解放前 信息流产品,总避免不了要做下拉加载。 下拉加载的数据,需要插到 list 的最前面,所以我们应该这样做: [代码]this.setData({ `list[-1]`: newList }) [代码] 哦不,对不起,上面是错的,应该是下面这样: [代码]this.setData({ list: this.data.list.unshift(newList) }); [代码] 这下好,又是一次性修改整个数组,一夜回到解放前… 为了解决这个问题,这里需要一点奇淫巧技: 为下拉加载维护一个单独的二维数组 pullDownList 在渲染时,用 wxs 将 pullDownList reverse 一下 此时,当下拉加载时,便可以只修改数组的某个子项: [代码]let length = this.data.pullDownList.length; this.setData({ [`pullDownList[${length}]`]: newList }); [代码] 关键在于渲染时候的反向渲染: [代码]<wxs module="utils"> function reverseArr(arr) { return arr.reverse() } module.exports = { reverseArr: reverseArr } </wxs> <block wx:for="{{utils.reverseArr(pullDownList)}}" wx:for-item="listItem" wx:key="{{listItem}}"> <child wx:for="{{listItem}}" wx:key="{{item}}"></child> </block> [代码] 问题解决! 参考资料 终极蛇皮上帝视角之微信小程序之告别 setData, 佯真愚, 2018年08月12日
2019-04-11 - 微信开发小技巧:小程序页面间如何进行通信
FlashEvent FlashEvent 小程序页面间的通信工具 - 类似于EventBus FlashEvent 在小程序中 能够简化各页面间的通信,让代码书写变得简单,能有效的解耦事件发送方和接收方,能避免复杂和容易出错的依赖性和生命周期问题。 github add: https://github.com/wuyajun7/FlashEvent 使用方式: 前置:将FlashEvent.js导入到项目的utils文件中 1、接收方js代码中 1.1 引入该类,如:let flashEvent = require(‘你的路径/utils/FlashEvent.js’); 1.2 注册FlashEvent,如:在onLoad中 [代码] flashEvent.register(flashEvent.EVENT_KEYS.FIRST_EVENT, this, function (data) { this.setData({ eventCallBack: data }) }) [代码] 1.3 注销FlashEvent,如:在onUnload中调用 flashEvent.unregister(flashEvent.EVENT_KEYS.FIRST_EVENT, this); 2、发送方js代码中 2.1 引入该类,如:let flashEvent = require(‘你的路径/utils/FlashEvent.js’); 2.2 发送事件,如:flashEvent.post(flashEvent.EVENT_KEYS.FIRST_EVENT, ‘发送的数据’); flashEvent 简单接入、方便使用
2019-03-28 - Service Worker学习与实践(三)——消息推送
在上一篇文章Service Worker学习与实践(二)——PWA简介中,已经讲到[代码]PWA[代码]的起源,优势与劣势,并通过一个简单的例子说明了如何在桌面端和移动端将一个[代码]PWA[代码]安装到桌面上,这篇文章,将通过一个例子阐述如何使用[代码]Service Worker[代码]的消息推送功能,并配合[代码]PWA[代码]技术,带来原生应用般的消息推送体验。 Notification 说到底,[代码]PWA[代码]的消息推送也是服务端推送的一种,常见的服务端推送方法,例如广泛使用的轮询、长轮询、[代码]Web Socket[代码]等,说到底,都是客户端与服务端之间的通信,在[代码]Service Worker[代码]中,客户端接收到通知,是基于Notification来进行推送的。 那么,我们来看一下,如何直接使用[代码]Notification[代码]来发送一条推送呢?下面是一段示例代码: [代码]// 在主线程中使用 let notification = new Notification('您有新消息', { body: 'Hello Service Worker', icon: './images/logo/logo152.png', }); notification.onclick = function() { console.log('点击了'); }; [代码] 在控制台敲下上述代码后,则会弹出以下通知: [图片] 然而,[代码]Notification[代码]这个[代码]API[代码],只推荐在[代码]Service Worker[代码]中使用,不推荐在主线程中使用,在[代码]Service Worker[代码]中的使用方法为: [代码]// 添加notificationclick事件监听器,在点击notification时触发 self.addEventListener('notificationclick', function(event) { // 关闭当前的弹窗 event.notification.close(); // 在新窗口打开页面 event.waitUntil( clients.openWindow('https://google.com') ); }); // 触发一条通知 self.registration.showNotification('您有新消息', { body: 'Hello Service Worker', icon: './images/logo/logo152.png', }); [代码] 读者可以在MDN Web Docs关于[代码]Notification[代码]在[代码]Service Worker[代码]中的相关用法,在本文就不浪费大量篇幅来进行较为详细的阐述了。 申请推送的权限 如果浏览器直接给所有开发者开放向用户推送通知的权限,那么势必用户会受到大量垃圾信息的骚扰,因此这一权限是需要申请的,如果用户禁止了消息推送,开发者是没有权利向用户发起消息推送的。我们可以通过serviceWorkerRegistration.pushManager.getSubscription方法查看用户是否已经允许推送通知的权限。修改[代码]sw-register.js[代码]中的代码: [代码]if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js').then(function (swReg) { swReg.pushManager.getSubscription() .then(function(subscription) { if (subscription) { console.log(JSON.stringify(subscription)); } else { console.log('没有订阅'); subscribeUser(swReg); } }); }); } [代码] 上面的代码调用了[代码]swReg.pushManager[代码]的[代码]getSubscription[代码],可以知道用户是否已经允许进行消息推送,如果[代码]swReg.pushManager.getSubscription[代码]的[代码]Promise[代码]被[代码]reject[代码]了,则表示用户还没有订阅我们的消息,调用[代码]subscribeUser[代码]方法,向用户申请消息推送的权限: [代码]function subscribeUser(swReg) { const applicationServerKey = urlB64ToUint8Array(applicationServerPublicKey); swReg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: applicationServerKey }) .then(function(subscription) { console.log(JSON.stringify(subscription)); }) .catch(function(err) { console.log('订阅失败: ', err); }); } [代码] 上面的代码通过serviceWorkerRegistration.pushManager.subscribe向用户发起订阅的权限,这个方法返回一个[代码]Promise[代码],如果[代码]Promise[代码]被[代码]resolve[代码],则表示用户允许应用程序推送消息,反之,如果被[代码]reject[代码],则表示用户拒绝了应用程序的消息推送。如下图所示: [图片] [代码]serviceWorkerRegistration.pushManager.subscribe[代码]方法通常需要传递两个参数: [代码]userVisibleOnly[代码],这个参数通常被设置为[代码]true[代码],用来表示后续信息是否展示给用户。 [代码]applicationServerKey[代码],这个参数是一个[代码]Uint8Array[代码],用于加密服务端的推送信息,防止中间人攻击,会话被攻击者篡改。这一参数是由服务端生成的公钥,通过[代码]urlB64ToUint8Array[代码]转换的,这一函数通常是固定的,如下所示: [代码]function urlB64ToUint8Array(base64String) { const padding = '='.repeat((4 - base64String.length % 4) % 4); const base64 = (base64String + padding) .replace(/\-/g, '+') .replace(/_/g, '/'); const rawData = window.atob(base64); const outputArray = new Uint8Array(rawData.length); for (let i = 0; i < rawData.length; ++i) { outputArray[i] = rawData.charCodeAt(i); } return outputArray; } [代码] 关于服务端公钥如何获取,在文章后续会有相关阐述。 处理拒绝的权限 如果在调用[代码]serviceWorkerRegistration.pushManager.subscribe[代码]后,用户拒绝了推送权限,同样也可以在应用程序中,通过Notification.permission获取到这一状态,[代码]Notification.permission[代码]有以下三个取值,: [代码]granted[代码]:用户已经明确的授予了显示通知的权限。 [代码]denied[代码]:用户已经明确的拒绝了显示通知的权限。 [代码]default[代码]:用户还未被询问是否授权,在应用程序中,这种情况下权限将视为[代码]denied[代码]。 [代码]if (Notification.permission === 'granted') { // 用户允许消息推送 } else { // 还不允许消息推送,向用户申请消息推送的权限 } [代码] 密钥生成 上述代码中的[代码]applicationServerPublicKey[代码]通常情况下是由服务端生成的公钥,在页面初始化的时候就会返回给客户端,服务端会保存每个用户对应的公钥与私钥,以便进行消息推送。 在我的示例演示中,我们可以使用[代码]Google[代码]配套的实验网站web-push-codelab生成公钥与私钥,以便发送消息通知: [图片] 发送推送 在[代码]Service Worker[代码]中,通过监听[代码]push[代码]事件来处理消息推送: [代码]self.addEventListener('push', function(event) { const title = event.data.text(); const options = { body: event.data.text(), icon: './images/logo/logo512.png', }; event.waitUntil(self.registration.showNotification(title, options)); }); [代码] 在上面的代码中,在[代码]push[代码]事件回调中,通过[代码]event.data.text()[代码]拿到消息推送的文本,然后调用上面所说的[代码]self.registration.showNotification[代码]来展示消息推送。 服务端发送 那么,如何在服务端识别指定的用户,向其发送对应的消息推送呢? 在调用[代码]swReg.pushManager.subscribe[代码]方法后,如果用户是允许消息推送的,那么该函数返回的[代码]Promise[代码]将会[代码]resolve[代码],在[代码]then[代码]中获取到对应的[代码]subscription[代码]。 [代码]subscription[代码]一般是下面的格式: [代码]{ "endpoint": "https://fcm.googleapis.com/fcm/send/cSEJGmI_x2s:APA91bHzRHllE6tNoEHqjHQSlLpcQHeiGr7X78EIa1QrUPFqDGDM_4RVKNxoLPV3_AaCCejR4uwUawBKYcQLmLpUrCUoZetQ9pVzQCJSomB5BvoFZBzkSnUb-ALm4D1lqwV9w_uP3M0E", "expirationTime": null, "keys": { "p256dh": "BDOx1ZTtsFL2ncSN17Bu7-Wl_1Z7yIiI-lKhtoJ2dAZMToGz-XtQOe6cuMLMa3I8FoqPfcPy232uAqoISB4Z-UU", "auth": "XGWy-wlmrAw3Be818GLZ8Q" } } [代码] 使用[代码]Google[代码]配套的实验网站web-push-codelab,发送消息推送。 [图片] web-push 在服务端,使用web-push-libs,实现公钥与私钥的生成,消息推送功能,Node.js版本。 [代码]const webpush = require('web-push'); // VAPID keys should only be generated only once. const vapidKeys = webpush.generateVAPIDKeys(); webpush.setGCMAPIKey('<Your GCM API Key Here>'); webpush.setVapidDetails( 'mailto:example@yourdomain.org', vapidKeys.publicKey, vapidKeys.privateKey ); // pushSubscription是前端通过swReg.pushManager.subscribe获取到的subscription const pushSubscription = { endpoint: '.....', keys: { auth: '.....', p256dh: '.....' } }; webpush.sendNotification(pushSubscription, 'Your Push Payload Text'); [代码] 上面的代码中,[代码]GCM API Key[代码]需要在Firebase console中申请,申请教程可参考这篇博文。 在这个我写的示例[代码]Demo[代码]中,我把[代码]subscription[代码]写死了: [代码]const webpush = require('web-push'); webpush.setVapidDetails( 'mailto:503908971@qq.com', 'BCx1qqSFCJBRGZzPaFa8AbvjxtuJj9zJie_pXom2HI-gisHUUnlAFzrkb-W1_IisYnTcUXHmc5Ie3F58M1uYhZU', 'g5pubRphHZkMQhvgjdnVvq8_4bs7qmCrlX-zWAJE9u8' ); const subscription = { "endpoint": "https://fcm.googleapis.com/fcm/send/cSEJGmI_x2s:APA91bHzRHllE6tNoEHqjHQSlLpcQHeiGr7X78EIa1QrUPFqDGDM_4RVKNxoLPV3_AaCCejR4uwUawBKYcQLmLpUrCUoZetQ9pVzQCJSomB5BvoFZBzkSnUb-ALm4D1lqwV9w_uP3M0E", "expirationTime": null, "keys": { "p256dh": "BDOx1ZTtsFL2ncSN17Bu7-Wl_1Z7yIiI-lKhtoJ2dAZMToGz-XtQOe6cuMLMa3I8FoqPfcPy232uAqoISB4Z-UU", "auth": "XGWy-wlmrAw3Be818GLZ8Q" } }; webpush.sendNotification(subscription, 'Counterxing'); [代码] 交互响应 默认情况下,推送的消息点击后是没有对应的交互的,配合clients API可以实现一些类似于原生应用的交互,这里参考了这篇博文的实现: [代码]Service Worker[代码]中的[代码]self.clients[代码]对象提供了[代码]Client[代码]的访问,[代码]Client[代码]接口表示一个可执行的上下文,如[代码]Worker[代码]或[代码]SharedWorker[代码]。[代码]Window[代码]客户端由更具体的[代码]WindowClient[代码]表示。 你可以从[代码]Clients.matchAll()[代码]和[代码]Clients.get()[代码]等方法获取[代码]Client/WindowClient[代码]对象。 新窗口打开 使用[代码]clients.openWindow[代码]在新窗口打开一个网页: [代码]self.addEventListener('notificationclick', function(event) { event.notification.close(); // 新窗口打开 event.waitUntil( clients.openWindow('https://google.com/') ); }); [代码] 聚焦已经打开的页面 利用[代码]cilents[代码]提供的相关[代码]API[代码]获取,当前浏览器已经打开的页面[代码]URLs[代码]。不过这些[代码]URLs[代码]只能是和你[代码]SW[代码]同域的。然后,通过匹配[代码]URL[代码],通过[代码]matchingClient.focus()[代码]进行聚焦。没有的话,则新打开页面即可。 [代码]self.addEventListener('notificationclick', function(event) { event.notification.close(); const urlToOpen = self.location.origin + '/index.html'; const promiseChain = clients.matchAll({ type: 'window', includeUncontrolled: true }) .then((windowClients) => { let matchingClient = null; for (let i = 0; i < windowClients.length; i++) { const windowClient = windowClients[i]; if (windowClient.url === urlToOpen) { matchingClient = windowClient; break; } } if (matchingClient) { return matchingClient.focus(); } else { return clients.openWindow(urlToOpen); } }); event.waitUntil(promiseChain); }); [代码] 检测是否需要推送 如果用户已经停留在当前的网页,那我们可能就不需要推送了,那么针对于这种情况,我们应该怎么检测用户是否正在网页上呢? 通过[代码]windowClient.focused[代码]可以检测到当前的[代码]Client[代码]是否处于聚焦状态。 [代码]self.addEventListener('push', function(event) { const promiseChain = clients.matchAll({ type: 'window', includeUncontrolled: true }) .then((windowClients) => { let mustShowNotification = true; for (let i = 0; i < windowClients.length; i++) { const windowClient = windowClients[i]; if (windowClient.focused) { mustShowNotification = false; break; } } return mustShowNotification; }) .then((mustShowNotification) => { if (mustShowNotification) { const title = event.data.text(); const options = { body: event.data.text(), icon: './images/logo/logo512.png', }; return self.registration.showNotification(title, options); } else { console.log('用户已经聚焦于当前页面,不需要推送。'); } }); }); [代码] 合并消息 该场景的主要针对消息的合并。比如,当只有一条消息时,可以直接推送,那如果该用户又发送一个消息呢? 这时候,比较好的用户体验是直接将推送合并为一个,然后替换即可。 那么,此时我们就需要获得当前已经展示的推送消息,这里主要通过[代码]registration.getNotifications() API[代码]来进行获取。该[代码]API[代码]返回的也是一个[代码]Promise[代码]对象。通过[代码]Promise[代码]在[代码]resolve[代码]后拿到的[代码]notifications[代码],判断其[代码]length[代码],进行消息合并。 [代码]self.addEventListener('push', function(event) { // ... .then((mustShowNotification) => { if (mustShowNotification) { return registration.getNotifications() .then(notifications => { let options = { icon: './images/logo/logo512.png', badge: './images/logo/logo512.png' }; let title = event.data.text(); if (notifications.length) { options.body = `您有${notifications.length}条新消息`; } else { options.body = event.data.text(); } return self.registration.showNotification(title, options); }); } else { console.log('用户已经聚焦于当前页面,不需要推送。'); } }); // ... }); [代码] [图片] 小结 本文通过一个简单的例子,讲述了[代码]Service Worker[代码]中消息推送的原理。[代码]Service Worker[代码]中的消息推送是基于[代码]Notification API[代码]的,这一[代码]API[代码]的使用首先需要用户授权,通过在[代码]Service Worker[代码]注册时的[代码]serviceWorkerRegistration.pushManager.subscribe[代码]方法来向用户申请权限,如果用户拒绝了消息推送,应用程序也需要相关处理。 消息推送是基于谷歌云服务的,因此,在国内,收到[代码]GFW[代码]的限制,这一功能的支持并不好,[代码]Google[代码]提供了一系列推送相关的库,例如[代码]Node.js[代码]中,使用web-push来实现。一般原理是:在服务端生成公钥和私钥,并针对用户将其公钥和私钥存储到服务端,客户端只存储公钥。[代码]Service Worker[代码]的[代码]swReg.pushManager.subscribe[代码]可以获取到[代码]subscription[代码],并发送给服务端,服务端利用[代码]subscription[代码]向指定的用户发起消息推送。 消息推送功能可以配合[代码]clients API[代码]做特殊处理。 如果用户安装了[代码]PWA[代码]应用,即使用户关闭了应用程序,[代码]Service Worker[代码]也在运行,即使用户未打开应用程序,也会收到消息通知。 在下一篇文章中,我将尝试在我所在的项目中使用[代码]Service Worker[代码],并通过[代码]Webpack[代码]和[代码]Workbox[代码]配置来讲述[代码]Service Worker[代码]的最佳实践。
2019-04-10 - Service Worker学习与实践(二)——PWA简介
在上一篇文章Service Worker学习与实践(一)——离线缓存中,已经讲到[代码]Service Worker[代码]的生命周期、如何创建、激活、更新[代码]Web[代码]应用程序的[代码]Service Worker[代码],并且给出了一个简单的示例来说明使用[代码]Service Worker[代码]来实现离线缓存的原理,在这篇文章里,主要是对[代码]Service Worker[代码]实现原生应用程序的能力做出一定解析,下一篇文章,将通过一个例子阐述如何使用[代码]Service Worker[代码]实现消息推送功能。 Progressive Web Apps(PWA) [代码]Progressive Web App[代码], 简称 PWA,是提升[代码]Web App[代码]的体验的一种新方法,能给用户原生应用的体验。 [代码]PWA[代码]能做到原生应用的体验不是靠特指某一项技术,而是经过应用一些新技术进行改进,在安全、性能和体验三个方面都有很大提升,[代码]PWA[代码]本质上是[代码]Web App[代码],借助一些新技术也具备了[代码]Native App[代码]的一些特性,兼具[代码]Web App[代码]和[代码]Native App[代码]的优点。 [代码]PWA[代码]的主要特点包括下面三点: 可靠 - 即使在不稳定的网络环境下,也能瞬间加载并展现 体验 - 快速响应,并且有平滑的动画响应用户的操作 粘性 - 像设备上的原生应用,具有沉浸式的用户体验,用户可以添加到桌面 [代码]PWA[代码]本身强调渐进式,并不要求一次性达到安全、性能和体验上的所有要求,开发者可以通过PWA Checklist查看现有的特征。 通过上面的[代码]PWA Checklist[代码],总结起来,[代码]PWA[代码]大致有以下的优势: 体验:通过[代码]Service Worker[代码]配合[代码]Cache Storage API[代码],保证了[代码]PWA[代码]首屏的加载效率,甚至配合本地存储可以支持离线应用; 粘性:[代码]PWA[代码]是可以安装的,用户点击安装到桌面后,会在桌面创建一个 PWA 应用,并且不需要从应用商店下载,可以借助[代码]Web App Manifest[代码]提供给用户和[代码]Native App[代码]一样的沉浸式体验,可以通过给用户发送离线通知,让用户回流; 渐进式:适用于大多数现代浏览器,随着浏览器版本的迭代,其功能是渐进增强的; 无版本问题:如[代码]Web[代码]应用的优势,更新版本只需要更新应用程序对应的静态文件即可,[代码]Service Worker[代码]会帮助我们进行更新; 跨平台:[代码]Windows[代码]、[代码]Mac OSX[代码]、[代码]Android[代码]、[代码]IOS[代码],一套代码,多处使用; 消息推送:即使用户已经关闭应用程序,仍然可以对用户进行消息推送; 总的说来,只要[代码]Web[代码]应用支持的功能,对于[代码]PWA[代码]而言,基本都支持,此外,还提供了原生能力。 使用[代码]PWA manifest[代码]添加桌面入口 注意这里说的[代码]manifest[代码]不是指的[代码]manifest[代码]缓存,这个[代码]manifest[代码]是一个[代码]JSON[代码]文件,开发者可以利用它控制在用户想要看到应用的区域(例如移动设备主屏幕)中如何向用户显示网络应用或网站,指示用户可以启动哪些功能,以及定义其在启动时的外观。 [代码]manifest[代码]提供了将网站书签保存到设备主屏幕的功能。当网站以这种方式启动时: 它具有唯一的图标和名称,以便用户将其与其他网站区分开来。 它会在下载资源或从缓存恢复资源时向用户显示某些信息。 它会向浏览器提供默认显示特性,以避免网站资源可用时的过渡过于生硬。 下面是我的博客网站的[代码]manifest.json[代码]文件,作为桌面入口配置文件的示例: [代码]{ "name": "Counterxing", "short_name": "Counterxing", "description": "Why did you encounter me?", "start_url": "/index.html", "display": "standalone", "orientation": "any", "background_color": "#ACE", "theme_color": "#ACE", "icons": [{ "src": "/images/logo/logo072.png", "sizes": "72x72", "type": "image/png" }, { "src": "/images/logo/logo152.png", "sizes": "152x152", "type": "image/png" }, { "src": "/images/logo/logo192.png", "sizes": "192x192", "type": "image/png" }, { "src": "/images/logo/logo256.png", "sizes": "256x256", "type": "image/png" }, { "src": "/images/logo/logo512.png", "sizes": "512x512", "type": "image/png" }] } [代码] 上面的字段含义也不用多解释了,大致就是启动的[代码]icon[代码]样式,应用名称、简写名称与描述等,其中必须确保有[代码]short_name[代码]和[代码]name[代码]。此外,最好设定好[代码]start_url[代码],表示启动的根页面路径,如果不添加,则是使用当前路径。 [代码]display[代码]为[代码]standalone[代码],则会隐藏浏览器的[代码]UI[代码]界面,如果设置[代码]display[代码]为[代码]browser[代码],则启动时保存浏览器的[代码]UI[代码]界面。 [代码]orientation[代码]表示启动时的方向,横屏、竖屏等,具体参数值可参考文档。 [代码]background_color[代码]和[代码]theme_color[代码]表示应用程序的背景颜色和主题颜色。 在创建好[代码]manifest.json[代码]后,将、使用[代码]link[代码]标签添加到应用程序的所有页面上,[代码]<link rel="manifest" href="/manifest.json">[代码] 安装到桌面 桌面端(以[代码]Mac OSX[代码]为例) 只有注册、激活了[代码]Service Worker[代码]的网站才能够安装到桌面,在[代码]Chrome 70[代码]版本之前,需要手动开启实验性功能,步骤如下: 进入[代码]chrome://flags[代码] 找到[代码]Desktop PWAs[代码],选择[代码]Enabled[代码] [图片] 此时,进入一个支持[代码]PWA[代码]的网站(例如Google I/O),在[代码]Chrome[代码]浏览器右上角,点击安装。即可安装到桌面。这里以我的博客为例: 可以到awesome-pwa查找目前支持[代码]PWA[代码]的网站列表 [图片] 接着点击安装: [图片] 这样,一个[代码]PWA[代码]应用就安装到你的机器上了,这里我的操作系统为[代码]Mac OSX[代码],应用程序可以通过[代码]Launchpad[代码]打开,在[代码]Windows[代码]也是同理的,会被安装到桌面上,可通过开始菜单找到应用程序。 [图片] 打开应用程序,发现其与原始应用几乎没有任何差距: [图片] Windows与上述方法类似,这里就不做过多阐述 [代码]Windows 10[代码]下安装[代码]PWA[代码]效果图: [图片] 移动端(以[代码]IOS[代码]为例) 由于当初苹果推出[代码]PWA[代码]时,并没有一个统一的[代码]manifest[代码]的规范,最开始的设计是通过[代码]meta[代码]和[代码]link[代码]标签来设定应用的对应参数的,所以,在移动端上的[代码]PWA[代码]应用,为了兼容[代码]Windows Phone[代码]和[代码]iPhone[代码],需要在所有页面的[代码]HTML[代码]的[代码]head[代码]中添加以下的标签: [代码]<meta name="msapplication-TileImage" content="./images/logo/logo152.png"> <meta name="msapplication-TileColor" content="#2F3BA2"> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-mobile-web-app-title" content="Counterxing"> <link rel="apple-touch-icon" href="./images/logo/logo152.png"> [代码] 添加好后,就可以体验我们的[代码]PWA[代码]了! [代码]IOS[代码]在[代码]11.3[代码]版本之后也支持[代码]PWA[代码]了,知道这一消息的我,卸载了手机上很多软件,立刻体验上了[代码]PWA[代码]。 这里以豆瓣移动端为例使用[代码]Safiri[代码]浏览器打开一个网站,点击下方分享图标,选择添加到主屏幕。 [图片] [图片] 然后在新弹出的一个浏览器页面,选择添加: [图片] 就以上简短的步骤,移动端上的一个[代码]PWA[代码]桌面应用就添加好了,赶紧体验吧! 小结 本文是笔者写的[代码]Service Worker[代码]学习与实践系列文章的第二篇,主要讲述的是配合[代码]Service Worker[代码]使用的[代码]PWA[代码]的优势,如何配置[代码]manifest.json[代码]文件来实现将[代码]PWA[代码]安装到桌面,并通过[代码]Mac OSX[代码]和[代码]IOS[代码]如何安装[代码]PWA[代码]到桌面的详细步骤,阐述了如何配置[代码]PWA[代码],使其方便地安装到桌面上。 下一篇文章中,主要讲述[代码]Service Worker[代码]在[代码]PWA[代码]实践中的重要能力:[代码]Web Push[代码]。
2019-04-10 - Service Worker学习与实践(一)——离线缓存
什么是[代码]Service Worker[代码] [代码]Service Worker[代码]本质上充当Web应用程序与浏览器之间的代理服务器,也可以在网络可用时作为浏览器和网络间的代理。它们旨在(除其他之外)使得能够创建有效的离线体验,拦截网络请求并基于网络是否可用以及更新的资源是否驻留在服务器上来采取适当的动作。他们还允许访问推送通知和后台同步[代码]API[代码]。 [代码]Service Worker[代码]的本质是一个[代码]Web Worker[代码],它独立于[代码]JavaScript[代码]主线程,因此它不能直接访问[代码]DOM[代码],也不能直接访问[代码]window[代码]对象,但是,[代码]Service Worker[代码]可以访问[代码]navigator[代码]对象,也可以通过消息传递的方式(postMessage)与[代码]JavaScript[代码]主线程进行通信。 [代码]Service Worker[代码]是一个网络代理,它可以控制[代码]Web[代码]页面的所有网络请求。 [代码]Service Worker[代码]具有自身的生命周期,使用好[代码]Service Worker[代码]的关键是灵活控制其生命周期。 [代码]Service Worker[代码]的作用 用于浏览器缓存 实现离线[代码]Web APP[代码] 消息推送 [代码]Service Worker[代码]兼容性 [代码]Service Worker[代码]是现代浏览器的一个高级特性,它依赖于[代码]fetch API[代码]、[代码]Cache Storage[代码]、[代码]Promise[代码]等,其中,[代码]Cache[代码]提供了[代码]Request / Response[代码]对象对的存储机制,[代码]Cache Storage[代码]存储多个[代码]Cache[代码]。 [图片] 示例 在了解[代码]Service Worker[代码]的原理之前,先来看一段[代码]Service Worker[代码]的示例: [代码]self.importScripts('./serviceworker-cache-polyfill.js'); var urlsToCache = [ '/', '/index.js', '/style.css', '/favicon.ico', ]; var CACHE_NAME = 'counterxing'; self.addEventListener('install', function(event) { self.skipWaiting(); event.waitUntil( caches.open(CACHE_NAME) .then(function(cache) { return cache.addAll(urlsToCache); }) ); }); self.addEventListener('fetch', function(event) { event.respondWith( caches.match(event.request) .then(function(response) { if (response) { return response; } return fetch(event.request); }) ); }); self.addEventListener('activate', function(event) { var cacheWhitelist = ['counterxing']; event.waitUntil( caches.keys().then(function(cacheNames) { return Promise.all( cacheNames.map(function(cacheName) { if (cacheWhitelist.indexOf(cacheName) === -1) { return caches.delete(cacheName); } }) ); }) ); }); [代码] 下面开始逐段逐段地分析,揭开[代码]Service Worker[代码]的神秘面纱: [代码]polyfill[代码] 首先看第一行:[代码]self.importScripts('./serviceworker-cache-polyfill.js');[代码],这里引入了Cache API的一个polyfill,这个[代码]polyfill[代码]支持使得在较低版本的浏览器下也可以使用[代码]Cache Storage API[代码]。想要实现[代码]Service Worker[代码]的功能,一般都需要搭配[代码]Cache API[代码]代理网络请求到缓存中。 在[代码]Service Worker[代码]线程中,使用[代码]importScripts[代码]引入[代码]polyfill[代码]脚本,目的是对低版本浏览器的兼容。 [代码]Cache Resources List[代码] And [代码]Cache Name[代码] 之后,使用一个[代码]urlsToCache[代码]列表来声明需要缓存的静态资源,再使用一个变量[代码]CACHE_NAME[代码]来确定当前缓存的[代码]Cache Storage Name[代码],这里可以理解成[代码]Cache Storage[代码]是一个[代码]DB[代码],而[代码]CACHE_NAME[代码]则是[代码]DB[代码]名: [代码]var urlsToCache = [ '/', '/index.js', '/style.css', '/favicon.ico', ]; var CACHE_NAME = 'counterxing'; [代码] [代码]Lifecycle[代码] [代码]Service Worker[代码]独立于浏览器[代码]JavaScript[代码]主线程,有它自己独立的生命周期。 如果需要在网站上安装[代码]Service Worker[代码],则需要在[代码]JavaScript[代码]主线程中使用以下代码引入[代码]Service Worker[代码]。 [代码]if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js').then(function(registration) { console.log('成功安装', registration.scope); }).catch(function(err) { console.log(err); }); } [代码] 此处,一定要注意[代码]sw.js[代码]文件的路径,在我的示例中,处于当前域根目录下,这意味着,[代码]Service Worker[代码]和网站是同源的,可以为当前网站的所有请求做代理,如果[代码]Service Worker[代码]被注册到[代码]/imaging/sw.js[代码]下,那只能代理[代码]/imaging[代码]下的网络请求。 可以使用[代码]Chrome[代码]控制台,查看当前页面的[代码]Service Worker[代码]情况: [图片] 安装完成后,[代码]Service Worker[代码]会经历以下生命周期: 下载([代码]download[代码]) 安装([代码]install[代码]) 激活([代码]activate[代码]) 用户首次访问[代码]Service Worker[代码]控制的网站或页面时,[代码]Service Worker[代码]会立刻被下载。之后至少每[代码]24[代码]小时它会被下载一次。它可能被更频繁地下载,不过每[代码]24[代码]小时一定会被下载一次,以避免不良脚本长时间生效。 在下载完成后,开始安装[代码]Service Worker[代码],在安装阶段,通常需要缓存一些我们预先声明的静态资源,在我们的示例中,通过[代码]urlsToCache[代码]预先声明。 在安装完成后,会开始进行激活,浏览器会尝试下载[代码]Service Worker[代码]脚本文件,下载成功后,会与前一次已缓存的[代码]Service Worker[代码]脚本文件做对比,如果与前一次的[代码]Service Worker[代码]脚本文件不同,证明[代码]Service Worker[代码]已经更新,会触发[代码]activate[代码]事件。完成激活。 如图所示,为[代码]Service Worker[代码]大致的生命周期: [图片] [代码]install[代码] 在安装完成后,尝试缓存一些静态资源: [代码]self.addEventListener('install', function(event) { self.skipWaiting(); event.waitUntil( caches.open(CACHE_NAME) .then(function(cache) { return cache.addAll(urlsToCache); }) ); }); [代码] 首先,[代码]self.skipWaiting()[代码]执行,告知浏览器直接跳过等待阶段,淘汰过期的[代码]sw.js[代码]的[代码]Service Worker[代码]脚本,直接开始尝试激活新的[代码]Service Worker[代码]。 然后使用[代码]caches.open[代码]打开一个[代码]Cache[代码],打开后,通过[代码]cache.addAll[代码]尝试缓存我们预先声明的静态文件。 监听[代码]fetch[代码],代理网络请求 页面的所有网络请求,都会通过[代码]Service Worker[代码]的[代码]fetch[代码]事件触发,[代码]Service Worker[代码]通过[代码]caches.match[代码]尝试从[代码]Cache[代码]中查找缓存,缓存如果命中,则直接返回缓存中的[代码]response[代码],否则,创建一个真实的网络请求。 [代码]self.addEventListener('fetch', function(event) { event.respondWith( caches.match(event.request) .then(function(response) { if (response) { return response; } return fetch(event.request); }) ); }); [代码] 如果我们需要在请求过程中,再向[代码]Cache Storage[代码]中添加新的缓存,可以通过[代码]cache.put[代码]方法添加,看以下例子: [代码]self.addEventListener('fetch', function(event) { event.respondWith( caches.match(event.request) .then(function(response) { // 缓存命中 if (response) { return response; } // 注意,这里必须使用clone方法克隆这个请求 // 原因是response是一个Stream,为了让浏览器跟缓存都使用这个response // 必须克隆这个response,一份到浏览器,一份到缓存中缓存。 // 只能被消费一次,想要再次消费,必须clone一次 var fetchRequest = event.request.clone(); return fetch(fetchRequest).then( function(response) { // 必须是有效请求,必须是同源响应,第三方的请求,因为不可控,最好不要缓存 if (!response || response.status !== 200 || response.type !== 'basic') { return response; } // 消费过一次,又需要再克隆一次 var responseToCache = response.clone(); caches.open(CACHE_NAME) .then(function(cache) { cache.put(event.request, responseToCache); }); return response; } ); }) ); }); [代码] 在项目中,一定要注意控制缓存,接口请求一般是不推荐缓存的。所以在我自己的项目中,并没有在这里做动态的缓存方案。 [代码]activate[代码] [代码]Service Worker[代码]总有需要更新的一天,随着版本迭代,某一天,我们需要把新版本的功能发布上线,此时需要淘汰掉旧的缓存,旧的[代码]Service Worker[代码]和[代码]Cache Storage[代码]如何淘汰呢? [代码]self.addEventListener('activate', function(event) { var cacheWhitelist = ['counterxing']; event.waitUntil( caches.keys().then(function(cacheNames) { return Promise.all( cacheNames.map(function(cacheName) { if (cacheWhitelist.indexOf(cacheName) === -1) { return caches.delete(cacheName); } }) ); }) ); }); [代码] 首先有一个白名单,白名单中的[代码]Cache[代码]是不被淘汰的。 之后通过[代码]caches.keys()[代码]拿到所有的[代码]Cache Storage[代码],把不在白名单中的[代码]Cache[代码]淘汰。 淘汰使用[代码]caches.delete()[代码]方法。它接收[代码]cacheName[代码]作为参数,删除该[代码]cacheName[代码]所有缓存。 sw-precache-webpack-plugin sw-precache-webpack-plugin是一个[代码]webpack plugin[代码],可以通过配置的方式在[代码]webpack[代码]打包时生成我们想要的[代码]sw.js[代码]的[代码]Service Worker[代码]脚本。 一个最简单的配置如下: [代码]var path = require('path'); var SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin'); const PUBLIC_PATH = 'https://www.my-project-name.com/'; // webpack needs the trailing slash for output.publicPath module.exports = { entry: { main: path.resolve(__dirname, 'src/index'), }, output: { path: path.resolve(__dirname, 'src/bundles/'), filename: '[name]-[hash].js', publicPath: PUBLIC_PATH, }, plugins: [ new SWPrecacheWebpackPlugin( { cacheId: 'my-project-name', dontCacheBustUrlsMatching: /\.\w{8}\./, filename: 'service-worker.js', minify: true, navigateFallback: PUBLIC_PATH + 'index.html', staticFileGlobsIgnorePatterns: [/\.map$/, /asset-manifest\.json$/], } ), ], } [代码] 在执行[代码]webpack[代码]打包后,会生成一个名为[代码]service-worker.js[代码]文件,用于缓存[代码]webpack[代码]打包后的静态文件。 一个最简单的示例。 [代码]Service Worker Cache[代码] VS [代码]Http Cache[代码] 对比起[代码]Http Header[代码]缓存,[代码]Service Worker[代码]配合[代码]Cache Storage[代码]也有自己的优势: 缓存与更新并存:每次更新版本,借助[代码]Service Worker[代码]可以立马使用缓存返回,但与此同时可以发起请求,校验是否有新版本更新。 无侵入式:[代码]hash[代码]值实在是太难看了。 不易被冲掉:[代码]Http[代码]缓存容易被冲掉,也容易过期,而[代码]Cache Storage[代码]则不容易被冲掉。也没有过期时间的说法。 离线:借助[代码]Service Worker[代码]可以实现离线访问应用。 但是缺点是,由于[代码]Service Worker[代码]依赖于[代码]fetch API[代码]、依赖于[代码]Promise[代码]、[代码]Cache Storage[代码]等,兼容性不太好。 后话 本文只是简单总结了[代码]Service Worker[代码]的基本使用和使用[代码]Service Worker[代码]做客户端缓存的简单方式,然而,[代码]Service Worker[代码]的作用远不止于此,例如:借助[代码]Service Worker[代码]做离线应用、用于做网络应用的推送(可参考push-notifications)等。 甚至可以借助[代码]Service Worker[代码],对接口进行缓存,在我所在的项目中,其实并不会做的这么复杂。不过做接口缓存的好处是支持离线访问,对离线状态下也能正常访问我们的[代码]Web[代码]应用。 [代码]Cache Storage[代码]和[代码]Service Worker[代码]总是分不开的。[代码]Service Worker[代码]的最佳用法其实就是配合[代码]Cache Storage[代码]做离线缓存。借助于[代码]Service Worker[代码],可以轻松实现对网络请求的控制,对于不同的网络请求,采取不同的策略。例如对于[代码]Cache[代码]的策略,其实也是存在多种情况。例如可以优先使用网络请求,在网络请求失败时再使用缓存、亦可以同时使用缓存和网络请求,一方面检查请求,一方面有检查缓存,然后看两个谁快,就用谁。 优化方向:目前我所负责的DICOM项目,虽然还没有用上[代码]Service Worker[代码],但前面经过不断地优化迭代,通过从增加http层的缓存、无损压缩图像的替换、有损压缩图像的渐进加载、更换DICOM解压缩策略、使用indexed DB缓存CT图像、首屏可见速度已经从20多秒降低到5秒左右,内存占用从700M以上降低到250M左右。后期还会一直深挖这一块。主要方向之一就是service worker的替换,全站缓存静态资源。此外,高优先级的则是DICOM无损图像解压算法的最优选择与优化、cornerstone的jpg图像展示。 项目优化还在继续,力求极致性能和用户体验~
2019-04-11 - 借助云开发实现小程序列表页(包含json数据的请求和解析)
视频讲解地址:https://edu.csdn.net/course/detail/9604 我们之前是请求服务器数据,然后把服务器数据显示到我们的小程序上,比如列表数据和详情页数据。但是我们如果不会后台开发,没有自己的服务器,怎么定义自己的列表和详情页数据,并且能通过小程序拿到显示到列表页和详情页呢。今天就来教大家摆脱后台,用小程序云开发实现自己的数据后台。并且请求到数据展示到列表页和详情页。 老规矩,先看效果图: [图片] 本节知识点: 1,借助云开发实现自己的小程序数据后台 2,请求列表数据并解析展示到列表页 3,请求详情页数据并解析到详情页 这里涉及到的基础的知识,大家可以去我博客看相关的教程。 小程序云开发入门—云数据库数据源的导入与导出 借助小程序云开发获取小程序用户openid 借助小程序云开发实现小程序的登陆注册功能 这里就默认你已经创建好自己的小程序云开发后台。 我们根据小程序云开发入门—云数据库数据源的导入与导出,把下面数据导入到云开发数据库。 [代码]{"_id":"XLHdu-SiwXKAQth1","xuexiao":"兰州交通大学","didian":"第七教学楼1101教室","shijian":"2019年4月25日下午3点半","gongsi":"中铁七局安装工程有限公司","danweidizhi":"北京市东城区美术馆后街999号","zhuanye":"土木工程类;建筑类;水电类","daiyu":"薪资3000-6000元,五险一金,年终奖,免费体检,出国旅游","lianxiren":"王先生:15611823566"} {"_id":"XLHdu-SiwXKAQth2","xuexiao":"兰州交通大学","didian":"第七教学楼1801教室","shijian":"2019年4月25日下午3点半","gongsi":"中建七局安装工程有限公司","danweidizhi":"北京市东城区美术馆后街666号","zhuanye":"土木工程类;建筑类;水电类","daiyu":"薪资3000-6000元,五险一金,年终奖,免费体检,出国旅游","lianxiren":"王先生:15611823566"} {"_id":"XLHdu-SiwXKAQthe","xuexiao":"兰州交通大学","didian":"校本部 第五教学楼113教室","shijian":"2019-04-15 14:30-16:00","gongsi":"中国能源建设集团北京电力建设有限公司 ","danweidizhi":"中国能源建设集团北京电力建设有限公司 ","zhuanye":"土木工程类;建筑类;水电类","daiyu":" 一、公司简介 北京电力建设有限公司始建于1964年,现隶属于中国能源建设集团有限公司,由中国葛洲坝集团股份有限公司整体托管,是建筑、安装施工综合性国家一级全民所有制企业,是国内最早通过质量体系、职业健康安全与环境管理体系认证的单位之一,是国家认定的500强建筑企业之一。公司建立了以“重品德、重能力、重业绩、重贡献”为导向的人才成长机制,鼓励立足岗位,充分发挥积极性、主动性和创造性,在岗位上建功、成长,立志打造一支作风正、素质高、能力强、技术精的一流队伍。 公司业务涉足火电、电网、风电、光伏、环保、调试、电力检修、工民建、市政、燃气、煤化工等领域,足迹遍布全国22个省及印度尼西亚。先后80余次荣获包括中国建设工程质量最高奖—“鲁班奖”在内的国家级和省部级以上奖项,拥有实用新型专利4项、行业级工法4项。公司正以管理先进、生机勃勃的现代化新型企业的崭新面貌展现在世人面前。 公司经营项目包括:承包境外电力、房屋建筑、市政公用、钢结构、高耸构筑物、环保工程和境内国际招标工程;施工总承包;专业承包;租赁机械设备;货物进出口、技术进出口、代理进出口等。 公司正在向市场型、管理型、产融结合型的产业结构转型升级 。大平台的企业,高起点的岗位,加速度的晋升通道助力优秀人才放飞梦想。 详细内容请查看北京电力建设有限公司网址(http://www.bpcc.ceec.net.cn)。 二、需求信息 序号 \u0009 需求专业 \u0009 本科 \u0009 硕士 1 \u0009 电气工程及其自动化 \u0009 11 \u0009 2 \u0009 自动化 \u0009 10 \u0009 3 \u0009 土木工程 \u0009 10 \u0009 4 \u0009 道路桥梁与渡河工程 \u0009 4 \u0009 5 \u0009 给排水科学与工程 \u0009 4 \u0009 6 \u0009 测绘工程 \u0009 3 \u0009 7 \u0009 工程管理 \u0009 3 \u0009 8 \u0009 工程造价 \u0009 4 \u0009 9 \u0009 金融学 \u0009 1 \u0009 1 10 \u0009 哲学类 \u0009 1 \u0009 1 11 \u0009 能源与动力工程 \u0009 6 \u0009 12 \u0009 新能源科学与工程 \u0009 6 \u0009 13 \u0009 机械设计制造及其自动化 \u0009 3 \u0009 14 \u0009 审计学 \u0009 2 \u0009 15 \u0009 会计学 \u0009 3 \u0009 16 \u0009 财务管理 \u0009 3 \u0009 17 \u0009 国际经济与贸易 \u0009 3 \u0009 18 \u0009 法学/社会学 \u0009 2 \u0009 1 19 \u0009 人力资源管理 \u0009 2 \u0009 20 \u0009 工商管理 \u0009 2 \u0009 22 \u0009 安全工程 \u0009 2 \u0009 23 \u0009 焊接技术与工程 \u0009 2 \u0009 24 \u0009 金属材料工程 \u0009 4 \u0009 25 \u0009 汉语言文学/历史学 \u0009 3 \u0009 1 26 \u0009 物理学 \u0009\u0009 1 27 \u0009 软件工程/数学与应用数学 \u0009\u0009 1 \u0009 合计 \u0009 94 \u0009 6 三、招聘条件: 1、应聘人员需为国家统招计划内全日制公办院校2019年大学本科应届毕业生(二本及以上) 2、英语取得国家四级及以上合格证书或考试成绩在425分及以上,身体健康,符合国家公务员录用体检标准,能长期在项目施工现场工作(法学,社会学除外)。 四、招聘流程 (一)应聘报名方式一:请将个人简历发送至bjdjrzb@bepc.com.cn的邮箱。邮件主题为:专业 姓名 学历 学校。个人自荐材料(个人简历)的文件名格式:“专业 姓名 学历 学校”。个人自荐材料(即个人简历)需附加盖学校公章的就业推荐表、计算机等级证书、外语等级证书、身份证、获奖证书等扫描件。 应聘报名方式二: (二)资格审查:公司经初审后,将通过手机短信、电话等方式通知初审通过的应聘者参加面试、测试事宜。请确保联系方式填写准确,通信畅通。资格初审未通过的毕业生公司恕不另行通知。 (三)应聘者参加面试时,须携带个人自荐材料(即个人简历),附加盖学校公章的就业推荐表、计算机等级证书、外语等级证书、身份证、获奖证书等的原件和复印件。 五、联系方式 陈老师:(010)58425874/13522903460、 王老师:(010)58425979/18686136372 通讯地址:北京市朝阳区定福庄西里一号 邮政编码:100024 ","lianxiren":"王先生:15611823566"} [代码] 上面的数据是我们要导入到云开发数据库的列表数据,就是我们小程序列表页要显示的就是这些数据。所以这里大家一定要认真对待,试想,你数据都没有导入成功,哪来的后面的数据的获取和解析呢。 如果你不会导入数据到云开发数据库,那只能自己在云开发控制台自己一条条的创建了。 [图片] 就像这样,点添加记录----> 输入字段 ---->创建数据 ----> 确定保存 数据少了还好,如果数据特别多,你自己就会累到吐。 如果你不会批量导入数据,或者没有数据可以批量导入 就到这个地址https://edu.csdn.net/course/detail/9604 看在线讲解视频学着怎么批量导入。 获取列表数据并解析到列表页 到这一步,就默认你上面的批量导入数据已经成功了。 那么我们就在index.js做数据请求操作。 请求云开发数据库里列表数据的核心代码如下 [代码] //从云开发数据库里列表 getList() { let that = this; wx.cloud.callFunction({ // 要调用的云函数名称 name: 'getHome', success: res => { wx.stopPullDownRefresh(); //停止刷新 // console.log(res) if (res.result) { let dataList = res.result.data; console.log({ dataList }) if (dataList === undefined || dataList.length == 0) { wx.showToast({ title: '没有数据', }) } else { that.setData({ //获取数据成功后的数据绑定 isShowArticle: true, dataList: dataList, }); } } else { wx.showToast({ title: '没有数据', }) } }, fail: err => { wx.stopPullDownRefresh(); //停止刷新 wx.showToast({ title: '没有数据', }) } }) }, [代码] 给大家讲解下,我们这里是借助里云开发提供的云函数功能来获取列表数据。为什么要这么做呢,因为小程序云开发规定,我们拿数据库里的资源,只能拿到自己的保存的数据,批量导入的数据是管理员导入的数据,不算你导入的。 这里问题就来了,我们一个列表数据比如有10条,其中只有5条是我们创建的,那么怎么拿到别人的5条呢。这时候就要借助云函数了。 我们使用云函数拿到所有的列表数据,我们云函数的位置如下 [图片] getHome是我们定义的用来获取数据列表的云函数。 云函数的创建和上传,我视频讲解里有完整的流程,大家如果文章看不懂可以去看视频讲解:https://edu.csdn.net/course/detail/9604 [图片] 然后我们再解析数据,并显示到列表页 我们index.wxml代码如下 [代码]<!--index.wxml--> <!-- 文章列表 --> <block wx:for="{{dataList}}" wx:key="item"> <view class='item-container' bindtap='goDetail' data-item='{{item}}'> <view class=' item_root '> <text class='item-gongsi'>{{item.gongsi}}</text> </view> <view class='item_root'> <text class='item-desc'>{{item.xuexiao}}</text> <text class='item-desc didian'>{{item.didian}}</text> </view> <view class='item_root'> <text class='item-desc'>{{item.shijian}}</text> </view> </view> </block> [代码] index.js完整代码如下 [代码]//index.js const app = getApp() Page({ //页面的初始数据 data: { isShowArticle: false, dataList: null }, onShow: function() { this.getList(); }, //页面相关事件处理函数--监听用户下拉动作 onPullDownRefresh: function() { this.getList(); }, //从云开发数据库里列表 getList() { let that = this; wx.cloud.callFunction({ // 要调用的云函数名称 name: 'getHome', success: res => { wx.stopPullDownRefresh(); //停止刷新 // console.log(res) if (res.result) { let dataList = res.result.data; console.log({ dataList }) if (dataList === undefined || dataList.length == 0) { wx.showToast({ title: '没有数据', }) } else { that.setData({ //获取数据成功后的数据绑定 isShowArticle: true, dataList: dataList, }); } } else { wx.showToast({ title: '没有数据', }) } }, fail: err => { wx.stopPullDownRefresh(); //停止刷新 wx.showToast({ title: '没有数据', }) } }) }, //去详情页 goDetail: function(event) { var item = event.currentTarget.dataset.item; console.log(item); wx.navigateTo({ url: '../askdetail/askdetail?id=' + item._id+"&gongsi="+item.gongsi, }) } }) [代码] 这样我们就可以轻松的借助云开发实现列表页数据的定义,列表页数据的获取,列表页数据的解析了。 如果文章你感觉不太直观,可以去看视频讲解:https://edu.csdn.net/course/detail/9604 完整的源码在视频讲解里也可以下载 如果有关于小程序的问题,可以加我微信2501902696(备注小程序)
2019-04-15 - 一个通用request的封装
小程序内置了[代码]wx.request[代码],用于向后端发送请求,我们先来看看它的文档: wx.request(OBJECT) 发起网络请求。使用前请先阅读说明。 OBJECT参数说明: 参数名 类型 必填 默认值 说明 最低版本 url String 是 - 开发者服务器接口地址 - data Object/String/ArrayBuffer 否 - 请求的参数 - header Object 否 - 设置请求的 header,header 中不能设置 Referer。 - method String 否 GET (需大写)有效值:OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT - dataType String 否 json 如果设为json,会尝试对返回的数据做一次 JSON.parse - responseType String 否 text 设置响应的数据类型。合法值:text、arraybuffer 1.7.0 success Function 否 - 收到开发者服务成功返回的回调函数 - fail Function 否 - 接口调用失败的回调函数 - complete Function 否 - 接口调用结束的回调函数(调用成功、失败都会执行) - success返回参数说明: 参数 类型 说明 最低版本 data Object/String/ArrayBuffer 开发者服务器返回的数据 - statusCode Number 开发者服务器返回的 HTTP 状态码 - header Object 开发者服务器返回的 HTTP Response Header 1.2.0 这里我们主要看两点: 回调函数:success、fail、complete; success的返回参数:data、statusCode、header。 相对于通过入参传回调函数的方式,我更喜欢promise的链式,这是我期望的第一个点;success的返回参数,在实际开发过程中,我只关心data部分,这里可以做一下处理,这是第二点。 promisify 小程序默认支持promise,所以这一点改造还是很简单的: [代码]/** * promise请求 * 参数:参考wx.request * 返回值:[promise]res */ function requestP(options = {}) { const { success, fail, } = options; return new Promise((res, rej) => { wx.request(Object.assign( {}, options, { success: res, fail: rej, }, )); }); } [代码] 这样一来我们就可以使用这个函数来代替wx.request,并且愉快地使用promise链式: [代码]requestP({ url: '/api', data: { name: 'Jack' } }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); [代码] 注意,小程序的promise并没有实现finally,Promise.prototype.finally是undefined,所以complete不能用finally代替。 精简返回值 精简返回值也是很简单的事情,第一直觉是,当请求返回并丢给我一大堆数据时,我直接resolve我要的那一部分数据就好了嘛: [代码]return new Promise((res, rej) => { wx.request(Object.assign( {}, options, { success(r) { res(r.data); // 这里只取data }, fail: rej, }, )); }); [代码] but!这里需要注意,我们仅仅取data部分,这时候默认所有success都是成功的,其实不然,wx.request是一个基础的api,fail只发生在系统和网络层面的失败情况,比如网络丢包、域名解析失败等等,而类似404、500之类的接口状态,依旧是调用success,并体现在[代码]statusCode[代码]上。 从业务上讲,我只想处理json的内容,并对json当中的相关状态进行处理;如果一个接口返回的不是约定好的json,而是类似404、500之类的接口异常,我统一当成接口/网络错误来处理,就像jquery的ajax那样。 也就是说,如果我不对[代码]statusCode[代码]进行区分,那么包括404、500在内的所有请求结果都会走[代码]requestP().then[代码],而不是[代码]requestP().catch[代码]。这显然不是我们熟悉的使用方式。 于是我从jquery的ajax那里抄来了一段代码。。。 [代码]/** * 判断请求状态是否成功 * 参数:http状态码 * 返回值:[Boolen] */ function isHttpSuccess(status) { return status >= 200 && status < 300 || status === 304; } [代码] [代码]isHttpSuccess[代码]用来决定一个http状态码是否判为成功,于是结合[代码]requestP[代码],我们可以这么来用: [代码]return new Promise((res, rej) => { wx.request(Object.assign( {}, options, { success(r) { const isSuccess = isHttpSuccess(r.statusCode); if (isSuccess) { // 成功的请求状态 res(r.data); } else { rej({ msg: `网络错误:${r.statusCode}`, detail: r }); } }, fail: rej, }, )); }); [代码] 这样我们就可以直接resolve返回结果中的data,而对于非成功的http状态码,我们则直接reject一个自定义的error对象,这样就是我们所熟悉的ajax用法了。 登录 我们经常需要识别发起请求的当前用户,在web中这通常是通过请求中携带的cookie实现的,而且对于前端开发者是无感知的;小程序中没有cookie,所以需要主动地去补充相关信息。 首先要做的是:登录。 通过[代码]wx.login[代码]接口我们可以得到一个[代码]code[代码],调用后端登录接口将code传给后端,后端再用code去调用微信的登录接口,换取[代码]sessionKey[代码],最后生成一个[代码]sessionId[代码]返回给前端,这就完成了登录。 [图片] 具体参考微信官方文档:wx.login [代码]const apiUrl = 'https://jack-lo.github.io'; let sessionId = ''; /** * 登录 * 参数:undefined * 返回值:[promise]res */ function login() { return new Promise((res, rej) => { // 微信登录 wx.login({ success(r1) { if (r1.code) { // 获取sessionId requestP({ url: `${apiUrl}/api/login`, data: { code: r1.code, }, method: 'POST' }) .then((r2) => { if (r2.rcode === 0) { const { sessionId } = r2.data; // 保存sessionId sessionId = sessionId; res(r2); } else { rej({ msg: '获取sessionId失败', detail: r2 }); } }) .catch((err) => { rej(err); }); } else { rej({ msg: '获取code失败', detail: r1 }); } }, fail: rej, }); }); } [代码] 好的,我们做好了登录并且顺利获取到了sessionId,接下来是考虑怎么把sessionId通过请求带上去。 sessionId 为了将状态与数据区分开来,我们决定不通过data,而是通过header的方式来携带sessionId,我们对原本的requestP稍稍进行修改,使得它每次调用都自动在header里携带sessionId: [代码]function requestP(options = {}) { const { success, fail, } = options; // 统一注入约定的header let header = Object.assign({ sessionId: sessionId }, options.header); return new Promise((res, rej) => { ... }); } [代码] 好的,现在请求会自动带上sessionId了; 但是,革命尚未完成: 我们什么时候去登录呢?或者说,我们什么时候去获取sessionId? 假如还没登录就发起请求了怎么办呢? 登录过期了怎么办呢? 我设想有这样一个逻辑: 当我发起一个请求的时候,如果这个请求不需要sessionId,则直接发出; 如果这个请求需要携带sessionId,就去检查现在是否有sessionId,有的话直接携带,发起请求; 如果没有,自动去走登录的流程,登录成功,拿到sessionId,再去发送这个请求; 如果有,但是最后请求返回结果是sessionId过期了,那么程序自动走登录的流程,然后再发起一遍。 其实上面的那么多逻辑,中心思想只有一个:都是为了拿到sessionId! 我们需要对请求做一层更高级的封装。 首先我们需要一个函数专门去获取sessionId,它将解决上面提到的2、3点: [代码]/** * 获取sessionId * 参数:undefined * 返回值:[promise]sessionId */ function getSessionId() { return new Promise((res, rej) => { // 本地sessionId缺失,重新登录 if (!sessionId) { login() .then((r1) => { res(r1.data.sessionId); }) .catch(rej); } } else { res(sessionId); } }); } [代码] 好的,接下来我们解决第1、4点,我们先假定:sessionId过期的时候,接口会返回[代码]code=401[代码]。 整合了getSessionId,得到一个更高级的request方法: [代码]/** * ajax高级封装 * 参数:[Object]option = {},参考wx.request; * [Boolen]keepLogin = false * 返回值:[promise]res */ function request(options = {}, keepLogin = true) { if (keepLogin) { return new Promise((res, rej) => { getSessionId() .then((r1) => { // 获取sessionId成功之后,发起请求 requestP(options) .then((r2) => { if (r2.rcode === 401) { // 登录状态无效,则重新走一遍登录流程 // 销毁本地已失效的sessionId sessionId = ''; getSessionId() .then((r3) => { requestP(options) .then(res) .catch(rej); }); } else { res(r2); } }) .catch(rej); }) .catch(rej); }); } else { // 不需要sessionId,直接发起请求 return requestP(options); } } [代码] 留意req的第二参数keepLogin,是为了适配有些接口不需要sessionId,但因为我的业务里大部分接口都需要登录状态,所以我默认值为true。 这差不多就是我们封装request的最终形态了。 并发处理 这里其实我们还需要考虑一个问题,那就是并发。 试想一下,当我们的小程序刚打开的时候,假设页面会同时发出5个请求,而此时没有sessionId,那么,这5个请求按照上面的逻辑,都会先去调用login去登录,于是乎,我们就会发现,登录接口被同步调用了5次!并且后面的调用将导致前面的登录返回的sessionId过期~ 这bug是很严重的,理论上来说,登录我们只需要调用一次,然后一直到过期为止,我们都不需要再去登录一遍了。 ——那么也就是说,同一时间里的所有接口其实只需要登录一次就可以了。 ——也就是说,当有登录的请求发出的时候,其他那些也需要登录状态的接口,不需要再去走登录的流程,而是等待这次登录回来即可,他们共享一次登录操作就可以了! 解决这个问题,我们需要用到队列。 我们修改一下getSessionId这里的逻辑: [代码]const loginQueue = []; let isLoginning = false; /** * 获取sessionId * 参数:undefined * 返回值:[promise]sessionId */ function getSessionId() { return new Promise((res, rej) => { // 本地sessionId缺失,重新登录 if (!sessionId) { loginQueue.push({ res, rej }); if (!isLoginning) { isLoginning = true; login() .then((r1) => { isLoginning = false; while (loginQueue.length) { loginQueue.shift().res(r1); } }) .catch((err) => { isLoginning = false; while (loginQueue.length) { loginQueue.shift().rej(err); } }); } } else { res(sessionId); } }); } [代码] 使用了isLoginning这个变量来充当锁的角色,锁的目的就是当登录正在进行中的时候,告诉程序“我已经在登录了,你先把回调都加队列里去吧”,当登录结束之后,回来将锁解开,把回调全部执行并清空队列。 这样我们就解决了问题,同时提高了性能。 封装 在做完以上工作以后,我们都很清楚的封装结果就是[代码]request[代码],所以我们把request暴露出去就好了: [代码]function request() { ... } module.exports = request; [代码] 这般如此之后,我们使用起来就可以这样子: [代码]const request = require('request.js'); Page({ ready() { // 获取热门列表 request({ url: 'https://jack-lo.github.io/api/hotList', data: { page: 1 } }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); // 获取最新信息 request({ url: 'https://jack-lo.github.io/api/latestList', data: { page: 1 } }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); }, }); [代码] 是不是很方便,可以用promise的方式,又不必关心登录的问题。 然而可达鸭眉头一皱,发现事情并不简单,一个接口有可能在多个地方被多次调用,每次我们都去手写这么一串[代码]url[代码]参数,并不那么方便,有时候还不好找,并且容易出错。 如果能有个地方专门记录这些url就好了;如果每次调用接口,都能像调用一个函数那么简单就好了。 基于这个想法,我们还可以再做一层封装,我们可以把所有的后端接口,都封装成一个方法,调用接口就相对应调用这个方法: [代码]const apiUrl = 'https://jack-lo.github.io'; const req = { // 获取热门列表 getHotList(data) { const url = `${apiUrl}/api/hotList` return request({ url, data }); }, // 获取最新列表 getLatestList(data) { const url = `${apiUrl}/api/latestList` return request({ url, data }); } } module.exports = req; // 注意这里暴露的已经不是request,而是req [代码] 那么我们的调用方式就变成了: [代码]const req = require('request.js'); Page({ ready() { // 获取热门列表 req.getHotList({ page: 1 }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); // 获取最新信息 req.getLatestList({ page: 1 }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); } }); [代码] 这样一来就方便了很多,而且有一个很大的好处,那就是当某个接口的地址需要统一修改的时候,我们只需要对[代码]request.js[代码]进行修改,其他调用的地方都不需要动了。 错误信息的提炼 最后的最后,我们再补充一个可轻可重的点,那就是错误信息的提炼。 当我们在封装这么一个[代码]req[代码]对象的时候,我们的promise曾经reject过很多的错误信息,这些错误信息有可能来自: [代码]wx.request[代码]的fail; 不符合[代码]isHttpSuccess[代码]的网络错误; getSessionId失败; … 等等的一切可能。 这就导致了我们在提炼错误信息的时候陷入困境,到底catch到的会是哪种[代码]error[代码]对象? 这么看你可能不觉得有问题,我们来看看下面的例子: [代码]req.getHotList({ page: 1 }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); [代码] 假如上面的例子中,我想要的不仅仅是[代码]console.log(err)[代码],而是想将对应的错误信息弹窗出来,我应该怎么做? 我们只能将所有可能出现的错误都检查一遍: [代码]req.getHotList({ page: 1 }) .then((res) => { if (res.code !== 0) { // 后端接口报错格式 wx.showModal({ content: res.msg }); } }) .catch((err) => { let msg = '未知错误'; // 文本信息直接使用 if (typeof err === 'string') { msg = err; } // 小程序接口报错 if (err.errMsg) { msg = err.errMsg; } // 自定义接口的报错,比如网络错误 if (err.detail && err.detail.errMsg) { msg = err.detail.errMsg; } // 未知错误 wx.showModal({ content: msg }); }); [代码] 这就有点尴尬了,提炼错误信息的代码量都比业务还多几倍,而且还是每个接口调用都要写一遍~ 为了解决这个问题,我们需要封装一个方法来专门做提炼的工作: [代码]/** * 提炼错误信息 * 参数:err * 返回值:[string]errMsg */ function errPicker(err) { if (typeof err === 'string') { return err; } return err.msg || err.errMsg || (err.detail && err.detail.errMsg) || '未知错误'; } [代码] 那么过程会变成: [代码]req.getHotList({ page: 1 }) .then((res) => { console.log(res); }) .catch((err) => { const msg = req.errPicker(err); // 未知错误 wx.showModal({ content: msg }); }); [代码] 好吧,我们再偷懒一下,把wx.showModal也省去了: [代码]/** * 错误弹窗 */ function showErr(err) { const msg = errPicker(err); console.log(err); wx.showModal({ showCancel: false, content: msg }); } [代码] 最后就变成了: [代码]req.getHotList({ page: 1 }) .then((res) => { console.log(res); }) .catch(req.showErr); [代码] 至此,一个简单的wx.request封装过程便完成了,封装过的[代码]req[代码]比起原来,使用上更加方便,扩展性和可维护性也更好。 结尾 以上内容其实是简化版的[代码]mp-req[代码],介绍了[代码]mp-req[代码]这一工具的实现初衷以及思路,使用[代码]mp-req[代码]来管理接口会更加的便捷,同时[代码]mp-req[代码]也提供了更加丰富的功能,比如插件机制、接口的缓存,以及接口分类等,欢迎大家关注mp-req了解更多内容。 以上最终代码可以在这里获取:req.js。
2020-08-04 - 自适应 tabBar 组件 不是底部的哦
https://developers.weixin.qq.com/s/47VZSGmR7Q7w 这是代码片段链接 项目中有好多地方都需要用到 navbar ,一个项目中重复的使用同一段代码感觉很烦人,所以就自己写了一个,适合 2-4 个 tab,【支持多个】多个的话稍微修改一下布局就可以了 ━((′д`)爻(′д`))━!!!-图片传不上去 大家可以 打开代码链接看一下 如果感觉写的还凑合的 帮忙点个赞! 有什么可以改进的也可以 在下方评论
2019-04-17 - 发送短信验证码后60秒倒计时
微信小程序发送短信验证码后60秒倒计时功能,效果图: [图片] 完整代码 index.wxml [代码]<!--index.wxml-->[代码][代码]<view class=[代码][代码]"container"[代码][代码]>[代码][代码] [代码][代码]<view class=[代码][代码]"section"[代码][代码]>[代码][代码] [代码][代码]<text>手机号码:</text>[代码][代码] [代码][代码]<input placeholder=[代码][代码]"请输入手机号码"[代码] [代码]type=[代码][代码]"number"[代码] [代码]maxlength=[代码][代码]"11"[代码] [代码]bindinput=[代码][代码]"inputPhoneNum"[代码] [代码]auto-focus />[代码][代码] [代码][代码]<text wx:if=[代码][代码]"{{send}}"[代码] [代码]class=[代码][代码]"sendMsg"[代码] [代码]bindtap=[代码][代码]"sendMsg"[代码][代码]>发送</text>[代码][代码] [代码][代码]<text wx:if=[代码][代码]"{{alreadySend}}"[代码] [代码]class=[代码][代码]"sendMsg"[代码] [代码]>{{second+[代码][代码]"s"[代码][代码]}}</text>[代码][代码] [代码][代码]</view>[代码][代码]</view>[代码] index.wxss [代码]/**index.wxss**/[代码][代码].userinfo {[代码][代码] [代码][代码]display[代码][代码]: flex;[代码][代码] [代码][代码]flex-[代码][代码]direction[代码][代码]: column;[代码][代码] [代码][代码]align-items: [代码][代码]center[代码][代码];[代码][代码]}[代码][代码].section {[代码][代码]display[代码][代码]: flex;[代码][代码]margin[代码][代码]: [代码][代码]16[代码][代码]rpx;[代码][代码]padding[代码][代码]: [代码][代码]16[代码][代码]rpx;[代码][代码]border-bottom[代码][代码]: [代码][代码]1[代码][代码]rpx [代码][代码]solid[代码] [代码]#CFD8DC[代码][代码];[代码][代码]}[代码][代码] [代码] [代码]text {[代码][代码] [代码][代码]width[代码][代码]: [代码][代码]200[代码][代码]rpx;[代码][代码]}[代码][代码] [代码] [代码]button {[代码][代码] [代码][代码]margin[代码][代码]: [代码][代码]16[代码][代码]rpx;[代码][代码]}[代码][代码] [代码] [代码].sendMsg {[代码][代码] [代码][代码]font-size[代码][代码]: [代码][代码]12[代码][代码];[代码][代码] [代码][代码]margin-right[代码][代码]: [代码][代码]0[代码][代码];[代码][代码] [代码][代码]padding[代码][代码]: [代码][代码]0[代码][代码];[代码][代码] [代码][代码]height[代码][代码]: inherit;[代码][代码] [代码][代码]width[代码][代码]: [代码][代码]80[代码][代码]rpx;[代码][代码]}[代码]index.js [代码]//index.js[代码][代码]//获取应用实例[代码][代码]const app = getApp()[代码][代码] [代码] [代码]Page({[代码][代码] [代码][代码]data: {[代码][代码] [代码][代码]send: true,[代码][代码] [代码][代码]alreadySend: false,[代码][代码] [代码][代码]second: [代码][代码]60[代码][代码],[代码][代码] [代码][代码]disabled: true,[代码][代码] [代码][代码]phoneNum: [代码][代码]''[代码][代码] [代码][代码]},[代码][代码] [代码][代码]// 手机号部分[代码][代码] [代码][代码]inputPhoneNum: function (e) {[代码][代码] [代码][代码]let phoneNum = e.detail.value[代码][代码] [代码][代码]this.setData({[代码][代码] [代码][代码]phoneNum: phoneNum[代码][代码] [代码][代码]})[代码][代码] [代码][代码]},[代码][代码] [代码][代码]sendMsg: function () {[代码][代码] [代码][代码]var phoneNum = this.data.phoneNum;[代码][代码] [代码][代码]if(phoneNum == [代码][代码]''[代码][代码]){[代码][代码] [代码][代码]wx.showToast({[代码][代码] [代码][代码]title: [代码][代码]'请输入手机号码'[代码][代码],[代码][代码] [代码][代码]icon: [代码][代码]'none'[代码][代码],[代码][代码] [代码][代码]duration: [代码][代码]2000[代码][代码] [代码][代码]})[代码][代码] [代码][代码]return ;[代码][代码] [代码][代码]}[代码][代码] [代码][代码]//此处省略发送短信验证码功能[代码][代码] [代码][代码]this.setData({[代码][代码] [代码][代码]alreadySend: true,[代码][代码] [代码][代码]send: false[代码][代码] [代码][代码]})[代码][代码] [代码][代码]this.timer()[代码][代码] [代码][代码]},[代码][代码] [代码][代码]showSendMsg: function () {[代码][代码] [代码][代码]if (!this.data.alreadySend) {[代码][代码] [代码][代码]this.setData({[代码][代码] [代码][代码]send: true[代码][代码] [代码][代码]})[代码][代码] [代码][代码]}[代码][代码] [代码][代码]},[代码][代码] [代码][代码]hideSendMsg: function () {[代码][代码] [代码][代码]this.setData({[代码][代码] [代码][代码]send: false,[代码][代码] [代码][代码]disabled: true,[代码][代码] [代码][代码]buttonType: [代码][代码]'default'[代码][代码] [代码][代码]})[代码][代码] [代码][代码]},[代码][代码] [代码][代码]timer: function () {[代码][代码] [代码][代码]let promise = new Promise((resolve, reject) => {[代码][代码] [代码][代码]let setTimer = setInterval([代码][代码] [代码][代码]() => {[代码][代码] [代码][代码]this.setData({[代码][代码] [代码][代码]second: this.data.second - [代码][代码]1[代码][代码] [代码][代码]})[代码][代码] [代码][代码]if (this.data.second <= [代码][代码]0[代码][代码]) {[代码][代码] [代码][代码]this.setData({[代码][代码] [代码][代码]second: [代码][代码]60[代码][代码],[代码][代码] [代码][代码]alreadySend: false,[代码][代码] [代码][代码]send: true[代码][代码] [代码][代码]})[代码][代码] [代码][代码]resolve(setTimer)[代码][代码] [代码][代码]}[代码][代码] [代码][代码]}[代码][代码] [代码][代码], [代码][代码]1000[代码][代码])[代码][代码] [代码][代码]})[代码][代码] [代码][代码]promise.then((setTimer) => {[代码][代码] [代码][代码]clearInterval(setTimer)[代码][代码] [代码][代码]})[代码][代码] [代码][代码]},[代码][代码]})[代码]完整的短信验证码登录实例参考: https://blog.csdn.net/zuoliangzhu/article/details/81219900
2019-04-17 - 开启websocket服务端口,调试微信websocket接口方法
使用node环境,在vscode 工具中,创建 app.js 文件 ,代码如下,记得安装 npm install websocket 和 npm install http 模块 . 开启 websocket接口服务后,就可以去封装 官方提供的 wx.sendSocketMessage 等接口了。 [代码]const http = require("http"); const WebSocketServer = require("websocket").server; const httpServer = http.createServer((request, response) => { console.log("[" + new Date() + "] Received request for " + request.url); response.writeHead(404); response.end(); }); const wsServer = new WebSocketServer({ httpServer, autoAcceptConnections: true }); wsServer.on("connect", connection => { connection .on("message", message => { if (message.type === "utf8") { console.log(">> message content from client: " + message.utf8Data); connection.sendUTF(message.utf8Data); // 输出内容返回给前端接口调用 } }) .on("close", (reasonCode, description) => { console.log( "[" + new Date() + "] Peer " + connection.remoteAddress + " disconnected." ); }); }); httpServer.listen(8080, () => { console.log("[" + new Date() + "] Serveris listening on port 8080"); }); #小程序页面示例代码,请参考文档 const socketOpen = false const socketMsgQueue = [] wx.connectSocket({ // url: 'test.php', url :“ws://localhost:8080/” }) wx.onSocketOpen(function (res) { socketOpen = true for (let i = 0; i < socketMsgQueue.length; i++) { sendSocketMessage(socketMsgQueue[i]) } socketMsgQueue = [] }) function sendSocketMessage(msg) { if (socketOpen) { wx.sendSocketMessage({ data: msg }) } else { socketMsgQueue.push(msg) } }[代码]
2019-04-18 - 路由的封装
小程序提供了路由功能来实现页面跳转,但是在使用的过程中我们还是发现有些不方便的地方,通过封装,我们可以实现诸如路由管理、简化api等功能。 页面的跳转存在哪些问题呢? 与接口的调用一样面临url的管理问题; 传递参数的方式不太友好,只能拼装url; 参数类型单一,只支持string。 alias 第一个问题很好解决,我们做一个集中管理,比如新建一个[代码]router/routes.js[代码]文件来实现alias: [代码]// routes.js module.exports = { // 主页 home: '/pages/index/index', // 个人中心 uc: '/pages/user_center/index', }; [代码] 然后使用的时候变成这样: [代码]const routes = require('../../router/routes.js'); Page({ onReady() { wx.navigateTo({ url: routes.uc, }); }, }); [代码] query 第二个问题,我们先来看个例子,假如我们跳转[代码]pages/user_center/index[代码]页面的同时还要传[代码]userId[代码]过去,正常情况下是这么来操作的: [代码]const routes = require('../../router/routes.js'); Page({ onReady() { const userId = '123456'; wx.navigateTo({ url: `${routes.uc}?userId=${userId}`, }); }, }); [代码] 这样确实不好看,我能不能把参数部分单独拿出来,不用拼接到url上呢? 可以,我们试着实现一个[代码]navigateTo[代码]函数: [代码]const routes = require('../../router/routes.js'); function navigateTo({ url, query }) { const queryStr = Object.keys(query).map(k => `${k}=${query[k]}`).join('&'); wx.navigateTo({ url: `${url}?${queryStr}`, }); } Page({ onReady() { const userId = '123456'; navigateTo({ url: routes.uc, query: { userId, }, }); }, }); [代码] 嗯,这样貌似舒服一点。 参数保真 第三个问题的情况是,当我们传递的参数argument不是[代码]string[代码],而是[代码]number[代码]或者[代码]boolean[代码]时,也只能在下个页面得到一个[代码]string[代码]值: [代码]// pages/index/index.js Page({ onReady() { navigateTo({ url: routes.uc, query: { isActive: true, }, }); }, }); // pages/user_center/index.js Page({ onLoad(options) { console.log(options.isActive); // => "true" console.log(typeof options.isActive); // => "string" console.log(options.isActive === true); // => false }, }); [代码] 上面这种情况想必很多人都遇到过,而且感到很抓狂,本来就想传递一个boolean,结果不管传什么都会变成string。 有什么办法可以让数据变成字符串之后,还能还原成原来的类型? 好熟悉,这不就是json吗?我们把要传的数据转成json字符串([代码]JSON.stringify[代码]),然后在下个页面把它转回json数据([代码]JSON.parse[代码])不就好了嘛! 我们试着修改原来的[代码]navigateTo[代码]: [代码]const routes = require('../../router/routes.js'); function navigateTo({ url, data }) { const dataStr = JSON.stringify(data); wx.navigateTo({ url: `${url}?jsonStr=${dataStr}`, }); } Page({ onReady() { navigateTo({ url: routes.uc, data: { isActive: true, }, }); }, }); [代码] 这样我们在页面中接受json字符串并转换它: [代码]// pages/user_center/index.js Page({ onLoad(options) { const json = JSON.parse(options.jsonStr); console.log(json.isActive); // => true console.log(typeof json.isActive); // => "boolean" console.log(json.isActive === true); // => true }, }); [代码] 这里其实隐藏了一个问题,那就是url的转义,假如json字符串中包含了类似[代码]?[代码]、[代码]&[代码]之类的符号,可能导致我们参数解析出错,所以我们要把json字符串encode一下: [代码]function navigateTo({ url, data }) { const dataStr = encodeURIComponent(JSON.stringify(data)); wx.navigateTo({ url: `${url}?encodedData=${dataStr}`, }); } // pages/user_center/index.js Page({ onLoad(options) { const json = JSON.parse(decodeURIComponent(options.encodedData)); console.log(json.isActive); // => true console.log(typeof json.isActive); // => "boolean" console.log(json.isActive === true); // => true }, }); [代码] 这样使用起来不方便,我们封装一下,新建文件[代码]router/index.js[代码]: [代码]const routes = require('./routes.js'); function navigateTo({ url, data }) { const dataStr = encodeURIComponent(JSON.stringify(data)); wx.navigateTo({ url: `${url}?encodedData=${dataStr}`, }); } function extract(options) { return JSON.parse(decodeURIComponent(options.encodedData)); } module.exports = { routes, navigateTo, extract, }; [代码] 页面中我们这样来使用: [代码]const router = require('../../router/index.js'); // page home Page({ onLoad(options) { router.navigateTo({ url: router.routes.uc, data: { isActive: true, }, }); }, }); // page uc Page({ onLoad(options) { const json = router.extract(options); console.log(json.isActive); // => true console.log(typeof json.isActive); // => "boolean" console.log(json.isActive === true); // => true }, }); [代码] route name 这样貌似还不错,但是[代码]router.navigateTo[代码]不太好记,[代码]router.routes.uc[代码]有点冗长,我们考虑把[代码]navigateTo[代码]换成简单的[代码]push[代码],至于路由,我们可以使用[代码]name[代码]的方式来替换原来[代码]url[代码]参数: [代码]const routes = require('./routes.js'); function push({ name, data }) { const dataStr = encodeURIComponent(JSON.stringify(data)); const url = routes[name]; wx.navigateTo({ url: `${url}?encodedData=${dataStr}`, }); } function extract(options) { return JSON.parse(decodeURIComponent(options.encodedData)); } module.exports = { push, extract, }; [代码] 在页面中使用: [代码]const router = require('../../router/index.js'); Page({ onLoad(options) { router.push({ name: 'uc', data: { isActive: true, }, }); }, }); [代码] navigateTo or switchTab 页面跳转除了navigateTo之外还有switchTab,我们是不是可以把这个差异抹掉?答案是肯定的,如果我们在配置routes的时候就已经指定是普通页面还是tab页面,那么程序完全可以切换到对应的跳转方式。 我们修改一下[代码]router/routes.js[代码],假设home是一个tab页面: [代码]module.exports = { // 主页 home: { type: 'tab', path: '/pages/index/index', }, uc: { path: '/pages/a/index', }, }; [代码] 然后修改[代码]router/index.js[代码]中[代码]push[代码]的实现: [代码]function push({ name, data }) { const dataStr = encodeURIComponent(JSON.stringify(data)); const route = routes[name]; if (route.type === 'tab') { wx.switchTab({ url: `${route.path}`, // 注意tab页面是不支持传参的 }); return; } wx.navigateTo({ url: `${route.path}?encodedData=${dataStr}`, }); } [代码] 搞定,这样我们一个[代码]router.push[代码]就能自动切换两种跳转方式了,而且之后一旦页面类型有变动,我们也只需要修改[代码]route[代码]的定义就可以了。 直接寻址 alias用着很不错,但是有一点挺麻烦得就是每新建一个页面都要写一个alias,即使没有别名的需要,我们是不是可以处理一下,如果在alias没命中,那就直接把name转化成url?这也是阔以的。 [代码]function push({ name, data }) { const dataStr = encodeURIComponent(JSON.stringify(data)); const route = routes[name]; const url = route ? route.path : name; if (route.type === 'tab') { wx.switchTab({ url: `${url}`, // 注意tab页面是不支持传参的 }); return; } wx.navigateTo({ url: `${url}?encodedData=${dataStr}`, }); } [代码] 在页面中使用: [代码]Page({ onLoad(options) { router.push({ name: 'pages/user_center/a/index', data: { isActive: true, }, }); }, }); [代码] 注意,为了方便维护,我们规定了每个页面都必须存放在一个特定的文件夹,一个文件夹的当前路径下只能存在一个index页面,比如[代码]pages/index[代码]下面会存放[代码]pages/index/index.js[代码]、[代码]pages/index/index.wxml[代码]、[代码]pages/index/index.wxss[代码]、[代码]pages/index/index.json[代码],这时候你就不能继续在这个文件夹根路径存放另外一个页面,而必须是新建一个文件夹来存放,比如[代码]pages/index/pageB/index.js[代码]、[代码]pages/index/pageB/index.wxml[代码]、[代码]pages/index/pageB/index.wxss[代码]、[代码]pages/index/pageB/index.json[代码]。 这样是能实现功能,但是这个name怎么看都跟alias风格差太多,我们试着定义一套转化规则,让直接寻址的name与alias风格统一一些,[代码]pages[代码]和[代码]index[代码]其实我们可以省略掉,[代码]/[代码]我们可以用[代码].[代码]来替换,那么原来的name就变成了[代码]user_center.a[代码]: [代码]Page({ onLoad(options) { router.push({ name: 'user_center.a', data: { isActive: true, }, }); }, }); [代码] 我们再来改进[代码]router/index.js[代码]中[代码]push[代码]的实现: [代码]function push({ name, data }) { const dataStr = encodeURIComponent(JSON.stringify(data)); const route = routes[name]; const url = route ? route.path : `pages/${name.replace(/\./g, '/')}/index`; if (route.type === 'tab') { wx.switchTab({ url: `${url}`, // 注意tab页面是不支持传参的 }); return; } wx.navigateTo({ url: `${url}?encodedData=${dataStr}`, }); } [代码] 这样一来,由于支持直接寻址,跳转home和uc还可以写成这样: [代码]router.push({ name: 'index', // => /pages/index/index }); router.push({ name: 'user_center', // => /pages/user_center/index }); [代码] 这样一来,除了一些tab页面以及特定的路由需要写alias之外,我们也不需要新增一个页面就写一条alias这么麻烦了。 其他 除了上面介绍的navigateTo和switchTab外,其实还有[代码]wx.redirectTo[代码]、[代码]wx.navigateBack[代码]以及[代码]wx.reLaunch[代码]等,我们也可以做一层封装,过程雷同,所以我们就不再一个个介绍,这里贴一下最终简化后的api以及原生api的映射关系: [代码]router.push => wx.navigateTo router.replace => wx.redirectTo router.pop => wx.navigateBack router.relaunch => wx.reLaunch [代码] 最终实现已经在发布在github上,感兴趣的朋友可以移步了解:mp-router。
2019-04-26 - 微信小程序 unionid 登录解决方案
第三方登录模块使开发者能快捷灵活的拥有自己的用户系统,是 LeanCloud 最受欢迎的功能之一。随着第三方平台的演化,特别是微信小程序的流行,LeanCloud 第三方登录模块也一直在改进: v2.0*:增加微信小程序一键登录功能。支持开发者不写任何后端代码实现微信小程序用户系统与 LeanCloud 用户系统的关联。 v3.6:增加 unionid 登录接口。支持开发者使用 unionid 关联一个微信开发者帐号下的多个应用从而共享一套 LeanCloud 用户系统。 这两个功能各自都非常简单可靠,但是其中重叠的部分需求却是一个难题:「如何在小程序中支持 unionid 登录,既能得到 unionid 登录机制的灵活性,又保留一键登录功能的便利性」。 在最近发布的 JavaScript SDK v3.13 中包含了微信小程序 unionid 登录支持。我们根据不同的需求设计了不同的解决方案。 * 这里的版本指开始支持该功能的 JavaScript SDK 版本。 一键登录 LeanCloud 的用户系统支持一键使用微信用户身份登录。要使用一键登录功能,需要先设置小程序的 AppID 与 AppSecret: 1.登录 微信公众平台,在 设置 > 开发设置 中获得 AppID 与 AppSecret。 前往 LeanCloud 控制台 > 组件 > 社交,保存「微信小程序」的 AppID 与 AppSecret。 这样你就可以在应用中使用[代码]AV.User.loginWithWeapp()[代码]方法来使用当前用户身份登录了。 [代码]AV.User.loginWithWeapp().then(user => { this.globalData.user = user; }).catch(console.error); [代码] 使用一键登录方式登录时,LeanCloud 会将该用户的小程序 [代码]openid[代码] 与 [代码]session_key[代码] 等信息保存在对应的 [代码]user.authData.lc_weapp[代码] 属性中,你可以在控制台的 [代码]_User[代码] 表中看到: [代码]{ "authData": { "lc_weapp": { "session_key": "2zIDoEEUhkb0B5pUTzsLVg==", "expires_in": 7200, "openid": "obznq0GuHPxdRYaaDkPOHk785DuA" } } } [代码] 如果用户是第一次使用此应用,调用登录 API 会创建一个新的用户,你可以在 控制台 > 存储 中的 [代码]_User[代码] 表中看到该用户的信息,如果用户曾经使用该方式登录过此应用(存在对应 openid 的用户),再次调用登录 API 会返回同一个用户。 用户的登录状态会保存在客户端中,可以使用 [代码]AV.User.current()[代码] 方法来获取当前登录的用户,下面的例子展示了如何为登录用户保存额外的信息: [代码]// 假设已经通过 AV.User.loginWithWeapp() 登录 // 获得当前登录用户 const user = AV.User.current(); // 调用小程序 API,得到用户信息 wx.getUserInfo({ success: ({userInfo}) => { // 更新当前用户的信息 user.set(userInfo).save().then(user => { // 成功,此时可在控制台中看到更新后的用户信息 this.globalData.user = user; }).catch(console.error); } }); [代码] [代码]authData[代码] 默认只有对应用户可见,开发者可以使用 masterKey 在云引擎中获取该用户的 [代码]openid[代码] 与 [代码]session_key[代码] 进行支付、推送等操作。详情的示例请参考 支付。 小程序的登录态([代码]session_key[代码])存在有效期,可以通过 wx.checkSession() 方法检测当前用户登录态是否有效,失效后可以通过调用 [代码]AV.User.loginWithWeapp()[代码] 重新登录。 使用 unionid 微信开放平台使用 unionid 来区分用户的唯一性,也就是说同一个微信开放平台帐号下的移动应用、网站应用和公众帐号(包括小程序),用户的 unionid 都是同一个,而 openid 会是多个。如果你想要实现多个小程序之间,或者小程序与使用微信开放平台登录的应用之间共享用户系统的话,则需要使用 unionid 登录。 要在小程序中使用 unionid 登录,请先确认已经在 微信开放平台 绑定了该小程序 在小程序中有很多途径可以 获取到 unionid。不同的 unionid 获取方式,接入 LeanCloud 用户系统的方式也有所不同。 一键登录时静默获取 unionid 当满足以下条件时,一键登录 API [代码]AV.User.loginWithWeapp()[代码] 能静默地获取到用户的 unionid 并用 unionid + openid 进行匹配登录。 微信开放平台帐号下存在同主体的公众号,并且该用户已经关注了该公众号。 微信开放平台帐号下存在同主体的公众号或移动应用,并且该用户已经授权登录过该公众号或移动应用。 要启用这种方式,需要在一键登录时指定参数 [代码]preferUnionId[代码] 为 true: [代码]AV.User.loginWithWeapp({ preferUnionId: true, }); [代码] 使用 unionid 登录后,用户的 authData 中会增加 _[代码]weixin_unionid[代码] 一项(与 [代码]lc_weapp[代码] 平级): [代码]{ "authData": { "lc_weapp": { "session_key": "2zIDoEEUhkb0B5pUTzsLVg==", "expires_in": 7200, "openid": "obznq0GuHPxdRYaaDkPOHk785DuA", "unionid": "ox7NLs5BlEqPS4glxqhn5kkO0UUo" }, "_weixin_unionid": { "uid": "ox7NLs5BlEqPS4glxqhn5kkO0UUo" } } } [代码] 用 unionid + openid 登录时,会按照下面的步骤进行用户匹配: 如果已经存在对应 [代码]unionid(authData._weixin_unionid.uid[代码])的用户,则会直接作为这个用户登录,并将所有信息([代码]openid[代码]、[代码]session_key[代码]、[代码]unionid[代码] 等)更新到该用户的 [代码]authData.lc_ewapp[代码] 中。 如果不存在匹配 unionid 的用户,但存在匹配 openid([代码]authData.lc_weapp.openid[代码])的用户,则会直接作为这个用户登录,并将所有信息([代码]session_key[代码]、[代码]unionid[代码] 等)更新到该用户的 [代码]authData.lc_ewapp[代码] 中,同时将 [代码]unionid[代码] 保存到 [代码]authData._weixin_unionid.uid[代码] 中。 如果不存在匹配 unionid 的用户,也不存在匹配 openid 的用户,则创建一个新用户,将所有信息([代码]session_key[代码]、[代码]unionid[代码] 等)更新到该用户的 [代码]authData.lc_ewapp[代码] 中,同时将 [代码]unionid[代码] 保存到 [代码]authData._weixin_unionid.uid[代码] 中。 不管匹配的过程是如何的,最终登录用户的 [代码]authData[代码] 都会是上面这种结构。 LeanTodo Demo 便是使用这种方式登录的,如果你已经关注了其关联的公众号(搜索 AVOSCloud,或通过小程序关于页面的相关公众号链接访问),那么你在登录后会在 LeanTodo Demo 的 设置 - 用户 页面看到当前用户的 [代码]authData[代码] 中已经绑定了 unionid。 [图片] 微信扫描二维码进入 Demo 需要注意的是: 如果用户不符合上述静默获取 unionid 的条件,那么就算指定了 [代码]preferUnionId[代码] 也不会使用 unionid 登录。 如果用户符合上述静默获取 unionid 的条件,但没有指定 [代码]preferUnionId[代码],那么该次登录不会使用 unionid 登录,但仍然会将获取到的 unionid 作为一般字段写入该用户的 [代码]authData.lc_weapp[代码] 中。此时用户的 [代码]authData[代码] 会是这样的: [代码]{ "authData": { "lc_weapp": { "session_key": "2zIDoEEUhkb0B5pUTzsLVg==", "expires_in": 7200, "openid": "obznq0GuHPxdRYaaDkPOHk785DuA", "unionid": "ox7NLs5BlEqPS4glxqhn5kkO0UUo" } } } [代码] 通过其他方式获取 unionid 后登录 如果开发者自行获得了用户的 unionid(例如通过解密 wx.getUserInfo 获取到的用户信息),可以在小程序中调用 [代码]AV.User.loginWithWeappWithUnionId()[代码] 投入 unionid 完成登录授权: [代码]AV.User.loginWithWeappWithUnionId(unionid, { asMainAccount: true }).then(console.log, console.error); [代码] 通过其他方式获取 unionid 与 openid 后登录 如果开发者希望更灵活的控制小程序的登录流程,也可以自行在服务端实现 unionid 与 openid 的获取,然后调用通用的第三方 unionid 登录接口指定平台为 [代码]lc_weapp[代码] 来登录: [代码]const unionid = ''; const authData = { openid: '', session_key: '' }; const platform = 'lc_weapp'; AV.User.loginWithAuthDataAndUnionId(authData, platform, unionid, { asMainAccount: true }).then(console.log, console.error); [代码] 相对上面提到的一些 Weapp 相关的登录 API,loginWithAuthDataAndUnionId 是更加底层的第三方登录接口,不依赖小程序运行环境,因此这种方式也提供了更高的灵活度: 可以在服务端获取到 unionid 与 openid 等信息后返回给小程序客户端,在客户端调用 [代码]AV.User.loginWithAuthDataAndUnionId[代码] 来登录。 也可以在服务端获取到 unionid 与 openid 等信息后直接调用 [代码]AV.User.loginWithAuthDataAndUnionId[代码] 登录,成功后得到登录用户的 [代码]sessionToken[代码] 后返回给客户端,客户端再使用该 [代码]sessionToken[代码] 直接登录。 关联第二个小程序 这种用法的另一种常见场景是关联同一个开发者帐号下的第二个小程序。 因为一个 LeanCloud 应用默认关联一个微信小程序(对应的平台名称是 [代码]lc_weapp[代码]),使用小程序系列 API 的时候也都是默认关联到 [代码]authData.lc_weapp[代码] 字段上。如果想要接入第二个小程序,则需要自行获取到 unionid 与 openid,然后将其作为一个新的第三方平台登录。这里同样需要用到 [代码]AV.User.loginWithAuthDataAndUnionId[代码] 方法,但与关联内置的小程序平台([代码]lc_weapp[代码])有一些不同: 需要指定一个新的 [代码]platform[代码] 需要将 [代码]openid[代码] 保存为 [代码]uid[代码](内置的微信平台做了特殊处理可以直接用 [代码]openid[代码] 而这里是作为通用第三方 OAuth 平台保存因此需要使用标准的 [代码]uid[代码] 字段)。 这里我们以新的平台 [代码]weapp2[代码] 为例: [代码]const unionid = ''; const openid = ''; const authData = { uid: openid, session_key: '' }; const platform = 'weapp2'; AV.User.loginWithAuthDataAndUnionId(authData, platform, unionid, { asMainAccount: true }).then(console.log, console.error); [代码] 获取 unionid 后与现有用户关联 如果一个用户已经登录,现在通过某种方式获取到了其 unionid(一个常见的使用场景是用户完成了支付操作后在服务端通过 getPaidUnionId 得到了 unionid)希望与之关联,可以在小程序中使用 [代码]AV.User#associateWithWeappWithUnionId()[代码]: [代码]const user = AV.User.current(); // 获取当前登录用户 user.associateWithWeappWithUnionId(unionid, { asMainAccount: true }).then(console.log, console.error); [代码] 启用其他登录方式 上述的登录 API 对接的是小程序的用户系统,所以使用这些 API 创建的用户无法直接在小程序之外的平台上登录。如果需要使用 LeanCloud 用户系统提供的其他登录方式,如用手机号验证码登录、邮箱密码登录等,在小程序登录后设置对应的用户属性即可: [代码]// 小程序登录 AV.User.loginWithWeapp().then(user => { // 设置并保存手机号 user.setMobilePhoneNumber('13000000000'); return user.save(); }).then(user => { // 发送验证短信 return AV.User.requestMobilePhoneVerify(user.getMobilePhoneNumber()); }).then({ // 用户填写收到短信验证码后再调用 AV.User.verifyMobilePhone(code) 完成手机号的绑定 // 成功后用户的 mobilePhoneVerified 字段会被置为 true // 此后用户便可以使用手机号加动态验证码登录了 }).catch(console.error); [代码] 验证手机号码功能要求在 控制台 > 存储 > 设置 > 用户账号 启用「用户注册时,向注册手机号码发送验证短信」。 绑定现有用户 如果你的应用已经在使用 LeanCloud 的用户系统,或者用户已经通过其他方式注册了你的应用(比如在 Web 端通过用户名密码注册),可以通过在小程序中调用 [代码]AV.User#associateWithWeapp()[代码] 来关联已有的账户: [代码]// 首先,使用用户名与密码登录一个已经存在的用户 AV.User.logIn('username', 'password').then(user => { // 将当前的微信用户与当前登录用户关联 return user.associateWithWeapp(); }).catch(console.error); [代码] 更多内容欢迎查看《在微信小程序与小游戏中使用 LeanCloud》。
2019-04-28 - request封装
fetch.js [代码]const api = 'www.qq.com' export const Fetch = ({ url = '', data = {}, header = { "content-type": "application/json" }, method = 'GET', api = Api }) => { return new Promise((resolve, reject) => { wx.request({ url: api + url, header: header, method: method, data: data, success: res => { // 成功时的处理 if (res.data.error == 0) { resolve(res.data); } else { reject(res.data); } }, fail: err => { reject(err); } }) }) } [代码] api.js [代码]import { Fetch } from './fetch.js'; export const PostMiniList = data => { return Fetch({ url: '/post/post_mini_list.json', data: data, method: 'POST', header: { 'content-type': 'application/x-www-form-urlencoded' } }) } export const GetMiniList = data => { return Fetch({ url: '/get/get_mini_list.json', data: data }) } [代码] index.js [代码]import { PostMiniList, GetMiniList } from './api.js'; PostMiniList({ a:1, b:2 }).then( res => { // 成功处理 }, err => { // 失败处理 } ) GetMiniList({ a:1, b:2 }).then( res => { // 成功处理 }, err => { // 失败处理 } ) [代码] 把所有api放在api.js里统一管理,利用promise使我们只关注返回的结果
2019-05-06 - 小程序图片上传,存储,获取,显示
我们在做小程序开发时,难免会遇到图片上传的功能,我们如果自己搭建图片服务器的话,成功太大了,并且还要写后台程序来接收上传的图片,还要有存储服务器。好在小程序云开发为我们提供了云存储的功能,这样我们就可以轻松的实现小程序图片的上传和存储。 老规矩,先看效果图 [图片] 本节知识点 1,小程序图片的选取 2,小程序图片的上传 3,小程序图片的存储 4,获取云端图片并显示 下面就来具体讲解下具体实现步骤 图片的选择和上传 index.wxml文件如下 [代码] <view class='item_root' bindtap='chuantupian'> <text>{{zhaopian}}</text> <view class='right_arrow' /> </view> [代码] index.js文件如下 [代码] //上传图片 chuantupian() { let that = this; let timestamp = (new Date()).valueOf(); wx.chooseImage({ success: chooseResult => { wx.showLoading({ title: '上传中。。。', }) // 将图片上传至云存储空间 wx.cloud.uploadFile({ // 指定上传到的云路径 cloudPath: timestamp + '.png', // 指定要上传的文件的小程序临时文件路径 filePath: chooseResult.tempFilePaths[0], // 成功回调 success: res => { console.log('上传成功', res) wx.hideLoading() wx.showToast({ title: '上传图片成功', }) if (res.fileID) { that.setData({ zhaopian: '图片如下', imgUrl: res.fileID }) } }, }) }, }) }, [代码] 到这里其实我们就可以实现图片的选取和上传功能了。 下面讲讲具体是如何实现的 首先我们通过wx.chooseImage来获取相册里的图片 再获取照片成功后,我们用当前时间戳命名图片,然后使用 wx.cloud.uploadFile方法来实现图片的上传 在上传成功后,会有如下回调。下图中的filenId就是我们在云存储中的路径,可以直接用这个路径来获取图片并显示的。 [图片] 到这里我们就轻松的实现了小程序图片上传的功能,是不是很简单。 有任何关于编程的问题都可以加我微信2501902696(备注编程开发) 编程小石头,码农一枚,非著名全栈开发人员。分享自己的一些经验,学习心得,希望后来人少走弯路,少填坑。 视频讲解地址:https://edu.csdn.net/course/play/9604/281187 [图片]
2019-06-11 - 微信小程序三种授权登录的方式
经过一段时间对微信小程序的研发后 总结出以下三种授权登录的方式,我给他们命名为‘一次性授权’‘永久授权’‘不授权’ 1.一次性授权 常规写法,需要获取用户公开信息(头像,昵称等)时,判断调取授权登录接口,但是此方法如果不经处理的话 用户如果拒绝授权或者删除该微信小程序后 需要重新调取并获取用户公开信息(头像,昵称等),此方法用户体验较差,不建议使用; 2.永久授权 在不必要使用用户公开信息(头像,昵称等)时,不调取授权登录接口,只有在必要的时候再去判断调取授权登录接口并把获取到的用户公开信息存入数据库,这样在每次登录时直接先运行指定函数从数据库索取需要的用户公开信息(头像,昵称等)即可,此方法在删除小程序后不用再次去授权登录(因为在用户第一次授权登录时已经把用户的公开信息存入数据库了以后直接向数据库索取即可),建议使用; 3.不授权 不需要授权登录获取用户公开信息(头像,昵称等),使用wx.login获取用户code并传入后台,后台可以通过用户的code值向微信要一个值(具体需要问后台,我只是个小前端,后台的东西不是很懂,只是知道一些逻辑而且也已经成功实现)然后通过这个用code换取的值就可以识别到指定用户,如果需要的话,前端要显示的头像、昵称等这些信息可以使用自定义可编辑的功能,当然,也可以通过<open-data type=“userAvatarUrl”></open-data><open-data type=“userNickName”></open-data>小程序提供的这个组件显示用户的头像及昵称(不过这个组件只有显示功能),用户如果想直接使用自己的头像昵称,也可以自行授权(比如添加个引导按钮什么之类的),建议使用; [图片][图片] 文中使用的微信自带接口、组件及函数: <open-data type=“userAvatarUrl”></open-data> <open-data type=“userNickName”></open-data> wx.login({ success(res){ console.log(res.code) } }) 微信授权登录 以上三种方式可以灵活运用,也可以把需要的结合到一起,并不冲突; 当然,大佬很多,我也只是个小前端而已,第一次发表技术方面的帖子,希望互相学习,互相指导,如有说的不对的地方还望大佬们及时指出!!! 谢谢
2019-04-18