Published on

Hybrid开发-JSBridge原理

Authors
  • avatar
    Name
    noodles
    每个人的花期不同,不必在乎别人比你提前拥有
Hybrid混合开发相对于单一的客户端开发有着开发周期短,迭代快的优势,但是Hybrid模式开发的页面存在着一定的缺陷,比如性能问题、缺乏客户端能力等。通过JSBridge这个桥梁可以实现客户端能力的打通,赋予了Hybrid应用更强的端能力。 JS
JSBridge作为客户端和H5的通信的桥梁,可以承接如下的能力:
  • 鉴权能力 JSBridge调用能力鉴权,白名单,黑名单等
  • 胶水能力 JSBridge兼容代码,做版本控制等调用透明
  • 测试能力 提供测试方法,方便测试
  • Scope(配置)能力 能基于配置产出精简版、目标版本JSBridge

下面以Android代码为例,介绍JSBridge的实现方式。

Js调用Native

Js调用Native通常有如下的方案:

  • 拦截请求(shouldOverrideUrlLoading/shouldInterceptRequest)
  • 拦截特定方法(prompt/alert/confirm)
  • 客户端注入JSBridge(addJavascriptInterface)

拦截请求

在安卓初始化Wevview的时候可以设定WebViewClient,WebViewClient主要功能是处理Webview加载时的通知和请求事件等。通过重写WebViewClient的shouldOverrideUrlLoading/shouldInterceptRequest就可以实现拦截h5的请求从而实现端能力调用。 实现思路如下:

  • 定义JSBridge实现Jsb方法
  • 定义JSBManager管理Jsb的调用
  • 实现拦截方法的重写
  • H5侧调用

定义JSBridge方法类

    // 以下例子均省略import语句 
    public class JSBridge {
      // 需要考虑callback和入参一致性问题
      public void showToast(JSONObject jsonObject) {
          try {
              Toast.makeText(MainActivity.context, jsonObject.getString("content"), Toast.LENGTH_LONG).show();
          } catch(Exception e) {
          }
      }
    }

定义JSBManager管理Jsb的调用

    public class JsbManager {
      // 通过HashMap获取JSBridge定义的所有方法
      public static Map<String, Method> methodMap = new HashMap<>();
      public void init() {
          Method[] methods = JSBridge.class.getDeclaredMethods();
          for(Method method : methods) {
              methodMap.put(method.getName(), method);
          }
      }
    }

实现拦截方法的重写

以下以shouldOverrideUrlLoading方法的重写为例子。在例子中定义的通信协议是myjsb://method?params。通过在拦截方法中对请求进行解析就可以实现调用对应客户端method的逻辑。

    public class CustomWebViewClient extends WebViewClient {
        private JsbManager jsbManager = new JsbManager();
        private JSBridge jsBridge = new JSBridge();
        public void initJsb() {
            // 初始jsbManager和jsBridge实例
            jsbManager.init();
            jsBridge = new JSBridge();
        }
        @Override
        public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
            // 处理jsb 协议情况  只拦截jsb协议的url 其他放行
            Uri uri = request.getUrl();
            String scheme = uri.getScheme();
            if(scheme.equals(new String("myjsb"))) {
                // 获取方法名 入参
                String methodName = uri.getAuthority();
                String query = uri.getQuery();
                try {
                    JSONObject jsonObject = new JSONObject(query);
                    Method method = jsbManager.methodMap.get(methodName);
                    // 调用对应的客户端逻辑
                    method.invoke(jsBridge,jsonObject);
                } catch(Exception e) {
                    e.printStackTrace();
                }
            }
            return super.shouldOverrideUrlLoading(view, request);
        }
    }
    // 主活动代码逻辑
    public class MainActivity extends AppCompatActivity {
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            // 创建WebViewClient
            CustomWebViewClient webViewClient = new CustomWebViewClient();
            // 调用JSBridge初始逻辑
            webViewClient.initJsb();
            WebView webView = (WebView) findViewById(R.id.webView);
            // 设置WebViewClient处理webviewt通知,请求等
            webView.setWebViewClient(webViewClient);
            // 开启调试功能
            webView.setWebContentsDebuggingEnabled(true);
            WebSettings webSettings = webView.getSettings();
            // 允许执行JS
            webSettings.setJavaScriptEnabled(true);
            // 这里加载项目本地的html文件方便调试
            webView.loadUrl("file:///android_asset/index.html");
        }
    }

H5侧调用


        <body>
            <div>this page test JSB</div>
            <script>
              // 通过创建iframe发起JSBridge调用
              function iframeCall(url) {
                let iframe = document.createElement('iframe')
                iframe.src = url
                iframe.style.display = 'none'
                document.documentElement.appendChild(iframe)
                setTimeout(() => { document.documentElement.removeChild(iframe) })
              }
              function callJsb(method, params) {
                let url = `myjsb://`
                if(!method) {
                  return
                }
                url += `${method}`
                if(!!params) {
                  url += `?${encodeURIComponent(JSON.stringify(params))}`
                }
                iframeCall(url)
              }
              callJsb('showToast', { content: 'xiaohong' })
            </script>
        </body>
拦截请求实现调用

使用iframe发送消息的方式会存在消息丢失,参数限制等问题,可以通过消息队列和拦截shouldInterceptRequest方法来实现。

拦截特定方法

在初始化WebView的时候可以同步设置WebChromeClient,WebChromeClient主要是辅助WebView处理Js对话框,标题等操作,通过拦截WebChromeClient相应的方法同样可以实现调用端能力。

实现WebChromeClient

    public class CustomWebChromeClient extends WebChromeClient {
        @Override
        public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
            // 此处举例为主 直接弹端toast
            // 实现上跟拦截url一致
            Log.d("mesage", message.startsWith("myjsb")+ "");
            if(message.startsWith("myjsb")) {
                Toast.makeText(MainActivity.context, "PropmtCall", Toast.LENGTH_LONG).show();
                // 此时js调起了 需要JsPromptResult.confirm(result)
                return true;
            } else {
                return super.onJsPrompt(view, url, message, defaultValue, result);
            }
        }
    }
    // 在初始化WebView的时候设置WebChromeClient
    CustomWebChromeClient webChromeClient = new CustomWebChromeClient();
    webView.setWebChromeClient(webChromeClient);

H5调用


        window.prompt('myjsb://')
重写Prompt方法调用

客户端注入JSBridge

通过addJavascriptInterface可以在初始化WebView的时候将客户端的调用逻辑暴露给H5。

实现JSInterface

        public class JsInterface {
            private Context context;
            public JsInterface(Context context) {
                this.context = context;
            }
            // JsInterface需要用@JavascriptInterface注解才可以被调用
            @JavascriptInterface
            public void showToast(String content) {
                Toast.makeText(this.context, content, Toast.LENGTH_LONG).show();
            }
        }

        // 在初始WebView的时候注入interface
        webView.addJavascriptInterface(new JsInterface(context), "myjsb");

H5调用

        window.myjsb.showToast("Interface")  
interface调用

Native调用Js

Nativa调用Js通常有如下的方案:

以下例子在H5中都定义了全局函数供Native调用

        function testNativeCall() {
          console.log("nativeCallJs")
          return 'nativeCallJs'
        }

loadUrl

可以通过webView.loadUrl("javascript: testNativeCall()")发起调用(需要等待Js执行完成)。loadUrl的方式会刷新页面且无法获取js的回调。

evaluateJavascript

    webView.evaluateJavascript("javascript: testNativeCall()", new ValueCallback<String>() {
        @Override
        public void onReceiveValue(String value) {
            return;
        }
    });
evaluate调用js

参考

小白必看,JSBridge 初探 跨端技能必备之JSBridge 从零开始写一个 JSBridge