慕尔名,我的“知乎”小程序(1)

5,677 阅读14分钟

前言

不知道有多少人跟我一样爱上“知乎”的呢?一直喜欢“知乎”所带来的用户体验效果和一些新颖的信息传送。所以在最近的小程序学习中,开始的第一个项目实训,就是“知乎”了。
开始做了才知道,“知乎”小程序的工程量之大,我这个前端新手不知应该需要多少个工作日,才能把“知乎”完整做成小程序。不过程序员的路本就是边学习边成长的,学无止境。小人不才,最近做了一些,忍不住先开始一波经验和问题的分享了。

“知乎”小程序开跑

成果分享

一、首页

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