こんにちはjunです。ここ数ヶ月は自作サービスRouteShareのネイティブアプリ版をreact nativeを用いて開発しています。RouteShareの中にはEditor.jsというリッチテキストエディタを用いてブログのようなコンテンツの作成ができる機能があります。
そこで問題となったのが「react nativeなど、ネイティブアプリ上でHTMLコンテンツを表現する方法」でした。react nativeはHTMLのマークアップ的にビューを作成できますが、使用するタグ(JSX)は違いますしreact nativeがJSXをよしなにネイティブコンポーネントとプロパティに変換しているだけです。HTML文字列を渡しても、ただの文字列として表示されます。
そのためreact nativeでHTMLコンテンツをブラウザのようにレンダリングするためには
という2つの方法があります。今回は「2」のwebviewを用いて
の3点について解説したいと思います。
【バージョンなど】
今回はReact nativeのインストールは解説しません。あらかじめReact nativeがインストールされ、npx react-native doctor
で問題が表示されない状態であるとします。
react nativeにてwebviewを利用するときにreact native webviewという便利なライブラリがあります。
このライブラリをインストールした後にandroidとiosで個別の設定を行います。
android/gradle.properties
に以下の値を設定します。
android.useAndroidX=true
android.enableJetifier=true
また外部ストレージ上の画像やファイルをリクエストする場合は android/app/src/main/AndroidManifest.xml
に以下のパーミッションを加えてください。
<uses-permission android:name="android.permission.INTERNET" />
以下のコマンドでposをインストールすればOKです。
npx pod-intall
or
cd ios && pod install
まずは既存のHTMLコンテンツをレンダリングする方法を解説します。私のサービスというとこのページの「ルート詳細」の箇所のようなとこです。リンクや画像の設定、インラインスタイルなどもできます。
これらのコンテンツ自体はHTML文字列で保存されており、webではXSS処理をした後にhtmlをそのまま流して表示しています。webvirewでも同じようにXSS処理をした後にそのHTML文字列を流し込んで表示させます。
まず以下のような方法で簡単にHTMLをレンダリングできます。
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を書くこともできますがさすがにきついことがあります。 ファイルを読み込ませる方法として
この2つがあります。ここでは2の方法を取ります。2のメリットとしては以下の通りです。
ローカルファイルを読み込ませる方法はiosと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下に読み込ませたいファイルを配置します。どこでもいいですが、assetsなどのディレクトリを作っておくといいです。
次にXcodeでiosのプロジェクトを開きます。
左のメニューからプロジェクトを右クリックして「Add Files to "..."」を選択します。ファインダーが開くので対象ファイルを選択します。ターゲットを選択して「Build Phases」に対象ファイルが追加されていればOKです。
HTML文字内では
<link rel="stylesheet" type="text/css" href="./webview.css">
と相対パスで指定します。このファイルも配置したり更新したときはビルドが必要です。ホットリロードのデバッグ中では再度ビルドしなおしてください。
上記の方法でcss,jsファイルを読み込ませることができます。linkタグを追加してスタイルが適用されることを確認してください。
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;
一通りレンダリングとスタイルの適用ができましたが、任意のHTMLコンテンツは高さが不明です。webviewコンポーネントは表示範囲の高さを指定する必要があり、表示範囲を超えるコンテンツはスクロールしてみることなります。
ただこれは結構厄介な点があります。例えば以下の図のようにネイティブのコンポーネントとwebviewのコンポーネントを積み上げて表示するとき、webviewのスクロールが発生しなかったりwebviewのスクロールが優先されて変なUXになることがあります。わかりにくいので図を用いて説明します。
全画面にいっぱいで表示する場合は特にレンダリング内容が表示領域を超えていても、スクロールが効いて全てのコンテンツが見れます。
コンポーネント全体でスマホの画面高さだけで詰められ、webviewの表示領域が限られているとします。そのとき表示領域分だけ表示され、スクロールされます。固定のヘッダーとフッターのようなイメージです。
ただ、HTMLコンテンツの一番下に緑のコンポーネントがあるようなレイアウトの場合、このように中途半端な表示領域がスクロールしてからスクリーン全体のスクロールができるようになます。
この状態を改善するためには「webview内bodyの高さを算出してネイティブ側に渡して、webviewコンポーネント自体にその高さを当てる」必要があります。そうすることでwebviewでのスクロールがなくなり、スクリーン全体のスクロールで移動できるようになります。