一篇写给 Android 刚入门小伙伴的 JSBridge 文章

复杂的程序都需要分层表达

正文

这篇文章写给那些经常在 JAVA 层 “扑腾”,几乎没有接触过 H5 相关技术的小伙伴。我会在文章中解释 JSBridge 是什么?JSBridge 的技术定位是什么?理清 JSBridge 出现是要解决什么问题,最后讲一下对于实现 JSBridge 的一些技术方案思考。

整篇文章,不贴过多的代码,只讲清原理和设计思路,因为只有理解了思路,才能更加灵活的做方案取舍。

注:这里的 H5 泛指前端,不仅仅是 HTML5 的含义。

背景

在一切开始之前,我们需要先搞明白 JSBridge 在客户端技术栈中所处的位置,有助于对 JSBrige 所能发挥的作用做一个整体的认知,也能从一个客户端技术演进的角度来理解 JSBridge 的重要性。

JSBridge 的思想其实对一直做 Native 的客户端开发者来说,是很陌生的,这是由其客户端的业务属性所决定的,但是只要涉及到一些运营属性,那么接触 JSBridge 就是不可以避免的。所以在这样的业务属性下,客户端的技术演进,肯定都是要经历以下的过程

Native -> Native + H5 -> Native + JSBridge -> Native + RN(Flutter)

各个阶段如下:

  • Native:在客户端初期,人力不足,只使用 Native 的能力实现一些重要的需求
  • Native + H5:在后续的发展过程中,会使用 H5 简单的处理一些静态的页面,无论是从排版来说,还是从开发成本来说,一些静态页面,例如隐私声明,协议约定等大段落文本,H5 在实时更新和发布成本来说都优于 Native 
  • Native + JSBridge:再到后边,整个团队对业务方向的摸索过程中,H5 因为高的更新的频次和可快速迭代发版的特性,会成为整个阶段的技术首选,客户端的角色就是提供对应的基本能力,这个时候就需要有一个成本不高,但是足够用的方案,Native + JSBridge ,易上手,方案简单,且维护成本较低。
  • Native + RN:但是 JSBridge 的方案太过简单,且性能上不去,而继续往后的客户端发展过程中,必然会对性能,视觉,交互有着更大的要求,原生的 WebView 的方案就会被放弃,那么 Native + RN 的方案就会脱颖而出,这套方案可以支撑更大的业务要求,对性能也提升很大,但是对技术要求和学习成本要求也更多,因为有新的语言要学习,新的框架要接入。
  • Native + Flutter:如果往更大的开放平台走,类似于支付宝和微信的平台式APP,那么使用 Flutter 这样自带渲染引擎的方案,或者自己开发定制一套方案是更好的选择

注:在方案的选取过程中,其实对于跨平台性也是一个很重要的因素,因为不涉及到方案比较,这里就不过多提及

理解了 JSBridge 在客户端演进过程中的作用后,接下来我们就来解开 JSBridge 的神秘面纱。

JSBridge

在了解了 JSBridge 的前后背景,我们从在这节内容开始直面 JSBridge。

这里我们先给 JSBridge 下一个简单的定义,从抽象的层面或者更大的角度来理解 JSBridge 的整个定位,至于对 JSBridge 直观的,可以触摸的理解,你在后边的实现方案中会看到。

JSBridge,简单来说,是一种协议,是一种思想,同时也是一类问题的一个通用解决方案。

JSBridge 是和 DHCP 协议,P2P 协议同样的解决问题的方案。

DHCP 协议用来解决 IP 静态分配导致的效率问题
P2P 协议用来解决单一服务器难以承受巨大的带宽压力问题
而 JSBridge 协议解决的是 H5 端和 Native 端的通信问题

但是给出 JSBridge 是一种协议这个结论来说未免太过简单抽象了一点,说它可以解决 H5 端和 Native 端通信的问题,许多人估计也是一头雾水,别着急,我们一步步来,先来看一下如果不使用 JSBridge,使用原生提供的解决方案是怎么样的。

原生解决方案

Google 在 API level 1 就已经提供了 addJavascriptInterface(Object object, String name) 来作为 JS 访问 Native 代码的入口,
原理是:将 Java 对象注入成为 JS 中 window 对象的一个属性,我们都知道 window 对象是浏览器中的顶级对象,名下的属性和方法可以在全局直接引用,所以 Java 对象的方法就被映射到了 window 的一个属性下了。

相应的,在 API level 1 也提供了 loadUrl(String url) 来加载 Url 或者 JS 代码

但是,这两个接口都有各自的问题

addJavascriptInterface(Object object, String name) 在 Android 4.2 之前有安全问题,Google 在 4.2 之后将其限制为加了 @JavascriptInterface 的注解才可以被访问到。

loadUrl(String url) 加载 JS,是没有返回值的,对于想要接收 JS 回调的情况来说,只能通过其他方式解决,
在 Android4.4 之后,Google 提供了 evaluateJavascript(String script, ValueCallback<String> resultCallback) 方法来加载 JS 方法并且可以接到回调。

但是不管是后续版本新增的接口也好,原有的接口也好,原生提供的方式都有一些很明显的弊端:

  1. 高低版本接口不一致,有兼容性要处理
  2. 低版本有安全漏洞,方案无法统一
  3. JS 调用 Native 没有回调,接口也不友好
  4. 无法实现自由的双端通信

因为以上的弊端,那么急需找出一种可以兼容高低版本,又没有安全漏洞,且实现统一的方案,这就是 JSBridge。

如何实现 JSBridge?

终于来到最重要的章节了

双端通信(更像是 C/S 端),其实就是

告诉对方我想要你做什么

想要对方做什么,可以换句话说,想要对方的什么资源,既然请求需要对方的每个资源,那么就需要标明要哪个资源,这种标定对方资源的请求听起来是不是有点耳熟,没错,就是 URI (Uniform Resource Identifier),唯一资源定位符

A Uniform Resource Identifier (URI) is a string of characters that unambiguously identifies a particular resource. To guarantee uniformity, all URIs follow a predefined set of syntax rules, but also maintain extensibility through a separately defined hierarchical naming scheme (e.g. http://).
– from Wikipedia

结合到我们的实际场景中来看,JS 端通过 URI 的消息格式从 Native 端获取某些资源,然后返回给 JS 端,这就是 JSBridge 要做的全部的事情。

以上内容,我们再细分为

  1. 定义 URI 格式(解决 JS 向 Native 请求数据的问题)
  2. 定义回调格式 (解决 Native 向 JS 回传数据的问题)

定义 URI 格式

翻译成人话,就是
约定 scheme 头部,标明协议类型,比如可以是 bridge
约定调用的类名,例如 JSMethod
约定调用的本次请求的唯一标识,例如 123(当前生命周期唯一标识即可)
约定调用的方法名,例如 getLocationInfo
约定调用的参数,例如 realtime=ture

将以上的内容使用 URI 协议拼接起来就是
bridge://JSMethod:123/getLocationInfo?realtime=true

定义回调

Native 在收到 JS 的请求之后,就需要对这次请求作出响应,提供 JS 端想要的数据,那么对 JS 端来说,我发起的 N 多请求,如何标明 Native 的返回的数据就是对应我刚刚发出的请求呢,我们在之前的 URI 中,针对本次的请求已经设计了一个唯一标识符,那么让 Native 回调时带上这个唯一标识符,JS 就可以和之前的请求一一对应起来了,至于如何回调的问题,我们可以约定一个回调函数 onResponse(int identiy, String jsonData) 将数据放入其中返回既可。

备注:
有些人可能直接想到的是 Java 中的 Callback 回调,但是有一点需要明确,Java 中可以使用回调是因为通信的双方都是 jvm 虚拟机可以识别的 object 对象,但是对于 JS 和 Java 这两个通信者来说,就需要找一个双方都可以识别的类型来完成回调类型的定义,对于 Android 的 WebView 内核来说,这个类型就是基本类型 String 字符串。

找到了方法标识想要的资源,那么就剩下解决如何告诉的问题了,在 HTTP 协议中,告知对方使用的是 IP 协议负责定位,TCP/UDP 协议负责传输,但是针对 Android 端与 H5 端的通信,我们互相通信的双方其实就是当前窗口生命周期内的二者,没有第三方,那么我们要做的就是只是找到可以让双方通信的”入口“即可。

通信入口

我们先来找 Native 端向 JS 端发送消息的办法,目前通用的解决方案有两种:

  1. loadUrl(String url)
  2. evaluateJavascript(String script, ValueCallback<String> resultCallback)

这两个方案的利弊和使用限制,我们在上边已经解释过了,这里就不再多说。

接下来就是 JS 端向 Native 端发送消息的办法,目前通用的解决方案有四种:
1.可以在 shouldOverrideUrl() 中拦截Url,在 url 中带参数过来,参数预先和 Native 协商好

这是 WebViewClient 的一个可以复写的方法,目的是在当前 WebView 加载 Url 时给应用程序一个控制的机会,控制什么,控制要不要加载这个 url,一般来说页面内部的重定向都会走这个函数,我们可以决定要不要拦截一些跳转,当然,我们也可以和 H5 约定在链接中加入参数来调用 Native 的某些资源

2.也可以在 onJsPrompt()onJsConfirm()onJsAlert(),中传入参数过来,同样和 Native 协商好就可以

这三个方法分别对应 JS 中的 window.prompt()window.confirm()window.alert(),同样,在 WebViewClient 中可以复写这三个方法接收消息

3.还有第三种方案,使用 onConsoleMessage(ConsoleMessage consoleMessage) 方案
这个方法对应 js 中的 console.log(),同样,可以在 WebViewClient 中复写接收消息

4.原生的注入方法,使用 addJavascriptInterface(Object object, String name),这个方案的利弊也已经在上边解释过了

总结

在定义好协议,找到通信入口之后,我们就可以在 JS 中使用定义好的协议请求 Native 的代码了,然后在自定义的回调中获取 Native 回传的内容,这部分内容可以参考 github 中的实现,原理都是相同的。

参考:稍后补上,尽请期待,( https://github.com/0xforee

结尾

这一期简单介绍了 JSBridge 的一些实现方案和思路,但是如果要写一个合格稳定可用的 JSBridge 除了要定好协议,基本实现出来,还需要一些额外的调优工作,我们在接下来的一期中继续探索这方面的内容。

一篇写给 Android 刚入门小伙伴的 JSBridge 文章

http://www.0xforee.top/2019/11/11/android-jsbridge/

作者

0xforee

发布于

2019-11-11

更新于

2023-08-12

许可协议


欢迎关注我的公众号 0xforee,第一时间获取更多有价值的思考

评论