JSBridge:混合开发中的双向通信[AndroidiOS&JavaScript]

Posted 恪愚

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JSBridge:混合开发中的双向通信[AndroidiOS&JavaScript]相关的知识,希望对你有一定的参考价值。

什么是WebView

WebView 是移动端中的一个控件,它为 JS 运行提供了一个沙箱环境。WebView 能够加载指定的 url,拦截页面发出的各种请求等各种页面控制功能,JSB 的实现就依赖于 WebView 暴露的各种接口。

由于历史原因,ios以8为分界,android以4.4为分界,分为高低两个版本。而它们的区别在于 —— 回调。高版本可以通过执行回调拿到 JS 执行完毕的返回值,然后准确进行下一步操作。而低版本无法执行回调!

什么是 JSB

Hybrid App 的核心。我们开发的 h5 页面运行在端上的 WebView 容器之中,很多业务场景下 h5 需要依赖端上提供的信息/能力,这时我们需要一个可以连接原生运行环境和 JS 运行环境的桥梁 。 这个桥梁就是 JSB,JSB 让 Web 端和 Native 端得以实现双向通信。

JSB的目的

JSB 的目的就是“让 Native 可以调用 web 端的 javascript 代码,让 web 端可以调用 Native 的原生代码”。

native 调用 web 端代码

无论 Android 还是 iOS,在调用 web 端代码的时候,必须是调用的“挂载在window上的函数”。拿一个例子来说:

<Button
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="调用JS方法"
    android:onClick="onJSFunction1"/>
public void onJSFunction1 (View v) 
    mWebView.evaluateJavascript("javascript:onFunction('android调用JS方法')", new ValueCallback<String>() 
        @Override
        public void onReceiveValue(String s) 
            AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
            builder.setMessage(s);
            builder.setNegativeButton("确定", null);
            builder.create().show();
        
    );

evaluateJavascript 就是调用 JS 中的方法:onFunction1,并传入参数,在回调中进行处理 —— 这个回调至关重要。

在 js 中:

window.onFunction = function(str) 
	alert(str);
	return "这是 onFunction 方法的返回值";

上面的 java 代码中,mWebView 是实例化的 腾讯X5内核组件。在它的配置中首先注意到这样一行代码:

/**
 * 允许加载的网页执行 JavaScript 方法
 */
webSettings.setJavaScriptEnabled(true);

允许网页执行 JS 方法。这个非常重要:它是 Native 是否能够向 web 通信的关键!这一点下面我们会提到。

在进行完一些基础配置后,我们会构建一个 JSBridge 对象:

addJavascriptInterface(
        new MyJaveScriptInterface(mContext, this),
        "AndroidJSBridge");

这个对象就叫做“AndroidJSBridge”,在这个处理中,这个 JSB 对象会被挂载到网页的 window 对象下,从而作为原生端和 web 端通信的桥梁。

web 调用 native 端代码

上面说了在初始化后 window 对象下会有一个 AndroidJSBridge 对象,在网页中也可以直接通过 window.AndroidJSBridge 拿到这个对象,从而调用 Android 端提供给网页端的方法(这里以Android为例):
现在 Android 提供了这样一个方法:

@JavascriptInterface
public void androidTestFunction1 (String str) 
    AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
    builder.setMessage(str);
    builder.setNegativeButton("确定", null);
    builder.create().show();

目的是在 APP 弹出一个 Alert 对话框,对话框中的内容为 JavaScript 传入的字符串。

注意:对于 Android 来说,当 js 调用方法并传参时,Android方法接收的参数只能是“基本数据类型”。对于“复杂数据类型”,只能通过JSON.stringify(Object) 转化成 string 类型。
而 iOS 则不受此限制。

在 web 项目中,拿原生项目来说,我们直接这么写即可:

<input type="button" value="调用androidTestFunction1" @click="toAndroidFunction1()" />

<script>
function toAndroidFunction1() 
	window.AndroidJSBridge.androidTestFunction1('调用 android 下的 function1 方法')

</script>


当然,在 android 方法中,我们也可以直接 return 数据,到了 web 端就是“回调”了。

function toAndroidFunction2() 
	let result = window.AndroidJSBridge.androidTestFunction1('androidTestFunction2方法的返回值');
	alert(result);


诶,为什么调用的 alert 也是弹出的 android 原生弹出框?
因为在初始化的时候,原生对 alert 进行了一次“劫持”:

/**
 * 监听网页中的url加载事件
 */
private void initChromeClient () 
    setWebChromeClient(new WebChromeClient()

        /**
         * alert()
         * 监听alert弹出框,使用原生弹框代替alert。
         */
        @Override
        public boolean onJsAlert(WebView webView, String s, String s1, JsResult jsResult) 

                AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
                builder.setMessage(s1);
                builder.setNegativeButton("确定", null);
                builder.create().show();
            jsResult.confirm();

            return true;
        
    );

双向通信

Native向web发送消息

其实原理上面已经说了:

Native 向 Web 发送消息基本原理上是在 WebView 容器中动态地执行一段 JS 脚本,通常情况下是调用一个挂载在全局上下文的方法。
具体来说 —— Native 端可以直接调用挂载在 window 上的全局方法并传入相应的函数执行参数,并且在函数执行结束后 Native 端可以直接拿到执行成功的返回值。

场景:页面上是一个web page,在其上有一个原生控件 Button 和 Input。在输入框中输入一段代码更改 web 页面上某个标签的 innerhtml

<EditText
    android:id="@+id/editText_name"
    android:layout_width="edit_parent"
    android:layout_height="edit_content"
    android:hint="请输入你想要执行的js代码" />

<Button
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="Native向Web发送消息" />
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import cn.sunday.hybridappdemo.views.X5WebView;
 
public class MainActivity extends AppCompatActivity 
	private X5WebView mWebView;
	
    private Button button;
    private EditText editText_name;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        button = (Button) findViewById(R.id.Button_quedin);
        this.editText_name = (EditText) findViewById(R.id.editText_name);
        
        button.setOnClickListener(new View.OnClickListener() //注册监听
            @Override //监听点击事件
            public void onClick(View v) 
                String name = editText_name.getText().toString();
                mWebView.evaluateJavascript("javascript:onShowPageNative(" + name + ")");
            
        )
    )

window.onShowPageNative = function(str) 
	new Function(str);

将文本框输入的字符视为 JS 字符串并调用相关 API 直接执行

你是否想到了著名的用于解决跨域问题的jsonp?其实很多地方都使用了“将代码作为字符串传递再执行”的原理。最出名的就是微信小程序的(双线程)混合架构模型中view层向逻辑层通信。

Web 向 Native 发送消息

Web 向 Native 发送消息本质上就是某段 JS 代码的执行端上是可感知的,目前业界主流的实现方案有两种,分别是拦截式和注入式。

拦截式

和浏览器类似 WebView 中发出的所有请求都是可以被 Native 容器感知到的,因此拦截式具体指的是 Native 拦截 Web 发出的 URL 请求,双方在此之前约定一个 JSB 请求格式,如果该请求是 JSB 则进行相应的处理,若不是则直接转发。

Native 拦截请求的钩子方法:

  • Android:shouldOverrideUrlLoading
  • IOS:
    • 8+:decidePolicyForNavigationAction
    • 8-:shouldStartLoadWithRequest

拦截式的流程存在几个问题:

通过何种方式发出请求?
Web 端发出请求的方式非常多样,例如 <a>iframe.srclocation.href、ajax 等,但 <a> 需要用户手动触发,location.href 可能会导致页面跳转,安卓端拦截 ajax 的能力有所欠缺,因此绝大多数拦截式实现方案均采用 iframe 来发送请求。

如何规定 JSB 的请求格式?
一个标准的 URL 由 <scheme>://<host>:<port><path> 组成,相信大家都有过从微信或手机浏览器点击某个链接意外跳转到其他 App 的经历,如果有仔细留意过这些链接的 URL 你会发现目前主流 App 都有其专属的一个 scheme来作为该应用的标识,例如微信的 URL scheme 就是 weixin://。JSB 的实现借鉴这一思路,定制业务自身专属的一个 URL scheme 来作为 JSB 请求的标识,例如字节内部的 bytedance://

// Web 通过动态创建 iframe,将 src 设置为符合双端规范的 url scheme
const CUSTOM_PROTOCOL_SCHEME = 'vdian'

function web2Native(event)     
    const messagingIframe = document.createElement('iframe');
    messagingIframe.style.display = 'none';
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + event;
    document.documentElement.appendChild(messagingIframe);

    setTimeout(() => 
        document.documentElement.removeChild(messagingIframe);
    , 200)

拦截式在双端都具有非常好的向下兼容性,曾经是最主流的 JSB 实现方案,但目前在高版本的系统中已经逐渐被淘汰,理由是它有如下几个劣势:

  • 连续发送时可能会造成消息丢失(可以使用消息队列解决该问题)
  • URL 字符串长度有限制
  • 性能一般,URL request 创建请求有一定的耗时(Android 端 200-400ms)

注入式

注入式的原理是通过 WebView 提供的接口向 JS 全局上下文对象(window)中注入对象或者方法,当 JS 调用时,可直接执行相应的 Native 代码逻辑,从而达到 Web 调用 Native 的目的。
—— 也就是上面说的“web 端调用 Native 端代码”。

这种方法简单而直观,并且不存在参数长度限制和性能瓶颈等问题,目前主流的 JSB SDK 都将注入式方案作为优先使用的对象。

以上是关于JSBridge:混合开发中的双向通信[AndroidiOS&JavaScript]的主要内容,如果未能解决你的问题,请参考以下文章

JSBridge:混合开发中的双向通信[AndroidiOS&JavaScript]

一文搞懂jsBridge的运行机制

一文搞懂jsBridge的运行机制

Android-混合开发JSBridge-基础篇

h5与app混合开发,jsbridge

Android中实现Native与H5的通信方案汇总