Stories

Detail Return Return

JSBridge 實現原理及開發實踐 - Stories Detail

JavaScript是運行在一個單獨的 JS Context中(例如: webview的webkit引擎,JSCore)
本位主要總結下 JSBridge 前端實現原理,來自工作中的總結,安卓/ios代碼僅為示意

JSBridge 是廣為流行的Hybrid 開發中JS和Native一種通信方式,簡單的説,JSBridge就是定義Native和JS的通信,Native只通過一個固定的橋對象調用JS,JS也只通過固定的橋對象調用native,

  • jsBridge 兩種交互方式 注入apiurl schema重寫h5全局方法

注入api的形式

安卓操作方法
  • native調用js

    // 安卓4.4版本之前,無法獲取返回值
    // mWebView = new WebView(this); // 即當前webview對象
    mWebView.loadUrl("javascript: 方法名('參數,需要轉為字符串')")
    
    // 安卓4.4及以後
    //  webView.evaluateJavascript("javascript:if(window.callJS){window.callJS('" + str + "');}", new ValueCallback<String>() {
    mWebView.evaluateJavascript("javascript: 方法名,參數需要轉換為字符串", new ValueCallback() {
        @Override
        public void onReceiveValue(String value) {
        // 這裏的value即為對應JS方法的返回值
        }
    })
    
    // js 在全局window上聲明一個函數供安卓調用
    window.callAndroid = function() {
        console.log('來自中h5的方法,供native調用')
        return "來自h5的返回值"
    }
    
    /** 總結:
      1. 4.4 之前Native通過loadUrl來調用js方法,只能讓某個js方法執行,但是無法獲取該方法的返回值
      2. 4.4 之後,通過evaluateJavaScript異步調用js方法,並且能在onReceive中拿到返回值
      3. 不適合傳輸大量數據
      4. mWebView.loadUrl("javascript: 方法名") 函數需在UI線程運行,因為mWebView為UI控件,會阻塞UI線程
    */
  • JS調用Native

    // 安卓環境配置
    WebSettings webSettings = mWebView.getSettings();
    // Android容器允許js腳本,必須要
    webSettings.setJavaScriptEnabled(true);
    // Android 容器設置僑連對象
    mWebView.addJavascriptInterface(getJSBridge(), "JSBridge");
    
    // Android中JSBridge的業務代碼
    private Object getJSBridge() {
        Object insterObj = new Object() {
            @JavascriptInterface
            public String foo() {
                // 此處執行 foo  bridge的業務代碼
                return "foo" // 返回值
            }
            @JavascriptInterface
            public String foo2(final String param) {
                // 此處執行 foo2 方法  bridge的業務代碼
                return "foo2" + param;
            }
        }
        return inserObj;
    }
    // js調用原生的代碼
    // JSBridge 通過addJavascriptInterface已被注入到 window 對象上了
    window.JSBridge.foo(); // 返回 'foo'
    window.JSBridge.foo2(); // 返回 'foo2:test'
    // 注意:在安卓4.2之前 addJavascriptInterface有風險,hacker可以通過反編譯獲取Native註冊的Js對象,然後在頁面通過反射Java的內置 靜態類,獲取一些敏感的信息和破壞
ios 操作方法
  • js 調用native

      // 注意:ios7 以前 js無法調用native方法,ios7之後可以引入第三方提供的 JavaScriptCore 庫
      /*
          總結:
          1. ios7 才出現這種方式,在這之前js無法直接調用Native,只能通過JSBridge方式調用
          2. JS 能調用到已經暴露的api,並且能得到相應返回值
          3. ios原生本身是無法被js調用的,但是通過引入官方提供的第三方“JavaScriptCore”,即可開發api給JS調用
      */
      // WKWebview  ios8之後才出現,js調用native方法
      // ios 代碼配置 https://zhuanlan.zhihu.com/p/32899522
      // js調用
      window.webkit.messageHandlers.{name}.postMessage(msgObj);
    
      /*
          * 優缺點
          ios開發自帶兩種webview控件 UIWebview(ios8 以前的版本,建議棄用)版本較老,
          可使用JavaScriptCore來注入全局自定義對象
          佔用內存大,加載速度慢
          WKWebview 版本較新 加載速度快,佔用內存小
      */
  • native 調用 js

        // UIWebview
        [webView stringByEvaluatingJavaScriptFromString:@"方法名(參數);"];
        // WKWebview
        [_customWebView evaluateJavaScript:[@"方法名(參數)"] completionHandler:nil];
        --------------------
        // js 調用 native
        // 引用官方庫文件 UIWebview(ios8 以前的版本,建議棄用)
        #import <JavaScriptCore/JavaScriptCore.h>
        // webview 加載完畢後設置一些js接口
        -(void)webViewDidFinishLoad:(UIWebView *)webView{
            [self hideProgress];
            [self setJSInterface];
        }
        
        -(void)setJSInterface{ 
            JSContext *context =[_wv valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
            // 註冊名為foo的api方法
            context[@"foo"] = ^() {
            //獲取參數
                NSArray *args = [JSContext currentArguments];
                NSString *title = [NSString stringWithFormat:@"%@",[args objectAtIndex:0]];
                //做一些自己的邏輯 返回一個值  'foo:'+title
                return [NSString stringWithFormat:@"foo:%@", title];
            };
        }
        window.foo('test'); // 返回 'foo:test'

    url scheme 介紹

  • url scheme是一種類似於url的鏈接,是為了方便app直接互相調用設計的:具體為:可以用系統的 OpenURI 打開類似與url的鏈接(可拼入參數),然後系統會進行判斷,如果是系統的 url scheme,則打開系統應用,否則找看是否有app註冊中scheme,打開對應app,需要注意的是,這種scheme必須原生app註冊後才會生效,如微信的scheme為 weixin://
  • 調用過程(如用 iframe.src),然後native用某種方法捕獲對應的url觸發事件,然後拿到當前觸發url,根據定好的協議(scheme://method/?params=xxx),然後native攔截該請求分析當前觸發了哪種方法,然後根據定義來實現
  • 客户端捕獲url

    • 安卓捕獲 url scheme:shouldoverrideurlloading 捕獲到url進行分析
    • ios: 在 UIWebView WKWebview 內發起的所有網絡請求,都可以通過 delegate函數在native層得到通知,通過 shouldStartLoadWithRequest捕獲webview中觸發的url scheme
  • 大致流程:h5 --> 通過某種方式觸發一個url --> native捕獲到url,進行分析 -->原生做處理 --> 如果需要回調 native再調用h5的JSBridge對象傳遞迴調
  • 缺點:速度可能稍慢一點,url長度會有限制,需要定義url結構解析較為複雜
  • 相較於注入api形式有以下有優點:

    1. Android4.2 一下,addJavaScriptInterface方式有安全漏洞
    2. ios7以下,js無法調用native
    3. url scheme交互方式是一套現有的成熟方案,可以兼容各種版本

重寫prompt/alert等原生方法

  • native會劫持webviewonJsAlert、onJsConfirm、onConsoleMessage、onJsPrompt並進行重寫,好像ios高版本對此方式做了限制

設計實現一個JSBridge

  1. 設計出一個native與js交互的全局橋對象
  2. js如何調用native
  3. native如何調用js
  4. h5中api方法 send/register

// 名稱: JSBridge 掛在 window上的一個屬性
window.JSBridge = {
    // ...其他屬性,比如:版本、app基礎信息
    // 回調函數集合
    _cbMap: {},
    // js註冊方法,native主動發起調用
    register(method, callback) {
        const callbackId = method
        this._cbMap[callbackId] = callback
    },
    // h5 調用native方法,調用時會將回調 id 存放到本地變量cbList中
    send(method, data, callback) {
        let callbackId
        if (callback) {
            callbackId = `${method}_${Date.now()}`
            this._cbMap[callbackId] = callback
        }
        const params = {
            method, // 和客户端約定好定為method字段,即bridge名稱
            callback: callbackId,
            data: {} // 業務參數
        }
        // 調用native將參數傳遞進去 ==> 通信方式以上三種可任意選擇
        callNative(JSON.Stringify(params))
    },
    // native回調js的方法 obj: { 回調id, 回調數據 }
    // 相當於native只調用此方法,參數為json字符串
    handler(obj) {
        const { callbackId, data } = JSON.parse(obj)
        // 執行對應的回調函數即send傳進來的callback,如果要返回值,可再發一個send
        this._cbMap[callbackId] && this._cbMap[callbackId].(data)
    }
}

// 調用示例
// 主動發消息
JSBridge.send('ui.callNative', {}, (data) => data) 
// 註冊在本地,被動接受客户端調用
JSBridge.register("ui.datatabupdate", (data) => data);
JSBridge.send內部的callNative的具體實現
  • url schema方式

      // url schema 實現
      // 變成字符串並編碼
      var url = scheme://ecape(JSON.stringify(param))
    
      // 使用內部創建好的iframe來觸發scheme(location.href = 可能會造成跳轉問題)
      var iframe = document.createElment('iframe');
      iframe.src = url;
      document.head.appendChild(iframe);
      setTimeout(() => document.head.removeChild('iframe'), 200)
  • api注入方式

      // ios
      window.webkit.messageHandlers.{name}.postMessage(JSON.stringify(params))
    
      // 安卓
      window.{name}.{name}(JSON.stringify(params))

參考資料

  1. https://www.cnblogs.com/dailc...
  2. https://zhuanlan.zhihu.com/p/... ,JSBridgeDemo
  3. https://github.com/chemdemo/c...
  4. 安卓與js交互
  5. ZOO TTEAM JSBridge

Add a new Comments

Some HTML is okay.