WEBVIEW

ReactNative(expo)でアプリとWeb(View)で値をやり取りする

Reactreactnativeexpo

この記事は最終更新日から1年以上が経過しています。

ReactNativeのWebView(Web)とアプリを連携する必要があったのでメモ。

忙しい人向け

  • アプリからWeb => WebViewのinjectedJavaScriptにJSを渡して値をインジェクトしてWeb側でよしなにする

  • Webからアプリ => window.ReactNativeWebView.postMessage("message")で送りonMessage()で受け取る

やりたいこと

例えば決済機能等の実装において、決済画面だけは決済サービス会社が提供するものを利用したいが、値はアプリ側で計算したものをデフォルト値として渡したいケースなど。

以下のような仕様。


最近の決済APIはカード情報非保持・非通過とするためWeb画面でカード番号を入れさせるものが多い。さらに言えばexpoでEjectしないで利用できるコンポーネントもない。

Web側

Web側は普通のHTMLだとまだ簡単なのですがReactを使うことが多いのでReactを使ってみます。

準備

場所作って必要なモジュールをインストール。

create-react-app web-app

cd web-app


npm install --save bootstrap reactstrap formik yup


実装

続いて実装。

index.js

bootstrap cssの読み込みとServiceWorkerを削除している(キャッシュが効いちゃうので)。

index.js

import React from 'react';

import ReactDOM from 'react-dom';

import 'bootstrap/dist/css/bootstrap.min.css';

import './index.css';

import App from './App';


ReactDOM.render(<App />, document.getElementById('root'));


App.js

App.jsに一旦すべて実装。

  • priceというidのinputを用意

  • cardというidのinputを用意

  • priceにinjectedJavaScriptから値を設定

ということを想定。私いつもFormikつかうので使ってます。

App.js

import React from 'react';

import { Form, FormGroup, Label, Input, Button, FormFeedback } from 'reactstrap';

import { Formik } from 'formik';

import * as Yup from 'yup';


class App extends React.Component {


handlePayment = async (values) => {


//1秒休む

await this.sleep(1000);


//終了したらアプリ側にメッセージを送る

window.ReactNativeWebView.postMessage(values.price + "円の決済が完了しました。");

}


//おやすみ補助関数

sleep = (msec) => {

return new Promise((resolve) => {

setTimeout(() => {

return resolve();

}, msec)

})

}


render() {

return (

<div className="container">

<h3 className="my-4 text-center">PaymentここはWeb</h3>

<div className="col-10 mx-auto">

<Formik

initialValues={{ price: 0, card: '1111-2222-3333-4444' }}

onSubmit={(values) => this.handlePayment(values)}

validationSchema={Yup.object().shape({

price: Yup.number().min(1).max(1000),

card: Yup.string().required(),

})}

>

{

({ handleSubmit, handleChange, handleBlur, values, errors, touched, setFieldValue }) => (

<Form>

<FormGroup>

<Label>金額</Label>

<Input

type="text"

name="price"

id="price" //idで強引に値をセット

value={values.price}

onChange={handleChange}

onBlur={handleBlur}

invalid={Boolean(touched.price && errors.price)}

disabled

/>

<FormFeedback>

{errors.price}

</FormFeedback>

</FormGroup>

<FormGroup>

<Label>カード番号</Label>

<Input

type="text"

name="card"

id="card"

value={values.card}

onChange={handleChange}

onBlur={handleBlur}

invalid={Boolean(touched.card && errors.card)}

/>

<FormFeedback>

{errors.card}

</FormFeedback>

</FormGroup>

<Button type="button" onClick={async () => {

const price = document.getElementById("price");

//Formik使ってるので値を明示的にセットしてやる(完了するうちにValidationが走らないようawait)

await setFieldValue("price", price.value);

handleSubmit();

}}>購入</Button>

</Form>

)

}

</Formik>

</div>

</div>

);

}

}


export default App;


とりあえず完成。window.ReactNativeWebView.postMessage()なんていう関数は標準のブラウザにはないのでchrome等でデバッグするとエラー出ますが無視します。

アプリ側

次にアプリ側。

場所の準備と必要コンポーネントをインストール。WebViewは普通にインストールするとexpoに怒られるのでexpo installコマンドで適切なバージョンのものをインストール。

expo init app-web-integration

cd app-web-integration


expo install react-navigation react-native-gesture-handler react-native-reanimated react-native-screens

expo install react-navigation-stack react-navigation-tabs react-navigation-drawer

expo install react-native-webview


まずApp.js。基本的にStackNavigatorを設定しているだけ。

Home.jsとPayment.jsを利用しています。

App.js

import React from 'react';

import { StyleSheet, Text, View } from 'react-native';

import { Card, Input, Button } from 'react-native-elements';


import { createAppContainer } from 'react-navigation';

import { createStackNavigator } from 'react-navigation-stack';


import Home from './Home';

import Payment from './Payment';


//stack navigator

const HomeStack = createStackNavigator(

{

Home: {

screen: Home,

},

Payment: {

screen: Payment,

}

}

);


const AppContainer = createAppContainer(HomeStack);


class App extends React.Component {

render() {

return (

<AppContainer />

);

}

}


export default App;


Home.js

ボタンを配置してPayment.jsに移動します。またその時金額をパラメーターとして渡しています。

Home.js

import React from 'react';

import { StyleSheet, Text, View } from 'react-native';

import { Card, Input, Button } from 'react-native-elements';


class Home extends React.Component {

render() {

return (

<View style={{ flex: 1, alignItems: 'center', marginTop: 50 }}>

<Text style={{ fontSize: 24 }}>Homeここはアプリ</Text>

<Button

title="100円コースを買う"

style={{ width: '80%', marginTop: 20 }}

onPress={() => this.props.navigation.navigate("Payment", { price: 100 })}

/>

<Button

title="200円コースを買う"

style={{ width: '80%', marginTop: 20 }}

onPress={() => this.props.navigation.navigate("Payment", { price: 200 })}

/>

</View>

);

}

}


export default Home;


Payment.js

このコンポーネントはWebViewになります。WebViewの、

  • injectedJavaScriptにWeb側に渡す値を設定

  • onMessageに戻りの処理を書く

Payment.js

import React from 'react';

import { StyleSheet, Text, View } from 'react-native';

import { Card, Input, Button } from 'react-native-elements';

import { WebView } from 'react-native-webview';


class Payment extends React.Component {


state = {

js: '',

}


componentDidMount = async () => {

//前のページからパラメータを受け取る(なければ0)

const price = await this.props.navigation.state.params.price ? this.props.navigation.state.params.price : 0;

//priceを設定するスクリプトを動的に生成

const js = `

const price = document.getElementById("price");

price.value = ${price}

`;

//stateを通じて渡す

this.setState({ js: js });

}


//Web側からのpostMessageに対応

onMessage = (event) => {

const message = event.nativeEvent.data;

this.props.navigation.navigate("Home");

alert(message);

}


render() {


//js内の変数が処理されないうちにWebViewがレンダリングするのを防ぐ

if (this.state.js === '') {

return <Text>Loading...</Text>

}


//WebViewをレンダリング

return (

<WebView

source={{ uri: 'http://localhost:3000/' }}

injectedJavaScript={this.state.js}

onMessage={this.onMessage}

/>

)

}

}


export default Payment;


かなり端折ってるけるけどとりあえず。