ネイティブアプリとWebView間での値の受け渡し方法についてです。
以前こちらの記事でWebViewベースでアプリを開発した際のメリットデメリットについても書きました。
よろしければこちらを参照してください。
アプリの大半をWebViewで作ってみて良かったとこ悪かったとこ
アプリの構成
- expo
- Next.js
- TypeScript
expoでネイティブアプリの部分を開発しました。アプリは会員登録してログインしてアプリのさまざまな機能を利用できます。ログイン画面はネイティブアプリになりますが、ログインした後や会員登録は全てWebViewでNext.jsで開発しています。
有名な使用例ではamazonの商品詳細ページがwebviewで作られておりアプリで見てもブラウザで見ても変わらず同じ見た目になっています。
WebViewからネイティブアプリにデータを渡す
WebViewからはwindow.ReactNativeWebView.postMessageにJSON.stringifyした文字列を渡します。
typeは何のイベントかを判定するために渡しています。例では他にidを渡していますがtokenなど他のものもいろいろ渡せます。
// WebView(Next.js)
export const customPostMessage = (message: string) => {
if (typeof window !== 'undefined') {
window.ReactNativeWebView?.postMessage(message);
}
};
customPostMessage(
JSON.stringify({
type: 'HANDLE_ADD_FAVORITE',
id,
}),
);
expo側ではWebViewタグのonMessageでWebViewMessageEventを受け取りJSON.parseします。
パースした後typeでどの関数を呼ぶか判定します。messageHandlersにはWebViewから呼ばれるイベントの関数をセットしておくようにしました。
まとめるとWebViewでwindow.ReactNativeWebView.postMessageによりデータをネイティブ側に送る。
ネイティブ側ではそのデータ内にあるtypeを見て対応する関数を実行する。このような流れになっています。
const messageHandlers: Record<string, (data: any) => void | Promise<void>> = {
PASSWORD_RESET: async () => handleForgotPwFlg(),
LAUNCH_QR_SCANNER: () => handleScanData(),
COPY_TO_CLIPBOARD: (data) => handleCopyToClipboard(data.data),
NAVIGATE_BACK: (data) => router.replace(data.destination),
DELETE_REGISTER_AND_NAVIGATE_BACK: handleDeleteRegisterAndNavigateBack,
NAVIGATE_TO_PASSCODE_SETUP: showPasscodeConfirmation,
LAUNCH_SCAN_CARD_NUMBER: () => handleScanCardNumber(),
OPEN_DIALOG: (data) => handleOpenDialog(data.dialogType),
GET_USER_LANGUAGE: handleGetUserLanguageMessage,
SET_USER_LANGUAGE: handleSetUserLanguageMessage,
LAUNCH_SCAN_CARD_USER_INFO: () => handleScanUserInfo(),
CAPTURE_SCREENSHOT: handleSaveCompleteScreen,
SHARE_SCREENSHOT: handleShareScreen,
PERSIST_REGISTER_STATE: handlePersistRegisterState,
GET_REGISTER_STATE: handleResetRegisterState,
HANDLE_SUCCESS_REGISTER: handleSuccessRegister,
HANDLE_FAVORITE_RECIPIENT: handleFavoriteRecipient,
GET_FAVORITE_RECIPIENTS: handleGetFavoriteRecipients,
DELETE_TOKENS: async () => await deleteToken(),
};
const handleMessage = async (event: WebViewMessageEvent) => {
try {
const data = JSON.parse(event.nativeEvent.data) as WebViewMessage;
const handler = messageHandlers[data.type];
if (handler) {
await handler(data);
} else {
console.warn(`未処理のメッセージタイプ: ${data.type}`);
}
} catch (error) {
console.error('メッセージの解析に失敗しました:', error);
}
};
return (
<WebView
source={{
uri: webViewFullUrl,
}}
style={styles.webview}
javaScriptEnabled={true}
domStorageEnabled={true}
startInLoadingState={true}
scalesPageToFit={true}
onMessage={handleMessage}
injectedJavaScript={injectedJavaScript}
ref={webViewRef}
nestedScrollEnabled={false}
overScrollMode="never"
onError={handleWebViewError}
onHttpError={handleWebViewHttpError}
onRenderProcessGone={handleReload}
onContentProcessDidTerminate={handleReload}
/>
);
ネイティブからWebViewにデータを渡す
先ほどの例のWebViewタグにrefが設定されています。このrefにあるinjectJavaScriptに対してwindow.postMessageとJSON.stringifyした文字列を渡せばデータを送ることができます。
以下の例はアプリのストレージに保存してあるお気に入りユーザーをネイティブ側に伝えています。
// expo
webViewRef.current?.injectJavaScript(`
window.postMessage(JSON.stringify({
type: 'GET_FAVORITES',
favoriteRecipients:${JSON.stringify(Array.from(favoriteRecipients.entries()))}
}), '*');
true;
`);
WebView側はuseEffectでmessageイベントを監視します。イベントを受け取ったらデータをパースしtypeを見て実行したいイベントかを確認します。
その後useStateにセットしています。
// webview
useEffect(() => {
if (isWebView()) {
window.ReactNativeWebView?.postMessage(
JSON.stringify({
type: 'GET_FAVORITES',
}),
);
// メッセージイベントリスナーを追加
const handleMessage = (event: any) => {
try {
const data = JSON.parse(event.data);
if (
data.type === 'GET_FAVORITES' &&
data.favoriteRecipients !== undefined
) {
setFavoriteRecipients(new Map(data.favoriteRecipients));
}
} catch (error) {
console.error('Error parsing message:', error);
}
};
window.addEventListener('message', handleMessage);
// クリーンアップ関数
return () => {
window.removeEventListener('message', handleMessage);
};
}
}, []);
最後に
以上でネイティブアプリとWebViewで値を受け渡しできます。
イベントが多いとuseEffectが増えてしまうことやpostMessageしたイベントが何を行なっているか見えにくくなること、postMessageで渡す値の型チェックができないことなどが理由であまり多用しない方がいいと思っています。