我们之前的demo中,页面都是很简单的一个主页面的结构。没有底部菜单,没在意页面的结构。而我们平时接触的app都有各种各样的页面交织组合在一起,所以这一篇文章中,我们开始来关注我们的页面结构。
先来看看我们本文相关的demo,地址:https://github.com/jsongo/weApp-frame,三个页面:
为了展示页面的主框架,这里做了三种风格的页面。第一个是个图片列表,从花瓣上抓的。第二个是个外卖的页面。第三个是个人中心页面。
一、页面功能简述
1、花瓣图片列表
用两列的瀑布流来展示,在顶部向下拉可以加载更多,滑动到底部可以加载更多图片。在网络请求数据的时候,会出现一个浮层,展示加载中的loading图标,顶部的title旁边也会出来加载中图标,等数据加载完,这两个图标都会消失。
这个页面还有个子页面,图片查看页,往后可以滑动到下一张,往前可以滑动到前一张。
列表数据来源:分析了下花瓣的网站,随便拿了个数据列表接口,通过传入不同参数来取得分页数据。
本页主要介绍:列表的排布、加载中图标的实现方式、下拉刷新的实现方式、滑动到底部加载更多的实现、navigator及参数传递的实现、引用传递的实现。
2、外卖订餐页面
页面顶部是个轮播图,几个图片轮流播放。下半部分是两个滑动的列表,菜品分类列表,和菜品详细列表,它们可以分开滑动。
本页主要介绍:轮播图组件、页面分区及各自列表滑动。
3、个人中心页面
上半部分是个人信息,下方是菜单列表。退出按钮点击后,可以从下方弹出菜单。
本页主要介绍:模态窗口、下文弹出菜单列表、toast
另外,还有下方tab菜单的实现方式。
二、图片列表的实现
1、底部加载更多
用户滑动到底部的时候,列表会自动加载下一页。
实现也比较简单,在scroll-view标签中的bindscrolltolower绑定个事件响应函数,当滑动到底部时,这个事件就会被触发。
不过这里有个问题要注意下,scroll-view要设置一个高度才行,要不然经常会滑到底部没反应,没触发。获取窗口的高度,把它设置给scroll-view就可以了:
wx.getSystemInfo({ success: ( res ) => { // 用这种方法调用,this指向Page this.setData({ winH: res.windowHeight }); } });
在界面中,把winH设置给scroll-view的height
这样scroll-view才能正常在滑动到底部的时候触发bindscrolltolower事件。加载完数据后,可以用之前我们在《【微信小程序开发?系列文章三】数据层》里讲过方法来追加数据。下面我们再介绍一种方法来追加数据。
追加的时候,我们的主要目的是保证data里的items数组的地址不变。所以其实可以直接拿到这个items数组的引用,然后给它push数据,方法如下:
var arr = this.data.items; arr.push(…newList); // ...三个点是用了es6的解包的语法 this.setData({items: arr}); // 一定要记得setData
这个方法比较hack,先用arr变量来保存this.data.items的引用,push操作不会改变原数组的地址,push完之后,还没更新界面,这时一定要记得再调用一下setData方法,用它来触发界面更新。所以这里,我们其实是把setData当然是UI update的方法来用。亲测了一下,整个列表没看到有全局刷新的问题。
2、下拉刷新
微信小程序提供了下拉刷新的样式。这个我们能修改的不多,只能配置内容和简单的修改几个地方的显示。
(1)配置
我们的程序中有一个app.json,在“window” 这一项里添加 enablePullDownRefresh: true ,小程序的页面在顶部下拉的时候,就会出现下拉刷新的样式。
(2)事件
当下拉被触发的时候,MINA框架会去找当前页面的page里是否有onPullDownRefresh这个事件响应函数,如果有就会调用。
所以这里在实现下拉刷新的时候,就要定义这个函数,然后在里面去做刷新。
(3)刷新的思路
当前列表数据可能不是最新的,如果这时重新去拉一次第一页的数据,可能会出现前面几项不在当前列表里,这些就是要刷新进来的数据。思路就是去比对id,先取原列表中第一个id,如果在列表里找到这个id的项,则停止,把这个项之前的数据都追回到列表里。但是如果在新加载的第一页中没找到这个id,说明这次加载的数量还不够,还要再加载一次。为了避免出bug时,或数据出问题的时候,无限加载的问题,我们设置一个MAX_PREPEND来限制最多加载的页数。
我们用数字来代表列表里的一项,简化下模型,让读者更容易理解。
比如我们第一次加载的列表是[0,1,2,3,4,5,6,7,8,9] 这10项(一次加载10项),第一个id是0。过了5个小时,列表数据有更新了,这时用户下拉刷新,我们拉到了[-28,-27,-26,-25,-24,-23,-22,-21,-20,-19] 这10项,这时我们发现,我们这次加载的新数据没有第一个id,即0,所以中间肯定还有一些项我们要再加载的。所以我们发了第二个请求,这次拉到了[-18,-17,-16,-15,-14,-13,-12,-11,-10,-9],这里又没有出现0,所以发起第三个请求去拉数据,这时我们拿到了[-8,-7,-6,-5,-4,-3,-2,-1,0,1],这次0这个id出现了。所以我们把0前面的8项也全都加入到新列表中。为了安全,我们设置了MAX_PREPEND,比如为3,表示最多发三个请求,如果这中个请求还没有拉到0这个id,那我们也不再发请求了。怕出现无限循环加载的情况,出现bug的时候可能会出现,或者后台把0这条数据删除了,后面这几次请求返回的列表里都不可能出现0,这时也会出现无限加载的情况。
当然上面这个逻辑还是有bug,假如真出现了后台把0这个项删除掉的时候,那我们加载的数据就可能会出现数据重复的现象。所以真正产品化的时候,如果有可能删除数据的情况,最好还是不要用拿第一个id的去对比的方法,因为可能会出现重复。这时要使用另一种算法,即,可以把所有的id都存到一个数组里,新数据拉回来的时候,一个个拿去数组里对比,没在数组中的就加载到界面中,有的话就忽略。然后出现全部项都在这个数组中的时候,就停止加载。有点复杂。
其实如果把这个逻辑放到后台去做,会简单很多,我们让id有序,有大小可以比较,或者用时间戳的方式来判断,前端做有点复杂。我们这个demo中能拿到的数据id也无序,时间也无序,情况远比上面的数字复杂很多,所以没办法只能暂时这么做。读者如果是自己的团队做后台的话,一定要把返回的数据按一定的规则有序化。
现在很多产品化的普遍做法是另一种思路,就是在刷新的时候,都只拉一页,如果还没全部拉取完,则在新页跟原列表数据中间,显示一个类似“查看更多”等的提示,点击的时候,再去拉取第二页、第三页等等。它的实现相对上面的思路会简单一些。
接下去讨论数据如果插入到界面中列表的前端去。
前面讨论追加列表数据的时候,说到了一种方法,取数组的引用,在保证数组地址不变的情况下,去往里面添加数据。push是一个不会改变地址的操作,不过是往数组后面添加的,那么要往前端插入,就要用unshift,它也会在保证数组地址不变的情况下,在前面添加新数据。
(4)停止刷新的样式
当我们加载完数据,刷新完的时候,要把微信帮我们做的加载图标隐藏掉,它自己不会正好在我们加载完数据的时候隐藏,虽然它有一个默认的隐藏时间。
框架给我们提供了一个wx.stopPullDownRefresh方法,我们在加载完数据,render完时,调用一下它就可以了。
(5)数据请求
关于数据请求,这里补充一点。
花瓣对请求做了点限制,直接请求地址会返回一个页面,需要加两个头部才行:
X-Request: JSON X-Requested-With: XMLHttpRequest
而第二个头部,X-Requested-With,框架默认已经帮我们加了。我们只要在请求的时候,加上一个第一个header就行了。
另外,小程序开发工具一开始的时候,有一个头部重复的问题,不过从v0.10.101000开始,就修复了。
3、图片列表
图片列表其实就是传统网页的布局,没有用到flex,只是让元素浮动float,然后限制一下宽度和高度,它自然就排成两行了。有兴趣的读者可以尝试用flex或table去实现一下。
4、图片播放的实现
官方其实提供了一个图片预览的api,可以用来实现图片的播放,实现也很简单:
wx.previewImage({ current: '', // 当前显示图片的http链接 urls: [ ] // 需要预览的图片http链接列表 });
在urls里传入图片列表就可以了,不过这个功能比较简单,没法定制,比如想在图片播放的时候,能显示些图片说明文字,这个功能就没法做到了。所以下面我们来讨论下如何实现自己定义图片播放的功能。
(1)点击跳转
列表里的每一项都加了navigator组件包裹着,它就相当于标签,用来做跳转。
不过要注意的一点是,navigator的宽高都被框架设置成auto,所以在wxss里设置宽高没有用,但在元素里内嵌样式是可以的,这里我们换种做法。在navigator里放个view把它撑开,给它固定个宽高就可以了,这时对navigator里面元素的点击,事件都会冒泡到navigator元素上,就会触发它的页面跳转。
当父元素被设置成 display:flex 时,加上几个center就可以让其内部元素自然水平和垂直居中:
display:flex; justify-content:center; align-items: center; text-align: center;
所以我们也把swiper组件的高度设置成整个窗口的高度,再用上面的方法把里的image元素自然居中就可以了。image元素的mode设置成aspectFit,它会保持原有的宽高比例,image只有这个mode值是保持图片完整的。更多mode属性可以看官方文档。
另外,图片播放页面是第二层弹出页面,没有底部菜单,这样就可以全屏。
5、loading样式的实现
微信小程序提供了两种loading的样式,一种是在顶部bar的标题旁边显示转动的图标,另一种是用浮层的形式在页面中间显示一个加载图标。
第一种的实现也比较简单
显示:
wx.showNavigationBarLoading
wx.hideNavigationBarLoading
wx.showToast({ title: '加载中...', icon: 'loading', duration: 10000 });
{{item.id==chosenType?'selected':''}}
整体布局其实就是wxss完成的,也没什么可讲。这一部分主要讨论:模态窗口、下文弹出菜单列表、toast的实现。
wx.showModal({ title: '关于', content: '这是一个演示程序,不要在意这些细节' });
先看几个参数:
title设置上面的标题
content就是对话框中部灰色的内容
(1)alert框
添加一个字段:showCancel,设置成false就可以了
success: function(res) { if (res.confirm === 1) { console.log('用户点击了确认按钮'); } else { console.log('用户点击了取消按钮'); } }
(4)modal的显示与隐藏
modal弹出框默认的就可以通过“取消”按钮来隐藏,暂还没有提供其它方式。
默认样式,上面有个打钩图标,目前微信没有提供定制的属性。如果要显示打叉x的图标,恐怕还没法用。期待后面会更新。
wx.showToast({ title: '注销成功!', icon: 'success', duration: 3000 });
3、action-sheet
action-sheet就是下文往上弹出来的小菜单,实现方式也比较简单:
wx.showActionSheet({ itemList: ['确定注销'], success: (res)=> { if (!res.cancel) { if (res.tapIndex == 0) { // 确定注销 this.doLogout(); } } } });
{ "navigationBarTitleText": "图片详情", "enablePullDownRefresh": false }
2、demo中的下文菜单
这个是在app.json里设置的,跟window并列,添加一个tabBar:
"tabBar": { "list": [{ "pagePath": "pages/index/index", "iconPath": "res/img/tab/home-off.png", "text": "首页", "selectedIconPath": "res/img/tab/home-on.png" }, { "pagePath": "pages/waimai/waimai", "iconPath": "res/img/tab/wm-off.png", "text": "外卖", "selectedIconPath": "res/img/tab/wm-on.png" }, { "pagePath": "pages/my/my", "iconPath": "res/img/tab/my-off.png", "text": "我的", "selectedIconPath": "res/img/tab/my-on.png" }], "color":"#174578", "selectdColor":"#3cc51f", "borderStyle":"white", "backgroundColor":"#116fb6" },