最近做一个项目,是将一个相对复杂(内容后台模块化配置)的mobile web页面嵌入到Android的webview展示,把遇到的问题和一些经验总结下
https://github.com/Dianjoy/gamepop
(1)图片!图片!图片!
我觉得不管是原生App还是Web App,加载优化的第一条就是合理的设置图片,这点往往容易被忽视。一切只在WIFI环境下的测试都是耍流氓!
这个项目的主页面,一开始前端负责切图的同事给出的静态页居然有1M多,其中最大的一张banner图接近300K! 直接从PSD切出来的高保真原汁原味的展示效果确实震撼,百分比布局下,在chrome放到全屏显示还是清晰无比。理想很丰满,现实却骨感,可惜我们不是生活在Provo,没有google fiber的情况下只能忍痛牺牲这种“网络不能承受之美”。wap页面就是手机上看的,一般4~5寸屏幕能清晰显示,6寸‘巨屏’牺牲点效果不影响使用就足够了。
目前总结大致的图片组成
- 横铺图片,大概占全屏的1/5~1/4左右的图片,建议30K左右
- 橱窗图,宽度1/4~1/2方图,8~15K
- 加载占位图、loading动画 单色,质量调低,1.5K
- 多个小图片,最好合成一张用css sprite布局,webview里的http请求很慢,能省则省
- 什么时代了,一般的渐变 圆角样式能用css3就一定不能老土再用图片了!
- 一些小图,可以base64成字符串,用css data:image保存(这个持保留意见,不直观,而且增加了css文件的体积,这种字串一般gzip压缩也不会变小多少)
(2)使用zepto.js代替jquery
或许你是javascript大牛建议一切用原生,但是简单的选择器和DOM操作肯定没有问题,何况手机上不用可以把大量IE兼容的代码直接忽略(暗爽)。但是真正做webapp,稍微复杂点还是需要使用一些插件,每个功能都用野生js重写,难度和稳健性先不说,代码也会越来越臃肿难以维护。(野生Javascript怎么也称不上优雅)
那么为什么强烈建议用zepto.js代替jquery呢,这可绝不仅仅因为gzip后差别20K的文件体积,而是因为Android Webview奇葩的js解析效率和更奇葩的onPageFinished事件,总之一旦用了jquery,页面的白屏loading肯定会多滚很多圈,宝贵的加载时间浪费在一个个用不到的函数对象的建立和兼容判断语句里了。
而用zepto.js可以有明显的改善,而且基本的选择器、DOM操作、ajax,写起来和jquery是完全一样的,无痛迁移,个别插件不兼容,往往也只需要把最后闭包外的(jQuery)改成($,window,document)就可以了。常用的插件一般也可以在github上找到zepto.js compatible version
(3)先载入DOM,延时加载和执行js
奇怪,这不就是$(document).ready和window.onload的却别么?糊弄谁呢
但确实不是这么简单,主要原因就在于Android Webview的onPageFinished事件,Android端一般是用这个事件来标识页面加载完成并显示的,也就是说在此之前,会一直loading,但是
Android的OnPageFinished事件会在Javascript脚本执行完成之后才会触发。如果在页面中使用JQuery,会在处理完DOM对象,执行完$(document).ready(function() {});事件自会后才会渲染并显示页面。(参见 http://hi.baidu.com/goldchocobo/item/9f7b0639f3cd2efe96f88dfb)这篇文章。文中使用的lazyload.js已经有了版本更新,语法也发生了变化,这样用即可
<script src= "js/lazyload.min.js" ></script> <script> function loadComplete(){ //do something } //针对Android webview渲染js慢的问题,延时加载 function loadscript(){ LazyLoad.js([ 'js/zepto.min.js' , 'js/jquery.lazyload.min.js' , 'js/mustache.js' , 'js/flowtype.js' ], loadComplete); } setTimeout(loadscript,10); </script> |
这里的关键就是setTimeout(loadscript,10),这个语句就是Webview里页面加载显示和载入和执行其它js和页面渲染事件的分水岭。把原来放在$(document).ready里面的主体程序放在loadComplete里面就行了。
经过测试,这个对包含复杂js的页面在webview中加载的提升最明显,如果你的页面一直在傻乎乎的loading loading loading.. 最好试一下这个办法。
不过我们的主体页面初始什么内容都没有,所有DOM都需要mustache根据api的配置,从模板中render,所以Android交了兵权之后还要在页面上空白或者显示自定义的loading图一小会,不过绝对比之前那种体验要明显快的多(大概15秒=>5秒的样子)。
(4)图片懒加载
原因还是因为不在Provo,注意此lazyload非彼lazyload,这里是jquery.lazyload,小改动就可以支持zepto.js
这个插件很常见,最好还是去github主页https://github.com/tuupola/jquery_lazyload/看用法,手机上调用的时候最好加上 threshold:300,否则滚动,由占位图加载的等待时间还是有点明显。
如果滚动加载失效(找不到原因),可以试试在lazyload之后加一条
$(window).trigger( "scroll" ); |
就可以了。另外lazyload占位图虽然小,但是最好能提前加载到缓存,这样页面显示的时候高度不会突变,把不同宽高比的占位图放在<body>不显示即可
< img src = "upload/images/other/load_full.jpg" style = "display:none;" /> < img src = "upload/images/other/load_half.jpg" style = "display:none;" /> |
(5)使用LocalStorage缓存DOM
如果你的页面主体和我们这次一样,初始的DOM只有一个loading甚至空白,所有的内容都需要api获取接口数据,然后根据模板(比如mustache.js)render之后在append到DOM里的话,那么不管怎么优化,每次还都是需要等待那么一会儿,api请求接收和js模板引擎的处理在webview上都明显的慢。
而有些页面虽然需要后台配置,但并不是那么动态,像一个商城的首页这种,即使前端显示更新不那么即时,也不是很大的问题,刷新或者下次进入再显示最新版本也可以接受甚至是更好的用户体验。
我们这里把第一次mustache render好的html块,存入LocalStorage,然后下次进入页面的时候,先直接从LocalStorage中读取并显示,api读取和模板渲染后的新DOM再更新到LocaStorage中(如果有必要,可以在这个时候,比较下新旧是否相同,不同再更新一次DOM)
function jq_lazyload(){ $( "div#page_all img.lazy" ).lazyload({threshold:300, load : function (e){$( this ).next( 'b' ).hide();$( this ).removeClass( 'lazy' );}}); $(window).trigger( "scroll" ); } function loadComplete(){ //omit ... //如果用localstorage则先lazyload img if (window.localStorage){ if (localStorage.getItem( 'dom_all' )){ jq_lazyload(); } } $.ajax({ url:server_url, dataType: "json" , type: "GET" , success: function (json){ var dom_all= "" ; for ( var i=0; i<json.floors.length; i++){ var style_this = json.floors[i].style; dom_all+=Mustache.render($( '#floor_tpl_' +style_this).html(), json.floors[i]); } if (!window.localStorage || !localStorage.getItem( 'dom_all' )){ document.getElementById( "page_all" ).innerHTML = dom_all; jq_lazyload(); } localStorage.setItem( 'dom_all' ,encodeURIComponent(dom_all)); dom_all= null ; //释放内存 } }); } function loadscript(){ if (window.localStorage){ if (localStorage.getItem( 'dom_all' )){ document.getElementById( "page_all" ).innerHTML = decodeURIComponent(localStorage.getItem( 'dom_all' )); } } LazyLoad.js([ 'js/zepto.min.js' , 'js/jquery.lazyload.min.js' , 'js/mustache.js' , 'js/flowtype.js' ], loadComplete); } setTimeout(loadscript,10); //处理Webview未lazyload完,进入其它页面,js中止,返回不执行 window.ontouchstart = function (e){ jq_lazyload(); } |
(6)Webview的设置
webview本身的设置也很重要,特别是cache和localStorage是否开始,是否app退出再进入就不存在了,各自空间有多大,这些需要和Android开发的同事沟通好,说不定就是一行参数设置,体验就大不同
- Cache开启和设置
//下面3个是跟浏览器缓存Cache相关的,一个页面的 图片jscss 载入过之后
//在服务器设置的文件有效期内,每次请求,会去服务器检查文件最后修改时间,如果一致,不会重新下载,而是使用缓存
browser.getSettings().setAppCacheEnabled( true ); browser.getSettings().setAppCachePath( "/data/data/[com.packagename]/cache" ); browser.getSettings().setAppCacheMaxSize( 5 * 1024 * 1024 ); // 5MB |
- LocalStorage相关设置
//下面是跟浏览的LocalStorage有关的,像首页的DOM,第一次载入,需要从服务器ajax请求接口json配置数据,然后用js从模板中渲染拼接成DOM,显示在页面中
//由于Android webview的JS处理很慢,这里把第一次渲染后的DOM存入LocalStorage中,以后打开页面不用请求API和JS渲染,优先加载页面,和Cache配置,速度会快很多
//但是Android webview的LocalStorage有个问题,关闭APP或者重启后,就清楚了,所以需要下面browser.getSettings().setDatabase相关的操作,把LocalStoarge存到DB中
browser.getSettings().setDatabaseEnabled( true ); browser.getSettings().setDomStorageEnabled( true ); String databasePath = browser.getContext().getDir( "databases" , Context.MODE_PRIVATE).getPath(); browser.getSettings().setDatabasePath(databasePath); myWebView.setWebChromeClient( new WebChromeClient(){ @Override public void onExceededDatabaseQuota(String url, String databaseIdentifier, long currentQuota, long estimatedSize, long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater) { quotaUpdater.updateQuota(estimatedSize * 2 ); } } |
- 浏览器自带缩放按钮取消显示
//这个是跟浏览器的页面缩放相关,不用显示浏览器的放大缩小按钮,这个一般在最下面出现,体验不好
browser.getSettings().setBuiltInZoomControls( false ); |
(7)服务器端设置 gzip etag Cache-Control
gzip就不说了,总之一定要开启html css js json的gzip压缩!!!
为了弄明白这个,非科班出身的我连着fiddler边调测边翻了小半本<计算机网络>的书,其实也还没完全弄明白。而且测试发现现在的浏览器特别是桌面的360(#Anti-360#)和一些国产手机浏览器,为了制造“极速”的假象,缓存处理很多地方都没有按照规范来,动不动就会过度缓存,导致页面不能及时更新。Android Webview的LOAD_CACHE_ELSE_NETWORK设置更是完全无视etag、expire time这些,强制使用缓存。
总之,这块还没完全弄明白,等后面彻底明白了再结合fiddler和apache总结下吧。给出我这边apache .htaccess相关配置
<IfModule mod_deflate.c> AddOutputFilter DEFLATE html xml php js css json </IfModule> <IfModule mod_headers.c> <FilesMatch "\.(ico|jpe?g|bmp|png|gif|swf|css|js|json)$"> Header set Cache-Control "max-age=2692000, public" </FilesMatch> <FilesMatch "\.(php|html)$"> Header set Cache-Control "max-age=60, private, must-revalidate" </FilesMatch> Header unset ETag </IfModule> |
(8)以上都不是
其实Hybrid App的最佳实践,还是应该把所有的html css js和主要的图片资源离线存储在Android的asset文件夹下,然后由Android实现从服务器端到手机的这个www主文件夹的更新机制,这样才不用凡事从server端下载(很多人讨论webapp时只大谈特谈性能,其实一切需要加载的实现方式才是最大的“阻塞”)。这样也可以随心所欲的使用一些Sencha Touch或AngularJS+UI这样的中型和重型框架,可惜上面提到的文件更新机制没有建立,暂时还没有机会实践这种模式。这种想法的文章不多,参考http://developer.appcelerator.com/question/146564/update-apps-local-html-webviewed-files的reply部分
就到这里吧…