我们之前的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元素上,就会触发它的页面跳转。
(2)播放哪张图?
很简单,在跳转的时候,给跳转的地址加个参数就可以了,参数带上点击的这张图在数组中的index。那么问题来了,在跳过去的页面怎么取得这个参数?
微信小程序官方文档里没有特别提到onLoad里,其实还可以传入一个参数options,它用来接收跳转时地址后面带的参数。这个就非常方便我们使用了。比如地址是 ../pic/pic?index=1,那么这里,我们用options.index就可以拿到1这个数据了。
onLoad拿到index数据后,setData来修改并指定当前图片列表播放这个index的图片就行了。
(3)播放的图片数据来源
播放图片,我们不是一张一张来显示的,因为如果只播放一张的话,用户在向左或向右滑动的时候,我们得去找这张图片的上一张图或下一张图来显示,这样做就比较复杂了。
所以我们在界面上放一个swiper组件,把当前列表里的所有图片全部设置到里面去。上面我们拿到的index,在这里就可以使用,swiper组件有个current属性用来指定当前显示哪一张图。
这个列表数据,我们不需要从index页面重新拷贝一份过来。数组,其实存一个引用就行了,这样的两个好处是:1节约内存,2更新的时候更新一个地方的数据就行。实现方式是,在index的onLoad方法里,把this.data.items的引用存到app的globalData里,再在图片播放页的onLoad方法里从app取出这个引用,把它设置给这个页面的data里就行。当前为了界面及时更新,我们把这个操作放到播放页面的onShow里好些,这样,每天切换到播放页来查看图片的时候,都会去更新图片列表,保证数据是最新的。
(4)播放页界面的实现
先加一个view组件,设置成整个页面大小,背景设置成顶部bar的颜色,这样看起来比较像一个整体。
上面提到,我们的图片是显示在swiper组件里的,因为这是一个可左右滑动的组件,不用我们自己去做滑动的事件处理。不过这个组件有个问题,它的高度也是在框架中固定死的,固定成150px,我们在wxss里改变它的值也没有作用。还好,在标签里内嵌样式可以把框架中定义的样式给覆盖掉。那么为了让图片垂直居中,我们第一想到的可能是,取到图片高度,把它设置给swiper的内嵌样式的height里,再用margin-top等方式让它居中。这里介绍另一种方式让图片居中。
当父元素被设置成 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
});
三、外卖页面的实现
1、页面布局
外卖页面基本可以分为三部分,顶部轮播图组件、下半部分是两个横排的滑动列表,他们可以各自滑动。
轮播组件可查看官方文档介绍。几个关键的参数,autoplay设置是否自动播放,current设置当前播放第几张,indicator-dots设置是否显示面板上的指示点。在上面的图片播放页中,我们把autoplay和indicator-dots都设置成false。注意这些true/false都要写成 false 的形式。
在这个页面中,我们模仿美团外卖中的首页,自动轮播顶部的广告图片。除了autoplay和indicator-dots设置成true外,还可以修改interval参数来设置等多久再播放下一个,修改duration来设置滑动的时长。
2、页面分区
主要是用wxss来控制,这里依然用flex简单的布局方式,按1:5的比例来放置两边的滚动区域。
左边的列表,给每个项都添加一个事件,每次点击某个项的时候,它的底色要变。这里我们通过控制class来改变样式,在class里添加:
{{item.id==chosenType?'selected':''}}
当选中的项的id为choseType时,class就添加selected,这样,我们就可以通过choseType这个变量来改变列表选中的项的样式。
那怎么实现两个列表数据的联动呢?
我们在每个项中也埋下了data-id,点击的时候就知道当前项的id,再拿它去请求相关的列表数据,从而可以拿到右侧的餐品列表数据,再更新到右侧列表上去就可以了。
每个列表都是独立的,可以各自设置scroll-y让他们各自滚动,互不影响。
四、个人中心页面的实现
整体布局其实就是wxss完成的,也没什么可讲。这一部分主要讨论:模态窗口、下文弹出菜单列表、toast的实现。
1、模态窗口
其实也就是我们网页中的alert、confirm这些弹出框。当他们弹出来的时候,后面的窗口是不可以操作的,这类窗口称为模态窗口。
小程序框架提供了一个wx.showModal,我们可以用它来实现alert / confirm / prompt。
wx.showModal({
title: '关于',
content: '这是一个演示程序,不要在意这些细节'
});
先看几个参数:
title设置上面的标题
content就是对话框中部灰色的内容
(1)alert框
添加一个字段:showCancel,设置成false就可以了
(2)confirm框
默认的就是confirm的样式,可以用cancelText字段设置取消文本,cancelColor设置它的字体颜色,confirmText设置确认文本,confirmColor设置它的字体颜色。
(3)prompt弹出输入框
modal框中间的内容是允许插入wxml标签的,所以我们如果插入一个input组件,它自然就成了prompt弹出框,如果需要用户输入的时候,可以用这个办法弹出。
用success得到用户点击了哪个按钮,如果是确认按钮的时候,success回调里通过参数拿到:
success: function(res) {
if (res.confirm === 1) {
console.log('用户点击了确认按钮');
}
else {
console.log('用户点击了取消按钮');
}
}
(4)modal的显示与隐藏
modal弹出框默认的就可以通过“取消”按钮来隐藏,暂还没有提供其它方式。
2、toast
默认样式,上面有个打钩图标,目前微信没有提供定制的属性。如果要显示打叉x的图标,恐怕还没法用。期待后面会更新。
wx.showToast({
title: '注销成功!',
icon: 'success',
duration: 3000
});
我们demo中用toast来提示注销成功。
3、action-sheet
action-sheet就是下文往上弹出来的小菜单,实现方式也比较简单:
wx.showActionSheet({
itemList: ['确定注销'],
success: (res)=> {
if (!res.cancel) {
if (res.tapIndex == 0) { // 确定注销
this.doLogout();
}
}
}
});
itemList用来设置上面的菜单列表,从左往右,从0开始编号,从上往下显示,最下页的是取消按钮,它不用在itemList里面写,默认会有。也可以通过cancel这个字段来去掉空上取消按钮。
取消按钮默认会有点击事件,点击会把整个actionSheet隐藏,点击半透明灰黑的地方也可以把它隐藏,这些都是这个组件默认帮我们做好的。
组件里success用来做点击的事件响应。如果用户点击取消按钮的话,res.cancel就为true,点击其它按钮则会通过res.tapIndex通知我们 用户点击的是哪个编号的按钮,从而我们可以根据它来做出相应的响应。
五、其它
1、各页面标题
每个页面有不同的标题。在用v0.10.101400版本开发的时候,测试wx.setNavigationBarTitle方法设置的标题没有作用,会被改回去。不过v0.10.102800版本修复了这个问题。
如果wx.setNavigationBarTitle方法失效的话,还有一个办法可以改标题,在xxx.json里设置新的title就可以把原来的覆盖。
demo代码中,除index页面外,其它页面都有一个xxx.json文件,内容大致如下:
{
"navigationBarTitleText": "图片详情",
"enablePullDownRefresh": false
}
第一个设置标题,第二个enablePullDownRefresh的作用是,在不需要下拉刷新的页面中,覆盖window中的这个参数,让它下拉不了。
这个方法还可以用来覆盖其它参数,如页面背景,字体颜色等等。
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"
},
list定义tab信息,只能配置最少2个、最多5个。每个tab可以用pagePath指定页面,用iconPath指定选中前的图标,selectedIconPath指定选中后的图标,text指定tab名称。
样式的设置,跟list并列,用color设置文字颜色,用selectdColor设置选中时的文字颜色,borderStyle设置顶部边框颜色,而且只支持black/white,backgroundColor设置tab背景色。
子菜单还不支持。
新开了个小程序技术交流群,有什么疑问可以加到群里提问: