查看原文
其他

【第576期】好好和h5沟通!几种常见的hybrid通信方式

zjutkz 前端早读课 2019-06-10

前言

现在运营做活动就不单单只做PC版的了,现在都以移动端的优先,再考虑PC版的。活动往往是内嵌在客户端中,那么就免不了要跟客户端交互,那么这个就涉及到今天要分享的通讯问题了。今天的文章由@zjutkz分享。


本文有点长,而且由些许的代码,相信看过这篇文章,对通讯的问题也会清楚很多。


正文从这开始~


说起hybrid大家不会陌生,主要意思就是nativeh5混合开发。为什么要这样做呢?大家可以想象一下针对于同一个活动,如果使用纯native的开发方式,AndroidiOS两边都要维护同一套界面甚至是逻辑,这样开发和维护的成本会很大,而使用hybrid的开发方式的话,让前端的同学去写一套界面和逻辑,对于native端来说只要使用对应的容器去展示就可以了(对于Android来说这个容器当然就是WebView)。那为什么不所有的页面都使用这种方式开发呢?因为使用h5来展示界面的话用户体验始终是不如native的,所以在这两者之间我们需要一个权衡。


介绍完了何为hybrid,我们来思考下面几个场景。


场景1,前端那边的页面有一个按钮,点击这个按钮需要显示一个native的组件(比如一个toast),或者点击这个按钮需要去在native端执行一个耗时的任务。


场景2,还是前端页面有一个按钮,点击这个按钮的逻辑是:如果登录了,则跳转到相应的界面,如果没有登录,则跳转到登录界面。而这个登录界面是我们native维护的。


看完上面两个场景,相信大家也发现了一个问题,hybrid这样的开发方式有一个问题需要解决,那就是前端和本地的通信。


下面让我带大家了解一下几种常见的通信方式吧。


前言

在看这篇文章之前你要确保你有那么一点点的js知识,没错只需要一点点,能看懂最简单的代码就可以。如果你之前没接触过js的话。。也没关系,我会把其中对应的逻辑用语言表达出来。


为什么需要用到js呢,因为前端体系中,像我们说的点击按钮这样的逻辑都是放在js脚本中执行的,有点像我们Android中的model层。(由于本人对前端的知识也只是略知一二,这个比方可能不太恰当,见谅见谅)。所以说到hybrid通信,主要就是前端的js和我们Android端的通信。


传统的JSInterface

首先先介绍一下最普通的一种通信方式,就是使用Android原生的JavascriptInterface来进行jsjava的通信。具体方式如下:


首先先看一段html代码



很简单,一个button,点击这个button就执行js脚本中的showToast方法。


而这个showToast方法做了什么呢?



可以看到control.showToast,这个是什么我们等下再说,下面看我们java的代码。


首先界面很简单,一个WebView。在对应的activity中做的事也就几件,首先打开js通道。



然后通过WebViewaddJavascriptInterface方法去注入一个我们自己写的interface


可以看到这个interface我们给它取名叫control


最后loadUrl



好了,让我们再看看js脚本中的那个showToast()方法。



这里的control就是我们的那个interface,调用了interfaceshowToast方法



可以看到先显示一个toast,然后调用log()方法,log()方法里调用了js脚本的log()方法。


jslog()方法做的事就是在控制台输出msg


这样我们就完成了jsjava的互调,是不是很简单。但是大家想过这样有什么问题吗?如果你使用的是AndroidStudio,在你的webSettings.setJavaScriptEnabled(true);这句函数中,AndroidStudio会给你一个提示。

这个提示的意思呢,就是如果你使用了这种方式去开启js通道,你就要小心XSS攻击了,具体的大家可以参考


虽然这个漏洞已经在Android 4.2上修复了,就是使用@JavascriptInterface这个注解。但是你得考虑兼容性啊,你不能保证,尤其在中国这样碎片化严重的地方,每个用户使用的都是4.2+的系统。所以基本上我们不会再利用Android系统为我们提供的addJavascriptInterface方法或者@JavascriptInterface注解来实现jsjava的通信了。那怎么办呢?方法都是人想出来的嘛,下面让我们看解决方案。


JSBridge

JSBridge,顾名思义,就是和js沟通的桥梁。其实这个技术在Android中已经不算新了,相信有些同学也看到过不少实现方案,这里说一种我的想法吧。其实说是我的想法,实际是公司里的大牛实现的,我现在做的就是维护并且扩展,不过这里还是拿出来和大家分享一下。


思路

首先先说思路,有经验的同学可能都知道AndroidWebView中有一个WebChromeClient类,这个类其实就是用来监听一些WebView中的事件的,我们发现其中有三个这样的方法。


这三个方法其实就对应于js中的alert(警告框)comfirm(确认框)prompt(提示框)方法,那这三个方法有什么用呢?前面我们说了JSBridge的作用是提供一种jsjava通信的框架,其实我们可以利用这三个方法去完成这样的事。比如我们可以在js脚本中调用alert方法,这样对应的就会走到WebChromeClient类的onJsAlert()方法中,我们就可以拿到其中的信息去解析,并且做java层的事情。那是不是这三个方法随便选一个就可以呢?其实不是的,因为我们知道,在js中,alertconfirm的使用概率还是很高的,特别是alert,所以我们最好不要使用这两个通道,以免出现不必要的问题。


好了,说到这里我们前期的准备工作也就做好了,其实就是通过重写WebViewWebChromeClient类的onJsPrompt()方法来进行jsjava的通信。


有了实现方案,下面就是一些具体的细节了,大家有没有想过,怎么样才能让java层知道js脚本需要调用的哪一个方法呢?怎么把js脚本的参数传递进来呢?同步异步的方式又该怎么实现呢?下面提供一种我的思路。


首先大家都知道http是什么,其实我们的JSBridge也可以效仿一下http,定义一个自己的协议。比如规定shemepath等等。下面来看一下一些的具体内容:

hybrid://JSBridge:1538351/method?{message:msg}


是不是和http协议有一点像,其实我们可以通过js脚本把这段协议文本传递到onPropmt()方法中并且进行解析。比如,shemehyrid://开头的就表示是一个hybrid方法,需要进行解析。后面的method表示方法名,message表示传递的参数等等。


有了这样一套协议,我们就可以去进行我们的通信了。


代码

先看一下我们htmljs的代码


有前端经验的同学应该能很轻松的看懂这样的代码,对于看不懂的同学我来解释一下,首先看界面。


可以看到有两个按钮,对应着html的这段代码



点击按钮会执行js脚本的这段代码



它其实就是一个函数,名字叫call,括号里的是它的参数(obj, method, params, callback, timeout)。那这几个参数是怎么传递的呢?回过头看我们的html代码,点击第一个按钮,会执行这个语句



其中括号(JSBridge,toast,{message:’我是气泡’,isShowLong:0},function(res){})里的第一个参数’JSBridge’对应着前面的obj,’toast’对应着method,以此类推。第二个按钮也是一样。


然后在call这个方法内,会执行Private类的registerCallcallMethod,我们来看callMehod()



注释说的很清楚了,就是通过传递进来的参数生成uri,并且调用window.prompt()方法,这个方法大家应该很眼熟吧,没错,在调用这个方法之后,程序就会相应的走到java代码的onJsPrompt()方法中。而生成的uri则是我们上面说过的那个我们自己定义的协议格式。


好了,我们总结一下这两个前端的代码。其实很简单,以界面的第一个按钮toast为例,点击这个按钮,它会执行相应的js脚本代码,然后就会像我们前面所讲的那样,走到onJsPrompt()方法中,下面让我们看看对应的java代码。



这是对应的WebChromeClient类,可以看到在onJsPrompt()方法中我们只做了一件事,就是丢给JsCallJava类去解析,再看JsCallJava类之前,我们可以先看看onJsPrompt()这个方法到底传进来了什么。



可以看到,我们传给JsCallJava类的那个message,就像我们前面定义的协议一样。shemehybrid://,表示这是一个hybrid方法,hostJSBridge,方法名字是toast,传递的参数是以json格式传递的,具体内容如图。不知道大家有没有发现,这里我有一个东西没有讲,就是JSBridge:后面的那串数字,这串数字是干什么用的呢?大家应该知道,现在我们整个调用过程都是同步的,这意味着我们没有办法在里面做一些异步的操作,为了满足异步的需求,我们就需要定义这样的port,有了这串数字,我们在java层就可以做异步的操作,等操作完成以后回调给js脚本,js脚本就通过这串数字去得到对应的callback,有点像startActivity中的那个requestCode。大家没听懂也没关系,后面我会在代码中具体讲解。


好了,下面我们可以来看JsCallJava这个类的具体代码了。


有点长,不过其实逻辑很好理解。首先我们调用的是call这个方法。它里面做了什么呢



可以看到其实就是通过js脚本传递过来的参数得到了方法名字,sid(前面说的那串数字)等等内容。下面看这段代码



通过name去得到一个map,这里的name是我们刚刚解析得到了,对应实际情况就是JSBridge,那这个mInjectNameMethods又是什么呢?



可以看到我们有一个JSBridge类,在JsCallJava的构造函数中,我们通过JSBridge这个类的getInjectPair()方法得到了一个Stringclass的映射关系,并且把class中符合标准的方法拿出来存放到mInjectNameMethods中,以便我们在call方法中调用。下面来看看JSBridge类。


它的getInjectPair方法其实就是得到了mClassMap,这个mapJSBridge类初始化的时候就有一个默认的值了。



key是”JSBridge”,value是我们的JSLogincal类。


对这个类上面的两个方法有没有很眼熟?名字和js脚本中的那两个方法一样有木有。我们调用链最后就会走到相应的同名方法中!


上面就是jsjs的整个过程了,其实吧,不应该放这么多的代码的,搞得像是源码分析一样,不过我觉得这样还是有一定好处的,至少跟着代码走一遍能加深印象嘛。


我们还是来捋一捋整个过程。

  • js脚本中把对应的方法名,参数等写成一个符合协议的uri,并且通过window.prompt方法发送给java层。

  • java层的onJsPrompt方法中接受到对应的message之后,通过JsCallJava类进行具体的解析。

  • JsCallJava类中,我们解析得到对应的方法名,参数等信息,并且在map中查找出对应的类的方法。


这里多说一句,还记得我们定义的协议中的host是什么吗?

hybrid://JSBridge:875725/toast?{message:”我是气泡”,isShowLong:0}


JSBridge,而我们在JsCallJava类中是通过这个host去查找对应的类的,我们可以看到在JSBridge类中


这意味着,如果你可以更换你的host,叫aaa都没关系,只要你在对应的map中的key也是aaa就可以了。


可能有的同学会说何必这么麻烦,直接在JsCallJava类中定义方法不就好了,这样还省的去写那么多的逻辑。可是大家有想过如果你把所有js脚本想要调用的方法都写在JsCallJava类中,这个类会有多难扩展和维护吗?而像我这样,如果你的js脚本处理的是登录相关逻辑,你可以写一个LoginLogical.class,如果是业务相关,你可以写一个BizLogical.class,这样不仅清晰,而且解耦。


当然,如果你仔细的看过代码,会发现其实在native层的那些同名函数其实是有规范的。


首先必须要是public static的,因为这样调用会更方便。


其次参数也有要求,有且仅有三个参数,WebViewJsonObject和一个CallbackWebView用来提供可能需要的context,另外java执行js方法也需要WebView对象。JsonObjectjs脚本传递过来的参数。而Callback则是java用于回调js脚本的。


可能你会发现JSBridge里处处都是规范,协议需要规范,参数需要规范。这些其实都是合理的,因为规范所以安全。


#####(4) 在得到对应的方法之后,就去调用它,以我们的toast


拿到对应的信息,直接makeToast就好了。


以上就是全部js调用java的过程,那我们java执行完逻辑以后,怎么回调js呢?这里我们以另外一个按钮的例子来说。



js脚本传递的一个json的参数,{data:1},从名字可以看出是先要java执行一个加逻辑。


这里我们模拟一下耗时操作,可以帮助大家更好的理解JSBridge中的异步操作。对应java层的方法执行完+1的操作之后,把结果封装成一个jsonObject,并且调用invokeJSCallback方法。



invokeJSCallback方法中直接调用了callbackapply方法。



apply方法中,我们直接拼装了一个jsonObject,里面包括了我们想要返回给js脚本的结果,并且直接调用了jsonComplete方法。



可以看到jsonComplete通过sid(那一串数字)拿到对应的callback并执行,而我们pluscallback里做了什么呢?


直接在控制台中输出结果。


所以当我们点击plug按钮以后,过两秒我们就可以在logcat中看到如下输出


好了,至此所有和JSBridge相关的代码就分析完了。其实原理非常的简单,通过jswindow.prompt方法将事先定义好的协议文本传输到java层,然后java层进行解析并调用相应的方法,最后通过callback将结果返回给js脚本。中间我们使用的那些类可以更好的解耦,如果你有心,甚至可以把所用逻辑相关代码抽离出来,把剩余的代码写成JSBridge.core作为库来使用。这样你想加什么功能直接写,不用改任何的源码。


UrlRouter

其实严格的说,UrlRouter不算是jsjava的通信,它只是一个通过url来让前端唤起native页面的框架。不过千万不要小看它的作用,如果协议定义的合理,它可以让前端,AndroidiOS三端有一个高度的统一,十分方便。


思路

其实吧,这个思路比JSBridge还要简单,就是我们通过自己实现的框架去拦截前端同学写的url,发现如果是符合我们UrlRouter的协议的话,就跳转到相应的页面。


至于怎么拦截呢?当然是通过WebViewClient类的shouldOverrideUrlLoading方法咯。


代码

首先我们还是先看一个html代码


很简单,有一个按钮,通过点击这个按钮,会加载一个url,这个url


这里多说一句,如果你也想用UrlRouter这样的形式的话,协议的sheme最好是http这样开头的,不要自己去重新定义,因为这样可以保证前端同学逻辑的清晰。如果你想着自己定义一个shemeshemeA,公司做别的app的同学也定义一个shemeshemeB,加上本来就要的http,前端的同学可能脑子都昏了。。。


下面来看看WebViewClient类。


很简单,在shouldOverrideUrlLoading方法中先拦截url交给Nav类处理,如果返回true则表示需要拦截,直接return true,否则交给WebViewloadUrl


接下去看看Nav


我们在NavWebViewClient类中是这样调用的



from方法创建了一个Nav类的实例,下面来看看toUri方法


toUri方法中调用了to方法,to方法做的就是将urisetData的方式注入到intent中。


接着通过一系列PackageManager的方法去判断有没有符合uriactivity,如果有则直接startActivity


是不是很简单,下面我以文中最开头的场景2为例子。


我们native端需要一个LoginActivity,并且根据上面的代码我们知道,这个LoginActivity必须要配置上对应的data才行。


这是我们的manifest文件,可以看到已经通过gradle配置了对应的data


这里我为什么要用grdle去配置呢?想象如果你有十几个页面,你难道要在manifest中都写一遍吗?用我这种方式,直接在build.gradle中写一遍就可以了。这里我是想给大家传递一个思想:


使用gradle我们可以做很多自动化的事,千万不要自己给自己找麻烦了。


看到这儿大家肯定会觉得,就这么简单?是的,大体的框架就这么简单,但是如果你想真正的用好它,还需要做很多工作。


比如在跳转到native页面,做完响应的逻辑之后,你怎么通知前端去更新呢?这里你可以使用startActivityForResult,也可以使用广播,甚至是eventBus。这需要你在你的框架内做好封装。


再比如,上面的例子是最简单的,但是如果前端的同学想在跳到对应的native页面的时候加上一些参数呢?你的intent该怎么处理?


还有,如果你想你的框架鲁棒性够强,是不是得提供一个hook工具呢?让调用者可以hook掉你内部的那个intent,从而添加自己想要添加的数据。


这些都是要解决的问题,这里我就不给大家上具体的代码了。毕竟只有你自己去实现了以后才会有更深的理解。

 

后语

本文涉及到的代码,大家可以通过“阅读原文”来查看。

 

关于本文

作者:@zjutkz

原文链接:http://zjutkz.net/2016/04/17/好好和h5沟通!几种常见的hybrid通信方式/

 



    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存