微信小程序学习小结(及《Days》核心代码总结?)(一?)
因为孩儿他娘情人节当天的礼物没到位,写小程序一枚作为补偿w
微信推出小程序有些时日了,安卓端各大厂商也开始用快应用跟进对抗,苹果方面,大概因为前段时间打赏风波结下的梁子,目前迟迟没有给腾讯开放将小程序放到桌面的权限,所以说,“真正在意软件的人,应该生产自己的硬件”,有技术实力赶紧上QPhone吧w
微信小程序是一个内置的网页程序,使用wx语言,有以下几种核心文件类型
wxs:对应js,用来控制页面表现(官方语言:响应内置组件的事件),也是和HTML+CSS+JS不同的地方,将页面表现的部分单独分离出来,本质上是一个专门处理页面样式及触摸事件,不能调用API的js文件,处理效率号称是js的20倍(iphone专享,安卓机没有差别),所有wxml中涉及到的函数都要通过moudle.exports暴露出来
js:对应js,存储小程序页面的初始变量、处理响应事件以及执行小程序的API,不能控制页面样式
wxml:对应html,是微信的网页页面语言,和HTML比增加了一些逻辑表达式和通讯用变量的支持(官方语言:数据绑定),小程序通过控制这些变量实现页面变化以及和wxs、js之间的交互(官方语言:视图层与逻辑层的通讯)
wxss:对应css,和CSS没什么区别,多支持了一个iphone专用的单位rpx,似乎暂时不支持自定义动画,但在wxml上有一个专门的animation属性
逻辑层的部分API只对苹果系统有效,果然是苹果最大的开发者之一w
除此之外,最外层还有一系列启动相关的配置文件和初始化文件。
小程序把wxml、wxss、wxs放在了视图层,逻辑层使用一个js文件,通过Page构造器进行每个页面的初始化和响应。在触控事件中,wxs可以通过callMethod('function',event)函数调用js中的函数,js完全无法调用wxs的函数,其他情况下,他们只能通过wxml中的data标签进行通信,相当于有限的单向通行。
微信在后端为个人免费提供了五个云函数创建权限、5G的json数据库和存储空间,开发工具自带git仓库,如果有站外请求,还要额外申请符合苹果ATS审核的https证书,调整服务器配置。从这个角度来讲,小程序挺适合编程入门学习的,五脏俱全w
==========================接下来进入正题======================
在官方文档和互联网的指导下,完成了第一个小程序Days,一个简单的计日器,记录正向和负向的纪念日,生成相应的计日卡片,可以对卡片进行手势操作,滑动删除卡片,页面效果是这样的
代码方面有两个核心功能点:滑动期间的移动和删除操作完成后的动画效果。滑动卡片移动,移开手指判定卡片是否删除,删除期间卡片淡出,然后隐藏对应卡片。
移动方面用到了wxs的instance.setStyle()动态调整
删除动画效果方面,在写代码的途中,构思了两种实现方式
一是js将当前卡片的id存储到data-属性中,再通过wxs设置该id的页面样式,隐藏,这种方法中的数据在逻辑层和视图层的传递次数很多,实现比较复杂,在发现第二种方法后弃用;
第二种是更好的方法,在js中构造一个数组hides存储已经隐藏的卡片id列表,在wxs中构造一个判断函数,页面对手势操作进行删除响应后,判断当前id是否在该数组中,如果在则隐藏,淡出动画通过向卡片增加wxss中的cake-hide类实现,这种实现方式符合小程序的设计理念,果然事半功倍w(或者说都是被这个设计逼的?
核心代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | <wxs src="./main.wxs" module="main"></wxs> <wxs src="./move_cake.wxs" module="touch"></wxs> <view class="body"> <!-- 另一种实现,通过current_id判断被选中的卡片 <view class="{{main.contains(item._id,hides)?'cakes cake-hide':'cakes'}}" data-cid='{{current_id}}' wx:for="{{counters}}" wx:key="_id" id="{{main.ids(item._id)}}" bindtouchstart="{{touch.touchstart}}" bindtouchmove="{{touch.touchmove}}" bindtouchend="{{touch.touchend}}" data-width='{{width}}' > --> <!-- 读取数据 --> <view class="{{main.contains(item._id,hides)?'cakes cake-hide':'cakes'}}" wx:for="{{counters}}" wx:key="_id" id="{{main.ids(item._id)}}" bindtouchstart="{{touch.touchstart}}" bindtouchmove="{{touch.touchmove}}" bindtouchend="{{touch.touchend}}" data-width='{{width}}' > <!-- cake标签 --> <view class="{{(main.days_last(item.number) < 0)?'cake_left':'cake'}}"> <!-- 第一行title --> <view class="{{(main.days_last(item.number) < 0)?'cake_word_left':'cake_word'}}"> {{item.name}} </view> <!-- 第二行数字 --> <view wx:if="{{main.days_last(item.number) != 0}}" class="{{(main.days_last(item.number) < 0)?'cake_num_left':'cake_num'}}"> {{main.days_abs(main.days_last(item.number))}} </view> <view wx:else class="cake_num">TODAY</view> <!-- 第三行文字 --> <text wx:if="{{main.days_last(item.number) < 0}}" class="cake_word_left">Days Left</text> <text wx:elif="{{main.days_last(item.number) == 0}}" class="cake_word">is the Day!</text> <text wx:else class="cake_word">Days</text> </view> </view> ... |
counter.wxml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | const app = getApp() Page({ /** * * counters:返回的记日器记录 * openId:用户信息,用于区分不同ID的记日器 * hides[]:所有隐藏的卡片id * hidden:loading卡片是否隐藏 * current_id:目标卡片的id值 * width:屏幕宽度 * */ data: { counters: [], //另一种实现,所有id //ids:[], //隐藏的id hides:[], openId: '', //loading卡片是否隐藏 hidden: false, //屏幕宽度 width: 0, current_id:'' }, ... eat_cake(res) { //console.log("evid:" + JSON.stringify(res)) let ev_id = res.currentTarget.id var that = this wx.cloud.callFunction({ name: 'delTarget', data: { table: 'counters', id: ev_id.substring(1) }, success: res => { let hide = that.data.hides hide.push(ev_id.substring(1)) //延时隐藏目标卡片 setTimeout(function () { that.setData({ current_id : ev_id, hides:hide }) }, 100) //console.log('callFunction hides result: ', that.data.hides + ':' + that.data.ids) } }) }, ... }) |
counter.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | @import "font.wxss"; ... .cakes { position:relative; padding:5%; font-family: "Comic Sans MS"; align-items: center; left:0; } .cake-hide{ display:none; } ... |
counter.wxss
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 | var startX = 0 var startY = 0 var lastLeft = 0 var lastTop = 0 var currentId = "" var ins="" var width=0 //var selected=false //另一种实现2 //var c_id='' touchStart = function (event, instance) { //从dataset获取屏幕宽度 width = event.currentTarget.dataset.width //获取目标id并选中 currentId = event.currentTarget.id /* 另一种实现,保存c_id,隐藏下一个之前,隐藏上一个c_id,因为通过id变量指定隐藏class时,同时只能隐藏一个 c_id = event.currentTarget.dataset.cid if (c_id != '') { console.log("cid"+c_id) instance.selectComponent("#" + c_id).setStyle({ 'display': 'none' }) } */ ins = instance.selectComponent("#" + currentId) //获取当前坐标 var touch = event.touches[0] || event.changedTouches[0] startX = touch.pageX startY = touch.pageY } touchMove = function (event, instance) { var touch = event.touches[0] || event.changedTouches[0] //移动后目标位置:pageX-startX为手指移动距离,lastLeft为当前位置 var left = touch.pageX - startX + lastLeft var top = touch.pageY - startY + lastTop //保存触控位置x值 startX = touch.pageX //保存目标位置x值 lastLeft = left lastTop = top console.log("pageX:" + touch.pageX) /*** //根据手指移动位置移动目标view框,做容错处理 if (selected == true || Math.abs(lastLeft) > 6 * Math.abs(lastTop)) { //滑动超过1/5屏幕宽度,确定为被选中 if (selected == false && (width < 10 * Math.abs(lastLeft))) selected = true console.log("mov_left:" + left) ins.setStyle({ 'left': left + 'px' }) } //不是作删除操作的,卡片不滑动 else { console.log("left/top:" + lastLeft / lastTop) lastLeft = 0 lastTop = 0 } */ instance.selectComponent("#" + currentId).setStyle({ 'left': left + 'px', 'opacity': (1 - Math.abs(left / width))*0.9 }) console.log("mov_left:" + left) } touchEnd=function (event, instance) { var touch = event.touches[0] || event.changedTouches[0] endX = touch.pageX console.log("currentId:" + currentId) if (lastLeft < -0.45 * width) { console.log("end cake:" + currentId) //另一种实现,增加class //ins.addClass("cakes-eat") //instance.callMethod('eat_cake',event) instance.selectComponent("#" + currentId).setStyle({ 'transition': 'left ' + Math.abs(lastLeft / width) + 's,opacity ' + Math.abs(lastLeft / width) + 's', 'left': '-100%', 'opacity': '0' }) instance.callMethod('eat_cake', event) } else if (lastLeft > 0.6 * width) { instance.selectComponent("#" + currentId).setStyle({ 'transition': 'left ' + Math.abs(lastLeft / width) + 's,opacity ' + Math.abs(lastLeft / width) + 's', 'left': '100%', 'opacity': '0' }) instance.callMethod('eat_cake', event) } else { console.log("lastledft:" + lastLeft) instance.selectComponent("#" + currentId).setStyle({ 'transition':'left 0.5s', 'left': '0' }) } lastLeft = 0 startX = 0 //elected = false console.log("lastleft:" + lastLeft) console.log("startx:" + startX) } module.exports = { touchstart: touchStart, touchmove: touchMove, touchend: touchEnd } |
move_cake.wxs(处理触控事件的函数)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | function daysLast(num) { var mday = getDate(); mday.setFullYear(parseInt(num.substring(0, 4))); mday.setMonth(parseInt(num.substring(5, 7)) - 1); mday.setDate(parseInt(num.substring(8))); var now = getDate(); return Math.floor((now.getTime() - mday.getTime()) / (24 * 60 * 60 * 1000)); } function daysAbs(num) { return Math.abs(num); } function idS(num) { return "A" + num } function isContain(id,hides){ return (hides.indexOf(id) != -1)?true:false } module.exports = { days_last: daysLast, days_abs: daysAbs, ids: idS, contains:isContain }; |
main.wxs(处理数据的函数)
因为需要屏幕的宽度来判断卡片滑动的距离是否到达执行删除操作的位置,卡片的属性中增加了一个data-width,从js中调用API,获取到屏幕宽度,存入data-width,在wxs中读取使用。这也是wxml和html处理逻辑的不同之处,wxml中的运算操作,尤其是三元运算非常重要,可以通过判断条件针对单个元素进行操作,对操作从数据库批量读出的数据非常有用。
有一个需要注意的坑是id属性的开头不能是数字,否则wxs会select不到,所以页面上的id要稍微做一下处理,增加一个'A'打头。
因为display没有transition属性,设置后会立即生效,跳过渐变动画,所以需要单独设置,通过对wxml中hides的延时数据绑定实现。
另外在网上看到说要用''不要用""的说法,因为对js了解不深,不清楚具体有什么区别。
还有一个奇怪的点是wxs文件似乎不支持let关键字
尝试了上下翻页期间不让卡片左右滑动的功能实现,体验并不好,没有将这些代码加进小程序里
font.wxss是转成了base64的字体文件
====================接下来是彩蛋的总结w=======================
小程序中设置了一个彩蛋,长按“+”号触发,是我们网站中时空旅行地图的小程序实现,因为小程序端的地图不能插入网页,所以在小程序中网页地址被替换成了图片地址,点击地图标记会出现日期和图片。
实现地图功能之前,需要做两个准备活动,一是申请腾讯位置服务,二是申请符合苹果ATS审核的https证书,前者直接按照流程指引申请,定义好喜欢的地图样式,获取对应的KEY。后者可以直接申请阿里云的赛门铁克个人免费版证书,限一个域名,按照阿里的文档说明在远端服务器端增加https支持,然后在小程序的开发设置中将网址加进去,这样小程序就可以读取远程服务器的图片。
核心功能点是点击加载图片,比较简单就直接贴代码了。
1 2 3 4 5 6 7 8 9 10 11 12 13 | <wxs src="main.wxs" module="main" ></wxs> <map scale="5" subkey="L7ABZ-5MREU-DSTVP-BIV3T-KHXYH-J7FN6" markers="{{main.set_markers(footprints)}}" bindmarkertap="markertap" > <covers wx:if="{{pic_addr==''}}" ></covers> <covers wx:else class="cover"> <cover-view class="card" > <cover-image class="photo" src="{{pic_addr}}" bindtap="cardtap"></cover-image> </cover-view> </covers> </map> |
map.wxml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | const app = getApp() Page({ /** * 页面的初始数据 * footprints:地图标记数据 * pic_add:照片地址 */ data: { footprints: [], pic_addr: '' }, //地图标记点击事件,点击后显示对应的图片 markertap(res) { this.data.pic_addr = this.data.footprints[res.markerId].add; console.log(this.data.pic_addr); this.setData({ pic_addr: this.data.pic_addr }) }, //照片点击事件,点击后关闭 cardtap(res) { this.setData({ pic_addr: '' }) }, ... }) |
map.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | function set_markers(fp) { var markers = []; for (var key = 0; key < fp.length; key++) { markers[key] = { 'id': key, 'iconPath': "../../images/marker.png", 'latitude': fp[key].latitude, 'longitude': fp[key].longitude, 'width':20, 'height':20, 'callout':{ 'content':fp[key].date, 'fontSize':15, 'bgColor': '#FB9966', 'color': '#FFFFFF', 'textAlign':'center', 'borderRadius':5, 'padding':10 } } } return markers; } module.exports = { set_markers: set_markers, }; |
main.wxs
需要注意的是在小程序的map中,只允许使用cover-view和cover-image标签,其他的即使打上也是显示不出来的,这两个标签的功能有限,更高级的web-view权限需要企业申请,大概是不希望个人用户开发出什么翻墙之类的功能w
=======================最后是云开发的小结w==================
腾讯对小程序个人开发者比较友好,云开发控制台、代码仓库直接整合进开发工具,提供相应的存储空间和一定程度的云函数算力,省去很多额外的成本,开发者只管开心地写代码就可以了。
后台方面,腾讯提供了极简的认证和权限管理,常用场景的数据库访问方式,在不授权的情况下也可以获取用户的openID,使用简单的JSON数据库,把后台工作尽可能的压缩了。
对云函数来说,最需要注意的问题大概是异步问题,数据库中的数据返回有100个结果的上限,所以在比如地图标记点这种数据较多的情况下,需要多次取出后一起返回。本质上云函数返回的是一个Promise对象,async、await是不可缺少的。另外因为云函数是异步返回的,所以在程序初始化的时候,遇到了Page初始化成功后还获取不到openID的情况OTZ,用判定后再次召唤openID的方式解决了。因为对ES6不太熟悉,还要继续学习。
参考资料:
为苹果ATS和微信小程序搭建 Nginx + HTTPS 服务
使用css transition属性实现一个带动画显隐的微信小程序部件
评论