移动端Hybrid通信 —— jsBridge实现与原理分析

时间:2021-09-13
本文章向大家介绍移动端Hybrid通信 —— jsBridge实现与原理分析,主要包括移动端Hybrid通信 —— jsBridge实现与原理分析使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

前言

        近期在做移动端的开发工作,之前大大小小的移动端项目也做了几个,遇到不少 需要交互的功能,这块东西端就像中国象棋中的“楚河汉界”一般的存在,本文我们将从移动端最常见的一种唤醒交互(如推广、引流等)讲起 ,一起探索Hybrid 开发模式下的 JS Bridge 通信技术。
        顾名思义,JS Bridge 的意思就是桥,也就是连接 JS 和 Native 的桥梁,它也是 Hybrid App 里面的核心。一般分为 JS 调用 Native 和 Native 主动调用 JS 两种形式。

场景导入

下面的场景大家应该不陌生

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

同样,你可以用同样的方式,来打开我们的吉客云。那这个功能是如何做到的呢?下面我们将从这一最基础的操作开始,逐步深入 jsBridge 交互的基本形式。

一、概念导入

URL Scheme

URL Scheme 是一种特殊的 URL,一般用于在 Web 端唤醒 App,甚至跳转到 App 的某个页面,比如在某个手机网站上付款的时候,可以直接拉起支付宝支付页面。
这里有个常用 App URL Scheme 汇总:URL Schemes 收集整理
在手机里面打开这个页面后点击这里,就会提示你是否要打开微信。
我们常说的 Deeplink 一般也是基于 URL Scheme 来实现的。一个 URI 的组成结构如下:
URI = scheme:[//authority]path[?query][#fragment] // scheme     = http // authority  = www.baidu.com // path       = /link // query      = url=xxxxx authority = [userinfo@]host[:port]
除了 http/https 这两个常见的协议,还可以自定义协议。借用维基百科的一张图:
通常情况下,App 安装后会在手机系统上注册一个 Scheme,比如 weixin:// 这种,所以我们在手机浏览器里面访问这个 scheme 地址,系统就会唤起我们的 App。
一般在 Android 里面需要到 AndroidManifest.xml 文件中去注册 Scheme:
<activity    android:name=".login.dispatch.DispatchActivity"    android:launchMode="singleTask"    android:theme="@style/AppDispatchTheme">    <intent-filter>        <action android:name="android.intent.action.VIEW" />        <category android:name="android.intent.category.DEFAULT" />        <category android:name="android.intent.category.BROWSABLE" />        <data android:scheme="taobao" />        <data android:host="xxx" />        <data android:path="/goods" />    </intent-filter> </activity>
在 iOS 中需要在 Xcode 里面注册,有一些已经是系统使用的不应该使用,比如 Maps、YouTube、Music。
具体可以参考苹果开发者官网文档:Defining a Custom URL Scheme for Your App

另外,Apple 在WWDC 2015上为 iOS 9 引入的一个新功能Universal Link。这里不展开讲,相关详细说明请参考:Support Universal Links 

二、JS 调用 Native
在 iOS 里面又需要区分 UIWebView 和 WKWebView 两种 WebView:
WKWebView 是 iOS8 之后出现的,目的是取代笨重的 UIWebView,它占用内存更少,大概是 UIWebView 的 1/3,支持更好的 HTML5 特性,性能更加强大。
但也有一些缺点,比如不支持缓存,需要自己注入 Cookie,发送 POST 请求的时候带不了参数,拦截 POST 请求的时候无法解析参数等等。
JS 调用 Native 通信大致有三种方法:
  1. 拦截 Scheme
  2. 弹窗拦截
  3. 注入 JS 上下文
这三种方式总体上各有利弊,下面会一一介绍。
1、拦截 Scheme
仔细思考一下,如果是 JS 和 Java 之间传递数据,我们该怎么做呢?
对于前端开发来说,调 Ajax 请求接口是最常见的需求了。不管对方是 Java 还是 Python,我们都可以通过 http/https 接口来获取数据。实际上这个流程和 JSONP 更加类似。
已知客户端是可以拦截请求的,那么可不可以在这个上面做文章呢?
如果我们请求一个不存在的地址,上面带了一些参数,通过参数告诉客户端我们需要调用的功能呢?
比如我要调用扫码功能:
axios.get('http://xxxx?func=scan&callback_id=yyyy')
客户端可以拦截这个请求,去解析参数上面的 func 来判断当前需要调起哪个功能。客户端调起扫码功能之后,会获取 WebView 上面的 callbacks 对象,根据 callback_id 回调它。
所以基于上面的例子,我们可以把域名和路径当做通信标识,参数里面的 func 当做指令,callback_id 当做回调函数,其他参数当做数据传递。对于不满足条件的 http 请求不应该拦截。
当然了,现在主流的方式是前面我们看到的自定义 Scheme 协议,以这个为通信标识,域名和路径当做指令。
这种方式的好处就是 iOS6 以前只支持这种方式,兼容性比较好。
JS 端
我们有很多种方法可以发起请求,目前使用最广泛的是 iframe 跳转:
  1. 使用 a 标签跳转
<a href="taobao://">点击我打开淘宝</a>
  1. 重定向
location.href = "taobao://"
  1. iframe 跳转
const iframe = document.createElement("iframe"); iframe.src = "taobao://" iframe.style.display = "none" document.body.appendChild(iframe)
Android 端
在 Android 侧可以用 shouldOverrideUrlLoading 来拦截 url 请求。
iOS 端
在 iOS 侧需要区分 UIWebView 和 WKWebView 两种方式。 在 UIWebView 中:
在 WKWebView 中:
目前不建议只使用拦截 URL Scheme 解析参数的形式,主要存在几个问题。
  1. 连续续调用 location.href 会出现消息丢失,因为 WebView 限制了连续跳转,会过滤掉后续的请求。
  2. URL 会有长度限制,一旦过长就会出现信息丢失 因此,类似 WebViewJavaScriptBridge 这类库,就结合了注入 API 的形式一起使用,这也是我们这边目前使用的方式,后面会介绍一下。
2、弹窗拦截
Android 实现
这种方式是利用弹窗会触发 WebView 相应事件来拦截的。
一般是在 setWebChromeClient 里面的 onJsAlert、onJsConfirm、onJsPrompt 方法拦截并解析他们传来的消息。
iOS 实现
我们以 WKWebView 为例:
这种方式的缺点就是在 iOS 上面 UIWebView 不支持,但是 WKWebView 又有更好的 scriptMessageHandler,比较尴尬。
注入上下文
前面我们有讲过在 iOS 中内置了 JavaScriptCore 这个框架,可以实现执行 JS 以及注入 Native 对象等功能。
这种方式不依赖拦截,主要是通过 WebView 向 JS 的上下文注入对象和方法,可以让 JS 直接调用原生。
PS:iOS 中的 Block 是 OC 对于闭包的实现,它本质上是个对象,定义 JS 里面的函数。
iOS UIWebView
iOS 侧代码:
JS 代码:
这种方式厉害的地方在于,JS 调用是同步的,可以立马拿到返回值。
我们也不再需要像拦截方式一样,每次传值都要把对象做 JSON.stringify,可以直接传 JSON 过去,也支持直接传一个函数过去。
iOS WKWebView
WKWebView 里面通过 addScriptMessageHandler 来注入对象到 JS 上下文,可以在 WebView 销毁的时候调用 removeScriptMessageHandler 来销毁这个对象。
前端调用注入的原生方法之后,可以通过 didReceiveScriptMessage 来接收前端传过来的参数。
使用 addScriptMessageHandler 注入的对象实际上只有一个 postMessage 方法,无法调用更多自定义方法。前端的调用方式如下:
window.webkit.messageHandlers.nativeObj.postMessage(data);
需要注意的是,这种方式要求 iOS8 及以上,而且返回不是同步的。和 UIWebView 一样的是,也支持直接传 JSON 对象,不需要 stringify。
Android addJavascriptInterface
安卓4.2之前注入 JS 一般是使用 addJavascriptInterface ,和前面的 addScriptMessageHandler 有一些类似,但又没有它的限制。
在 JS 里面调用:
window.DatePickerBridge._pick(...)
但这种方案有一定风险,可以参考这篇文章:WebView中接口隐患与手机挂马利用
在 Android4.2 之后提供了 @JavascriptInterface 注解,暴露给 JS 的方法必须要带上这个。
所以前面的 _pick 方法需要带上这个注解。
Native 调用 JS
Native 调用 JS 一般就是直接 JS 代码字符串,有些类似我们调用 JS 中的 eval 去执行一串代码。一般有 loadUrl、evaluateJavascript 等几种方法,这里逐一介绍。
但是不管哪种方式,客户端都只能拿到挂载到 window 对象上面的属性和方法。
Android
在 Android 里面需要区分版本,在安卓4.4之前的版本支持 loadUrl,使用方式类似我们在 a 标签的 href 里面写 JS 脚本一样,都是javascript:xxx 的形式。
这种方式无法直接获取返回值。
webView.loadUrl("javascript:foo()")
在安卓4.4以上的版本一般使用 evaluateJavascript 这个 API 来调用。这里需要判断一下版本。
UIWebView
在 iOS 的 UIWebView 里面使用 stringByEvaluatingJavaScriptFromString 来调用 JS 代码。这种方式是同步的,会阻塞线程。
results = [self.webView stringByEvaluatingJavaScriptFromString:"foo()"];
WKWebView
WKWebView 可以使用 evaluateJavaScript 方法来调用 JS 代码。
JS Bridge 设计
前面讲完了 JS 和 Native 互调的所有方法,这里来介绍一下我们这边 JS Bridge 的设计吧。
我们以基于 WebViewJavascriptBridge 这个库来实现JS Bridge 通信 。
主要是结合 Scheme 协议+上下文注入来做。考虑到 Android 和 iOS 不一样的通信方式,这里进行了封装,保证提供给外部的 API 一致。
具体功能的调用我们封装成了 npm 包,下面的是几个基础 API:
  1. callHandler(name, params, callback):这个是调用 Native 功能的方法,传模块名、参数、回调函数给 Native。
  2. hasHandler(name):这个是检查客户端是否支持某个功能的调用。
  3. registerHandler(name):这个是提前注册一个函数,等待 Native 回调,比如 pageDidBack 这种场景。
那么这几个 API 又是如何实现的呢?这里 Android 和 iOS 封装不一致,应当分开来说。
Android Bridge
前面我们有说过安卓可以通过 @JavascriptInterface 注解来将对象和方法暴露给 JS。
所以这里的几个方法都是通过注解暴露给 JS 来调用的,在 JS 层面做了一些兼容处理。
hasHandler
首先最简单的是这个 hasHandler,就是在客户端里面维护一张表(其实我们是写死的),里面有支持的 Bridge 模块信息,只需要用 switch...case 判断一下就行了。
callHandler
然后我们来看 callHandler 这个方法,它是提供 JS 调用 Native 功能的方法。在调用这个方法之前,我们一般需要先判断一下 Native 是否支持这个功能。
如果 Native 没有支持这个 Bridge,我们就需要对回调进行兼容性处理。这个兼容性处理包括两个方面,一个是功能方面,一个是 callback 的默认回参。
比如我们调用 Native 的弹窗功能,如果客户端没支持这个 Bridge,或者我们是在浏览器里面打开的这个页面,此时应该退出到使用 Web 的 alert 弹窗。
对于 callback,我们可以默认给传个 0,表示当前不支持这个功能。
假设这个 alert 的 bridge 接收两个参数,分别是 title 和 content,那么此时就应该使用浏览器自带的 alert 展示出来。
这个 fallback 函数我们希望能够更加通用,每个调用方法都应该有自己的 fallback 函数,所以前面的 callHandler 应该设计成这样:
我们可以基于这个函数封装一些功能方法,比如前面的 alert:
具体效果类似下面这种,这是从 Google 上随便找的一张图(侵删):
那么客户端又如何实现回调 callback 函数的呢?前面说过,客户端想调用 JS 方法,只能调用挂载到 window 对象上面的。
因此,这里使用了一种很巧妙的方法,实际上 callback 函数依然是 JS 执行的。
在调用 Native 之前,我们可以先将 callback 函数和一个 uniqueId 映射起来,然后存在 JS 本地。我们只需要将 callbackId 传给 Native 就行了。
在客户端这里,当 send 方法接收到参数之后,会执行相应功能,然后使用 webView.loadUrl 主动调用前端的一个接收函数。
所以 JS 需要事前定义好这个 onReceive 方法,它接收一个 callbackId 和一个 result。
大致流程如下:
registerHandler
注册的流程比较简单,也是我们把 callback 函数事先存到一个 messageHandler 对象里面,不过这次的 key 不再是一个随机的 id,而是 name。
这里不像 callHandler 需要主动调用 window.bridge.send 去通知客户端,只需要等客户端到了相应的时机来调用 window.bridge.onReceive 就行了。
所以这里还需要改造一下 onReceive 方法。由于不再会有 callbackId 了,所以客户端可以传个空值,然后将 handlerName 放到 result 里面。
这种情况下的流程如下,可以发现完全不需要 JS 调用 Native:
iOS Bridge
讲完了 Android,我们再来讲讲 iOS,原本 iOS 可以和 Android 设计一致,可是由于种种原因导致有不少差异。
iOS 和 Android 中最显著的差异就在于这个 window.bridge.send 方法的实现,Android 里面是直接调用 Native 的方法,iOS 中是通过 URL Scheme 的形式调用。
协议依然是 WebViewJavaScriptBridge 里面的协议,URL Scheme 本身不会传递数据,只是告诉 Native 有新的调用。
然后 Native 会去调用 JS 的方法,获取队列里面所有需要执行的方法。
所以我们需要事先创建好一个 iframe,插入到 DOM 里面,方便后续使用。
callHandler
每次调用的时候只需要复用这个 iframe 就行了。这里是处理 callback 并通知 Native 的代码:
通知 Native 之后,它怎么拿到我们的 handlerName 和 data 呢?我们可以实现一个 fetchQueue 的方法。
然后将其挂载到 window.WebViewJavascriptBridge 对象上面。
  
这样 iOS 就可以使用 evaluateJavaScript 轻松拿到这个 messageQueue。
那么 iOS 又是如何回调 JS 的 callback 函数呢?这个其实和 Android 的 onReceive 是同样的原理。
这里可以实现一个 _handleMessageFromObjC 方法,同样挂载到 window.WebViewJavascriptBridge 对象上面,等待 iOS 回调。
流程如下图:
registerHandler
registerHandler 和 Android 原理是一模一样的,都是提前注册一个事件,等待 iOS 调用,具体就不多讲了,这里直接放代码:

总结

        上述就是 Hybrid 开发中 JS 和 Native 交互的大致原理,经过上述介绍大家对这一技术应该有了一个系统的认识,但也忽略了不少细节(如初始化 WebViewJavascriptBridge 对象等等 ),谨此记录,感兴趣的也可以参考一下这个库:GitHub - lzyzsd/JsBridge: android java and javascript bridge, inspired by wechat webview jsbridge 

期待三连

原文地址:https://www.cnblogs.com/liuyongqi/p/15262550.html