背景
H5与端通信指的是HTML5页面与客户端应用(如iOS、Android和HarmonyOS)之间的数据交互,常用于构建融合了H5灵活性与客户端应用性能的混合应用(Hybrid App)
Hybrid APP
以下我们列举了一些使用H5页面的APP示例:
- 微信:公众号内容、小程序,使用户可直接在微信内访问基于H5开发的服务
- 支付宝:小程序服务大部分基于H5开发
- 淘宝:商品展示和活动推广应用H5
- 头条:新闻内容使用H5页面
- 拼多多:首页中集成了H5
H5对比NA
那么使用H5和NA开发分别有啥特点呢
特性 | H5页面 | 原生应用 |
---|---|---|
跨平台 | 无需为不同平台开发不同代码 | 直接访问设备硬件资源,提供更优性能 |
开发成本 | 较低,统一代码库 | 较高,需为不同平台(iOS、Android,未来还有HarmonyOS)开发 |
快速迭代 | 更新快速,无需应用商店审核 | 更新需经过应用商店审核,耗时 |
易于分享 | 可通过URL链接分享 | 需要从应用商店下载安装 |
搜索引擎优化 | 易于被索引 | 不易被索引 |
性能 | 通常不如原生应用,特别是在图形渲染和动画方面 | 直接访问设备硬件,性能更优 |
用户体验 | 受限于浏览器和HTML5标准,无法提供与原生应用相同的流畅体验和交互效果 | 更流畅的动画和丰富的交互效果 |
功能访问 | 特有的硬件功能,如摄像头、GPS等,H5页面无法充分利用 | 可访问设备硬件功能,如摄像头、GPS、语音、短信、蓝牙和拨号等 |
离线支持 | 需要网络连接 | 可支持离线 |
安全性 | 较低,依赖浏览器安全措施 | 更好 |
发布流程 | 无需经过应用商店审核 | 需要通过应用商店审核,审核过程耗时 |
更新维护 | 快速,可即时更新 | 用户需要手动下载新版本,更新率受影响 |
平台限制 | 受限于Web标准和浏览器兼容性 | 受限于特定平台的规范和限制 |
推广难度 | 较低,易于在社交网络上传播 | 较高,需要用户下载安装 |
举个🌰
h5面板:分为纯h5面板和原生容器实现半屏
- 非纯h5面板:面板是原生的popwindow控件 ,控件里面放了一个webview,加载了一个内容网页
- 纯h5面板:半屏容器和内容均由h5实现
- 纯原生面板:容器和内容均由原生实现
通信时机
什么时候通信就看双端的现有能力就行,端不知道H5的页面功能细节,H5的变动需要响应到端的变化时就需要通信,反过来一样
其实H5和NA的交互,本质上就2种调用:
- NA 调用 H5
- 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。
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请求的方法:
-
a
标签:需要用户操作 -
location.href
:可能引起页面的跳转丢失 -
iframe.src
:常用,需要控制 URL 的长度 -
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:使用
WebView
的addJavascriptInterface
方法,将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约定好传参格式:
- H5无需识别客户端,传入不同参数直接调用 NA 即可,剩下的交给客户端拦截相同方法,识别参数,可实现多端一致
- 能起到隔离的作用
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端
UIWebView
:stringByEvaluatingJavaScriptFromString
实现
WKWebview
:evaluateJavaScript:javaScriptString
实现
//Swift
webview.stringByEvaluatingJavaScriptFromString("方法名(参数)")
//OC
[webView stringByEvaluatingJavaScriptFromString:@"方法名(参数);"];
PS:NA调用JS方法时,能拿到JS方法的返回值
JSBridge原理(JSB)
Bridge是一种在H5页面和原生APP之间建立通信通道的机制,它通常涉及在原生端暴露一个API接口,H5页面可以通过调用这个接口来触发原生代码。
容器一旦接到网页的请求,就根据请求去调用底层系统的 API,然后再返回结果给网页。API Bridge 往往以 JavaScript 语言提供,方便网页调用,这时又称为 JSbridge。
JSB的流程如下所示:
其实原生的WebView/UIWebView控件已经能够和JS通信了,为什么还要JSBridge呢?
其实使用JSBridge有很多方面的考虑:
- Android 4.2以下,addJavascriptInterface方式有安全问题
- iOS 7以下,JS无法调用NA
H5 调用 NA
-
H5 调用 NA,会调用 invokeSchema 方法
- 先生成一个 message,为这条message生成一个callbackId
- 执行registerCallback方法注册回调,将回调push到responseCallbacks
- 更改 iframe 的
src
-
当 NA 检测到
iframe src
的变化,执行_handleMessageFromNative方法,获取到 JS 侧的 responseCallbacks 中的所有 message -
从NA获取到的message中获取responseId有2种情况:
- 取不到 responseId :说明是第一次调用 bridge 传过来的,此时会生成一个返回给调用方的 message,其 reponseId 是传过来的 message 的 callbackId,当 native 执行 responseCallbacks 时,会将生成的包含 responseId 的 message 返回给H5
- 有 responseId:说明这个 message 是 JS 调 Native 之后回调接收的 message,所以从一开始responseCallbacks中根据 responseId(一开始存的时候是用的callbackId,两个值是相同的)取出这个回调函数并执行
-
这样就完成了一次 JS 调用 Native 的流程
- 优点:JavaScript 端可以确定 JSBridge 的存在,直接调用即可
- 缺点:不同容器的 API Bridge不一样。为某个容器写的网页,不能放在另一个容器使用,也无法在浏览器使用,除非网页脚本做了兼容
NA 调用 H5
NA 调用 webview 注册的 JSBridge 的逻辑是相似的,不过就不是通过触发 iframe 的 src 触发执行的了,因为NA可以主动调用 _handleMessageFromNative 方法,在webview中执行对应逻辑
JSBridge封装
基本每个公司内自己会封装一套Hybrid解决方案库,并且可能不同部门间的通信协议库还不同,这种封装提供了一套标准化、简化的接口,使得我H5端和NA端的交互更加高效和易于管理
自定义封装Hybrid的特点包括:安全性、易用性、扩展性、可维护性、统一接口、可定制性等
JSB流程
一个简易版JSB流程如下所示:
全局桥对象
const JSBridge = window.JSBridge || (window.JSBridge = {});
JSB是H5页面中定义在全局对象window的一个属性,该对象有以下方法:
- _handleMessageFromNative:原生调用H5页面注册的方法,或通知H5页面执行回调方法
- registerCallback:H5调用注册方法供NA调用
- invokeSchema:H5调用NA的方法,调用后实际是本地通过url scheme触发,调用时会将callbackId存放到本地变量responseCallbacks中
JS调用NA
我们定义好了全局JSB对象,可以通过registerCallback方法调用原生API,经历了以下步骤:
- 判断是否有回调函数,有则生成callbackId,并将callbackId和对应回调添加进入回调函数集合responseCallbacks列表
/**
* 注册回调方法
*/
const registerCallback = (name, responseCallback) => {
if (!responseCallbacks[name]) {
responseCallbacks[name] = [];
}
responseCallbacks[name].push(responseCallback);
};
- 通过特定方法,将传入的数据、方法名拼接成一个url scheme,原生捕获到这个scheme后会分析
let uri = 'xxxxx://xxxJS_'; // API_Name:callbackId/handlerName?data
- 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名、参数提取出来:
-
根据api名,在本地找寻对应的api方法,并且记录该方法执行完后的callbackId
-
根据提取出来的参数将定义好的参数进行转化
-
NA本地执行对应的api方法
-
找到这次api调用对应的callbackId,连同需要传递的参数组装成JSON格式的参数,JSON格式为:{responseId:回调id,responseData:回调数据}
- responseId:H5页面中对应需要执行的callbackId,在H5注册回调时就已经产生
- responseData:NA需要传递给H5的回调数据, {code:(整型,调用是否成功,1成功,0失败),result:结果信息
-
通过JSBridge通知H5页面回调
NA调用JS
NA通过JSBridge调用H5的JS方法或者通知H5回调
//将回调信息传给H5
JSBridge._handleMessageFromNative(messageJSON);
messageJSON数据格式根据两种不同的类型:
- NA通知H5回调: 上述Native通知H5回调的JSON格式
- NA主动调用H5:{handlerName:api名,data:数据,callbackId:回调id}
H5中API注册
h5怎么注册供原生调用的api呢?
- data:原生传过来的数据
- 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
虽然主要用于开发原生应用,但RN也支持Web视图,可用于嵌入H5页面,并与NA通信。
Flutter
docs.flutter.cn/get-started…
Flutter是一个跨平台的UI工具集, 类似于RN,用于从单一的Dart代码库构建原生应用。也支持通过平台通道与H5内容通信。
写在最后
我们主要学习了H5页面与客户端应用之间的通信过程、主要方式、优缺点及应用场景;同时我们还探讨了JSB的原理和简单实现,希望对你有所帮助
参考
cloud.tencent.com/developer/a…
jonny-wei.github.io/blog/mobile…
开源项目参考:github.com/marcuswesti…