📲

WebViewからReact Nativeの関数を型安全に呼び出す

2024/04/03に公開

今までCapacitor(=WebViewのみ)で実装されていたユビーのモバイルアプリ(Android/iOS)を、React NativeとWebViewを組み合わせたハイブリッドアプリとして刷新しています。その過程で、WebView内で実行されるJavaScriptから、React Native側で定義した関数を型安全に呼び出せるライブラリを実装しました。その使用例と仕組みを紹介します。

https://github.com/yukukotani/react-native-webview-rpc

使い方

React Native側の実装

まずライブラリをインストールします。Peer dependenciesであるreact-native-webviewとcomlinkも入れてください。

npm install @react-native-webview-rpc/native
npm install react-native-webview comlink # peer dependencies

次に、WebViewから実行したい関数を実装します。ここではネイティブのアラートを出す関数を実装します。

加えて、関数の型をエクスポートしておきます。これは後述するWeb側の実装で参照します。

rpcs.tsx
import { Alert } from "react-native";

const rpcs = {
  async alert(title: string, body: string) {
    Alert.alert(title, body);
    return "ok";
  },
};

export type WebViewRpcs = typeof rpcs;

最後に、useWebViewRpcHandlerを用いてメッセージイベントのハンドラを取得します。これをreact-native-webviewが提供するWebViewコンポーネントのプロパティに渡してください。

App.tsx
import { useRef } from "react";
import WebView from "react-native-webview";
import { useWebViewRpcHandler } from "@react-native-webview-rpc/native";
import { rpcs } from "./rpcs";

export default function App() {
  const ref = useRef<WebView>(null);
  const onMessage = useWebViewRpcHandler(ref, rpcs);

  return (
    <WebView
      ref={ref}
      onMessage={onMessage}
      source={{ uri: "http://localhost:5173" }}
    />
  );
}

Web側の実装

まずは同じくライブラリをインストールします。@react-native-webview-rpc/nativeではなく@react-native-webview-rpc/webであることと、react-native-webviewが不要であることに注意してください。

npm install @react-native-webview-rpc/web
npm install comlink # peer dependencies

次にwrapを用いて、React Nativeの関数を実行するためのプロキシオブジェクトを生成します。型引数には、先述のrpcs.tsxからエクスポートしたWebViewRpcsを渡します。

import { wrap } from '@react-native-webview-rpc/web';
import type { WebViewRpcs } from "../native/rpcs";

const rpcs = wrap<WebViewRpcs>();

このrpcsオブジェクトを通して、React Native側で定義した通りのシグネチャで関数を実行することができます。

const result = await rpcs.alert("Hello", "World");

仕組み

Peer dependenciesでお察しかもしれませんが、Comlinkを使って実装しています。Comlinkは元々はWeb Worker内のオブジェクトや関数を透過的に扱えるようにするライブラリですが、Web Worker相当のインターフェースがEndpointという概念に抽象化されていて、postMessageでメッセージを送ってonmessageイベントでメッセージを受け取るようなメッセージングの仕組みがあれば応用することができます。

https://github.com/GoogleChromeLabs/comlink/blob/dffe9050f63b1b39f30213adeb1dd4b9ed7d2594/src/protocol.ts#L29-L33

https://github.com/GoogleChromeLabs/comlink/blob/dffe9050f63b1b39f30213adeb1dd4b9ed7d2594/src/protocol.ts#L7-L19

例えば、iframeのようにwindowオブジェクトを持つものは window.postMessage()window.addEventListener() でメッセージングができます。それに対応するEndpointがビルトインで提供されています

では、WebViewをこのインターフェースで扱うにはどうしたらよいでしょうか。

WebView -> React Native のメッセージ

まずは関数呼び出しを伝えるメッセージです。

react-native-webviewはWebView内のwindowオブジェクトにpostMessageを生やしてくれます。Web側でこれを叩いて WebView から React Native にメッセージを送ります。

https://github.com/yukukotani/react-native-webview-rpc/blob/a5f8af97d8a1bd1e57391a2c392dfae168248594/packages/web/src/endpoint.ts#L4-L7

このメッセージを React Native 側のonMessageプロパティで受け取り、Comlinkが登録するリスナーに流します。メッセージの中身を解釈して関数を実行するのはComlinkがやってくれます。

https://github.com/yukukotani/react-native-webview-rpc/blob/a5f8af97d8a1bd1e57391a2c392dfae168248594/packages/native/src/endpoint.ts#L55-L68

React Native -> WebView のメッセージ

次に関数呼び出しのレスポンスを伝えるメッセージです。

React Native からは injectJavaScript を用いて、WebView内でメッセージイベントを発火させます。JSONはJavaScriptのサブセットですから、JSON.stringifyしたメッセージをinjectするといい具合にオブジェクトとして解釈されます。

https://github.com/yukukotani/react-native-webview-rpc/blob/a5f8af97d8a1bd1e57391a2c392dfae168248594/packages/native/src/endpoint.ts#L49-L53

Web側ではこのイベントに対して素直にリスナーを登録します。メッセージの解釈は同様にComlinkがやってくれます。

https://github.com/yukukotani/react-native-webview-rpc/blob/a5f8af97d8a1bd1e57391a2c392dfae168248594/packages/web/src/endpoint.ts#L8-L10

このように、React Native, Webの両方でEndpointインターフェースに合わせてメッセージングの実装をするだけで、残りはComlinkの仕組みを使いまわせます。Proxyを駆使した透過的呼び出しやメッセージのシリアライズなどについて考えなくて済むのでとっても楽です。

おわりに

Comlinkのイケてる抽象化によって、WebViewからReact Nativeの関数を型安全に呼び出せた事例を紹介しました。ユビーではWeb技術を駆使したモバイルアプリの開発も加速しています。少しでも興味持ってくださった方はぜひお話しましょう!DMでもpittaでも何でもご連絡ください。

https://pitta.me/matches/ctRgxKmjoujl

https://herp.careers/v1/ubiehr/2PGTZ45P-x31

Ubie テックブログ

Discussion