React Nativeで任意のReact コンポーネントを使う方法
この記事は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側23export 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_PREFIX11 ) {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 };3031 // 略3233 render() {34 return (35 <WebView36 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}5152
React NativeからWebViewにメッセージを送信する方法
逆にReact Native側からメッセージを送信するためには、WebViewのReactElementに対してpostMessageを実行します。 何らか、メッセージの型を決めておくと便利です。
1// React Native側23export class RichTextInput extends React.PureComponent<Props, State> {4 webview?: WebView;5 createWebViewRef = (webview: WebView) => {6 this.webview = webview;7 };89 public addUserMention = (user: User) => {10 this.sendMessage("INSERT_USER_MENTION", { user });11 };1213 public addHashTag = (hashTag: HashTag) => {14 this.sendMessage("INSERT_USER_MENTION", { hashTag });15 };1617 public setHtmlContent = (html: string) => {18 this.sendMessage("SET_HTML_CONTENTS", {19 html20 });21 };2223 sendMessage = (type: string, payload?: any) => {24 // only send message when webview is loaded25 if (this.webview) {26 debugLog(`WebViewQuillEditor: sending message ${type}`);2728 // @ts-ignore29 this.webview.postMessage(30 JSON.stringify({31 prefix: MESSAGE_PREFIX,32 type,33 payload34 }),35 "*"36 );37 }38 };3940 render() {41 return (42 <WebView43 ref={this.createWebViewRef}44 source={RICH_TEXT_SOURCE}45 // 略46 />47 );48 }49}5051
これをWebView側で受信するためには、documentにmessage
イベントで通知されるのでこのイベントをlistenして処理します
12export 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 }1415 handleMessage: EventListener = (event) => {16 const data = (event as any).data as string;1718 let msgData;19 try {20 msgData = JSON.parse(data) as any;21 if (22 msgData.hasOwnProperty("prefix") &&23 msgData.prefix === MESSAGE_PREFIX24 ) {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