为什么要在App中嵌入H5页面?
由于app原生页面每次有更新,都需要重新打包一次发布到应用平台上,且每次要向各个应用商店进行提交审核。之后用户需要手动进行点击更新安装,安装成本较高。
h5开发速度快,一端开发多端运行,如果APP用户常见页面频换,那么用H5,维护起来更容易。迭代版本时,不需要打包便可以发布(实时更新、快速迭代),与云端实现实时数据交互。
h5与原生app交互
移动端 web 应用,很多时候都需要与原生 app 进行交互、沟通(运行在 webview 中),比如微信的 jssdk,通过 window.wx 对象调用一些原生 app 的功能。
h5 与原生 app 的交互,本质上说,就是两种调用:app 调用 h5 的代码/h5 调用 app 的代码
app调用h5的代码
因为 app 是宿主,可以直接访问 h5,所以这种调用比较简单,就是在 h5 中曝露一些全局对象(包括方法),然后在原生 app 中调用这些对象。
h5调用app的代码
因为 h5 不能直接访问宿主 app,所以这种调用就相对复杂一点。
JavaScript调用Native的方式,主要有两种:注入api和拦截url scheme。
- 注入api:由app向h5注入一个全局 js 对象,然后在h5直接访问这个对象
- 拦截url scheme:由h5发起一个自定义协议请求,app 拦截这个请求后,再由 app 调用 h5 中的回调函数。这种方式要稍复杂一点,因为需要自定义协议,可以作为第一种方式的补充。
- 由 app 自定义协议,比如 sdk://action?params
- 在 h5 定义好回调函数,比如 window.bridge = {getDouble: value => {}, getTriple: value => {}}
- 由 h5 发起一个自定义协议请求,比如 location.href = ‘sdk://double?value=10’
- app 拦截这个请求后,进行相应的操作,获取返回值
- 由 app 调用 h5 中的回调函数,比如 window.bridge.getDouble(20);
JSBridge
JSBridge的用途
JSBridge 就像其名称中的『Bridge』的意义一样,是 Native和非Native之间的桥梁,它的核心是构建 Native 和非Native间消息通信的通道,而且是双向通信的通道。
- JS向Native发送消息:调用相关功能、通知Native当前JS的相关状态等。
- Native向JS发送消息:回溯调用结果、消息推送、通知JS当前Native的状态等。
疑问:消息都是单向的,那么调用 Native 功能时 Callback 如何实现?
JSBridge的实现原理
JavaScript 是运行在一个单独的 JS Context 中(例如,WebView的Webkit引擎、JSCore)。由于这些 Context 与原生运行环境的天然隔离,我们可以将这种情况与 RPC(Remote Procedure Call,远程过程调用)通信进行类比,将 Native 与 JavaScript 的每次互相调用看做一次 RPC 调用。
在 JSBridge 的设计中,可以把前端看做 RPC 的客户端,把 Native 端看做 RPC 的服务器端,从而 JSBridge 要实现的主要逻辑就出现了:通信调用(Native 与 JS 通信)和句柄解析调用。这个流程类似JSONP的流程。
JSBridge通信原理
1. JS调用Native
通过上面的介绍,我们知道JavaScript 调用 Native 的方式主要有两种:注入api和拦截url scheme。
1.1 注入api
注入 API 方式的主要原理是,通过 WebView 提供的接口,向 JavaScript 的 Context(window)中注入对象或者方法,让 JavaScript 调用时,直接执行相应的 Native 代码逻辑,达到 JavaScript 调用 Native 的目的。
对于 iOS 的 UIWebView,实例如下:
1 | JSContext *context = [uiWebView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]; |
前端调用方式:
1 | window.RenrencheJSBridge(message); |
对于 iOS 的 WKWebView,实例如下:
1 | @interface WKWebVIewVC ()<WKScriptMessageHandler> |
前端的调用方式:
1 | window.webkit.messageHandlers.GoBack.postMessage(params); |
对于 Android 可以采用下面的方式:
1 | public class JavaScriptInterfaceDemoActivity extends Activity{ |
前端调用方式:
1 | window.nativeBridge.postMessage(message); |
⚠️ 在 4.2 之前,Android 注入 JavaScript 对象的接口是 addJavascriptInterface,但是这个接口有漏洞,可以被不法分子利用,危害用户的安全,因此在 4.2 中引入新的接口 @JavascriptInterface(上面代码中使用的)来替代这个接口,解决安全问题。所以 Android 注入对对象的方式是 有兼容性问题的。
1.2 拦截url scheme
url scheme是一种类似于url的链接,是为了方便app直接互相调用设计的,形式和普通的 url 近似,主要区别是 protocol 和 host 一般是自定义的,例如: qunarhy://hy/url?url=ymfe.tech,protocol 是 qunarhy,host 则是 hy。
缺陷:
- 使用 iframe.src 发送 URL SCHEME 会有 url 长度的隐患。
- 创建请求,需要一定的耗时,比注入 API 的方式调用同样的功能,耗时会较长。
为什么之前要采用这种不优雅的方式?因为它支持iOS6,但是现在基本可以忽略
⚠️ 通过 location.href 连续调用 Native,很容易丢失一些调用。
2. Native调用JS
相比于 JavaScript 调用 Native, Native 调用 JavaScript 较为简单,毕竟不管是 iOS 的 UIWebView 还是 WKWebView,还是 Android 的 WebView 组件,都以子组件的形式存在于 View/Activity 中,直接调用相应的 API 即可。
Native 调用 JavaScript,其实就是执行拼接 JavaScript 字符串,从外部调用 JavaScript 中的方法,因此 JavaScript 的方法必须在全局的 window 上。
对于 iOS 的 UIWebView,示例如下:
1 | result = [uiWebview stringByEvaluatingJavaScriptFromString:javaScriptString]; |
对于 iOS 的 WKWebView,示例如下:
1 | [wkWebView evaluateJavaScript:javaScriptString completionHandler:completionHandler]; |
对于 Android,在 Kitkat(4.4)之前并没有提供 iOS 类似的调用方式,只能用 loadUrl 一段 JavaScript 代码,来实现:
1 | webView.loadUrl("javascript:" + javaScriptString); |
而 Kitkat 之后的版本,也可以用 evaluateJavascript 方法实现:
1 | webView.evaluateJavascript(javaScriptString, new ValueCallback<String>() { |
⚠️ 使用 loadUrl 的方式,并不能获取 JavaScript 执行后的结果。
通信原理总结
- JavaScript 调用 Native 推荐使用 注入 API 的方式(iOS6 忽略,Android 4.2以下使用 WebViewClient 的 onJsPrompt 方式)。
- Native 调用 JavaScript 则直接执行拼接好的 JavaScript 代码即可。
对于其他方式,诸如 React Native、微信小程序 的通信方式都与上描述的近似,并根据实际情况进行优化。
以 React Native 的 iOS 端举例:JavaScript 运行在 JSCore 中,实际上可以与上面的方式一样,利用注入 API 来实现 JavaScript 调用 Native 功能。
不过 React Native 并没有设计成 JavaScript 直接调用 Object-C,而是 为了与 Native 开发里事件响应机制一致,设计成 需要在 Object-C 去调 JavaScript 时才通过返回值触发调用。原理基本一样,只是实现方式不同。
当然不仅仅 iOS 和 Android,其他手机操作系统也用相应的 API,例如 WMP(Win 10)下可以用 window.external.notify 和 WebView.InvokeScript/InvokeScriptAsync 进行双向通信。其他系统也类似。
JSBridge封装
JSBridge 的接口主要功能有两个:
- 调用 Native(给 Native 发消息)
- 被 Native 调用(接收 Native 消息)。
因此JSBridge可以设计成如下:
1 | class Bridge { |
上面的疑问:消息都是单向的,那么调用 Native 功能时 Callback 怎么实现的?
对于 JSBridge 的 Callback ,其实就是 RPC 框架的回调机制。也可以用更简单的 JSONP 机制解释:
当发送JSONP请求时,url参数里会有callback参数,其值是当前页面唯一的,而同时以此参数值为key将回调函数存到window上,随后,服务器返回 script中,也会以此参数值作为句柄,调用相应的回调函数。
由此可见,callback参数这个唯一标识是这个回调逻辑的关键。这样,我们可以参照这个逻辑来实现JSBridge:用一个自增的唯一id,来标识并存储回调函数,并把此id以参数形式传递给 Native,而Native也以此id作为回溯的标识。这样,即可实现Callback回调逻辑。
实战应用:
1 | class Bridge { |
⚠️ 注:
- 在 Native 端配合实现 JSBridge 的 JavaScript 调用 Native 逻辑:
接收到 JavaScript 消息 => 解析参数,拿到 bridgeName、data 和 callbackId => 根据 bridgeName 找到功能方法,以 data 为参数执行 => 执行返回值和 callbackId 一起回传前端。 - Native 调用 JavaScript:
直接自动生成一个唯一的 ResponseId,并存储句柄,然后和 data 一起发送给前端
参考文章:
https://juejin.im/post/5abca877f265da238155b6bc
https://mp.weixin.qq.com/s/vwIRE3nYzoG-PfvfT45Wsg