H5和Native的通信方式及原理是啥?

899 阅读14分钟

背景

H5与端通信指的是HTML5页面与客户端应用(如iOS、Android和HarmonyOS)之间的数据交互,常用于构建融合了H5灵活性与客户端应用性能的混合应用(Hybrid App)

Hybrid APP

以下我们列举了一些使用H5页面的APP示例:

  1. 微信:公众号内容、小程序,使用户可直接在微信内访问基于H5开发的服务
  2. 支付宝:小程序服务大部分基于H5开发
  3. 淘宝:商品展示和活动推广应用H5
  4. 头条:新闻内容使用H5页面
  5. 拼多多:首页中集成了H5

H5对比NA

那么使用H5和NA开发分别有啥特点呢

特性H5页面原生应用
跨平台无需为不同平台开发不同代码直接访问设备硬件资源,提供更优性能
开发成本较低,统一代码库较高,需为不同平台(iOS、Android,未来还有HarmonyOS)开发
快速迭代更新快速,无需应用商店审核更新需经过应用商店审核,耗时
易于分享可通过URL链接分享需要从应用商店下载安装
搜索引擎优化易于被索引不易被索引
性能通常不如原生应用,特别是在图形渲染和动画方面直接访问设备硬件,性能更优
用户体验受限于浏览器和HTML5标准,无法提供与原生应用相同的流畅体验和交互效果更流畅的动画和丰富的交互效果
功能访问特有的硬件功能,如摄像头、GPS等,H5页面无法充分利用可访问设备硬件功能,如摄像头、GPS、语音、短信、蓝牙和拨号等
离线支持需要网络连接可支持离线
安全性较低,依赖浏览器安全措施更好
发布流程无需经过应用商店审核需要通过应用商店审核,审核过程耗时
更新维护快速,可即时更新用户需要手动下载新版本,更新率受影响
平台限制受限于Web标准和浏览器兼容性受限于特定平台的规范和限制
推广难度较低,易于在社交网络上传播较高,需要用户下载安装

举个🌰

h5面板:分为纯h5面板和原生容器实现半屏

  1. 非纯h5面板:面板是原生的popwindow控件 ,控件里面放了一个webview,加载了一个内容网页
  2. 纯h5面板:半屏容器和内容均由h5实现

image.png

  1. 纯原生面板:容器和内容均由原生实现

image.png 

通信时机

什么时候通信就看双端的现有能力就行,端不知道H5的页面功能细节,H5的变动需要响应到端的变化时就需要通信,反过来一样

其实H5和NA的交互,本质上就2种调用:

  1. NA 调用 H5
  2. H5 调用 NA

JS 调用 Native

JS 调用 NA 的方式主要有拦截URL Scheme、重写 prompt 、注入 API 等方法

Android

js调NA,需要对WebView设置以下属性

WebSettings webSettings = mWebView.getSettings();  
 //Android容器允许JS脚本
webSettings.setJavaScriptEnabled(true);
//Android容器设置侨连对象
mWebView.addJavascriptInterface(getJSBridge(), "JSBridge");

通过addJavascriptInterface添加暴露出来的JSBridge,然后在对象内部声明对应的API

private Object getJSBridge(){  
    Object insertObj = new Object(){  
        @JavascriptInterface
        public String foo(){  
            return "jsbrdige";  
        }
    };  
    return insertObj;  
}  

H5调用NA方法:

window.JSBridge.foo(); //返回:'jsbrdige'

说明:

  • Android 4.2以下:通过addJavascriptInterface注册可供JS调用的Java对象,但是系统没有限制注册Java类的方法,hacker可以利用反射机制调用未注册的其他Java类,导致JS能力的无限增强,进而借助客户端能力为所欲为
  • Android 4.2以上:暴露的API要加上注解@JavascriptInterface,否则会找不到方法

PS:WebView中接口曾出现过隐患,可以参考WebViw漏洞利用

iOS

NA通过引入官方提供的JavaScriptCore库,可以将API绑定到JSContext上(JS默认通过window.top可调用)。

NA注册API函数:

-(void)webViewDidFinishLoad:(UIWebView *)webView{
    [self hideProgress];
    [self setJSInterface];
}
-(void)setJSInterface{
    JSContext *context =[_wv valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
    // 注册名为foo的api方法
    context[@"foo"] = ^() {
        //获取参数
        NSArray *args = [JSContext currentArguments];
        NSString *title = [NSString stringWithFormat:@"%@",[args objectAtIndex:0]];
        //返回一个值  'foo:'+title
        return [NSString stringWithFormat:@"foo:%@", title];
    };    
}               

JS调用NA方法

window.top.foo('test');

PS:iOS原生无法被JS调用,通过引入官方提供的第三方”JavaScriptCore”,即可开放API给JS调用

实现方式

URL Scheme

适用于应用间导航通信,当需要从Web页面触发某些应用内操作或跳转到其他应用时使用。在移动开发中,它允许一个应用通过定义的URL格式启动另一个应用,并传递数据。常用于:

  • 应用间跳转:从一个应用跳转到另一个应用,同时传递参数
  • 深度链接(Deeplink):允许Web页面或消息链接直接打开应用中的特定页面或执行特定操

Scheme格式如下

<protocol>://<host>/<path>?<qeury>#fragment

你可以在浏览器里面直接输入 weixin://,系统就会提示你是否要打开微信。输入 mqq:// 就会帮你唤起手机 QQ。

image.png Scheme必须原生APP注册后才生效,许多应用程序定义了自己的Scheme,如微信 weixin://、twitter://,它们可以被浏览器或其他应用识别并启动相应应用程序

而在我们实际开发中,APP不会注册对应的Scheme,而是由H5通过某种方式触发scheme(如用iframe.src),然后NA捕获URL触发事件拿到当前触发URL,联调已有协议

我们可以自定义 JSB 通信的 URL Schema,比如:jsbridge://showToast?text=hello

NA 加载 WebView之后,Web 发送的所有请求都会经过 WebView 组件,所以 NA 可以重写 WebView 里的方法,拦截 Web发起的请求,我们判断请求的格式:

  • 符合自定义的URL Schema:解析URL并调用NA的方法
  • 不符合自定义的URL Schema:直接转发,请求真正的服务

Web发送URL请求的方法:

  1. a标签:需要用户操作
  2. location.href:可能引起页面的跳转丢失
  3. iframe.src:常用,需要控制 URL 的长度
  4. ajax请求:Android没有相应的拦截方法

Android 和 iOS 都可以拦截 URL Scheme 并解析 Scheme 决定是否执行对应 NA 代码:

  • Android:Webview提供shouldOverrideUrlLoading方法拦截 H5 发送的URL Scheme请求
  • iOS:WKWebview根据拦截到的URL Scheme和参数执行操作

拦截 URL Scheme 不存在漏洞问题、使用灵活,可以实现 H5 和 Native 页面的无缝切换

例如在某一页面需要快速上线的情况下,先开发出 H5 页面。某一链接填写的是 H5 链接,在对应的 NA 页面开发完成前先跳转至 H5 页面,待 NA 页面开发完后再拦截,跳转NA 页面,此时 H5 的链接无需修改

JavaScript Interface

通过 webView 提供API,App 将 NA 的接口注入到 JS 的 Context(window)的对象中,Web 端可直接在全局window使用这个暴露的全局 JS 对象,进而调用NA方法

  • Android:使用WebViewaddJavascriptInterface方法,将Java对象映射为JavaScript对象,使得Web页面通过JS调用原生Java代码
  • iOS:WKWebView使用evaluateJavaScript执行JS代码,并获取回调或结果。UIWebview提供JavaScriptScore执行JS 代码;WKWebview提供了window.webkit.messageHandlers方法允许JS向NA发送消息

重写 prompt 等原生 JS 方法

  • Android 4.2 前注入对象的接口是 addJavascriptInterface ,但是由于安全原因不再使用。一般通过修改浏览器的部分 Window 对象方法完成操作。主要是拦截 alert、confirm、prompt、console.log 四个方法,分别被Webview的 onJsAlert、onJsConfirm、onConsoleMessage、onJsPrompt 监听
  • iOS 由于安全机制,WKWebView对 alert、confirm、prompt 等方法做了拦截,NA通过此方式与JS 交互,需要实现WKWebView的三个WKUIDelegate代理方法

和NA约定好传参格式:

  1. H5无需识别客户端,传入不同参数直接调用 NA 即可,剩下的交给客户端拦截相同方法,识别参数,可实现多端一致
  2. 能起到隔离的作用

Native 调用 JS

NA 调用 JS 比较简单, H5 将 JS 方法暴露在 Window 上给 NA 调用即可

Android端

遵循:”javascript: 方法名(‘参数,需要转为字符串’)”的规则即可。

Android 4.4前的版本支持 loadUrl,使用方式类似我们在 a 标签的href里面写 JS 脚本一样,都是javascript:xxx的形式,这种方式无法直接获取返回值。

Android4.4之前,调用的方式:

// mWebView = new WebView(this);            
mWebView.loadUrl("javascript: 方法名('参数,需要转为字符串')"); 

 runOnUiThread(new Runnable() {  
        @Override  
        public void run() {  
            mWebView.loadUrl("javascript: 方法名('参数,需要转为字符串')");  
            Toast.makeText(Activity名.this, "调用方法...", Toast.LENGTH_SHORT).show();  
        }  
});  

Android 4.4及以后,使用evaluateJavascript调用:

mWebView.evaluateJavascript("javascript: 方法名('参数,需要转为字符串')", new ValueCallback() {
        @Override
        public void onReceiveValue(String value) {
            //这里的value即为对应JS方法的返回值
        }
});

说明:

  • 4.4前NA通过loadUrl调用JS方法,只能让某个JS方法执行,无法获取该方法的返回值
  • 4.4之后通过evaluateJavascript异步调用JS方法,能在onReceiveValue中拿到返回值

iOS端

UIWebViewstringByEvaluatingJavaScriptFromString实现

WKWebviewevaluateJavaScript:javaScriptString实现

//Swift
webview.stringByEvaluatingJavaScriptFromString("方法名(参数)")
//OC
[webView stringByEvaluatingJavaScriptFromString:@"方法名(参数);"];

PS:NA调用JS方法时,能拿到JS方法的返回值

JSBridge原理(JSB)

Bridge是一种在H5页面和原生APP之间建立通信通道的机制,它通常涉及在原生端暴露一个API接口,H5页面可以通过调用这个接口来触发原生代码。

容器一旦接到网页的请求,就根据请求去调用底层系统的 API,然后再返回结果给网页。API Bridge 往往以 JavaScript 语言提供,方便网页调用,这时又称为 JSbridge。

image.png

JSB的流程如下所示:

image.png 其实原生的WebView/UIWebView控件已经能够和JS通信了,为什么还要JSBridge呢?

其实使用JSBridge有很多方面的考虑:

  1. Android 4.2以下,addJavascriptInterface方式有安全问题
  2. iOS 7以下,JS无法调用NA

H5 调用 NA

  1. H5 调用 NA,会调用 invokeSchema 方法

    1. 先生成一个 message,为这条message生成一个callbackId
    2. 执行registerCallback方法注册回调,将回调push到responseCallbacks
    3. 更改 iframe 的 src
  2. 当 NA 检测到 iframe src 的变化,执行_handleMessageFromNative方法,获取到 JS 侧的 responseCallbacks 中的所有 message

  3. 从NA获取到的message中获取responseId有2种情况:

    1. 取不到 responseId :说明是第一次调用 bridge 传过来的,此时会生成一个返回给调用方的 message,其 reponseId 是传过来的 message 的 callbackId,当 native 执行 responseCallbacks 时,会将生成的包含 responseId 的 message 返回给H5
    2. 有 responseId:说明这个 message 是 JS 调 Native 之后回调接收的 message,所以从一开始responseCallbacks中根据 responseId(一开始存的时候是用的callbackId,两个值是相同的)取出这个回调函数并执行
  4. 这样就完成了一次 JS 调用 Native 的流程

image.png

  • 优点:JavaScript 端可以确定 JSBridge 的存在,直接调用即可
  • 缺点:不同容器的 API Bridge不一样。为某个容器写的网页,不能放在另一个容器使用,也无法在浏览器使用,除非网页脚本做了兼容

NA 调用 H5

NA 调用 webview 注册的 JSBridge 的逻辑是相似的,不过就不是通过触发 iframe 的 src 触发执行的了,因为NA可以主动调用 _handleMessageFromNative 方法,在webview中执行对应逻辑

JSBridge封装

基本每个公司内自己会封装一套Hybrid解决方案库,并且可能不同部门间的通信协议库还不同,这种封装提供了一套标准化、简化的接口,使得我H5端和NA端的交互更加高效和易于管理

自定义封装Hybrid的特点包括:安全性、易用性、扩展性、可维护性、统一接口、可定制性等

JSB流程

一个简易版JSB流程如下所示:

image.png

全局桥对象

const JSBridge = window.JSBridge || (window.JSBridge = {});

JSB是H5页面中定义在全局对象window的一个属性,该对象有以下方法:

  1. _handleMessageFromNative:原生调用H5页面注册的方法,或通知H5页面执行回调方法
  2. registerCallback:H5调用注册方法供NA调用
  3. invokeSchema:H5调用NA的方法,调用后实际是本地通过url scheme触发,调用时会将callbackId存放到本地变量responseCallbacks中

image.png

JS调用NA

我们定义好了全局JSB对象,可以通过registerCallback方法调用原生API,经历了以下步骤:

  1. 判断是否有回调函数,有则生成callbackId,并将callbackId和对应回调添加进入回调函数集合responseCallbacks列表
/**
 * 注册回调方法
 */
 const registerCallback = (name, responseCallback) => {
    if (!responseCallbacks[name]) {
        responseCallbacks[name] = [];
    }
    responseCallbacks[name].push(responseCallback);
};

  1. 通过特定方法,将传入的数据、方法名拼接成一个url scheme,原生捕获到这个scheme后会分析
let uri = 'xxxxx://xxxJS_'; // API_Name:callbackId/handlerName?data
  1. H5创建隐藏iframe触发scheme
//创建隐藏iframe
let ifr = document.createElement('iframe');
ifr.style.display = 'none';
//触发scheme
ifr.src = uri;
document.documentElement.appendChild(ifr);

NA获悉api被调用

我们已经成功在H5页面中触发scheme,NA如何捕获scheme被触发呢?

Android捕获url scheme

通过shouldoverrideurlloading捕获到url scheme的触发

public boolean shouldOverrideUrlLoading(WebView view, String url){
	//读取到url后自行进行分析处理
	//如果返回false,则WebView处理链接url,如果返回true,代表WebView根据程序来执行url
	return true;
}

iOS捕获url scheme

在UIWebView内发起的所有网络请求,都可以通过delegate函数在Native层得到通知。这样,我们可以在webview中捕获url scheme的触发(原理是利用 shouldStartLoadWithRequest)

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
    NSURL *url = [request URL];
     
    NSString *requestString = [[request URL] absoluteString];
    //获取url scheme后自行进行处理

参数和回调的格式

Native已经接收到了JS调用的方法,接下来NA就应该按照规定的数据格式解析数据了,Native接收到URL后,可以将回调参数id、api名、参数提取出来:

  1. 根据api名,在本地找寻对应的api方法,并且记录该方法执行完后的callbackId

  2. 根据提取出来的参数将定义好的参数进行转化

  3. NA本地执行对应的api方法

  4. 找到这次api调用对应的callbackId,连同需要传递的参数组装成JSON格式的参数,JSON格式为:{responseId:回调id,responseData:回调数据}

    1. responseId:H5页面中对应需要执行的callbackId,在H5注册回调时就已经产生
    2. responseData:NA需要传递给H5的回调数据, {code:(整型,调用是否成功,1成功,0失败),result:结果信息
  5. 通过JSBridge通知H5页面回调

NA调用JS

NA通过JSBridge调用H5的JS方法或者通知H5回调

//将回调信息传给H5
JSBridge._handleMessageFromNative(messageJSON);	

messageJSON数据格式根据两种不同的类型:

  1. NA通知H5回调: 上述Native通知H5回调的JSON格式
  2. NA主动调用H5:{handlerName:api名,data:数据,callbackId:回调id}

H5中API注册

h5怎么注册供原生调用的api呢?

  1. data:原生传过来的数据
  2. callback:内部封装,执行callback后会触发url scheme,通知NA获取回调信息
Hybrid.register('test', (resData: MsgRes) => {
    const {code, msg, result} = resData;
    if (code === 1) {}
});

Hybrid开发库

这些框架在web基础上包装一层Native,通过JSB实现和NA的交互

Cordova/PhoneGap/Lonic

cordova.apache.org/docs/en/12.…

流行的H5与原生APP交互框架,能使用JS开发跨平台的移动应用,提供了一系列的插件来实现H5和NA的交互

React Native

reactnative.cn/docs/intro-…

虽然主要用于开发原生应用,但RN也支持Web视图,可用于嵌入H5页面,并与NA通信。

Flutter

docs.flutter.cn/get-started…

Flutter是一个跨平台的UI工具集, 类似于RN,用于从单一的Dart代码库构建原生应用。也支持通过平台通道与H5内容通信。

写在最后

我们主要学习了H5页面与客户端应用之间的通信过程、主要方式、优缺点及应用场景;同时我们还探讨了JSB的原理和简单实现,希望对你有所帮助

参考

juejin.cn/post/725366…

ruanyifeng.com/blog/2019/1…

juejin.cn/post/703698…

cloud.tencent.com/developer/a…

jonny-wei.github.io/blog/mobile…

开源项目参考:github.com/marcuswesti…