本人對知乎日報是情有獨鍾,看個人博客和github就知道了,寫了幾個不一樣技術類型的知乎日報APP
要作微信小程序首先要對html,css,js有必定的基礎,還有對微信小程序的API也要很是熟悉javascript
我將該教程分爲如下三篇css
三篇分別講不一樣的組件和功能塊html
這篇要講java
日報的內容也是最難作的,由於接口返回的內容是html…,天呀,是html!小程序肯本就不支持,解析html的過程很是痛苦,由於本人的正則表達式只是幾乎爲0,解析方案的尋找過程很虐心,經典的jQuery是用不了了,又沒有dom
,沒法用傳統的方式解析html。嘗試了正則學習,可是也是沒法在短期內掌握,尋找了不少解析庫,大可能是依賴瀏覽器api。不過,上天是不會忽視有心人的,哈哈,仍是被我找到了解決方案。幸運的我發現了一個用正則編寫的和相似與語法分析方法的xml解析庫。這個庫是一個very good的網友封裝的html解析庫。詳情點擊 用Javascript解析html。git
因爲日報詳情內容的html部分結構太大,這裏只列出了簡要的結構,這個結構是通用的(不過不保證知乎會變更結構,要是變更了,以前的解析可能就沒用了…心累)github
<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
就能夠獲得全部的段落。以後再解析出段落中的圖片。正則表達式
如下是詳情頁的內容展現模版json
<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>