1. 程式人生 > >微信小程式之知乎日報

微信小程式之知乎日報

上一次的《微信小程式之小豆瓣圖書》製作了一個圖書的查詢功能,只是簡單地應用到了網路請求,其他大多數小程式應有的知識。而本次的示例是知乎日報,功能點比較多,頁面也比上次複雜了許多。在我編寫這個DEMO之前,網上已經有很多網友弄出了相同的DEMO,也是非常不錯的,畢竟這個案例很經典,有比較完整的API,很值得模仿學習。本次個人的DEMO也算是一次小小的練習吧。

由於知乎日報是一個資訊類的App,UI的佈局主要是以資訊列表頁、資訊詳情頁和評論頁為主,當然本次也附帶了應用設定頁,不過現階段功能尚未編寫,過段時間會更新補充,繼續完善。

API分析

本次應用使用了知乎日報的API,相比上次豆瓣圖書的數量比較多了,但是部分仍然有限制,而且自己沒有找到評論介面的分頁引數,所以評論這塊沒有做資料的分頁。

以下是使用到的具體API,更加詳細引數和返回結構可參照網上網友分享的 知乎日報-API-分析 ,在此就不做再次分析了。

啟動介面圖片

引數 說明
size 圖片尺寸,格式:寬*高。例如: 768*1024

獲取剛進入應用時的顯示封面,可以根據傳遞的尺寸引數來獲取適配使用者螢幕的封面。

獲取最新日報

http://news-at.zhihu.com/api/4/news/latest

返回的資料用於日報的首頁列表,首頁的結構有上下部分,上部分是圖片滑動模組,用於展示熱門日報,下部分是首頁日報列表,以上介面返回的資料有熱門日報和首頁日報

獲取日報詳細

http://news-at.zhihu.com/api/4/news/{id}

引數 說明
id 日報id

在點選日報列表也的日報項時,需要跳轉到日報詳情頁展示日報的具體資訊,這個介面用來獲取日報的展示封面和具體內容。

歷史日報

http://news.at.zhihu.com/api/4/news/before/{date}

引數 說明
date 年月日格式時間yyyyMMdd,例如:20150903、20161202

這個介面也是用與首頁列表的日報展示,但是不同的是此介面需要傳一個日期引數,如20150804格式。獲取最新日報介面只能獲取當天的日報列表,如果需要獲取前天或者更久之前的日報,則需要這個介面單獨獲取。

日報額外資訊

http://news-at.zhihu.com/api/4/story-extra/{id}

引數 說明
id 日報id

在日報詳情頁面中,不僅要展示日報的內容,好需要額外獲取此日報的評論數目和推薦人數等額外資訊。

日報長評

http://news-at.zhihu.com/api/4/story/{id}/long-comments

引數 說明
id 日報id

日報的評論頁面展示長評用到的介面(沒有找到分頁引數,分頁沒有做)

日報短評

http://news-at.zhihu.com/api/4/story/{id}/short-comments

引數 說明
id 日報id

日報的評論頁面展示段評用到的介面(沒有找到分頁引數,分頁沒有做)

主題日報欄目列表

http://news-at.zhihu.com/api/4/themes

主頁的側邊欄顯示有主題日報的列表,需要通過這個介面獲取主題日報欄目列表

主題日報具體內容列表

http://news-at.zhihu.com/api/4/theme/{themeId}

引數 說明
themeId 主題日報欄目id

在主頁側欄點選主題日報進入主題日報的內容頁,需要展示此主題日報下的日報列表。

程式碼編寫

啟動頁

作為一個仿製知乎日報的偽APP,高大上的啟動封面是必須的,哈哈。啟動頁面很簡單,請求一個應用啟動封面介面,獲取封面路徑和版權資訊。當進入頁面,在onLoad事件中獲取螢幕的寬和高來請求適合尺寸的圖片,在onReady中請求載入圖片,在請求成果之後,延遲2s進入首頁,防止頁面一閃而過。

onLoad: function( options ) {    var _this = this;
    wx.getSystemInfo( {      success: function( res ) {
        _this.setData( {          screenHeight: res.windowHeight,          screenWidth: res.windowWidth,
        });
      }
    });
},onReady: function() {    var _this = this;    var size = this.data.screenWidth + '*' + this.data.screenHeight;
    requests.getSplashCover( size, ( data ) => {
      _this.setData( { splash: data });
    }, null, () => {
      toIndexPage.call(_this);
    });
}  
/**
 * 跳轉到首頁
 */function toIndexPage() {
  setTimeout( function() {
    wx.redirectTo( {      url: '../index/index'
    });
  }, 2000 );
}

zhihu.png

首頁

輪播圖

首頁頂部需要用到輪播圖來展示熱門日報,小程式中的Swipe元件可以實現。

<swiper class="index-swiper" indicator-dots="true" interval="10000">
    <block wx:for="{{sliderData}}">
        <swiper-item data-id="{{item.id}}" bindtap="toDetailPage">
            <image mode="aspectFill" src="{{item.image}}" style="width:100%" />
            <view class="mask"></view>
            <view class="desc"><text>{{item.title}}</text></view>
        </swiper-item>
    </block></swiper>

所有的內容都必須要在swiper-item標籤中,因為我們的圖片不止有一張,而是有多個熱門日報資訊,需要用迴圈來展示資料。這裡需要指定的是image裡的屬性mode設定為aspectFill是為了適應元件的寬度,這需要犧牲他的高度,即有可能裁剪,但這是最好的展示效果。toDetailPage是點選事件,觸發跳轉到日報詳情頁。在跳轉到日報詳情頁需要附帶日報的id過去,我們在迴圈列表的時候把當前日報的id存到標籤的data中,用data-id標識,這有點類似與html5中的data-*API。當在這個標籤上發生點選事件的時候,我們可以通過Event.currentTarget.dataset.id來獲取data-id的值。

首頁

日報列表

列表的佈局大同小異,不過這裡的列表涉及到分頁,我們可以毫不猶豫地使用scroll-view元件,它的scrolltolower是非常好用的,當元件滾動到底部就會觸發這個事件。上次的小豆瓣圖書也是使用了這個元件分頁。不過這次的分頁動畫跟上次不一樣,而是用一個附帶旋轉動畫的重新整理圖示,使用官方的動畫api來實現旋轉。

<view class="refresh-block" wx:if="{{loadingMore}}">
    <image animation="{{refreshAnimation}}" src="../../images/refresh.png"></image></view>

程式碼中有一個顯眼的animation屬性,這個屬性就是用來控制動畫的。

/**
 * 旋轉上拉載入圖示
 */function updateRefreshIcon() {  var deg = 360;  var _this = this;  var animation = wx.createAnimation( {    duration: 1000
  });  var timer = setInterval( function() {    if( !_this.data.loadingMore )
      clearInterval( timer );
    animation.rotateZ( deg ).step();
    deg += 360;
    _this.setData( {      refreshAnimation: animation.export()
    })
  }, 1000 );
}

當列表載入資料時,給動畫設定一個時長duration,然後按Z軸旋轉,即垂直方向旋轉rotateZ,每次旋轉360度,週期是1000毫秒。

列表的佈局跟上次的小豆瓣圖書的結構差不多,用到了迴圈結構wx:for和判斷語句wx:if、 wx:else來控制不同的展示方向。

<view class="common-list">
    <block wx:for="{{pageData}}">
        <view class="list-item {{item.images[0] ? 'has-img': ''}}" wx:if="{{item.type != 3}}" data-id="{{item.id}}" bindtap="toDetailPage">
            <view class="content">
                <text>{{item.title}}</text>
            </view>
            <image wx:if="{{item.images[0]}}" src="{{item.images[0]}}" class="cover"></image>
        </view>
        <view class="list-spliter" wx:else>
            <text>{{item.title}}</text>
        </view>
    </block></view>

class="list-spliter"這塊是用來顯示日期,列表中的日報只要不是同一天的記錄,就在中間插入一條日期顯示塊。在列表項中有一個三元運算判斷輸出具體的class{{item.images[0] ? 'has-img': ''}},是因為列表中可能沒有圖片,因此需要判定當前有沒有圖片,沒有圖片就不新增class為has-img來控制帶有圖片列表項的佈局。

浮動按鈕

因為小程式中沒有側欄元件,無法做到側滑手勢顯示側欄(本人發現touchstart事件和tap事件有衝突,無法實現出手勢側滑判斷,所以沒有用側滑手勢,可能是本人理解太淺了,沒有發現解決方法,嘿嘿...),浮動按鈕的樣式參照了Android中的FloatAction經典按鈕。可以浮動在介面上,還可以滑動到任意位置,背景為稍微透明。

<view class="float-action" 
bindtap="ballClickEvent" style="opacity: 
{{ballOpacity}};bottom:{{ballBottom}}px;right:{{ballRight}}px;" 
bindtouchmove="ballMoveEvent"> </view>
.float-action {  
position: absolute;  bottom: 20px;  
right: 30px;  width: 50px;  height: 50px;  
border-radius: 50%;  
box-shadow: 2px 2px 10px #AAA;  
background: #1891D4;  z-index: 100;
}

按鈕的樣式隨便弄了一下,寬高用了px是因為後面的移動判斷需要獲取螢幕的寬高資訊,這些資訊的單位是px。wxml綁定了點選事件和移動事件,點選事件是控制側欄彈出,滑動事件是按鈕移動。

//浮動球移動事件ballMoveEvent: function( e ) {    
var touchs = e.touches[ 0 ];    
var pageX = touchs.pageX;    var pageY = touchs.pageY;    
if( pageX < 25 ) return;    if( pageX > this.data.screenWidth - 25 ) return;    
if( this.data.screenHeight - pageY <= 25 ) return;    if( pageY <= 25 ) return;    
var x = this.data.screenWidth - pageX - 25;    
var y = this.data.screenHeight - pageY - 25;    
this.setData( {        ballBottom: y,        ballRight: x
    });
}

touchmove事件中的會傳遞一個event引數,通過這個引數可以獲取到當前手勢滑動到的具體座標資訊e.touches[ 0 ]

側滑選單

側滑選單是一個經典APP佈局方案,小程式中沒有提供這個元件,甚是遺憾。不過實現起來也不是很難,但是總感覺有點彆扭...

側滑選單的樣式採用了固定定位的佈局position: fixed,預設隱藏與左側,當點選浮動按鈕時彈出,點選遮罩或者側欄上邊的關閉按鈕時收回。側欄的彈出和收回動畫採用小程式提供的動畫API。

<view class="slide-mask" style="display:{{maskDisplay}}" bindtap="slideCloseEvent"></view><view class="slide-menu" style="right: {{slideRight}}px;width: {{slideWidth}}px;height:{{slideHeight}}px;" animation="{{slideAnimation}}">
  <icon type="cancel" size="30" class="close-btn" color="#FFF" bindtap="slideCloseEvent" />
  <scroll-view scroll-y="true" style="height:100%;width:100%">
    <view class="header">
      <view class="userinfo">
        <image src="../../images/avatar.png" class="avatar"></image>
        <text>Oopsguy</text>
      </view>
      <view class="toolbar">
        <view class="item">
          <image src="../../images/fav.png"></image>
          <text>收藏</text>
        </view>
        <view class="item" bindtap="toSettingPage">
          <image src="../../images/setting.png"></image>
          <text>設定</text>
        </view>
      </view>
    </view>
    <view class="menu-item home">
      <text>首頁</text>
    </view>
    <view class="slide-inner">
      <block wx:for="{{themeData}}">
        <view class="menu-item" data-id="{{item.id}}" bindtap="toThemePage">
          <text>{{item.name}}</text>
          <image src="../../images/plus.png"></image>
        </view>
      </block>
    </view>    
  </scroll-view></view>
/*slide-menu*/.slide-mask {  
position: fixed;  width: 100%;  

top: 0;  left: 0;  bottom: 0;  

background: rgba(0, 0, 0, .3);  z-index: 800;
}.slide-menu {  position: fixed;  top: 0;  background: #FFF;  z-index: 900;
}/*.slide-menu .slide-inner {
  padding: 40rpx;
}*/.slide-menu .header {  background: #019DD6;  height: 200rpx;  color: #FFF;  padding: 20rpx 40rpx 0 40rpx;
}.userinfo {  height: 80rpx;  line-height: 80rpx;  overflow: hidden;
}.userinfo .avatar {  width: 80rpx;  height: 80rpx;  border-radius: 50%;  margin-right: 40rpx;  float: left;
}.userinfo text {  float: left;  font-size: 35rpx;
}.toolbar {  height: 100rpx;  padding-top: 25rpx;  line-height: 75rpx;
}.toolbar .item {  width: 50%;  display: inline-block;  overflow: hidden;  text-align: center
}.toolbar .item text {  display: inline-block;  font-size: 30rpx
}.toolbar .item image {  display: inline-block;  position: relative;  top: 10rpx;  margin-right: 10rpx;  height: 50rpx;  width: 50rpx;
}.slide-menu .menu-item {  position: relative;  height: 100rpx;  line-height: 100rpx;  padding: 0 40rpx;  font-size: 35rpx;
}.slide-menu .menu-item:active {  background: #FAFAFA;
}.slide-menu .menu-item image {  position: absolute;  top: 25rpx;  right: 40rpx;  width: 50rpx;  height: 50rpx;
}.slide-menu .home {  color: #019DD6}.slide-menu .close-btn {  position: absolute;  top: 20rpx;  right: 40rpx;  z-index: 1000}

以上是側欄的一個簡單的佈局和樣式,包含了側欄中的使用者資訊塊和主題日報列表。當然這些資訊是需要通過js的中網路請求來獲取的。側欄結構上邊有一個class為slide-mask的view,這是一個遮罩元素,當側欄彈出的時候,側欄後邊就有一層輕微透明的黑色遮罩。側欄的高度和寬度初始是不定的,需要在進入頁面的時候,馬上獲取裝置資訊來獲取螢幕的高度寬度調整側欄樣式。

//獲取裝置資訊,螢幕的高度寬度onLoad: function() {    var _this = this;
    wx.getSystemInfo( {      success: function( res ) {
        _this.setData( {          screenHeight: res.windowHeight,          
        
        screenWidth: res.windowWidth,          
        
        slideHeight: res.windowHeight,          
        
        slideRight: res.windowWidth,          
        
        slideWidth: res.windowWidth * 0.7
        });
      }
    });
}

寬度我取了螢幕寬度的70%,高度一致。側欄的彈出收回動畫使用內建動畫API

//側欄展開function slideUp() {  var animation = wx.createAnimation( {    duration: 600
  });  this.setData( { maskDisplay: 'block' });
  animation.translateX( '100%' ).step();  this.setData( {    slideAnimation: animation.export()
  });
}//側欄關閉function slideDown() {  var animation = wx.createAnimation( {    duration: 800
  });
  animation.translateX( '-100%' ).step();  this.setData( {    slideAnimation: animation.export()
  });  this.setData( { maskDisplay: 'none' });
}

側欄彈出的時候,遮罩的css屬性display設定為block顯示,側欄通過css動畫transform來想右側移動了100%的寬度translateX(100%),側欄收回時,動畫恰好與彈出的相反,其實這些動畫最後都會翻譯為css3動畫屬性,這些API只是css3動畫的封裝。為了點選遮罩收回側欄,遮罩的tap事件也要繫結slideCloseEvent

//浮動球點選 側欄展開ballClickEvent: function() {
    slideUp.call( this );
},//遮罩點選  側欄關閉slideCloseEvent: function() {
    slideDown.call( this );
}

ribao.png

日報詳情頁

內容

日報的內容也是最難做的,因為介面返回的內容是html...,天呀,是html!小程式肯本就不支援,解析html的過程非常痛苦,因為本人的正則表示式只是幾乎為0,解析方案的尋找過程很虐心,經典的jQuery是用不了了,又沒有dom,無法用傳統的方式解析html。嘗試了正則學習,但是也是無法在短時間內掌握,尋找了很多解析庫,大多是依賴瀏覽器api。不過,上天是不會忽視有心人的,哈哈,還是被我找到了解決方案。幸運的我發現了一個用正則編寫的和類似與語法分析方法的xml解析庫。這個庫是一個very good的網友封裝的html解析庫。詳情點選 用Javascript解析html

由於日報詳情內容的html部分結構太大,這裡只列出了簡要的結構,這個結構是通用的(不過不保證知乎會變動結構,要是變動了,之前的解析可能就沒用了...心累)

<div class="question">
    <h2 class="question-title">日本的六大財閥現在怎麼樣了?</h2>
    <div class="answer">
        <div class="meta">
            ![](//upload-images.jianshu.io/upload_images/2192094-454caed2ef752b34.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)            <span class="author">leon,</span><span class="bio">data analyst</span>
        </div>
        <div class="content">
            <p>“財閥”在戰後統稱為 Group(集團),是以銀行和傳統工業企業為核心的鬆散集合體,由於歷史淵源而有相互持股。</p>
            <p>Group 對於當今日本企業的意義在於:</p>
            <p><strong>MUFG:三菱集團、三和集團(みどり會)</strong></p>
            <p><img class="content-image" src="http://pic1.zhimg.com/70/90c319ac7a7b2723e5b511de954f45bc_b.jpg" alt=""
                /></p>
        </div>
    </div>
    <div class="view-more"><a href="http://www.zhihu.com/question/23907827">檢視知乎討論<span class="js-question-holder"></span></a></div></div>

外層的.question是日報中問題答案的顯示單位,可能有多個,因此需要迴圈顯示。.question-title是問題的標題,.meta中是作者的資訊,img.avatar是使用者的頭像,span.author是使用者的名稱,span.bio可能使使用者的簽名吧。最難解析的是.content中的內容,比較多。但是有個規律就是都是以<p>標籤包裹著,獲取了.content中的所有p就可以得到所有的段落。之後再解析出段落中的圖片。

以下是詳情頁的內容展示模版

<view style="padding-bottom: 150rpx;">
    <block wx:for="{{news.body}}">
        <view class="article">
            <view class="title" wx:if="{{item.title && item.title != ''}}">
                <text>{{item.title}}</text>
            </view>
            <view class="author-info" wx:if="{{(item.avatar && item.avatar != '') || (item.author && item.author != '') || (item.bio && item.bio != '')}}">
                <image wx:if="{{item.avatar && item.avatar != ''}}" class="avatar" src="{{item.avatar}}"></image>
                <text wx:if="{{item.author && item.author != ''}}" class="author-name">{{item.author}}</text>
                <text wx:if="{{item.bio && item.bio != ''}}" class="author-mark">,{{item.bio}}</text>
            </view>
            <view class="content" wx:if="{{item.content && item.content.length > 0}}">
                <block wx:for="{{item.content}}" wx:for-item="it">
                    <block wx:if="{{it.type == 'p'}}">
                        <text>{{it.value}}</text>
                    </block>
                    <block wx:elif="{{it.type == 'img'}}">
                        <image mode="aspectFill" src="{{it.value}}" data-src="{{it.value}}" bindtap="previewImgEvent" />
                    </block>
                    <block wx:elif="{{it.type == 'pstrong'}}">
                        <text class="strong">{{it.value}}</text>
                    </block>
                    <block wx:elif="{{it.type == 'pem'}}">
                        <text class="em">{{it.value}}</text>
                    </block>
                    <block wx:elif="{{it.type == 'blockquote'}}">
                        <text class="qoute">{{it.value}}</text>
                    </block>
                    <block wx:else>
                        <text>{{it.value}}</text>
                    </block>
                </block>
                
            </view>

            <view  class="discuss" wx:if="{{item.more && item.more != ''}}">
                <navigator url="{{item.more}}">檢視知乎討論</navigator>
            </view>
        </view>
    </block></view>

可以看出模版中的內容展示部分用了蠻多的block加判斷語句wx:if  wx:elif  wx:else。這些都是為了需要根據解析後的內容型別來判斷需要展示什麼標籤和樣式。解析後的內容大概格式是這樣的:

{
    body: [
       title: '標題',
       author: '作者', 
       bio: '簽名', 
       avatar: '頭像', 
       more: '更多地址',
       content: [   //內容
            {
                type: 'p',
                value: '普通段落內容'
            },
            {
                type: 'img',
                value: 'http://xxx.xx.xx/1.jpg'
            },
            {
                type: 'pem',
                value: '...'
            },
            ...
       ]
    ],
    ...
}

需要注意的一點是主題日報有時候返回的html內容是經過unicode編碼的不能直接顯示,裡邊全是類似&#xxxx;的字元,這需要單獨為主題日報的日報詳情解析編碼。

再點選主題日報中的列表項是,傳遞一個標記是主題日報的引數theme

//跳轉到日報詳情頁toDetailPage: function( e ) {    var id = e.currentTarget.dataset.id;
    wx.navigateTo( {      url: '../detail/detail?theme=1&id=' + id
    });
},

然後在Detail.js的onLoad事件中接受引數

//獲取列表殘過來的引數 id:日報id, 
theme:是否是主題日報內容(因為主題日報的內容有些需要單獨解析)
onLoad: function( options ) {    var id = options.id;    
var isTheme = options[ 'theme' ];    
this.setData( { id: id, isTheme: isTheme });
},

之後開始請求介面獲取日報詳情,並根據是否是主題日報進行個性化解析

//載入頁面相關資料function loadData() {  var _this = this;  var id = this.data.id;  var isTheme = this.data.isTheme;  //獲取日報詳情內容
  _this.setData( { loading: true });
  requests.getNewsDetail( id, ( data ) => {
    data.body = utils.parseStory( data.body, isTheme );
    _this.setData( { news: data, pageShow: 'block' });
    wx.setNavigationBarTitle( { title: data.title }); //設定標題
  }, null, () => {
    _this.setData( { loading: false });
  });
}

以上傳入一個isTheme引數進入解析方法,解析方法根據此引數判斷是否需要進行單獨的編碼解析。

內容解析的庫程式碼比較多,就不貼出了,可以到git上檢視。這裡給出解析的封裝。

var HtmlParser = require( 'htmlParseUtil.js' );String.prototype.trim = function() {  return this.replace( /(^\s*)|(\s*$)/g, '' );
}String.prototype.isEmpty = function() {  return this.trim() == '';
}/**
 * 快捷方法 獲取HtmlParser物件
 * @param {string} html html文字
 * @return {object} HtmlParser
 */function $( html ) {  return new HtmlParser( html );
}/**
 * 解析story物件的body部分
 * @param {string} html body的html文字
 * @param {boolean} isDecode 是否需要unicode解析
 * @return {object} 解析後的物件
 */function parseStory( html, isDecode ) {  var questionArr = $( html ).tag( 'div' ).attr( 'class', 'question' ).match();  var stories = [];  var $story;  if( questionArr ) {    for( var i = 0, len = questionArr.length;i < len;i++ ) {
      $story = $( questionArr[ i ] );
      stories.push( {        title: getArrayContent( $story.tag( 'h2' ).attr( 'class', 'question-title' ).match() ),        avatar: getArrayContent( getArrayContent( $story.tag( 'div' ).attr( 'class', 'meta' ).match() ).jhe_ma( 'img', 'src' ) ),        author: getArrayContent( $story.tag( 'span' ).attr( 'class', 'author' ).match() ),        bio: getArrayContent( $story.tag( 'span' ).attr( 'class', 'bio' ).match() ),        content: parseStoryContent( $story, isDecode ),        more: getArrayContent( getArrayContent( $( html ).tag( 'div' ).attr( 'class', 'view-more' ).match() ).jhe_ma( 'a', 'href' ) )
      });
    }
  }  return stories;
}/**
 * 解析文章內容
 * @param {string} $story htmlparser物件
 * @param {boolean} isDecode 是否需要unicode解析
 * @returb {object} 文章內容物件
 */function parseStoryContent( $story, isDecode ) {  var content = [];  var ps = $story.tag( 'p' ).match();  var p, strong, img, blockquote, em;  if( ps ) {    for( var i = 0, len = ps.length;i < len;i++ ) {
      p = ps[ i ]; //獲取<p>的內容
      if( !p || p.isEmpty() )        continue;

      img = getArrayContent(( p.jhe_ma( 'img', 'src' ) ) );
      strong = getArrayContent( p.jhe_om( 'strong' ) );
      em = getArrayContent( p.jhe_om( 'em' ) );
      blockquote = getArrayContent( p.jhe_om( 'blockquote' ) );      if( !img.isEmpty() ) { //獲取圖片
        content.push( { type: 'img', value: img });
      }      else if( isOnly( p, strong ) ) { //獲取加粗段落<p><strong>...</strong></p>
        strong = decodeHtml( strong, isDecode );        if( !strong.isEmpty() )
          content.push( { type: 'pstrong', value: strong });
      }      else if( isOnly( p, em ) ) { //獲取強調段落 <p><em>...</em></p>
        em = decodeHtml( em, isDecode );        if( !em.isEmpty() )
          content.push( { type: 'pem', value: em });
      }      else if( isOnly( p, blockquote ) ) { //獲取引用塊 <p><blockquote>...</blockquote></p>
        blockquote = decodeHtml( blockquote, isDecode );        if( !blockquote.isEmpty() )
          content.push( { type: 'blockquote', value: blockquote });
      }      else { //其他型別 歸類為普通段落 ....太累了 不想解析了T_T
        p = decodeHtml( p, isDecode );        if( !p.isEmpty() )
          content.push( { type: 'p', value: p });
      }
    }
  }  return content;
}/**
 * 取出多餘或者難以解析的html並且替換轉義符號
 */function decodeHtml( value, isDecode ) {  if( !value ) return '';
  value = value.replace( /<[^>]+>/g, '' )
    .replace( / /g, ' ' )
    .replace( /“/g, '"' )
    .replace( /”/g, '"' ).replace( /·/g, '·' );  if( isDecode )    return decodeUnicode( value.replace( /&#/g, '\\u' ) );  return value;

    
}/**
 * 解析段落的unicode字元,主題日報中的內容又很多是編碼過的
 */function decodeUnicode( str ) {  var ret = '';  var splits = str.split( ';' );  for( let i = 0;i < splits.length;i++ ) {
    ret += spliteDecode( splits[ i ] );
  }  return ret;
};/**
 * 解析單個unidecode字元
 */function spliteDecode( value ) {  var target = value.match( /\\u\d+/g );  if( target && target.length > 0 ) { //解析類似  "7.1 \u20998" 參雜其他字元
    target = target[ 0 ];    var temp = value.replace( target, '{{@}}' );
    target = target.replace( '\\u', '' );
    target = String.fromCharCode( parseInt( target ) );    return temp.replace( "{{@}}", target );
  } else {    // value = value.replace( '\\u', '' );
    // return String.fromCharCode( parseInt( value, '10' ) )
    return value;
  }
}/**
 * 獲取陣列中的內容(一般為第一個元素)
 * @param {array} arr 內容陣列
 * @return {string} 內容
 */function getArrayContent( arr ) {  if( !arr || arr.length == 0 ) return '';  return arr[ 0 ];
}function isOnly( src, target ) {  return src.trim() == target;
}module.exports = {  parseStory: parseStory
}

程式碼的解析過程比較繁雜,大家可以根據返回的html結構和參照解析庫的作者寫的文章來解讀。

底部工具欄

一般資訊APP的詳情頁都有一個底部的工具欄用於操作分享、

收藏、評論和點贊等等。為了更好地鍛鍊動手能力,自己也做了一個底部工具欄,

雖然官方的APP並沒有這個東西。前面介紹到的獲取額外資訊API在這裡就被使用了。

本來自己是想把推薦人數和評論數顯示在底部的圖片右上角,但是由於本人的設計問題,底部的字號已經是很小了,

顯示數量的地方的字號又不能再小了,這樣看起來數字顯示的地方和圖示的大小几乎一樣,很是彆扭,

所以就不現實數字了。這塊還是有很多待完善的功能的,比較收藏功能和是否有評論提示功能等。

<view class="toolbar">
    <view class="inner">
        <view class="item" bindtap="showModalEvent"><image src="../../images/share.png" /></view>
        <view class="item" bindtap="reloadEvent"><image src="../../images/refresh.png" /></view>
        <view class="item"><image src="../../images/favorite.png" /></view>
        <view class="item" data-id="{{id}}" bindtap="toCommentPage"><image src="../../images/insert_comment.png" />
            <view class="tip"></view>
        </view>
        <view class="item">
            <image src="../../images/thumb_up_active.png" />
        </view>
    </view></view>

底部有分享、收藏、評論和點贊按鈕,分享肯定是做不了啦,哈哈,但是效果還是需要有的,就一個modal彈窗,顯示各類社交應用的圖示就行啦。

<modal class="modal" confirm-text="取消" no-cancel hidden="{{modalHidden}}" bindconfirm="hideModalEvent">
    <view class="share-list">
        <view class="item"><image src="../../images/share_qq.png" /></view>
        <view class="item"><image src="../../images/share_pengyouquan.png" /></view>
        <view class="item"><image src="../../images/share_qzone.png" /></view>
    </view>
    <view class="share-list" style="margin-top: 20rpx">
        <view class="item"><image src="../../images/share_weibo.png" /></view>
        <view class="item"><image src="../../images/share_alipay.png" /></view>
        <view class="item"><image src="../../images/share_plus.png" /></view>
    </view></modal>

model的隱藏和顯示都是通過hidden屬性來控制。

底部工具欄中還有一個按鈕是重新整理,其實就是一個重新呼叫介面請求資料的過程而已。

//重新載入資料reloadEvent: function() {
    loadData.call( this );
},

3.png

評論頁面

評論頁面蠻簡單的,就是展示評論列表,

但是要展示兩部分,一部分是長評,另一部分是短評。長評跟短評的佈局都是通用的。

進入到評論頁面時,如果長評有資料,則先載入長評,短評需要使用者點選短評標題才載入,

否則就直接載入短評。這需要上一個詳情頁面中傳遞日報的額外資訊過來(即長評數量和短評數量)。

之前已經在日報詳情頁面中,順便載入了額外的資訊

//請求日報額外資訊(主要是評論數和推薦人數)
requests.getStoryExtraInfo( id, ( data ) => {
    _this.setData( { extraInfo: data });
});

在跳轉到評論頁面的時候順便傳遞評論數量,

這樣我們就不用在評論頁面在請求一次額外資訊了。

//跳轉到評論頁面toCommentPage: function( e ) {    
var storyId = e.currentTarget.dataset.id;    

var longCommentCount = this.data.extraInfo ? 
this.data.extraInfo.long_comments : 0; //長評數目
    var shortCommentCount = this.data.extraInfo ? 
    this.data.extraInfo.short_comments : 0; //短評數目
    //跳轉到評論頁面,並傳遞評論數目資訊
    wx.navigateTo( {      
    url: '../comment/comment?lcount=' + 
    longCommentCount + '&scount=' 
    + shortCommentCount + '&id=' + storyId
    });
}

評論頁面接受引數

//獲取傳遞過來的日報id 和 評論數目onLoad: function( options ) {    

var storyId = options[ 'id' ];    

var longCommentCount = parseInt( options[ 'lcount' ] );   
var shortCommentCount = parseInt( options[ 'scount' ] );   
this.setData( { storyId: storyId, longCommentCount: 

longCommentCount, shortCommentCount: shortCommentCount });
},

進入頁面立刻載入資料

//載入長評列表onReady: function() {    

var storyId = this.data.storyId;    
var _this = this;    

this.setData( { loading: true, toastHidden: true });    
    //如果長評數量大於0,則載入長評,否則載入短評
    if( this.data.longCommentCount > 0 ) {
      requests.getStoryLongComments( storyId, ( data ) => {        console.log( data );
        _this.setData( { longCommentData: data.comments });
      }, () => {
        _this.setData( { toastHidden: false, toastMsg: '請求失敗' });
      }, () => {
        _this.setData( { loading: false });
      });
    } else {
      loadShortComments.call( this );
    }
}/**
 * 載入短評列表
 */function loadShortComments() { 
 
  var storyId = this.data.storyId;  
  
  var _this = this;  this.setData( { loading: true, toastHidden: true });
  requests.getStoryShortComments( storyId, ( data ) => {
    _this.setData( { shortCommentData: data.comments });
  }, () => {
    _this.setData( { toastHidden: false, toastMsg: '請求失敗' });
  }, () => {
    _this.setData( { loading: false });
  });
}

評論頁面的展示也是非常的簡單,一下給出長評模版,短評也是一樣的,裡面的點贊按鈕功能木有實現哦。

<view class="headline">
    <text>{{longCommentCount}}條長評</text></view><view class="common-list">
    <block wx:for="{{longCommentData}}">
        <view class="list-item has-img" data-id="{{item.id}}">
            <view class="content">
                <view class="header">
                    <text class="title">{{item.author}}</text>
                    <image class="vote" src="../../images/thumb_up.png" />
                </view>
                <text class="body">{{item.content}}</text>
                <text class="bottom">{{item.time}}</text>
            </view>
            <image src="{{item.avatar}}" class="cover" />
        </view>  
    </block></view>

4.png

主題日報

主題日報的樣式跟首頁幾乎一模一樣,我是拷貝過來修改了一點點(懶)。

卻別在多了一行主編區域。不過這個主編區域沒有實現什麼功能,

本來是點選主編的頭像跳轉到主編的個人首頁簡介,沒有時間安排就不做了,這也是需要解析html的(累)。

主題日報列表需要接受一個具體的主題日報id,根據這個id來請求介面獲取主題日報的日報列表。

//接受主頁傳遞過來的主題日報idonLoad: function( options ) {    this.setData( { id: options.themeId });
}

主題日報的請求列表方式和主頁的列表方式差不多,

由於沒有發現分頁引數,主題日報的日報列表這部分也沒有分頁請求。主題日報的日報詳情還是跳轉到日報詳情頁面的。

5.png

設定頁面

本來想做設定頁面裡列出的功能,

但是工作比較忙,還是歸入到後邊的完善計劃吧,現階段只做了簡單的頁面佈局。

但是還是講一下自己的思路

  • 夜間模式就是改變應用的顯示樣式,利用到了css,

  • 我們可以在page中放置一個頂層的view來包括起所有的wxml元素,當切換主題時給頁面頂層元素一個主題控制類。

<view class="light">
    ....</view><view class="night">
    ...</view>
onShow: function() {    var app = getApp(); 

this.setData({theme: app.globalData.theme});
}
<view class="{{theme}}">
    ...</view>

清除快取功能,當然是把臨時檔案和localStorage中的資料清空。

clearDataEvent: function() {
    wx.clearStorage(); //清除應用資料}

應用的無圖瀏覽模式跟主題的思路差不多,

就是判斷應用快取中的設定是否是無圖模式,如果是就在內容顯示的時候加一個判斷,根據這個值來判斷是否顯示圖片型別的內容。

onLoad: function() {    var app = getApp(); 
this.setData({imageMode: app.getImageMode()});
}
<view>
    <image wx:if="{{imageMode}}" src="..." />
    <!--或者-->
    <block wx:if="{{imageMode}}">
        <image src="..." />
    </block></view>

6.png

總結

問題

  • 蠻多圖片顯示不出來,不知到是為什麼,src路徑正常,以前的小豆瓣圖書也是有圖片列表,但是沒有出現這種情況。

  • 程式碼結構比較爛,很多地方都沒有優化處理,複用率較低,待重構。

  • 頁面佈局有些不合理,尺寸控制的不夠好。

  • 部分wxml沒有用模版功能代替重複的渲染工作,達不到複用效果。

閒語

本次編寫的小程式用到了蠻多知識點,雖然花費了不少時間,

但是一切都是非常的值得。

編寫的過程中遇到最大的困難就是解析html內容,

可以說是絞盡腦汁,哈哈,智商不足啦。

很期待能有網友能奉獻出更好的解決方法。這個小例子做的比較簡陋,

很多功能沒有完全實現,跟別人的Android和React仿客戶端相比,小巫見大巫啦。還得抽空完成後續的更多功能。

到目前為止,小程式已經更新了幾次,支援了ES5/ES6轉換、

下拉重新整理事件、上傳檔案等功能,不過還有很多API還不能在模擬環境下顯示效果。

自己覺得一直做類似於豆瓣圖書和知乎日報等除了網路請求之外沒什麼特別的地方的應用也不好,

需要嘗試新的API來擴充套件自己的視野,後續打算往未使用到的API進行案例製作。不知不覺已經踏出校園準備有4個月了,

很懷念以前的學習日子,做過很多案例,但是都沒有寫日誌和儲存的習慣。

這次寫的字數蠻多的,可累死我了。很幸運自己初入工作圈就能碰上小程式風暴,期待它正式公測!