jeremygo

jeremygo

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

JSBridge の原理

イントロダクション#

現在、モバイル端末が主流の時代において、技術選定は基本的にハイブリッド開発(Hybrid)であり、ネイティブ Native と Web H5 の両方の利点を組み合わせて使用しています:

  • ネイティブ技術は iOS(Objective C、Swift)、Android(Java)を指し、開発効率は低いですが、リリースはユーザーの更新に依存し、パフォーマンスが高く、機能のカバレッジも広いです。
  • Web 技術は JavaScript を指し、開発効率は高いですが、リリース更新が柔軟である一方、パフォーマンスが低く、機能特性も制限されています。

このモデルでは、H5 はしばしばネイティブの機能を使用する必要があります。例えば、カメラを開く、ローカルアルバムを表示する、画像をアップロードする、ページを共有するなどです。また、ネイティブも Web 側に更新状態をプッシュする必要があります。
JavaScript は独立した JS コンテキスト(Webview コンテナ、JSCore など)で実行され、ネイティブの実行環境とは隔離されているため、ネイティブと Web 側の双方向通信を実現するメカニズムが必要です。このメカニズムが JSBridge です:

JavaScript エンジンまたは Webview コンテナを媒介として、協定プロトコルを通じて通信し、ネイティブと Web の双方向通信を実現するメカニズムです。

JSBridge を通じて、Web 側はネイティブ側の Java インターフェースを呼び出すことができ、ネイティブ側も Web 側の JS インターフェースを呼び出すことができ、双方向の呼び出しを実現します。

通信原理#

JS がネイティブを呼び出す#

JavaScript がネイティブを呼び出す方法は主に三つあります:API 注入URL SCHEME のインターセプト、および prompt などの JS グローバルメソッドのオーバーライド

API 注入#

主な原理は、Webview が提供するインターフェースを通じて、JS の実行環境(window)にオブジェクトやメソッドを注入し、JS が呼び出すときに直接対応するネイティブコードのロジックを実行することです。

iOS UIWebView 実装(主に JavaScriptCore

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

context[@"postBridgeMessage"] = ^(NSArray<NSArray *> *calls) {
    // ネイティブロジック
};

フロントエンド呼び出し

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;
    // オブジェクトを注入し、フロントエンドがそのメソッドを呼び出すときにネイティブがキャッチできる
    [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);
        // ネイティブロジック
    }
}

フロントエンド呼び出し

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

Android 実装

/**
 4.2 以前、Android で JavaScript オブジェクトを注入するインターフェースは addJavascriptInterface でしたが、
 このインターフェースには脆弱性があり、不正な者に利用され、ユーザーの安全を脅かす可能性があるため、
 4.2 では新しいインターフェース @JavascriptInterface が導入され、このインターフェースを置き換え、安全問題を解決しました。
 したがって、Android でオブジェクトを注入する方法には互換性の問題があります。
*/  
public class JavaScriptInterfaceDemoActivity extends Activity {
private WebView Wv;

    @Override
    public void onCreate(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");
    }

    public class JavaScriptInterface {
         Context mContext;

         JavaScriptInterface(Context c) {
             mContext = c;
         }

         public void postMessage(String webMessage){	    	
             // ネイティブロジック
         }
     }
}

フロントエンド呼び出し

window.nativeBridge.postMessage(message);
URL Scheme のインターセプト#

URL Scheme は URL に似たリンクで、アプリが直接相互に呼び出すために設計されています。形式は次の通りです:
<protocol>://<host>/<path>?<qeury>#fragment
カスタムの JSBridge 通信の URL Scheme:jsbridge://showToast?text=hello

主な原理は、ネイティブが Webview を読み込んだ後、Web が送信するすべてのリクエストが Webview コンポーネントを通過するため、ネイティブは対応するメソッドをオーバーライドしてリクエストをインターセプトし、カスタムの URL Scheme 形式に一致するかどうかを判断して解析処理を行い、ネイティブメソッドを呼び出すことができます。

Web が URL リクエストを送信する方法はいくつかあります。

  • a タグ(欠点:ユーザー操作が必要)
  • location.href(欠点:ページ遷移による呼び出しの喪失の可能性)
  • ajax リクエスト(Android には対応するインターセプトメソッドがない)
  • iframe src を使用

上記の方法の中で iframe src は初期から頻繁に使用されている手法で、互換性が非常に良いです:

  • Android は shouldOverrideUrlLoading メソッドを提供してインターセプトします。
  • UIWebViewshouldStartLoadWithRequest を使用し、WKWebViewdecidePolicyForNavigationAction を使用します。

ただし、一定の欠点もあります:

  • URL に基づいているため、長さに制限があります。
  • リクエストの作成には一定の時間がかかり、API 注入の方法で同じ機能を呼び出すよりも時間がかかります。
prompt などの JS グローバルメソッドのオーバーライド#

Webview には setWebChromeClient というメソッドがあり、WebChromeClient オブジェクトを設定できます。その中には onJsAlertonJsConfirmonJsPrompt の三つのメソッドがあり、JS が window.alertwindow.confirmwindow.prompt を呼び出すと、上記の対応するメソッドもトリガーされます。このメカニズムを利用していくつかの処理を行うこともできます。
上記のメソッドをインターセプトすることはパフォーマンスに一定の影響を与えるため、使用頻度が低いメソッドを選択する必要があります。Android では、他のメソッドに比べて prompt メソッドはほとんど使用されないため、prompt を占有するのが最適な方法です。

ネイティブが JS を呼び出す#

ネイティブ側が 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) {

  }
});

コールバックを伴う呼び出し#

上記で述べたネイティブと Web の双方向通信のいくつかの方法は、一方の視点から見ると依然として単方向通信のプロセスです:Web 側がネイティブメソッドを呼び出し、ネイティブが直接関連操作を行いますが、結果を Web に返すことはできません。
では、どのようにして対側の操作後に結果を返すことができるのでしょうか?

以前の単方向通信に基づいて、一方が呼び出すときにパラメータに callbackId を追加して対応するコールバックをマークします。対側が呼び出しリクエストを受け取った後、実際の操作を行い、再度呼び出して結果と callbackId を返します。元の側は callbackId に基づいて対応するコールバックをマッチさせ、結果を渡して実行すればよいのです。実際には、両側が一度ずつ呼び出しを行います。

image

具体的な例:Web 側でボタンをクリックし、ネイティブ側の入力フィールドの値を取得し、その値を Web 側のポップアップで表示します。

// Web 側コード
<body>
  <div>
    <button id="showBtn">ネイティブの入力を取得し、Web ポップアップで表示</button>
  </div>
</body>
<script>
  let id = 1;
  // id に基づいてコールバックを保存
  const callbackMap = {};
  // JSSDK を使用してネイティブ通信のイベントをラップし、グローバル環境の汚染を避ける
  window.JSSDK = {
    // ネイティブ側の入力フィールドの value を取得し、コールバックを伴う
    getNativeEditTextValue(callback) {
      const callbackId = id++;
      callbackMap[callbackId] = callback;
      // JSB メソッドを呼び出し、callbackId を渡す
      window.NativeBridge.getNativeEditTextValue(callbackId);
    },
    // ネイティブ側から渡された callbackId を受け取る
    receiveMessage(callbackId, value) {
      if (callbackMap[callbackId]) {
        // ID に基づいてコールバックをマッチさせ、実行する
        callbackMap[callbackId](value);
      }
    }
  };

	const showBtn = document.querySelector('#showBtn');
  // ボタンイベントをバインド
  showBtn.addEventListener('click', e => {
    // JSSDK を通じて呼び出し、コールバック関数を渡す
    window.JSSDK.getNativeEditTextValue(value => window.alert('ネイティブ入力値:' + value));
  });
</script>
// Android 側コード
webView.addJavascriptInterface(new NativeBridge(this), "NativeBridge");

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

  // ネイティブ側の入力値を取得
  @JavascriptInterface
  public void getNativeEditTextValue(int callbackId) {
    MainActivity mainActivity = (MainActivity)ctx;
    // ネイティブ側の入力フィールドの 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 -> ネイティブのコールバックを伴う呼び出しが実現され、同様にネイティブ -> Web も同じロジックです(callbackId に対して bridgeName の概念があります)。

ここに完全な呼び出しシーケンス図も貼っておきます。全体のプロセスは JSONP のメカニズムに非常に似ており、両側が一つの呼び出しメカニズムに従う必要があります:

image

参照方法#

実際の JSBridge の参照方法には以下の二つがあり、それぞれ利点と欠点があります:

方法利点欠点
ネイティブ側注入JSBridge バージョンがネイティブと容易に一致し、互換性を考慮する必要がない注入のタイミングが不確定で、失敗の再試行メカニズムが必要
同時に JS 側の呼び出し時にも注入されているかどうかを判断する必要があります
JS 側参照タイミングが制御可能で、JS からの直接呼び出しが便利JSBridge とネイティブ間の多バージョンの互換性を考慮する必要があります

参考#

JSBridge の原理
深く浅く JSBridge:原理から使用まで
JSBridge 原理と実践

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。