业务场景
在移动端混合开发过程中,React Native使用webview内嵌H5,经常有H5向React Native发送消息和React Native向H5发送消息的场景,但是直接发送了消息并没有提供一个成功或者失败的回调。有很多时候我们不仅需要发送消息,还需要发送消息等待处理完成发回结果继续执行后续的业务逻辑。这个时候官方提供的通信方案就不能完全满足业务需求了,因此我封装了一个工具包可以解决这个问题。
以下主要讨论的业务场景是React Native使用webview内嵌H5的业务场景
封装的工具包 :react-native-webview-callback
工具包封装的思路:
常规React Native与H5的通信方式
我们从不同的端发送消息,梳理一下常规的通信方式
H5端向React Native端发送消息
H5端向 React Native端发送消息常见的有以下三种:URL Scheme 通信、
URL Scheme 通信
React Native 可以通过定义自定义的 URL Scheme,允许在 H5 页面中通过跳转特定的 URL 来与 React Native 进行通信。H5 页面可以使用 JavaScript 的 window.location.href 或者<a>
标签等方式触发跳转,从而将数据传递给 React Native。优点:比较直接;缺点:传输数据有限,传递不了太多数据,有安全风险;
Native Bridge
可以通过Android或者iOS Bridge的方式,都与客户端通信,不过这样通信链路太长了。
通过wevview的postMessage
在内嵌的网页中可以使用window.ReactNativeWebView.postMessage向React Native发送消息,在React Native端的webview监听onmessage即可监听到消息。
React Native端向H5端发送消息
通过webview的injectJavaScript方法
React Native端可以直接运行webview的injectJavaScript方法在H5上,实现消息传递,改变全局变量等操作。
通过webview的postMessage方法
在React Native端可以使用webview的Ref比如webViewRef.current.postMessage来向H5发送消息,H5端监听onmessage可以接收到消息。
使用封装好的工具实现相互通信
以上方案都是单项消息通信,消息发出去了,但是不知道有没有收到,也写比较多的逻辑才能实现成功和失败的回调,每个地方都写一遍维护起来比较混乱。
我封装了一个工具可以直接使用。 使用起来是这样的。
H5调用React Native端自定义的API方法,并获取成功失败的回调
tsx
import { useEffect, useState } from 'react'
import {
mergeReactNativeApi,
useReactNativeAddListener,
reactNativeCallH5,
} from 'react-native-webview-callback';
import myEvent from './customH5Api';
import './App.css';
function App() {
useH5AddListener(mergeH5Api(myEvent)) // just need init once ,Entry file
useEffect(() => {
window.localStorage.setItem('token', 'mytokenStrXXXXX');// mock data
},[])
const [result, setResult] = useState('--');
const testFn = () => {
h5CallreactNative({
methodName: "getAppInfo", // 这里是React Native端自定义的API
data: "", // 这里传递一些需要的数据
})
.then((data) => { // React Native端返回成功的回调
if (typeof data === 'object') {
setResult(JSON.stringify(data))
} else if (typeof data === 'string') {
setResult(data)
}
console.log("Successful data:", data);
})
.catch((error) => {// React Native端返回失败的回调
alert("Failed from React Native callback");
console.error("Failed data:", error);
});
}
return (
<>
<h1>H5</h1>
<div className="card">
<button onClick={testFn}>
click to getAPPInfo
</button>
<p>
{result}
</p>
</div>
</>
)
}
export default App
React Native调用H5端自定义的API方法,并获取成功失败的回调
tsx
// App.tsx
import React, {useRef} from 'react';
import {
SafeAreaView,
ScrollView,
Alert,
StyleSheet,
Text,
useColorScheme,
View,
} from 'react-native';
import {WebView} from 'react-native-webview';
import myEvent from './customNativeApi';
import {
mergeReactNativeApi,
useReactNativeAddListener,
reactNativeCallH5
} from 'react-native-webview-callback';
const {alert} = Alert;
function App(): JSX.Element {
const webViewRef: any = useRef(null);
// 收到消息
const onMessage = (event: any) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
useReactNativeAddListener({
bridgeReactNativeApi: mergeReactNativeApi(myEvent), // Merge into custom methods on listening objects
webViewRef,
event,
});
};
const handleLoadEnd = () => {
reactNativeCallH5({
dataParms: {
methodName: 'getWebToken', // 这是调用H5自定义的API
data: '', // 根据具体场景传递业务数据
},
webViewRef: webViewRef,
})
.then((data: any) => { // 成功回调
alert(data || 'Successful data:');
console.log('Successful data:', data);
})
.catch(error => { // 失败回调
console.log('Failed data:', error);
alert('Failed from H5 callback');
});
};
return (
<WebView
ref={webViewRef}
source={{
uri: 'http://192.168.XXX.XXX:5173', // 换成你自己的IP地址或者域名地址
}}
originWhitelist={['*']}
allowFileAccess={true}
onMessage={onMessage}
onLoadEnd={handleLoadEnd}
geolocationEnabled={true}
allowUniversalAccessFromFileURLs={true}
useWebKit={true}
/>
);
}
export default App;
实现原理
上文中说过React Native可以通过postMessage向H5发送消息,同时H5也可以通过postMessage向React Native发送消息。这样就有通信的基础,只要约定好通信的数据及参数格式,当消息发过来的时候封装一个promise,返回成功和失败的回调方法即可。
React Native端核心源码
- channelName:通信的渠道名称,避免受到网页其他postMessage的干扰;
- methodType:代表当前处于调用状态还是回调状态
- methodName:自定义API方法名
- data: 传递的数据
- sourceMethodName :原始方法名(因为回调的之后方法名做了覆盖)
- successKey:成功回调需要调用的方法名,通过event emit触发,该名称由methodName和时间戳拼接,这样同一个方法不同时间被调用可以确保有序的响应。
- errorKey:失败回调需要调用的方法名,通过event emit触发,该名称由methodName和时间戳拼接,这样同一个方法不同时间被调用可以确保有序的响应。
ts
export interface useReactNativeAddListenerArgs {
bridgeReactNativeApi: any;
webViewRef: any;
event: any;
}
export const useReactNativeAddListener = (
messageProps: useReactNativeAddListenerArgs,
) => {
const {bridgeReactNativeApi, webViewRef, event} = messageProps;
const dataSource = event?.nativeEvent?.data;
try {
if (dataSource && dataSource !== 'undefined') {
const messageData: MethodCallArgs = dataSource && JSON.parse(dataSource) || {};
const {
channelName,
methodType,
methodName,
data,
sourceMethodName,
successKey,
errorKey,
} = messageData;
if (channelName === defaultChannelName) {
if (methodType === CallType.callBack) {
// H5 回调到react native
eventEmiter.emit(methodName, data);
eventEmiter.off(successKey, () => {});
eventEmiter.off(errorKey, () => {});
} else if (methodType === CallType.call) {
// H5调用react native的方法
if (
bridgeReactNativeApi.hasOwnProperty(sourceMethodName) &&
typeof bridgeReactNativeApi[sourceMethodName] === 'function'
) {
bridgeReactNativeApi[sourceMethodName](data)
.then((res: any) => {
const successObj = {
...messageData,
data: res,
methodType: CallType.callBack,
methodName: successKey,
};
webViewRef?.current?.postMessage(JSON.stringify(successObj), '*');
})
.catch(err => {
const errObj = {
...messageData,
data: err,
methodType: CallType.callBack,
methodName: errorKey,
};
webViewRef?.current?.postMessage(JSON.stringify(errObj), '*');
});
}
}
}
}
} catch (error) {
console.error(error);
}
};
H5端 核心源码
与React native类似,这里就不做参数的具体解释了。
ts
// 统一封装react native调用H5及回调返回,通过promise的方式
export const reactNativeCallH5 = (
reactNativeCallH5Props: reactNativeCallH5Args,
) => {
const {dataParms, webViewRef} = reactNativeCallH5Props;
return new Promise((resolve, reject) => {
const {
channelName = defaultChannelName,
methodType = CallType.call,
methodName = 'methodName',
data = '',
} = dataParms;
const timeStr = `${new Date().getTime()}`;
const successKey = `${methodName}_${timeStr}_success`;
const errorKey = `${methodName}_${timeStr}_error`;
const obj: MethodCallArgs = {
channelName,
methodType,
methodName,
sourceMethodName: methodName,
data,
timeStr,
successKey,
errorKey,
};
// 挂载成功的回调
eventEmiter.on(successKey, res => {
resolve(res);
});
// 挂载失败的回调
eventEmiter.on(errorKey, err => {
reject(err);
});
webViewRef?.current?.postMessage(JSON.stringify(obj), '*');
});
};
其他
本npm包一共导出了以下这些方法,大家有兴趣可以看一下源码和DEMO
- mergeH5Api(合并基础API方法和项目中自定义的H5 API提供给React Native调用)
- useH5AddListener (H5端初始化,只需要在入口文件执行一次)
- h5CallreactNative (业务中H5端用这个方法调用React Native的自定义API方法)
- mergeReactNativeApi(合并基础API方法和项目中自定义的H5 API提供给H5调用)
- useReactNativeAddListener(React Native端初始化,只需要webview 的onmessage中执行一次)
- reactNativeCallH5 (业务中React Native端用这个方法调用H5的自定义API方法)
DEMO地址
react-native-webview-callback npm包 React Native和H5项目接入的完整DEMO可以看react-native-webview-callback-demo
欢迎使用react-native-webview-callback ,如果有问题或者建议欢迎提issues或者PR,可以的话帮忙点个start ^_^