前言
不知道有多少人跟我一样爱上“知乎”的呢?一直喜欢“知乎”所带来的用户体验效果和一些新颖的信息传送。所以在最近的小程序学习中,开始的第一个项目实训,就是“知乎”了。
开始做了才知道,“知乎”小程序的工程量之大,我这个前端新手不知应该需要多少个工作日,才能把“知乎”完整做成小程序。不过程序员的路本就是边学习边成长的,学无止境。小人不才,最近做了一些,忍不住先开始一波经验和问题的分享了。
“知乎”小程序开跑
成果分享
一、首页
1. tab栏切换
首页由三个tab项组成,”关注”,“推荐”,“热榜”,这几个页面的切换功能分别设置了“点击切换”和“滑屏切换”:
刚开始做点击事件的时候一股脑门的简单劲,简单粗暴,直接给三个tab设置data-index,然后判断每次事件触发取到的index值,然后转到相应内容,”if…elseif…else…”,如果有很多个tab怎么办?if…到何年何月啊~喔得天!就觉得自己太low了,一直觉得能以最短的代码写最好的功能就很让人敬佩,好吧我还是尝试了一下的:
home.js部分代码:
switchTab (e) {//tab点击事件
let index = parseInt(e.target.dataset.index)
this.setData({
currentIndex: index
})
},
handleChangeTab (e) {//tab滑屏事件
const p = 33.3
this.setData({
lineStyle: `left: ${p * e.detail.current}%`
})
}
直接将index值parseInt,赋值到每次转到的当前tab的index值:currentIndex,数值的比较相对于字符串的比较就轻松许多了不是吗?
而后滑屏事件里自动会引用点击事件中的结果,这个时候不得不又为parseInt点个赞,我只需要将那条“选中线”在每次触发该事件时自动乘当前的currentIndex,得到它在不同当前tab下的位置值。
没错就没用if…else!用烦了,还好还有点余地让我换口味,没办法的时候该low还不是得low,这个时候就得安慰自己是‘走走基层’体验生活了hhhhh
2. 图片占位问题
在进行首页的三个tab页时,都存在一个问题,页面中有些分享的文中,插入了图片,而有些则没有插入。但在我们的代码中,一般初始都是加了这个图片元素的,至于数据中加不加入图片,是发文用户自己的事儿了吧。
但是呢,小程序起步,我就遇到这样一个问题,不管有没有加入图片,页面上仍然会给这个图片元素留了空白占位,导致其它内容全部被挤下去了。
所以我就在想:怎么能让这里有图片就显示,没图片就不留占位呢?
解决:
wx:if => 条件渲染
加入wx:if的元素,页面会自动判断是否该渲染该元素所包含的代码块,wx:if:”{{绑定的数据}}”,绑定的数据有值时渲染,无值时不渲染。
3. 热榜:简便进行多列布局
看到热榜页面的样式就感觉它就像个多行三列的网格,所以撇开了繁琐的页面设计与标签类名间的位置样式值设置,果断用了栅格布局:
home.wxml部分代码:
<view class="container-item" wx:for="{{hotList}}" wx:key="{{item.id}}">
<view class="row">
<view class="col">
<view class="col-1">{{item.rank}}</view>
<view class="col-7">
<view class="title">{{item.title}}</view>
<view class="status">{{item.status}}</view>
</view>
<view class="col-4">
<image wx:if="{{item.titleImage}}" class="title-image" src="{{item.titleImage}}"></image>
</view>
</view>
</view>
</view>
当然,小程序暂时还没有col-number的固定值,所以直接设置类名并不会发生变化,没办法直接像使用api一样直接使用,所以,就在app.wxss全局样式中定义了每个col块的宽度:
.col>.col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12{
overflow: hidden;
}
.col-1{
width: 8.33333333333333%;
}
.col-2{
width: 16.66666666666666%;
}
.col-3{
width: 25%;
}
.col-4{
width: 33.33333333333333%;
}
.col-5{
width: 41.66666666666666%;
}
.col-6{
width: 50%;
}
.col-7{
width: 58.33333333333333%;
}
.col-8{
width: 66.66666666666666%;
}
.col-9{
width: 75%;
}
.col-10{
width: 83.33333333333333%;
}
.col-11{
width: 91.66666666666666%;
}
.col-12{
width: 100%;
}
这样后面这个程序中若还想再建这种布局就能直接简便地引用了。
当然,这个页面中,在设置这个之前,还少不了flex布局的实现,“弹性布局”真是我这段时间学习css中最喜欢的东西了。相关知识大家可以看看阮一峰老师的文章。
二、“想法”页
1.header栏fix及滚动fix功能
该界面完成了一个☝️个人觉得平时刷知乎的时候比较妙的用户体验,:头部nav在下滑到一定距离时仍然fix其主要“功能转到点”在头部,如果用户在下滑到比较长距离的时候想要去到头部的导航,不用重新上滑很长的距离到顶部去实现这个动作。
先看一波效果:
前端框架semantic ui
在这方面的效果就做得好棒,看人家主页semantic ui。
所以在做这个项目的时候特意也挑了这个效果去实现。
作为一个前端初学者,还是得依靠我们的“主角”scroll-view,
配置项 | 作用 |
---|---|
scroll-top | 设置竖向滚动条位置 |
scroll-y | 允许纵向滚动 |
bindscroll | 滚动时触发的回调函数 |
我在这里用了scroll事件进行两个小页面(也就是页面滚动前与滚动到 一定距离显示市显示的不同top nav顶部导航条)的切换,配合小程序的”wx:if else “框架进行条件渲染,当页面滚动到设置的目标距离时,切换新生成的顶部导航。
thought.wxml部分代码:
切换前的nav:
<view wx:if="{{!display}}" class="nav">
<view class="title">想法</view>
<view class="message">
<image class="messageImg" src="{{messageImg}}" wx:if="{{messageImg}}"></image>
<text class="messageTitle" >消息</text>
</view>
<view class="myThought">
<image class="thoughtImg" src="{{thoughtImg}}" wx:if="{{thoughtImg}}"></image>
<text class="thoughtTitle">我的想法</text>
</view>
</view>
切换后的nav:
<view wx:else class="nav1">
<view class="title">想法</view>
<view class="message">
<image class="messageImg" src="{{messageImg}}" wx:if="{{messageImg}}"></image>
</view>
<view class="myThought">
<image class="thoughtImg" src="{{thoughtImg}}" wx:if="{{thoughtImg}}"></image>
</view>
</view>
分别给两个nav设置不同样式。
scroll事件触发nav转换:
Scroll事件接收两个参数:
1.scroll-top;
2.display属性
scroll: function(e){
if (e.detail.scrollTop > 200) {
this.setData({
display: true
})
} else {
this.setData({
display: false
})
}
}
在这个地方就有几个开始动手时就踩到的坑:
1.使用竖向滚动条时必须为组件设置一个固定高度,否则bindscroll事件无法触发;
2.scroll-top在设置竖向滚动条位置时,如果设置的值没有变化,组件不会渲染!
这种功能呢,用我们老师的话来说… 没错,就是很有“质感”!
2 .轮播内容
h5写过轮播的都只有写过的知道,相对还是比较麻烦的,并没有一个轮播图组件,有个ViewPage也需要自己定制,小程序中swiper组件封装的相对还是方便的,使用方式也相对容易些。
主要属性:
配置项 | 作用 |
---|---|
indicator-dots | 是否显示面板指示点 |
autoplay | 是否自动切换 |
current | 当前所在页面的index |
interval | 自动切换时间间隔 |
duration | 滑动动画时长 |
bindchange | current改变时触发change事件 |
属性只需要设置就行了 也可以抽到js文件的data中进行数据绑定,监听使用bindchange,在js中做业务处理。
wxml代码块:
<swiper class="swiper" autoplay="true" interval="2000" duration="1000" circular="true" >
<block wx:for="{{discussion}}" wx:for-index="index">
<swiper-item class="discuss">
<image wx:if="{{item.url}}" src="{{item.url}}" class="swiper-url"/>
<view class="discuss-desc">
<view class="discussing">{{item.discussing}}</view>
<view class="desc-title">{{item.title}}</view>
<view class="desc-question"> {{item.descQuestion}}
<!-- <swiper class="desc-question" autoplay="true" interval="3000" duration="1000" vertical="true">
<block wx:for="{{item.descQuestion}}" wx:if="{{item.descQuestion}}">
<swiper-item class="question">
<view class="question-item">{{item.questionItem}}</view>
</swiper-item>
</block>
</swiper> -->
</view>
</view>
</swiper-item>
</block>
</swiper>
其实这个页面里的轮播中,还嵌套了其中一个项的纵向轮播,刚开始我直接在swiper中另嵌套了一个swiper,然后设置verticle=“true”,但只能将外层swiper的轮播切换时间增加,才能给里面的纵向轮播预留时间显示出来,所以体验效果并不好,有知道怎么做的可以传授下一下(๑`・ᴗ・´๑)ヾ(●´∇`●)ノ哇~。
三、搜索页
1.“回车确定”搜索内容
每次输入搜索内容后,点击回车确定,转到详情页,搜索历史栏也随之新增一条新记录,同时将记录保存到本地缓存,页面刷新之后仍然存在历史记录。
这里用到了input组件的bindconfirm事件,也就是每次输入搜索内容回车确定时触发的事件,这里我用wx:for循环设置了一个historyRecord数组,该数组接收两个参数,每次输入内容的id值和recordItem内容,key值id区分不同一行不同内容的历史记录,recordItem是输出在搜索历史栏的记录value:
search.wxml
<view class="search-history">
<text class="zhhs">搜索历史</text>
<view class="search-history-item" wx:for="{{historyRecord}}" wx:key="item.id">
<image class="search-history-icon" src="/assets/icons/shizhong.png"></image>
<text>{{item.recordItem}}</text>
</view>
</view>
在进行bindconfirm事件处理上,刚开始就生生踩雷了,刚开始学小程序都得了解的事,我就犯了错,我们都应该知道,要改变data里的数据,只能用setData,刚开始我直接在外部使用了this.data.historyRecord.push({id:’’,recordItem:e.detail.value});
然后就报错显示没有push这个方法,然后我当然就去找度娘问清楚啦,被提示data里的数据只能用setData改变,然后一敲脑袋,最后就是下面的样子了:
search.is
bindconfirm: function(e){
console.log(e);
var historyRecord = this.data.historyRecord;
var recordItem = e.detail.value;
historyRecord.unshift({
id:'0',
recordItem: recordItem
});
this.setData({
historyRecord:historyRecord
});
}
要使用setData之外的方法,就要借用变量来赋值啦,将数组赋到外部定义的变量,就可以使用setdata之外的方法了。
而该变量本身就是数组,数组的方法之多,够用的了!起先我用的是push()方法,后来又去摸了下知乎搜索页,发现历史记录的最新记录都是直接插入首行,好的,数组方法unshift()满足你!
2.“点击搜索结果”搜索内容
每次输入搜索内容后,出现搜索结果条页,点击结果条,转到详情页,搜索历史栏也随之新增一条新记录。
这里用到了模糊查询,关键字查询,利用了数组的filter过滤方法,遍历数据库(我这里暂时用了假数据,后期掌握后端方法之后补上),选出包含输入词的相关结果,罗列在结果条目上供选择。
Search.wxml部分代码
<view wx:else class="search-like">
<view class="search-like-item" data-param="{{item.text}}" wx:for="{{searchLikeList}}" wx:key="{{index}}" bindtap="turnTo">
<image class="search-like-icon" src="/assets/icons/sousuo.png"></image>
<text>{{item.text}}</text>
<image data-index="{{index}}" class="turn" src="/assets/icons/turn.png"></image>
</view>
</view>
Search.js部分代码
changeSearch (e) {
let value = e.detail.value
if (value === '') {
this.setData({
haveSerachLike: false
})
return
}
let arr = this.data.searchLikeAllList.filter(item => item.text.indexOf(value) > -1)
console.log(arr)
this.setData({
haveSerachLike: true,
searchLikeList: arr,
})
},
turnTo: function(e){
this.saveHistory({
id: 0,
recordItem: e.target.dataset.param
})
wx.navigateTo({
url: '../searchDetail/searchDetail'
})
},
选择结果条目上的某条内容后,点击进入详情页,同样,在历史记录中新增一条记录内容,记录保存在本地缓存中。
3.清除搜索记录
效果图:
既然有了保存记录功能,当然也少不了清楚搜索记录的功能。
感觉自己抱紧了数组方法的大腿,这里再次用到过滤filter,先给要赋予清除事件的元素通过设置data - index 的方法来标识要传递的值,然后在deleteRecord事件中将历史记录中被该事件选中的index值进行过滤,即删除所传递的index值,然后返回过滤后的数组,即没被过滤掉的记录。
Search.wxml部分代码:
<view class="search-history">
<text class="zhhs">搜索历史</text>
<view class="search-history-item" wx:for="{{historyRecord}}" wx:key="{{index}}">
<image class="search-history-icon" src="/assets/icons/shizhong.png"></image>
<text>{{item.recordItem}}</text>
<image data-index="{{index}}" class="delete" src="/assets/icons/delete.png" bindtap="deleteRecord"></image>
</view>
search.js部分代码:
deleteRecord: function(e){
console.log(e);
let filterArr = this.data.historyRecord.filter((item, index) => {
return index !== e.target.dataset.index
})
this.setData({
historyRecord: filterArr
})
wx.setStorage({
key: 'historyRecord',
data: filterArr
})
},
4. 热搜词
效果图:
搜索页的热搜词模块list出来的是最近热门搜索,排在第一个的就是最热搜索,依次按照热门程度排序。
(1)布局:
布局方式采用弹性布局,热搜词横向排序,并且在设置固定百分比宽度的情况下用了弹性布局下flex-wrap:wrap;进行超出则换行操作。
(2)功能实现:
在绑定的热搜词数据中给它加入hotstatus参数,表示热度情况,类型为number:
<view class="search-item">
<view class="hot-search-item" wx:for="{{hots}}" wx:key="{{item.id}}">
<view class="hot-item">
<view class="text">
<image class="hot-img" src="{{item.hotImg}}" wx:if="{{item.hotImg}}"></image>
<text>{{item.text}}</text>
</view>
<view class="hot-status" >{{hotStatus}}</view>
</view>
</view>
</view>
然后在页面加载事件onload中添加排序方法,检索热词数组中每个hotstatus值,按从大到小排序排列在“知乎热搜”块中:
onLoad: function (options) {
var hots = this.data.hots;
var hots2 = hots.sort((x, y) => y.hotStatus - x.hotStatus);
// reverse()方法会反转数组项的顺序
// hots.reverse();
console.log(hots2);
this.setData({
hots: hots2
})
5 .代码优化
做了好几个搜索页的功能,发现“保存历史记录并加入本地缓存”这个功能在好几个地方都用到了,每个事件中都写一遍,代码繁琐,逻辑可读性略差,所以我将该功能封装成一个方法,每次需要用到的时候,直接带着相应参数引用即可:
saveHistory (param) {
let arr = this.data.historyRecord
arr.unshift(param)
wx.setStorage({
key: 'historyRecord',
data: arr
})
this.setData({
historyRecord: arr
})
}
随即,上面第一条的bindconfirm事件函数则变为:
bindconfirm: function(e){
console.log(e);
var recordItem = e.detail.value;
this.saveHistory({
id: 0,
recordItem
})
turnTo事件函数:
turnTo: function(e){
this.saveHistory({
id: 0,
recordItem: e.target.dataset.param
})
wx.navigateTo({
url: '../searchDetail/searchDetail'
})
},
代码是不是变得更加简洁了?逻辑更加清晰了?这就是我们一直追求的“用最短的代码写最棒的功能!”
四、附页:下拉刷新上拉加载更多
原生App开发中,下拉刷新和上拉加载是使用得比较多的一个功能了。
小程序开发中,小程序只提供了下拉刷新的接口。
Bug & Tip:
- 在滚动 scroll-view 时会阻止页面回弹,所以在 scroll-view 中滚动,是无法触发 onPullDownRefresh
- 若要使用下拉刷新,请使用页面的滚动,而不是 scroll-view ,这样也能通过点击顶部状态栏回到页面顶部,在这里其实也就说了在使用scroll-view时是不能使用onPullDownRefresh了。
我这里直接用了scroll-view实现下拉刷新上拉加载更多,scroll-view有三个event事件:
配置项 | 作用 |
---|---|
bindscrolltoupper | 滚动到顶部触发的回调函数 |
bindscrolltolower | 滚动到底部触发回调函数 |
bindscroll | 滚动时触发的回调函数 |
这里js代码里面其实就是处理逻辑:
上拉:我们需要在数组container-list的后面拼接数据和处理请求的页码;
首先需要封装一个获取页码数据的方法
getPage:
getPage: function(){
var that = this;
var pageIndex = that.data.currentPage;
wx.request({
url: '',
data: {
page: pageIndex
},
success: function(res){
if(pageIndex != 1){ // 加载更多
console.log('加载更多');
var tempArray = that.data.articles;
tempArray = tempArray.concat(that.data.articles);
that.setData({
allPages: that.data.allPages,
articles: tempArray,
hideBottom: true
})
}
}
然后判断当前页是否是最后一页:
if (that.data.currentPage == that.data.allPages){
that.setData({
loadMoreData: '已经到顶'
})
return;
}
这里加了一个定时,是为了延长上拉下拉视图的显示时间:
setTimeout(function(){
console.log('上拉加载更多');
var currentPage = that.data.currentPage;
currentPage = currentPage + 1;
that.setData({
currentPage: currentPage,
hideBottom: false
})
that.getPage();
},300);
下拉:我们需要把当前页码设置成1,articles取当前网络请求的数据。网络请求getData函数上拉下拉的区分是通过当前页码值区分的。
refresh: function(event){
var that = this;
page = 1;
that.setData({
articles: [{这里加入传入的刷新的数据}]
scrollTop: 0,
hidden:true
});
that.getPage();
// GetList(this)
},
来一波成品图:
这个功能各网页、app上都用得很广,我做得还不太完善,后续慢慢修改完善。
结语:
因为时间比较短,知乎也是个大项目,页面、功能还需要慢慢完善,发布了的功能也有一些待改进的地方,后续慢慢把这个项目做下去,慢慢打磨技术。爱代码,爱知乎! 欢迎同样志同道合的码友们多多指教和交流。ヾ(❀╹◡╹)ノ~
顺便附上我的项目地址:仿“知乎”微信小程序
icon“赞助地”:icon
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。