技術スタック JavascriptReact Nativeiosandroid

react native webviewを用いたリッチテキストの編集と表示

2024.11.08

こんにちはjunです。ここ数ヶ月は自作サービスRouteShareのネイティブアプリ版をreact nativeを用いて開発しています。RouteShareの中にはEditor.jsというリッチテキストエディタを用いてブログのようなコンテンツの作成ができる機能があります。

そこで問題となったのが「react nativeなど、ネイティブアプリ上でHTMLコンテンツを表現する方法」でした。react nativeはHTMLのマークアップ的にビューを作成できますが、使用するタグ(JSX)は違いますしreact nativeがJSXをよしなにネイティブコンポーネントとプロパティに変換しているだけです。HTML文字列を渡しても、ただの文字列として表示されます。

そのためreact nativeでHTMLコンテンツをブラウザのようにレンダリングするためには

  1. HTMLをパースしてネイティブコンポーネント(react nativeの該当するJSX)に書き換える
  2. webviewというネイティブアプリ内で使用できるブラウザコンポーネントでHTMLをレンダリングする

という2つの方法があります。今回は「2」のwebviewを用いて

  • すでにあるスタイルでのHTMLレンダリング
  • リッチテキストエディタ(editor .js)を用いたコンテンツ作成
  • webviewとアプリのデータ送受信

の3点について解説したいと思います。

【バージョンなど】

  • react-native: 0.75.4
  • react: 18.3.1
  • react-native-webview: 13.12.3
  • OS: Macbook air M2 Sonoma14.5
  • Xcode 15.4
  • Android Studio Android Debug Bridge version 1.0.41 Version 34.0.1-9680074

セットアップ

今回はReact nativeのインストールは解説しません。あらかじめReact nativeがインストールされ、npx react-native doctor で問題が表示されない状態であるとします。

react nativeにてwebviewを利用するときにreact native webviewという便利なライブラリがあります。

このライブラリをインストールした後にandroidとiosで個別の設定を行います。

android

android/gradle.properties に以下の値を設定します。

android.useAndroidX=true
android.enableJetifier=true

また外部ストレージ上の画像やファイルをリクエストする場合は android/app/src/main/AndroidManifest.xml に以下のパーミッションを加えてください。

<uses-permission android:name="android.permission.INTERNET" />

ios

以下のコマンドでposをインストールすればOKです。

npx pod-intall
or 
cd ios && pod install

webviewによるHTMLレンダリング

まずは既存のHTMLコンテンツをレンダリングする方法を解説します。私のサービスというとこのページの「ルート詳細」の箇所のようなとこです。リンクや画像の設定、インラインスタイルなどもできます。

これらのコンテンツ自体はHTML文字列で保存されており、webではXSS処理をした後にhtmlをそのまま流して表示しています。webvirewでも同じようにXSS処理をした後にそのHTML文字列を流し込んで表示させます。

webview内でもXSSが発生することがあります。表示するHTMLは表示する前にxssライブラリなどを用いて使用できるタグや属性などを制限してください。今回は処理の解説はしません。

簡単なHTMLの表示

まず以下のような方法で簡単にHTMLをレンダリングできます。

sample.tsx
import WebView from "react-native-webview";
const Renderer: React.FC = () => { 
    const webViewRef = useRef<WebView|null>(null);
    const WEBVIEW_URL = "https://your-service.com" // あなたのwebサービスのURL。とりあえず試すならPCのIPでもOK
    const html = `
    <!DOCTYPE html>
        <html>
            <head>
                <meta http-equiv="content-type" content="text/html; charset=utf-8">
                <meta name="viewport" content="width=device-width, initial-scale=1">
                <style>
                body{
                    background-color: transparent;
                }
                </style>
            </head>
            <body>
                <h1>this is test content</h1>
            </body>
        </html>
    `;
    return (<WebView
        originWhitelist={['*']}
        ref={webViewRef}
        source={{ html: html,baseUrl: Platform.OS === "android" ? WEBVIEW_URL : "" }}
        style={{ width: "100%", flex:1 }}
        javaScriptEnabled={true}
    ></Webview>)
}

export default Renderer;

このようにsource={{ html: html }} HTML文字列を渡すとそのOSのデフォルトブラウザの仕様に従ったスタイルが表示されるようになります。このHTMLに表示したいHTML、cssファイルやJSファイルを読み込めるようにすればwebと同じレンダリングができるようになります。

まずはcssファイルが読み取れるようにします。

ローカルファイルの設定

インラインでstyleとjsを書くこともできますがさすがにきついことがあります。 ファイルを読み込ませる方法として

  1. リンクを利用してネットワークからcss,jsを取得する
  2. OSのローカルファイルとして配置して読み込ませる

この2つがあります。ここでは2の方法を取ります。2のメリットとしては以下の通りです。

  • ローカルファイルとして配置されるのでオフラインでも表示できるようになる。
  • 読み込みが早い

ローカルファイルを読み込ませる方法はiosとandroidで異なります。

android

androidはまだ簡単でandroid/app/src/main/assets に読み込ませたいファイルを配置します。そしてHTML文字内では

 <link rel="stylesheet" type="text/css" href="file:///android_asset/webview.css">

のように android/app/src/main/assets = file:///android_asset/ から対象ファイルをしていできます。

このファイルを配置したり更新したときはビルドが必要です。ホットリロードのデバッグ中では再度ビルドしなおしてください。

ios

iosは少し面倒です。まずはios下に読み込ませたいファイルを配置します。どこでもいいですが、assetsなどのディレクトリを作っておくといいです。

次にXcodeでiosのプロジェクトを開きます。

左のメニューからプロジェクトを右クリックして「Add Files to "..."」を選択します。ファインダーが開くので対象ファイルを選択します。ターゲットを選択して「Build Phases」に対象ファイルが追加されていればOKです。

HTML文字内では

 <link rel="stylesheet" type="text/css" href="./webview.css">

と相対パスで指定します。このファイルも配置したり更新したときはビルドが必要です。ホットリロードのデバッグ中では再度ビルドしなおしてください。

上記の方法でcss,jsファイルを読み込ませることができます。linkタグを追加してスタイルが適用されることを確認してください。

sample.tsx
import WebView from "react-native-webview";
import { Platform } from "react-native";
const Renderer: React.FC = () => { 
    const webViewRef = useRef<WebView|null>(null);
    const WEBVIEW_URL = "https://your-service.com" // あなたのwebサービスのURL。とりあえず試すならPCのIPでもOK
    const styleFilePath = Platform.OS === "android" ? "file:///android_asset/webview.css" : "./webview.css" ;
    const html = `
    <!DOCTYPE html>
        <html>
            <head>
                <meta http-equiv="content-type" content="text/html; charset=utf-8">
                <meta name="viewport" content="width=device-width, initial-scale=1">
                <link rel="stylesheet" type="text/css" href="${styleFilePath}">
                <style>
                body{
                    background-color: transparent;
                }
                </style>
            </head>
            <body>
                <h1>this is test content</h1>
            </body>
        </html>
    `;
    return (<WebView
        originWhitelist={['*']}
        ref={webViewRef}
        source={{ html: html,baseUrl: Platform.OS === "android" ? WEBVIEW_URL : "" }}
        style={{ width: "100%", flex:1 }}
        javaScriptEnabled={true}
    ></Webview>)
}

export default Renderer;
iosではoriginWhitelistとbaseUrlを指定しないとURIによるファイルインポートが行われません!

webviewの高さ調整

一通りレンダリングとスタイルの適用ができましたが、任意のHTMLコンテンツは高さが不明です。webviewコンポーネントは表示範囲の高さを指定する必要があり、表示範囲を超えるコンテンツはスクロールしてみることなります。

ただこれは結構厄介な点があります。例えば以下の図のようにネイティブのコンポーネントとwebviewのコンポーネントを積み上げて表示するとき、webviewのスクロールが発生しなかったりwebviewのスクロールが優先されて変なUXになることがあります。わかりにくいので図を用いて説明します。

全画面にいっぱいで表示する場合は特にレンダリング内容が表示領域を超えていても、スクロールが効いて全てのコンテンツが見れます。

コンポーネント全体でスマホの画面高さだけで詰められ、webviewの表示領域が限られているとします。そのとき表示領域分だけ表示され、スクロールされます。固定のヘッダーとフッターのようなイメージです。

ただ、HTMLコンテンツの一番下に緑のコンポーネントがあるようなレイアウトの場合、このように中途半端な表示領域がスクロールしてからスクリーン全体のスクロールができるようになます。

この状態を改善するためには「webview内bodyの高さを算出してネイティブ側に渡して、webviewコンポーネント自体にその高さを当てる」必要があります。そうすることでwebviewでのスクロールがなくなり、スクリーン全体のスクロールで移動できるようになります。

Copyright © 2021 jun. All rights reserved.