3 min read

[RN] コードインジェクションによるウェブページの動作修正

[RN] コードインジェクションによるウェブページの動作修正
[RN] コードインジェクションによるウェブページの動作修正
Code Injection with Webview(mock 이미지 출처: https://github.com/react-native-webview/react-native-webview/blob/master/docs/Reference.md)

RN , AI Tools, react-native-webview

RN , AI Tools, react-native-webview

記事の順序

  • 問題状況
  • 要件分析について
  • 良い解決策を見つける
  • 解決策1と解決策2の違い
  • 感じたこと

問題状況

  • はじめに
    • アプリ内で一部のページをウェブビューとして使用しています。
    • これまでは、プライバシーポリシーなどユーザーとの相互作用がないページのみをウェブビューとして使用し、ヘッダーやフッターなどは非表示にすることで、ユーザーがウェブビュー内でページ遷移する心配がありませんでした。
    • しかし、一部のページをウェブビューとして使用することになり、ユーザーがウェブビュー内でページ遷移を試みた場合、それを防ぐ必要が出てきました。
  • 最初は一般的な方法でページ遷移を検知して阻止しようとしました。しかし、想定通りにはいきませんでした。理由は、Next.jsのルーターがクライアントサイドルーティングを使用しているためです。
  • 結論:ウェブビュー内でのページ遷移を防ぐ方法が必要です。

要件分析について

最初に与えられた要件は「アプリでページ変更を検知できるようにリロードする」ように見えました。

しかし、ページ遷移をキャッチする理由をよく考えてみると

  • ウェブビューでのページ遷移を別のアクション(例:アプリの商品詳細画面への遷移)に置き換えるため

アプリでルーティングを制御できれば問題が解決するということが分かります。

良い解決策を見つける

解決策1:クエリパラメータ

  • 最初に思いついた方法は、ヘッダーとフッターを非表示にするために使用していたクエリパラメータでフルリロードを制御する方法です。
  • クエリパラメータのfullReloadがtrueの場合のみ、router.pushの代わりにwindow.location.hrefを使用して、完全なページリロードが発生するように分岐する方法です。
const fullReload = queryParams.fullReload === 'true';

if (fullReload) {
  // fullReloadがtrueの場合
  window.location.href = path;
} else {
  // その他はrouter.push使用
  router.push(path);
};

動作はしますが、良い方法とは考えられないため使用しませんでした。

  • 良くない理由1:フロントエンドで処理すべきクエリパラメータが増え、複雑性が高まる
  • 良くない理由2:ウェブ開発者が誤って分岐を削除したり、クエリパラメータを無視したりすると簡単に破綻する
  • 良くない理由3:ウェブビュー進入時にこのクエリを付けて入ると、必ずページ変更時にフルリロードが発生する(状況に応じてフルリロードを選択的に適用することができない)
  • 良くない理由4:フロントエンドのコードだけを見ても、どのような意図で書かれたコードなのかすぐには理解しづらい(明確さに欠ける)

解決策2:JavaScript Injectionの実装

  • 検索で見つけたJavaScript Injectionで実装することにしました。
  • ページ遷移が発生するケースを考えてみると、
    • 作品をクリック > 購入ボタンをクリック > ページ遷移
    • 戻るボタンをクリック > ページ遷移
  • すべてのケースが**Router(ページ遷移)**に関連していることがわかります。
  • そこで、Code Injectionを使用してRouterの動作を変更してみてはどうかと考えました。
  • 実現可能性をClaudeに確認し、何度かやり取りした結果、以下のような成果物を作成することができました。
// ルーターの動作を制御するためのコードインジェクション。
// 以下の関数をWebViewコンポーネントの
// injectedJavaScript、injectedJavaScriptBeforeContentLoaded
// propsに渡す必要があります。

const injectedJavaScript = `
    (function() {
      const overrideRouterPush = function() {
        window.isNativeApp = true;
        const originalPush = window.next.router.push;
        window.next.router.push = function(url, as, options) {
          const shouldInterceptPush = [
            '/example1',
            '/example2'
          ].some(pattern => url?.includes(pattern));

          if (shouldInterceptPush) {
            window.ReactNativeWebView.postMessage(JSON.stringify({
              event: 'routerPush',
              data: {
                url: url,
              },
            }));
            return;
          }

          originalPush.call(this, url, as, options);
        };
      };

      window.onload = overrideRouterPush;
    })();
  `;

クエリパラメータを使用する方法では状況に応じてrouter.pushを使用したりページ遷移を制御したりする選択的な適用が難しかったのですが、この方法で実装することでアプリ側ですべての動作をコントロールすることが可能になります。

  • ステップ1:JavaScriptインジェクションを使用してNext.jsのrouter.pushを修正。(メッセージ送信、上記のコードを参照)
  • ステップ2:後処理が必要な場合はonMessageで処理を行う。
// onMessageプロパティをWebViewコンポーネントに渡す例
// ここでjavascript codeinjectionから送信されたメッセージを処理できます。
const onMessage = (event) => {
  const message = JSON.parse(event.nativeEvent.data) as Message;
  if (message.event === 'routerPush') {
    // 後処理が必要な場合
  };
  return true;
};

解決策1と解決策2の違い

  • Code Injectionを使用する方法では、Query Paramsを使用する方法(解決策2)と比較すると、アプリで必要な要件をアプリ側で記述するため、混乱が生じにくくなっています。
  • フロントエンド開発者とのコミュニケーションを必要とせず直接制御が可能で、フロントエンド側のミスによってバグが発生するリスクも比較的低くなっています。

感じたこと

  • ウェブ開発ばかりしていたので久しぶりにアプリ開発に携わりました。最初はテスト用として書いたコードが実際に適用されて嬉しく思いました。
  • iOSとAndroidをそれぞれシミュレーターとエミュレーターで設定するのも久しぶりだったので、かなり時間がかかりました。
  • 要件があるからといってそのまま作業するのではなく、分析してみる姿勢が良いと思います。時間はかかりましたが、より良い方法で作業できたと思います。