视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001
WebView实用功能与技巧
2020-11-09 07:23:24 责编:小采
文档


引子 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…

    下载本文
    显示全文
    专题