引子 web?native?这是个争论了很久的问题。 自从微信开放了更多的js接口之后,移动web开发重新火了起来,前端程序猿也水涨船高。 毫无疑问,web页面有诸多优点: 跨平台:一次开发,可以同时在android,ios和other phone上运行(美好的愿望) 快速迭代,内容
引子web?native?这是个争论了很久的问题。
自从微信开放了更多的js接口之后,移动web开发重新火了起来,前端程序猿也水涨船高。
毫无疑问,web页面有诸多优点:
跨平台:一次开发,可以同时在android,ios和other phone上运行(美好的愿望)
快速迭代,内容统一:内容修改后,不同版本都能展示最新内容。可以避免频繁的客户端升级,也无需经过app store的审核
语言优势:庞大javascript开发人员,能够带来移动端内容的繁荣
本人多年开发研究web与native混合app,因为这里面有很多的坑,很有必要把经验归纳一番。
准备区分app和浏览器我们的web页面是通过哪个应用打开的呢?这是要解决的第一个问题,这样才能做到app与系统浏览器的内容差异化。
通过域名
将不同用途的页面归类到不同服务器或web项目下,这是最简单也最笨的方法,如果同一个页面要在三端上都展示,那么就要复制3份了
通过元数据
通过ua标识
这是web页面统计访问终端的品牌和分辨率的常用方法
我会在webview的默认ua后面,加上自定义的标识,包括app的标识 (androidapp、iphoneapp)、应用包名和app的版本号
判断是否微信浏览器就可以使用这个方法,同时最好检查是否加载了微信自定义的weixin.js文件
配置webview基础参数final websettings settings = getsettings();// 允许js弹出提示框settings.setjavascriptcanopenwindowsautomatically(true);// 允许web执行jssettings.setjavascriptenabled(true);// 设置浏览器标识settings.setuseragentstring(buildappuseragent(getcontext(), settings.getuseragentstring()));// 是否支持缩放,默认不支持(看起来更native)settings.setsupportzoom(false);
settings.setbuiltinzoomcontrols(false);// 打开h5的离线缓存settings.setappcacheenabled(true);final string cachedir = getcontext().getdir("cache", context.mode_private).getpath();
settings.setappcachepath(cachedir);// 打开h5的dom storage(localstorage,sessionstorage)settings.setdomstorageenabled(true);// 如果要使用离线缓存和domstorage必须要设置web databasefinal string dbdir = getcontext().getdir("database", context.mode_private).getpath();
settings.setdatabaseenabled(true);
settings.setdatabasepath(dbdir);
webviewclient对象监听页面加载情况(开始加载、资源加载、完成加载、页面错误)通常情况下,我们都会有一个loading界面覆盖在webview上面,当页面加载完成隐藏loading。
这里存在2个小问题:
webview只有当所有内容都加载完成后,才会回调onpagefinished。
也就是说,当dom元素加载完成,并且所有图片也加载完成才会触发,这对于追求体验的移动app来说,显然无法接受。尤其是当网络不好或图片太大时尤为明显,用户看到的是明明页面已经基本出来了,却仍然在loading,无法操作。
其实js端可以监听到domcontentloaded事件,此时是关闭loading的最佳时机
出现加载错误时,不仅会调用onreceivederror,仍然会调用onpagefinished
有些人喜欢把loading界面和error界面写在同一个layout里,出现错误时显示error,完成加载时隐藏整个layout。
这对于普通页面来说没有问题,但是对于webview就会出现error无法被显示的情况。
所以最后将loading和error分开,在onpagefinished时,只需要隐藏loading部分。
拦截页面链接重写shouldoverrideurlloading方法,当返回值为true时,需要自己处理url请求,webview将不会插手。
此方法只在涉及页面跳转时被触发。
这里存在2个问题:
低版本的某些请求不会触发此方法,直接在本webview打开了新页面。
如果想拦截,则需要在onpagestarted方法里判断url是否已经改变,更改了的话就表明打开了新的链接。
页面重定向无法识别
当页面存在重定向时,api并没有提供方法作区分,这时需要自己处理。
替换加载内容重写shouldinterceptrequest方法,可以替换js、css、img等内容。
创建webresourceresponse对象,并传入文件输入流,即可用其他资源替换本来要加载的内容。
通过此方法,我们可以预先下载页面所需的js和css文件,保存到本地;然后当打开页面时,直接使用本地已下载的文件。这样可以大幅提高页面加载速度。
注:此方法只能api level 11以后使用,低版本可通过contentprovider实现类似功能。
webchromeclient对象webviewclient类主要设计链接和资源加载的功能实现
webchromeclient类则会涉及更底层的内容,如控制台调试、js弹出框、显示自定义view等。
替换alert对话框web页面一般会通过alert方法,显示一些提示信息,但是对话框的样式却因不同的品牌差异很大,为了使我们的app保持统一风格,有必要替换成我们自己设计的对话框。
重写onjsalert方法,将message显示到dialog。
该方法是模态的,必须返回内容才能关闭对话框,调用jsresult.cancel或者jsresult.confirm。如果只调用了dialog.dismiss而没有调用jsresult的方法,会出现,虽然对话框消失,但是线程一直处于阻塞状态,造成假死现象,无法进行任何操作。
全屏播放视频webview默认不能全屏播放,需要client端提供全屏的window。
代码如下:
@override
public void onshowcustomview (view view, webchromeclient.customviewcallback callback) {
mcustomviewcallback = callback;android.view.window window = webactivity.this.getwindow();window.setflags(
android.view.windowmanager.layoutparams.flag_fullscreen,
android.view.windowmanager.layoutparams.flag_fullscreen);setrequestedorientation(android.content.pm.activityinfo.screen_orientation_landscape);mtitlebar.setvisibility(view.invisible);mwebview.setvisibility(view.invisible);mexitfullscreenbtn.setvisibility(view.visible);mvideoviewcontainer.setvisibility(view.visible);mvideoviewcontainer.addview(view);}
@override
public void onhidecustomview () {
mcustomviewcallback.oncustomviewhidden();android.view.window window = webactivity.this.getwindow();window.setflags(0,
android.view.windowmanager.layoutparams.flag_fullscreen);setrequestedorientation(android.content.pm.activityinfo.screen_orientation_portrait);mtitlebar.setvisibility(view.visible);mwebview.setvisibility(view.visible);mexitfullscreenbtn.setvisibility(view.invisible);mvideoviewcontainer.setvisibility(view.invisible);mvideoviewcontainer.removeallviews();}
cookie如果web端需要用户登录的操作,那么就涉及到native和web端同步登录状态,这就需要用到cookie。
// 打开cookie支持cookiesyncmanager.createinstance(this);
cookiemanager cookiemanager = cookiemanager.getinstance();
cookiemanager.setacceptcookie(true);
// 设置cookie
sbuff.append(key).append("=").append(value);sbuff.append("; path=/");sbuff.append("; domain=").append(domain);cookiemanager.setcookie(url, sbuff.tostring());......
cookiesyncmanager.getinstance().sync();
// 删除cookie// 清除过期的cookiecookiemanager.removeexpiredcookie();// 清除所有cookiecookiemanager.removeallcookie();
因为并不存在单独删除cookie的某个字段的方法,所有要清除某个字段,要先将其设为已过期,再调用removeexpiredcookie
如何同步登录状态简单的情况,只需要有userid即可认为已经登录,分如下两种情况:
- 先从native登录
native端调用登录接口,拿到useid后,当需要打开web时,在loadurl之前,将userid保存到cookie中,服务端就会从cookie中读出userid。
- 先从web端登录
web登录后,webview会自动保存cookie。web端需要与native端定义接口,将username和userid通知给native端,native保存起来。
web端与native端互相通讯webview调用web端js方法mwebview.loadurl("javascript:jsmethod()");
api 19以后,提供了更加便捷的方法,可以直接获取js的返回值。(ios本身已经提供类似api)
webview.evaluatejavascript (string script, valuecallback<string> resultcallback)
web端调用native代码早期api提供的方法:webview.addjavascriptinterface(object object, string name)
webview会将object注入到web端的window对象中,name是object对象定义的方法,web端通过object.name即可调用native端的功能。
but,这个方法存在安全漏洞 漏洞详细说明
如果你的targetsdkversion>=17,那么必须将java方法加上注解@javascriptinterface,否则web端是无法调用的。
自定义scheme通过自定义scheme的方式,在shouldoverrideurlloading拦截,并定向到相应的native逻辑。
ios端需通过此方法实现。
利用webchromeclient.onjsprompt因为js端很少使用prompt(一般使用alert)方法,所以我们可以利用这个方法,通过自定义协议格式,来实现web与native端的通信。
因为此方法需要返回jsresult对象,所以利用此机制,可以实现方法的同步调用(web端可以获取到接口方法的返回值,有利于代码和逻辑的简化)
大名鼎鼎phonegap(现在叫cordova)就是利用此方法实现的web与native通讯。
我会专门写一篇,我是如何实现web与native通信的。
其他技巧屏蔽长按事件重写performlongclick
或者setonlongclicklistener实现空的listener
隐藏选择框可以使用全局的一个css
*{-webkit-tap-highlight-color: rgba(0, 0, 0, 0);}
缩放按钮引起的崩溃在某些机型上,当显示webview的缩放按钮时,退出activity,就会报如下错误:
android.view.windowleaked: activity com.secoo.activity.web.webactivity has leaked window android.widget.zoombuttonscontroller$container{438e8248 v.e..... ........ 0,0-1536,194} that was originally added here
at android.view.viewrootimpl.(viewrootimpl.java:382)
at android.view.windowmanagerglobal.addview(windowmanagerglobal.java:261)
at android.view.windowmanagerimpl.addview(windowmanagerimpl.java:76)
at android.widget.zoombuttonscontroller.setvisible(zoombuttonscontroller.java:371)
......
这是因为按钮的隐藏是延迟触发,在activity退出之后,造成了window的泄露。
解决方法:
// api 11之后,不显示缩放按钮
webview.getsettings().setdisplayzoomcontrols(false);// api 11之前,在finish时,从view层级中删除webview
viewgroup viewgroup = (viewgroup)(mwebview.getparent());viewgroup.removeview(mwebview);
技术交流请留言…
to be continue…