本人對知乎日報是情有獨鍾,看我的博客和github就知道了,寫了幾個不同技術類型的知乎日報APP
要做微信小程序首先要對html,css,js有一定的基礎,還有對微信小程序的API也要非常熟悉
我將該教程分為以下三篇
三篇分別講不同的組件和功能塊
這篇要講
- 日報詳情頁
- 底部工具欄
- 評論頁面
日報詳情頁
日報的內容也是最難做的,因為接口返回的內容是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"> <img class="avatar" src="http://pic1.zhimg.com/e53a7f35d5b1e27b00aa90a2c1468a8c_is.jpg"> <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 = transferSign(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() ) { //獲取圖片 img=img.replace("pic1","pic3"); img=img.replace("pic2","pic3"); 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 } /** * 將轉義字符轉為實體 * @param data * @returns {*} */ function transferSign(data){ data=data.replace(/–/g,"–"); data=data.replace(/—/g,"—"); data=data.replace(/…/g,"…"); data=data.replace(/•/g,"•"); data=data.replace(/’/g,"’"); data=data.replace(/–/g,"–"); return data; }
代碼的解析過程比較繁雜,大家可以根據返回的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" bindtap="collectOrNot" wx:if="{{isCollect}}"><image src="../../images/star_yellow.png" /></view> <view class="item" bindtap="collectOrNot" wx:else><image src="../../images/star.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.png" /> </view> </view> </view>
底部有分享、收藏、評論和點贊按鈕,收藏功能主要用到數據的儲存,存在就去掉后儲存,不存在就添加后儲存
collectOrNot: function() { var pageData = wx.getStorageSync('pageData') || [] console.log(pageData); if (this.data.isCollect){ for(var i=0;i<pageData.length;i++){ if (pageData[i].id==this.data.id){ pageData.splice(i,1); this.setData( { isCollect: false }); break; } } }else { var images=new Array(this.data.news.image); var item ={id:this.data.id,title:this.data.news.title,images:images}; console.log(item); pageData.unshift(item); this.setData( { isCollect: true }); } try { wx.setStorageSync('pageData',pageData); } catch (e) { } console.log(pageData); }
分享肯定是做不了啦,哈哈,但是效果還是需要有的,就一個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 ); },
評論頁面
評論頁面蠻簡單的,就是展示評論列表,但是要展示兩部分,一部分是長評,另一部分是短評。長評跟短評的布局都是通用的。進入到評論頁面時,如果長評有數據,則先加載長評,短評需要用戶點擊短評標題才加載,否則就直接加載短評。這需要上一個詳情頁面中傳遞日報的額外信息過來(即長評數量和短評數量)。
之前已經在日報詳情頁面中,順便加載了額外的信息
//請求日報額外信息(主要是評論數和推薦人數) 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>




