微信小程序开发学习 中级实战 详情篇

Posted by dony on May 15, 2019

§ 详情 - 页面制作

开始前请把 ch4-1 分支中的 code/ 目录导入微信开发工具 这一章节中,主要介绍详情页的页面制作过程

首先看一下我们最终要展示的页面

1557861635077

页面结构大体分为三部分,也是最常见的布局方式:头部、中间体、尾部。最顶部的是页面 title,也就是标题,如果是一般的页面,我们只需要在 detail.json 中增加如下配置即可:

“navigationBarTitleText”: “Quora精选:为什么聪明人总能保持冷静”

但我们制作的详情页面信息是随着文章内容一直变化的,所以需要在代码中单独处理,就不需要在 detail.json 中添加 这里,我们先制作出:头部和尾部。中间的内容部分,会由 parse.js 解析文章数据生成。

开始之前,我们先修改 app.wxss 文件,引入需要用到的公用样式表和第三方样式

@import "./styles/base.wxss";
@import "./lib/wxParse/wxParse.wxss";

.green{
    color: #26b961;
}
page{
    height: 100%;
    background-color: #f8f8f8;
}

Step 1. 页面准备

  1. 由于文章需要上下滚动,我们采用 scroll-view 组件来包括整个页面内容
<!-- detail.html -->
<scroll-view scroll-y="true" enable-back-to-top="true" class="root-wrap">
</scroll-view> 

scroll-view 组件,相当于我们在常规的 div 标签上增加了滚动功能并进行封装

2.然后调整下页面的高度和背景色

 /* detail.css */
  page {
    background: #fbfbfb;
    height: 100%
  }

  .root-wrap {
    height: 100%
  }

Step 2. 页面头部制作

  1. 头部包含三块内容:大标题、左浮动显示作者、右浮云显示日期,制作如下:
  <!-- detail.html -->
  <scroll-view scroll-y="true" enable-back-to-top="true" class="root-wrap">
    <view class="wrapper">
      <view class="info">
        <view class="info-title">Quora精选:为什么聪明人总能保持冷静</view>
        <view class="info-desc cf">
          <text class="info-desc-author fl">哈利波特</text>
          <text class="info-desc-date fr">2017/06/27</text>
        </view>
        <view class="info-line under-line"></view>
      </view>
    </view>
  </scroll-view> 

2.对应样式文件,注意: fl(float:left)fr(float:right)cf(clear:float) 三个样式都是在 base.wxss 中设置的全局样式

/* detail.css */
  page {
    background: #fbfbfb;
    height: 100%
  }

  .root-wrap {
    height: 100%
  }

  .wrapper {
    padding-bottom: 96rpx
  }

  .wrapper .top-img {
    width: 100%;
    height: 470rpx;
    vertical-align: top
  }

  .wrapper .info {
    padding: 0 36rpx
  }

  .wrapper .info-title {
    padding: 40rpx 0;
    line-height: 60rpx;
    font-size: 44rpx;
    font-weight: 500;
    color: #333
  }

  .wrapper .info-desc {
    font-size: 28rpx;
    line-height: 30rpx;
    color: #c1c1c1
  }

  .wrapper .info-desc-author {
    max-width: 65%;
    text-overflow: ellipsis;
    white-space: nowrap;
    overflow: hidden
  }

  .wrapper .info-line {
    margin-top: 24rpx
  }

Step 3. 页面尾部制作

页尾类似于于菜单导航功能,用户可以进入 下一篇返回 列表,并且当页面滚动时候,固定在底部不动

修改页面 detail.html

  <!-- 增加以下内容,footbar节点与info节点平级 -->
  <view class="footbar">
    <form>
      <button class="footbar-back clearBtnDefault">
        <view class="icon footbar-back-icon"></view>
      </button>
      <button class="footbar-btn clearBtnDefault">下一篇</button>
      <button class="footbar-share clearBtnDefault">
        <view class="icon footbar-share-icon"></view>
      </button>
    </form>
  </view>

修改样式表

  /* detail.css 增加以下样式内容 */
  .wrapper .footbar {
    position: fixed;
    left: 0;
    bottom: 0;
    width: 100%;
    height: 96rpx;
    line-height: 96rpx;
    background: #fff;
    font-size: 32rpx;
    color: #333
  }

  .wrapper .footbar-back,.wrapper .footbar-share {
    position: absolute;
    width: 96rpx;
    height: 96rpx;
    bottom: 0;
    z-index: 2
  }

  .wrapper .footbar .icon {
    position: absolute;
    width: 42rpx;
    height: 38rpx;
    top: 30rpx
  }

  .wrapper .footbar-back {
    left: 0
  }

  .wrapper .footbar-back-icon {
    left: 30rpx;
    background: url(https://n1image.hjfile.cn/mh/2017/06/06/1305a8ac4dc9347b59cc8c2c667122e5.png) 0 0 no-repeat;
    background-size: contain
  }

  .wrapper .footbar-list {
    left: 0
  }

  .wrapper .footbar-list-icon {
    left: 30rpx;
    background: url(https://n1image.hjfile.cn/mh/2017/06/09/1e630ac45547e6ab5260928e1d57a3c6.png) 0 0 no-repeat;
    background-size: contain
  }

  .wrapper .footbar-btn {
    text-align: center;
    margin: 0 96rpx;
    height: 96rpx;
    line-height: 96rpx
  }

  .wrapper .footbar-share {
    right: 0
  }

  .wrapper .footbar-share-icon {
    right: 30rpx;
    background: url(https://n1image.hjfile.cn/mh/2017/06/09/ebc3852fb865bd19182c09ca599d8ac1.png) 0 0 no-repeat;
    background-size: contain
  }

  .wrapper .clearBtnDefault {
    margin: 0;
    padding: 0;
    background: #fff;
    border: 0;
    border-radius: 0
  }

  .wrapper .clearBtnDefault:after {
    content: '';
    border: none;
    border-radius: 0;
    width: 0;
    height: 0
  }

页面尾部制作完成,下一步我们将处理中间的文章内容部分。

Step 4. 为中间的 content 内容预留位置

完整的页面代码如下

<scroll-view scroll-y="true" enable-back-to-top="true" class="root-wrap">
      <view class="wrapper">
          <view class="info">
            <view class="info-title">Quora精选:为什么聪明人总能保持冷静</view>
            <view class="info-desc cf">
              <text class="info-desc-author fl">哈利波特</text>
              <text class="info-desc-date fr">2017/06/27</text>
            </view>
            <view class="info-line under-line"></view>
          </view>
          <!-- 增加正文视图位置  -->
          <view class="content">
              文章正文
          </view>
          <view class="footbar">
              <form>
                  <button class="footbar-back clearBtnDefault">
                      <view class="icon footbar-back-icon"></view>
                  </button>
                  <button class="footbar-btn clearBtnDefault">下一篇</button>
                  <button class="footbar-share clearBtnDefault">
                      <view class="icon footbar-share-icon"></view>
                  </button>
              </form>
          </view>
      </view>
  </scroll-view>

§ 详情 - 数据渲染

开始前请把 ch4-2 分支中的 code/ 目录导入微信开发工具 这一节中,我们开始详情的接口调用、数据加载和视图渲染过程。

Step 1. 引入公用的一些工具库,修改 detail.js

'use strict';

import util from '../../utils/index';
import config from '../../utils/config';

// WxParse HtmlFormater 用来解析 content 文本为小程序视图
import WxParse from '../../lib/wxParse/wxParse';
// 把 html 转为化标准安全的格式
import HtmlFormater from '../../lib/htmlFormater';

let app = getApp();
Page({

});

Step 2. 修改 detail.js 在页面初始化时候,请求接口,加载详情数据

Page({
  onLoad (option) {
    /*
    * 函数 `onLoad` 会在页面初始化时候加载运行,其内部的 `option` 是路由跳转过来后的参数对象。
    * 我们从 `option` 中解析出文章参数 `contendId`,然后通过调用 `util` 中封装好的 `request` 函数来获取 `mock` 数据。 
    */ 
    let id = option.contentId || 0;
    this.init(id);
  },
  init (contentId) {
    if (contentId) {
      this.requestDetail(contentId)
          .then(data => {
              util.log(data)
          })
    }
  },
  requestDetail(contentId){
    return util.request({
      url: 'detail',
      mock: true,
      data: {
          source: 1
      }
    })
    .then(res => {
      return res
    })
  }
})

运行之后,我们查看下控制台输出的数据,是不是很清晰!

Step 3. 接着,把页面头部数据渲染出来

修改 requestDetail 函数,并增加日期格式化的方法,达到我们想要的效果,然后重新返回数据

Page({
  // 此处省略部分代码

  requestDetail(contentId){
    return util.request({
      url: 'detail',
      mock: true,
      data: {
          source: 1
      }
    })
    .then(res => {
      let formateUpdateTime = this.formateTime(res.data.lastUpdateTime)
      // 格式化后的时间
      res.data.formateUpdateTime = formateUpdateTime
      return res.data
    })
  },
  formateTime (timeStr = '') {
    let year = timeStr.slice(0, 4),
        month = timeStr.slice(5, 7),
        day = timeStr.slice(8, 10);
    return `${year}/${month}/${day}`;
  }
}

现在我们已经获取到了后端返回的数据,并且已经把部分数据标准处理过。下一步,我们把返回的数据同步到 Model 层中(也就是 data 对象中) 我们增加 configPageData 函数,用它来处理数据同步到 data的逻辑:

Page({
  data: {
    detailData: {

    }
  },
  init (contentId) {
    if(contentId) {
      this.requestDetail(contentId)
          .then(data => {
              this.configPageData(data)
          })
    }
  },
  configPageData(data){
    if (data) {
        // 同步数据到 Model 层,Model 层数据发生变化的话,视图层会自动渲染
        this.setData({
            detailData: data
        });
        //设置标题
        let title = this.data.detailData.title || config.defaultBarTitle
        wx.setNavigationBarTitle({
            title: title
        })
    }
  }
})

因为页面的标题是随着文章变化的,所以需要我们动态设置,这里我们调用了小程序自带的方法来设计

wx.setNavigationBarTitle({
  title: '标题'
})

修改视图 detail.wxml 的头部 class="info" 内容:

<view class="info">
    <view class="info-title"></view>
    <view class="info-desc cf">
        <text class="info-desc-author fl"></text>
        <text class="info-desc-date fr"></text>
    </view>
    <view class="info-line under-line"></view>
</view>

Step 4. 调用 parse 解析接口返回的 content 字段(文本内容)

当详情数据返回后,我们已经对部分数据进行了过滤处理,现在修改 detail.js 中的 init 函数,增加对文章正文的处理:

    articleRevert () {
      // this.data.detailData 是之前我们通过 setData 设置的响应数据
      let htmlContent = this.data.detailData && this.data.detailData.content;
      WxParse.wxParse('article', 'html', htmlContent, this, 0);
    },
    init (contentId) {
      if (contentId) {
        this.requestDetail(contentId)
          .then(data => {
            this.configPageData(data)
          })
          //调用wxparse
          .then(()=>{
            this.articleRevert()
          })
      }
    },

注意看上面的 articleRevert 函数,变量 htmlContent 指向文章的正文数据,当其传入到组件 WxParse 后,同时带入了 5 个参数

WxParse.wxParse('article', 'html', htmlContent, this, 0);

第一个参数 article 很重要,在 WxParse 中,我们传入了当前对象 this,当变量 htmlContent 解析之后,会把解析后的数据赋值给当前对象,并命名为 article

所以当文章数据解析后,当前环境上下文中已经存在了数据 article,可以直接在 detail.wxml 中引用:

this.data.article

修改 detail.wxml,引用我们的文章正文数据:

<!-- 先引入解析模板  -->
<import src="../../lib/wxParse/wxParse.wxml"/>

<!-- 修改文章正文节点  -->
<view class="content">
    <template is="wxParse" data=""/>
</view>

再看下页面效果,文章已经正常的显示了,但我们还需要优化下样式,比如增加一些行高、文字间距、字体大小颜色、图片居中等。修改样式文件 detail.wxss增加 以下样式

.wrapper .content {
  padding: 0 36rpx;
  padding-bottom: 40rpx;
  line-height: 56rpx;
  color: #333;
  font-size: 36rpx;
  overflow: hidden;
  word-wrap: break-word
}

.wrapper .content .langs_cn,.wrapper .content .para.translate {
  font-size: 32rpx;
  color: #666
}

.wrapper .content .langs_cn,.wrapper .content .langs_en,.wrapper .content .para,.wrapper .content .wxParse-p {
  margin: 44rpx 0
}

.wrapper .content image {
  max-width: 100%;
  vertical-align: top
}

.wrapper .content .tip {
  color: #999;
  font-size: 28rpx;
  text-align: center;
  height: 28rpx;
  line-height: 28rpx
}

.wrapper .content .tip-icon {
  vertical-align: top;
  margin-right: 8rpx;
  width: 26rpx;
  height: 26rpx;
  border: 1px solid #999;
  border-radius: 6rpx;
  box-sizing: border-box
}

.wrapper .content .tip-icon.selected {
  border: none;
  background: url(https://n1image.hjfile.cn/mh/2017/06/12/20703f295b7b3ee4f5fe077c4e464283.png) 0 0 no-repeat;
  background-size: contain
}

§ 详情 - 功能完善

开始前请把 ch4-3 分支中的 code/ 目录导入微信开发工具 这一节中,我们把详情的其他功能完善起来:下一篇、 分享、 返回列表。

Step 1. 增加 下一篇 功能

增加 下一篇 的功能,我们需要在视图中绑定一个事件,来触发代码中的响应函数,此函数会调用接口,返回下一篇文章内容数据。

1、修改视图文件 detail.wxml,增加相应的绑定事件

<button class="footbar-btn clearBtnDefault" bindtap="next">下一篇</button>

2、修改代码 detail.js,增加绑定事件对应的 next 及相关函数:

next(){
  this.requestNextContentId()
    .then(data => {
      let contentId = data && data.contentId || 0;
      this.init(contentId);
    })
},
requestNextContentId () {
  let pubDate = this.data.detailData && this.data.detailData.lastUpdateTime || ''
  let contentId = this.data.detailData && this.data.detailData.contentId || 0
  return util.request({
    url: 'detail',
    mock: true,
    data: {
      tag:'微信热门',
      pubDate: pubDate,
      contentId: contentId,
      langs: config.appLang || 'en'
    }
  })
  .then(res => {
    if (res && res.status === 0 && res.data && res.data.contentId) {
      util.log(res)
      return res.data
    } else {
      util.alert('提示', '没有更多文章了!')
      return null
    }
  })
}

大概介绍下这两个函数: 点击触发 next 函数,它会先调用 requestNextContentId,通过把当前文章的 lastUpdateTimecontentId 参数传递给后端,后端会返回下一篇文章的 contentId,这样我们就知道了文章 Id,然后就像刚开始一样,把 contentId 再次传递给 init(contentId) 函数,获取文章的详情数据,然后是渲染视图……

这个时候,可能你已经发现了一个用户体验上的 bug:当页面滚动到一定程度后点击下一篇,新的页面没有滚动到顶部。所以我们需要修复这个 bug,当文章更新后,正常情况下,页面应该滚动到顶部,也就是滚动条在最开始位置。现在我们开始修复它:

scroll-view 组件有个属性 scroll-top,这个属性代表着滚动条当前的位置,也就是说,当它的值为 0 时候,滚动条在最顶部,所以我们需要在数据 data 中记录这个值,当需要文章滚动到页面顶部时候,我们只需要修改 scroll-top 的值就可以了。 这里我们用 scrollTop 来表示:

// 修改 detail.js 的数据 data
data:{
  scrollTop: 0,
  detailData: {}
}

修改视图文件,注意增加 enable-back-to-top 属性,作用就不解释了,直接看属性名的单词应该就明白:

<scroll-view scroll-y="true" scroll-top="" enable-back-to-top="true" class="root-wrap">

当我们需要文章返回到顶部时候,只要设置这个变量值就可以了。这里我们对赋值操作提取出单独的函数:

goTop () {
  this.setData({
    scrollTop: 0
  })
}

在函数 init() 中调用:

init (contentId) {
  if (contentId) {
    this.goTop()
    this.requestDetail(contentId)
        .then(data => {
          this.configPageData(data);
        })
        //调用wxparse
        .then(()=>{
          this.articleRevert();
        });
  }
}

Step 2. 增加 分享 功能

调用小程序会对分享事件做监听,如果触发分享功能后,监听事件会返回一个对象,包含了分享出去的信息内容,并且可以分别对分享成功和分享失败做处理

<!-- 
<button class="footbar-share clearBtnDefault">
  <view class="icon footbar-share-icon"></view>
</button> 
-->
<button class="footbar-share clearBtnDefault" open-type="share">
  <view class="icon footbar-share-icon"></view>
</button>

button 组件增加了 open-type="share" 属性后,就可以触发 onShareAppMessage 监听事件:

onShareAppMessage () {
  let title = this.data.detailData && this.data.detailData.title || config.defaultShareText;
  let contentId = this.data.detailData && this.data.detailData.contentId || 0;
  return {
    // 分享出去的内容标题
    title: title,

    // 用户点击分享出去的内容,跳转的地址
    // contentId为文章id参数;share参数作用是说明用户是从分享出去的地址进来的,我们后面会用到
    path: `/pages/detail/detail?share=1&contentId=${contentId}`,
    
    // 分享成功
    success: function(res) {},
    
    // 分享失败
    fail: function(res) {}
  }
},

这里需要我们注意下,此接口对部分版本不支持,所以如果版本不支持时候,我们要给用户一个提示信息。所以我们需要给分享按钮另外绑定一个点击事件,如果不支持的话,提示用户:

notSupportShare () {
  // deviceInfo 是用户的设备信息,我们在 app.js 中已经获取并保存在 globalData 中
  let device = app.globalData.deviceInfo;
  let sdkVersion = device && device.SDKVersion || '1.0.0';
  return /1\.0\.0|1\.0\.1|1\.1\.0|1\.1\.1/.test(sdkVersion);
},
share () {
  if (this.notSupportShare()) {
    wx.showModal({
      title: '提示',
      content: '您的微信版本较低,请点击右上角分享'
    })
  }
}

在视图中绑定 share 监听事件:

<!--
<button class="footbar-share clearBtnDefault" open-type="share">
  <view class="icon footbar-share-icon"></view>
</button>
-->
<button class="footbar-share clearBtnDefault" bindtap="share" open-type="share">
  <view class="icon footbar-share-icon"></view>
</button>

Step 3. 增加 返回列表 功能

我们需要在 detail.js 中增加一个返回列表的函数,然后视图中绑定触发事件

// detail.js 增加以下内容
Page({
  back(){
    wx.navigateBack()
  }
})

在视图中绑定事件:

<!--
<button class="footbar-back clearBtnDefault">
  <view class="icon footbar-back-icon"></view>
</button>
-->
<button class="footbar-back clearBtnDefault" bindtap="back">
  <view class="icon footbar-back-icon"></view>
</button>

由于 wx.navigateBack 相当于浏览器的 history,通过浏览记录返回的。那么如果用户并不是从列表进来的,比如是从分享出去的详情打开呢?这时候记录是不存在的。

如果是通过分享进来的,有带进来参数 share=1,如 step 2 中的分享功能,那么我们在刚进到页面时候,就可以通过 share=1 是否存在来标识出来:

onLoad (option) {
  let id = option.contentId || 0;
  this.setData({
    isFromShare: !!option.share
  });
  this.init(id);
},

然后修改 back 函数,如果是从分享入口进来的,那么我们就通过导航的方式来返回列表;如果不是,就正常的通过加载记录来返回:

// detail.js 增加以下内容
Page({
  back(){
    if (this.data.isFromShare) {
      wx.navigateTo({
        url: '../index/index'
      })
    } else {
      wx.navigateBack()  
    }
  }
})

至此,我们简单的小程序实战已经完成!!!