关于前端:JSBridge-实现原理解析

30次阅读

共计 9411 个字符,预计需要花费 24 分钟才能阅读完成。

文章首发于我的博客 https://github.com/mcuking/bl…

相干代码请查阅 https://github.com/mcuking/JS…

JSBridge 我的项目以 js 与 android 通信为例,解说 JSBridge 实现原理,上面提到的办法在 iOS(UIWebview 或 WKWebview)均有对应办法。

1. native to js

两种 native 调用 js 办法,留神被调用的办法须要在 JS 全局上下文上

loadUrl

evaluateJavascript

1.1 loadUrl

mWebview.loadUrl("javascript: func()");

1.2 evaluateJavascript

mWebview.evaluateJavascript("javascript: func()", new ValueCallback<String>() {
    @Override
    public void onReceiveValue(String value) {return;}
});

上述两种 native 调用 js 的形式比照如下表:

形式 长处 毛病
loadUrl 兼容性好 1. 会刷新页面 2. 无奈获取 js 办法执行后果
evaluateJavascript 1. 性能好 2. 可获取 js 执行后的返回值 仅在安卓 4.4 以上可用

2. js to native

三种 js 调用 native 办法

拦挡 Url Schema(假申请)

拦挡 prompt alert confirm

注入 JS 上下文

2.1 拦挡 Url Schema

即由 h5 收回一条新的跳转申请,native 通过拦挡 URL 获取 h5 传过来的数据。

跳转的目的地是一个非法不存在的 URL 地址,例如:

"jsbridge://methodName?{"data": arg,"cbName": cbName}"

具体示例如下:

"jsbridge://openScan?{"data": {"scanType":"qrCode"},"cbName":"handleScanResult"}"

h5 和 native 约定一个通信协议,例如 jsbridge, 同时约定调用 native 的办法名 methodName 作为域名,以及前面带上调用该办法的参数 arg,和接管该办法执行后果的 js 办法名 cbName。

具体能够在 js 端封装相干办法,供业务端对立调用,代码如下:

window.callbackId = 0;

function callNative(methodName, arg, cb) {
    const args = {data: arg === undefined ? null : JSON.stringify(arg),
    };

    if (typeof cb === 'function') {
      const cbName = 'CALLBACK' + window.callbackId++;
      window[cbName] = cb;
      args['cbName'] = cbName;
    }

    const url = 'jsbridge://' + methodName + '?' + JSON.stringify(args);

    ...
}

以上封装中较为奇妙的是将用于接管 native 执行后果的 js 回调办法 cb 挂载到 window 上,并为避免命名抵触,通过全局的 callbackId 来辨别,而后将该回调函数在 window 上的名字放在参数中传给 native 端。native 拿到 cbName 后,执行完办法后,将执行后果通过 native 调用 js 的形式(下面提到的两种办法),调用 cb 传给 h5 端(例如将扫描后果传给 h5)。

至于如何在 h5 中发动申请,能够设置 window.location.href 或者创立一个新的 iframe 进行跳转。

function callNative(methodName, arg, cb) {
    ...

    const url = 'jsbridge://' + method + '?' + JSON.stringify(args);

    // 通过 location.href 跳转
    window.location.href = url;

    // 通过创立新的 iframe 跳转
    const iframe = document.createElement('iframe');
    iframe.src = url;
    iframe.style.width = 0;
    iframe.style.height = 0;
    document.body.appendChild(iframe);

    window.setTimeout(function() {document.body.removeChild(iframe);
    }, 800);
}

native 会拦挡 h5 收回的申请,当检测到协定为 jsbridge 而非一般的 http/https/file 等协定时,会拦挡该申请,解析出 URL 中的 methodName、arg、cbName,执行该办法并调用 js 回调函数。

上面以安卓为例,通过笼罩 WebViewClient 类的 shouldOverrideUrlLoading 办法进行拦挡,android 端具体封装会在上面独自的板块进行阐明。

import android.util.Log;
import android.webkit.WebView;
import android.webkit.WebViewClient;

public class JSBridgeViewClient extends WebViewClient {
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {JSBridge.call(view, url);
        return true;
    }
}

拦挡 URL Schema 的问题

  • 间断发送时音讯失落

如下代码:

window.location.href = "jsbridge://callNativeNslog?{"data":"111","cbName":""}";
window.location.href = "jsbridge://callNativeNslog?{"data":"222","cbName":""}";

js 此时的诉求是在同一个运行逻辑内,疾速的间断发送出 2 个通信申请,用客户端自身 IDE 的 log,按程序打印 111,222,那么理论后果是 222 的通信音讯基本收不到,间接会被零碎摈弃丢掉。

起因:因为 h5 的申请归根结底是一种模仿跳转,跳转这件事件上 webview 会有限度,当 h5 间断发送多条跳转的时候,webview 会间接过滤掉后发的跳转申请,因而第二个音讯基本收不到,想要收到怎么办?js 里将第二条音讯延时一下。

// 发第一条音讯
location.href = "jsbridge://callNativeNslog?{"data":"111","cbName":""}";

// 延时发送第二条音讯
setTimeout(500,function(){location.href = "jsbridge://callNativeNslog?{"data":"222","cbName":""}";
});

但这并不能保障此时是否有其余中央通过这种形式进行申请,为零碎解决此问题,js 端能够封装一层队列,所有 js 代码调用音讯都先进入队列并不立即发送,而后 h5 会周期性比方 500 毫秒,清空一次队列,保障在很快的工夫内相对不会间断发 2 次申请通信。

  • URL 长度限度

如果须要传输的数据较长,例如办法参数很多时,因为 URL 长度限度,仍以失落局部数据。

2.2 拦挡 prompt alert confirm

即由 h5 发动 alert confirm prompt,native 通过拦挡 prompt 等获取 h5 传过来的数据。

因为 alert confirm 比拟罕用,所以个别通过 prompt 进行通信。

约定的传输数据的组合形式以及 js 端封装办法的能够相似下面的 拦挡 URL Schema 提到的形式。

function callNative(methodName, arg, cb) {
    ...

    const url = 'jsbridge://' + method + '?' + JSON.stringify(args);

    prompt(url);
}

native 会拦挡 h5 收回的 prompt,当检测到协定为 jsbridge 而非一般的 http/https/file 等协定时,会拦挡该申请,解析出 URL 中的 methodName、arg、cbName,执行该办法并调用 js 回调函数。

上面以安卓为例,通过笼罩 WebChromeClient 类的 onJsPrompt 办法进行拦挡,android 端具体封装会在上面独自的板块进行阐明。

import android.webkit.JsPromptResult;
import android.webkit.WebChromeClient;
import android.webkit.WebView;

public class JSBridgeChromeClient extends WebChromeClient {
    @Override
    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {result.confirm(JSBridge.call(view, message));
        return true;
    }
}

这种形式没有太大毛病,也不存在间断发送时信息失落。不过 iOS 的 UIWebView 不反对该形式(WKWebView 反对)。

2.3 注入 JS 上下文

即由 native 将实例对象通过 webview 提供的办法注入到 js 全局上下文,js 能够通过调用 native 的实例办法来进行通信。

具体有安卓 webview 的 addJavascriptInterface,iOS UIWebview 的 JSContext,iOS WKWebview 的 scriptMessageHandler。

上面以安卓 webview 的 addJavascriptInterface 为例进行解说。

首先 native 端注入实例对象到 js 全局上下文,代码大抵如下,具体封装会在上面的独自板块进行解说:

public class MainActivity extends AppCompatActivity {

    private WebView mWebView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mWebView = (WebView) findViewById(R.id.mWebView);

        ...

        // 将 NativeMethods 类上面的提供给 js 的办法转换成 hashMap
        JSBridge.register("JSBridge", NativeMethods.class);

        // 将 JSBridge 的实例对象注入到 js 全局上下文中,名字为 _jsbridge,该实例对象下有 call 办法
        mWebView.addJavascriptInterface(new JSBridge(mWebView), "_jsbridge");
    }
}

public class NativeMethods {
    // 用来供 js 调用的办法
    public static void methodName(WebView view, JSONObject arg, CallBack callBack) {}}

public class JSBridge {
    private WebView mWebView;

    public JSBridge(WebView webView) {this.mWebView = webView;}


    private  static Map<String, HashMap<String, Method>> exposeMethods = new HashMap<>();

    // 静态方法,用于将传入的第二个参数的类上面用于提供给 javacript 的接口转成 Map,名字为第一个参数
    public static void register(String exposeName, Class<?> classz) {...}

    // 实例办法,用于提供给 js 对立调用的办法
    @JavascriptInterface
    public String call(String methodName, String args) {...}
}

而后 h5 端能够在 js 调用 window._jsbridge 实例上面的 call 办法,传入的数据组合形式能够相似下面两种形式。具体代码如下:

window.callbackId = 0;

function callNative(method, arg, cb) {
  let args = {data: arg === undefined ? null : JSON.stringify(arg)
  };

  if (typeof cb === 'function') {
    const cbName = 'CALLBACK' + window.callbackId++;
    window[cbName] = cb;
    args['cbName'] = cbName;
  }

  if (window._jsbridge) {window._jsbridge.call(method, JSON.stringify(args));
  }
}

注入 JS 上下文的问题

以安卓 webview 的 addJavascriptInterface 为例,在安卓 4.2 版本之前,js 能够利用 java 的反射 Reflection API,获得结构该实例对象的类的內部信息,并能间接操作该对象的外部属性及办法,这种形式会造成安全隐患,例如如果加载了内部网页,该网页的歹意 js 脚本能够获取手机的存储卡上的信息。

在安卓 4.2 版本后,能够通过在提供给 js 调用的 java 办法前加装璜器 @JavascriptInterface,来表明仅该办法能够被 js 调用。

上述三种 js 调用 native 的形式比照如下表:

形式 长处 毛病
拦挡 Url Schema(假申请) 无安全漏洞 1. 间断发送时音讯失落 2. Url 长度限度,传输数据大小受限
拦挡 prompt alert confirm 无安全漏洞 iOS 的 UIWebView 不反对该形式
注入 JS 上下文 官网提供,不便简捷 在安卓 4.2 以下有安全漏洞

3. 安卓端 java 的封装

native 与 h5 交互局部的代码在下面曾经提到了,这里次要是讲述 native 端如何封装裸露给 h5 的办法。

首先独自封装一个类 NativeMethods,将供 h5 调用的办法以私有且静态方法的模式写入。如下:

public class NativeMethods {public static void showToast(WebView view, JSONObject arg, CallBack callBack) {...}
}

接下来思考如何在 NativeMethods 和 h5 之前建设一个桥梁,JSBridge 类因运而生。
JSBridge 类下次要有两个静态方法 register 和 call。其中 register 办法是用来将供 h5 调用的办法转化成 Map 模式,以便查问。而 call 办法次要是用接管 h5 端的调用,合成 h5 端传来的参数,查找并调用 Map 中的对应的 Native 办法。

JSBridge 类的静态方法 register

首先在 JSBridge 类下申明一个动态属性 exposeMethods,数据类型为 HashMap。而后申明静态方法 register,参数有字符串 exposeName 和类 classz,将 exposeName 和 classz 的所有静态方法 组合成一个 map,例如:

{
    jsbridge: {
        showToast: ...
        openScan: ...
    }
}

代码如下:

private  static Map<String, HashMap<String, Method>> exposeMethods = new HashMap<>();

public static void register(String exposeName, Class<?> classz) {if (!exposeMethods.containsKey(exposeName)) {exposeMethods.put(exposeName, getAllMethod(classz));
    }
}

由上可知咱们须要定义一个 getAllMethod 办法用来将类里的办法转化为 HashMap 数据格式。在该办法里同样申明一个 HashMap,并将满足条件的办法转化成 Map,key 为办法名,value 为办法。

其中条件为 私有 public 动态 static 办法且第一个参数为 Webview 类的实例,第二个参数为 JSONObject 类的实例,第三个参数为 CallBack 类的实例。(CallBack 是自定义的类,前面会讲到)
代码如下:

private static HashMap<String, Method> getAllMethod(Class injectedCls) {HashMap<String, Method> methodHashMap = new HashMap<>();

    Method[] methods = injectedCls.getDeclaredMethods();

    for (Method method: methods) {if(method.getModifiers()!=(Modifier.PUBLIC | Modifier.STATIC) || method.getName()==null) {continue;}
        Class[] parameters = method.getParameterTypes();
        if (parameters!=null && parameters.length==3) {if (parameters[0] == WebView.class && parameters[1] == JSONObject.class && parameters[2] == CallBack.class) {methodHashMap.put(method.getName(), method);
            }
        }
    }

    return methodHashMap;
}

JSBridge 类的静态方法 call

因为注入 JS 上下文和两外两种,h5 端传过来的参数模式不同,所以解决参数的形式略有不同。
上面以拦挡 Prompt 的形式为例进行解说,在该形式中 call 接管的第一个参数为 webView,第二个参数是 arg,即 h5 端传过来的参数。还记得拦挡 Prompt 形式时 native 端和 h5 端约定的传输数据的形式么?

"jsbridge://openScan?{"data": {"scanType":"qrCode"},"cbName":"handleScanResult"}"

call 办法首先会判断字符串是否以 jsbridge 结尾(native 端和 h5 端之间约定的传输数据的协定名),而后该字符串转成 Uri 格局,而后获取其中的 host 名,即办法名,获取 query,即办法参数和 js 回调函数名组合的对象。最初查找 exposeMethods 的映射,找到对应的办法并执行该办法。

public static String call(WebView webView, String urlString) {if (!urlString.equals("") && urlString!=null && urlString.startsWith("jsbridge")) {Uri uri = Uri.parse(urlString);

        String methodName = uri.getHost();

        try {JSONObject args = new JSONObject(uri.getQuery());
            JSONObject arg = new JSONObject(args.getString("data"));
            String cbName = args.getString("cbName");


            if (exposeMethods.containsKey("JSBridge")) {HashMap<String, Method> methodHashMap = exposeMethods.get("JSBridge");

                if (methodHashMap!=null && methodHashMap.size()!=0 && methodHashMap.containsKey(methodName)) {Method method = methodHashMap.get(methodName);

                    if (method!=null) {method.invoke(null, webView, arg, new CallBack(webView, cbName));
                    }
                }
            }
        } catch (Exception e) {e.printStackTrace();
        }

    }
    return null;
}

CallBack 类

js 调用 native 办法胜利后,native 有必要返回给 js 一些反馈,例如接口是否调用胜利,或者 native 执行后的失去的数据(例如扫码)。所以 native 须要执行 js 回调函数。

执行 js 回调函数形式实质是 native 调用 h5 的 js 办法,形式仍旧是下面提到的两种形式 evaluateJavascript 和 loadUrl。简略来说能够间接将 js 的回调函数名传给对应的 native 办法,native 执行通过 evaluateJavascript 调用。

但为了对立封装调用回调的形式,咱们能够定义一个 CallBack 类,在其中定义一个名为 apply 的静态方法,该办法间接调用 js 回调。

留神:native 执行 js 办法须要在主线程上。

public class CallBack {
    private  String cbName;
    private WebView mWebView;

    public CallBack(WebView webView, String cbName) {
        this.cbName = cbName;
        this.mWebView = webView;
    }

    public void apply(JSONObject jsonObject) {if (mWebView!=null) {mWebView.post(() -> {mWebView.evaluateJavascript("javascript:" + cbName + "(" + jsonObject.toString() + ")", new ValueCallback<String>() {
                    @Override
                    public void onReceiveValue(String value) {return;}
                });
            });
        }
    }
}

到此为止 JSBridge 的大抵原理都讲完了。但性能仍可再加欠缺,例如:

native 执行 js 办法时,可承受 js 办法中异步返回的数据,比方在 js 办法中申请某个接口在返回数据。间接调用 webview 提供的 evaluateJavascript,在第二个参数的类 ValueCallback 的实例办法 onReceiveValue 并不能接管到 js 异步返回的数据。

前面有空 native 调用 js 形式会持续欠缺的,最初以一句古语互勉:

路漫漫其修远兮 吾将上下而求索

正文完
 0