React Native の WebView 内外でやりとりする
最近 React Native をやり始めた erukiti です。ビギナーです。優しくしてください。
諸々の事情から WebView で構成されたコンテンツを描画することになりました。
WebView は、ウェブフロントエンド的にわかりやすく言えば、IFrame のような技術です。WebView コンポーネントの中では、静的な HTML か、どこかのサイトのウェブページが内部にレンダリングされます。
Android Native なら同名の WebView があり、iOS/macOS なら、WkWebView があります。
今回は、React Native における WebView で、WebView の中から外、あるいはその逆にやりとりをするための方法についてまとめました。
React Native の WebView
React Native の WebView は以前のバージョンだと組み込みだったものが、今は独立したパッケージになってるようですね。
- https://github.com/react-native-webview/react-native-webview
 - https://github.com/react-native-webview/react-native-webview/blob/master/docs/Getting-Started.md
 - https://github.com/react-native-webview/react-native-webview/blob/master/docs/Reference.md
 
さて今回の記事では WebView で、内から外、外から内への通信を行う方法です。
ちょうど、ドキュメントにCommunicating between JS and Native というそのものズバリなものがあります。
- React Native WebView のプロパティで JS を指定する 
injectedJavaScript - React Native WebView の ref から JS を指定する 
injectJavaScript - WebView の中身からメッセージを送信する 
window.ReactNativeWebView.postMessageと React Native WebView のプロパティでメッセージを受信するonMessage 
この 3 つを使ってやりとりをすることになります。
injectedJavaScript
パッケージ公式の https://github.com/react-native-webview/react-native-webview/blob/master/docs/Guide.md#the-injectedjavascript-prop はクラスコンポーネントにおける説明です。関数型コンポーネント(Hooks)で書くとこんな感じでしょうか。
const App: React.FC = () => {
  return (
    <WebView
      style={{ flex: 1 }}
      source={{
        url: 'http://localhost:3000',
      }}
      injectedJavaScript={`
        alert('Injected!');
        true; // 必須
      `}
    />
  )
}
injectedJavaScrpt は文字列を JavaScript として実行するプロパティです。
- 
console.logはどこに出るか不明なのでたぶん使えない - 末尾の 
trueがなぜか必要note: this is required, or you'll sometimes get silent failures 
実行タイミングによって、
- injectedJavaScriptBeforeContentLoaded
 - injectedJavaScriptForMainFrameOnly
 - injectedJavaScriptBeforeContentLoadedForMainFrameOnly
 
という亜種もあります。
基本的には、ページを読み込む前にグローバルに影響を及ぼすコードを走らせるものです。
injectJavaScript
初期化時点でグローバルに JS を実行するだけだとあまりに不便です。そこで WebView の ref からメソッドで実行する injectJavaScript というものも用意されています。
const App: React.FC = () => {
  const ref = React.useRef<any>()
  React.useEffect(() => {
    setTimeout(() => {
      ref.injectJavaScript(`
        alert('inject!');
        true;
      `)
    }, 3000)
  }, [])
  return (
    <WebView
      ref={ref}
      style={{ flex: 1 }}
      source={{
        url: 'http://localhost:3000',
      }}
    />
  )
}
型は適当。
ref.injectJavaScript(code) で任意のタイミングでコードを実行可能です。Button を押したらコードを実行する、生体認証が OK ならコードを実行する、のような処理が可能です。
postMessage / onMessage
WebView 内部で実行されてるコードの方から、React Native のアプリへ働きかけたいときに使える方法も用意されています。
window.ReactNativeWebView.postMessage(message) です。ただし、message は文字列でなければいけません。複数のデータ、複雑なデータ構造などを送りたい場合は JSON なりにすると良いでしょう。
postMessage で送信された文字列は onMessage プロパティで受け取ることができます。
// React Native 側
const App: React.FC = () => {
  return (
    <WebView
      style={{ flex: 1 }}
      source={{
        url: 'http://localhost:3000'
      }}
      onMessage={(ev) => console.log(ev.nativeEvent.data)}
    >
  )
}
// WebView 内部のコード
if ('ReactNativeWebView' in window) {
  window.ReactNativeWebView.postMessage(
    JSON.stringify({ type: 'hoge', hoge: 'ほげ' }),
  )
}
のように使います。
postMessage なのに、送信だけしかできず返信を受け取ることが出来ないなど、使い勝手が良いとは言えませんが、一応 WebView 内部から通信が可能です。
まとめ
- 初期化につかえる 
injectedJavaScriptプロパティ(同系統のものが残り三つある) - ワンショットできる 
injectJavaScriptメソッド - WebView 内部から送信できる 
postMessage関数 
統一感が無いですが、これらを組み合わせて使うしかないようです。
React Native 初心者なので、間違っているかもしれません。間違いや、こうしたほうがいい、みたいなものがあればぜひコメントなどを書いていただけると助かります。
Discussion