简介
-
近些年,移动端普及化越来越高,开发过程中选用 Native 还是 H5 一直是热门话题。Native 和 H5 都有着各自的优缺点,为了满足业务的需要,公司实际项目的开发过程中往往会融合两者进行 Hybrid 开发。Native 和 H5 分处两地,看起来无法联系,那么如何才能让双方协同实现功能呢?
-
所以出现了 JSBridge ,顾名思义,JS 桥梁,它是用 JS 实现 H5 和 Native 之间双向通行的方法统称。
-
JSBridge 主要提供了 JS 调用 Native 代码的能力,实现原生功能如查看本地相册、打开摄像头、指纹支付等。
H5 和 Native 对比
| 功能 | H5 | Native |
|---|---|---|
| 稳定性 | 调用系统浏览器内核,稳定性较差 | 使用原生内核,更加稳定 |
| 受网速 影响 | 较大 | 较小 |
| 用户体验 | 功能受浏览器限制,体验有时较差 | 原生系统 api 丰富,能实现的功能较多,体验较好 |
| 流畅度 | 有时加载慢,给用户“卡顿”的感觉 | 加载速度快,更加流畅 |
| 可移植性 | 兼容跨平台跨系统,如 PC 与 移动端,iOS 与 Android | 可移植性较低,对于 iOS 和 Android 需要维护两套代码
|
总结:原生用户体验方面比 H5好, 但是 H5更加灵活,所以我们通过 JSBridge 可以结合两者的优点。
手机上展示 H5 页面
我们要在手机上展示 H5 页面,那么会需要一个容器,一个载体,根据我们的尝试,我们会想到 iframe 和 webview,那么应该选择 iframe 还是 webview 呢。 **iframe **是一个 HTML 标签,浏览器识别到这个标签,会为它创建一个视图区域,来展示其中链接的内容。
**webview **以Android为例。在Android手机中内置了一款高性能webkit内核浏览器,在SDK中封装为一个叫做WebView组件。他可以直接用来加载 HTML 。
小结: 我们可知,iframe 适用于 PC Web 端,他需要浏览器的支持。而 webview 是手机内置的一个控件,可以很好的支持 HTML 的渲染,而且有丰富的 API 供我们使用,所以我们一般 JSBridge 中都是使用 webview 作为容器。
JSBridge 的双向通信
既然是双向通行,那么就会分为 H5 向 Native 发送请求,和 Native 向 H5 发送请求。
H5 向 Native 发送请求
1. 拦截 URL Scheme
url scheme 是一种类似于 url 的链接,是为了方便 app 直接互相调用设计的。 具体为,可以用系统的OpenURI打开一个类似于url的链接(可拼入参数),然后系统会进行判断,如果是系统的url scheme,则打开系统应用, 否则找看是否有app注册这种scheme,打开对应app 需要注意的是,这种scheme必须原生app注册后才会生效,如微信的scheme为(weixin://) 格式如下
jsbridge://showToast?text=hello
Android 和 iOS 都可以通过拦截 URL Scheme 并解析 scheme 来决定是否进行对应的 Native 代码逻辑处理。我们对请求的格式进行判断:如果符合我们自定义的 URL Schema ,对 URL 进行解析,拿到相关操作、操作,进而调用原生 Native 的方法如果不符合我们自定义的 URL Schema ,我们直接转发,请求真正的服务
Web发送 URL 请求的方法
- a 标签
- location.href
- 使用 iframe.src
- 发送 ajax 请求
这些方法, a 标签需要用户操作 location.href 可能会引起页面的跳转丢失调用,发送 ajax 请求 Android 没有相应的拦截方法,所以使用 iframe.src 是经常会使用的方案
拦截方法
Android Webview提供了shouldOverrideUrlLoading Api,来提供给 Native 拦截 H5 发送的 URL Scheme 请求。
这种方式从早期就存在,兼容性很好,但是由于是基于URL的方式,长度受到限制而且不太直观,数据格式有限制,而且建立请求有时间耗时。 代码如下
public class CustomWebViewClient extends WebViewClient {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
......
// 场景一: 拦截请求、接收 scheme
if (url.equals("xxx")) {
// handle
...
// callback
view.loadUrl("javascript:setAllContent(" + json + ");")
return true;
}
return super.shouldOverrideUrlLoading(url);
}
}
小结:
- 首先要确定通信格式,形成一个映射,通过拦截你的 Url ,触发不同的 Api。
2.向 Webview 注入 JS API
这个方法会通过 webView 提供的接口,App 将 Native 的相关接口注入到 JS 的 Context(window)的对象中,一般来说这个对象内的方法名与 Native 相关方法名是相同的,Web 端就可以直接在全局 window下使用这个全局 JS 对象,进而调用原生端的方法。
Android(4.2+)提供了 addJavascriptInterface 注入addJavascriptInterface
// 注入全局JS对象
webView.addJavascriptInterface(new NativeBridge(this), "NativeBridge");
class NativeBridge {
private Context ctx;
NativeBridge(Context ctx) {
this.ctx = ctx;
}
// 增加JS调用接口
@JavascriptInterface
public void showNativeDialog(String text) {
new AlertDialog.Builder(ctx).setMessage(text).create().show();
}
}
在 Web 端调用。
window.NativeBridge.showNativeDialog('hello');
小结:addJavascriptInterface方式有安全漏洞,所以渐渐被抛弃。
3.重写 prompt 等原生方法
Android 4.2 之前注入对象的接口是 addJavascriptInterface ,但是由于安全原因慢慢不被使用。一般会通过修改浏览器的部分 Window 对象的方法来完成操作。主要是拦截 alert、confirm、prompt、console.log 四个方法,分别被Webview的 onJsAlert、onJsConfirm、onConsoleMessage、onJsPrompt 监听。
public boolean onJsPrompt(WebView view, String origin, String message, String defaultValue, final JsPromptResult result) {
String handledRet = parentEngine.bridge.promptOnJsPrompt(origin, message, defaultValue);
xxx;
return true;
}
使用该方式时,可以与 Android 和 iOS 约定好使用传参的格式,这样 H5 可以无需识别客户端,传入不同参数直接调用 Native 即可。剩下的交给客户端自己去拦截相同的方法,识别相同的参数,进行自己的处理逻辑即可实现多端表现一致,另外,如果能与 Native 确定好方法名、传参等调用的协议规范,这样其它格式的 prompt 等方法是不会被识别的,能起到隔离的作用。
Native 向 H5 发送请求
Native 调用 JS 比较简单,只要 H5 将 JS 方法暴露在 Window 上给 Native 调用即可。
Android 实现
- loadUrl 方法使用起来方便简洁,但是效率低无法获得返回结果且调用的时候会刷新 WebView 。
webView.loadUrl("javascript:" + javaScriptString);
- evaluateJavascript 方法效率高获取返回值方便,调用时候不刷新 WebView,但是只支持 Android 4.4+。
webView.evaluateJavascript(javaScriptString, new ValueCallback<String>() {
@Override
public void onReceiveValue(String value){
xxx
}
});
IOS 实现
通过evaluateJavaScript:javaScriptString 来实现,支持 iOS 8.0 及以上系统。
// swift
func evaluateJavaScript(_ javaScriptString: String,
completionHandler: ((Any?, Error?) -> Void)? = nil)
// javaScriptString 需要调用的 JS 代码
// completionHandler 执行后的回调
小结:实际上是用来一个类似 JSONP 的方式。