React Nativeで任意のReact コンポーネントを使う方法

公開日: 2018年12月4日GitHub

この記事はReact Nativeアドベントカレンダーの5日目の記事です。 React Nativeでリッチテキストエディタをどうやって実装したのかを紹介します

やりたかったこと

新規事業でSNSアプリケーションをReact NativeとExpoを使って開発していました(現在リリース申請中)。 このSNSでユーザーは投稿のなかに他のユーザーにメンションを追加したり、ハッシュタグを追加することができます。

そこでTwitterやFacebookで出てくるような、サジェスト機能の実装をする必要がでてきました。 考えられる方法はざっくり以下です

  • 頑張ってReact NativeのViewで書く
  • iOS, Androidのネイティブで書いてブリッジを書く
  • WebViewで実装する(DOMのcontenteditable="true"を活用する)

今回やったのは3番目のWebViewの実装をすることができます

出来上がったもの

こんな感じのリッチテキストエディタです。 Quill.jsベースで、ハッシュタグ、メンションなどを実装しています。Quill.jsは内部的にcontenteditableを利用しているので、WebView経由じゃないとこのような実装をすることができません。

どうやるのか

React NativeではWebViewを経由して任意のhtmlファイルを読み込んで画面内に表示したり、任意のJavaScriptコードを実行することができます。 これはつまり、ブラウザ内でしか実現できない挙動をWebView経由であればReact Nativeアプリにマウントできるということです。

これはReactであってもVueであっても、PureJSであっても同様です。ありとあらゆるJSコードをReact Native上で実行することができます。 また、postMessageやonMessageを経由して、React NativeとWebView間で通信することができます。

今回はこの仕組みで、Quill.jsをReact Nativeアプリのリッチテキストエディタとして採用しました。

コードサンプル

WebViewからReact Nativeに対してメッセージを送信する方法

WebView内からReact Native側にメッセージを送信するためには、window.postMessageを実行します。 また、送信されたメッセージをReact Native側で受け取るためにはWebViewコンポーネントのonMessageで、WebView内からのメッセージを受信することができます。

1// React Native側
2
3export class RichTextInput extends React.PureComponent<Props, State> {
4 handleMessage = (event: NativeSyntheticEvent<WebViewMessageEventData>) => {
5 let msgData;
6 try {
7 msgData = JSON.parse(event.nativeEvent.data);
8 if (
9 msgData.hasOwnProperty("prefix") &&
10 msgData.prefix === MESSAGE_PREFIX
11 ) {
12 // ここに任意のメッセージ処理を書く
13 switch (msgData.type) {
14 case "EDITOR_LOADED":
15 // 任意の処理
16 break;
17 case "EDITOR_SENT":
18 // 任意の処理
19 break;
20 case "TEXT_CHANGED":
21 // 任意の処理
22 break;
23 }
24 }
25 } catch (err) {
26 console.warn(err);
27 return;
28 }
29 };
30
31 // 略
32
33 render() {
34 return (
35 <WebView
36 ref={this.createWebViewRef}
37 source={RICH_TEXT_SOURCE}
38 onLoadEnd={this.onWebViewLoaded}
39 onMessage={this.handleMessage}
40 startInLoadingState={true}
41 originWhitelist={["*"]}
42 javaScriptEnabled={true}
43 onError={this.onError}
44 scalesPageToFit={false}
45 mixedContentMode={"always"}
46 domStorageEnabled={true}
47 />
48 );
49 }
50}
51
52

React NativeからWebViewにメッセージを送信する方法

逆にReact Native側からメッセージを送信するためには、WebViewのReactElementに対してpostMessageを実行します。 何らか、メッセージの型を決めておくと便利です。

1// React Native側
2
3export class RichTextInput extends React.PureComponent<Props, State> {
4 webview?: WebView;
5 createWebViewRef = (webview: WebView) => {
6 this.webview = webview;
7 };
8
9 public addUserMention = (user: User) => {
10 this.sendMessage("INSERT_USER_MENTION", { user });
11 };
12
13 public addHashTag = (hashTag: HashTag) => {
14 this.sendMessage("INSERT_USER_MENTION", { hashTag });
15 };
16
17 public setHtmlContent = (html: string) => {
18 this.sendMessage("SET_HTML_CONTENTS", {
19 html
20 });
21 };
22
23 sendMessage = (type: string, payload?: any) => {
24 // only send message when webview is loaded
25 if (this.webview) {
26 debugLog(`WebViewQuillEditor: sending message ${type}`);
27
28 // @ts-ignore
29 this.webview.postMessage(
30 JSON.stringify({
31 prefix: MESSAGE_PREFIX,
32 type,
33 payload
34 }),
35 "*"
36 );
37 }
38 };
39
40 render() {
41 return (
42 <WebView
43 ref={this.createWebViewRef}
44 source={RICH_TEXT_SOURCE}
45 // 略
46 />
47 );
48 }
49}
50
51

これをWebView側で受信するためには、documentにmessageイベントで通知されるのでこのイベントをlistenして処理します

1
2export default class ReactQuillEditor extends React.Component<{}, State> {
3 componentDidMount() {
4 if (document) {
5 document.addEventListener("message", this.handleMessage);
6 } else if (window) {
7 window.addEventListener("message", this.handleMessage);
8 } else {
9 console.log("unable to add event listener");
10 }
11 this.loadEditor();
12 (window as any).app = this;
13 }
14
15 handleMessage: EventListener = (event) => {
16 const data = (event as any).data as string;
17
18 let msgData;
19 try {
20 msgData = JSON.parse(data) as any;
21 if (
22 msgData.hasOwnProperty("prefix") &&
23 msgData.prefix === MESSAGE_PREFIX
24 ) {
25 switch (msgData.type) {
26 case "LOAD_EDITOR":
27 // 略
28 break;
29 case "SEND_EDITOR":
30 // 略
31 break;
32 default:
33 }
34 }
35 } catch (err) {
36 this.printElement(`reactQuillEditor error: ${err}`);
37 return;
38 }
39 };
40}
41

頑張ってhtmlファイルを作る

もしもこのやり方でコンポーネントを作るならば、WebViewに読み込ませるためのhtmlファイルを作成し、そしてそれをアプリ内にassetファイルとしてコンパイルする必要があります。

上記を実行するため、このhtmlをビルドするための専用のpackage.jsonを書くことをおすすめします。

html-webpack-pluginおよび、html-webpack-inline-source-pluginを利用することで、ビルドしたJSやCSSをhtml内にインライン化することができます。

https://www.npmjs.com/package/html-webpack-inline-source-plugin

まとめ

Webブラウザではできるけど、React Nativeでは難しい!という挙動があったとき、上記のパターンで実装すると大抵のことはReact Nativeで実行できてしまえそうです。ただし、ブラウザのロードにやや時間が掛かったりする(indicatorがぐるぐる回る)ので、あくまで仕方ないときの策として考えておくと良さそうです。

参考

https://medium.com/@reginald.johnson/introducing-react-native-webview-quilljs-e6ca0d13c45c

This site uses Google Analytics.
source code