窗体切换白屏的现实问题
HTML5的性能比原生差很多,比如切页时白屏、列表滚动不流畅、下拉刷新和上拉翻页卡顿。
在低端Android手机上,很多原生App常用的功能和体验效果都很难使用HTML5技术模拟。
我们首先来看第一个问题,如何避免切页白屏。
浏览器的页面在切换时,由于其页面加载机制,在跳转到下一个页面时,先要请求联网、载入页面代码、构建dom、渲染,最后才显示出来。
在最终结果渲染完毕前,会出现几十毫秒甚至数秒的白屏。原生App是没有这个问题的。
虽然使用SPA单页应用模型,即ajax+div切换也可以避免白屏,但把所有页面写在一个SPA页面里,页面多了手机上也跑不起来,而且工程大了代码那个乱。。。被坑过的人自然知道。
解决窗体切换白屏的4种方案
标准HTML5无法解决,我们就使用扩展的手段。
HTML5+是一套增强HTML5的规范,它可以用JS调用几十万原生API。
想要解决切页白屏这个问题,需要使用plus.webview类来做MPA多页应用。
plus.webview类是对原生的webview对象的js化封装,使用js可以操作webview。
解决白屏的原理是:把每个页面当作一个webview,但用js来控制它就像控制div一样。
因为webview可以隐式创建,后台载入内容,并且在载入完毕时有js事件通知,我们可以在新页面载入完成后再把它通过动画移入屏幕,从而避免白屏。
同时webview之间相互独立,不会出现SPA下不同页面js和css冲突的问题。
通过操作webview来避免切页白屏,有几种常见的做法:
一种是称之为预载,即后台预载新页面的HTML文件及资源,使用时直接调出这个已经创建好的webview;
另一种称之为现载,即点击前页的链接开始走waiting转圈,同时后台开始加载完整的新页面,加载完再用js控制显示到前台。
还有一种称之为分开载入,随后会细讲。
- 1、预加载
所谓预载,即后台预载新页面的HTML文件及资源,使用时直接调出这个已经创建好的webview。
Hello mui、csdn、36kr等项目源码,都使用了预载思路。
以新闻类app为例,启动首先载入资讯列表list页面,然后后台创建了一个隐藏的webview,加载了一个内容模板show页面。
在点击list页面的一个新闻item时,调用webview的窗体控制动画,把show页面侧滑进屏幕。
但show页面仅仅是一个模板而没有数据,在show页面刚侧滑进屏幕时,在show页面有一个“加载中”的提示。
紧接着show页面开始执行ajax请求,联网加载数据并显示出来。
我们可以在list页面的item点击里,一边移动窗体,一边通知新页面执行ajax。webview间相互传递消息使用webview的evalJS方法。
这种做法,相当于用户是在新的show页面来等待联网数据。
示例代码如下:
var webviewShow;
document.addEventListener('plusready', function(){ //扩展的js对象在plusready后方可使用
webviewShow = plus.webview.create("show.html");
});
function clicklist (id) { //list点击item后的事件
webviewShow.show("slide-in-right",150);
}
在mui框架里,对这个过程进行了简化封装,使用preload参数来控制。参考:http://dcloudio.github.io/mui/javascript/#preload
预加载,由于不显示出来,并不会过多增加资源占用。(同时显示在屏幕上的webview不要超过3个,隐藏在后台的webview不要超过20个)
如果是list转到content,不同的item点击只是一个页面,完全可以使用预载。
但如果页面不同且较多,后台预载太多webview会有点慢。这里的慢不是会造成手机慢,而是预载是耗时间的。
Hello H5+这个示例使用了预加载技术,有一些网页在启动时就被预载了。点击到这些网页时,切换会非常快。
之所以不预载所有的网页,不是内存不够,是时间不够。
预载这些网页时手机CPU消耗比较高,此时如果滑动列表,会发现滑动不流畅。
为了避免这个问题,Hello H5+的首页是延迟进入的,等待预载的几个页面都完成后才调用plus.navigator.closeSplashscreen()关闭启动封面图片。
如果预载太多页面,等待全部预载结束后再进入首页,会导致App启动非常慢。
- 2、现载
所谓现载,就是用户点击时后台创建webview加载新页面完整内容,渲染后再显示到前台
有时我们无法使用预载,为了避免白屏,就要等待新webview完成后再通过动画把它移进来。
当点击list页面的item时,首先通过plus.nativeUI.waiting()来弹出一个等待框。
紧接着在后台create一个webview,载入show页面。
show页面在后台联网获取数据。
show页面在数据解析渲染后,有两种方式通知list页面。
一种是在list页面注册show页面的webview的loaded回调,在新webview载入完成后系统会自动触发loaded事件。
还有一种是在show页面合适的js位置通过evalJS方法通知list页面关闭等待框,并执行窗体切换把show页面显示出来。
当然show页面也可以不通知list页面,而是自己直接关闭等待框,并调用窗体切换动画把自己显示出来。
示例代码如下:
function clicklist (id) { //list点击item后的事件
var nwaiting = plus.nativeUI.showWaiting();//显示原生等待框
webviewShow = plus.webview.create("show.html");//后台创建webview并打开show.html
webviewShow.addEventListener("loaded", function() { //注册新webview的载入完成事件
nwaiting.close(); //新webview的载入完毕后关闭等待框
webviewShow.show("slide-in-right",150); //把新webview窗体显示出来,显示动画效果为速度150毫秒的右侧移入动画
}, false);
}
需要注意的是,为了减少窗体切换的等待,一般不在点击后使用webview的create方法,因为创建过程也有时间消耗。
比较好的方法是提前创建一个webview,需要载入页面时使用webview的loadURL方法来载入。
如果中间还是出现白屏,可以通过延迟显示新webview来解决。
目前官方演示demo里,把新webview移入进来是在新webview的loaded回调中做的,这个时机相当于新页面的DOMContentLoaded时机,即dom tree完成但页面未必渲染完毕。
此时可以settimeout延迟新webview的显示动画,也可以干脆不在上级页面操作显示动画,而是在下级页面的onload或你认为页面已经渲染完毕的时间来操作把下级页面用动画显示出来。
- 3、head和body分开载入
了解以上两种方式后,我们可以玩些更复杂的。
一般窗体都是2部分组成,顶部的标题栏(带返回按钮)和中间的内容区,或称body区。
我们把它分成2个webview,一个叫webviewHead,一个叫webviewBody。(名字是随便起的,方便下面引用)
我们可以在首页先预载webviewHead,载入一个head.html的页面,这个HTML里上面是title内容+back按钮或其他菜单按钮,其中间body区只有一行字“加载中...”
然后再预载一个webviewBody,先放着不用。
在点击切换窗体时,首先用窗体动画直接把这个已经预载的webviewHead移入窗体内,此时用户会看到一个新界面,上面显示加载中字样。
同时js代码操作webviewBody的loadURL,给它载入你需要载入的页面,等待这个webview的loaded触发后,再把它append到webviewBody里。
这样,可以做到不预载,也能让用户尽可能的感受到窗体的流畅切换。
Hello mui的窗体切换,采用了这种方式。大家可以去看它的实现代码。
使用这种方式是要注意:head.html的body区域的背景色和webviewBody里载入的页面的背景色应该统一,这样在把webviewBody append到webviewHead时才不会突兀。
另外,head和body分开载入,对于body的显示会略有延迟。如果body是静态内容,在iOS上由于性能很好,HTML的页面切换比较快,其实head和body合并为一个页面直接载入会更快点。
-4、预截图
预加载可以避免白屏的发生,但窗体动画有时还不如预期流畅,有些新窗体移入过程中,还在不停联网获取数据,不停重绘界面,导致窗体进入过程感觉卡顿,此时还有一个高级技巧是截图动画。
从HBuilder6.1起,5+ runtime提供了一个plus.nativeObj.Bitmap的对象,同时webview对象提供了一个截图方法,可以把webview显示区域保存到bitmap对象中。此外webview的动画方法中支持传bitmap,这样给开发者提供了一个性能调优的手段。
我们可以预载一个webview,然后把这个webview预先截图下来,然后在窗体移入时在动画参数里传入保存这个截图的bitmap对象,这样窗体移动时,移动的就不是webview,而是移动的图片,这样能让窗体动画流畅许多。
当然这个方案也不是万能,它有利有弊,因而也有其适用场景。
截图以后图即固定,窗体动画过程中,图不会改变,如果webview自身在变或外部通过evaljs在控制其变化,那动画过程中无法反应该变化,
几种窗体切换方式的比较和适用场景
大家搞明白webview的原理后,其实可以灵活的根据自己的需求应用webview。
但我们还是归纳一下之前提到的3种切换方式适用的场景,当然我们也会提到SPA的适用场景。
1. 如果是类似于新闻资讯或公文列表等应用,每个列表item点击后打开的是同一个页面。应该使用预载的方式,直接preload,然后每次在这个show页面里面通过ajax请求服务器来更新数据。
2. 如果是类似九宫格导航,或每个列表item去往不同的页面,如果页面数量低于10个,可以在启动时直接预载这10个webview。
如果页面太多,无法全部预载,建议把导航拆成二级导航。
比如带tab或segment的九宫,或者二级导航列表,在展开二级导航时做预载。
如果业务场景合适,还可以做智能预载,判断用户空闲时间和接下来可能要点击的页面来预载。当然高手才能搞定这么复杂的代码。
3. 如果页面因为各种条件无法预载,至少把能预载的预载一部分,剩余的按如下方式处理:
- 3.1 如果都是本地页面不联网的简单页面。
比如很多软件的设置界面,其内部的二级页面跳转,内容都很简单,也不涉及区域滚动卡的问题,此时SPA比较好。确实其实SPA也不是一无是处,它适用的场景下用它是比较好的。当然这么做虽然体验好,但一个App里一会MPA、一会SPA,一般开发者可能会头晕。那些追求极致体验的高级开发者可以通过混合MPA和SPA,博采众家之长来达到最佳用户体验。
而对于普通开发者,其实统一使用head和body分离的多webview方式其效果也完全可以商用。
在Hello mui里,下方有setting模板,就是一个spa的样例。在iOS上,这个样例的效果可以完全达到原生效果,顶部标题栏的渐变非常酷。
- 3.2 如果是本地页面不涉及联网但下拉刷新或超长复杂图文列表,那还是head和body分开载入的方式比较好。
因为SPA的复杂div滚动和下拉刷新在低端Android手机很卡。
比如一些列表页面,本地有缓存的内容,从websql等本地数据库里加载内容,通过下拉刷新的方式更新数据,这类页面本身设计就应该是双webview的。
- 3.3 如果是联网获取内容的页面,使用head和body分开载比较合适
如果切一个页面,要等待新页面载入,新页面载入后还要等待联网载入数据,连续2个等待框就会让用户反感。
反正也是要等待联网,干脆就和等待webview加载一起等了。
先把预载的webviewHead移入屏幕,然后等待webviewBody载入HTML及联网获取数据,一并载入完毕后把webviewBody拍到主webview上。
mui框架的窗体函数封装
mui框架为了简化窗体管理的工作,把一些常用的窗体模型做了简化封装。
但对于复杂的窗体切换,仍需开发者搞明白上面提到的窗体切换原理。
mui的init方法,通过参数封装了preload和subpage,这样就可以方便的预载webview,对于head和body分离的双webivew界面,也可以方便的通过subpage参数来控制webviewBody。
mui的openWindow方法,封装了显示waiting,载入新页面,处理动画,关闭waiting等工作。
mui的back样式控制,自动封装了窗体的隐藏和关闭。
这些方法具体参考mui的js API。
Hello H5+和Hello mui的窗体切换比较
Hello H5+和Hello mui都是简单的本地静态页面,但页面数量非常多,导致无法全部预载。
所以其实很多实际的App可以通过预载实现比这2个示例App更好的窗体切换效果。
Hello H5+的转场策略是:
1. 预载一些常见的,比如主列表前2个webview,以及窗体切换和下拉刷新的webview。
2. 其他的窗体采用了“现载”的方式,即点击item后立即弹waiting,然后后台加载webview,载入完毕后移入屏幕。
Hello mui的转场策略是:
整体采用head和body分开载入的方式。
从直观的感受看2个示例,在iOS上,Hello H5+效果更好。在Android上,各有千秋。
但静态示例的设计和实际业务场景不同,由于实际业务大量存在联网内容,所以其实head和body分开载入更实用。
mui封装了head和body分开载入的模板设计,而Hello H5+只是api演示,不会做封装,所以造成2个示例的转场方式不同。
mui的窗体切换,这里有一篇单独的文章描述http://ask.dcloud.net.cn/article/106
启动后首页的白屏
首页是没有预加载的概念的。
启动封面的图片关闭触发条件,默认是在首页的webview的loaded事件发生后关闭。
如果首页内容较大或联网后、框架载入后重绘屏幕,即在首页HTML的DOMContentLoaded后无法立即渲染界面,会出现启动封面图片消失后,页面还没渲染好的情况。
此时需要手动控制封面图片消失。
首先在工程下manifest.json里找到plus、splashscreen、autoclose节点,设置为false,即手动控制封面图片的消失。
然后在首页合适的位置,一般在联网并构造完新的dom时,调用js关闭封面图片,plus.navigator.closeSplashscreen();
这样就能防止第一个页面的白屏。
关于Android手机返回时页面会先模糊后清晰的处理方法
为了节约系统资源,在webview不可见时,我们的引擎默认会回收掉它的渲染资源。
如果页面复杂、渲染的慢,在返回时可能会因为来不及渲染而造成先模糊后清晰的问题。
此时或者优化页面写法,加快渲染。或者使用我们提供的api,使得webview在不可见时一样不移除渲染资源。
具体API地址见plus.webview的webviewStyle对象里的render参数,render设为always即可不移除渲染,解决模糊的问题。http://www.html5plus.org/doc/zh_cn/webview.html#plus.webview.WebviewStyle
另外从HBuilder5.8开始,Android上新引入了pop-in动画,这个动画效果经过特殊处理,在返回时不会虚一下。
关于因Android硬件加速配置不当导致的闪屏、滚动不流畅、视频无画面等问题
5+runtime默认是开启Android硬件加速的,但Android的一些非google官方rom的硬件加速有bug。尤其是Android5.0初期的一些rom。
关于这方面的处理方案,单独起了一篇文章,参考http://ask.dcloud.net.cn/article/55
关于pop-in挤压动画和slide-in-right右移动画的取舍
plus.webview提供了很多切换动画,上下左右平移、淡入淡出、缩放、挤压......但比较常用的动画是右移slide-in-right和挤压pop-in。
一般在iOS上,强烈推荐使用pop-in,更接近原生体验。
在Android上,其实也是pop-in效果更好,但为了达到更好的效果,也需要开发者编码时注意一些写法,如果写不好,效果还不如slide-in-right
这里是pop-in动画使用注意:http://ask.dcloud.net.cn/article/225
后记
不管使用哪种方法,都要注意一点,手机App的HTML页面必须本身性能足够高。
页面体积要小、加载和渲染要快。
互联网上有很多提升HTML、JS、CSS性能的方案,此处不再罗列。
但注意一点,如非必要,不要使用框架。
pc上web框架的盛行,也是后来pc浏览器性能足够高之后的事情,互联网发展初期的开发者并不像如今这般依赖框架。
手机,尤其是低端Android机的性能也很差,如果照着写pc web的思路写页面,最终的用户体验必然会非常差。
首先,AMD框架不要想了,包括angularjs在内,js动态解析标签再替换渲染是很慢的。
其次,jquery、zepto也尽量不要使用。document.getElementById("") 、document.querySelectorAll("")、$(""),这三者性能依次下降,尤其是在低端Android上遍历dom时,当你辛辛苦苦减少白屏和用户等待时间时,你会非常愤怒这些js框架拖了你的后腿。
并且HBuilder提供了很多代码块来快速完成代码,比如敲dg就可以出document.getElementById(""),比敲$("#")要快多了。
当然个别页面为了使用一些现成的jquery插件而引用了框架,倒也不会对app整体产生太大影响,这需要开发者自己根据产品对性能追求的极致程度来把握了