您的当前位置:首页正文

详解react-native WebView 返回处理(非回调方法可解决)

2020-11-27 来源:星星旅游

1.前言

项目中有些页面内容是变更比较频繁的,这些页面我们会考虑用 网页 来解决。

在RN项目中提供一个公用的Web页,如果是网页内容,就跳转到这个界面展示。

此时会有一个问题是,网页会有一级页面,二级页面,这就会设计到导航栏返回键的处理(以及在Android上返回键的处理)。

这个问题,在RN官网就可找到解决方式。就是用 onNavigationStateChange 这个回调方法记录当前的导航状态,从而判断是返回上一级页面还是退出这个网页,回到App的其他界面。

但是,当网页的实现是React时,就会有问题了,你会发现,当页面跳转的时候,onNavigationStateChange这个回调方法没有回调!!!怎么肥四!!

一开始尝试了把网页地址换成百度的,可以接收回调,一切都运行的很好,可是换成我们的链接就不行,所以就把锅甩给了后台,以为是React哪边写的不对。

因为上一个项目时间紧,没有时间好好去看一下源码,就想了一个不是很完善的解决方案,就是网页用js来回调App来告知现在的导航状态,这样的解决方式显示是不友好的。

现在稍微有点时间看了源码才知道真正原因。

2.原因

下面就分析一下这个问题的原因和我的解决方式。

1.首先,先找到源码的位置。

node_modules\react-native\ReactAndroid\src\main\java\com\facebook\react\views\webview

node_modules\react-native\Libraries\Components\WebView

目录结构是这样的:

 

2.实现的代码段 (JAVA端)

RN的实际运行代码都是原生代码,所以,像WebView组件的一些事件回调,其实都是原生代码中的回调触发的。如下

(ReactWebViewManager.java) rn版本0.47.1

protected static class ReactWebViewClient extends WebViewClient { //WebViewClient就是我们在写Android原生代码时,监听网页加载情况使用的工具。
 protected static final String REACT_CLASS = "RCTWebView"; //定义的原生组件名,在后面JS中会对应到。

 //...

 @Override
 public void onPageStarted(WebView webView, String url, Bitmap favicon) { //有很多回调方法,此处只举一例
 super.onPageStarted(webView, url, favicon);
 mLastLoadFailed = false;

 dispatchEvent(
 webView,
 new TopLoadingStartEvent( //自己定义的时间,dispatch后,事件会传给js
 webView.getId(),
 createWebViewEvent(webView, url)));
 }

 //...
 }

(ReactWebViewManager.java) rn版本0.43.3  ,RN不同版本会有代码调整,所以RN升级的时候,需要仔细的回归测试。

protected static class ReactWebViewClient extends WebViewClient { //WebViewClient就是我们在写Android原生代码时,监听网页加载情况使用的工具。
 protected static final String REACT_CLASS = "RCTWebView"; //定义的原生组件名,在后面JS中会对应到。

 //...

 @Override
 public void onPageStarted(WebView webView, String url, Bitmap favicon) { //有很多回调方法,此处只举一例
 super.onPageStarted(webView, url, favicon);
 mLastLoadFailed = false;

 dispatchEvent(
 webView,
 new TopLoadingStartEvent( //自己定义的时间,dispatch后,事件会传给js
 webView.getId(),
 createWebViewEvent(webView, url)));
 }

 @Override
 public void doUpdateVisitedHistory(WebView webView, String url, boolean isReload) { //坑在这,这里就是导航有变化的时候会回调在这个版本是有这个处理的,但是不知道在哪个版本删掉了 -.-
 super.doUpdateVisitedHistory(webView, url, isReload);

 dispatchEvent(
 webView,
 new TopLoadingStartEvent(
 webView.getId(),
 createWebViewEvent(webView, url)));
 }

 //...
 }

(TopLoadingStartEvent.java) 回调JS的Event

public class TopLoadingStartEvent extends Event<TopLoadingStartEvent> {
 public static final String EVENT_NAME = "topLoadingStart"; //对应方法是onLoadingStart, 因为对RN的结构不熟悉,在此处花了很长时间研究是怎么对应的,最后找到了定义对应的文件
 private WritableMap mEventData;

 public TopLoadingStartEvent(int viewId, WritableMap eventData) {
 super(viewId);
 mEventData = eventData;
 }

 @Override
 public String getEventName() {
 return EVENT_NAME;
 }

 @Override
 public boolean canCoalesce() {
 return false;
 }

 @Override
 public short getCoalescingKey() {
 // All events for a given view can be coalesced.
 return 0;
 }

 @Override
 public void dispatch(RCTEventEmitter rctEventEmitter) {
 rctEventEmitter.receiveEvent(getViewTag(), getEventName(), mEventData);
 }
}

(node_modules\react-native\ReactAndroid\src\main\java\com\facebook\react\uimanager\UIManagerModuleConstants.java)

这个文件里,定义了对应关系

/**
 * Constants exposed to JS from {@link UIManagerModule}.
 */
/* package */ class UIManagerModuleConstants {

 /* package */ static Map getDirectEventTypeConstants() {
 return MapBuilder.builder()
 .put("topContentSizeChange", MapBuilder.of("registrationName", "onContentSizeChange"))
 .put("topLayout", MapBuilder.of("registrationName", "onLayout"))
 .put("topLoadingError", MapBuilder.of("registrationName", "onLoadingError"))
 .put("topLoadingFinish", MapBuilder.of("registrationName", "onLoadingFinish"))
 .put("topLoadingStart", MapBuilder.of("registrationName", "onLoadingStart"))
 .put("topSelectionChange", MapBuilder.of("registrationName", "onSelectionChange"))
 .put("topMessage", MapBuilder.of("registrationName", "onMessage"))
 .build();
 }
}

3.实现的代码段 (JS端)

(node_modules\react-native\Libraries\Components\WebView\WebView.android.js)

在下面的代码中可以看到只有 onLoadingStart    和 onLoadingFinish 才会调用  updateNavigationState ,问题就出现在这了,由于我们的网页实现是React,只有一个页面啊!所以只会调用一次 onLoadingStart  和 onLoadingFinish 。再点击详情页并不会跳转到新页面,而是刷新原来的页面。所以也就没有 updateNavigationState 回调了。

class WebView extends React.Component {
 static propTypes = { //给外部定义的可设置的属性
 ...ViewPropTypes,
 renderError: PropTypes.func,
 renderLoading: PropTypes.func,
 onLoad: PropTypes.func,
 //...
 }

 render() { //绘制页面内容
 //...
 var webView =
 <RCTWebView
 ref={RCT_WEBVIEW_REF}
 key="webViewKey"
 style={webViewStyles}
 source={resolveAssetSource(source)}
 onLoadingStart={this.onLoadingStart}
 onLoadingFinish={this.onLoadingFinish}
 onLoadingError={this.onLoadingError}/>;

 return (
 <View style={styles.container}>
 {webView}
 {otherView}
 </View>
 );
 }

 onLoadingStart = (event) => {
 var onLoadStart = this.props.onLoadStart;
 onLoadStart && onLoadStart(event);
 this.updateNavigationState(event);
 };

 onLoadingFinish = (event) => {
 var {onLoad, onLoadEnd} = this.props;
 onLoad && onLoad(event);
 onLoadEnd && onLoadEnd(event);
 this.setState({
 viewState: WebViewState.IDLE,
 });
 this.updateNavigationState(event);
 };

 updateNavigationState = (event) => {
 if (this.props.onNavigationStateChange) {
 this.props.onNavigationStateChange(event.nativeEvent);
 }
 };
}

var RCTWebView = requireNativeComponent('RCTWebView', WebView, { //对应上面JAVA中的 ‘RCTWebView'
 nativeOnly: { messagingEnabled: PropTypes.bool, }, });


 module.exports = WebView; 

2.解决方法

既然原因找到了,就容易解决了

解决方式:自定义WebView,添加 doUpdateVisitedHistory 处理,在每次导航变化的时候,通知JS。

1. 拷贝下图中的文件到我们自己项目中的Android代码目录下

拷贝完后的Android目录:

ReactWebViewManager.java中需要修改几个地方

public class ReactWebViewManager extends SimpleViewManager<WebView> {
 protected static final String REACT_CLASS = "RCTWebView1"; //此处修改一下名字

 protected static class ReactWebViewClient extends WebViewClient {
 @Override
 public void doUpdateVisitedHistory(WebView webView, String url, boolean isReload) {
 super.doUpdateVisitedHistory(webView, url, isReload);

 dispatchEvent( //在导航变化的时候,dispatchEvent
 webView,
 new TopCanGoBackEvent(
 webView.getId(),
 createCanGoBackWebViewEvent(webView, url)));
 }
 }
}

TopCanGoBackEvent是我自己添加的一个Event,专门用来通知导航变化

TopCanGoBackEvent.java

public class TopCanGoBackEvent extends Event<TopCanGoBackEvent> {

 public static final String EVENT_NAME = "topChange"; 
 private WritableMap mEventData;

 public TopCanGoBackEvent(int viewId, WritableMap eventData) {
 super(viewId);
 mEventData = eventData;
 }

 @Override
 public String getEventName() {
 return EVENT_NAME;
 }

 @Override
 public boolean canCoalesce() {
 return false;
 }

 @Override
 public short getCoalescingKey() {
 // All events for a given view can be coalesced.
 return 0;
 }

 @Override
 public void dispatch(RCTEventEmitter rctEventEmitter) {
 rctEventEmitter.receiveEvent(getViewTag(), getEventName(), mEventData);
 }
}

新建 ReactWebViewPage.java

public class ReactWebViewPackage implements ReactPackage {

 @Override
 public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {

 return Collections.emptyList();
 }

 @Override
 public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
 return Arrays.<ViewManager>asList(
 new ReactWebViewManager()
 );
 }
}

然后在MainApplication中添加这个模块

public class MainApplication extends Application implements ReactApplication {
 @Override
 protected List<ReactPackage> getPackages() {
 return Arrays.<ReactPackage>asList(
 new MainReactPackage(),
 new ReactWebViewPackage() //WebView
 );
 }
}