上一次的《微信小程序之小豆瓣图书》制作了一个图书的查询功能,只是简单地应用到了网络请求,其他大多数小程序应有的知识。而本次的示例是知乎日报,功能点比较多,页面也比上次复杂了许多。在我编写这个DEMO之前,网上已经有很多网友弄出了相同的DEMO,也是非常不错的,毕竟这个案例很经典,有比较完整的API,很值得模仿学习。本次个人的DEMO也算是一次小小的练习吧。
由于知乎日报是一个资讯类的App,UI的布局主要是以资讯列表页、资讯详情页和评论页为主,当然本次也附带了应用设置页,不过现阶段功能尚未编写,过段时间会更新补充,继续完善。
API分析
本次应用使用了知乎日报的API,相比上次豆瓣图书的数量比较多了,但是部分仍然有限制,而且自己没有找到评论接口的分页参数,所以评论这块没有做数据的分页。
以下是使用到的具体API,更加详细参数和返回结构可参照网上网友分享的 知乎日报-API-分析 ,在此就不做再次分析了。
启动界面图片
http://news-at.zhihu.com/api/4/start-image/{size}
参数 | 说明 |
---|---|
size获取刚进入应用时的显示封面,可以根据传递的尺寸参数来获取适配用户屏幕的封面。获取最新日报 |
http://news-at.zhihu.com/api/4/news/latest
返回的数据用于日报的首页列表,首页的结构有上下部分,上部分是图片滑动模块,用于展示热门日报,下部分是首页日报列表,以上接口返回的数据有热门日报和首页日报
获取日报详细
http://news-at.zhihu.com/api/4/news/{id}
参数 | 说明 |
---|---|
id在点击日报列表也的日报项时,需要跳转到日报详情页展示日报的具体信息,这个接口用来获取日报的展示封面和具体内容。历史日报 |
http://news.at.zhihu.com/api/4/news/before/{date}
参数 | 说明 |
---|---|
date这个接口也是用与首页列表的日报展示,但是不同的是此接口需要传一个日期参数,如20150804格式。获取最新日报接口只能获取当天的日报列表,如果需要获取前天或者更久之前的日报,则需要这个接口单独获取。日报额外信息 |
http://news-at.zhihu.com/api/4/story-extra/{id}
参数 | 说明 |
---|---|
id在日报详情页面中,不仅要展示日报的内容,好需要额外获取此日报的评论数目和推荐人数等额外信息。日报长评 |
http://news-at.zhihu.com/api/4/story/{id}/long-comments
参数 | 说明 |
---|---|
id日报的评论页面展示长评用到的接口(没有找到分页参数,分页没有做)日报短评 |
http://news-at.zhihu.com/api/4/story/{id}/short-comments
参数 | 说明 |
---|---|
id日报的评论页面展示段评用到的接口(没有找到分页参数,分页没有做)主题日报栏目列表 |
http://news-at.zhihu.com/api/4/themes
主页的侧边栏显示有主题日报的列表,需要通过这个接口获取主题日报栏目列表
主题日报具体内容列表
http://news-at.zhihu.com/api/4/theme/{themeId}
参数 | 说明 |
---|---|
themeId在主页侧栏点击主题日报进入主题日报的内容页,需要展示此主题日报下的日报列表。代码编写启动页 |
作为一个仿制知乎日报的伪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 );}
首页
轮播图
首页顶部需要用到轮播图来展示热门日报,小程序中的Swipe组件可以实现。
{{item.title}}
所有的内容都必须要在swiper-item标签中,因为我们的图片不止有一张,而是有多个热门日报信息,需要用循环来展示数据。这里需要指定的是image里的属性mode设置为aspectFill是为了适应组件的宽度,这需要牺牲他的高度,即有可能裁剪,但这是最好的展示效果。toDetailPage是点击事件,触发跳转到日报详情页。在跳转到日报详情页需要附带日报的id过去,我们在循环列表的时候把当前日报的id存到标签的data中,用data-id标识,这有点类似与html5中的data-*API。当在这个标签上发生点击事件的时候,我们可以通过Event.currentTarget.dataset.id来获取data-id的值。
日报列表
列表的布局大同小异,不过这里的列表涉及到分页,我们可以毫不犹豫地使用scroll-view组件,它的scrolltolower是非常好用的,当组件滚动到底部就会触发这个事件。上次的小豆瓣图书也是使用了这个组件分页。不过这次的分页动画跟上次不一样,而是用一个附带旋转动画的刷新图标,使用官方的动画api来实现旋转。
0}}">{{it.value}}{{it.value}}{{it.value}}{{it.value}}{{it.value}}查看知乎讨论
可以看出模版中的内容展示部分用了蛮多的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 ]; //获取的内容 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 ) ) { //获取加粗段落...strong = decodeHtml( strong, isDecode ); if( !strong.isEmpty() )content.push( { type: 'pstrong', value: strong }); } else if( isOnly( p, em ) ) { //获取强调段落 ...em = decodeHtml( em, isDecode ); if( !em.isEmpty() )content.push( { type: 'pem', value: em }); } else if( isOnly( p, blockquote ) ) { //获取引用块 ...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 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在这里就被使用了。本来自己是想把推荐人数和评论数显示在底部的图片右上角,但是由于本人的设计问题,底部的字号已经是很小了,显示数量的地方的字号又不能再小了,这样看起来数字显示的地方和图标的大小几乎一样,很是别扭,所以就不现实数字了。这块还是有很多待完善的功能的,比较收藏功能和是否有评论提示功能等。
底部有分享、收藏、评论和点赞按钮,分享肯定是做不了啦,哈哈,但是效果还是需要有的,就一个modal弹窗,显示各类社交应用的图标就行啦。
底部工具栏中还有一个按钮是刷新,其实就是一个重新调用接口请求数据的过程而已。
//重新加载数据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 }); });}
评论页面的展示也是非常的简单,一下给出长评模版,短评也是一样的,里面的点赞按钮功能木有实现哦。
{{longCommentCount}}条长评{{item.author}}{{item.content}}{{item.time}}
主题日报
主题日报的样式跟首页几乎一模一样,我是拷贝过来修改了一点点(懒)。却别在多了一行主编区域。不过这个主编区域没有实现什么功能,本来是点击主编的头像跳转到主编的个人首页简介,没有时间安排就不做了,这也是需要解析html的(累)。
主题日报列表需要接受一个具体的主题日报id,根据这个id来请求接口获取主题日报的日报列表。
//接受主页传递过来的主题日报idonLoad: function( options ) { this.setData( { id: options.themeId });}
主题日报的请求列表方式和主页的列表方式差不多,由于没有发现分页参数,主题日报的日报列表这部分也没有分页请求。主题日报的日报详情还是跳转到日报详情页面的。
设置页面
本来想做设置页面里列出的功能,但是工作比较忙,还是归入到后边的完善计划吧,现阶段只做了简单的页面布局。
但是还是讲一下自己的思路
夜间模式就是改变应用的显示样式,利用到了css,我们可以在page中放置一个顶层的view来包括起所有的wxml元素,当切换主题时给页面顶层元素一个主题控制类。
那怎么实现换肤立即生效呢?一个页面刚启动是会经过onLoad、onShow等,当第二次进来的时候页面的onLoad事件就不会在次触发,而是触发onShow事件,我们可以通过onShow事件来获取存在全局缓存中的主题设置。
onShow: function() { var app = getApp(); this.setData({theme: app.globalData.theme});}
...
clearDataEvent: function() { wx.clearStorage(); //清除应用数据}
onLoad: function() { var app = getApp(); this.setData({imageMode: app.getImageMode()});}
总结
问题
闲语
本次编写的小程序用到了蛮多知识点,虽然花费了不少时间,但是一切都是非常的值得。编写的过程中遇到最大的困难就是解析html内容,可以说是绞尽脑汁,哈哈,智商不足啦。很期待能有网友能奉献出更好的解决方法。这个小例子做的比较简陋,很多功能没有完全实现,跟别人的Android和React仿客户端相比,小巫见大巫啦。还得抽空完成后续的更多功能。
到目前为止,小程序已经更新了几次,支持了ES5/ES6转换、下拉刷新事件、上传文件等功能,不过还有很多API还不能在模拟环境下显示效果。自己觉得一直做类似于豆瓣图书和知乎日报等除了网络请求之外没什么特别的地方的应用也不好,需要尝试新的API来扩展自己的视野,后续打算往未使用到的API进行案例制作。不知不觉已经踏出校园准备有4个月了,很怀念以前的学习日子,做过很多案例,但是都没有写日志和保存的习惯。这次写的字数蛮多的,可累死我了。很幸运自己初入工作圈就能碰上小程序风暴,期待它正式公测!
现阶段比较完整的效果动态图
本次示例的源码地址:
https://github.com/oopsguy/WechatSmallApps
http://git.oschina.net/oopsguy/WechatSmallApps
部分图片显示不了的问题已得到修复,感谢热心网友pull request
如果大家喜欢,给个star激励一下我,以后会有更好的作品与大家分享:)
来源:oopsguy