JSBridge定义
  1. 定义:正如其命名一样,JSBridge就相当于jsnative之间进行全双工通信的一座桥梁,其内部定义了一套用于jsnative进行通信的规范(包括协议、方法、传参及回调等);
  2. 用途:JSBridge可以桥连jsnative的通信,从而使基于容器的web开发和优化成为可能,如比较火的hybrid app技术;能够提升页面性能,丰富页面功能等;
JSBridge原理简析6+
  1. 框架简析:
    JSBridge框架其实主要由两部分组成:第一部分是Native调用js,主要用于消息推送、状态同步及回溯调用结果等;第二部分是js调用Native,主要用于调用系统api、事件监听及状态同步等;

  2. Native调用js

    1. Android
      Android有两种方式:4.4.0以前使用方法loadUrl——调用方便,无法获取回调结果,会刷新webview;4.4.0+使用方法evaluateScript提供更加高效完善的功能——可以获取返回值并且不刷新webview;
      需要注意的是,这里可调用的方法是全局方法;
      # loadUrl
      mWebView.loadUrl("javascript: methodName(paramStr)");
      
      # evaluateScript
      mWebView.evaluateScript("javascript: method(paramStr)", new ValueCallback<String>() {
      	@override
      	public void onReceiveValue() {
      		// do something after receive js callback value
      	}
      }
      
    2. IOS
      # UIWebView
      mWebView.stringByEvaluatingJavaScriptFromString("methodName(paramStr)");
      
      #WKWebView
      mWebView.evaluateScript("methodName(paramStr)");
      
  3. js调用Native

    1. url schema拦截
      h5native约定一套通信协议作为通信基础,一般如下:
      schema://methodName?params=xxx&cb=xxx;
      其中schema为双方协商的协议名,methodName为js调用native的方法名,params为参数集字符串,cb为接收回调结果的js方法名;在h5中发起请求时,一般通过构建一个不可见的iframe发起请求;请求以约定的方式以url形式发送,native会拦截h5的所有请求(如进行长连接优化等),如果发现url中的协议名是约定的协议名(如jsbridge),则会解析其中的methodNameparamscb等信息。如下给出了简单实现:

      window.callId = 0;
      const callNative = (method, params = null, cb) => {
      	const paramsObj = {
      		data: params ? JSON.stringify(params) : null,
      	}
      	if (typeof cb === 'function') {
      		const cbName = cb + window.callId++;
      		window[cbName] = cb;
      		paramsObj['cbName'] =  cbName;
      	}
      	// 设定通信url供native拦截
      	const url = `jsbridge://${method}?${JSON.stringify(paramsObj)}`;
      	
      	const iframe = document.createElement('iframe');
      	iframe.src = url;
      	iframe.style.width = 0;
      	iframe.style.height = 0;
      	iframe.frameborder = 0;
      	iframe.style.display = 'none';
      	document.body.appendChild(iframe);
      	
      	setTimeout(() => {
      		iframe.parentNode.removeChild(iframe);
      	}, 100);
      }
      

      缺点:消息传输通过url传输,因此传输数据长度受到限制;

      在这里插入图片描述

    2. promptalertconfirm拦截
      一般通过prompt进行通信,其他实现与url schema拦截类似;native收到prompt事件后会通过onJsPrompt等类似事件对prompt做处理,从而获取js传入的methodparamscb等;

      function callNative(method, params, cb) {
      	...
      	const url = `jsbridge://${method}?${JSON.stringify(paramsObj)}`;
      
      	prompt(url);
      }
      

      缺点:iosUIWebkit不支持;

    3. 注入JS上下文
      这种方法一般是将需要供js调用的native方法通过实例对象的方式通过webview提供的方法注入到js全局上下文,这样位于webview内的h5页面中js可以直接调用native的实例方法;
      注入方式:Android的webview通过addJ avascriptInterfaceios UIWebview通过JSContextios WKWebview通过scriptMessageHandler
      下面是来自文献2的Android客户端关于注入JS上下文的简单demo;

      首先,声明一个NativeMethods的类,用于定义对外暴露给js调用的方法,格式如methodName(webview, args, cb)
      然后定义一个JSBridge类:其中定义了两个静态方法和一个实例方法;register用于将nativeMethods类下的方法转为hashMap格式,便于查询;call是暴露给js的供js统一调用的方法;
      最后在webview创建后注册对外方法并将JSBridge实例通过addJavascriptInterface注入到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) {
              ...
              if (!exposeMethods.containsKey(exposeName)) {
      	        exposeMethods.put(exposeName, getAllMethod(classz));
      	    }
          }
      
          // 实例方法,用于提供给 js 统一调用的方法
          @JavascriptInterface
          public String call(String methodName, String args) {
              ...
          }
      }
      
      

      根据上述代码可以看到注入到js全局对象中的实例对象为_jsBridge;在h5中的调用如下:

      window.callId = 0;
      const callNative = (method, params = null, cb) => {
      	const paramsObj = {
      		data: params ? JSON.stringify(params) : null,
      	}
      	if (typeof cb === 'function') {
      		const cbName = cb + window.callId++;
      		window[cbName] = cb;
      		paramsObj['cbName'] =  cbName;
      	}
      	if (window._jsBridge) {
      		window._jsBridge.call(method, JSON.stringify(paramsObj));
      	} else {
      		// 兜底方案
      		const url = `jsbridge://${method}?${JSON.stringify(paramsObj)}`;
      		prompt(url);
      	}
      
      

      缺点:安卓4.2以下存在安全漏洞,可能会导致用户信息泄露;

      JSBridge静态方法的call方法实现:

      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;
      }
      

      除此之外,js调用native方法成功需要给h5响应的结果反馈,因此native端需要定义一个Callback类用于处理js调用成功的结果反馈;其本质还是native调用js方法,以下是安卓端使用evaluatecript方法实现的Callback类:

      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的引用
  1. h5引用
    h5引用即使用对应的封装好的JSBridgenpm包,内部封装了js调用native方法的方法集;该方式能够保证调用时JSBridge一定存在;缺点是当有变更时,需要nativeh5同时做变更进行兼容;
  2. native注入
    即上边提到的将JSBridge实例对象注入到js全局上下文;该方式有利于保障API与Native的一致性,但是由于注入方法和时机受到限制,h5调用时总是要判断JSBridge实例对象是否存在;
参考文献
  1. JSBridge 深度剖析
  2. 从零开始写一个 JSBridge
Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐