jeremygo

jeremygo

我是把下一颗珍珠串在绳子上的人

JSBridge 原理

介紹#

如今移動端盛行的年代,技術選型基本都是混合開發(Hybrid),即結合原生 Native 與 Web H5 兩者的優點同時使用這兩類技術:

  • 原生技術指 iOS(Objective C、Swift)、Android(Java):開發效率較低,發布依賴用戶更新,但性能更高且功能覆蓋率更高
  • Web 技術指 JavaScript:開發效率更高,發布更新靈活,但性能較低且功能特性也受限

這種模式下,H5 經常需要使用 Native 的功能,比如打開攝像頭、查看本地相冊、圖片上傳、頁面分享等等,Native 也需要向 Web 端推送更新狀態等。
而 JavaScript 是運行在單獨的 JS Context(Webview 容器、JSCore 等),與原生運行環境有隔離,需要有一種機制實現 Native 和 Web 端的雙向通信,這種機制就是 JSBridge:

JavaScript 引擎或 Webview 容器作為媒介,通過協定協議進行通信,實現 Native 和 Web 雙向通信的一種機制。

通過 JSBridge,Web 端可以調用 Native 端的 Java 接口,Native 端也可以調用 Web 端的 JS 接口,實現雙向調用。

通信原理#

JS 調用 Native#

JavaScript 調用 Native 主要有三種方式:注入 API攔截 URL SCHEME重寫 prompt 等 JS 全局方法

注入 API#

主要原理是:通過 Webview 提供的接口,向 JS 的運行環境(window)中注入對象或方法,讓 JS 調用時直接執行相應的 Native 代碼邏輯

iOS UIWebView 實現(主要是 JavaScriptCore

JSContext *context = [uiWebView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

context[@"postBridgeMessage"] = ^(NSArray<NSArray *> *calls) {
    // Native 邏輯
};

前端調用

window.postBridgeMessage(message)

iOS WKWebView 實現(主要是 WKScriptMessageHandler

@interface WKWebVIewVC ()<WKScriptMessageHandler>

@implementation WKWebVIewVC

- (void)viewDidLoad {
    [super viewDidLoad];

    WKWebViewConfiguration* configuration = [[WKWebViewConfiguration alloc] init];
    configuration.userContentController = [[WKUserContentController alloc] init];
    WKUserContentController *userCC = configuration.userContentController;
    // 注入對象,前端調用其方法時,Native 可以捕獲到
    [userCC addScriptMessageHandler:self name:@"nativeBridge"];

    WKWebView wkWebView = [[WKWebView alloc] initWithFrame:self.view.frame configuration:configuration];
}

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    if ([message.name isEqualToString:@"nativeBridge"]) {
        NSLog(@"前端傳遞的數據 %@: ",message.body);
        // Native 邏輯
    }
}

前端調用

window.webkit.messageHandlers.nativeBridge.postMessage(message);

Android 實現

/**
 在 4.2 之前,Android 注入 JavaScript 對象的接口是 addJavascriptInterface,
 但是這個接口有漏洞,可以被不法分子利用,危害用戶的安全,
 因此在 4.2 中引入新的接口 @JavascriptInterface 來替代這個接口,解決安全問題。
 所以 Android 注入對對象的方式是 有兼容性問題的
*/  
publicclassJavaScriptInterfaceDemoActivityextendsActivity{
private WebView Wv;

    @Override
    publicvoidonCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);

        Wv = (WebView)findViewById(R.id.webView);     
        final JavaScriptInterface myJavaScriptInterface = new JavaScriptInterface(this);    	 

        Wv.getSettings().setJavaScriptEnabled(true);
        Wv.addJavascriptInterface(myJavaScriptInterface, "nativeBridge");
    }

    publicclassJavaScriptInterface{
         Context mContext;

         JavaScriptInterface(Context c) {
             mContext = c;
         }

         publicvoidpostMessage(String webMessage){	    	
             // Native 邏輯
         }
     }
}

前端調用

window.nativeBridge.postMessage(message);
攔截 URL Scheme#

URL Scheme 是一種類似 url 的鏈接,為了方便 App 直接相互調用設計,格式如下:
<protocol>://<host>/<path>?<qeury>#fragment
自定義的一個 JSBridge 通信的 URL Scheme:jsbridge://showToast?text=hello

主要原理是:Native 加載 Webview 後,Web 發送的所有請求都會經過 Webview 組件,所以 Native 可以重寫對應方法來攔截請求判斷符合自定義的 URL Scheme 格式則進行解析處理進而調用原生 Native 方法

Web 發送 URL 請求有以下幾種方式

  • a 標籤(缺點:需要用戶操作)
  • location.href(缺點:可能引起頁面跳轉丟失調用)
  • ajax 請求(Android 沒有相應攔截方法)
  • 使用 iframe src

以上方法中 iframe src 從早期開始就是經常使用的方案,兼容性很好:

  • 安卓提供了shouldOverrideUrlLoading 方法攔截
  • UIWebView 使用 shouldStartLoadWithRequestWKWebView 則使用decidePolicyForNavigationAction

不過也有一定的缺陷:

  • 基於 URL,長度有所限制
  • 創建請求有一定耗時,比起注入 API 的方式調用同樣功能耗時更長
重寫 prompt 等 JS 全局方法#

Webview 有一個方法 setWebChromeClient 可以設置 WebChromeClient 對象,其中有三個方法 onJsAlertonJsConfirmonJsPrompt,當 JS 調用 window.alertwindow.confirmwindow.prompt 時上面對應的方法也會被觸發,利用這個機制也能做一些處理
由於攔截上述方法會對性能造成一定影響,因此需要選擇使用頻率較低的方法,而在 Android 中,相比其它幾個方法,幾乎不會使用到 prompt 方法,因此佔用 prompt 是最佳方案。

Native 調用 JS#

Native 端調用 JS 端,這個比較簡單,JavaScript 作為解釋性語言,最大的特性就是可以隨時隨地地通過解釋器執行一段 JS 代碼,所以可以將拼接的 JavaScript 代碼字符串,傳入 JS 解析器執行就可以,JS 解析器在這裡就是 Webview。

  • iOS UIWebview 使用 stringByEvaluatingJavaScriptFromString
  NSString *jsStr = @"執行的JS代碼";
[webView stringByEvaluatingJavaScriptFromString:jsStr];
  • iOS WKWebView 使用 evaluateJavaScript
[webView evaluateJavaScript:@"執行的JS代碼" completionHandler:^(id _Nullable response, NSError * _Nullable error) {
  
}];
  • Android 4.4 之前只能使用 loadUrl 實現並且無法執行回調且調用的時候會刷新 WebView
String jsCode = String.format("window.showWebDialog('%s')", text);
webView.loadUrl("javascript: " + jsCode);
  • Android 4.4 之後提供了 evaluateJavascript 來執行 JS 代碼可以獲取返回值執行回調
String jsCode = String.format("window.showWebDialog('%s')", text);
webView.evaluateJavascript(jsCode, new ValueCallback<String>() {
  @Override
  public void onReceiveValue(String value) {

  }
});

帶回調的調用#

上面提到的 Native、Web 雙向通信的幾種方法站在一端看還是一個單向通信的過程:Web 端調用 Native 方法,Native 直接進行相關操作但無法將結果返回 Web。
那麼如何實現對端操作後返回結果呢?

基於之前的單向通信,一端調用時在參數中加一個 callbackId 標記對應的回調,對端收到調用請求後進行實際操作後再進行一次調用將結果、callbackId 回傳回來,原端根據 callbackId 匹配相應的回調將結果傳入執行即可,實際上兩端都走了一次調用。

image

具體例子:在 Web 端點擊按鈕,獲取 Native 端輸入框的值,並將值以 Web 端彈窗展示。

// web 端代碼
<body>
  <div>
    <button id="showBtn">獲取 Native 輸入,以 Web 彈窗展現</button>
  </div>
</body>
<script>
  let id = 1;
  // 根據 id 保存callback
  const callbackMap = {};
  // 使用 JSSDK 封裝調用與 Native 通信的事件,避免過多的污染全局環境
  window.JSSDK = {
    // 獲取 Native 端輸入框 value,帶有回調
    getNativeEditTextValue(callback) {
      const callbackId = id++;
      callbackMap[callbackId] = callback;
      // 調用 JSB 方法,並將 callbackId 傳入
      window.NativeBridge.getNativeEditTextValue(callbackId);
    },
    // 接收 Native 端傳來的 callbackId
    receiveMessage(callbackId, value) {
      if (callbackMap[callbackId]) {
        // 根據 ID 匹配 callback,並執行
        callbackMap[callbackId](value);
      }
    }
  };

	const showBtn = document.querySelector('#showBtn');
  // 綁定按鈕事件
  showBtn.addEventListener('click', e => {
    // 通過 JSSDK 調用,將回調函數傳入
    window.JSSDK.getNativeEditTextValue(value => window.alert('Natvie輸入值:' + value));
  });
</script>
// Android 端代碼
webView.addJavascriptInterface(new NativeBridge(this), "NativeBridge");

class NativeBridge {
  private Context ctx;
  NativeBridge(Context ctx) {
    this.ctx = ctx;
  }

  // 獲取 Native 端輸入值
  @JavascriptInterface
  public void getNativeEditTextValue(int callbackId) {
    MainActivity mainActivity = (MainActivity)ctx;
    // 獲取 Native 端輸入框的 value
    String value = mainActivity.editText.getText().toString();
    // 需要注入在 Web 執行的 JS 代碼
    String jsCode = String.format("window.JSSDK.receiveMessage(%s, '%s')", callbackId, value);
    // 在 UI 線程中執行
    mainActivity.runOnUiThread(new Runnable() {
      @Override
      public void run() {
        mainActivity.webView.evaluateJavascript(jsCode, null);
      }
    });
  }
}

這樣就實現了 Web -> Native 帶有回調的調用,同理 Native -> Web 也是一樣的邏輯(相對 callbackId 有 bridgeName 的概念)

這裡也貼一張完整的調用時序圖,可以看到整個過程跟 JSONP 的機制非常相像,需要兩端遵循一套調用機制:

image

引用方式#

實際的 JSBridge 引用方式有以下兩種,各有利弊:

方式優點缺點
Native 端注入JSBridge 版本容易與 Native 對齊,不用考慮兼容注入時機不確定,需要有失敗重試機制
同時 JS 端調用時也要判斷是否已注入
JS 端引用時機可控,JS 直接調用方便需要考慮 JSBridge 與 Native 之間多版本的兼容

參考#

JSBridge 的原理
深入淺出 JSBridge:從原理到使用
JSBridge 原理與實踐

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。